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
adduses 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. |
| 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.