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.pytargeted a nonexistent.claude/CLAUDE.mdfor months β the profileβCLAUDE.mdleg 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)fromscripts.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 withschema_valid: False, because its required-key schema still demanded v2 fields (signature_phrases,words_to_use,by_medium.spoken,by_modality.writtenβ¦).integrate.pygated writes on that score, so the write-path could not run at all. - A worse instrument than the replacement.
/voice-refreshdoes 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.