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:
| Stage | What it does | API | Model calls |
|---|---|---|---|
| Corpus | holds the person's writing, byte for byte, with metadata and a fingerprint | copyCorpus() | none |
| Preparation | drops samples marked as not theirs and near-duplicates, reconstructs briefs, fills in labels | prepareVoice() | one teacher call per sample |
| Retrieval | picks the samples most like a request in form, to show the writer | metadataRetriever(), voice.retrieve() | none |
| Writing | drafts candidates with the exemplars, scores them, keeps the best, maybe revises | voice(), voice.write(), <Write> | one per candidate, and a revision |
| Scoring | measures how much a text sounds like the exemplars | stylometry(), judge() | none, or one per draft for judge() |
| Evaluation | checks the voice against held-out writing or a baseline, before you rely on it | evalSuite(), evaluate() from june/train | the writer and graders |
| Saving | records a voice as JSON and loads it again, refusing a different corpus | voice.manifest(), loadVoice() | none |
| Training | optional: fine-tunes a model on the prepared corpus | voice.train() | a trainer |
| Learning | turns a person's edit of a draft into training or evaluation material | voice.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 provide | The Copy Agent decides |
|---|---|
| a corpus from an array or JSONL | reading a folder of Markdown and text files, with front matter as metadata |
| exclusion, deduplication, reverse instructions, inferred labels, splits | which writing to collect, and keeping prepared samples on disk between processes |
| retrieval by request labels | turning a conversation into a request, with a type picked from the voice's own types |
candidates, scoring, one revision, <Write> in a workflow | what each reply records, and the chat around it |
voice.edit(), and datasets and eval cases from chat feedback | which 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:
- 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.
- 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,readmeanddocs. - 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. - 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.
- 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
- Corpora and authorship: input formats, metadata and provenance, authorship hygiene, voice drift
- Voice preparation: exclusion, deduplication, reverse instructions, inferred labels, splits
- Writing with a voice:
voice(), requests, form and length, candidates and revision,<Write> - Retrieval: why structure matters more than topic, weights, labels
- Scoring and evaluation:
stylometry(),judge(), custom evaluators, evaluating a voice - Saving and training: manifests,
loadVoice(), optional fine-tuning - Learning from edits: preference examples, new samples, held-out edits
Before you collect anyone's writing, read corpora and consent.