On this page

Walkthrough: importing your own writing from transcripts

A hundred passages is not a corpus. The writing you have actually done is mostly not in files — it is in the transcripts of your own coding sessions, and this walkthrough moves it into the writing-style store with commands that already exist. No new importer, no second parser inside Skryba.

The counts below are from one machine, printed by the runs shown. Yours will differ; the shape will not.

Why writing-style add cannot read a transcript

add is a prose reader. It reads a file named on the command line whatever its extension, and in a named folder it reads only the files the selected model route names as the person's prose — but it reads every file as UTF-8 text. A session transcript is JSONL: one JSON object per line, your turns interleaved with the model's. Pointed at one, add would ingest JSON syntax as prose.

Teaching Skryba to parse another product's session format is the wrong fix. Transcript Lake already owns transcripts: it maintains a masked, queryable archive of every coding-agent session on the machine, and its query command runs arbitrary DuckDB SQL over canonical views — including COPY … TO, which writes a file. So the transcript side needs no new code either. What was missing was the query.

event_type = 'user' is a role, not an author

This is the whole difficulty, and skipping it produces a corpus that poisons an adapter. The Lake's events view held 131,847 rows of event_type = 'user' on this machine, and most of them nobody typed. Every runtime injects machine traffic into the user role:

Injection Measured
OMP agent progress blocks (### Session update) 59,882 of 82,044 non-sidechain OMP user turns
Agent briefs the harness writes to subagents (# Target, # Acceptance) 3,199 of 9,489 in-band candidates
Assignment prompts (Complete the assignment…) 3,218
Secret-scrub placeholders marking pasted payloads ([masked:…]) 3,680
Runtime wake and IRC relay notices 885
Claude subagent conversations (sidechain = true) 4,594

sidechain marks a subagent conversation, not a person, so it is excluded rather than trusted. What remains after every measured class is removed is text a person typed.

The filter

The query ships with Skryba as examples/transcript-own-prose.sql, commented with what each clause removes and why. Two of its rules are judgements rather than markers, and are worth stating plainly:

  • Formatted prose is machine prose. Bodies containing bold runs (**) or inline markdown links were agent status reports in every sample inspected. A person typing into a terminal does not reach for them.
  • Word bounds match the store. Only bodies of 60–400 whitespace words survive, the same band add uses by default, and a body that is less than three-quarters letters and spaces is a log, a form or a diff rather than prose.

The run, end to end

# 1. The Lake writes the prose to a file. COPY … TO is DuckDB's, and
#    `body || chr(10)` leaves a blank line between rows, which is the
#    paragraph break `add` splits passages on.
transcript-lake query "COPY (
  SELECT body || chr(10) FROM ($(cat examples/transcript-own-prose.sql))
) TO '$HOME/own-prose.md' (FORMAT CSV, HEADER false, QUOTE '', DELIMITER E'\x01')"

# 2. Skryba ingests it like any other prose file.
skryba writing-style add "$HOME/own-prose.md"

Step 1 reduced 131,847 user events to 898 rows and wrote 964,133 bytes — 120,907 words. Step 2 printed:

{
  "action": "add",
  "passages": 594,
  "passage_words": 57851,
  "added": 594,
  "skipped": 0
}

594 passages, 60 to 376 words each, 97.4 on average. The gap between 898 rows and 594 passages is add doing its own job on the way in: dropping fenced code, tables, images, bare links and HTML, then re-applying the word bounds to what is left.

For scale, the same store ingested from this operator's four hand-written project documents — two AGENTS.md-class files and two READMEs — holds 187 passages and 20,522 words. The transcripts are roughly three times that, and they are prose he wrote without meaning to keep.

What the filter cannot remove

It admits its residue rather than claiming to be clean. Two classes survive because they are indistinguishable from prose by any rule available here:

  • Text he pasted under his own words. A sampled passage opens ok now lets do this: — his — and continues into a pasted tax-form fragment. The turn is genuinely his; most of its words are not his writing.
  • Pages pasted whole. A browser TLS interstitial survived as one passage: ordinary English sentences, ordinary length, no marker distinguishing them from something typed.

Both are a minority of 594 and both are safe to leave: an adapter trained on a corpus with a few per cent of pasted English learns slightly less, not something false. Neither is worth a rule that would also drop real writing.

What the importer still does not reach

Named so the boundary is visible rather than discovered later. On this machine:

Source Measured What stands in the way
PDF and Office documents 2,199 .pdf, 426 .docx, 9 .doc under ~/Documents, ~/Desktop, ~/Downloads Not text. Whether named or chosen in a folder, add refuses one when it reads it as UTF-8. Needs an extraction step that exists in no product here.
Published documentation 36 pages, 17,005 words, wrapped in docs.json Authored prose inside JSON string values. The file read verbatim would ingest JSON syntax. The same COPY … TO unwrap would fix it; nothing does it today.
Git commit messages 10,172 words across 82 commits in one repository alone Stored in git objects, not files of prose: a folder read asks the model not to open version-control internals, and the objects are not text. Needs a git log capture step.
Apple Notes NoteStore.sqlite, 103,563,264 bytes A SQLite container, not text.
Mail Absent No ~/Library/Mail, no .emlx files, no Mail container. Nothing to import.

Then train on it

skryba writing-style train \
  --model <checkpoint> \
  --output my-voice.safetensors \
  --rank 8 --epochs 1 --precision f16

train exports and drives ster tune sft in one invocation; see the training boundary. Corpus size is the constraint that matters, and it is why this walkthrough exists: a few hundred passages is where a personal adapter starts being worth training at all.

Maintained as part of the website-owned Skryba documentation corpus.