From ba8189876cf412a756cfccce13a7a4e2de40092b Mon Sep 17 00:00:00 2001 From: daymade Date: Fri, 24 Jul 2026 01:41:36 +0800 Subject: [PATCH 1/4] docs(transcript-fixer): 3 native-pass actionability fixes (SKILL.md) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Based on friction hit across two real transcripts: - step2: dictionary-false-positive feedback loop — disable the bad rule in-session via --report-false-positive, passing the rule's STORED from->to pair (as *_changes.md From/To shows, NOT wrong->right semantics; the 买买->卖卖 FP stored from=买买,to=卖卖 -> pass '买买' '卖卖'); one call disables it. - step5: common-word batch where most occurrences are the domain term but a few are genuine — grep -n every occurrence, judge each, then replace_all + Edit-revert the genuine minority (real case: 公开 x11, 10 工勘 + 1 real 公开渠道). - step7: minimal inline items.json example (line/original/suggestion/kind/evidence) + action_pack-optional note, so --enqueue-review no longer requires a jump to references. Doc-only (no script logic). Gate: regression audit (2 candidates, preserved_or_moved) + security scan + independent fresh-context review (re-review empty = both fixes executable). Co-Authored-By: Claude --- daymade-audio/transcript-fixer/CHANGELOG.md | 1 + daymade-audio/transcript-fixer/SKILL.md | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/daymade-audio/transcript-fixer/CHANGELOG.md b/daymade-audio/transcript-fixer/CHANGELOG.md index 50560ac..3d700a7 100644 --- a/daymade-audio/transcript-fixer/CHANGELOG.md +++ b/daymade-audio/transcript-fixer/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Upstream-corrected inputs get a raw-diff pass (Native AI Correction step 2, doc-only).** When the transcript already passed through an automated corrector (a sync pipeline's pre-classify stage, a previous Stage 3 API run), the input is no longer raw ASR — upstream corrections are baked in with no evidence trail. The workflow now requires diffing against the raw source before triage, with the two opposite consequences spelled out: every upstream entity swap becomes a suspect (an upstream AI "correction" can be a fluent wrong guess — real case: raw 「新的车辆」 smoothed into 「新出来的反馈」, grammatical and plausible but wrong, caught only by the raw diff), and already-correct fixes are checked *before* being proposed again. - **Feishu-minute audio wiring recipe for dashboard playback (doc-only).** `lark-cli minutes +download` end-to-end, run from a cache/state directory (its `--output` only accepts a relative path inside the current directory); when lark-cli's SSRF guard refuses Feishu's signed-download host with `blocked download URL: local/internal host is not allowed` (the host's name literally starts with `internal-`), `--url-only` prints a JSON envelope whose `download_url` field `curl` can fetch directly; the file stays outside any docs repo; `ffprobe` duration ≈ the transcript's last timestamp verifies timeline pairing before `Q` playback is trusted. - **Quick Start notes absolute script paths for cwd-resetting harnesses (doc-only).** The relative `scripts/...` examples fail with `Failed to spawn: scripts/fix_transcription.py` in agent harnesses whose shell resets the working directory between calls; substitute the absolute path (`$CLAUDE_SKILL_DIR/scripts/...`). +- **Native AI Correction: three actionability fixes from real two-transcript use (doc-only).** (1) Step 2 makes the false-positive feedback loop explicit — a Stage 1 false positive is debt you owe the dictionary: disable the bad rule in the same session with `--report-false-positive`, passing the rule's **stored from→to pair** (as `*_changes.md` From/To shows it, NOT "wrong→right" semantics — the `买买→卖卖` FP stored `from=买买, to=卖卖` → pass `"买买" "卖卖"`); one call disables it. (2) Step 5 adds a "common-word batch where most occurrences are the domain term but a few are genuine" pattern: `grep -n` every occurrence, judge each, then `replace_all` + revert the genuine minority (real case: `公开` ×11 — 10 工勘 + 1 real 公开渠道). (3) Step 7 ships a minimal inline `items.json` example (line/original/suggestion/kind/evidence) + `action_pack`-optional note, so `--enqueue-review` no longer requires a jump to references. - **Second-pass residual adjudication heuristics + trap-scan (Native AI Correction step 6, doc-only).** The cold subagent's residual list is now explicitly *candidates, not conclusions* (one real run: 10 rows → 4 accepted), with six production-validated accept/reject rules: near-homophone + in-document self-proof accepts; sound-distance falsifies implausible swaps (the sound test now cuts both ways); the **ASR-capability counter-check** (a word the same engine rendered correctly nearby is inside its recognition range, so a different rendering is more likely what was said — a strong prior, not a proof); intelligible real words stay; evidence-free reconstructions die; minimal edit only (never insert unspoken words). New fast-tier **trap-scan**: grep the file for the domain context file's documented trap patterns before (or instead of) a full cold re-read. - **User-verdict compounding loop (step 4, doc-only).** A user's mid-session resolution of an uncertain item is now treated as the strongest source and cashed in three ways immediately: apply the fix → persist the variant (roster for recurring people, `--add --domain` for project terms) → record it in the ledger/roster/context with verbatim wording, date, and a ✅ user-confirmed marker. Two refinements: collision-check the FROM side against real people in other projects before dict-adding (real-name → real-name fixes go to the domain context as a trap, never the dictionary); confirmed-*correct* entities get a one-line note too, so the next run doesn't re-ask. Dictionary decision matrix gains a "real name → different real name: never a rule" row. - **`--enqueue-review` item schema documented** (`references/script_parameters.md` §Review Queue Item Schema): full field/alias table (only `original` required), the silent-drop gotcha for unknown keys (`line`, not `line_hint`), temp-dir items skipped entirely, CLI `--domain` fills only items without their own, empty-`suggested` + no-action-pack errors at resolve time, and the five-field dedup key. SKILL.md step 7 cross-references it. diff --git a/daymade-audio/transcript-fixer/SKILL.md b/daymade-audio/transcript-fixer/SKILL.md index c7090a4..360ad5c 100644 --- a/daymade-audio/transcript-fixer/SKILL.md +++ b/daymade-audio/transcript-fixer/SKILL.md @@ -637,7 +637,7 @@ A recording can be long but still fast-tier (two known speakers, plain language) **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. +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 the "real-word → real-word" kind the 4+ char blind spot auto-applies as low-risk (e.g. `买买→卖卖` rewriting a "买工作流" to "卖卖工作流") — disable it in the same session with `--report-false-positive -d ` — 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 pair that *identifies* the rule, not a correction. 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: @@ -717,6 +717,7 @@ A recording can be long but still fast-tier (two known speakers, plain language) 5. 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 + - **Common-word batch where most occurrences are the domain term but a few are genuine** (a high-frequency word the domain repurposes in *most* of its occurrences, yet not all — the residual real uses are exactly what a common word is for). Never blind `replace_all`. First `grep -n` every occurrence and judge each from its sentence. When the large majority share the domain meaning and only one or two are real, the efficient shape is: `replace_all` the word to the domain term, then `Edit` those one or two genuine-usage sites back — faster and less error-prone than N separate Edits, and the re-grep below catches any misjudgment. Real case: `公开` across 11 lines of a sales call — 10 were 工勘 (the field-survey sales-funnel stage) and one was a real "公开的渠道"; `replace_all` → 工勘, then revert the single "公开的渠道". (The domain term itself still doesn't go in the dictionary when the source word is common — record it as a context trap per "Domain Correction Contexts"; this bullet is only about *applying* the fix within one transcript.) - Re-grep each changed term afterward to confirm it landed and didn't hit look-alikes you meant to keep 6. **Second pass — catch what one read missed.** A single linear read reliably leaves residue: an idiom degraded into a near-homophone, a term wrong in just one spot among many correct ones, an acronym misheard as another. Always re-scan once for leftovers. A cheap targeted variant comes first: **trap-scan** — scan the file for every trap pattern the domain's context file documents (the recurring homophones this domain is known to produce). Run it mechanically, not as a hand-rolled grep loop (a 30-trap context file is 30+ greps by hand, and the list is exactly what a tired operator truncates): @@ -739,7 +740,15 @@ A recording can be long but still fast-tier (two known speakers, plain language) - **Prefer the smallest edit that explains the error — rank candidates by phonetic distance before you judge any of them.** The rule above bounds how *much* one candidate may change; this one decides *which* candidate wins when several would read fine. ASR errors are small perturbations — the engine maps a heard sound to the nearest word it knows — so among candidates that all make sense, the one changing the fewest phonemes is almost always what was said. Useful fingerprint in Mandarin: **a reduplicated or multi-syllable tail surviving intact while only the leading syllable differs** points at an initial-consonant confusion (retroflex/alveolar `sh`/`s`, `zh`/`z`, `ch`/`c`, and the `n`/`l`, `f`/`h` pairs), so search same-final/different-initial candidates *before* concluding the whole word was misheard. **Where this fails is not while you generate candidates — it's while you audit text that is already there** (an upstream correction, or a fix you accepted on the first pass). Reviewing existing text puts you in verify-mode: you ask "is this reasonable?", it is, and you move on — never noticing you were handed one candidate rather than a ranked set. A candidate that rewrites three syllables can be perfectly idiomatic *and* be a rewrite; the only thing that separates it from the one-phoneme candidate is that you generated both and compared. So when auditing any already-applied correction, force the question: **is there a smaller edit that also explains this?** If you cannot answer it, you have validated rather than verified. A second-pass subagent that returns 8 sharp rows beats one that returns 8000 tokens of narration every time. Task works when you're in the main context; if it isn't available — e.g. these instructions are themselves running inside a subagent, which can't spawn another — just do one more thorough independent re-read yourself. Never skip the second pass over a missing tool. -7. **Emit a needs-checking list AND enqueue it** — the chat summary alone evaporates when the session ends, so every *Uncertain* item gets dual-written: (a) in your chat summary to the human — line number, the original text you left in place, what you suspect, why you couldn't confirm it; (b) into the persistent review queue via `--enqueue-review items.json` (see "Review Queue & Dashboard" above; item field/alias schema: `references/script_parameters.md` §Review Queue Item Schema — unknown keys are silently dropped, so write `line`, not `line_hint`) with the same fields plus a proposed action pack, so the human can one-keystroke-resolve it later in the dashboard — or a later agent session can close it with new evidence (`--resolve-review ID --decision … --note ""`). Entity/name questions get `kind: entity` (they compound into the dictionary/roster, so they lead the queue); pure phrasing doubts get `kind: wording`. If nothing is uncertain, say so. +7. **Emit a needs-checking list AND enqueue it** — the chat summary alone evaporates when the session ends, so every *Uncertain* item gets dual-written: (a) in your chat summary to the human — line number, the original text you left in place, what you suspect, why you couldn't confirm it; (b) into the persistent review queue via `--enqueue-review items.json` (see "Review Queue & Dashboard" above; item field/alias schema: `references/script_parameters.md` §Review Queue Item Schema — unknown keys are silently dropped, so write `line`, not `line_hint`) with the same fields plus a proposed action pack, so the human can one-keystroke-resolve it later in the dashboard — or a later agent session can close it with new evidence (`--resolve-review ID --decision … --note ""`). Entity/name questions get `kind: entity` (they compound into the dictionary/roster, so they lead the queue); pure phrasing doubts get `kind: wording`. If nothing is uncertain, say so. A minimal `items.json` for `--enqueue-review` (one object per uncertain item; `suggestion` may be empty when you have no candidate — the dashboard lets a human fill it later): + + ```json + [ + {"line": 142, "original": "", "suggestion": "", "kind": "entity", + "evidence": "speaker-label fragment near line 142; not in roster or project alias ledger — needs user confirmation"} + ] + ``` + (`action_pack` is optional — omit it and an accept-in-dashboard does a single `file_edit` by default; add it only if you want accept to also `dict_add` / `append_note` automatically. Full action-pack schema: `references/script_parameters.md` §Review Queue Item Schema.) **`original` carries only the suspect token, never the whole sentence** — the sentence goes in `context`. Whatever you put in `original` is what a dashboard verdict will *replace wholesale*: an accept does `file_edit(old=original, new=suggestion)`, and an override swaps the entire `original` span for the human's typed text. If `original` is a full clause like 「我们的民宿就完了」 and the human types the two-character brand 「栖云」, the clause is gone — that is a real 2026-07 incident (#24), and the lost words had to be re-added by hand. `original: "民宿的误写词"` + `context: "…我们的民宿就完了"` would have made the same verdict correct by default. (The dashboard now shows the full replacement span above the override input and warns on suspiciously short replacements — but the right granularity at enqueue is the fix that costs nothing.) 8. Verify with diff against the file you actually edited (`diff `) — every change should trace back to a triage decision From fe75d57a3a6a322f894f769f3d8b210db40149d7 Mon Sep 17 00:00:00 2001 From: daymade Date: Fri, 24 Jul 2026 01:42:52 +0800 Subject: [PATCH 2/4] chore(marketplace): bump daymade-audio 1.14.1 -> 1.14.2 Releases transcript-fixer SKILL.md actionability fixes (regression+security+independent review green) to installed copies; marketplace update keys off this bump. Co-Authored-By: Claude --- .claude-plugin/marketplace.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ed8e3c6..dcb8c27 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -204,7 +204,7 @@ "description": "Audio processing suite covering the full speech pipeline: ASR transcription (Qwen3, StepFun) with batch-mode guards against music-only repetition-loop hallucinations, speaker diarization and CAM++ voiceprint identification for multi-speaker recordings, transcript error correction, structured meeting minutes generation, and TTS voice synthesis (StepFun). Install once for the complete audio workflow.", "source": "./daymade-audio", "strict": false, - "version": "1.18.0", + "version": "1.18.1", "category": "suite", "keywords": [ "suite", From 83faa64b7965189d470af04777b56b40b4ca7225 Mon Sep 17 00:00:00 2001 From: daymade Date: Fri, 24 Jul 2026 01:58:58 +0800 Subject: [PATCH 3/4] fix(transcript-fixer): correct FP example + align report-false-positive arg docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-2 full-flow independent review caught a factual error in step 2's example: - the 买买→卖卖 example wrongly attributed the rule to the 4+ char blind spot (买买 is 2 chars; it applied via --apply-domain), and misstated the source as 买工作流 (single) when it was 买买工作流 (double — the only form the rule matches). Rewrote to the real mechanism + correct source text. - Quick Start --report-false-positive arg labels (错误词/正确词) conflicted with step 2 NOT-wrong→right semantics — aligned both to from_text/to_text. - step 2 'not a correction' was self-contradictory — reworded. Two pre-existing flow inconsistencies the review also surfaced (step2 original-file vs step9; _needs_review.md AI-pass vs dashboard) are recorded in independent-review.md as out of scope for this change. Co-Authored-By: Claude --- daymade-audio/transcript-fixer/.security-scan-passed | 4 ++-- daymade-audio/transcript-fixer/SKILL.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/daymade-audio/transcript-fixer/.security-scan-passed b/daymade-audio/transcript-fixer/.security-scan-passed index 692c216..b4c6905 100644 --- a/daymade-audio/transcript-fixer/.security-scan-passed +++ b/daymade-audio/transcript-fixer/.security-scan-passed @@ -1,4 +1,4 @@ Security scan passed -Scanned at: 2026-08-04T15:40:34.502448+00:00 +Scanned at: 2026-08-04T16:10:24.789373+00:00 Tool: gitleaks + pattern-based validation -Content hash: 57c8c15e62de7d06c2576504286055c232ea24160a81ae4ddf2bf06175c1a201 +Content hash: 40e2530f86f9f2f3db11a9015c77c32b7351adcf78112cb6c18913baed5ac0bf diff --git a/daymade-audio/transcript-fixer/SKILL.md b/daymade-audio/transcript-fixer/SKILL.md index 360ad5c..dbe08f1 100644 --- a/daymade-audio/transcript-fixer/SKILL.md +++ b/daymade-audio/transcript-fixer/SKILL.md @@ -104,7 +104,7 @@ Two-phase pipeline with persistent learning: - **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. +- **Report false positives**: `--report-false-positive "" "" -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) @@ -637,7 +637,7 @@ A recording can be long but still fast-tier (two known speakers, plain language) **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 the "real-word → real-word" kind the 4+ char blind spot auto-applies as low-risk (e.g. `买买→卖卖` rewriting a "买工作流" to "卖卖工作流") — disable it in the same session with `--report-false-positive -d ` — 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 pair that *identifies* the rule, not a correction. 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. +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 -d ` — 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: From e419016be4a7852e6abe0fae14d3f4516bcb7d36 Mon Sep 17 00:00:00 2001 From: daymade Date: Wed, 5 Aug 2026 00:36:46 +0800 Subject: [PATCH 4/4] fix(transcript-fixer): the items.json example shipped three keys wrong, one of them silently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent reviews of this branch, the second one reproducing everything against the real loader and CLI. Every claim below was checked against the code before the doc was changed. 1. `file` was missing — and it is the key the other two guarantees hang on. `review_queue.py:212` gates the verbatim-anchor check on `item["file_path"]`, and `:793` gates the default `file_edit` on `item.file_path`. With no `file`: a paraphrased `context` is silently accepted instead of rejected, and an Accept records the verdict, exits 0, and never touches the transcript. That is a green path that does nothing — worse than the bug in (2), which at least greys the button out. No items.json example in any doc set `file`. 2. `"suggestion"` → `"suggested"`. `:511` reads `raw.get("suggested") or raw.get("suggested_text")`; the misspelling is dropped in silence, and `--resolve-review` later refuses with "item N has no suggestion to accept". Fixed in the template, in the step-7 lead-in sentence five lines above it, and in the paragraph below (see 4). 3. `action_pack` → `actions`. `:509` reads `raw.get("actions", [])`; `action_pack` appears nowhere in the loader. 4. Touching a deliberately-restored line, so stating it rather than doing it quietly: `fa5c964` restored the incident-#24 paragraph byte-for-byte, and it contains `file_edit(old=original, new=suggestion)` — a key name that does not exist (`:797` builds `"new": item.suggested_text`). Changed to `new=suggested`. That commit's objection was to rewriting the incident's example wording; 「我们的民宿就完了」 and `original: "民宿的误写词"` are untouched. Also restores the precondition SKILL.md:247 states correctly and my earlier draft of this sentence dropped: the default single `file_edit` needs a file anchor, not just an empty action pack. Known residual, not fixed here because it is inside that restored paragraph: its `context: "…我们的民宿就完了"` opens with an ellipsis, so copied literally it is non-verbatim and would be rejected once `file` is set. Editing it is a proposal for whoever owns that record, not a side effect of this branch. Co-Authored-By: Claude Opus 5 (1M context) --- .../transcript-fixer/.security-scan-passed | 4 ++-- daymade-audio/transcript-fixer/CHANGELOG.md | 2 +- daymade-audio/transcript-fixer/SKILL.md | 14 ++++++++++---- .../references/dictionary_guide.md | 2 +- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/daymade-audio/transcript-fixer/.security-scan-passed b/daymade-audio/transcript-fixer/.security-scan-passed index b4c6905..1a87756 100644 --- a/daymade-audio/transcript-fixer/.security-scan-passed +++ b/daymade-audio/transcript-fixer/.security-scan-passed @@ -1,4 +1,4 @@ Security scan passed -Scanned at: 2026-08-04T16:10:24.789373+00:00 +Scanned at: 2026-08-04T16:53:41.245730+00:00 Tool: gitleaks + pattern-based validation -Content hash: 40e2530f86f9f2f3db11a9015c77c32b7351adcf78112cb6c18913baed5ac0bf +Content hash: 4681602d938e85aa1012c164c232ae86c59ce2b12d906d5fc4337225c12240de diff --git a/daymade-audio/transcript-fixer/CHANGELOG.md b/daymade-audio/transcript-fixer/CHANGELOG.md index 3d700a7..f839984 100644 --- a/daymade-audio/transcript-fixer/CHANGELOG.md +++ b/daymade-audio/transcript-fixer/CHANGELOG.md @@ -35,7 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Upstream-corrected inputs get a raw-diff pass (Native AI Correction step 2, doc-only).** When the transcript already passed through an automated corrector (a sync pipeline's pre-classify stage, a previous Stage 3 API run), the input is no longer raw ASR — upstream corrections are baked in with no evidence trail. The workflow now requires diffing against the raw source before triage, with the two opposite consequences spelled out: every upstream entity swap becomes a suspect (an upstream AI "correction" can be a fluent wrong guess — real case: raw 「新的车辆」 smoothed into 「新出来的反馈」, grammatical and plausible but wrong, caught only by the raw diff), and already-correct fixes are checked *before* being proposed again. - **Feishu-minute audio wiring recipe for dashboard playback (doc-only).** `lark-cli minutes +download` end-to-end, run from a cache/state directory (its `--output` only accepts a relative path inside the current directory); when lark-cli's SSRF guard refuses Feishu's signed-download host with `blocked download URL: local/internal host is not allowed` (the host's name literally starts with `internal-`), `--url-only` prints a JSON envelope whose `download_url` field `curl` can fetch directly; the file stays outside any docs repo; `ffprobe` duration ≈ the transcript's last timestamp verifies timeline pairing before `Q` playback is trusted. - **Quick Start notes absolute script paths for cwd-resetting harnesses (doc-only).** The relative `scripts/...` examples fail with `Failed to spawn: scripts/fix_transcription.py` in agent harnesses whose shell resets the working directory between calls; substitute the absolute path (`$CLAUDE_SKILL_DIR/scripts/...`). -- **Native AI Correction: three actionability fixes from real two-transcript use (doc-only).** (1) Step 2 makes the false-positive feedback loop explicit — a Stage 1 false positive is debt you owe the dictionary: disable the bad rule in the same session with `--report-false-positive`, passing the rule's **stored from→to pair** (as `*_changes.md` From/To shows it, NOT "wrong→right" semantics — the `买买→卖卖` FP stored `from=买买, to=卖卖` → pass `"买买" "卖卖"`); one call disables it. (2) Step 5 adds a "common-word batch where most occurrences are the domain term but a few are genuine" pattern: `grep -n` every occurrence, judge each, then `replace_all` + revert the genuine minority (real case: `公开` ×11 — 10 工勘 + 1 real 公开渠道). (3) Step 7 ships a minimal inline `items.json` example (line/original/suggestion/kind/evidence) + `action_pack`-optional note, so `--enqueue-review` no longer requires a jump to references. +- **Native AI Correction: three actionability fixes from real two-transcript use (doc-only).** (1) Step 2 makes the false-positive feedback loop explicit — a Stage 1 false positive is debt you owe the dictionary: disable the bad rule in the same session with `--report-false-positive`, passing the rule's **stored from→to pair** (as `*_changes.md` From/To shows it, NOT "wrong→right" semantics — the `买买→卖卖` FP stored `from=买买, to=卖卖` → pass `"买买" "卖卖"`); one call disables it. (2) Step 5 adds a "common-word batch where most occurrences are the domain term but a few are genuine" pattern: `grep -n` every occurrence, judge each, then `replace_all` + revert the genuine minority (real case: `公开` ×11 — 10 工勘 + 1 real 公开渠道). (3) Step 7 ships a minimal inline `items.json` example (line/original/suggested/context/kind/evidence) + an `actions`-optional note, so `--enqueue-review` no longer requires a jump to references — and it names the two keys the silent-drop rule catches one field over: the verdict is `suggested`, not `suggestion`, and the action pack's key is `actions`, not `action_pack`. Also sweeps `references/dictionary_guide.md`, whose `--report-false-positive` row still taught the "错→对" argument order this entry's item (1) repudiates. - **Second-pass residual adjudication heuristics + trap-scan (Native AI Correction step 6, doc-only).** The cold subagent's residual list is now explicitly *candidates, not conclusions* (one real run: 10 rows → 4 accepted), with six production-validated accept/reject rules: near-homophone + in-document self-proof accepts; sound-distance falsifies implausible swaps (the sound test now cuts both ways); the **ASR-capability counter-check** (a word the same engine rendered correctly nearby is inside its recognition range, so a different rendering is more likely what was said — a strong prior, not a proof); intelligible real words stay; evidence-free reconstructions die; minimal edit only (never insert unspoken words). New fast-tier **trap-scan**: grep the file for the domain context file's documented trap patterns before (or instead of) a full cold re-read. - **User-verdict compounding loop (step 4, doc-only).** A user's mid-session resolution of an uncertain item is now treated as the strongest source and cashed in three ways immediately: apply the fix → persist the variant (roster for recurring people, `--add --domain` for project terms) → record it in the ledger/roster/context with verbatim wording, date, and a ✅ user-confirmed marker. Two refinements: collision-check the FROM side against real people in other projects before dict-adding (real-name → real-name fixes go to the domain context as a trap, never the dictionary); confirmed-*correct* entities get a one-line note too, so the next run doesn't re-ask. Dictionary decision matrix gains a "real name → different real name: never a rule" row. - **`--enqueue-review` item schema documented** (`references/script_parameters.md` §Review Queue Item Schema): full field/alias table (only `original` required), the silent-drop gotcha for unknown keys (`line`, not `line_hint`), temp-dir items skipped entirely, CLI `--domain` fills only items without their own, empty-`suggested` + no-action-pack errors at resolve time, and the five-field dedup key. SKILL.md step 7 cross-references it. diff --git a/daymade-audio/transcript-fixer/SKILL.md b/daymade-audio/transcript-fixer/SKILL.md index dbe08f1..c74984d 100644 --- a/daymade-audio/transcript-fixer/SKILL.md +++ b/daymade-audio/transcript-fixer/SKILL.md @@ -740,17 +740,23 @@ A recording can be long but still fast-tier (two known speakers, plain language) - **Prefer the smallest edit that explains the error — rank candidates by phonetic distance before you judge any of them.** The rule above bounds how *much* one candidate may change; this one decides *which* candidate wins when several would read fine. ASR errors are small perturbations — the engine maps a heard sound to the nearest word it knows — so among candidates that all make sense, the one changing the fewest phonemes is almost always what was said. Useful fingerprint in Mandarin: **a reduplicated or multi-syllable tail surviving intact while only the leading syllable differs** points at an initial-consonant confusion (retroflex/alveolar `sh`/`s`, `zh`/`z`, `ch`/`c`, and the `n`/`l`, `f`/`h` pairs), so search same-final/different-initial candidates *before* concluding the whole word was misheard. **Where this fails is not while you generate candidates — it's while you audit text that is already there** (an upstream correction, or a fix you accepted on the first pass). Reviewing existing text puts you in verify-mode: you ask "is this reasonable?", it is, and you move on — never noticing you were handed one candidate rather than a ranked set. A candidate that rewrites three syllables can be perfectly idiomatic *and* be a rewrite; the only thing that separates it from the one-phoneme candidate is that you generated both and compared. So when auditing any already-applied correction, force the question: **is there a smaller edit that also explains this?** If you cannot answer it, you have validated rather than verified. A second-pass subagent that returns 8 sharp rows beats one that returns 8000 tokens of narration every time. Task works when you're in the main context; if it isn't available — e.g. these instructions are themselves running inside a subagent, which can't spawn another — just do one more thorough independent re-read yourself. Never skip the second pass over a missing tool. -7. **Emit a needs-checking list AND enqueue it** — the chat summary alone evaporates when the session ends, so every *Uncertain* item gets dual-written: (a) in your chat summary to the human — line number, the original text you left in place, what you suspect, why you couldn't confirm it; (b) into the persistent review queue via `--enqueue-review items.json` (see "Review Queue & Dashboard" above; item field/alias schema: `references/script_parameters.md` §Review Queue Item Schema — unknown keys are silently dropped, so write `line`, not `line_hint`) with the same fields plus a proposed action pack, so the human can one-keystroke-resolve it later in the dashboard — or a later agent session can close it with new evidence (`--resolve-review ID --decision … --note ""`). Entity/name questions get `kind: entity` (they compound into the dictionary/roster, so they lead the queue); pure phrasing doubts get `kind: wording`. If nothing is uncertain, say so. A minimal `items.json` for `--enqueue-review` (one object per uncertain item; `suggestion` may be empty when you have no candidate — the dashboard lets a human fill it later): +7. **Emit a needs-checking list AND enqueue it** — the chat summary alone evaporates when the session ends, so every *Uncertain* item gets dual-written: (a) in your chat summary to the human — line number, the original text you left in place, what you suspect, why you couldn't confirm it; (b) into the persistent review queue via `--enqueue-review items.json` (see "Review Queue & Dashboard" above; item field/alias schema: `references/script_parameters.md` §Review Queue Item Schema — unknown keys are silently dropped, so write `line`, not `line_hint`) with the same fields plus a proposed action pack, so the human can one-keystroke-resolve it later in the dashboard — or a later agent session can close it with new evidence (`--resolve-review ID --decision … --note ""`). Entity/name questions get `kind: entity` (they compound into the dictionary/roster, so they lead the queue); pure phrasing doubts get `kind: wording`. If nothing is uncertain, say so. A minimal `items.json` for `--enqueue-review` (one object per uncertain item; `suggested` may be empty when you have no candidate — the dashboard lets a human fill it later): ```json [ - {"line": 142, "original": "", "suggestion": "", "kind": "entity", + {"file": "/abs/path/to/the/transcript.md", "line": 142, + "original": "", "suggested": "", "kind": "entity", + "context": "", "evidence": "speaker-label fragment near line 142; not in roster or project alias ledger — needs user confirmation"} ] ``` - (`action_pack` is optional — omit it and an accept-in-dashboard does a single `file_edit` by default; add it only if you want accept to also `dict_add` / `append_note` automatically. Full action-pack schema: `references/script_parameters.md` §Review Queue Item Schema.) + **`file` is the key that makes the other two work, and omitting it fails silently in the worst direction.** Both guarantees below are gated on it (`review_queue.py:212` and `:793` each test `file_path` first): + - *Verbatim-anchor rejection.* With `file` set, a `context` that is not a literal substring of that file is **rejected at enqueue** (exit 3) — so an authoring error dies immediately instead of at verdict time. With `file` absent there is no file to check against, so a paraphrased `context` is accepted and the drift surfaces much later. + - *The default edit.* With `file` set and no explicit action pack, an accept runs a single `file_edit(old=original, new=suggested)`. With `file` absent the accept still records the verdict and still exits 0 — **and never touches the transcript.** Nothing errors; the queue just says `accepted` while the file is unchanged. - **`original` carries only the suspect token, never the whole sentence** — the sentence goes in `context`. Whatever you put in `original` is what a dashboard verdict will *replace wholesale*: an accept does `file_edit(old=original, new=suggestion)`, and an override swaps the entire `original` span for the human's typed text. If `original` is a full clause like 「我们的民宿就完了」 and the human types the two-character brand 「栖云」, the clause is gone — that is a real 2026-07 incident (#24), and the lost words had to be re-added by hand. `original: "民宿的误写词"` + `context: "…我们的民宿就完了"` would have made the same verdict correct by default. (The dashboard now shows the full replacement span above the override input and warns on suspiciously short replacements — but the right granularity at enqueue is the fix that costs nothing.) + Two key names, both of which the silent-drop rule above catches one field over: the verdict is `suggested` (alias of `suggested_text`), **not `suggestion`** — the wrong spelling costs you the dashboard's Accept button, and `--resolve-review` then refuses with *"item N has no suggestion to accept"*. The action pack's key is `actions`, **not `action_pack`**; it is optional — supply it only when accept should also `dict_add` / `append_note`. Full field/alias table: `references/script_parameters.md` §Review Queue Item Schema. + + **`original` carries only the suspect token, never the whole sentence** — the sentence goes in `context`. Whatever you put in `original` is what a dashboard verdict will *replace wholesale*: an accept does `file_edit(old=original, new=suggested)`, and an override swaps the entire `original` span for the human's typed text. If `original` is a full clause like 「我们的民宿就完了」 and the human types the two-character brand 「栖云」, the clause is gone — that is a real 2026-07 incident (#24), and the lost words had to be re-added by hand. `original: "民宿的误写词"` + `context: "…我们的民宿就完了"` would have made the same verdict correct by default. (The dashboard now shows the full replacement span above the override input and warns on suspiciously short replacements — but the right granularity at enqueue is the fix that costs nothing.) 8. Verify with diff against the file you actually edited (`diff `) — every change should trace back to a triage decision 9. Finalize and archive: - **Primary path (recommended):** Re-run `--stage 1` on the original `file.md` — **plain, without `--apply-all`** (an explicit `--apply-all` always runs corrections and never finalizes, so a stale sidecar can't silently swallow the run). If `file_stage1.md` is newer than `file.md`, transcript-fixer automatically promotes it to `file.md` and removes the intermediate sidecars (`_stage1.md`, `_stage2.md`, `_dryrun.md`, `_changes.md`, `_needs_review.md`, `_uncertain.md`, `_对比.html`). This is the default way to finalize; it is atomic, preserves manual edits (it skips promotion when `file.md` is newer), and avoids macOS `mv` alias hazards. diff --git a/daymade-audio/transcript-fixer/references/dictionary_guide.md b/daymade-audio/transcript-fixer/references/dictionary_guide.md index c18eab9..8a13555 100644 --- a/daymade-audio/transcript-fixer/references/dictionary_guide.md +++ b/daymade-audio/transcript-fixer/references/dictionary_guide.md @@ -39,7 +39,7 @@ uv run scripts/fix_transcription.py --add "错误词" "正确词" [--domain ]` | 列出规则 | | `--audit [--domain ]` | 体检词典,报告可疑规则(短词 / 冲突等) | -| `--report-false-positive "错" "对" -d ` | 停用一条误报规则、降低其置信度 | +| `--report-false-positive "" "" -d ` | 停用一条误报规则、降低其置信度。**两个参数是规则在词典里存的 from→to 对**(照 `*_changes.md` 的 From/To 抄),不是「错词→对词」语义——详见 SKILL.md 的 Native AI Correction step 2 | | `--load-presets ` | 导入某 domain 的预置规则集(如 `tech`) | ## 学习闭环(AI → 词典自动晋升)