On this page

Walkthrough: the local generator, watched end to end

skryba rewrite --generator local had never been run. Its argv had been read and its refusals had been quoted, but no ster generate process had ever been started by it: rewrite builds the detector ensemble before it builds the generator, and a machine holding no detector credential never reaches the second step. This page is that run, and the three defects it found.

Two localhost fixtures stand in for the paid APIs, exactly as in offline rewrite and scoring: a Sapling-shaped detector endpoint and a Brama-compatible /v1/chat/completions. Neither is a detector and neither is a model, and the second one decides how this run ends. The generator on the local route is not a fixture: real ster generate, real TinyLlama/TinyLlama-1.1B-Chat-v1.0 from the Hugging Face cache, and the real personal adapter trained in capturing your writing.

skryba at 9a486e8. ster --version printed ster 0.13.0. uptime on this Apple M2 Max printed a 1-minute load average of 70.61 before these runs and 128.16 between two of them, on twelve cores, and the machine is shared with its operator: every wall-clock figure below is context and not a measurement. Every other number was copied from a run watched print.

The two fixtures, stated exactly

SAPLING_API_URL pointed at a localhost endpoint answering Sapling's documented {"score": number} shape. It reads a fixed word list and a SHA-256 digest, and never a model:

score = min(0.99, 0.4200
                + 0.0400 x (occurrences of: additionally, furthermore, moreover,
                            in conclusion, it is important to note, delve,
                            leverage, utilize, significant, comprehensive,
                            robust, seamless)
                + (sha256(text)[0..8] mod 601) / 10000)

That is deterministic in the text alone, which is the only property the loop needs: identical candidates score identically, different candidates differ, and selection by lowest ai_probability has something to rank. It says nothing at all about whether a person wrote the text. No probability on this page is detector output.

BRAMA_URL pointed at the same server, which told the judge apart from the generator by the first words of the system message. The judge returned one fixed, valid evaluation for every candidate:

{
  "meaning_preservation": 0.97,
  "factual_consistency": 0.96,
  "grammar": 0.98,
  "coherence": 0.95,
  "naturalness": 0.93,
  "readability": 0.94,
  "critical_error": false,
  "issues": [],
  "rationale": "Fixture judge: one fixed evaluation, no model was consulted."
}

That weighs to quality_score 0.9601999999999999 and clears the default --minimum-quality 0.90. It clears it without reading the candidate. That is the single most important fact on this page: a fixture judge cannot fail a rewrite that destroyed its source, and further down it does not.

On the Brama route only, the same endpoint was also the generator, making deterministic per-strategy substitutions. Nothing about generation on the local route was a fixture.

The input

Two paragraphs, 30 and 31 whitespace words. The first is the passage the offline rewrite walkthrough uses, so the two pages are comparable:

Additionally, the study found that participants who received the intervention reported higher satisfaction. Furthermore, retention improved by twelve percent over the six-month follow-up period, and no adverse events were recorded.

Moreover, the research team utilized a comprehensive follow-up protocol at weeks four, twelve, and twenty-four. It is important to note that three participants withdrew for reasons unrelated to the intervention itself.

With --max-words-per-chunk 40 that is two chunks, one per paragraph: 30 words fits, 30 + 31 does not.

What refuses before a process is spawned

$ skryba rewrite passage.txt --generator local --local-model TinyLlama/TinyLlama-1.1B-Chat-v1.0
Error: no detector credentials found; configure GPTZERO_API_KEY, WINSTON_API_KEY, ORIGINALITY_API_KEY, or SAPLING_API_KEY

$ skryba rewrite passage.txt --generator local
Error: --generator local needs --local-model naming the checkpoint Ster maps

$ skryba rewrite passage.txt --generator local --local-model '   '
Error: the local generator needs a checkpoint; pass --local-model

$ skryba rewrite passage.txt --generator local --local-model TinyLlama/TinyLlama-1.1B-Chat-v1.0 --local-max-new-tokens 0
Error: --local-max-new-tokens must be positive

Each exited 1. The first is the ordering the rewrite reference names: detectors are built before the generator, so a machine with no detector key gets the detector sentence even when it asked for the local route. With HOME pointed at an empty directory and PATH reduced to /usr/bin:/bin, so that neither entry on PATH nor any of the five well-known locations holds the binary:

$ skryba rewrite passage.txt --generator local --local-model TinyLlama/TinyLlama-1.1B-Chat-v1.0
Error: ster is not installed: install it with `cargo install --git https://github.com/wisent-ai/ster --locked`, or put the `ster` binary on PATH.

And a --adapter that is not there — Ster's refusal rather than Skryba's, carried through in Ster's own words:

$ skryba rewrite passage.txt --generator local --local-model TinyLlama/TinyLlama-1.1B-Chat-v1.0 --adapter no-such-adapter.safetensors
Error: failed to read adapter /Users/…/no-such-adapter.safetensors

The run

skryba rewrite passage.txt \
  --generator local \
  --local-model TinyLlama/TinyLlama-1.1B-Chat-v1.0 \
  --adapter ~/.stado/work/skryba-corpus/my-voice.safetensors \
  --local-device cpu --local-precision f16 --local-max-new-tokens 128 \
  --json --candidates 8 --max-words-per-chunk 40 --temperature 0.9 --seed 42

Exit status 0. Stdout was 20,392 bytes. The run took 7m41.247s. Repeated later without changing a flag it took 3m29.182s and wrote byte-identical stdout, because each ster generate is seeded with its own candidate index and nothing on this route is drawn from a clock. The identical work taking 461s and then 209s is the load average on a shared machine, and the reason no number here that is measured in seconds should be read as a property of the product.

16 ster generate processes ran: 8 candidates times 2 chunks. The process table was sampled every 100ms for the length of the run. Those 16 were the only ones, none was ever observed in state Z, and none outlived the run:

lifetimes, seconds: 31.12 49.03 45.36 33.49 58.35 29.58 34.29 26.81
                    32.85  4.73  4.81 13.07 35.44 13.71 13.26 26.02
min 4.73   max 58.35   mean 28.25   sum 451.92

Never more than one at a time, which is the whole point of the route and the reason it is chosen rather than default: each process maps the checkpoint again, and a group generated in parallel would hold one copy of TinyLlama's weights per candidate. 451.92s of process lifetime inside 461.25s of wall clock is that sequence, observed rather than asserted; the repeat run's 16 processes summed to 201.32s inside 209.18s. The same passage on the Brama route took 2.046s for the same 16 candidates: on this machine the weights being local cost between 100 and 225 times the wall clock, depending on what else was running.

The detector fixture was called 15 times and the judge 15 times, for 15 distinct texts: every candidate that survived generation was scored by the ensemble and evaluated by the judge, exactly once each. 15 and not 16, because of the line Skryba printed on stderr:

candidate 6 (specific) came back as whitespace from `ster generate`; dropping it from the group

The report

Both chunks selected a candidate; neither fell back to its source. --seed 42 drew the same strategy indices here as on the Brama route, because strategy sampling happens before the generator is asked for anything: 3 3 3 2 0 2 4 5 for the first chunk and 0 0 5 3 2 0 3 1 for the second. Across the run all six built-in strategies were sampled. specific, index 4, was drawn exactly once — at offset 6 of the first chunk, which is the candidate that came back as whitespace, so four distinct strategies reached scoring in that chunk and five in the other.

chunk 0   source 30 words   used_source_fallback false
  [0] 3 rhythmic         ai_probability 0.4699   quality_passed true
  [1] 3 rhythmic         ai_probability 0.45     quality_passed true
  [2] 3 rhythmic         ai_probability 0.4418   quality_passed true   <- selected
  [3] 2 scholarly-human  ai_probability 0.5137   quality_passed true
  [4] 0 natural-editor   ai_probability 0.4652   quality_passed true
  [5] 2 scholarly-human  ai_probability 0.5388   quality_passed true
  [6] 5 light-touch      ai_probability 0.4729   quality_passed true

chunk 1   source 31 words   used_source_fallback false
  [0] 0 natural-editor   ai_probability 0.4408   quality_passed true
  [1] 0 natural-editor   ai_probability 0.4637   quality_passed true
  [2] 5 light-touch      ai_probability 0.4658   quality_passed true
  [3] 3 rhythmic         ai_probability 0.4493   quality_passed true
  [4] 2 scholarly-human  ai_probability 0.4706   quality_passed true
  [5] 0 natural-editor   ai_probability 0.4393   quality_passed true   <- selected
  [6] 3 rhythmic         ai_probability 0.4735   quality_passed true
  [7] 1 plainspoken      ai_probability 0.4407   quality_passed true

quality_score was 0.9601999999999999 on all 15, because the fixture judge returns one object. Every cached field was false: all 15 texts were distinct, so nothing here could come from the score cache.

The selected rhythmic candidate for the first chunk, quoted from the report:

Introduction

Despite the high impact of ground-up strategies for improving patient satisfaction and retention, few clinicians have access to the potential benefits. The pilot study was designed to recruit experienced clinicians with limited funds and resources, in order to present an effective and achievable protocol that could be further modified for efficient implementation. This study reviewed the literature to determine the best strategies for improving satisfaction and retention for the acute surgical subspecialty, specifically for the introduction of practice-changing interventions and in real-time intuitive feedback.

Background & Purpose

And the selected natural-editor candidate for the second chunk, quoted in full:

Conclusion:

Eleven characters, one word, and the lowest ai_probability in its group of eight. Joined, result.text was:

Introduction

Despite the high impact of ground-up strategies for improving patient satisfaction and retention, few clinicians have access to the potential benefits. The pilot study was designed to recruit experienced clinicians with limited funds and resources, in order to present an effective and achievable protocol that could be further modified for efficient implementation. This study reviewed the literature to determine the best strategies for improving satisfaction and retention for the acute surgical subspecialty, specifically for the introduction of practice-changing interventions and in real-time intuitive feedback.

Background & Purpose

Conclusion:

Not one claim of the source survives. Both paragraphs are gone, replaced by an invented clinical abstract and a section heading. The loop did exactly what it is specified to do — the least detectable candidate that passes the quality gate — and the quality gate was a fixture that passes everything. A real Brama judge would have scored meaning_preservation near zero and set critical_error on all 15, no candidate would have passed, and both chunks would have kept their source. That is the guarantee the product rests on, and a stand-in judge is exactly what switches it off.

The other candidates in the same two groups show the rest of what a 1.1B checkpoint does with this instruction. Three began by echoing the prompt's own delimiter:

Passage:
These results suggest that the intervention was successful, that the potential risks were manageable, and that the participants experienced a significant improvement in satisfaction and retention. Additionally, the study found that the intervention was a reliable measure of satisfaction.

One answered the instruction instead of following it:

Could you perform the following task for me?

One restated it:

However, full statistical analyses and detailed program implementation plans were not feasible for this project. To keep the rewritten passage authentic, omit all demographic scores, methods, dates, and qualifications.

Three defects the run found

None of these was found by reading the source. All three needed the process to actually run.

One empty completion aborted the whole document

The first attempt exited 1 after 2m12.620s and seven ster generate processes, six of which had produced a candidate, with nothing written — no report, no output file, no partial text:

$ skryba rewrite passage.txt --generator local …
Error: `ster generate` returned nothing for a 81-word prompt; raise --local-max-new-tokens

Reproduced by hand with the exact argv Skryba builds for candidate 6 of the first chunk — strategy specific, --seed 6 — ster generate exited 0 and wrote 129 bytes, every one of them a newline. The completion was not truncated for want of budget; the model spent all 128 tokens on \n, so the remedy the sentence named would not have helped. Worse, the failure landed before any candidate was scored, which is upstream of the branch that keeps a source chunk when nothing beats it — the one invariant that makes the product safe to run at all.

A group is sampled with replacement and the source chunk is already the fallback when nothing in the group survives, so one degenerate sample is worth one candidate. It is now dropped and named on stderr, and the chunk is selected from what is left. When a whole group comes back that way the group is empty, the quality filter selects nothing, and used_source_fallback is true — the existing branch, reached without adding a new one. That is the fix in 9a486e8, and it is why the run above finished with 7 candidates in one chunk and 8 in the other.

The group paid twice for identical text

On the Brama route the same passage cost 16 detector requests and 16 quality evaluations for 9 distinct candidate texts. A group of 8 regularly draws the same strategy more than once — 3 3 3 2 0 2 4 5 above draws rhythmic three times — and a deterministic generator returns byte-identical text for it. Two different strategies can collide as well: light-touch and plainspoken produced the same text for the second chunk, which is why 16 candidates held only 9 distinct texts.

The score cache cannot catch this. The whole group is scored concurrently, so every request is in flight before the first result is written and all of them miss. The judge has no cache in front of it at all.

Identical text has an identical score by definition, so the group is now collapsed before it leaves: one measurement per distinct text, handed back to every position that asked for it. The same command now costs 9 and 9, and the report is unchanged — same 8 candidates per chunk, same probabilities to four decimals, same selection, same 19,046 bytes of stdout. Generation is not deduplicated and must not be: a real model asked the same prompt twice does not answer identically, and GRPO needs its group to be the size it asked for.

Ster's refusals arrived behind two Error: frames

A missing adapter used to read:

Error: Error: failed to read adapter /Users/…/no-such-adapter.safetensors

Ster prints its own Error: frame, Skryba passed the line through verbatim, and anyhow printed a second frame in front of it. The leading frame is now dropped and the sentence is untouched, including any Caused by: chain below it.

The same passage on the Brama route

Same loop, same seed, same eight candidates per chunk, same fixture judge and fixture detector; the generator is the fixture instead of Ster.

skryba rewrite passage.txt --generator brama --json \
  --candidates 8 --max-words-per-chunk 40 --temperature 0.9 --seed 42

Exit 0, 2.046s, 19,046 bytes of stdout. It selected scholarly-human at ai_probability 0.4321 for the first chunk and light-touch at 0.4246 for the second, and result.text was:

In addition, the study found that participants who received the intervention reported higher satisfaction. Beyond that, retention improved by twelve percent over the six-month follow-up period, and no adverse events were recorded.

Also, the research team used a full follow-up protocol at weeks four, twelve, and twenty-four. Note that three participants withdrew for reasons unrelated to the intervention itself.

What differs

Not better or worse. The two generators were asked the same thing and what came back differs in kind.

Brama route (fixture generator) Local route (real Ster and adapter)
Rewrite of chunk 0 32 words, every claim intact 88 words, no claim of the source present
Rewrite of chunk 1 27 words, every claim intact Conclusion:
What changed in the source three connectives, one hedge, two word choices everything
Distinct candidate texts per group 5 of 8, then 4 of 8 7 of 7, then 8 of 8
Wall clock 2.046s 7m41.247s
Cost per candidate one HTTP POST one process, one mapping of the checkpoint
Selected ai_probability 0.4321 and 0.4246 0.4418 and 0.4393

The fixture generator's determinism is why its groups collapse to 5 and 4 distinct texts while the local groups do not: each ster generate is seeded with its own candidate index, so the same strategy drawn three times produces three different candidates. The fixture generator preserves meaning because all it does is substitute words — Additionally to In addition, Furthermore to Beyond that, Moreover to Also, It is important to note that to Note that, utilized to used, comprehensive to full. The local generator does not preserve meaning at all.

When nothing passes the gate, the source is kept

The branch that makes the loop safe was exercised on the local route, with real generation, by raising the gate above what the fixture judge returns:

skryba --minimum-quality 0.98 rewrite passage.txt \
  --generator local --local-model TinyLlama/TinyLlama-1.1B-Chat-v1.0 \
  --adapter ~/.stado/work/skryba-corpus/my-voice.safetensors \
  --local-precision f16 --local-max-new-tokens 128 \
  --json --candidates 2 --max-words-per-chunk 40 --seed 42

Exit 0, 48.797s, 4 ster generate processes. All four candidates came back with quality_passed false; both chunks reported used_source_fallback true with strategy source-fallback and strategy_index null; the detector fixture and the judge were each called 6 times, four candidates plus two sources; and the printed text was the input, byte for byte. The source chunks scored 0.549 and 0.6282 against the fixture — higher than every one of the four candidates it rejected, which scored 0.4699, 0.45, 0.4639 and 0.4776. Skryba kept them anyway, which is the rule: quality is a gate, not a ranking preference.

What this proves, and what it does not

It proves the local route runs as specified. Chunking split the passage in two; the policy sampled eight strategies per chunk and all six built-in strategies over the run; one ster generate process ran per candidate, 16 of them, one at a time, each mapping the checkpoint again, all of them reaped and none left behind; a degenerate sample cost one candidate instead of the document; every surviving candidate was detector-scored and judged exactly once; the least detectable quality-passing candidate was selected for each chunk; and with the gate raised the source was kept instead. --candidates is honoured exactly: 8 produced the 16 processes counted above, and on the Brama route, where a sweep is cheap, 1, 3 and 12 produced 1, 3 and 12 candidates per chunk and 2, 6 and 24 generator calls. Nothing on this path writes a temporary file — the prompt travels in argv, which is why the sampled process table holds the whole system prompt — and the only files any of these runs created were the score cache and what --output and --report named.

It does not prove the local route produces a usable rewrite. On this input it produced the opposite, and the run says plainly why. A 1.1B checkpoint at --temperature 0.9 with a rank-8 adapter is too small for an instruction that requires holding every claim of a passage while rewording it, and the fixture judge that let the result through does not read text. The judge is the component that would have refused this, and replacing it with a stand-in is what allowed a one-word rewrite to be selected over its source. Read the page as the shape of the plumbing, verified; not as evidence about what a personal adapter writes like, and not as a detector score of anything.

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