On this page
Walkthrough: capturing your writing and training an adapter
Every number and every line of output below was captured from real runs on an
Apple M2 Max. Nothing here is composed. Sections 1 to 3 ran against built
skryba and ster binaries over a small hand-picked set of passages;
sections 4 and 5 are a later run over a transcript corpus.
These runs predate the move of Skryba's stores into its
fleet database. They named a SQLite file with
--writing-style-store, and each report's store field is that file's path.
That flag no longer exists: today the same commands write the passages and
preferences tables, a separate store is a separate schema chosen with
SKRYBA_DATABASE_SCHEMA, and store reads stado://database/skryba/<SCHEMA>.
The counts, the export shapes and Ster's output are unchanged.
1. Ingest prose you wrote
Three Markdown roots from this workspace — one docs directory and two READMEs:
skryba --writing-style-store ~/.stado/work/skryba-ws/store.sqlite3 \
writing-style add ~/Documents/CodingProjects/Wisent/ster/docs \
~/Documents/CodingProjects/Wisent/skryba/README.md \
~/Documents/CodingProjects/Wisent/ster/README.md
{
"action": "add",
"store": "~/skryba-ws/store.sqlite3",
"passages": 86,
"passage_words": 9189,
"preferences": 0,
"added": 86,
"skipped": 0
}
Two more READMEs, to pass 100 held passages:
skryba --writing-style-store … writing-style add \
~/Documents/CodingProjects/Wisent/skryba-desktop/README.md \
~/Documents/CodingProjects/Wisent/brama/README.md
{
"action": "add",
"store": "~/skryba-ws/store.sqlite3",
"passages": 116,
"passage_words": 13705,
"preferences": 0,
"added": 30,
"skipped": 0
}
added is 30 against 116 held: passages is the store's total, added is
what this invocation inserted. Run either command again and added is 0
with the same count in skipped — the passage text is the primary key.
2. Export the first 100 passages for ster tune sft
skryba --writing-style-store … writing-style export \
--format sft --limit 100 --name my-prose --output sft-100.json
{
"action": "export",
"store": "~/skryba-ws/store.sqlite3",
"passages": 116,
"passage_words": 13705,
"preferences": 0,
"added": 0,
"skipped": 33,
"output": "~/skryba-ws/sft-100.json",
"written": 67
}
100 ingested passages produced 67 SFT examples, with 33 skipped. A
passage is skipped when it cannot be cut at a sentence boundary into a lead
of at least --lead-words words and a completion of at least that many
again; at the default 30 that is a passage under roughly 60 words of
sentences, which the 60-word ingest floor admits. Lower --lead-words and
more passages qualify.
The first example in the written file, elided in the middle:
{
"name": "my-prose",
"examples": [
{
"prompt": "Continue this passage in the same voice:\n\n`--chat-template` belongs to the local route for the same reason `--model` does. `auto`, the default, asks the local generator through the model's own chat template when the checkpoint publishes one, and `off` asks it as raw text.",
"completion": "Synthesis is the first step of the funnel and everything downstream inherits what it writes, …"
}
]
}
The prompt is the person's own opening; the completion is the rest of their own passage. Why that prompt and not a synthesized instruction is the training boundary.
3. Record one preference, and export the pair set
keep reads the pair as JSON on stdin — the same shape the desktop app
sends:
printf '{"machine":"The utilization of the system demonstrates significant improvements in operational efficiency.","kept":"The thing got faster. That is the whole finding.","strategy":"plainspoken"}' \
| skryba --writing-style-store … writing-style keep
{
"action": "keep",
"store": "~/skryba-ws/store.sqlite3",
"passages": 116,
"passage_words": 13705,
"preferences": 1,
"added": 1,
"skipped": 0
}
skryba --writing-style-store … writing-style export \
--format dpo --name my-prose --output dpo.json
{
"trait_name": "my-prose",
"pairs": [
{
"positive": "The thing got faster. That is the whole finding.",
"negative": "The utilization of the system demonstrates significant improvements in operational efficiency."
}
]
}
positive is what the person kept. Ster scores a pair whole and carries no
prompt field, so both sides are complete texts.
4. Hand the file to Ster
Skryba drives Ster from here. writing-style train writes the export and runs
ster tune over it in one invocation, printing Ster's own progress as it goes.
The run below is not the 116-passage store above. It is the transcript corpus of importing your own writing from transcripts — 594 passages and 57,851 words held in the store — trained at Ster's defaults for layers and sequence length. Every number and every line in this section and the next was copied from that run's own log and output files; the binary revisions it ran under were not recorded.
skryba writing-style train --model TinyLlama/TinyLlama-1.1B-Chat-v1.0 \
--output my-voice.safetensors \
--rank 8 --epochs 1 --learning-rate 0.0001 --precision f16
train reports the exact ster command it ran, so the run can be repeated by
hand:
ster tune sft --model TinyLlama/TinyLlama-1.1B-Chat-v1.0 --device cpu --precision f16 --examples ~/skryba-corpus/my-voice-sft.json --output ~/skryba-corpus/my-voice.safetensors --rank 8 --epochs 1 --learning-rate 0.0001
It also reports what the export held:
{
"action": "train",
"store": "~/skryba-corpus/transcripts.sqlite3",
"passages": 594,
"passage_words": 57851,
"preferences": 0,
"added": 0,
"skipped": 206,
"output": "~/skryba-corpus/my-voice-sft.json",
"written": 388,
"adapter": "~/skryba-corpus/my-voice.safetensors"
}
Preflight, verbatim on stderr:
applying the model's own chat template to every prompt and completion
training 88 adapter tensors (1126400 parameters) at rank 8 across 22 layers
skipping example 15: 535 tokens exceed the 512 token limit
skipping example 18: 598 tokens exceed the 512 token limit
skipping example 22: 1104 tokens exceed the 512 token limit
skipping example 43: 555 tokens exceed the 512 token limit
skipping example 70: 570 tokens exceed the 512 token limit
skipping example 94: 603 tokens exceed the 512 token limit
skipping example 189: 634 tokens exceed the 512 token limit
skipping example 195: 515 tokens exceed the 512 token limit
skipping example 208: 750 tokens exceed the 512 token limit
skipping example 235: 564 tokens exceed the 512 token limit
skipping example 258: 591 tokens exceed the 512 token limit
skipping example 262: 533 tokens exceed the 512 token limit
skipping example 299: 709 tokens exceed the 512 token limit
skipping example 310: 622 tokens exceed the 512 token limit
skipping example 328: 538 tokens exceed the 512 token limit
skipping example 337: 605 tokens exceed the 512 token limit
skipping example 341: 520 tokens exceed the 512 token limit
skipping example 371: 522 tokens exceed the 512 token limit
skipping example 385: 628 tokens exceed the 512 token limit
Of the 388 exported examples, Ster kept 369 and skipped 19 as too long for the 512-token default, each named with its own token count. Ster skips rather than truncates because a cut completion would teach the model to stop early — the same refusal to quietly damage input that makes Skryba keep a source chunk no candidate beat.
Then the loss, one line per optimizer step, and the report Ster prints beside the adapter it wrote:
epoch 1/1 step 1/47 example 8/369 loss 4.0223
epoch 1/1 step 2/47 example 16/369 loss 3.9832
epoch 1/1 step 3/47 example 24/369 loss 3.6383
epoch 1/1 step 4/47 example 32/369 loss 3.6623
epoch 1/1 step 5/47 example 40/369 loss 3.8131
epoch 1/1 step 6/47 example 48/369 loss 3.7611
epoch 1/1 step 7/47 example 56/369 loss 3.8155
epoch 1/1 step 8/47 example 64/369 loss 3.9063
epoch 1/1 step 9/47 example 72/369 loss 3.7933
epoch 1/1 step 10/47 example 80/369 loss 4.0304
epoch 1/1 step 11/47 example 88/369 loss 4.3588
epoch 1/1 step 12/47 example 96/369 loss 4.2277
epoch 1/1 step 13/47 example 104/369 loss 3.3999
epoch 1/1 step 14/47 example 112/369 loss 3.9819
epoch 1/1 step 15/47 example 120/369 loss 3.6649
epoch 1/1 step 16/47 example 128/369 loss 3.4196
epoch 1/1 step 17/47 example 136/369 loss 3.8398
epoch 1/1 step 18/47 example 144/369 loss 4.2003
epoch 1/1 step 19/47 example 152/369 loss 3.5169
epoch 1/1 step 20/47 example 160/369 loss 3.1474
epoch 1/1 step 21/47 example 168/369 loss 3.7752
epoch 1/1 step 22/47 example 176/369 loss 3.3477
epoch 1/1 step 23/47 example 184/369 loss 3.6460
epoch 1/1 step 24/47 example 192/369 loss 3.9034
epoch 1/1 step 25/47 example 200/369 loss 3.5074
epoch 1/1 step 26/47 example 208/369 loss 3.5496
epoch 1/1 step 27/47 example 216/369 loss 3.3223
epoch 1/1 step 28/47 example 224/369 loss 3.8836
epoch 1/1 step 29/47 example 232/369 loss 3.9215
epoch 1/1 step 30/47 example 240/369 loss 3.4182
epoch 1/1 step 31/47 example 248/369 loss 3.2696
epoch 1/1 step 32/47 example 256/369 loss 3.8175
epoch 1/1 step 33/47 example 264/369 loss 3.8437
epoch 1/1 step 34/47 example 272/369 loss 3.9878
epoch 1/1 step 35/47 example 280/369 loss 3.4380
epoch 1/1 step 36/47 example 288/369 loss 3.8520
epoch 1/1 step 37/47 example 296/369 loss 3.6187
epoch 1/1 step 38/47 example 304/369 loss 3.2537
epoch 1/1 step 39/47 example 312/369 loss 4.0119
epoch 1/1 step 40/47 example 320/369 loss 3.5254
epoch 1/1 step 41/47 example 328/369 loss 3.4898
epoch 1/1 step 42/47 example 336/369 loss 3.6592
epoch 1/1 step 43/47 example 344/369 loss 3.4357
epoch 1/1 step 44/47 example 352/369 loss 3.1206
epoch 1/1 step 45/47 example 360/369 loss 3.7489
epoch 1/1 step 46/47 example 368/369 loss 2.8343
epoch 1/1 step 47/47 example 369/369 loss 2.8896
epoch 1/1 mean loss 3.6797
{
"path": "~/skryba-corpus/my-voice.safetensors",
"report": {
"accumulation": 8,
"alpha": 16.0,
"batch": 1,
"chat_template": "applied",
"epochs": 1,
"examples": 388,
"final_loss": 2.889592409133911,
"first_loss": 4.0222649574279785,
"layers": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21
],
"learning_rate": 0.0001,
"mean_final_epoch_loss": 3.6796915531158447,
"precision": "f16",
"rank": 8,
"skipped_long": 19,
"steps": 47,
"targets": [
"query",
"value"
],
"trainable_parameters": 1126400,
"trainable_tensors": 88,
"trained_examples": 369
}
}
369 examples at --accumulation 8 is 47 steps in one epoch: first loss
4.0222649574279785, final loss 2.889592409133911, mean over the epoch
3.6796915531158447. The loss is computed over the completion only —
ster tune sft masks the prompt out of the target window — so what falls here
is the model's cost on the person's own prose and not on the framing line in
front of it. It falls, and it wanders while it falls: the step-to-step numbers
are noise around a trend rather than a curve. The adapter Ster wrote is
4,513,072 bytes of query and value low-rank updates across all 22 layers.
The run took 2h11m on cpu. That figure is derived from file timestamps — the log was created at 17:45:38 and the adapter written at 19:57:02 — because Ster prints no wall-time field.
5. Generate twice, with the adapter and without it
Same checkpoint, same prompt, same decoding, run twice; the second command
adds --adapter and changes nothing else:
ster generate --model TinyLlama/TinyLlama-1.1B-Chat-v1.0 \
--prompt "$PROMPT" --max-new-tokens 120 --temperature 0 --seed 42
ster generate --model TinyLlama/TinyLlama-1.1B-Chat-v1.0 \
--prompt "$PROMPT" --max-new-tokens 120 --temperature 0 --seed 42 \
--adapter my-voice.safetensors
$PROMPT is one of the export's own prompts — the framing line, a blank line,
and the passage's opening sentences:
Continue this passage in the same voice:
Independent static review (13 findings) confirms the known web/import/resume/Cua/destructive blockers and adds these unique must-fix items: 1) Runtime manifest is unsigned/shallowly checked: worker must recompute source_input/key/correlation/constraints from exact committed record+binding, and rederive mobile package/bundle; not trust base64 fields.
That passage is one of the 19 examples this run skipped for exceeding the 512-token limit, so it is the operator's own writing that the adapter was never trained on.
Without the adapter:
Continuation:
Independent static review (13 findings) confirms the known web/import/resume/Cua/destructive blockers and adds these unique must-fix items:
1) Runtime manifest is unsigned/shallowly checked:
Worker must recompute source_input/key/correlation/constraints from exact committed record+binding, and rederive mobile package/bundle; not trust base64 fields.
This finding confirms that the runtime manifest is unsigned, which means that it is not signed by the
With the adapter:
2) Web/import/resume/Cua/destructive blockers: worker must recompute source_input/key/correlation/constraints from exact committed record+binding, and rederive mobile package/bundle; not trust base64 fields.
Both runs stopped at the 120-token cap. The first prints a Continuation:
heading and repeats the prompt before continuing; the second prints neither.
What this proves, and what it does not
It proves the boundary holds: Skryba captured a person's own prose, exported 67 SFT examples and one DPO pair from the 116-passage store and 388 SFT examples from the 594-passage transcript corpus, and Ster read those files without a translation step, trained 369 of the 388, wrote an adapter, and generated with it. Skryba loaded no model, mapped no checkpoint, and read no credential to do any of it.
It does not settle what an adapter writes like. A loss that falls from 4.0222649574279785 to 2.889592409133911 over 47 steps says the adapter is learning something about these sentences, and the pair of generations above is one prompt at one temperature on one held-out passage. What differs between them is stated where they sit: a heading and a repeated prompt in the first, neither in the second.
Maintained as part of the website-owned Skryba documentation corpus.