Skip to content

Voice Profile

How vita knows what bio-zack sounds like, and how that knowledge gets re-grounded.

Not to be confused with

  • voice-location.md β€” the iOS voice assistant (speech in, TTS out). Unrelated.
  • flows/sleep/voice_drift_audit.py β€” audits the feed composer's content personas, not bio-zack's communication profile.

What it is

profile/communication.json is the canonical description of bio-zack's writing: length, capitalization, register by medium and modality, and the small set of words that are measurably out of distribution. A compressed version is injected into CLAUDE.md between VOICE_PROFILE_START / VOICE_PROFILE_END markers, so it rides into every agent invocation (~1,151 tokens of a ~8,045-token CLAUDE.md).

   live corpora                scripts/voice/            profile/                 CLAUDE.md
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ slack          β”‚          β”‚ corpus.py    β”‚         β”‚                  β”‚      β”‚ ## voice   β”‚
β”‚ telegram (in)  β”‚  ──────► β”‚ patterns.py  β”‚ ──────► β”‚ communication    β”‚ ───► β”‚ (between   β”‚
β”‚ claude cli     β”‚          β”‚ vocabulary.pyβ”‚         β”‚ .json            β”‚      β”‚  markers)  β”‚
β”‚ signal         β”‚          β”‚ synthesize   β”‚         β”‚                  β”‚      β”‚            β”‚
β”‚ interviews     β”‚          β”‚ validate     β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚ integrate    β”‚                  β”‚
                            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β”‚
                                                    sync_claude_md.py

Refreshing it

Interactive only. Run /voice-refresh (skill: .claude/skills/voice-refresh/). bio-zack runs this to see how his language is or isn't changing and to decide what the profile should say. Nothing rewrites the profile autonomously.

Cadence is quarterly. More often and there isn't enough new writing to move a number.

Staleness

uv run python -m scripts.voice.staleness           # human-readable
uv run python -m scripts.voice.staleness --json    # machine-readable
uv run python -m scripts.voice.staleness --nudge   # upsert heartbeat reminder
uv run python -m scripts.voice.staleness --stage   # write sleep staging file

Two triggers, either fires:

Trigger Threshold
Age 90 days since synthesized_at
Unseen corpus 5,000+ messages written since

Volume is the better signal β€” a quiet quarter matters less than a loud month.

Wired into /sleep as the voice_profile_staleness step (flows/sleep/voice_profile_staleness.py), which writes data/sleep/staging/voice-profile-staleness.json and upserts a heartbeat topic when stale. The step surfaces staleness; it never refreshes.

Corpus traps

Every one of these was measured 2026-08-06. Each produces a confidently wrong profile if missed β€” this table is the most important thing on the page.

Trap Reality Handling
Slack duplication 7.22x β€” 502,015 raw rows vs 69,493 unique for sender=="zack" dedup by id (issue 3acb7c7c)
Claude sessions are mostly machine 30,523 raw user rows β†’ ~1,655 human keep entrypoint == "cli"; drop sdk-cli/sdk-ts daemon invocations
Telegram is half dictation 10.5% of messages, 50.1% of words split typed vs dictated before any writing claim
Agent-drafted paste ~1.2% slack, 4.7% telegram messages heuristic flag; lowercase-start veto kills most false positives
Assistant output direction=="out" is silicon-zack never ingest β€” teaches the model its own voice as his
Medium-mix confounding 2019 telegram-heavy, 2026 slack-heavy report drift within a medium; control by fixed recipient

Profile shape (v3)

Field Purpose
by_modality typed vs dictated β€” where the variance actually is
by_medium chat / email / dictated
by_recipient kept thin; recipient explains far less variance than modality. Retained because scripts/generate_agent_prompts.py:94 consumes it
vocabulary.words_to_avoid word β†’ measured evidence, not a bare list
vocabulary.register_gap where assistant output diverges from his
corpus_stats provenance including dedup/filter decisions

Durable preference

bio-zack does not want an emphasized words-to-use / words-to-avoid list (2026-08-06). Those lists were assistant-authored, not his. Include a word only on measured out-of-distribution usage or non-usage. The v2 list was actively harmful: systematic had 0 uses in 245k words of his writing, and leverage was listed as his while the assistant used it 12x more than he did.

Consumers

File Uses
scripts/voice/sync_claude_md.py compresses profile β†’ CLAUDE.md
scripts/voice/prompt_builder.py builds voice prompt fragments
scripts/proactive/composer.py telegram message composition
scripts/generate_agent_prompts.py per-agent prompt generation (by_recipient)
scripts/memory/build_view.py, scripts/hooks/post_compact.py context rebuilds

Gotchas

  • sync_claude_md.py targeted a nonexistent .claude/CLAUDE.md for months β€” the profileβ†’CLAUDE.md leg was dead and edits reached nothing (fixed 2026-08-06). It now fails closed when markers are missing rather than guessing an insertion point. Verify the rendered block after every sync; don't trust the "Done!" line.
  • Writing the profile: use atomic_write(path, dict) from scripts.utils β€” it refuses a pre-serialized JSON string.
  • The ambient block rides in every invocation. If it grows, move conditional slices to on-demand loading rather than letting the always-on core bloat.

History

Date Change
2026-01-06 v2 synthesized from hand-staged exports (98k messages, 2018-2026)
2026-08-06 v3 measured re-grounding: live corpora, by_modality added, prescriptive word list dropped, sync leg repaired. Evidence: data/reports/2026-08-06-voice-profile-refresh.md
2026-08-06 LLM write-path (synthesize β†’ validate β†’ integrate) retired; ambient block cut 1,151 β†’ 550 tokens; conformance gauge added

Retired 2026-08-06 β€” the LLM write-path

scripts/voice/synthesize.py, validate.py, integrate.py and the matching CLI subcommands are gone. They generated a profile draft with an LLM, scored it, and merged it into communication.json automatically.

Why:

  • Dormant since January. Never run in seven months.
  • It rejected the live profile. validate_profile() scored v3 at 0.2 with schema_valid: False, because its required-key schema still demanded v2 fields (signature_phrases, words_to_use, by_medium.spoken, by_modality.written…). integrate.py gated writes on that score, so the write-path could not run at all.
  • A worse instrument than the replacement. /voice-refresh does the same job as an interactive session where bio-zack sees the evidence and makes the calls β€” which is the point, since the profile describes him.
  • A validator that silently rejects the live profile is worse than no validator.

There was a trap here worth remembering: cli validate validated data/voice/synthesis_draft.json β€” a stale January draft β€” not profile/communication.json. Running it returned a healthy "0.900 / Valid: True" while the live profile actually scored 0.2. The green light was measuring the wrong file.

Kept: parse, patterns, vocab, stats, sample. Measuring is still useful; auto-writing the profile from an LLM draft is not. analyze now runs the measurement legs only. Code is in git history if it's ever wanted back.