Skip to content

Copy and voice

A style prompt tells a model how you hope someone writes. A corpus shows how they actually write. june/copy builds a voice from that evidence: for each request it retrieves the person's most relevant writing as examples, drafts several candidates, scores them against those examples, and keeps the best. When the person edits a draft, the edit becomes evidence for the next version of the voice.

The request and the examples have different jobs. The request owns the facts, the intent and the form: what to say, to whom, and how long. The examples only show how this person says things. They aren't meant to be a source of facts, and when they disagree with the request's form, the request should win; Writing with a voice says how to make sure it does.

A first voice

A corpus whose samples already say what they are (type, audience and so on) can write straight away, with no preparation and no training:

import { openai } from "june/sdk/openai";
import { copyCorpus, voice } from "june/copy";

const fonzie = voice({
  name: "fonzie",
  corpus: copyCorpus("./fonzie.jsonl"),
  model: openai("gpt-5-mini"),
});

const result = await fonzie.write({
  type: "homepage-hero",
  audience: "startup founders",
  brief: "Introduce the recruiting agent. Two lines, under 25 words.",
});

result.text; // the best of three drafts
result.exemplars; // the samples the model saw, and why each was picked
result.score; // how much it sounds like them, overall and by dimension

fonzie.jsonl has one sample per line, such as {"text":"…","metadata":{"type":"homepage-hero","audience":"startup founders"}}; Corpora and authorship has the format.

That's a working voice: one call, three drafts, the best one back, and the record of why it was chosen. It relies on a corpus whose samples are labelled. Most archives aren't, and some of what's in them isn't the person's writing. prepareVoice() cleans the corpus and has a teacher model label it, and its result is what you pass as corpus from then on.

Exemplars, training and evaluation are different things

It helps to keep four ideas apart, because they're easy to blur:

  • Preparation changes nothing about the writing. It decides which samples are in, and adds synthetic prompts and labels next to them. Its output is a dataset.
  • Exemplars are what a voice shows the writer at request time: a handful of the person's real samples, chosen by retrieval. This is how a base model writes in their voice, and it's the part of a voice you'll tune most.
  • Evaluation tells you whether the voice is any good, against writing it didn't see or a model without the voice. The scores voice.write() uses to pick a draft are not an evaluation.
  • Fine-tuning is optional, and it isn't the default way to improve a voice. It teaches a model the invariant parts of a voice from the prepared dataset, and it's worth it only when an evaluation shows a gap that better examples don't close. Exemplars still supply what fits the situation, since a support email and a homepage hero shouldn't sound identical.

june/copy is built on the generic datasets, evals and training primitives; nothing in june/train knows about copy.

How it fits together

real writing ──copyCorpus()──▶ corpus ──prepareVoice()──▶ prepared voice
                                                                │
                      ┌─────────────────────────────────────────┘
                      ▼
request ──▶ voice.write():  retrieve exemplars ─▶ draft candidates ─▶ score ─▶ best draft
                                                                                 │
                                    a person edits it ◀──────────────────────────┘
                                          │
                     voice.edit() ──▶ preference example, or a new sample in the corpus

Each stage is a separate step you can run, inspect and change on its own, and none of them changes a voice behind your back:

StageWhat it doesAPIModel calls
Corpusholds the person's writing, byte for byte, with metadata and a fingerprintcopyCorpus()none
Preparationdrops samples marked as not theirs and near-duplicates, reconstructs briefs, fills in labelsprepareVoice()one teacher call per sample
Retrievalpicks the samples most like a request in form, to show the writermetadataRetriever(), voice.retrieve()none
Writingdrafts candidates with the exemplars, scores them, keeps the best, maybe revisesvoice(), voice.write(), <Write>one per candidate, and a revision
Scoringmeasures how much a text sounds like the exemplarsstylometry(), judge()none, or one per draft for judge()
Evaluationchecks the voice against held-out writing or a baseline, before you rely on itevalSuite(), evaluate() from june/trainthe writer and graders
Savingrecords a voice as JSON and loads it again, refusing a different corpusvoice.manifest(), loadVoice()none
Trainingoptional: fine-tunes a model on the prepared corpusvoice.train()a trainer
Learningturns a person's edit of a draft into training or evaluation materialvoice.edit()none

What June does, and what your app decides

june/copy holds the primitives. The policy around them, such as where writing comes from, when a voice changes, and who decides, belongs to the application. The Copy Agent is a complete application built only on June's public packages, and a reference for how the two divide:

june/copy and june/train provideThe Copy Agent decides
a corpus from an array or JSONLreading a folder of Markdown and text files, with front matter as metadata
exclusion, deduplication, reverse instructions, inferred labels, splitswhich writing to collect, and keeping prepared samples on disk between processes
retrieval by request labelsturning a conversation into a request, with a type picked from the voice's own types
candidates, scoring, one revision, <Write> in a workflowwhat each reply records, and the chat around it
voice.edit(), and datasets and eval cases from chat feedbackwhich edits are learned from and which are held out for evaluation
evaluate() and compareEvaluations()its graders, its benchmark, and what counts as better
voice manifests, loadVoice(), optional voice.train()candidates, an active agent, and promotion by a person, never automatically

Candidates, active agents and promotion aren't June concepts. A voice in June is a value: it never changes itself or replaces another. june/copy doesn't learn a brand on its own; a person decides what the next voice is.

A worked example: June.dev's own copy

June.dev's site copy is drafted in its author's voice with this package and the Copy Agent, and each step is one you'd take for any person or brand:

  1. A reviewed list of sources. Every piece of writing the voice may learn from is listed with where it lives, a pinned revision, its type, who wrote it, and whether it's in or out and why. Nothing is scraped, and when authorship is unclear, it's out. See building a corpus from reviewed sources.
  2. A corpus of prose. Each included source becomes one sample, with code, quotes and markup removed, and its origin kept in metadata. The corpus has about 130 samples, labelled with a small vocabulary of types such as essay, tutorial, readme and docs.
  3. Preparation, then a review of it. prepareVoice() kept every sample (the source list had already excluded what didn't belong) and found no duplicates. Reading a sample of the reconstructed briefs and labels, and counting samples by type, is how you know what the voice will retrieve.
  4. An evaluation before use. A fixed set of real site-copy requests ran through the voice and through the same writer model with no voice, graded on sounding like the author, following the brief, and keeping to the facts. The voice sounded clearly more like the author; the plain writer followed briefs better, and nearly every difference was about form and length.
  5. Drafts a person reviews. Each request states its facts, its length and its form. The voice drafts, a person approves or edits every piece, and the edits go back as evidence.

Two of the most useful lessons came from this run, and both are explained as package behavior in these guides: retrieval matches labels by shared words, so requests should use the corpus's own type vocabulary; and the writer follows its exemplars' form, so say the length and form in the request. The site copy's README and the evaluation have the details.

Guides

  1. Corpora and authorship: input formats, metadata and provenance, authorship hygiene, voice drift
  2. Voice preparation: exclusion, deduplication, reverse instructions, inferred labels, splits
  3. Writing with a voice: voice(), requests, form and length, candidates and revision, <Write>
  4. Retrieval: why structure matters more than topic, weights, labels
  5. Scoring and evaluation: stylometry(), judge(), custom evaluators, evaluating a voice
  6. Saving and training: manifests, loadVoice(), optional fine-tuning
  7. Learning from edits: preference examples, new samples, held-out edits

Before you collect anyone's writing, read corpora and consent.