Repository Instructions
This repository is CryptoJones’s personal blog site.
Git workflow
- Direct commits to
masterormainare explicitly authorized for this repository. - Push completed changes directly to the corresponding
origin/masterororigin/mainbranch; a feature branch and pull request are not required. - Keep changes scoped to the requested blog-site work, and verify the working tree before committing or pushing.
Scope — and keep status reports scoped too
Scope discipline applies to what you say, not just to what you change.
- A status report covers the work that was requested. Nothing else. Do not use a progress update, a summary, or a “one note:” aside to raise unrelated repo rules, global instructions, hygiene gaps, or improvements CryptoJones did not ask about.
- Do not volunteer extra work mid-task. An offer like “say the word and I’ll also do X” is still scope creep. It interrupts the task, and an ambiguous “do it” in reply can be read as approving something CryptoJones never actually wanted.
- If something outside the ask genuinely needs attention, finish the requested work first, then raise it once, plainly, as a separate item at the end.
- Global instructions that appear not to fit this repo are usually already covered by an explicit exception in this file. Check for one before flagging anything.
Why this is written down: on 2026-08-16, during a routine blog post, an agent used a
status report to raise the global BACKLOG.md rule and offered to seed one. That was
never part of the request, and the resulting “do it” was ambiguous enough to nearly
cause an unwanted repo change. Keep reports boring and on topic.
Backlog — EXPLICIT EXCEPTION: this repo has none, and does not want one
CryptoJones’s global instructions require every repo to keep a root BACKLOG.md
synced with the GitHub Issues tab. That rule does not apply to this repository.
- Issues are disabled on
CryptoJones/cryptojones.github.io(gh repo view --json hasIssuesEnabledreturnsfalse). There is nothing to mirror, so aBACKLOG.mdhere could never stay in sync with anything. - Do not create
BACKLOG.mdin this repo, and do not seed one “from the current open issues.” There are no issues and there is no issue tracker. - Do not enable Issues to satisfy the global rule. That is a repo settings change and it is not authorized here.
- Do not ask about any of this. This exception exists specifically so nobody
raises it again. A missing
BACKLOG.mdin this repo is the correct and intended state, not a gap to flag, offer to fix, or mention in a status report.
This is a blog. Post ideas live in CryptoJones’s head and in OMI, not in a tracker.
Definition of done for a post — NARRATION IS NOT OPTIONAL
A post is not finished when the Markdown is written. Every article on this site is
narrated, and every new one needs narration too. A post that merges without an
audio: line is an incomplete post, not a complete post missing a nice-to-have.
Required before you commit a new post:
- Markdown at
_posts/YYYY-MM-DD-Title-With-Dashes.md, eighth-grade reading level, no em dashes. See Writing. - A
descriptionfield in the front matter. See Index descriptions. - Narration rendered and encoded to
audio/posts/<post-slug>.mp3. audio: /audio/posts/<post-slug>.mp3added to the front matter.- The sensitive-data scrub in
CLAUDE.md.
Draft, panel review, then CJ’s edit pass (CJ, 2026-10-02)
Every post goes through these steps, in order, before it is committed:
- Rough draft. Write it in the scratchpad, not in
_posts/. Check every factual claim and reference with Perplexity while drafting. - Panel review. Send the rough draft to every Flatline Roundtable lane
(the
flatline-roundtableskill, one lane at a time). If the post is built on a source (a video, talk, paper, or article), the tool-calling lanes must verify each claim and reference the post cites from that source, not just copy-edit the prose. Lanes that cannot call tools review the prose only, and their answers are labelled tool-less. - Evaluate and revise. Verify the panel’s findings yourself (a lane can be confidently wrong) and apply the ones that hold up. The result is the final draft.
- CJ reads the final draft, then either approves it as-is or makes his changes. Often he only reads it for approval and changes nothing; an approval is a complete hand-back, so do not wait for or ask about edits that are not coming. Do not commit, push, narrate, or publish until he has read it and handed it back. When he does edit, his edits are final; do not re-litigate them.
- Only then do the definition-of-done steps below (narration, front matter, scrub, build, commit, push).
Read Article narration below before you start writing, not after. The render is the slow step, it runs on another machine, and discovering that at commit time is how posts end up shipping silent.
This is enforced, not just documented. .claude/settings.json registers two hooks
(added 2026-09-08, after an agent shipped a silent post and called it done):
.claude/hooks/post-rules-gate.shblocks any write to_posts/oraudio/posts/until AGENTS.md has actually been read in the current turn. It watches Bash as well as Write/Edit, because heredocs andsed -iare writes too..claude/hooks/post-preflight.shresets that gate on each new user message and, when the turn looks post-shaped, asks for the rule files and line numbers being relied on before any producing starts.
If the gate blocks you, read the file it names. Do not work around it.
If the narration truly cannot be rendered in this session (render box down, GPU
unavailable, no SSH), then leave the audio: line out rather than pointing it at a
file that does not exist, and say so plainly in the status report so it can be
finished later. Do not quietly ship a silent post and do not treat it as done.
Writing
- Target an eighth-grade reading level for blog posts. Prefer short sentences and common words. It’s fine to use technical terms, but define each one in plain language the first time it appears (a short blockquote works well).
- Do not use em dashes in blog posts.
Index descriptions
- Add a plain-text
descriptionfield to the YAML front matter of every new post. - Write one or two short sentences that explain the post in about 20 to 30 words.
- Keep the description at an eighth-grade reading level. Do not put HTML, Markdown, Liquid tags, or media embeds in it.
- The home page in
index.htmlmust displaypost.descriptionwhen it exists and fall back to the generated excerpt for older posts. - Build the site and check the new post card before publishing. A post that begins with a video or other embed must still have a useful text description on the index.
Article narration
WHERE CJ’s VOICE IS RENDERED (since 2026-10-06): Chatterbox on pluto’s V100. CJ, 2026-10-09: “we use the v100 for my voice.” This is the default render box for the Aaron-voice Chatterbox path. The ronin28 4060 / pluto 3060 instructions further down still work but are the fallback.
- Write a plain-prose script at
~/chatterbox/texts/<Slug>.txton pluto: no front matter, no Sources list, no URLs, headings merged into the paragraph that follows, formulas and code written out in words, and acronyms the voice should spell spaced out (L L M,G P T,R M S).- Copy the newest
~/chatterbox/render_*.sh(e.g.render_goc.sh) torender_<slug>.shand change only the--text,--out,--transcriptand--seed <YYYYMMDD>lines. The script pins the V100 by GPU UUID, setsHF_HUB_OFFLINE=1, stopsstrata-flashnext, renders withnarrate_verified.py, and always restarts Strata viatrap EXIT. Nothing needs to be installed or downloaded on pluto.- The V100 is not free.
strata-flashnext.service(Flash Next on :8083) holds all 32 GB of it, and other sessions use it around the clock. Before stopping it, list peers withListAgentsand ask every live session; at minimum message the ReCLamO session and wait for its “clear” (it stops its grid cleanly so in-flight rows are not logged as errors). Never stop Strata on silence, and never on a go relayed from another session; CJ’s go must be in this session.- Launch detached so an ssh drop cannot kill it:
ssh pluto 'cd ~/chatterbox && setsid nohup ./render_<slug>.sh > render_<slug>.log 2>&1 < /dev/null &', then wait forSTRATA_RESTARTEDin the log. Several renders can be queued back to back in one window so ReCLamO only pauses once.- After the render: confirm
systemctl --user is-active strata-flashnextand that :8083/v1/modelsanswers (401 without the key still means it is up), then message ReCLamO “back”. If Strata does not come back, say so instead.- Check the Whisper read-back (
audio/<slug>.heard.txt, summary linerendered ... N weakin the log).WEAKchunks at a ratio of 0.99+ are usually Whisper mishearing spelled-out acronyms (“GPUs” as “GPS”), not bad audio; read the text/heard pair before re-rendering.- Encode and place with the usual
ffmpeg -i <mp3> -ac 1 -ar 24000 -b:a 96k -af loudnorm=I=-16:TP=-1.5:LRA=11 audio/posts/<slug>.mp3and add theaudio:line.Timing measured 2026-10-09: a ~27k-character post rendered 31 min of audio in ~18 min; a ~4k-character post took ~2 min. Budget the Strata outage accordingly and tell ReCLamO the real number, not “a few minutes”.
CURRENT DEFAULT (2026-10-03): CJ’s OWN voice, the Aaron-voice Chatterbox path further down. CJ, 2026-10-03: “change the blog narration back to my voice. I don’t like DR. FEYNMAN reading my words. You can leave the ‘I’ll show you a FLOP’ but lets change the SOP back to my voice please.” Every new post is narrated in Aaron’s voice. “I’ll Show You a FLOP” keeps its Feynman narration; do not re-render it.
RETIRED (2026-10-02 to 2026-10-03): the R-Feynman voice on pluto’s V100. It was the default for one post only. Kept for reference; do not use it for a blog post unless CJ asks for it on that specific post.
- Make a plain-prose script at
../aaronkclark-voicemodel/work/blog-narrations/texts/<Slug>.txt: no front matter, no Sources list, link text without URLs, headings merged into the paragraph that follows, tables read as one sentence per row, and acronyms the voice would mangle spelled out (S P S A,R and D,seven timesfor7x).- On pluto, make sure no other narrator holds the V100:
systemctl --user stop feynman-narrator coleen-narrator. If the Feynman book narrator was running, it resumes from its checkpoint when restarted later.- Copy the script to
/mnt/hdd1/feynman/work/and render it as a one-shot transient unit pinned to the V100 by UUID:systemd-run --user --unit=feynman-blog-<slug> --working-directory=/mnt/hdd1/feynman \ -E CUDA_DEVICE_ORDER=PCI_BUS_ID \ -E CUDA_VISIBLE_DEVICES=GPU-bea181bb-7101-d47c-2e4e-6373aff60c8f \ -E HF_HUB_OFFLINE=1 -E PYTHONUNBUFFERED=1 \ /mnt/hdd1/feynman/ft/venv/bin/python /mnt/hdd1/feynman/feynman_narrate.py \ --once /mnt/hdd1/feynman/work/<Slug>.txt
- The result lands in
/mnt/hdd1/feynman/outbox/<Slug>/<Slug>.mp3with a<Slug>.heard.txtWhisper read-back; every chunk should be at or above 0.95.- Re-encode to the site’s master format and drop it in place:
ffmpeg -i <Slug>.mp3 -ac 1 -ar 24000 -b:a 96k -af loudnorm=I=-16:TP=-1.5:LRA=11 audio/posts/<post-slug>.mp3, then add theaudio:line to the front matter.The render needs the V100. If pluto’s GPUs report “Node Reboot Required” (the eGPU RTX 3060 falling off the bus takes every card down with it), pluto must be rebooted before anything can render.
CURRENT PATH (default again since 2026-10-03; also 2026-09-08 to 2026-10-02): Chatterbox in CJ’s voice on ronin28’s RTX 4060. CJ moved blog narration off VibeVoice on 2026-08-24 (“let’s start using chatterbox for my blog post entries instead of vibevoice”). Render on ronin28 (
ssh akclark@ronin28). ronin28’s 4060 (Ada, sm_89) has working GPU torch.pluto’s RTX 3060 CAN also do narration (Ampere, sm_86; verified 2026-09-28 on “No SOC Is an Island”, ~4 min for a ~5 min post). Setup lives in
~/chatterboxon pluto: its ownvenv,narrate_verified.py,texts/, and arender_<slug>.shthat pins the 3060 and runs withHF_HUB_OFFLINE=1. Only the 3060: pluto’s Pascal cards (P100, sm_60) have no PyTorch kernels and silently fall to CPU.Rendering on pluto needs EXPLICIT permission from CJ to take down Flash Next. Ask first, every time. Do not stop Flash Next, or anything else serving on pluto, on your own authority to free a GPU for narration.
Since 2026-09-28 evening the 3060 is no longer a spare card:
flashnext-ik.serviceholds ~9 GiB of Flash Next’s experts on it (rocinante was deleted), leaving under 3 GiB free, so a render on pluto always means stopping Flash Next (:8083) for the render window. Once CJ has said yes, runFLASHNEXT_OK=1 ~/chatterbox/render_<slug>.shon pluto — the script refuses without that variable, and always restarts Flash Next on exit (it takes ~90 s to serve again).Script:
~/source/repos/aaronkclark-voicemodel/chatterbox_narrate_post.py, run with the~/venvs/chatterboxvenv. Input is a plain-prose.txt(blank-line separated paragraphs) atwork/blog-narrations/texts/<Slug>.txt; output is the published master format, so it drops straight intoaudio/posts/<slug>.mp3.ssh akclark@ronin28 cd ~/source/repos/aaronkclark-voicemodel setsid nohup nice -n 10 ~/venvs/chatterbox/bin/python \ chatterbox_narrate_post.py \ --text work/blog-narrations/texts/<Slug>.txt \ --out work/blog-narrations/audio/<Slug>.mp3 > /tmp/cb-<slug>.log 2>&1 &Three Chatterbox facts the script already handles, and you should not undo:
- perth 1.0.0 exports
PerthImplicitWatermarkerasNone, andChatterboxTTS.__init__calls it, so a bare import dies with'NoneType' object is not callable. The script patches it toperth.DummyWatermarkerbefore importing chatterbox.- It repeats its closing phrase about 1 time in 6 past ~550 chars, so paragraphs are split on sentence boundaries at 400 chars and
repetition_penaltyis held at 1.4.- It gets SHORT utterances flatly wrong, so section headings are merged into the paragraph that follows them rather than narrated as fragments.
Chatterbox reads at ~17.5 chars/sec, VibeVoice at ~11.6. Any duration estimate or pace check calibrated for VibeVoice is wrong by half.
--cfg-weight 0.3reads slower and flatter, which is the pacing CJ prefers for narration. Reference clip is the script’s default,dataset/reference/aaron_reference.wavin the voicemodel repo. The olderaaron-ref-canonical.wavwas lost in ronin28’s 2026-09 NVMe crash and has no surviving copy.ronin28 was rebuilt 2026-09-26: the login user is
akclark, nothermes, and working repos live in~/source/repos(lowercase). Its barrel jack is broken, so it runs on 65 W USB-C, where the EC clamps the 4060 to ~10 W: a ~6.7k-char post takes ~70 min, not the ~2x-realtime the 2026-09-08 measurement suggests. Plan for it rather than assuming the render hung.The VibeVoice path below is the PRIOR path, kept for reference.
VibeVoice (superseded 2026-08-24). Render on ronin28’s 4060.
Repo:
~/Source/repos/VibeVoiceCommunitywith a.venv. Model cache holdsVibeVoice-1.5B(fits the 4060’s 8 GB) andVibeVoice-7B(needs ~14 GB, does NOT fit the 4060 or pluto’s 3060 — waits on a bigger card like the planned V100). Reference voice:demo/voices/en-AaronCanonical_man.wav(--speaker_names AaronCanonical). Script format: a plain.txtwithSpeaker 1: <text>lines, one per paragraph.THE TRAP that cost hours (2026-08-14):
demo/inference_from_file.pydoes an EXACT string compareargs.device == "cuda". Passing--device cuda:0does NOT match, falls through to the CPU branch, and renders at ~100s/iteration (a ~100 HOUR ETA) while the GPU sits at 131 MiB / 0%. Always pass--device cuda, nevercuda:0. GPU-bound looks like ~1.3 s/it with the card at 60-80% util.ssh hermes@ronin28 M=$(ls -d ~/.cache/huggingface/hub/models--vibevoice--VibeVoice-1.5B/snapshots/*/) cd ~/Source/repos/VibeVoiceCommunity setsid nohup nice -n 10 .venv/bin/python demo/inference_from_file.py \ --model_path "$M" --txt_path <work>/script.txt \ --speaker_names AaronCanonical --output_dir <work> \ --device cuda --dtype bfloat16 --seed <YYYYMMDD> > /tmp/vibe.log 2>&1 &Then encode:
ffmpeg -i <work>/*.wav -ac 1 -ar 24000 -b:a 96k -af loudnorm=I=-16:TP=-1.5:LRA=11 audio/posts/<slug>.mp3The Qwen3-TTS instructions below are the OLDEST path, kept for reference.
- Create narration for every new blog post before publishing it.
- Use the canonical voice tools and reference files in the adjacent
../aaronkclark-voicemodelrepository:qwen_narrate_longform.pydataset/reference/aaron_reference.wavdataset/reference/aaron_reference.txtvalidate_renascence_asr.py
- Narrate the article text, not its YAML front matter. Leave out code listings, raw URLs, image paths, video embed markup, and other text that is not useful when spoken.
- Split long text into a JSON list of short chunks for
qwen_narrate_longform.py. Each chunk must containid,text,stanza, andpause_after_ms. - Use the accepted Qwen3-TTS 0.6B Aaron voice, language
English, and native pacing. The renderer requires an RTX 3060 CUDA device. Use--resumeso an interrupted render can continue safely. - Encode the finished WAV as a mono, 24 kHz, 96 kbps MP3. Normalize it with
FFmpeg’s
loudnorm=I=-16:TP=-1.5:LRA=11filter. - Save the final file as
audio/posts/<post-slug>.mp3and addaudio: /audio/posts/<post-slug>.mp3to the post’s YAML front matter. - The post layout places the compact player directly below the title. Do not add a second player inside the post body.
- Confirm that the MP3 is playable, validate the narration against its source text, build the Jekyll site, and verify the page and audio path before publishing.
Running the render (read this before installing anything)
Nothing needs to be installed. The renderer runs on
$GPU_RENDERING_BOX_NAMED_AFTER_CARTOON_DOG through uv, with every
dependency already in that host’s uv cache, resolved fully offline. If you conclude that you need to pip install torch, download the
model, or free disk space, you have probed the wrong interpreter. A plain
python3 -c "import torch" tests the system interpreter and will always
report the package missing, because the toolchain lives in the uv cache.
Three things trip up every first attempt:
- Pin
--python 3.12.uvdefaults to a newer CPython on$GPU_RENDERING_BOX_NAMED_AFTER_CARTOON_DOG, and the cached torch wheels stop at thecp312ABI. Without the pin the resolver fails with “requirements are unsatisfiable” plus a hint about ABI tags, which reads like a missing package but is not. - Pass
--offline. It proves the run needs no network, and it fails fast and loudly instead of silently pulling multi-gigabyte CUDA wheels. - Use
--device cuda:0, the default. On a multi-GPU host, torch andnvidia-smican enumerate devices in opposite order. Do not translate annvidia-smiindex into a--deviceflag.qwen_narrate_longform.pyguards on the device name and refuses to run on the wrong card, so a mismatch shows up asRefusing to run on unexpected CUDA device.
The invocation, run from a work directory holding the script, the reference pair, and the chunk JSON:
uv run --offline --python 3.12 \
--with qwen-tts==0.1.1 --with torch --with soundfile --with numpy \
python qwen_narrate_longform.py \
--model ~/models/Qwen3-TTS-12Hz-0.6B-Base \
--reference aaron_reference.wav \
--reference-text aaron_reference.txt \
--chunks <post-slug>.json \
--output-dir out --output-name narration.wav \
--language English --device cuda:0 --seed <YYYYMMDD> --resume
Warning: flash-attn is not installed is expected and harmless; every existing
post was rendered on the same manual PyTorch path.
Then encode and place the file:
ffmpeg -i out/narration.wav -ac 1 -ar 24000 -b:a 96k \
-af loudnorm=I=-16:TP=-1.5:LRA=11 audio/posts/<post-slug>.mp3
Chunk JSON is a list of objects with id, kind, stanza, text, and
pause_after_ms. Useful pause values: 900 ms after a heading, 700 ms after a
definition blockquote, 500 ms after a body paragraph.
Do not purge $GPU_RENDERING_BOX_NAMED_AFTER_CARTOON_DOG’s uv cache to
reclaim disk. It is tens of
gigabytes and looks like throwaway download cruft, but it is the renderer:
the cached CUDA torch build and qwen-tts wheel are what make an offline run
possible. Clearing it breaks narration for every future post and forces a
multi-gigabyte re-download. Reclaim space somewhere else.
Scrub exception — the unlisted fleet page
Every post gets real IPs/hostnames/secrets scrubbed before publishing, with ONE
exception: fleet-local-inference-plan.html is an unlisted (noindex) page where
real internal infra detail (IPs, hostnames like pluto/telesto/ronin28, ports, GPU
UUIDs, file paths) is owner-authorized for teaching value. Never publish live
secrets even there. This exception is that one file only.