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", diff --git a/daymade-audio/transcript-fixer/.security-scan-passed b/daymade-audio/transcript-fixer/.security-scan-passed index 692c216..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-04T15:40:34.502448+00:00 +Scanned at: 2026-08-04T16:53:41.245730+00:00 Tool: gitleaks + pattern-based validation -Content hash: 57c8c15e62de7d06c2576504286055c232ea24160a81ae4ddf2bf06175c1a201 +Content hash: 4681602d938e85aa1012c164c232ae86c59ce2b12d906d5fc4337225c12240de diff --git a/daymade-audio/transcript-fixer/CHANGELOG.md b/daymade-audio/transcript-fixer/CHANGELOG.md index 50560ac..f839984 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/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 c7090a4..c74984d 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. +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: @@ -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,9 +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. +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 + [ + {"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"} + ] + ``` + **`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. + + 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=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.) + **`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 → 词典自动晋升)