| name | explain |
| description | Personalized explanation engine. Re-explains the previous assistant turn (or a specified concept) by tying it back to projects you have already built. Memory grounding is the core feature. After the text lands, the skill speaks the same explanation back in your cloned voice using a local Qwen3-TTS install (no cloud, no API keys).
Trigger only on explicit asks. The user types `/explain` (with or without an argument), or says things like "explain what you just did", "what do you mean by X", "how does this work", "explain this pattern", "I don't get it", "explain like I'm implementing it". Do NOT auto-trigger on vague signals or perceived confusion.
|
Explain
Memory lives in $EXPLAIN_MEMORY_DIR (default:
~/.claude/projects/-Users-<username>/memory/, matching Claude Code's
auto-memory naming). This skill grounds every new concept in that
memory so explanations become "this is like the thing you built in
project Y" instead of generic textbook definitions. After the text
response is complete the skill speaks the same explanation in your
cloned voice via a local Qwen3-TTS install on Apple Silicon.
When to invoke
Invoke when the user types /explain (with or without args), or uses
one of the natural-language triggers above. Never invoke on vague
signals. The user has explicitly said they do not want auto-detected
explanations.
Input modes
| User input | Behavior |
|---|
/explain | Explain the previous assistant turn in full structured format |
/explain <term> | Focus on <term> inside recent context |
quick explain or tldr explain | Sections 1 and 2 only, both short |
explain deeper | Full format, expanded mental model |
explain like I'm implementing it | Add a sixth section with concrete build steps and gotchas |
explain with my past projects | Force section 3 even when the link is weak (label it "weak link") |
/explain --no-voice, silent explain, mute | Skip TTS for this turn |
Procedure
All commands below run from this skill's base directory (the directory
that contains this SKILL.md).
-
Identify the target. Quote one line of the previous assistant
turn so the user can confirm what is being explained.
-
Pull memory. Run:
python3 ./scripts/retrieve_memories.py --query "<concept keywords>"
The script reads YAML-frontmatter memory files (curated + auto-indexed)
and returns ranked JSON: name, type, description, body, score.
Use it to find honest analogies. For a pre-formatted markdown context
block use ./scripts/build_explanation_context.py with the same flags.
Memory sources scanned (all under $EXPLAIN_MEMORY_DIR, default
~/.claude/projects/-Users-<username>/memory/):
*.md at the root (curated entries)
auto/obsidian_*.md (Obsidian vault notes, if indexed)
auto/local_*.md (project dirs scanned from the filesystem)
auto/github_*.md (GitHub repos via gh)
auto/captured_*.md (snapshots from the capture-project skill)
After picking the primary match, also consult
$EXPLAIN_MEMORY_DIR/auto/connections.md for that match's top
neighbors. That is how you surface secondary parallels like "you
used the same shape in X and Y." Regenerate it periodically with
./scripts/build_connections.py.
-
Compose explanation in five sections (see
references/explanation_style.md):
- Plain meaning
- In this exact context
- Your past-project connection (skip honestly if no real link)
- Mental model
- Next time you see this
Never invent forced analogies. If the retriever returns nothing
useful, say so in section 3 and skip it.
-
Speak it. After the text response is shown, invoke TTS in the
background so playback does not block the next turn. Use
run_in_background=true on the Bash call:
python3 ./scripts/tts_qwen3.py --text "<full explanation, plain text>" --play
--play is the default. The script sends the full cleaned text as
ONE synth request to the warm daemon, gets back an mp3 path, and
plays it via afplay. End-to-end smooth, no inter-sentence gaps.
TTFA equals total synth time of the full explanation (~10-20s for
a typical multi-sentence explanation on a warm 1.7B model).
Why one call instead of chunked + pipelined? The 1.7B Qwen3-TTS
model has RTF ~0.5x on M-series. Synthesis is slower than playback,
so chunked pipelined playback inserts ~2-3s gaps between sentences
while the next chunk synthesizes. Single-call synth waits longer
for the first sound but flows cleanly once it starts.
Closest prior art (tcmartin24/claude-tts) reaches the same
conclusion: persistent subprocess for warm model + full-synth-then-
play for smoothness.
The daemon (scripts/tts_daemon.py) loads the MLX model once and
stays warm. First call of a session auto-spawns it (~5-10s cold
start) and pays the full synth time. Subsequent calls hit the warm
daemon directly.
Flags for experimentation:
--chunked forces chunked + pipelined playback (faster TTFA, has gaps).
--stream uses the daemon's streaming mode (smooth only if model RTF ≥ 1.0; e.g., a 0.6B variant set via EXPLAIN_TTS_MODEL).
Battery-aware lifecycle: idle timeout 15 min on AC, 5 min on
battery, auto-exits below 20% battery. Manage via
./scripts/daemon_ctl.sh {start,stop,status,tail}.
Skip this step when the user passed --no-voice, said silent,
or said mute. If EXPLAIN_TTS_DIR or EXPLAIN_VOICE_NAME is
unset the script exits non-zero with a clear message and no audio
plays. That is fine; the text was the point.
The script strips markdown/code blocks before chunking so audio
reads naturally.
-
Capture feedback. When the user replies "that analogy worked"
or "no, relate it more to project X", append the signal to
references/memory_schema.md (or to the matched memory file's
analogy_hooks field) so future runs adjust which memories
surface.
Stop or interrupt audio
If the user asks a new question while audio is playing, stop the player:
pkill -f tts_qwen3.py 2>/dev/null; pkill afplay 2>/dev/null
Run this whenever a new turn begins and the previous turn had voice on.
Cheap and safe; ignores missing processes.
Privacy contract
- Memory contents stay local.
- TTS runs entirely on-device. No network calls, no API keys, no third
party ever sees the explanation text or the voice samples.
- The reference voice clip cloned from never leaves disk; only the
registered voice name is referenced at synthesis time.
Streaming roadmap
Today the full explanation text streams to chat first, then audio
synthesizes and plays. Real-time narrated streaming (chunk text by
sentence, queue ordered audio chunks, support interrupt) is the next
iteration. Architecture sketch in references/tts_providers.md.
Files
scripts/retrieve_memories.py: memory loader and relevance ranker.
scripts/build_explanation_context.py: formats retriever output as a markdown context block.
scripts/build_connections.py: regenerates the cross-project connections map.
scripts/tts_qwen3.py: streaming TTS client. Strips markdown, chunks text, talks to the daemon, plays audio in order.
scripts/tts_daemon.py: persistent MLX server. Loads Qwen3-TTS once, serves synth requests over a Unix socket, idle/battery-aware shutdown.
scripts/daemon_ctl.sh: start/stop/status/tail for the daemon.
references/explanation_style.md: the five-section format with examples.
references/memory_schema.md: optional richer memory metadata and feedback storage.
references/tts_providers.md: adapter contract and streaming roadmap.
Install
Wire the cloned repo as /explain in Claude Code with:
ln -s /path/to/explain-eg ~/.claude/skills/explain
One-time TTS setup on Apple Silicon. Clone the upstream repo anywhere
on disk, then point $EXPLAIN_TTS_DIR at it:
git clone https://github.com/darrenoakey/tts /your/preferred/path
cd /your/preferred/path
./run install # creates venv, downloads ~5GB model
./run clone-voice myvoice /path/to/your_voice_sample.wav
Then export the env vars (in ~/.zshrc or wherever you keep them):
export EXPLAIN_TTS_DIR=/your/preferred/path
export EXPLAIN_VOICE_NAME=myvoice
# optional, override memory location:
# export EXPLAIN_MEMORY_DIR=$HOME/.claude/projects/-Users-$(id -un)/memory
Re-source the shell and /explain works in any Claude Code session
with audio in your own voice.