mcpbeat

Transcript Fixer

daymade/transcript-fixer

>- Corrects speech-to-text transcription errors using dictionary rules and Claude's built-in AI (no external API key required — Native AI Correction is the DEFAULT). Stage 3 API is a backup for automation without Claude Code. Builds personalized correction databases that learn from each fix, auto-loads person-name ASR variants from your people roster, and reads per-domain context files that prime the AI pass for context-dependent homophones. Triggers when working with ASR/STT output containing recognition errors, homophones, garbled technical terms, person-name errors, or Chinese/English mixed content. Also triggers on requests to clean up meeting notes, lecture transcripts, interview recordings, or any text produced by speech recognition. Use this skill even when the user just says "fix this transcript", "clean up these meeting notes", or mentions garbled names without invoking ASR specifically.

297k tokens
context cost
the whole folder, loaded on every use
98
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
1314
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/daymade/claude-code-skills --skill transcript-fixer

The instruction itself

15 sections, as written by the author

Transcript Fixer

默认模式:Claude 内置 AI(Native AI Correction)——无需任何外部 API key。

Stage 1 字典纠错(免费、即时)→ Claude 自己读原文做智能纠错 → compound 进字典。

Stage 3 API 仅用于无 Claude Code 的自动化批处理场景(备选)。

Two-phase correction pipeline: deterministic dictionary rules (instant, free) followed by AI-powered error detection. Corrections accumulate in ~/.transcript-fixer/corrections.db, improving accuracy over time.

What each phase is actually good at (calibration, not a rule): the dictionary shines on *recurring* errors — product names, common homophones, anything you've corrected before — at zero cost and zero latency. But on a fresh database, on high-quality ASR (e.g. transcripts from a strong engine like Whisper, Otter, or Feishu / Tencent-Meeting), or in specialized domains (finance, medical, legal), the dictionary often matches almost nothing — the errors that remain are proper nouns and domain terms it has never seen. There, the AI pass does essentially all the real work. Treat Stage 1 as a cheap pre-filter for known repeats, not as the primary corrector, and don't be alarmed when it changes only a handful of lines on a clean transcript.

Prerequisites

All scripts use PEP 723 inline metadata — uv run auto-installs dependencies. Requires uv (install guide).

The commands below use relative script paths (scripts/<name>.py), so they only work from the skill's own directory — and in agent harnesses the shell's working directory resets between calls, which surfaces as Failed to spawn: scripts/fix_transcription.py on the very first command. Take the skill directory from the "Base directory for this skill" line printed when this skill was invoked, and either cd there in the same command or prefix every script path with it. Do not rely on $CLAUDE_SKILL_DIR — it is unset in at least some harnesses (verified 2026-08), so a command built on it fails with the same error it was meant to prevent. If you no longer have the invocation line, find -L ~/.claude ~/.codex -name SKILL.md -path '*transcript-fixer*' locates the bundle — but it returns dozens of hits — every installed *version*, plus backups, staging copies and pre-edit snapshots — and the first is not the newest. Skip any path containing skill-before, -workspace, source-sync-backups, .tmp or .staging. Among what remains, prefer the highest version directory; some installs (a marketplace checkout, another agent's skills dir) carry no version at all, so if you end up choosing between those, take the one with the newest mtime and sanity-check it against this file's content before trusting it.

Quick Start

# First time: Initialize database
uv run scripts/fix_transcription.py --init

# Single file — Stage 1 runs in SAFE MODE by default: only low-risk
# (non-word, high-confidence) corrections auto-apply. Medium/high-risk ones
# (common words, <=2-char, real-word fragments) are written to
# *_needs_review.md for you / the AI pass to judge, not applied silently.
uv run scripts/fix_transcription.py --input meeting.md --stage 1

# Trust ONE project domain's rules (recommended for batches): rules of the
# domain you explicitly pass via --domain apply at every risk level — they were
# hand-confirmed for this project's vocabulary, so domain match = trust. The
# roster and everything else keep safe-mode deferral. One pass instead of three
# (safe run -> review sidecar -> --apply-all rerun).
uv run scripts/fix_transcription.py --input meeting.md --stage 1 --domain myproject --apply-domain

# Apply EVERY risk level regardless of origin (the pre-safe-mode behavior).
# Higher false-positive risk — only when you've reviewed ALL loaded rules.
uv run scripts/fix_transcription.py --input meeting.md --stage 1 --apply-all

# Dry run: preview all Stage 1 changes (with risk levels) without writing *_stage1.md
uv run scripts/fix_transcription.py --input meeting.md --stage 1 --dry-run

# Extract likely ASR errors without applying any corrections
uv run scripts/fix_transcription.py --extract-uncertain -i meeting.md -o ./review

# Batch: multiple files in parallel (use shell loop)
for f in /path/to/*.txt; do
  uv run scripts/fix_transcription.py --input "$f" --stage 1
done

After Stage 1, Claude reads the output and fixes remaining ASR errors natively (no API key needed). The full method — triage by confidence, verify-don't-guess, second pass, needs-checking list — is in Native AI Correction below; read that section as the source of truth. For a quick, clean transcript it collapses to: read the domain's context file if one exists (~/.transcript-fixer/contexts/<domain>.md) → read the whole thing → fix the obvious one-off errors inline → --add any recurring or project-specific ones (especially names) to a --domain dictionary so they auto-fix next time (see "Project-Specific & Person-Name Corrections").

See references/example_session.md for a concrete input/output walkthrough.

⚠️ Stage 3 API — 备选方案(仅限无 Claude Code 的自动化批处理)

如果你正在 Claude Code 里运行此 skill,跳过本节——直接用上面的 Stage 1 + Native AI Correction,不要跑 --stage 3

Stage 3 是给 CI/脚本/无 Claude 环境的批量自动化用的,需要额外配置 GLM API key。

# 备选: 仅限无 Claude Code 的批处理
export GLM_API_KEY="<api-key>"  # From https://open.bigmodel.cn/
uv run scripts/fix_transcript_enhanced.py input.md --output ./corrected

See references/installation_setup.md for the full config-file format and references/glm_api_setup.md for GLM endpoint details.

Core Workflow

Two-phase pipeline with persistent learning:

  • Initialize (once): uv run scripts/fix_transcription.py --init
  • Add domain corrections: --add "错误词" "正确词" --domain <domain>
  • Phase 1 — Dictionary: --input file.md --stage 1 (instant, free)
  • Phase 2 — AI Correction(默认: Claude 内置 AI): Claude reads the Stage 1 output and fixes remaining errors natively — this is the primary path, no API key needed. The full method is under Native AI Correction below. 备选: --stage 3 API 模式仅限无 Claude Code 的自动化批处理(需额外配置 GLM API key——见上方 §⚠️ Stage 3 API)。在 Claude Code 内不要跑 --stage 3
  • Save stable patterns: --add "错误词" "正确词" after each session
  • Review learned patterns: --review-learned and --approve high-confidence suggestions

Domains: general, embodied_ai, finance, medical, tech, or custom (e.g., legal, gaming)

Learning: Repeated AI corrections are written to SQLite history; --review-learned turns high-confidence repeated patterns into pending suggestions, and --approve FROM TO promotes the exact suggestion into the dictionary.

New safety & review commands

  • Safe mode is the Stage 1 default: only low-risk (non-word, high-confidence) corrections auto-apply; medium/high-risk ones (common words, ≤2-char, real-word fragments) are tracked to *_needs_review.md instead of being applied silently. So Applied: 0 on a clean transcript is correct, not a bug — the risky rules are waiting in *_needs_review.md for you or the AI pass to judge. Pass --apply-all to apply every risk level (the old behavior); --review is kept as a deprecated no-op. This reconnects the risk classifier that was being computed and then ignored — but it does NOT eliminate every false positive: rules whose from_text is a 4+ char valid phrase are still graded low and auto-apply (see references/false_positive_guide.md → "The 4+ char real-word blind spot").
  • Preview changes before applying: --dry-run writes *_dryrun.md with every planned Stage 1 change and its risk level.
  • Always-on changes report: --changes-file writes *_changes.md with before/after/risk for every correction (on by default in safe mode).
  • Machine-readable status for callers (--json): prints ONE line of {applied, deferred, output_path, needs_review_path, input_unchanged, review_enqueued} on stdout (the human-readable log is routed to stderr for that run). Consumers read this instead of inferring a no-op from whether *_stage1.md exists on disk — input_unchanged: true (or output_path: null) is the authoritative no-op signal for a domain. This is a cross-skill contract (a caller's pre-classify chain consumes it); keep the field names and semantics stable (review_enqueued was added additively: how many safe-mode deferrals landed in the persistent review queue — see "Review Queue & Dashboard"). Without --json the human-readable output is unchanged.
  • Extract uncertain ASR tokens: --extract-uncertain -i file.md writes *_uncertain.md with likely errors (short all-caps tokens, transliteration fragments, repeated words) without changing the file.
  • Load domain presets: --load-presets tech imports a curated set of tech/Claude Code ASR corrections.
  • Report false positives: --report-false-positive "错误词" "正确词" -d domain disables a bad dictionary rule and lowers its confidence.
  • Audit for risky rules: --audit flags existing rules that look like false-positive sources (common words, ≤2-char, substring collisions, and — with jieba — 4+ char real-word phrases). It is advisory: it surfaces candidates, it does NOT disable anything. Disabling is a human decision — review each hit by hand and back up the DB first, because the audit cannot know your context and mislabels a large fraction of good rules (e.g. GDP 5.5→GPT 5.5 looks wrong generically but is a correct fix for an AI-heavy user). See references/false_positive_guide.md.

When called by another skill (cross-skill invocation contract)

This skill is often wired into another skill's ingest pipeline — e.g. a meeting-sync skill runs Stage 1 as a pre-classify hook before filing the transcript. That caller pipeline changes one assumption that bites silently, so a caller MUST follow this contract or it will run Stage 1, apply almost nothing, and report success.

The failure mode (verified, reproducible). Safe mode defers medium/high-risk corrections to *_needs_review.md rather than applying them. On a single file you edit by hand, that's fine — you read the sidecar next. But a caller pipeline typically runs transcript-fixer inside a TemporaryDirectory and reads only the corrected transcript.txt back out. The *_needs_review.md sidecar lives in that temp dir and is deleted with it — so 95%+ of the dictionary's corrections silently vanish while the run reports "complete." Real measurement on a 95-minute transcript with a 108-rule domain: safe mode applied 2/108, deferred 106 to a sidecar that was immediately discarded. The run looked clean; only ~2% of known corrections landed. The user then had to run transcript-fixer a second time by hand to get the other 98%.

Caller rule — pass --apply-domain for hand-confirmed project domains. The domains a pipeline wires in (its config domains: list) are exactly the domains whose rules a human already curated for that project's vocabulary. A domain match there is not a guess — it's a confirmed fix — so the pipeline should trust it the same way a batch run does:

# CORRECT for a caller pipeline — trust the configured project domains
uv run scripts/fix_transcription.py --input "$staged" --stage 1 \
  --domain "$domain" --apply-domain --json

With --apply-domain, the same 108-rule run applies 97/97 at low risk instead of 2/108. The general domain (catch-all, lower curation) can stay in safe mode — only the project-specific domains earned full trust. If a caller cannot pass --apply-domain, it MUST instead read deferred from the --json status object and either persist the *_needs_review.md sidecar to a non-temp location for a downstream pass, or surface a non-zero deferred count to the user as a failure. Silently dropping deferred corrections and reporting success is the bug.

The --json status line is the contract surface. It prints {applied, deferred, output_path, needs_review_path, input_unchanged} on one stdout line. deferred is the number that must not be silently lost. input_unchanged: true / output_path: null is the authoritative "0 corrections this domain" signal — do NOT infer no-op from whether *_stage1.md exists on disk (the file-presence check is what once aborted the whole chain and dropped corrections). Keep these field names and semantics stable; a caller's pre-classify chain depends on them.

The complementary side: keep the dictionary warm. A caller pipeline that trusts --apply-domain only delivers value to the degree its project domain is populated. Every confirmed correction the downstream native pass makes should be --added back to that domain (--add "ASR-variant" "correct" --domain <project>), so the next ingest auto-fixes it and the native pass keeps getting lighter. A cold domain + --apply-domain still applies almost nothing — the fix is --apply-domain *and* ongoing --add discipline together.

After fixing, always save reusable corrections to dictionary. The skill's core value — see references/iteration_workflow.md for the complete checklist.

Dictionary Addition After Fixing

After native AI correction, review all applied fixes and decide which to save. Use this decision matrix:

| Pattern type | Example | Action |

|-------------|---------|--------|

| Non-word → correct term | 克劳锐→Claude, cloucode→Claude Code | ✅ Add (zero false positive risk) |

| Rare word → correct term | 拉行链→LangChain, 哈金费斯→Hugging Face | ✅ Add (verify it's not a real word first) |

| Person/company name ASR error | 卡帕西→Karpathy, Anthropics→Anthropic | For important recurring people, add to your people roster instead (see "People Roster" below) — it carries relationship context and survives DB resets. For one-off names: ✅ --add --domain (stable, unique) |

| Common word → context word | 争→蒸, 减→剪, affect→effect | ❌ Never add as a rule — record the trap + its disambiguating cue in the domain's context file instead (see "Domain Correction Contexts") |

| Real brand → different brand | Xcode→Claude Code, Clover→Claude | ❌ Skip (real words in other contexts) |

| Real name → different real name | 李明黎明 (two real people in different projects) | ❌ Never a rule — same hazard as real brand → brand, but it corrupts a real person's name. Domain context trap with a disambiguating cue instead (see the user-verdict refinements in Native AI Correction step 4) |

The middle path, and it applies to exactly one of the ❌ rows. The

*common word → context word* row () forbids a bare common word as a

rule, because it fires everywhere the word is legitimately used. It does not

forbid the same fix carried by enough surrounding text that the phrase only

occurs in the mishearing — 村里商量<name>商量 is defensible where bare

村里 would be reckless. **The *real name → different real name* row is not

relaxed by this and never anchored into the dictionary**: keep it in the domain

context file as the row itself says.

That exclusion holds because **the validator cannot be trusted either way on a

person's name.** --add runs a jieba check that warns when the FROM side

decomposes into all-known words, and whether a name counts as "known" is an

accident of jieba's dictionary: measured, 李娜商量 warns (李娜 has frequency

438) while 张伟商量 is silent (张伟 is out-of-vocabulary, frequency 0). So a

name-anchored rule that passes quietly tells you nothing, and one that warns

tells you nothing either. With no reliable signal on the class whose blast radius

is a real person's name in every future transcript, the row stays out. (The same

reasoning excludes the *real brand → different brand* row: XcodeClaude Code

is right in one project and destroys a build log in the next, and no validator

knows which one you are in.)

Warning versus error, because they end differently. A valid_phrase warning

means *review this by hand*, not *it was rejected* — the rule is added and

--add exits 0. common_word and both_common are errors: --add exits 1

and writes nothing, and --force is the only way past. substring_collision

is *both*, depending on which branch fires — a hit against the curated collision

map is an error, while the broader dynamic check is only a warning and the rule

lands. So read the exit status rather than the noise: a loud add may have

succeeded, and a rule you believe you saved may not be in the database at all.

Reach for --force only after reading *which* check objected, since it silences

the blocking ones too.

One caveat decides whether an anchored rule is worth adding: anchor to a

recurring collocation, not to a one-off sentence fragment. A snippet of one

particular sentence never matches again — it costs a dictionary row, compounds

nothing, and dead rows are what make a domain slow to load and hard to audit.

When even a collocation would be too narrow, the trap belongs in the domain

context file with its disambiguating cue.

Batch add multiple corrections in one session:

uv run scripts/fix_transcription.py --add "错误1" "正确1" --domain tech
uv run scripts/fix_transcription.py --add "错误2" "正确2" --domain business
# Chain with && for efficiency

Review Queue & Dashboard (uncertain items → one-keystroke verdicts)

Confirmed corrections compound through the dictionary; uncertain ones used to

evaporate — the native pass listed them in chat (gone when the session ends),

safe-mode deferrals sat in a *_needs_review.md sidecar (discarded by temp-dir

callers), and learned suggestions waited behind a CLI nobody ran. The review

queue gives all three one persistent home in corrections.db (review_items),

and the dashboard makes deciding them nearly free — that friction is what stood

between "AI suspects an error" and "the dictionary learns the answer."

Queue CLI (all support --json):

# Enqueue uncertain items (native pass step 7 does this; '-' reads stdin)
uv run scripts/fix_transcription.py --enqueue-review items.json
# Inspect
uv run scripts/fix_transcription.py --list-review            # pending, priority-sorted
uv run scripts/fix_transcription.py --show-review 12         # full evidence + action pack
# Decide (agent path — humans use the dashboard)
uv run scripts/fix_transcription.py --resolve-review 12 --decision accepted --by reviewer
uv run scripts/fix_transcription.py --resolve-review 12 --decision overridden --override-to "正确词" --note "<evidence>"
uv run scripts/fix_transcription.py --resolve-review 12 --decision kept_original   # transcript was right
uv run scripts/fix_transcription.py --resolve-review 12 --decision reopen          # undo (reverts applied edits)

Each item carries: the original text (left untouched in the file), a pre-filled

suggestion, kind (entity/unknown lead the queue — they compound into

dictionary+roster; homophone/wording trail), the evidence your search ladder

produced, and an optional action pack executed on accept: file_edit

(replace in the transcript), dict_add (add to a --domain dictionary),

append_note (add a trap line to a domain context file). No action pack + a

file anchor = the default single file_edit.

Fail-closed anchor guard: the whole action pack is planned in memory

against the CURRENT file state (each edit validated against the content as the

pack's previous actions left it), and only when every action plans successfully

does anything reach disk — original text missing (file edited since enqueue),

ambiguous (multiple occurrences with no unique winner near the line hint), or a

drifted context (no nearby line matches the snippet recorded at enqueue) →

nothing is written, the CLI exits 2 with a {"error": "re_anchor_needed"}

status object, and the item stays pending. A wrong auto-edit is worse than a

missed one. Machine callers should parse the stdout error field rather than

the bare return code (argparse usage errors also exit 2). On overridden, only

retargeted file_edits run — suggestion-specific dict_add/append_note

actions are dropped (they were planned for a suggestion the human rejected).

(One scope note: the context check only runs when the original occurs MORE

THAN ONCE — a unique occurrence has no look-alike to refuse, so a

single-occurrence edit applies without consulting the snippet.)

When the guard refuses: --reanchor-review repairs the item. A refusal is

not a dead end and NOT a cue to hand-edit the file around the queue — that

leaves the item pending forever and the edit unaudited. Run the re-anchor and

then verdict again:

uv run scripts/fix_transcription.py --reanchor-review <id> [<id>...]
# file itself is gone (moved/renamed/cleaned)? add search root(s):
uv run scripts/fix_transcription.py --reanchor-review <id> --reanchor-root <dir-with-transcripts>

Two drift shapes are repaired against current disk state, both fail-closed:

context/line drift (file edited since enqueue — re-locates original in

the file, preferring lines that still match the RECORDED context snippet over

mere distance, refreshes line + verbatim context) and file gone (searches

the recorded parent dir plus every --reanchor-root for *.md containing

original; exactly one candidate re-points the anchor, zero changes nothing,

and multiple asks for --reanchor-to FILE — the explicit-target form, which

is itself refused if original is not in it). After a successful re-anchor,

the guard's context check passes and A/W/CLI resolve proceed normally

(explicit action packs get their file_edit path rewritten to the new file).

The refusal messages themselves name this command. (Root-caused 2026-08-03: an item

enqueued with a PARAPHRASED context could never be verdicted — the human's

override died at the guard and the file got hand-edited around the queue

before this command existed.)

**Enqueue validates anchors verbatim — authoring errors die at enqueue, not

at verdict.** When an item declares a readable file, --enqueue-review

checks that original (and context, if given) literally appears in it, and

repairs a line hint that points beyond the resolve window (±3 lines) of a

UNIQUE match (a hint inside the window works as-is and is left alone; repairs

are printed to stderr). Anything else is REJECTED on the spot with the

reason, and the run exits 3 — the JSON carries the rejects under

rejected_unanchored (items under added WERE enqueued; fix the rejects and

re-enqueue them). context must be copied verbatim from the file; a

paraphrase drifts the anchor at the first surrounding edit. (Files that don't

exist yet are not validated — e.g. items enqueued for a file on another

machine; the resolve-time guard owns that case. stage1_deferred items are

also exempt — their from_text is the engine's evolving text after earlier

rules applied in-memory, legitimately not in the input file yet.)

One verdict fixes one occurrence — sweep the siblings yourself. A resolved

item edits exactly one span. When the original text occurs several times the

guard does not edit them all: it picks the occurrence nearest the recorded line

hint whose context matches, and refuses (re_anchor_needed) when it cannot

choose — no line hint at all, nothing matching near the hint, or two occurrences

equally near it. Either way the other occurrences are left standing,

including on the very line the verdict just edited, which is

where a repeated name is most likely. Measured on one real batch: ten items

resolved, four of them left six more occurrences behind, two of those on a line a

verdict had already touched. So a verdict batch has a second half:

# 1. See what was actually decided. The default listing shows PENDING only —
#    the items you just resolved are precisely the ones it hides.
uv run scripts/fix_transcription.py --list-review --review-status accepted
uv run scripts/fix_transcription.py --list-review --review-status overridden
# 2. Read the verdict that was recorded, per item.
uv run scripts/fix_transcription.py --show-review <id> --json

Take the replacement from resolved_text, never from the listing line. On

an override the human's typed text lands in resolved_text while

suggested_text still holds the suggestion they *rejected* — and the

human-readable listing prints the suggestion. Propagating from that line pushes

the rejected answer into every remaining occurrence, which is worse than leaving

them alone. An override is free text, so read it before propagating: a typo

typed once otherwise becomes a typo in five places.

Fix the remaining occurrences with Edit, or a sed scoped to that one file

— this is within-file propagation of a decision a human already made, not the

cross-file find-and-replace the batch rules forbid — then re-grep to confirm.

Sweep entity-kind items only. A homophone or wording verdict is a

judgement about *that sentence* — those are the context-dependent class step 5

says to anchor to surrounding text, and the class the row keeps out of

blanket rules. Propagating one across a file is the mistake the dictionary matrix

exists to prevent.

**And within entity, a verdict settles the entity, not every token that sounds

like it** — this is step 4's carve-out, unchanged. An occurrence that is a

*referred-to* third party rather than the person being addressed ("I'll ask

<token> from the bank") can legitimately need the opposite answer: leave it and

enqueue it on its own. A verdict the human reached by listening to one clip

deserves the same caution — those seconds of audio settle that utterance, and a

second occurrence is a second utterance. Sweep the occurrences that are plainly

the same entity in the same sense; that is the ordinary case, and the one the

measurement above counted.

Sweep after the whole batch is resolved, not between verdicts. A swept

occurrence that a still-pending item is anchored to will fail that item's guard

(re_anchor_needed, exit 2) and have to be re-enqueued.

An override does not compound on its own — finish it with --add. On

overridden the queue drops the dict_add / append_note actions (they were

planned for the suggestion the human rejected), so the strongest signal in the

whole loop — a human personally correcting the AI — is the one case that never

reaches the dictionary unless you put it there:

--add "<original>" "<resolved_text>" --domain <project>, subject to the

real-word rules above.

Dashboard (single reviewer, local):

uv run scripts/review-dashboard/server.py   # opens http://127.0.0.1:8767

Prodigy-style single-focus card: live file context with the anchor line

highlighted, suggestion pre-filled, evidence shown, keyboard-first —

Q play the utterance · A accept · R original-is-correct · W override

(type the right text) · S skip/can't judge · Z undo · ↑↓/J K navigate

(verdict keys deliberately cluster on the left hand; the right hand stays on

the mouse). Env knobs: REVIEW_DASHBOARD_PORT (default 8767),

REVIEW_DASHBOARD_NO_BROWSER=1 to skip auto-opening a browser tab.

Reads go straight to the DB (read-only); every write shells out to the CLI,

so the state machine, anchor guards, and audit log stay the single source of

truth, and agent (CLI) and human (page) are equal writers.

Audio playback (Q) — often the reviewer can't judge a garbled utterance

from text alone; hearing the original second settles it. A transcript opts in

by declaring its recording EXPLICITLY in frontmatter (no implicit directory

scanning — if the field is absent, the card simply has no play button):

---
date: 2026-08-02
minute_token: abc123
audio: /absolute/path/to/recording.m4a
---

The audio: line is the one you add; the others stand for whatever the

transcript already carries. It is written bare on purpose — see below, and

note that this example is copied verbatim often enough that a trailing #

annotation on that line has shipped as a real bug more than once.

**Add the line to the block the transcript already has — do not append a second

one.** A synced transcript normally arrives with frontmatter (date,

minute_token, participants…), and the parser stops at the first ---

terminator it meets, so a second block below it is never read.

Write the value bare — no trailing comment. The parser takes everything after

the first colon (line.split(":", 1)[1].strip()) and does not strip #, so

audio: /path/x.m4a # same timeline becomes a path ending in # same timeline,

which does not exist. Same for the block's shape: it must open at line 1, be

closed by its ---, and the key must sit unindented.

Every one of those mistakes fails the same way — the card shows **no play button

and no error**, which reads exactly like "this transcript has no audio." If a

card you expected to have audio doesn't, suspect the frontmatter before you

suspect the recording.

The file must be on the same timeline the transcript's timestamps refer to

the exact file fed to the ASR. A transcript produced from a 1.3x-speed input

pairs only with the 1.3x file; pairing it with the original makes every clip play

the wrong seconds.

The dashboard derives the clip window from the speaker-timestamp lines

(<speaker> HH:MM:SS.mmm) around the anchor, streams the file with HTTP Range

(instant seek, no full download), and plays just that utterance; ± 3s widens

the window when the cut lands mid-sentence. Verify the timeline pairing once

per recording source (ffprobe duration ≈ the transcript's last timestamp) —

a mismatched speed rate plays the wrong seconds everywhere.

Wiring audio for a Feishu-minute transcript (the common case when the

transcript came from a minutes-sync pipeline) — use the bundled script, which

does the download, the timeline check, and prints the frontmatter line:

uv run scripts/fetch_minute_audio.py \
  --token <minute-token> --profile <lark-cli-profile> \
  --output ~/.transcript-fixer/cache/audio/<name>.m4a \
  --transcript <path/to/transcript.md>

Both arguments come from outside the transcript's body. --token is the

minute_token: field in the transcript's own frontmatter (a minutes-sync

pipeline writes it there; if it is absent, the minute URL's last path segment is

the same value). --profile is a lark-cli profile name — list them with

lark-cli profile list and pick the one belonging to the account that owns the

recording; the transcript does not record it, so if the owner is not obvious,

ask rather than guess (a wrong profile fails in the silent way described below).

Keep the audio outside the docs repo — a media blob should not ride into its git.

Exit codes — check the status, not the output: diagnostics go to stderr while

the audio: line goes to stdout, so a run that verified nothing still prints a

usable-looking line.

| code | meaning |

|---|---|

| 0 | verified — audio and transcript share a timeline |

| 1 | timeline mismatch: a file downloaded, but do not wire it |

| 2 | downloaded, pairing unverified — ffprobe absent or its output unusable, no --transcript, the transcript has no <speaker> HH:MM:SS.mmm lines, or every one of them is 00:00:00 (argparse also exits 2 on a malformed invocation; its message says so) |

| 3 | nothing usable produced — bad --transcript path (checked before any network work), or the fetch failed: lark-cli errored, curl failed, the download was too small, or the --profile cannot read this minute, which is the most common cause and is not a bad token |

A 2 caused by missing speaker-timestamp lines is worth stopping for rather than

working around: the dashboard builds its clip windows from those same lines, so

audio wired to such a transcript has nothing to play.

The by-hand route, for when lark-cli is unavailable or the script fails:

mkdir -p ~/.transcript-fixer/cache/audio && cd $_   # --output below accepts only
                                                    # a relative path inside the
                                                    # CURRENT dir ("../" refused)
LARK_CLI_NO_PROXY=1 lark-cli minutes +download \
  --minute-tokens <token> --profile <profile> --output ./audio.m4a
# If that trips the SSRF guard, take the signed URL and fetch it yourself.
# Parse the envelope as JSON — a regex scrape leaves escapes literal and
# truncates the URL at its first parameter:
URL=$(LARK_CLI_NO_PROXY=1 lark-cli minutes +download \
        --minute-tokens <token> --profile <profile> --url-only \
      | python3 -c 'import sys,json
raw = sys.stdin.read()                      # the CLI may print prose around the
s, e = raw.find("{"), raw.rfind("}")        # JSON, so isolate the object first
print(json.loads(raw[s:e+1])["data"]["download_url"])')
[ -n "$URL" ] || { echo "no download_url — check the profile"; exit 3; }
curl -sSL --noproxy '*' -o audio.m4a "$URL"
# Verify the pairing yourself: compare the duration against the transcript's
# LAST speaker timestamp. Treat a gap over max(60s, 5% of that timestamp) as a
# mismatch — recordings usually run a minute or two past the last utterance,
# but a speed-rate mismatch shows up as a large proportional gap.
ffprobe -v quiet -show_entries format=duration -of csv=p=0 audio.m4a

Three things the script encodes, each of which is a real failure by hand:

  • lark-cli's own SSRF guard refuses its own download host. The error is

blocked download URL: local/internal host is not allowed — Feishu's

signed-download domain is literally named internal-api-drive-stream.… and

the internal- prefix trips the guard. The fallback is --url-only plus your

own curl -L, which is what the script runs.

  • The --url-only envelope is real JSON — parse it, don't pattern-match it.

The URL lives at data.download_url (nested, not top level), and a regex

scrape leaves JSON escapes such as & literal, producing a URL that

truncates at its first parameter and downloads a redirect stub instead of

audio. json.loads handles this natively and a hand-rolled extraction is

where the escaping bug comes from.

  • **A minute is a per-tenant, per-user resource, so the --profile is the part

that usually fails, not the token.** A profile from another tenant — or one

the minute was never shared with — authenticates fine and still returns no

download_url. Pass the profile belonging to the account that owns the

recording.

Wire the audio before enqueueing items you intend to have judged by ear

(step 4 routes cross-language proper nouns there) — otherwise the reviewer opens

a card with no play button and no way to answer the question you asked.

Stage 1 integration: safe-mode deferrals are auto-enqueued

(source: stage1_deferred) at run time, so a caller discarding the sidecar no

longer loses them. Exception: an input under the OS temp dir is NOT enqueued

(the anchor would be a dead pointer once the staging copy vanishes) — the

--json deferred count still reports those to the caller, and the additive

review_enqueued field says how many landed in the queue.

False Positive Prevention

Adding wrong dictionary rules silently corrupts future transcripts. Read references/false_positive_guide.md before adding any correction rule, especially for short words (≤2 chars) or common Chinese words that appear correctly in normal text.

Project-Specific & Person-Name Corrections (--domain isolation)

The most important pattern for recurring, project-specific errors — person names, project jargon, product codenames — is the --domain flag. It is also the *answer* to the false-positive worry above: a person-name fix that's right in your project (a teammate's name the ASR keeps garbling) might collide with a real, differently-spelled person in someone else's transcript — so it must NOT go into the global (general) dictionary.

--domain makes such rules safe by isolating them:

# Add the rule under an isolated, project-named domain (not 'general')
uv run scripts/fix_transcription.py --add "<ASR-garbled-name>" "<correct-name>" --domain <project>
# Apply ONLY that domain's rules to this project's transcripts
uv run scripts/fix_transcription.py --input meeting.md --stage 1 --domain <project>

A rule added under --domain <project> only fires when you pass --domain <project> at correction time. Other projects (their own domain, or default all) are unaffected — so even a risky short-word / common-word person-name rule is safe, because it only fires inside the project where it's correct.

Why this beats a one-off script (the core value, do not skip)

Facing a transcript — or a whole batch — full of the same ASR-garbled names, the tempting move is a quick sed / python find-and-replace. Don't. That is the single biggest anti-pattern with this skill:

  • A throwaway script fixes *this batch* and the knowledge then evaporates: next batch, next week, next project, you rewrite it from scratch. It does not compound.
  • The dictionary compounds: --add once, and every future transcript auto-corrects via --stage 1 --domain <project>. Wire that one command into the project's ingest step and the names are fixed forever, for free.
  • The dictionary has false-positive protection (short-word warnings, the audit command, --report-false-positive); a raw sed has none and will silently corrupt look-alike words.

Rule of thumb: recurring or project-specific error → --add ... --domain <project> (it compounds). Never a throwaway sed/python replace. A one-off script is acceptable only for a genuinely one-time, never-recurring fix — and even then the dictionary is usually less effort.

ASR is especially unstable on Chinese names: one person can shatter into a dozen homophone variants (in one real project a single surname+given-name was seen as 13+ [姓变体]×[名变体] combinations). Capture every confirmed variant with --add --domain <project> so they all collapse to the canonical name on every future run.

People Roster (long-term person-name SSOT)

For important recurring people whose names ASR consistently garbles

(coworkers, clients, family, workshop attendees), maintain a people roster

markdown file — the SSOT for person names — rather than adding them one-by-one

to the DB. Transcript-fixer auto-loads person-name corrections from this roster

at Stage 1 time when people_roster_path is set in

~/.transcript-fixer/config.json.

Roster format (canonical: ### Name + - ASR 变体: variant1, variant2):

### Nina Zhao
- **ASR 变体**: Nena, 妮娜

### 小雨
- **ASR 变体**: 晓雨, 小宇老师

Both example shapes are worth copying. An English given name spoken inside

Chinese speech produces *two* kinds of variant — a misspelling (Nena) and a

Chinese transliteration (妮娜) — and a Chinese nickname produces homophone

variants plus honorific forms (小宇老师). List every form you have actually

seen; each one is a rule that fires for free.

Setup (once):

# Edit ~/.transcript-fixer/config.json and add:
#   "paths": { "people_roster_path": "/path/to/people.md" }

After this, every --stage 1 run automatically merges roster corrections

(in-memory only — never written to DB). The DB always wins on conflicts, so the

roster fills gaps without overriding hand-tuned entries. See

scripts/core/people_roster.py for the parser.

When to use the roster vs --add to DB:

| Person | Go to | Why |

|--------|-------|-----|

| Long-term recurring (coworker, client, family, workshop attendee) | people.md | SSOT with relationship context; survives DB resets |

| One-off / minor name | DB (--add --domain) | Quick, no context needed |

Domain Correction Contexts (per-domain AI priors)

The dictionary handles deterministic replacements; the people roster handles names. A third class of error can't safely live in either: context-dependent homophones — words that are only wrong in a particular discussion context. Think in a meeting about producing N video clips per day, or a finance call where a common word collides with a ticker nickname. A dictionary rule on a common word silently corrupts every other transcript, and a generic AI pass lacks the domain prior to fix it confidently — it either guesses wrong or leaves it for the human. (Real case: a transcript had four 减到 N 条 occurrences that all meant 剪到; the AI pass suspected but wouldn't touch them without a domain prior, and the user had to fix them by hand.)

Domain context files close this gap. One markdown file per domain, in user space next to your corrections.db and people.md (never inside the skill bundle — it survives skill updates and keeps project knowledge private):

~/.transcript-fixer/contexts/<domain>.md

(If you relocated the config dir via TRANSCRIPT_FIXER_CONFIG_DIR, contexts live under that dir's contexts/.)

During native correction (see workflow below), read the transcript's domain context file before triaging. It should contain three things:

  • One line of business context — what this domain's recordings are usually about
  • Known homophone traps — each with the *contextual cue* that disambiguates it ("when the sentence is about producing/editing clips, is intended"), optionally with a dated real example
  • Pointers to authoritative name sources — the project's alias ledger, the relevant people-roster section, existing DB domains — so the verification ladder (step 4 below) knows where to look first

What must NOT go in a context file: hard replacement rules. 减→剪 as a rule belongs in NEITHER the context file NOR the dictionary — the file primes your judgment with priors and cues; it never authorizes blind replacement. Every fix still goes through the confidence triage below.

Maintenance loop (mirrors the dictionary's --add habit): when a native session surfaces a context-dependent recurring error — you fixed it here, and it'll recur in this domain's future transcripts — append it to the domain's context file with its disambiguating cue. Deterministic non-word/name fixes keep going to --add --domain / the roster as before.

Format and a worked template: references/domain_context_guide.md.

Note: contexts are consumed by the native workflow (the agent reads the file — no code involved). API mode (--stage 2/3, the backup channel) does not inject them yet; if that channel gets completed, the same files should feed its prompt.

Native AI Correction (Default Mode)

When running inside Claude Code, use Claude's own language understanding for Phase 2 — on high-quality ASR this is where almost all the real correction happens. Scale the effort to the transcript. Don't turn a 10-second memo into a research project, but don't starve a 90-minute strategy call either. Pick the tier from the recording's shape, not your mood:

| Signal | Fast tier (minutes, not hours) | Full tier (the whole ladder earns its keep) |

|---|---|---|

| Length | short (≤ ~15 min / a few hundred lines) | long (30+ min / 1000+ lines) |

| Speakers | one or two, names you already know | 3+ speakers, or unfamiliar names |

| Vocabulary | plain language, no domain jargon | domain-heavy (finance/medical/legal/project codenames) or many proper nouns |

| Stakes | internal memo, throwaway | client-facing, committed to a shared repo, drives a decision |

  • Fast tier — Stage 1 (--apply-domain), read the domain context file if one exists, read the whole thing once, fix the obvious one-off errors inline, --add any recurring/project-specific term to a --domain. Skip: the cross-domain name ladder, the second-pass subagent, the needs-checking ceremony. One linear pass, done.
  • Full tier — everything below: full triage with the name-verification ladder, the independent second-pass subagent, and an explicit needs-checking list. The effort is justified because a long/domain-heavy transcript has both more errors *and* harder-to-confirm ones, and a wrong proper noun committed to a shared repo propagates.

A recording can be long but still fast-tier (two known speakers, plain language) or short but full-tier (a 5-minute call full of unfamiliar drug names that feed a report). Let the *vocabulary and stakes* call the tier, with length as a tiebreaker — that's where the real work is.

Correction scope includes the metadata lines, not just the body. A filed transcript usually carries ASR-derived metadata — a Keywords: line, frontmatter, a title — and those lines contain the *same* recognition errors as the spoken body (e.g. a Keywords: line still listing 克劳锐 when every body mention was already corrected to Claude). Fix them with the same rules. There is no "metadata is sacred, leave it" exception: the metadata is a search/grep surface too, and a keyword left in its ASR-garbled form will silently fail every future grep Claude while the body looks clean. When you re-grep the final file to confirm a correction landed, include the metadata lines in that check.

  • Run Stage 1 (dictionary) on all files (parallel if multiple)
  • Verify Stage 1 — diff against the original. If the dictionary introduced false positives, work from the original file instead and apply your edits there.

And when the input already passed through an automated corrector (a sync pipeline's pre-classify stage, a previous Stage 3 API run), your input is NOT raw ASR — upstream corrections are baked in with no evidence trail. Before triaging, diff against the raw source (the caller's raw transcript — sync engines typically keep one alongside the corrected copy, e.g. transcript_raw.txt — or re-pull from the source API). Two things fall out of that diff, in opposite directions: (a) every upstream entity swap is itself a suspect in step 4's triage, because an upstream AI "correction" can be a fluent wrong guess — real case: raw ASR 「新的车辆」 was "smoothed" by a pipeline AI into 「新出来的反馈」 (grammatical, plausible, wrong: the speaker said a near-homophone name), and only the raw diff caught it; (b) what upstream already fixed correctly is settled — check the diff *before* proposing a fix that's already applied, or you redo work and risk "fixing" a correct form back to a wrong one

  • Load the domain's priors, then read the entire transcript. If ~/.transcript-fixer/contexts/<domain>.md exists for this transcript's domain, read it first — it primes which homophone traps to suspect and names the authoritative sources for step 4's ladder (see "Domain Correction Contexts" above). Then read the entire transcript before proposing corrections — later context disambiguates earlier errors (a name garbled near the start often becomes obvious later). For large files, read in chunks but finish the whole thing before deciding anything
  • Triage each candidate error into one of three buckets — this triage is the part that takes judgment. First override three reflexes that repeatedly misfile names (all three are real, recurring failures — they send a fixable name straight to "ask the user"):
  • Speaker labels first — the transcript usually already holds the names.

Before searching anywhere, collect the set of speaker labels in the file;

if a garbled token matches one by SOUND, it is almost certainly that same

person, and the label carries the spelling — a label is copied from a name

*registry* (a human annotating the recording, or the attendee list /

voiceprint enrollment a diarizer matched against), while the body is raw

ASR of a spoken sound. Four qualifiers, and the first one is not optional.

**(a) Apply it only to a name being ADDRESSED or SELF-INTRODUCED, never one

being REFERRED TO.** "hi, <token>" and "my name is <token>" identify a

speaker; "I'll ask <token> from the bank" identifies a third party who may

merely *sound* like one — and rewriting them to a speaker's name corrupts a

real person, the exact hazard the dictionary table calls "Real name →

different real name ❌ Never a rule". Two acoustically identical tokens in

one file can need opposite answers for this reason alone, so a referred-to

name keeps walking the ladder rather than resolving here.

**(b) Match against ALL labels — including, but not limited to, the one

above the block** — a garbled name usually sits in a block spoken by

someone else (A greeting B), and sometimes in that speaker's own block

(a self-introduction). **(c) The label settles WHO; the roster still

settles the canonical spelling** — a hand-typed Joe normalizes to the

roster's Jo, spelling only, never length. **(d) Labels annotated by a

human are a human identification: apply them, and do NOT put that name on

the needs-checking list or ask the user to confirm it — they answered it by

labeling it.** (Real case: walked the whole ladder on a name printed above

every one of that speaker's blocks, found nothing, then asked the user to

confirm it. They had labeled it themselves.)

When you cannot tell whether labels were hand-annotated or auto-assigned,

assume auto-assigned: voiceprint matching can attach a perfectly

spelled name to the wrong speaker, and that never looks garbled — so treat

the label as a strong candidate to confirm through the ladder, not as a

stop condition. Fall through to the ladder for 说话人 N / Speaker N,

role labels (主持人 / Interviewer), a third party who is not one of the

speakers, or a label that is itself visibly garbled. Fix the body only

— never edit a label or reassign who said what — keep the edit minimal

(do not expand a given name into a full name nobody said), and --add the

confirmed variant to a --domain so the next transcript fixes it free.

  • Judge ASR errors by SOUND, not by glyphs. Chinese ASR errors are homophone / near-homophone substitutions, so decide "same entity?" by pronunciation, not by whether the characters match exactly. A name that comes through as X小Y when the roster or dictionary already holds X晓Y (小/晓 are the same sound) is the same person → Confident fix — do NOT downgrade it to Uncertain just because 小≠晓 on the page. Same logic for a foreign name whose syllables all map by sound to a near-homophone transliteration. The dictionary having a sound-alike canonical is *evidence for* the fix, not a mismatch to be dismissed.
  • But sound similarity is *sufficient* evidence of identity, not *necessary* — and the exception is a whole class, not a rarity. A name spoken in one language while the engine transcribes another (an English given name inside Chinese speech, a transliterated surname) can come out phonetically unrelated to its canonical form, and — worse — as something that reads like a perfectly ordinary *different* real name. In one measured case a single person surfaced as three separate tokens, none a near-homophone of her name and each plausible as somebody else entirely; the tokens were recognizable only because all three sat where the same absent principal belonged, and what *confirmed* them was the human listening to those seconds of audio.

This does not reopen the (a) bullet above. That rule forbids resolving a *referred-to* token into one of the speakers' names, using speaker labels as the source — the failure mode where a third party gets overwritten with whoever is in the room. This class is the opposite direction: the token resolves to a known non-speaker whose canonical form comes from the roster or the project's ledger, and it is settled by the human's ear rather than by a label. Where the two are hard to tell apart, the (a) bullet wins and the token keeps walking the ladder.

So a candidate that fails the sound test but sits in a known person's slot is neither dismissed nor rewritten: enqueue it as kind: entity for audio verification (the dashboard's Q is the instrument for exactly this). Put your best candidate in suggested even when you doubt it — an item with no suggestion cannot be accepted at all (--decision accepted errors on it), so the reviewer would be forced to retype the answer for every card. Wire the transcript's audio *before* enqueueing (see the dashboard's audio section): the frontmatter is read live at view time, so adding it later does light up the play button — but editing the transcript shifts line numbers against the ±3-line window each item's anchor was recorded with, which is the expensive half to undo.

  • A name you can't place defaults to the search ladder below, NOT to asking the user. "Only the user knows this name" is the single most common wrong reflex. The canonical spelling is almost always already on this machine under a different project's domain — so you must query all domains at once (the cross-domain SQL in the ladder below), not the one domain you happened to pass to --stage 1, which may be brand-new and empty. Querying only that one and giving up looks exactly like "I checked" while finding nothing that was right there.
  • Confident fix — non-words, obvious garbling, product-name variants you already recognize, or a homophone that's unambiguous in context (theirthere where context forces it; 彭波彭博 when every other mention already reads 彭博). Apply directly (step 5).
  • Needs verification — a proper noun you can't confirm from context: a person / company / ticker / product / place name (a misheard drug name in a medical interview, a researcher's surname in a podcast, a ticker on an earnings call), or any term you can't point to a specific source for — even one you think you recognize ("I'm pretty sure" is exactly how wrong names slip in). Resolve it through a local-first search ladder before asking the user. For project / personal entities the authoritative spelling almost always already lives on this machine, and WebSearch is near-useless on internal names — it returns wrong same-name people, or nothing — and worse, a fluent wrong guess becomes a confident fix that's hard to catch later. Search in this order:
  • People rosterpeople.md (or wherever people_roster_path in

~/.transcript-fixer/config.json points). This is your curated SSOT

of long-term recurring people with their ASR variants annotated under

- ASR 变体:. A garbled name that already maps to a canonical

person here — e.g. NenaNina Zhao, 小宇老师小雨 — is a

Confident fix: apply immediately. **This one step replaces asking the

user for every name they've already documented.** Skip only for

transcripts whose speakers are confirmed NOT in the roster.

  • All domains of corrections.db, not just the current --domain. The same entity shatters into different ASR variants across projects, and every prior fix already collapsed them to the canonical name — so the answer is often sitting in another domain you didn't pass to --stage 1. Checking only the current domain and giving up is the recurring failure mode.

sqlite3 ~/.transcript-fixer/corrections.db "SELECT from_text, to_text, domain FROM active_corrections WHERE to_text LIKE '%<fragment>%' OR from_text LIKE '%<fragment>%';"

  • Project delivery docs & the alias ledger — cost reports, review sheets, deliverables, PKM notes for that project. These are human-written correct spellings, the strongest possible source. grep -rl "<fragment>" <project-dir> then read the hits. (The domain context file you loaded before triage usually names the project's alias ledger explicitly — start there.) Read every name table the ledger holds, not just the one that looks like "the speaker list." A project's people are almost always split across role-based tables — internal speakers, external collaborators, client-side, vendor/dealer-side, attendees — and the person you're chasing often lives in a sibling table you didn't open. If a name you end up confirming wasn't reachable from the context file's name-source manifest, that manifest is incomplete: add the missing table to it so the next run can't miss it. (See domain_context_guide.md Rule 6 for the failure case this prevents.)
  • Memory (~/.claude/.../memory/) — project relationship maps and person profiles often record canonical names explicitly.
  • WebSearch — only for genuinely public entities (a public-company ticker, a known researcher, a drug name). Skip for anything project-internal.

Only after all of these strike out do you ask the user — and by then you've shown the entity isn't already recorded on this machine, which makes the ask legitimate. A confirmed result becomes a Confident fix; if the search *can't* confirm it, it drops to Uncertain. Batch these: collect the unique unknowns and run the ladder once per unique entity, not once per occurrence.

And when the user answers, their verdict is ✅ authoritative — the strongest source in this whole loop — and it compounds three ways in the same session. A user who says "X is actually Y (my colleague on team Z)" has handed you a source stronger than any local document. Cash it in immediately: ① apply the fix; ② persist the variant where it compounds — an important recurring person goes to the people roster (per the roster-vs-DB table above), a project term or one-off name goes to --add ... --domain <project> (the same ASR will mishear the same name again next week); ③ record it in the ledger / roster / domain context with the user's words, the date, and a ✅ "user-confirmed" marker — no later session should re-ask. Two refinements learned the hard way:

  • Collision-check the FROM side before dict-adding. If the garbled string is itself a real person's name elsewhere in your world (another project's roster holds a *different* real 李明), a 李明黎明 dictionary rule will corrupt that person's future transcripts. That fix belongs in the domain context file as a trap with its disambiguating cue ("in editing-team context, 李明 = 黎明"), never in the dictionary.
  • Confirmed-correct entities deserve a note too. When the user confirms a name is right as-is ("he's a real blogger, spelled exactly like that"), record the verdict (one line in the domain context or roster). An unrecorded "correct as-is" is a question the next run will burn five minutes re-asking.
  • Uncertain — you suspect an error but can't confirm it even after searching (a syllable that maps to several real entities; a structurally broken sentence). Leave the original text exactly as-is and record it in the needs-checking list (step 7). A fluent-but-wrong "fix" is harder to catch downstream than an obvious garble — silence beats a confident guess.
  • Apply the confident fixes efficiently:
  • Global replacements (unique non-words like "克劳锐"→"Claude"): if it recurs across transcripts — most product/name garbles do — --add it to a --domain so it compounds to every future run; for a genuinely one-off term, one sed -i '' with multiple -e flags
  • Context-dependent (a word that's only wrong in one context, like "争"→"蒸" in a distillation discussion): sed with a longer surrounding phrase for uniqueness, or the Edit tool
  • Re-grep each changed term afterward to confirm it landed and didn't hit look-alikes you meant to keep

How to use it

Copy the folder

Take daymade/transcript-fixer from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.