Repository Instructions

Repository Instructions

This repository is CryptoJones’s personal blog site.

Git workflow

  • Direct commits to master or main are explicitly authorized for this repository.
  • Push completed changes directly to the corresponding origin/master or origin/main branch; 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 hasIssuesEnabled returns false). There is nothing to mirror, so a BACKLOG.md here could never stay in sync with anything.
  • Do not create BACKLOG.md in 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.md in 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:

  1. Markdown at _posts/YYYY-MM-DD-Title-With-Dashes.md, eighth-grade reading level, no em dashes. See Writing.
  2. A description field in the front matter. See Index descriptions.
  3. Narration rendered and encoded to audio/posts/<post-slug>.mp3.
  4. audio: /audio/posts/<post-slug>.mp3 added to the front matter.
  5. 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:

  1. Rough draft. Write it in the scratchpad, not in _posts/. Check every factual claim and reference with Perplexity while drafting.
  2. Panel review. Send the rough draft to every Flatline Roundtable lane (the flatline-roundtable skill, 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.
  3. 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.
  4. 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.
  5. 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.sh blocks any write to _posts/ or audio/posts/ until AGENTS.md has actually been read in the current turn. It watches Bash as well as Write/Edit, because heredocs and sed -i are writes too.
  • .claude/hooks/post-preflight.sh resets 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 description field 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.html must display post.description when 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.

  1. Write a plain-prose script at ~/chatterbox/texts/<Slug>.txt on 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).
  2. Copy the newest ~/chatterbox/render_*.sh (e.g. render_goc.sh) to render_<slug>.sh and change only the --text, --out, --transcript and --seed <YYYYMMDD> lines. The script pins the V100 by GPU UUID, sets HF_HUB_OFFLINE=1, stops strata-flashnext, renders with narrate_verified.py, and always restarts Strata via trap EXIT. Nothing needs to be installed or downloaded on pluto.
  3. 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 with ListAgents and 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.
  4. 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 for STRATA_RESTARTED in the log. Several renders can be queued back to back in one window so ReCLamO only pauses once.
  5. After the render: confirm systemctl --user is-active strata-flashnext and that :8083 /v1/models answers (401 without the key still means it is up), then message ReCLamO “back”. If Strata does not come back, say so instead.
  6. Check the Whisper read-back (audio/<slug>.heard.txt, summary line rendered ... N weak in the log). WEAK chunks 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.
  7. 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>.mp3 and add the audio: 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.

  1. 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 times for 7x).
  2. 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.
  3. 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
  1. The result lands in /mnt/hdd1/feynman/outbox/<Slug>/<Slug>.mp3 with a <Slug>.heard.txt Whisper read-back; every chunk should be at or above 0.95.
  2. 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 the audio: 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 ~/chatterbox on pluto: its own venv, narrate_verified.py, texts/, and a render_<slug>.sh that pins the 3060 and runs with HF_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.service holds ~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, run FLASHNEXT_OK=1 ~/chatterbox/render_<slug>.sh on 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/chatterbox venv. Input is a plain-prose .txt (blank-line separated paragraphs) at work/blog-narrations/texts/<Slug>.txt; output is the published master format, so it drops straight into audio/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:

  1. perth 1.0.0 exports PerthImplicitWatermarker as None, and ChatterboxTTS.__init__ calls it, so a bare import dies with 'NoneType' object is not callable. The script patches it to perth.DummyWatermarker before importing chatterbox.
  2. 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_penalty is held at 1.4.
  3. 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.3 reads slower and flatter, which is the pacing CJ prefers for narration. Reference clip is the script’s default, dataset/reference/aaron_reference.wav in the voicemodel repo. The older aaron-ref-canonical.wav was lost in ronin28’s 2026-09 NVMe crash and has no surviving copy.

ronin28 was rebuilt 2026-09-26: the login user is akclark, not hermes, 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/VibeVoiceCommunity with a .venv. Model cache holds VibeVoice-1.5B (fits the 4060’s 8 GB) and VibeVoice-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 .txt with Speaker 1: <text> lines, one per paragraph.

THE TRAP that cost hours (2026-08-14): demo/inference_from_file.py does an EXACT string compare args.device == "cuda". Passing --device cuda:0 does 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, never cuda: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>.mp3

The 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-voicemodel repository:
    • qwen_narrate_longform.py
    • dataset/reference/aaron_reference.wav
    • dataset/reference/aaron_reference.txt
    • validate_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 contain id, text, stanza, and pause_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 --resume so 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=11 filter.
  • Save the final file as audio/posts/<post-slug>.mp3 and add audio: /audio/posts/<post-slug>.mp3 to 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:

  1. Pin --python 3.12. uv defaults to a newer CPython on $GPU_RENDERING_BOX_NAMED_AFTER_CARTOON_DOG, and the cached torch wheels stop at the cp312 ABI. 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.
  2. Pass --offline. It proves the run needs no network, and it fails fast and loudly instead of silently pulling multi-gigabyte CUDA wheels.
  3. Use --device cuda:0, the default. On a multi-GPU host, torch and nvidia-smi can enumerate devices in opposite order. Do not translate an nvidia-smi index into a --device flag. qwen_narrate_longform.py guards on the device name and refuses to run on the wrong card, so a mismatch shows up as Refusing 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.