New to Claude Skills? Learn how to install them →

daymade on GitHub

Transcript Fixer

Free

Correct transcription errors with AI and dictionary rules.

Get this skill

Free · Opens the source repo

What Transcript Fixer does

Transcript Fixer is a skill designed to improve the accuracy of speech-to-text transcriptions by correcting common errors using a combination of dictionary rules and AI processing. This skill is particularly useful for users who frequently work with automatic speech recognition (ASR) outputs, such as meeting notes, lecture transcripts, or interview recordings. The tool operates in two stages: the first stage applies deterministic dictionary corrections for recurring errors, while the second stage utilizes Claude's built-in AI to detect and correct more complex mistakes that the dictionary may not address.

The skill allows for the creation of personalized correction databases that learn from each fix, enhancing the accuracy of future corrections. Users can also load person-name variants from a roster, which is especially beneficial for contexts where proper nouns are frequently misrecognized. Additionally, the skill can read per-domain context files to improve the handling of context-dependent homophones, making it adaptable to various specialized fields such as finance, medical, and legal domains.

To get started, users initialize a database and can then run the correction process on their transcripts. The skill is designed to handle different risk levels for corrections, allowing users to review changes before applying them. This ensures that even if the initial dictionary corrections yield few results, the AI pass will still provide comprehensive error detection and correction, making the skill a robust solution for anyone dealing with transcription errors regularly.

When to use it

Use Transcript Fixer when working with ASR outputs that contain errors, especially in meeting notes or lectures.

When not to use it

This skill may not be effective on high-quality ASR outputs where the dictionary has little to match, or in cases where no corrections are needed.

What you can build with it

Cleaning Up Meeting Notes

Use Transcript Fixer to quickly correct errors in meeting notes generated by ASR, ensuring clarity and accuracy.

Improving Lecture Transcripts

Enhance the readability of lecture transcripts by applying corrections for common transcription errors and technical terms.

Handling Interview Recordings

Automatically fix errors in interview recordings, making them easier to review and analyze.

How to install Transcript Fixer

View source

1. Install with the skills CLI

npx skills add daymade/claude-code-skills/transcript-fixer --agent claude-code

2. Or install it manually

Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.

Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs

Inside SKILL.md

Written by daymade

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

# Sibling domains load together (comma-separated) — one project's vocabulary
# often lives in several domains that grew at different times (myproject,
# myproject-alt, ...), and a transcript that straddles them should be fixed in
# ONE pass, not one rerun each. --apply-domain trusts the whole union.
uv run scripts/fix_transcription.py --input meeting.md --stage 1 --domain myproject,myproject-alt --apply-domain

# Which domains does this project even have? A 0-correction run prints the
# hint listing every OTHER domain with its rule count — read it, then rerun
# with the siblings added. (Write commands like --add stay single-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

# ⚠️ STOP — Stage 1 alone is NOT the job. It is the pre-filter, not the
# corrector: on clean ASR (Feishu / Tencent / Whisper) the dictionary often
# matches almost nothing, and the Native AI pass below does essentially all
# the real work. Reporting "transcript clean" after Stage 1 alone is the
# recurring failure this skill exists to prevent (real case, 2026-08: an
# ingest pipeline ran Stage 1 on a 73-min transcript, got 0 hits, declared
# it clean — 54 errors were later found by the native pass it skipped).
# "The dictionary applied N fixes" does not change this either.
# "Done" = Stage 1 → Native AI Correction → --add the confirmed fixes.

After Stage 1, Claude reads the output and fixes remaining ASR errors natively (no API key needed) — this is the primary path, and skipping it is not a valid shortcut, even for a quick transcript (a "quick, clean" transcript is exactly where the dictionary is weakest and the native read matters most). 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"). If you are finishing after Stage 1, name explicitly why the native pass does not apply — "the pipeline ran the script" is not a reason. The only valid exemptions: the human user explicitly scoped this one run to the dictionary pass (a caller pipeline's standing "run Stage 1" wiring is NOT this exemption — see "When called by another skill" below), or you have evidence the native pass already ran on this transcript (a dated note in the file or the ingest log). "The transcript looked short/clean", "the dictionary already applied N fixes", and "I'm in a hurry" are not exemptions — they are the failure.

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:

  1. Initialize (once): uv run scripts/fix_transcription.py --init
  2. Add domain corrections: --add "错误词" "正确词" --domain <domain>
  3. Phase 1 — Dictionary: --input file.md --stage 1 (instant, free)
  4. 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
  5. Save stable patterns: --add "错误词" "正确词" after each session
  6. 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 "<from_text>" "<to_text>" -d domain disables a bad dictionary rule (pass the rule's stored from→to pair — for a false-positive rule that's the reverse of semantic wrong→right; see Native AI Correction step 2).
  • 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 hit one of two verified failures: it will run Stage 1, apply almost nothing, and report success (deferred corrections silently discarded — the next section) — or it will run Stage 1, skip the native pass entirely, and report the transcript clean (the "Stage 1 is the whole script call" paragraph below). This contract has TWO MUSTs; complying with only the first one ships the second failure with a false sense of having done it right.

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.

Stage 1 is the whole script call — it must not be the whole job. The contract above keeps Stage 1 from silently dropping its own corrections; it says nothing about the pass that does most of the work on a clean transcript. A caller that stops after Stage 1 ships a transcript the native AI pass never read and reports it as clean. So the caller's ingest step MUST either run the Native AI Correction pass itself (hand the filed transcript to an agent (possibly this one) with this skill loaded — the skill, not just the script path; agent-less CI automation completes through the Stage 3 API pass above instead) or surface "Stage 1 only" to the user as an incomplete state, never as success. And note the trap in how this skill gets wired: a caller that references it by script path alone (e.g. a transcript_fixer.script_path config entry) never loads this file, so every contract in it — this one included — is invisible to that run. Wiring the script path without wiring the skill is exactly the configuration that produced the 2026-08 "0 hits, declared clean, 54 errors missed" incident.

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 typeExampleAction
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→AnthropicFor 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 brandXcode→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.

Measure the corpus before you add — the validators can't see your project. The built-in safety checks answer "is this a real word in Chinese"; they cannot answer the question that actually decides a project-domain rule: "when this word appears in THIS project's transcripts, is it ever the real meaning?" That is empirical, and the evidence is one command away:

# How does this term actually appear across the project's transcripts?
uv run scripts/fix_transcription.py --probe "候选误识词" --corpus /path/to/transcripts/

# Or probe as part of the add itself (prints the evidence before writing):
uv run scripts/fix_transcription.py --add "候选误识词" "正确词" --domain myproject \
  --check-corpus --corpus /path/to/transcripts/

The probe prints per-file counts plus sampled context windows, with the decision rule attached: every sampled occurrence an ASR error → a bare rule is safe; any real meaning present → anchored form, or don't add (record the trap in the domain context file instead); zero occurrences → a bare rule is zero-risk but compounds nothing. The surprise this kills: intuition says "this is obviously an error form", and a 30-second sweep finds the word carrying perfectly real meanings all over the corpus — or the reverse, a "real word" whose every single in-corpus occurrence is the mishearing, making the bare rule safe where a word-checker would have scared you off it.

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.)

Promote each decision_note; the queue only stores it. The dashboard's 备注 field and the CLI's --note record the reviewer's reason, but neither turns that reason into a reusable rule. After a review batch, inspect the full queue JSON:

uv run scripts/fix_transcription.py --list-review --review-status all --json

The human-readable list never prints decision_note. Human-readable --show-review prints it only after an item leaves pending; JSON always carries the field, including on an item that reopen returned to pending. Inspect every item with a non-empty note, regardless of status, and do not pre-project a field list that could discard a field the reviewer supplied.

Route the note by meaning rather than by verdict:

The note saysPromote it toDo not
an apparent error is an intentional, context-dependent substitutionthe domain context file, with the cue that distinguishes when to preserve ituse --add, which would rewrite the text
a dictionary rule fired where it should not--report-false-positive "<from>" "<to>" -d <domain>leave the rule active behind a context note
a stable FROM→TO correction will recur in this domain--add "<from>" "<to>" --domain <project>, subject to the real-word rules below
a recurring person's name has a non-obvious spellingthe people roster, which is hand-edited

A decision_note is never an action. A preplanned append_note action runs only when its item is accepted; overridden drops suggestion-specific dict_add and append_note actions, while kept_original and skipped run no actions. Explicitly promote the note after the verdict. This is the same gap as "An override does not compound on its own" below: corrected text stops at resolved_text, and the reason stops at decision_note.

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.

codemeaning
0verified — audio and transcript share a timeline
1timeline mismatch: a file downloaded, but do not wire it
2downloaded, 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)
3nothing 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, shelf 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.

Precedence has three layers, and the third one is domain-scoped while the roster is global — the asymmetry is what surprises people:

  1. A DB rule active in the run's domain wins.
  2. Otherwise the roster supplies the pair.
  3. Unless the pair is disabled in the run's domain — then the roster copy is suppressed too, and the run prints 🚫 People roster: N variant(s) suppressed.

Layer 3 is per-domain, so retiring a pair with --report-false-positive --domain A does not retire it under --domain B: the roster is global and nothing vetoes it there, so the rule keeps firing in B. That is intended (a false positive in one domain is often correct in another), but it means "I disabled it and it still fires" almost always means a different domain — check that before editing the roster, which stops the pair everywhere at once, including in other projects sharing the same file. --report-false-positive now names the domains where the pair is still active, and exits 3 (already disabled here) or 4 (roster-only, no DB row to disable) so automation can tell those apart from a real failure.

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

PersonGo toWhy
Long-term recurring (coworker, client, family, workshop attendee)people.mdSSOT with relationship context; survives DB resets
One-off / minor nameDB (--add --domain)Quick, no context needed

Name-variant explosion — one person, every initial consonant. A person whose name a diarizer labels once can still shatter in the body into a whole family of variants, sometimes across different initial consonants (h/f/w/g/zh all heard for one surname in a single 56-minute call — real case 2026-08-08: one speaker surfaced under seven different surname-initials). This is not a bug to chase per-variant; it is the canonical-name problem in disguise. Handle it as a unit:

  1. Fix the canonical FIRST — ask the user or take the diarization label, settle one spelling, and only then sweep. A variant family resolved without a canonical produces seven half-fixes and a confused roster.
  2. Sweep every variant in the file in ONE pass (single-file sed with all variants in one command, then re-grep to zero), not variant-by-variant.
  3. Record the whole family in the roster's ASR 变体 line — every form you actually saw, including the weird ones. The next transcript will produce new members of the family, and the roster is what keeps the canonical stable while the family grows.
  4. Honorific forms (X老师 / X总) are variants too — an honorific is what a speaker actually said, so never replace it with the bare name, but the surname inside it gets the same sweep and the same roster entry.

Mid-turn verdicts compound immediately — never defer them. When the user answers a name/number question while you are still working (a mid-message correction, a one-word answer to your shortlist), the verdict is the strongest source in the whole loop and costs nothing to bank: fix the file, --add the confirmed variant, and update the roster/context in the SAME turn — not "after the batch", which is where deferrals die. Four mid-turn verdicts in one real batch session all compounded the turn they were given (2026-08-08), including one that corrected the reviewer's own stale training data about a version number. A user verdict that contradicts your search results is the verdict winning, not an anomaly to double-check.

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:

  1. One line of business context — what this domain's recordings are usually about
  2. 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
  3. 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:

SignalFast tier (minutes, not hours)Full tier (the whole ladder earns its keep)
Lengthshort (≤ ~15 min / a few hundred lines)long (30+ min / 1000+ lines)
Speakersone or two, names you already know3+ speakers, or unfamiliar names
Vocabularyplain language, no domain jargondomain-heavy (finance/medical/legal/project codenames) or many proper nouns
Stakesinternal memo, throwawayclient-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.

  1. Run Stage 1 (dictionary) on all files (parallel if multiple)

  2. Verify Stage 1 — diff against the original. If the dictionary introduced false positives, work from the original file instead and apply your edits there. A false positive here is debt you owe the dictionary: the same bad rule fires on every future transcript until retired, so the moment you spot one — a rule that turned correct speech wrong, especially "real-word → real-word" rules (both sides are valid-word-shaped, so the non-word guard doesn't catch them; and under --apply-domain every matching rule applies regardless of its risk class) — e.g. a 买买→卖卖 rule rewrote a correct "买买工作流" into "卖卖工作流" — disable it in the same session with --report-false-positive <from_text> <to_text> -d <domain> — pass the rule's stored from→to pair exactly as Stage 1's *_changes.md shows it (the From/To columns) or as it sits in the dictionary, NOT "wrong-word → right-word" semantics. The direction is counter-intuitive for a false positive: the 买买→卖卖 rule stored from=买买, to=卖卖 (it rewrote a correct 买买 into a wrong 卖卖), so you pass "买买" "卖卖" — the rule's stored from→to pair, which is what the tool keys on. One call disables the rule and lowers its confidence (the tool prints "The rule has been disabled"); it will not fire on the next transcript. If the word is genuinely ambiguous (correct in some contexts, wrong only here) rather than plain wrong, don't disable the rule — record the disambiguating cue in the domain context file instead. Fixing this transcript while leaving the trap armed guarantees the next one trips it too. 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

    How to judge each upstream change — the one test that works, and the one that doesn't. Run the sound-distance test from step 6 on every upstream edit, in the direction it is written there: if the two sides are too far apart phonetically for any ASR to have produced the swap, it is not a correction — it is the model rewriting what the speaker said, and it gets reverted. An ASR mishears sounds; it does not exchange a word for a synonym, and it does not change a pronoun. Two shapes recur, and neither looks like an error on the page:

    • A term swapped for a plausible near-synonym. The two words share no sounds, so no engine could have confused them — and the give-away is corpus-level: the replacement appears nowhere else in the project's material, while the original is that project's standard vocabulary (a term an earlier meeting defined). Grep both forms across the corpus before accepting either.
    • A pronoun or subject rewritten. Reads more logical than the original, and silently reassigns who a statement is about — which is a fact change, not a transcription fix. Pronouns in most languages are phonetically unrelated to each other; an engine that mishears one for another would be mangling the whole sentence.

    Why this needs its own test rather than your judgment: an upstream corrector optimizes for fluency, so everything it emits reads well — which makes "does the result make sense?" a check with zero discriminating power against exactly this failure. You cannot read your way to catching it, and the smoother the pipeline, the more confident the wrong text looks. The diff is the only instrument that sees it. Two consequences worth planning around: run the diff before your own read-through, so upstream's edits arrive as candidates rather than as the text you are proof-reading; and when you do revert one, sweep whatever you have already written that quoted the corrupted form (step 9's derived-document sweep — notes and summaries written from the pre-revert text carry the same corruption, and unlike the transcript they carry no marker saying so).

  3. **Load the domain's priors, then read the entire tra

This file is truncated. Read the full SKILL.md on GitHub.

Frequently asked questions about Transcript Fixer

Similar skills