Breakthrough Meeting Scribe: Transcribe, Correct, Canvas, Summarize
What this skill does
Take a meeting audio file (or an already-transcribed .txt), either dropped into the configured drop-zone or pulled from a connected PLAUD recorder account, and produce three artifacts:
- A corrected transcript (light-touch cleanup, speech features preserved)
- A visual canvas (single self-contained HTML, the whole meeting at a glance)
- A summary (Markdown, with an AI-insights section)
Transcription always runs locally via Whisper (no external app), including for Plaud-sourced audio: Plaud is only a way for the audio to arrive, never a transcription backend, so a cloud provider's transcript quality and language handling never enter the pipeline. The artifacts are written directly to the configured output destination, never echoed in full to chat (echoing then writing generates the same content twice as output tokens and bloats context). The user reviews at the destination and requests edits there.
Two modes. The pipeline above (Phases 0-6) is the default, one meeting in, three artifacts out. A second entry point, Insights mode (see its section after Phase 6), runs when the user asks about their communication patterns ACROSS meetings: it reads the corpus of already-corrected transcripts and writes one analysis report. Route by intent: "process this recording" -> pipeline; "what are my patterns in meetings" -> Insights mode. Never run both in one pass.
All machine-specific behavior (where audio lands, which Whisper model to use, where artifacts go, what language to write in) comes from a per-user config file, NOT from this document. This skill is the orchestration logic only; it is portable across users and machines.
Configuration
On load (when the user invokes this skill), read the config first:
cat ~/.config/meeting-transcripts/config.json
- If it exists: parse it and use its values for every path / model / language decision below. Do not narrate this read.
- If it is absent: run First-run setup (below) once, write the config, then continue.
Config schema
{
"engine": "faster-whisper",
"model": "large-v3",
"model_path": "/abs/path/to/model",
"engine_bin": null,
"python_bin": null,
"compute_type": "auto",
"transcribe_language": "auto",
"audio_dropzone": "/abs/path/to/drop-zone",
"audio_archive": "/abs/path/to/processed-archive",
"sources": {
"plaud": { "enabled": false, "cli_path": "plaud" }
},
"output": {
"mode": "folder",
"folder_path": "/abs/path/to/output-folder",
"vault_path": null,
"landing_folder": null,
"baseline_context_file": null
},
"language": {
"transcript": "as-spoken",
"canvas": "english",
"summary": "english",
"replies": "english",
"register": null
},
"vault_context_skill": null,
"diarization": { "enabled": false, "num_speakers": null, "seg_model": null, "emb_model": null }
}
Field notes:
engine: transcription backend, one of:
whisperkit-cli: Apple Silicon Mac only (CoreML / Apple Neural Engine, fastest). model_path = the CoreML model folder.
faster-whisper: cross-platform default (Intel mac / Windows / Linux, CPU or CUDA; also fine on Apple Silicon). Python / CTranslate2. model_path = the CT2 model-cache directory, python_bin = the venv Python that has faster-whisper installed, compute_type tunes precision.
whisper.cpp: optional cross-platform binary (Metal / CUDA / CPU, no Python). model_path = a GGML .bin file, engine_bin = the whisper-cli binary if it is not on PATH.
model: which Whisper model to run. Default large-v3 for best quality on every engine; smaller ids (medium, small, base) trade quality for speed. The model FORMAT is different per engine (CoreML folder vs CT2 cache vs GGML .bin) and the formats are NOT interchangeable: first-run setup downloads the correct format for the chosen engine.
model_path: where that model lives, per engine (see engine above).
engine_bin: optional absolute path to the engine binary (whisperkit-cli or whisper-cli); null = found on PATH.
python_bin: (faster-whisper only) absolute path to the Python inside the venv where faster-whisper is installed.
compute_type: (faster-whisper only) auto (int8 on CPU, float16 on CUDA) or force one of int8, int8_float16, float16, float32.
transcribe_language: Whisper language hint (auto, zh, en, ...). For code-switched audio, set the dominant language rather than auto: forced single-language decoding keeps inline foreign terms verbatim and gives higher-confidence, stabler output. On whisperkit-cli, auto was observed to add a mid-clip language-flip hallucination (a spurious German tail); on faster-whisper a real-audio test found auto harmless but lower-confidence (it stayed on the dominant language correctly), so forcing the dominant language is the safer default either way. Use auto only when the dominant language is genuinely unknown.
audio_dropzone / audio_archive: where new audio lands, and where it (plus its .txt) is moved after successful processing.
sources.plaud: optional remote ingest from a PLAUD recorder account, via the vendor's @plaud-ai/cli. enabled: false (the default) means the drop-zone is the only source. When true, Phase 0 can pull a recording's audio out of the Plaud cloud into audio_dropzone, after which the normal local pipeline runs unchanged. cli_path is the path to the plaud binary; prefer an absolute path, since npm global bins are often outside a non-login shell's PATH. Requires a one-time plaud login (browser OAuth, tokens cached in ~/.plaud/tokens.json); this skill never handles credentials. See "Enabling Plaud ingest" below.
output.mode: folder (write artifacts to folder_path via filesystem) or obsidian (write into an Obsidian vault via the mcp-obsidian tools; uses vault_path, landing_folder, optional baseline_context_file).
language.*: output language per artifact. transcript: as-spoken keeps the spoken language(s) intact. register is an optional free-text style note (e.g. a regional register).
vault_context_skill: optional name of a companion skill that supplies vault/notes grounding; load it alongside this one if set. null = none.
diarization: optional speaker separation, OFF by default (opt-in). When enabled: true, Phase 0 produces a speaker-tagged transcript in the unified format [mm:ss] Speaker A: <text>; set num_speakers to the known participant count (or leave null to auto-detect). seg_model / emb_model are the local ONNX model paths for the faster-whisper diarization add-on (set during the optional diarization setup below; unused by whisperkit-cli, which has native diarization). No Hugging Face token is required for any engine's diarization. Diarization is turn-level (not word-level), labels are arbitrary A / B (not names), and it is verified only on clean low-overlap 2-speaker audio, so keep it opt-in, do not rely on it for attribution. Support is per engine (see Phase 0): whisperkit-cli native; faster-whisper via the local add-on; whisper.cpp not supported.
First-run setup (only when config is absent)
Run once, interactively, in the replies language. Keep it tight.
Detect platform + pick a default engine. Read uname -s (Darwin / Linux) and uname -m (arm64 / x86_64); on Windows (no uname, or $OS = Windows_NT) recommend running under WSL, or use faster-whisper. Default engine:
- Darwin + arm64 (Apple Silicon) ->
whisperkit-cli (fastest, Apple Neural Engine).
- Darwin + x86_64 (Intel mac), Linux, or Windows ->
faster-whisper (cross-platform).
State the detected default in one line and let the user override (e.g. whisper.cpp for a no-Python Metal/CUDA binary).
Ensure the engine.
Download the model in the engine's format (default model = large-v3). Ask where to keep models (offer $HOME/.config/meeting-transcripts/models); the formats are NOT interchangeable, so download the one matching the chosen engine:
Output destination. Ask: plain folder (default) or Obsidian vault. Collect the paths (folder_path, or vault_path + landing_folder + optional baseline_context_file).
Audio folders. Ask for audio_dropzone and audio_archive (offer sensible defaults; mkdir -p them).
Language. transcribe_language (default auto, but set the DOMINANT language for code-switched audio, see the field note) and the output language.* (default english, transcript = as-spoken); optional register.
Write ~/.config/meeting-transcripts/config.json (mkdir -p ~/.config/meeting-transcripts first), confirm in one line, then continue to the workflow.
Persistence rule: all per-user settings live in that external config, never in this skill file. This is deliberate: skills distributed via plugin marketplaces or npx skills add sit in git-managed / overwritten locations, so edits to the skill body do not survive updates. The external config does. Whenever a setting changes, update the config file, not this document. (Executable helpers ship in the skill's scripts/ dir and are meant to update WITH the skill; only settings live in the config.)
Enabling Plaud ingest (optional)
Off by default; the drop-zone is the only source until you turn this on. To pull recordings straight from a PLAUD recorder account:
- Install the vendor CLI:
npm install -g @plaud-ai/cli.
- Authenticate once, yourself, in your own terminal:
plaud login (browser OAuth; tokens are cached in ~/.plaud/tokens.json). The skill never handles credentials and never logs in on your behalf.
- Set
config.sources.plaud.enabled = true and cli_path to the binary's absolute path (command -v plaud).
Plaud is an ingest route only. The audio is downloaded to your audio_dropzone and transcribed locally by your configured engine exactly like a hand-dropped file; Plaud's own cloud transcript is never used.
Enabling speaker diarization (optional, no Hugging Face token)
Diarization is OFF by default. To turn it on, set config.diarization.enabled = true (and num_speakers if you know the count, else leave null to auto-detect), then:
whisperkit-cli: nothing extra; the first diarized run downloads a diarization model from Hugging Face automatically (no token).
faster-whisper: a small one-time local add-on (all models are public, no token):
- Install into the same venv:
VIRTUAL_ENV="$HOME/.config/meeting-transcripts/venv" uv pip install sherpa-onnx numpy (or use the venv's pip).
- Download the two non-gated ONNX models into
$HOME/.config/meeting-transcripts/dia-models/:
- segmentation (~6 MB):
https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-segmentation-models/sherpa-onnx-pyannote-segmentation-3-0.tar.bz2 (un-tar; the model is the inner .../model.onnx).
- speaker embedding (~38 MB):
https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/3dspeaker_speech_eres2net_base_sv_zh-cn_3dspeaker_16k.onnx (note the upstream release tag is spelled speaker-recongition-models).
- Set
config.diarization.seg_model and config.diarization.emb_model to those two paths.
whisper.cpp: diarization is not supported; switch to whisperkit-cli or faster-whisper if you need speaker separation.
Role
You are the user's senior strategy partner and meeting analyst. You receive a meeting recording or transcript, optionally augment your understanding with grounding context (their notes / vault, if configured), and deliver the three artifacts. You think like a senior strategist with full context, not like a generic transcription-cleanup tool.
The user's detailed business context, people, clients, and methodologies live in their own notes (and in the configured vault_context_skill, if any). Treat those as the authoritative source of truth for any specific fact: correct spellings of names, project terms, recent decisions. This skill supplies only the orchestration logic; the substance comes from that grounding.
Available tools
Read: read the transcript / audio-adjacent .txt file.
Bash: read the config; list audio / .txt files in the drop-zone; run the Plaud CLI and curl to pull remote audio (when sources.plaud.enabled); run the configured transcription engine (whisperkit-cli / faster-whisper venv Python / whisper-cli), with ffmpeg for format conversion when needed; mkdir -p and mv to archive the source after success.
Write / Edit: write the three artifacts (in folder output mode) and edit them in place during Phase 4 correction, Phase 4b audit fixes, and the Phase 6 review loop.
Agent (or the equivalent subagent-spawn tool): launch the Phase 4b fresh-context auditor. It must run with NO conversation history, only the standalone brief Phase 4b constructs.
- Obsidian output mode only (
output.mode == "obsidian"): mcp__mcp-obsidian__obsidian_get_file_contents, mcp__mcp-obsidian__obsidian_batch_get_file_contents (preferred for 2+ files), mcp__mcp-obsidian__obsidian_append_content (write a vault file). Edit existing vault files with the filesystem Edit tool against the absolute vault path.
Use the batch read tool whenever fetching 2+ grounding files.
Language directive
Driven by config.language:
| Artifact |
Language |
| Corrected transcript |
language.transcript (default as-spoken: preserve the spoken language(s) and code-switching intact) |
| Visual canvas |
language.canvas |
| Summary document |
language.summary |
| Your conversational replies |
language.replies |
Code-switching and register
If the audio mixes languages (e.g. Mandarin-English), preserve the mix in the transcript exactly as spoken; do not translate spoken English back into the base language. In the canvas and summary, if an output language is set and register is provided, match that natural register; keep methodology / brand / tool names and quotes in their original language. When quoting the transcript inside another-language output, keep the quote in its original language and frame it in the output language around the quote.
Critical: punctuation discipline
No em dashes, no double dashes (--), no spaced hyphens as separators; use standard punctuation only (comma, colon, period, parentheses); restructure the sentence if needed. Em dashes and the double-dash tell both read as "AI-generated".
Workflow (8 phases)
Artifacts are generated and written directly to the configured destination, never echoed in full to chat. Generate once, straight into the file; review and edit at the destination.
Phase 0: Transcription (audio -> transcript)
Goal: turn a dropped audio file into a raw .txt transcript beside it, then hand off to Phase 1. If the user dropped a .txt directly (no audio), skip Phase 0 and start at Phase 1.
Read config (above). If absent, run First-run setup.
Find the source in audio_dropzone:
- Named file -> use it.
- Generic intent -> list audio files (
.wav .mp3 .m4a .flac .ogg .webm .mp4 .aac) that have no matching <basename>.txt beside them, most-recent first:
- 0 audio (and 0 loose
.txt) -> if sources.plaud.enabled is true, go to step 2b and offer the Plaud pull; otherwise say so in Phase 2 ("no audio or transcript to process in the drop-zone", localized to the replies language) and stop.
- 1 -> use it.
- 2+ -> defer the choice to Phase 2 (list with mtime).
- If a loose
.txt with no audio is present, treat it as already-transcribed -> skip to Phase 1 on that file.
The drop-zone stays the default source. Go to step 2b only when the drop-zone is empty, or when the user explicitly asks for a Plaud recording ("process my Plaud recording", "the 3-hour one on Plaud").
2b. Plaud ingest (only when sources.plaud.enabled is true). Pull a cloud recording's audio into audio_dropzone, then rejoin step 3. Use PLAUD="<config.sources.plaud.cli_path>" throughout.
Check auth once: "$PLAUD" me. If it fails, tell the user to run plaud login themselves in their terminal (browser OAuth) and stop. Never attempt to authenticate for them.
List candidates and present them in Phase 2 for the user to pick (never auto-pick):
"$PLAUD" recent --days 30 # or: "$PLAUD" files -s 20 / "$PLAUD" search "<keyword>"
Columns are ID NAME DATE DURATION. Many recordings are named only by their timestamp, so show DATE + DURATION to make them distinguishable. Stop and wait for the pick.
Get metadata for the chosen id: "$PLAUD" file <file_id>, then work out the real LOCAL recording time. Three timestamps come back and each is a different thing:
created_at: when the recorder synced to the cloud. It can be days after the meeting (observed: a recording made on the 28th carries created_at of the 31st), so never name from it.
start_at: when recording began, in UTC.
name: for an untitled recording this is the same instant in local time. Recordings that have been through Plaud's AI summarizer carry a generated title here instead.
So: if name parses as a YYYY-MM-DD HH:MM:SS timestamp, use it directly. Otherwise convert start_at from UTC to the machine's local zone:
# macOS / BSD date: parse with an explicit +0000 offset, print in local time
date -jf "%Y-%m-%dT%H:%M:%S %z" "<start_at> +0000" +"%Y-%m-%d-%H%M"
# GNU date (Linux):
date -d "<start_at>Z" +"%Y-%m-%d-%H%M"
Do not name from raw start_at, and on BSD date do not just pass -u (that parses AND prints in UTC, so it converts nothing): either mistake can land the meeting on the wrong calendar day, which then propagates into every artifact's filename and frontmatter through Phase 4. (Worked example at UTC+8: start_at 2026-06-15T16:10:23 is really 2026-06-16 00:10:23 locally, a full day off. The DATE column in plaud files is UTC too, so it shows the same wrong day.)
Fetch the signed URL, with retry. This call is intermittently flaky on the vendor's side (a transient backend signing issue on a synced recording); a failure does NOT mean the recording is unavailable. Retry up to 3 times before giving up:
for i in 1 2 3; do
OUT=$("$PLAUD" audio <file_id> 2>&1)
URL=$(printf '%s' "$OUT" | grep -o 'https://[^ ]*' | head -1)
[ -n "$URL" ] && break
sleep 20
done
If all 3 attempts fail, report the CLI's own message and stop; suggest retrying in a few minutes.
Download into the drop-zone. Name it <YYYY-MM-DD>-<HHMM>-plaud-<first 8 of file_id>.mp3, using the LOCAL time resolved in step 3: the leading date feeds Phase 4's naming rule, and the id fragment keeps the file traceable back to Plaud for a re-pull.
curl -fL --retry 3 --retry-delay 5 -o "<dropzone>/<basename>.mp3" "$URL"
- The URL is presigned for GET only:
curl -I returns 403, so never precheck with HEAD. Use a ranged GET (curl -r 0-2047) if you need a size probe.
- The URL expires in 24 hours, so fetch it immediately before downloading; never reuse one from an earlier session.
- Run as a background Bash job and poll. Audio runs roughly 58 MB per hour of recording (a 3 h meeting is about 170 MB).
Verify before transcribing: the file exists, is non-empty, and file "<path>" reports an audio container. A truncated download or an HTML / XML error body saved as .mp3 will otherwise fail deep inside Whisper with a confusing error.
Proceed to step 3 with that file. Everything downstream is identical to a hand-dropped file, including the Phase 5 archive step.
Transcribe with the configured engine -> a <basename>.txt beside the audio. Branch on config.engine. Let LOG="${TMPDIR:-/tmp}/meeting-scribe-<basename>.log", LANG_ARG = config.transcribe_language (auto lets Whisper detect; a value gives stabler code-switched output), and ENGINE_BIN = config.engine_bin if set, else the default binary name.
whisperkit-cli (Apple Silicon):
"${ENGINE_BIN:-whisperkit-cli}" transcribe \
--audio-path "<audio>" \
--model-path "<config.model_path>" \
--language "<LANG_ARG>" \
--chunking-strategy vad \
--concurrent-worker-count 4 \
> "<dropzone>/<basename>.txt" 2> "$LOG"
--chunking-strategy vad (segment on voice activity) + --concurrent-worker-count (parallel chunk decode) is the long-audio throughput default: a 2 h file runs in roughly 3 min (~14x realtime) and segmentation improves. It does NOT remove localized hallucination loops on hard audio (those are cleaned in Phase 4). Tune the worker count to the machine.
faster-whisper (cross-platform default). Calls the bundled wrapper <skill-dir>/scripts/fw_transcribe.py with the venv Python (config.python_bin); faster-whisper decodes most formats directly, no ffmpeg needed:
"<config.python_bin>" "<skill-dir>/scripts/fw_transcribe.py" \
"<audio>" "<config.model>" "<config.model_path>" "<LANG_ARG>" "<config.compute_type>" \
> "<dropzone>/<basename>.txt" 2> "$LOG"
<skill-dir> is this skill's install directory (provided when the skill loads); the wrapper ships in scripts/ and updates with the skill. No decode-level levers are applied: a lever sweep on real loop-prone audio found faster-whisper large-v3 does NOT loop or truncate the way whisperkit can, so initial_prompt (risked injecting primed words), no_repeat_ngram_size (no loop to suppress) and vad_filter (speed only) add risk or speed, not accuracy. Loop and brand cleanup happen engine-agnostically in Phase 4.
whisper.cpp (optional binary). Wants 16 kHz mono WAV, so pipe through ffmpeg first:
ffmpeg -nostdin -loglevel error -y -i "<audio>" -ar 16000 -ac 1 "${TMPDIR:-/tmp}/<basename>.wav"
"${ENGINE_BIN:-whisper-cli}" -m "<config.model_path>" -f "${TMPDIR:-/tmp}/<basename>.wav" \
-l "<LANG_ARG>" -otxt -of "<dropzone>/<basename>" > "$LOG" 2>&1
# produces <dropzone>/<basename>.txt
- Long audio (roughly > 20 min or > 30 MB): run the transcription as a background Bash job and poll for completion rather than blocking, to avoid command timeouts. On CPU, faster-whisper and whisper.cpp are slower than the Apple Neural Engine, so budget more time and lean on the background+poll path.
- First run of an engine may fetch a tokenizer / model shard from Hugging Face; this is expected.
Speaker diarization (opt-in, only when config.diarization.enabled is true). Goal: ONE unified, engine-agnostic output, [mm:ss] Speaker A: <text> lines sorted by time, so nothing downstream branches on engine for speaker handling. Pass config.diarization.num_speakers when the participant count is known. Caveats to carry into any user-facing note: turn-level not word-level; labels are arbitrary A / B (not names, map them in Phase 4 if grounding makes it clear); verified only on clean, low-overlap 2-speaker audio (overlap / 3+ speakers / auto-count untested). Per engine:
whisperkit-cli: add --diarization --diarization-num-speakers <N> to the transcribe command above and send stdout to <dropzone>/<basename>.raw.txt (not .txt). whisperkit appends a ---- Speaker Diarization Results ---- block (NIST RTTM, with transcript tokens packed into the ortho field) to stdout; its --report is non-functional for this, so you MUST capture stdout. Convert to the unified format with the bundled parser:python3 "<skill-dir>/scripts/parse_diarization.py" "<dropzone>/<basename>.raw.txt" > "<dropzone>/<basename>.txt"
Run diarization in ONE call over the whole file. The first-ever run may download an HF diarization model; allow a few minutes, watch the log.
faster-whisper: no native diarization; meeting-scribe adds it via a token-free local add-on (sherpa-onnx + two public ONNX models, see "Enabling speaker diarization" above). When config.diarization.seg_model and emb_model are set, convert the source to 16 kHz mono WAV first, then call the bundled merge wrapper (it transcribes with faster-whisper, diarizes with sherpa-onnx, and emits the unified format directly):ffmpeg -nostdin -loglevel error -y -i "<audio>" -ar 16000 -ac 1 "${TMPDIR:-/tmp}/<basename>.16k.wav"
"<config.python_bin>" "<skill-dir>/scripts/fw_diarize.py" \
"${TMPDIR:-/tmp}/<basename>.16k.wav" "<config.model>" "<config.model_path>" \
"<LANG_ARG>" "<config.compute_type>" "<config.diarization.num_speakers or 0>" \
"<config.diarization.seg_model>" "<config.diarization.emb_model>" \
> "<dropzone>/<basename>.txt" 2> "$LOG"
If the add-on is not set up (seg_model / emb_model are null), fall back to the non-diarized command above. (Validated token-free on a 2-speaker clip: correct A/B attribution + auto speaker-count.)
whisper.cpp: no real diarization (--tdrz / tinydiarize is experimental, 2-speaker only). Not supported here: ignore diarization.enabled, produce the normal non-diarized transcript, and tell the user to switch engine (whisperkit-cli or faster-whisper) if they need speaker separation.
Confirm the .txt was produced and is non-empty, then proceed to Phase 1 using it. On failure, surface the tail of the stderr log and stop.
The source audio is archived together with its .txt at the end of Phase 5 (after all artifacts succeed), not here.
Phase 1: Bootstrap (silent)
Before responding, silently:
- Identify the transcript file: the
.txt from Phase 0, or the file the user named / the single loose .txt in the drop-zone.
- Read the transcript with
Read. For a large file, read in chunks but have the full content before Phase 4.
- Load baseline grounding if
output.mode == "obsidian" and baseline_context_file is set: read it (and load vault_context_skill if configured). In folder mode with no grounding configured, skip.
- If a grounding read fails, proceed without it and flag the constraint at the top of Phase 2.
Do not narrate this step. One brief Phase 2 response is the first user-visible output.
Phase 2: Classification + context request
A brief response in the replies language.
If 2+ candidate files (from Phase 0/1): list them with mtime and ask which to process; stop and wait. After the pick, re-enter Phase 1 silently on the chosen file.
Otherwise (single file identified and read):
Part A: Classification. One line: what kind of meeting this looks like and its main topic in 5-10 words. Use a generic, content-derived type, e.g.: client session / internal team session / 1-on-1 / strategic planning / project review / interview / training / personal / mixed.
Part B: Context request. Ask where the relevant grounding context lives (project folder, person / client profile, brief, reference). Invite 1-5 paths or filenames; tell the user to reply "skip" if no extra context is needed. Stop and wait until you receive paths or a skip.
(In folder output mode with no grounding source configured, Part B may be skipped; proceed with general analysis.)
Phase 3: Context loading
If the user provided paths:
- Fetch them (batch read in Obsidian mode;
Read in folder mode).
- If they gave a folder, ask which specific files matter; do not silently fetch a whole folder.
- Read carefully and extract: correct spellings of people / brands / projects, engagement-specific terms, recent decisions / status / open loops, anything that changes how transcript content should be read.
- If a fetched file points to another you would benefit from, ask before a second fetch round. Do not chain-fetch silently.
If "skip", proceed with baseline grounding (if any) plus general knowledge.
Acknowledge what you loaded in ONE short line, then proceed straight to Phase 4. No extra gate. Do not echo artifacts.
Phase 4: Draft transcript + terminology confirmation
The transcript is drafted, its terminology locked with the user, and the file corrected in place, all BEFORE the canvas or summary exist. (Order is the point: the canvas and summary inherit every name in the transcript, so a name fixed after they are generated silently survives wrong inside them. That exact failure, stale terminology baked into "final" artifacts, is what this gate prevents.)
Step 1: Draft + write the transcript.
Generate -transcript.md and write it directly to the configured destination now. Never print it into chat. It goes to disk this early deliberately: it is the durable intermediate that this phase and Phase 4b correct in place, and the Phase 4b auditor needs a file path it can read cold.
File naming (shared by all three artifacts; the canvas and summary reuse this base in Phase 5):
- Base:
Meeting-YYYY-MM-DD-<slug>
- Date: from the source filename's date prefix if present, else today.
- Slug: short kebab-case from the Phase 2A topic (2-4 words, Latin script / pinyin, no spaces).
- Three files:
-transcript.md, -canvas.html, -summary.md.
Where to write (by output.mode):
folder -> Write into output.folder_path.
obsidian -> mcp__mcp-obsidian__obsidian_append_content into landing_folder (vault-relative). Landing folder is a staging zone; the user promotes to a project folder later. If the user named a target folder, use it. In-place corrections in step 4 and in Phase 4b use the filesystem Edit tool against the absolute vault path (same mechanic as the Phase 6 loop).
If the target file already exists, ask before overwriting (rerun case).
Corrected-transcript rules (apply while writing -transcript.md):
- Frontmatter:
type: meeting-transcript, meeting, date, participants, source_file, plus any grounding links.
- Fix proper nouns using grounding context first, then general context. Fix obvious mistranscriptions where context makes the word unambiguous.
- Brand / tool-name correction map. Whisper mis-hears common AI/tech names; fix them here deterministically (this is the chosen substitute for source-level prompt biasing). Known mis-hearings -> correct, applied only when context makes the AI/tech meaning unambiguous:
quad / clock / Clock -> Claude; clock code -> Claude Code; AI tip / cheggbd -> ChatGPT; entropic -> Anthropic. The exact garbling is render-dependent (engine- and model-specific; e.g. on Mandarin-accented audio clock is by far the most common Claude garble), so treat this as a GROWING dictionary, not a fixed list; add new mis-hearings as you confirm them.
- Preserve speech features: fillers, false starts, repetitions, trailing thoughts, code-switching exactly as transcribed.
- Collapse Whisper repetition loops. A token or short phrase repeated 4+ times in a row (e.g.
卖鸯子卖鸯子卖鸯子卖鸯子, 找多更多找多更多) is an ASR hallucination on hard audio, not real speech: collapse it to a single instance or drop it, and mark [unclear] if the underlying words cannot be recovered. This is distinct from genuine emphatic repetition (e.g. 对对对 / 是是是, up to ~3x), which stays; only 4+ identical runs are treated as loops.
- Speaker labels: if Phase 0 produced a diarized transcript (
[mm:ss] Speaker A/B: ...), keep the turn structure and map the arbitrary A / B to real names where grounding context makes it clear (note the mapping in a one-line processing note at the top); diarization is turn-level, so a long first turn may swallow brief interjections; split them back out only if obvious. Without diarization, keep speaker labels as transcribed and infer conservatively. Drop pure Whisper hallucination lines (foreign-language garbage from silence / cross-talk).
- Timestamps: keep as they came.
- Terms still uncertain after grounding -> mark inline
[unclear: best guess] AND add them to the step 2 list below.
- Light-touch only. Do NOT rewrite, smooth, or paraphrase.
Step 2: Compile the uncertainty list (while drafting, not as a separate read-through). Collect every proper noun / company name / place / technical term where the engine's output is ambiguous and grounding did NOT resolve it. Two tiers, both go on the list:
- Unknown: no confident reading. Show the variants as heard.
- Guess, unverified: a plausible reading (the "80% sure" case). Never silently apply it; show the guess for confirmation, e.g.
Contozo / Kontoso / Contosa -> Contoso?.
List discipline (the list is for a human to answer against, not a dump):
- Group by kind: people / companies + brands / places / jargon + technical terms.
- Collapse variants: all mis-hearings of one real term are ONE entry (five garblings of one name is one question, not five). Note the occurrence count.
- Cap the ask at ~15 entries, highest-frequency first. Anything past the cap stays tagged
[unclear: best guess] in the file; say how many were cut ("plus N low-frequency terms tagged inline") so the user can ask for the rest.
Step 3: Present the list and STOP. Ask for corrections and wait. This is a hard gate (the one place the user's own knowledge is irreplaceable, and the cheapest point to apply it, before anything downstream consumes the names). Partial answers are expected and fine: the user may answer all, some, or say "leave the rest, correct later". Never hold the pipeline hostage to 100% coverage; whatever comes back is what gets applied.
Step 4: Apply. Edit the confirmed terms into the transcript in place. A confirmed term fixes EVERY variant of it across the whole file, not just the flagged instance. Confirmed guesses lose their [unclear] tag; unanswered items keep theirs. If a fetched grounding file (or baseline_context_file) would plausibly resolve the SAME term on a future meeting, offer once, in one line, to append the newly confirmed term to that file, so Phase 3 resolves it silently next time instead of asking again (this is the per-user analogue of the brand / tool-name map above: a GROWING dictionary, but user-specific and living in their own grounding, not in this skill body). Then proceed to Phase 4b.
Phase 4b: Fresh-context audit (independent second pass)
A single correction pass reliably under-catches internal-consistency errors: the pass that produced the draft is anchored to its own first readings, while a cold reader spots the same real word spelled two different wrong ways, or a homophone slip that is only visibly wrong because the same concept appears correctly elsewhere in the file. (Observed on a real run: a fresh reader found roughly 35 errors the first pass had missed, including a repeated character-substitution pattern and a homophone slip.) So: audit with an agent that has no memory of this session.
Skip condition. Skip 4b (say so in one line) when the transcript is short (roughly under 15 minutes of audio) AND Phase 4 surfaced 0-2 uncertain terms, or when the user says skip. Long, name-dense, or code-switched transcripts always get the audit.
Step 1: Spawn the auditor. Launch a subagent with NO conversation context; its brief must be fully self-contained:
- The transcript file path (the Phase 4 corrected file).
- Standalone framing: who is in the meeting, what it is about, the languages and register in play (e.g. code-switched Mandarin-English).
- The terms already confirmed in Phase 4 (so it does not re-flag them, and treats them as fixed anchors).
- The job: read the WHOLE transcript and find OTHER likely transcription errors using internal logical / contextual consistency only: a real word spelled two different wrong ways in different paragraphs; a term that does not fit its sentence but a homophone does; a stray variant of an established participant name. NOT things that require the user's private knowledge.
- Report, never edit. Return findings ranked by confidence, each = location + current text -> proposed text + a one-line reason. (Read-only by design: a fresh agent editing the file directly can silently overwrite corrections the user just confirmed, and a report lets a confidence bar gate what gets auto-applied.)
Step 2: Triage the findings.
- High confidence -> auto-apply via
Edit, no gate. Rule of thumb for "high": the proposed form already appears elsewhere in the same transcript in unambiguous form, OR it matches a term the user confirmed in Phase 4, OR it is a systematic substitution with 3+ consistent instances. A bare guess with no internal corroboration is NOT high confidence, however plausible.
- Medium / low / unresolved -> surface, never block. Tag the spots inline
[unclear: best guess] and carry the list into the Phase 5 chat report as an optional follow-up. Proceed to Phase 5 without waiting; answers that arrive later are applied through the Phase 6 loop, to all three artifacts.
Phase 5: Generate canvas + summary + write to destination (NO full-text echo)
Only now, with terminology locked, generate the remaining two artifacts, **from the corrected transcri
…(truncated)
1---2name: breakthrough-meeting-scribe3description: Turn meeting audio into three artifacts (a corrected transcript, an HTML visual canvas, and a Markdown summary with AI insights), transcribed locally with Whisper on any platform: no cloud, no external transcription app. Use when the user hands over a meeting recording or wants meeting notes from one ("处理这个录音", "会议纪要", "把录音转成文字", or an audio file lands in the configured drop-zone), wants an existing transcript corrected or cleaned ("处理 transcript", "转录纠错", "转写文本", or a .txt lands in the drop-zone), wants a recording pulled from their connected PLAUD account ("process my Plaud recording", "从 Plaud 拿"), asks for edits to a transcript, canvas, or summary this skill just produced, or asks for cross-meeting Insights on their communication patterns instead of processing one recording ("analyze my meetings", "how do I come across in meetings", "分析我的沟通模式", "复盘我最近的会议", "跨会议分析").4---56# Breakthrough Meeting Scribe: Transcribe, Correct, Canvas, Summarize78## What this skill does910Take a meeting **audio file** (or an already-transcribed `.txt`), either dropped into the configured drop-zone or pulled from a connected PLAUD recorder account, and produce three artifacts:11121. A **corrected transcript** (light-touch cleanup, speech features preserved)132. A **visual canvas** (single self-contained HTML, the whole meeting at a glance)143. A **summary** (Markdown, with an AI-insights section)1516Transcription always runs **locally** via Whisper (no external app), including for Plaud-sourced audio: Plaud is only a way for the audio to arrive, never a transcription backend, so a cloud provider's transcript quality and language handling never enter the pipeline. The artifacts are written **directly to the configured output destination**, never echoed in full to chat (echoing then writing generates the same content twice as output tokens and bloats context). The user reviews at the destination and requests edits there.1718**Two modes.** The pipeline above (Phases 0-6) is the default, one meeting in, three artifacts out. A second entry point, **Insights mode** (see its section after Phase 6), runs when the user asks about their communication patterns ACROSS meetings: it reads the corpus of already-corrected transcripts and writes one analysis report. Route by intent: "process this recording" -> pipeline; "what are my patterns in meetings" -> Insights mode. Never run both in one pass.1920All machine-specific behavior (where audio lands, which Whisper model to use, where artifacts go, what language to write in) comes from a per-user config file, NOT from this document. This skill is the orchestration logic only; it is portable across users and machines.2122## Configuration2324On load (when the user invokes this skill), **read the config first**:2526```bash27cat ~/.config/meeting-transcripts/config.json28```2930- **If it exists**: parse it and use its values for every path / model / language decision below. Do not narrate this read.31- **If it is absent**: run **First-run setup** (below) once, write the config, then continue.3233### Config schema3435```json36{37 "engine": "faster-whisper",38 "model": "large-v3",39 "model_path": "/abs/path/to/model",40 "engine_bin": null,41 "python_bin": null,42 "compute_type": "auto",43 "transcribe_language": "auto",44 "audio_dropzone": "/abs/path/to/drop-zone",45 "audio_archive": "/abs/path/to/processed-archive",46 "sources": {47 "plaud": { "enabled": false, "cli_path": "plaud" }48 },49 "output": {50 "mode": "folder",51 "folder_path": "/abs/path/to/output-folder",52 "vault_path": null,53 "landing_folder": null,54 "baseline_context_file": null55 },56 "language": {57 "transcript": "as-spoken",58 "canvas": "english",59 "summary": "english",60 "replies": "english",61 "register": null62 },63 "vault_context_skill": null,64 "diarization": { "enabled": false, "num_speakers": null, "seg_model": null, "emb_model": null }65}66```6768Field notes:6970- `engine`: transcription backend, one of:71 - **`whisperkit-cli`**: Apple Silicon Mac only (CoreML / Apple Neural Engine, fastest). `model_path` = the CoreML model folder.72 - **`faster-whisper`**: cross-platform default (Intel mac / Windows / Linux, CPU or CUDA; also fine on Apple Silicon). Python / CTranslate2. `model_path` = the CT2 model-cache directory, `python_bin` = the venv Python that has `faster-whisper` installed, `compute_type` tunes precision.73 - **`whisper.cpp`**: optional cross-platform binary (Metal / CUDA / CPU, no Python). `model_path` = a GGML `.bin` file, `engine_bin` = the `whisper-cli` binary if it is not on PATH.74- `model`: which Whisper model to run. Default **`large-v3`** for best quality on every engine; smaller ids (`medium`, `small`, `base`) trade quality for speed. **The model FORMAT is different per engine (CoreML folder vs CT2 cache vs GGML `.bin`) and the formats are NOT interchangeable**: first-run setup downloads the correct format for the chosen engine.75- `model_path`: where that model lives, per engine (see `engine` above).76- `engine_bin`: optional absolute path to the engine binary (`whisperkit-cli` or `whisper-cli`); `null` = found on PATH.77- `python_bin`: (faster-whisper only) absolute path to the Python inside the venv where `faster-whisper` is installed.78- `compute_type`: (faster-whisper only) `auto` (int8 on CPU, float16 on CUDA) or force one of `int8`, `int8_float16`, `float16`, `float32`.79- `transcribe_language`: Whisper language hint (`auto`, `zh`, `en`, ...). For code-switched audio, **set the dominant language rather than `auto`**: forced single-language decoding keeps inline foreign terms verbatim and gives higher-confidence, stabler output. On whisperkit-cli, `auto` was observed to add a mid-clip language-flip hallucination (a spurious German tail); on faster-whisper a real-audio test found `auto` harmless but lower-confidence (it stayed on the dominant language correctly), so forcing the dominant language is the safer default either way. Use `auto` only when the dominant language is genuinely unknown.80- `audio_dropzone` / `audio_archive`: where new audio lands, and where it (plus its `.txt`) is moved after successful processing.81- `sources.plaud`: optional remote ingest from a PLAUD recorder account, via the vendor's `@plaud-ai/cli`. `enabled: false` (the default) means the drop-zone is the only source. When `true`, Phase 0 can pull a recording's audio out of the Plaud cloud into `audio_dropzone`, after which the normal local pipeline runs unchanged. `cli_path` is the path to the `plaud` binary; prefer an absolute path, since npm global bins are often outside a non-login shell's PATH. Requires a one-time `plaud login` (browser OAuth, tokens cached in `~/.plaud/tokens.json`); this skill never handles credentials. See "Enabling Plaud ingest" below.82- `output.mode`: **`folder`** (write artifacts to `folder_path` via filesystem) or **`obsidian`** (write into an Obsidian vault via the mcp-obsidian tools; uses `vault_path`, `landing_folder`, optional `baseline_context_file`).83- `language.*`: output language per artifact. `transcript: as-spoken` keeps the spoken language(s) intact. `register` is an optional free-text style note (e.g. a regional register).84- `vault_context_skill`: optional name of a companion skill that supplies vault/notes grounding; load it alongside this one if set. `null` = none.85- `diarization`: optional speaker separation, **OFF by default** (opt-in). When `enabled: true`, Phase 0 produces a speaker-tagged transcript in the unified format `[mm:ss] Speaker A: <text>`; set `num_speakers` to the known participant count (or leave `null` to auto-detect). `seg_model` / `emb_model` are the local ONNX model paths for the `faster-whisper` diarization add-on (set during the optional diarization setup below; unused by `whisperkit-cli`, which has native diarization). **No Hugging Face token is required for any engine's diarization.** Diarization is **turn-level** (not word-level), labels are arbitrary `A` / `B` (not names), and it is verified only on clean low-overlap 2-speaker audio, so keep it opt-in, do not rely on it for attribution. Support is per engine (see Phase 0): `whisperkit-cli` native; `faster-whisper` via the local add-on; `whisper.cpp` not supported.8687### First-run setup (only when config is absent)8889Run once, interactively, in the replies language. Keep it tight.90911. **Detect platform + pick a default engine.** Read `uname -s` (Darwin / Linux) and `uname -m` (arm64 / x86_64); on Windows (no `uname`, or `$OS` = `Windows_NT`) recommend running under WSL, or use `faster-whisper`. Default engine:92 - Darwin + arm64 (Apple Silicon) -> **`whisperkit-cli`** (fastest, Apple Neural Engine).93 - Darwin + x86_64 (Intel mac), Linux, or Windows -> **`faster-whisper`** (cross-platform).94 State the detected default in one line and let the user override (e.g. `whisper.cpp` for a no-Python Metal/CUDA binary).95962. **Ensure the engine.**97 - **`whisperkit-cli`**: `command -v whisperkit-cli`; if missing and Homebrew is present -> `brew install whisperkit-cli`.98 - **`faster-whisper`**: create an isolated venv and install into it. Prefer `uv` if present:99 ```bash100 uv venv "$HOME/.config/meeting-transcripts/venv"101 VIRTUAL_ENV="$HOME/.config/meeting-transcripts/venv" uv pip install faster-whisper102 ```103 else `python3 -m venv "$HOME/.config/meeting-transcripts/venv" && "$HOME/.config/meeting-transcripts/venv/bin/pip" install faster-whisper`. Set `python_bin` = `$HOME/.config/meeting-transcripts/venv/bin/python`, `compute_type` = `auto`.104 - **`whisper.cpp`**: `command -v whisper-cli`; if missing, on mac `brew install whisper-cpp`, on Linux use the distro package or build from source (needs `cmake`), on Windows use a release binary or WSL. Set `engine_bin` if it is not on PATH.1051063. **Download the model in the engine's format** (default `model` = `large-v3`). Ask where to keep models (offer `$HOME/.config/meeting-transcripts/models`); the formats are NOT interchangeable, so download the one matching the chosen engine:107 - **`whisperkit-cli`** (CoreML): the current `whisperkit-cli` (it now identifies as `argmax-cli`) has **no standalone download command**, and `transcribe` errors out (`Either audioPath or audioFolder must be provided`) *before* downloading if given no audio. So trigger the ~1.5 GB fetch as a side effect of transcribing a 1-second silent WAV (this is why ffmpeg is required even though whisperkit decodes audio natively at run time):108 ```bash109 SILENCE="<dir>/.silence.wav"110 ffmpeg -y -f lavfi -i anullsrc=r=16000:cl=mono -t 1 -ar 16000 -ac 1 "$SILENCE"111 whisperkit-cli transcribe --audio-path "$SILENCE" --model large-v3 --download-model-path "<dir>"112 rm -f "$SILENCE"113 ```114 The model lands NESTED at `<dir>/models/argmaxinc/whisperkit-coreml/<model-name>/`. Set `model_path` to that folder (the one that actually holds the `.mlmodelc` bundles), NOT `<dir>` itself; locate it with `find "<dir>" -maxdepth 7 -name AudioEncoder.mlmodelc | grep -v '/.cache/'` (the `grep -v` skips the incomplete Hugging Face staging copy under `.cache/huggingface/download/`), then take its `dirname` and validate the bundles.115 - **`faster-whisper`** (CT2): warm the bundled wrapper once to download into `model_path`: `"<python_bin>" "<skill-dir>/scripts/fw_transcribe.py" --warm large-v3 "<model_path>" auto` (~1.5 GB). Validate the cache folder is non-empty.116 - **`whisper.cpp`** (GGML): download a single `.bin`, e.g. `curl -L -o "<dir>/ggml-large-v3.bin" https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3.bin` (~3 GB); set `model_path` to that file and confirm it exists.1171184. **Output destination.** Ask: plain **folder** (default) or **Obsidian vault**. Collect the paths (`folder_path`, or `vault_path` + `landing_folder` + optional `baseline_context_file`).1191205. **Audio folders.** Ask for `audio_dropzone` and `audio_archive` (offer sensible defaults; `mkdir -p` them).1211226. **Language.** `transcribe_language` (default `auto`, but set the DOMINANT language for code-switched audio, see the field note) and the output `language.*` (default `english`, `transcript` = `as-spoken`); optional `register`.1231247. **Write** `~/.config/meeting-transcripts/config.json` (`mkdir -p ~/.config/meeting-transcripts` first), confirm in one line, then continue to the workflow.125126**Persistence rule:** all per-user settings live in that external config, never in this skill file. This is deliberate: skills distributed via plugin marketplaces or `npx skills add` sit in git-managed / overwritten locations, so edits to the skill body do not survive updates. The external config does. Whenever a setting changes, update the config file, not this document. (Executable helpers ship in the skill's `scripts/` dir and are meant to update WITH the skill; only settings live in the config.)127128### Enabling Plaud ingest (optional)129130Off by default; the drop-zone is the only source until you turn this on. To pull recordings straight from a PLAUD recorder account:1311321. Install the vendor CLI: `npm install -g @plaud-ai/cli`.1332. Authenticate once, **yourself, in your own terminal**: `plaud login` (browser OAuth; tokens are cached in `~/.plaud/tokens.json`). The skill never handles credentials and never logs in on your behalf.1343. Set `config.sources.plaud.enabled = true` and `cli_path` to the binary's absolute path (`command -v plaud`).135136Plaud is an ingest route only. The audio is downloaded to your `audio_dropzone` and transcribed locally by your configured engine exactly like a hand-dropped file; Plaud's own cloud transcript is never used.137138### Enabling speaker diarization (optional, no Hugging Face token)139140Diarization is OFF by default. To turn it on, set `config.diarization.enabled = true` (and `num_speakers` if you know the count, else leave `null` to auto-detect), then:141142- **`whisperkit-cli`**: nothing extra; the first diarized run downloads a diarization model from Hugging Face automatically (no token).143- **`faster-whisper`**: a small one-time **local add-on** (all models are public, **no token**):144 1. Install into the same venv: `VIRTUAL_ENV="$HOME/.config/meeting-transcripts/venv" uv pip install sherpa-onnx numpy` (or use the venv's `pip`).145 2. Download the two non-gated ONNX models into `$HOME/.config/meeting-transcripts/dia-models/`:146 - segmentation (~6 MB): `https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-segmentation-models/sherpa-onnx-pyannote-segmentation-3-0.tar.bz2` (un-tar; the model is the inner `.../model.onnx`).147 - speaker embedding (~38 MB): `https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/3dspeaker_speech_eres2net_base_sv_zh-cn_3dspeaker_16k.onnx` (note the upstream release tag is spelled `speaker-recongition-models`).148 3. Set `config.diarization.seg_model` and `config.diarization.emb_model` to those two paths.149- **`whisper.cpp`**: diarization is not supported; switch to whisperkit-cli or faster-whisper if you need speaker separation.150151## Role152153You are the user's senior strategy partner and meeting analyst. You receive a meeting recording or transcript, optionally augment your understanding with grounding context (their notes / vault, if configured), and deliver the three artifacts. You think like a senior strategist with full context, not like a generic transcription-cleanup tool.154155The user's detailed business context, people, clients, and methodologies live in their own notes (and in the configured `vault_context_skill`, if any). **Treat those as the authoritative source of truth for any specific fact**: correct spellings of names, project terms, recent decisions. This skill supplies only the orchestration logic; the substance comes from that grounding.156157## Available tools158159- `Read`: read the transcript / audio-adjacent `.txt` file.160- `Bash`: read the config; list audio / `.txt` files in the drop-zone; run the Plaud CLI and `curl` to pull remote audio (when `sources.plaud.enabled`); run the configured transcription engine (whisperkit-cli / faster-whisper venv Python / whisper-cli), with ffmpeg for format conversion when needed; `mkdir -p` and `mv` to archive the source after success.161- `Write` / `Edit`: write the three artifacts (in `folder` output mode) and edit them in place during Phase 4 correction, Phase 4b audit fixes, and the Phase 6 review loop.162- `Agent` (or the equivalent subagent-spawn tool): launch the Phase 4b fresh-context auditor. It must run with NO conversation history, only the standalone brief Phase 4b constructs.163- **Obsidian output mode only** (`output.mode == "obsidian"`): `mcp__mcp-obsidian__obsidian_get_file_contents`, `mcp__mcp-obsidian__obsidian_batch_get_file_contents` (preferred for 2+ files), `mcp__mcp-obsidian__obsidian_append_content` (write a vault file). Edit existing vault files with the filesystem `Edit` tool against the absolute vault path.164165Use the batch read tool whenever fetching 2+ grounding files.166167## Language directive168169Driven by `config.language`:170171| Artifact | Language |172|----------|----------|173| Corrected transcript | `language.transcript` (default `as-spoken`: preserve the spoken language(s) and code-switching intact) |174| Visual canvas | `language.canvas` |175| Summary document | `language.summary` |176| Your conversational replies | `language.replies` |177178### Code-switching and register179180If the audio mixes languages (e.g. Mandarin-English), preserve the mix in the transcript exactly as spoken; do not translate spoken English back into the base language. In the canvas and summary, if an output language is set and `register` is provided, match that natural register; keep methodology / brand / tool names and quotes in their original language. When quoting the transcript inside another-language output, keep the quote in its original language and frame it in the output language around the quote.181182### Critical: punctuation discipline183184No em dashes, no double dashes (`--`), no spaced hyphens as separators; use standard punctuation only (comma, colon, period, parentheses); restructure the sentence if needed. Em dashes and the double-dash tell both read as "AI-generated".185186## Workflow (8 phases)187188Artifacts are generated and written **directly to the configured destination**, never echoed in full to chat. Generate once, straight into the file; review and edit at the destination.189190### Phase 0: Transcription (audio -> transcript)191192**Goal:** turn a dropped audio file into a raw `.txt` transcript beside it, then hand off to Phase 1. If the user dropped a `.txt` directly (no audio), skip Phase 0 and start at Phase 1.1931941. **Read config** (above). If absent, run First-run setup.1952. **Find the source** in `audio_dropzone`:196 - Named file -> use it.197 - Generic intent -> list audio files (`.wav .mp3 .m4a .flac .ogg .webm .mp4 .aac`) that have **no matching `<basename>.txt`** beside them, most-recent first:198 - 0 audio (and 0 loose `.txt`) -> if `sources.plaud.enabled` is true, go to step 2b and offer the Plaud pull; otherwise say so in Phase 2 ("no audio or transcript to process in the drop-zone", localized to the replies language) and stop.199 - 1 -> use it.200 - 2+ -> defer the choice to Phase 2 (list with mtime).201 - If a loose `.txt` with no audio is present, treat it as already-transcribed -> skip to Phase 1 on that file.202203 The drop-zone stays the default source. Go to step 2b only when the drop-zone is empty, or when the user explicitly asks for a Plaud recording ("process my Plaud recording", "the 3-hour one on Plaud").2042052b. **Plaud ingest** (only when `sources.plaud.enabled` is true). Pull a cloud recording's audio into `audio_dropzone`, then rejoin step 3. Use `PLAUD="<config.sources.plaud.cli_path>"` throughout.206207 1. **Check auth** once: `"$PLAUD" me`. If it fails, tell the user to run `plaud login` themselves in their terminal (browser OAuth) and stop. Never attempt to authenticate for them.208 2. **List candidates** and present them in Phase 2 for the user to pick (never auto-pick):209 ```bash210 "$PLAUD" recent --days 30 # or: "$PLAUD" files -s 20 / "$PLAUD" search "<keyword>"211 ```212 Columns are `ID NAME DATE DURATION`. Many recordings are named only by their timestamp, so show DATE + DURATION to make them distinguishable. Stop and wait for the pick.213 3. **Get metadata** for the chosen id: `"$PLAUD" file <file_id>`, then work out the real LOCAL recording time. Three timestamps come back and each is a different thing:214 - `created_at`: when the recorder synced to the cloud. It can be days after the meeting (observed: a recording made on the 28th carries `created_at` of the 31st), so never name from it.215 - `start_at`: when recording began, **in UTC**.216 - `name`: for an untitled recording this is the same instant in **local** time. Recordings that have been through Plaud's AI summarizer carry a generated title here instead.217218 So: if `name` parses as a `YYYY-MM-DD HH:MM:SS` timestamp, use it directly. Otherwise convert `start_at` from UTC to the machine's local zone:219 ```bash220 # macOS / BSD date: parse with an explicit +0000 offset, print in local time221 date -jf "%Y-%m-%dT%H:%M:%S %z" "<start_at> +0000" +"%Y-%m-%d-%H%M"222 # GNU date (Linux):223 date -d "<start_at>Z" +"%Y-%m-%d-%H%M"224 ```225 **Do not name from raw `start_at`**, and on BSD `date` do not just pass `-u` (that parses AND prints in UTC, so it converts nothing): either mistake can land the meeting on the wrong calendar day, which then propagates into every artifact's filename and frontmatter through Phase 4. (Worked example at UTC+8: `start_at` `2026-06-15T16:10:23` is really `2026-06-16 00:10:23` locally, a full day off. The DATE column in `plaud files` is UTC too, so it shows the same wrong day.)226 4. **Fetch the signed URL, with retry.** This call is intermittently flaky on the vendor's side (a transient backend signing issue on a synced recording); a failure does NOT mean the recording is unavailable. Retry up to 3 times before giving up:227 ```bash228 for i in 1 2 3; do229 OUT=$("$PLAUD" audio <file_id> 2>&1)230 URL=$(printf '%s' "$OUT" | grep -o 'https://[^ ]*' | head -1)231 [ -n "$URL" ] && break232 sleep 20233 done234 ```235 If all 3 attempts fail, report the CLI's own message and stop; suggest retrying in a few minutes.236 5. **Download** into the drop-zone. Name it `<YYYY-MM-DD>-<HHMM>-plaud-<first 8 of file_id>.mp3`, using the LOCAL time resolved in step 3: the leading date feeds Phase 4's naming rule, and the id fragment keeps the file traceable back to Plaud for a re-pull.237 ```bash238 curl -fL --retry 3 --retry-delay 5 -o "<dropzone>/<basename>.mp3" "$URL"239 ```240 - The URL is presigned for **GET only**: `curl -I` returns 403, so never precheck with HEAD. Use a ranged GET (`curl -r 0-2047`) if you need a size probe.241 - The URL expires in **24 hours**, so fetch it immediately before downloading; never reuse one from an earlier session.242 - Run as a background Bash job and poll. Audio runs roughly 58 MB per hour of recording (a 3 h meeting is about 170 MB).243 6. **Verify before transcribing**: the file exists, is non-empty, and `file "<path>"` reports an audio container. A truncated download or an HTML / XML error body saved as `.mp3` will otherwise fail deep inside Whisper with a confusing error.244 7. Proceed to step 3 with that file. Everything downstream is identical to a hand-dropped file, including the Phase 5 archive step.2453. **Transcribe** with the configured engine -> a `<basename>.txt` beside the audio. Branch on `config.engine`. Let `LOG="${TMPDIR:-/tmp}/meeting-scribe-<basename>.log"`, `LANG_ARG` = `config.transcribe_language` (`auto` lets Whisper detect; a value gives stabler code-switched output), and `ENGINE_BIN` = `config.engine_bin` if set, else the default binary name.246247 **`whisperkit-cli`** (Apple Silicon):248 ```bash249 "${ENGINE_BIN:-whisperkit-cli}" transcribe \250 --audio-path "<audio>" \251 --model-path "<config.model_path>" \252 --language "<LANG_ARG>" \253 --chunking-strategy vad \254 --concurrent-worker-count 4 \255 > "<dropzone>/<basename>.txt" 2> "$LOG"256 ```257 - `--chunking-strategy vad` (segment on voice activity) + `--concurrent-worker-count` (parallel chunk decode) is the long-audio throughput default: a 2 h file runs in roughly 3 min (~14x realtime) and segmentation improves. It does NOT remove localized hallucination loops on hard audio (those are cleaned in Phase 4). Tune the worker count to the machine.258259 **`faster-whisper`** (cross-platform default). Calls the bundled wrapper `<skill-dir>/scripts/fw_transcribe.py` with the venv Python (`config.python_bin`); faster-whisper decodes most formats directly, no ffmpeg needed:260 ```bash261 "<config.python_bin>" "<skill-dir>/scripts/fw_transcribe.py" \262 "<audio>" "<config.model>" "<config.model_path>" "<LANG_ARG>" "<config.compute_type>" \263 > "<dropzone>/<basename>.txt" 2> "$LOG"264 ```265 `<skill-dir>` is this skill's install directory (provided when the skill loads); the wrapper ships in `scripts/` and updates with the skill. No decode-level levers are applied: a lever sweep on real loop-prone audio found faster-whisper large-v3 does NOT loop or truncate the way whisperkit can, so `initial_prompt` (risked injecting primed words), `no_repeat_ngram_size` (no loop to suppress) and `vad_filter` (speed only) add risk or speed, not accuracy. Loop and brand cleanup happen engine-agnostically in Phase 4.266267 **`whisper.cpp`** (optional binary). Wants 16 kHz mono WAV, so pipe through ffmpeg first:268 ```bash269 ffmpeg -nostdin -loglevel error -y -i "<audio>" -ar 16000 -ac 1 "${TMPDIR:-/tmp}/<basename>.wav"270 "${ENGINE_BIN:-whisper-cli}" -m "<config.model_path>" -f "${TMPDIR:-/tmp}/<basename>.wav" \271 -l "<LANG_ARG>" -otxt -of "<dropzone>/<basename>" > "$LOG" 2>&1272 # produces <dropzone>/<basename>.txt273 ```274275 - **Long audio** (roughly > 20 min or > 30 MB): run the transcription as a background Bash job and poll for completion rather than blocking, to avoid command timeouts. On CPU, faster-whisper and whisper.cpp are slower than the Apple Neural Engine, so budget more time and lean on the background+poll path.276 - First run of an engine may fetch a tokenizer / model shard from Hugging Face; this is expected.277278 **Speaker diarization (opt-in, only when `config.diarization.enabled` is true).** Goal: ONE unified, engine-agnostic output, `[mm:ss] Speaker A: <text>` lines sorted by time, so nothing downstream branches on engine for speaker handling. Pass `config.diarization.num_speakers` when the participant count is known. Caveats to carry into any user-facing note: turn-level not word-level; labels are arbitrary `A` / `B` (not names, map them in Phase 4 if grounding makes it clear); verified only on clean, low-overlap 2-speaker audio (overlap / 3+ speakers / auto-count untested). Per engine:279280 - **`whisperkit-cli`:** add `--diarization --diarization-num-speakers <N>` to the transcribe command above and send stdout to `<dropzone>/<basename>.raw.txt` (not `.txt`). whisperkit appends a `---- Speaker Diarization Results ----` block (NIST RTTM, with transcript tokens packed into the ortho field) to stdout; its `--report` is non-functional for this, so you MUST capture stdout. Convert to the unified format with the bundled parser:281 ```bash282 python3 "<skill-dir>/scripts/parse_diarization.py" "<dropzone>/<basename>.raw.txt" > "<dropzone>/<basename>.txt"283 ```284 Run diarization in ONE call over the whole file. The first-ever run may download an HF diarization model; allow a few minutes, watch the log.285 - **`faster-whisper`:** no native diarization; meeting-scribe adds it via a **token-free local add-on** (sherpa-onnx + two public ONNX models, see "Enabling speaker diarization" above). When `config.diarization.seg_model` and `emb_model` are set, convert the source to 16 kHz mono WAV first, then call the bundled merge wrapper (it transcribes with faster-whisper, diarizes with sherpa-onnx, and emits the unified format directly):286 ```bash287 ffmpeg -nostdin -loglevel error -y -i "<audio>" -ar 16000 -ac 1 "${TMPDIR:-/tmp}/<basename>.16k.wav"288 "<config.python_bin>" "<skill-dir>/scripts/fw_diarize.py" \289 "${TMPDIR:-/tmp}/<basename>.16k.wav" "<config.model>" "<config.model_path>" \290 "<LANG_ARG>" "<config.compute_type>" "<config.diarization.num_speakers or 0>" \291 "<config.diarization.seg_model>" "<config.diarization.emb_model>" \292 > "<dropzone>/<basename>.txt" 2> "$LOG"293 ```294 If the add-on is not set up (`seg_model` / `emb_model` are null), fall back to the non-diarized command above. (Validated token-free on a 2-speaker clip: correct A/B attribution + auto speaker-count.)295 - **`whisper.cpp`:** no real diarization (`--tdrz` / tinydiarize is experimental, 2-speaker only). **Not supported** here: ignore `diarization.enabled`, produce the normal non-diarized transcript, and tell the user to switch engine (whisperkit-cli or faster-whisper) if they need speaker separation.2964. **Confirm** the `.txt` was produced and is non-empty, then proceed to Phase 1 using it. On failure, surface the tail of the stderr log and stop.297298The source audio is archived together with its `.txt` at the end of Phase 5 (after all artifacts succeed), not here.299300### Phase 1: Bootstrap (silent)301302Before responding, silently:3033041. **Identify the transcript file**: the `.txt` from Phase 0, or the file the user named / the single loose `.txt` in the drop-zone.3052. **Read the transcript** with `Read`. For a large file, read in chunks but have the full content before Phase 4.3063. **Load baseline grounding** if `output.mode == "obsidian"` and `baseline_context_file` is set: read it (and load `vault_context_skill` if configured). In `folder` mode with no grounding configured, skip.3074. If a grounding read fails, proceed without it and flag the constraint at the top of Phase 2.308309Do not narrate this step. One brief Phase 2 response is the first user-visible output.310311### Phase 2: Classification + context request312313A brief response in the replies language.314315**If 2+ candidate files (from Phase 0/1):** list them with mtime and ask which to process; stop and wait. After the pick, re-enter Phase 1 silently on the chosen file.316317**Otherwise (single file identified and read):**318319**Part A: Classification.** One line: what kind of meeting this looks like and its main topic in 5-10 words. Use a generic, content-derived type, e.g.: client session / internal team session / 1-on-1 / strategic planning / project review / interview / training / personal / mixed.320321**Part B: Context request.** Ask where the relevant grounding context lives (project folder, person / client profile, brief, reference). Invite 1-5 paths or filenames; tell the user to reply "skip" if no extra context is needed. Stop and wait until you receive paths or a skip.322323(In `folder` output mode with no grounding source configured, Part B may be skipped; proceed with general analysis.)324325### Phase 3: Context loading326327If the user provided paths:3283291. Fetch them (batch read in Obsidian mode; `Read` in folder mode).3302. If they gave a folder, ask which specific files matter; do not silently fetch a whole folder.3313. Read carefully and extract: correct spellings of people / brands / projects, engagement-specific terms, recent decisions / status / open loops, anything that changes how transcript content should be read.3324. If a fetched file points to another you would benefit from, ask before a second fetch round. Do not chain-fetch silently.333334If "skip", proceed with baseline grounding (if any) plus general knowledge.335336Acknowledge what you loaded in ONE short line, then proceed straight to Phase 4. No extra gate. Do not echo artifacts.337338### Phase 4: Draft transcript + terminology confirmation339340The transcript is drafted, its terminology locked with the user, and the file corrected in place, all BEFORE the canvas or summary exist. (Order is the point: the canvas and summary inherit every name in the transcript, so a name fixed after they are generated silently survives wrong inside them. That exact failure, stale terminology baked into "final" artifacts, is what this gate prevents.)341342**Step 1: Draft + write the transcript.**343344Generate `-transcript.md` and write it directly to the configured destination now. **Never print it into chat.** It goes to disk this early deliberately: it is the durable intermediate that this phase and Phase 4b correct in place, and the Phase 4b auditor needs a file path it can read cold.345346**File naming** (shared by all three artifacts; the canvas and summary reuse this base in Phase 5):347348- Base: `Meeting-YYYY-MM-DD-<slug>`349 - Date: from the source filename's date prefix if present, else today.350 - Slug: short kebab-case from the Phase 2A topic (2-4 words, Latin script / pinyin, no spaces).351- Three files: `-transcript.md`, `-canvas.html`, `-summary.md`.352353**Where to write (by `output.mode`):**354355- **`folder`** -> `Write` into `output.folder_path`.356- **`obsidian`** -> `mcp__mcp-obsidian__obsidian_append_content` into `landing_folder` (vault-relative). Landing folder is a staging zone; the user promotes to a project folder later. If the user named a target folder, use it. In-place corrections in step 4 and in Phase 4b use the filesystem `Edit` tool against the absolute vault path (same mechanic as the Phase 6 loop).357358If the target file already exists, ask before overwriting (rerun case).359360**Corrected-transcript rules** (apply while writing `-transcript.md`):361362- Frontmatter: `type: meeting-transcript`, `meeting`, `date`, `participants`, `source_file`, plus any grounding links.363- Fix proper nouns using grounding context first, then general context. Fix obvious mistranscriptions where context makes the word unambiguous.364- **Brand / tool-name correction map.** Whisper mis-hears common AI/tech names; fix them here deterministically (this is the chosen substitute for source-level prompt biasing). Known mis-hearings -> correct, applied only when context makes the AI/tech meaning unambiguous: `quad` / `clock` / `Clock` -> Claude; `clock code` -> Claude Code; `AI tip` / `cheggbd` -> ChatGPT; `entropic` -> Anthropic. The exact garbling is render-dependent (engine- and model-specific; e.g. on Mandarin-accented audio `clock` is by far the most common Claude garble), so treat this as a GROWING dictionary, not a fixed list; add new mis-hearings as you confirm them.365- Preserve speech features: fillers, false starts, repetitions, trailing thoughts, code-switching exactly as transcribed.366- **Collapse Whisper repetition loops.** A token or short phrase repeated 4+ times in a row (e.g. `卖鸯子卖鸯子卖鸯子卖鸯子`, `找多更多找多更多`) is an ASR hallucination on hard audio, not real speech: collapse it to a single instance or drop it, and mark `[unclear]` if the underlying words cannot be recovered. This is distinct from genuine emphatic repetition (e.g. `对对对` / `是是是`, up to ~3x), which stays; only 4+ identical runs are treated as loops.367- Speaker labels: if Phase 0 produced a diarized transcript (`[mm:ss] Speaker A/B: ...`), keep the turn structure and map the arbitrary `A` / `B` to real names where grounding context makes it clear (note the mapping in a one-line processing note at the top); diarization is turn-level, so a long first turn may swallow brief interjections; split them back out only if obvious. Without diarization, keep speaker labels as transcribed and infer conservatively. Drop pure Whisper hallucination lines (foreign-language garbage from silence / cross-talk).368- Timestamps: keep as they came.369- Terms still uncertain after grounding -> mark inline `[unclear: best guess]` AND add them to the step 2 list below.370- Light-touch only. Do NOT rewrite, smooth, or paraphrase.371372**Step 2: Compile the uncertainty list** (while drafting, not as a separate read-through). Collect every proper noun / company name / place / technical term where the engine's output is ambiguous and grounding did NOT resolve it. Two tiers, both go on the list:373374- **Unknown**: no confident reading. Show the variants as heard.375- **Guess, unverified**: a plausible reading (the "80% sure" case). Never silently apply it; show the guess for confirmation, e.g. `Contozo / Kontoso / Contosa -> Contoso?`.376377List discipline (the list is for a human to answer against, not a dump):378379- **Group by kind**: people / companies + brands / places / jargon + technical terms.380- **Collapse variants**: all mis-hearings of one real term are ONE entry (five garblings of one name is one question, not five). Note the occurrence count.381- **Cap the ask at ~15 entries**, highest-frequency first. Anything past the cap stays tagged `[unclear: best guess]` in the file; say how many were cut ("plus N low-frequency terms tagged inline") so the user can ask for the rest.382383**Step 3: Present the list and STOP.** Ask for corrections and wait. This is a hard gate (the one place the user's own knowledge is irreplaceable, and the cheapest point to apply it, before anything downstream consumes the names). Partial answers are expected and fine: the user may answer all, some, or say "leave the rest, correct later". Never hold the pipeline hostage to 100% coverage; whatever comes back is what gets applied.384385**Step 4: Apply.** `Edit` the confirmed terms into the transcript in place. A confirmed term fixes EVERY variant of it across the whole file, not just the flagged instance. Confirmed guesses lose their `[unclear]` tag; unanswered items keep theirs. If a fetched grounding file (or `baseline_context_file`) would plausibly resolve the SAME term on a future meeting, offer once, in one line, to append the newly confirmed term to that file, so Phase 3 resolves it silently next time instead of asking again (this is the per-user analogue of the brand / tool-name map above: a GROWING dictionary, but user-specific and living in their own grounding, not in this skill body). Then proceed to Phase 4b.386387### Phase 4b: Fresh-context audit (independent second pass)388389A single correction pass reliably under-catches internal-consistency errors: the pass that produced the draft is anchored to its own first readings, while a cold reader spots the same real word spelled two different wrong ways, or a homophone slip that is only visibly wrong because the same concept appears correctly elsewhere in the file. (Observed on a real run: a fresh reader found roughly 35 errors the first pass had missed, including a repeated character-substitution pattern and a homophone slip.) So: audit with an agent that has no memory of this session.390391**Skip condition.** Skip 4b (say so in one line) when the transcript is short (roughly under 15 minutes of audio) AND Phase 4 surfaced 0-2 uncertain terms, or when the user says skip. Long, name-dense, or code-switched transcripts always get the audit.392393**Step 1: Spawn the auditor.** Launch a subagent with NO conversation context; its brief must be fully self-contained:394395- The transcript file path (the Phase 4 corrected file).396- Standalone framing: who is in the meeting, what it is about, the languages and register in play (e.g. code-switched Mandarin-English).397- The terms already confirmed in Phase 4 (so it does not re-flag them, and treats them as fixed anchors).398- The job: read the WHOLE transcript and find OTHER likely transcription errors using internal logical / contextual consistency only: a real word spelled two different wrong ways in different paragraphs; a term that does not fit its sentence but a homophone does; a stray variant of an established participant name. NOT things that require the user's private knowledge.399- **Report, never edit.** Return findings ranked by confidence, each = location + current text -> proposed text + a one-line reason. (Read-only by design: a fresh agent editing the file directly can silently overwrite corrections the user just confirmed, and a report lets a confidence bar gate what gets auto-applied.)400401**Step 2: Triage the findings.**402403- **High confidence -> auto-apply** via `Edit`, no gate. Rule of thumb for "high": the proposed form already appears elsewhere in the same transcript in unambiguous form, OR it matches a term the user confirmed in Phase 4, OR it is a systematic substitution with 3+ consistent instances. A bare guess with no internal corroboration is NOT high confidence, however plausible.404- **Medium / low / unresolved -> surface, never block.** Tag the spots inline `[unclear: best guess]` and carry the list into the Phase 5 chat report as an optional follow-up. Proceed to Phase 5 without waiting; answers that arrive later are applied through the Phase 6 loop, to all three artifacts.405406### Phase 5: Generate canvas + summary + write to destination (NO full-text echo)407408Only now, with terminology locked, generate the remaining two artifacts, **from the corrected transcri409410…(truncated)