[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6DC0RW4FGNY67YPG0PVMGD2":3,"skill-same:01M6DC0RW4FGNY67YPG0PVMGD2":64},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":39,"linkOnly":40,"markdownContent":41,"markdownBody":42,"frontMatter":43,"refName":44,"commitSha":45,"skillDirectory":46,"forksCount":14,"repositoryHtmlUrl":47,"repositoryHomepageUrl":6,"ownerHtmlUrl":48,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":49,"install":50,"resources":63},"01M6DC0RW4FGNY67YPG0PVMGD2","subtitle-translate",null,"Use when translating subtitles\u002Fcaptions to another language and\u002For generating + exporting SRT\u002FVTT\u002FASS for a media-studio video — including bilingual (stacked) subs and the audio-dub (TTS) path. Covers the tiered local→hosted MT engine (TranslateGemma GGUF via llama.cpp, language-aware routing +.","Prekzursil\u002Fagent-skills-toolchain","Prekzursil","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F54636077?v=4","01M6DACZ1PCQHD99SCR1YXG28B","Andrei Mihai Visalon","01M6DACZ670WZF535V3WYYSS4Z",0,"MIT","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fsubtitle-translate\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000016","图像与多媒体","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"translate","subtitle translate","cpp","routing","subtitles","Llama","Video","Media","Audio","Captions","tts","engine","use","language","local","2026-11-10 10:12:30",false,"---\nname: subtitle-translate\ndescription: \"Use when translating subtitles\u002Fcaptions to another language and\u002For generating + exporting SRT\u002FVTT\u002FASS for a media-studio video — including bilingual (stacked) subs and the audio-dub (TTS) path. Covers the tiered local→hosted MT engine (TranslateGemma GGUF via llama.cpp, language-aware routing +.\"\n---\n\n# Subtitle translate \u002F generate \u002F export (+ dub)\n\nThis skill is distilled from the REAL media-studio implementation. Every command,\nfield name, model id, and gotcha below is taken from these source files (read\nthem when in doubt):\n\n- `media_studio\u002Ffeatures\u002Fsubtitles.py` — the pure track logic: `generate`,\n  `edit`, `translate`, `export`, `stack_bilingual`, and the SRT\u002FVTT\u002FASS\n  serializers + parsers + timestamp helpers.\n- `media_studio\u002Fmodels\u002Ftranslation.py` — the **TieredTranslator** MT engine\n  (tier1\u002Ftier2 local GGUF + tier3 hosted), language routing, fallback chain.\n- `media_studio\u002Fhandlers\u002Fmedia_ops.py` — the RPC wire shapes\n  (`subtitles.generate\u002Fedit\u002Fexport\u002Ftranslate`).\n- `media_studio\u002Ffeatures\u002Ftts\u002Fdub.py` — the audio-dub pipeline (`tts.dub.start`).\n- `media_studio\u002Ffeatures\u002Fctc_align.py` — word-timing forced alignment (karaoke).\n- `media_studio\u002Ffeatures\u002Fcaption.py` — ASS document build + ffmpeg burn\u002Fsoft-mux argv.\n- `media_studio\u002Ffeatures\u002Ftranscribe.py` — faster-whisper (upstream source of cues).\n\nThe frozen schemas (CONTRACTS.md §3, identical on both sides):\n\n```\nWord          = {text, start, end}\nSegment       = {start, end, text, words:[Word]}\nTranscript    = {language, segments:[Segment], durationSec}\nCue           = {index, start, end, text}        # +optional speaker \u002F emphasis \u002F emoji\nSubtitleTrack = {id, lang, name, format, kind:\"soft\"|\"hard\", cues:[Cue]}\n```\n\n`FORMATS = (\"srt\", \"ass\", \"vtt\")` (`subtitles.py:44`). `\"ssa\"` is normalized to\n`\"ass\"` (`_normalize_format`, `subtitles.py:753`).\n\n---\n\n## RPC surface (what the UI actually calls)\n\nFrom `handlers\u002Fmedia_ops.py` — these are the wire methods, not internal helpers:\n\n| Method | Params | Returns | Sync? |\n|---|---|---|---|\n| `subtitles.generate` | `{videoId}` | `{track}` | direct (reads stored Transcript, calls `generate_polished`) |\n| `subtitles.edit` | `{trackId, cues}` | `{track}` | direct |\n| `subtitles.export` | `{trackId, format}` | `{path}` | direct |\n| `subtitles.translate` | `{trackId, targetLang, bilingual?, order?}` | `{jobId}` → `job.done {track}` | **long job** (progress streamed) |\n| `tts.dub.start` | `{videoId, trackId, engine, voice?, sampleId?, targetLang?}` | `{jobId}` → `{audioTrack, path}` | **long job** |\n\nNote: `subtitles.generate` (the handler) takes a `videoId` and loads the stored\ntranscript, then calls the PURE `generate_polished(transcript, settings=...)` —\nthe pure `subtitles.generate` takes a *Transcript*, not a videoId\n(`media_ops.py:35`). Speaker labels are prefixed when\n`captionSpeakerLabels` is on (`media_ops.py:60`, `format_speaker_prefix`).\n\n---\n\n## Recipe A — translate an existing track\n\nThe pure entry point is `subtitles.translate(track, target_lang, *, provider | translator, progress, cancelled)`\n(`subtitles.py:343`). It returns a **new** track with the same cue\ntimings\u002Findices, `lang` updated to `target_lang`; the input is never mutated.\n**Exactly one** translation seam must be supplied — an injected `translator`\n(`str -> str`) OR a `provider` (wrapped via `make_provider_translator`).\n\nIn production the handler does NOT use a single provider — it uses the\n**TieredTranslator** (`models\u002Ftranslation.py`) via\n`translator.translate_track(track, target_lang, progress=, cancelled=)`\n(`media_ops.py:137`). Cues are translated and timings preserved verbatim.\n\nPer-cue translation prompt (`subtitles.py:313` \u002F `translation.py:207`):\n\n```\nYou are a professional subtitle translator. Translate the user's text into\n{lang}. Reply with ONLY the translation — no quotes, no notes, no explanation.\nPreserve meaning and keep it concise enough to read as a subtitle.\n```\n\nBlank cues short-circuit to blank output with **no** model call\n(`make_provider_translator._translate`, `subtitles.py:330`), so empty cues stay\nempty and you do not pay for them.\n\n### The tiered MT engine (the actual translation model)\n\n`TieredTranslator` (`translation.py:247`) routes by target language:\n\n- **tier1** `TranslateGemma-4B-it Q4_K_M` (2.49 GB, fully GPU-resident) — fast\n  local default for the ~40 high\u002Fmid-resource langs in `TIER1_LANGS`\n  (`translation.py:102`: ar bg ca cs da de el en es … ro ru … zh).\n- **tier2** `TranslateGemma-12B-it Q4_K_M` (7.4 GB, **partial** offload,\n  `TIER2_GPU_LAYERS = 24` on a 6 GB card, labelled `SLOW` in progress) — the\n  low-resource\u002Fquality langs in `TIER2_LANGS` (bn gu is kn ml mr pa sw ta te ur zu).\n- **tier3** hosted OpenAI-compatible `CloudProvider` — everything outside the\n  table (`DEFAULT_TIER = TIER_HOSTED`), keyed on a non-empty `settings.cloudApiKey`.\n\n`route()` \u002F `fallback_chain()` (`translation.py:172`): the routed tier is tried\nfirst, then the remaining tiers in ascending order (tier1→tier2→tier3). On a\n**mid-batch** tier failure the WHOLE batch is retried on the next tier — never a\nmixed-tier patchwork (`translate`, `translation.py:302`). The local tiers reach\nthe GPU only through the injected `ModelRunner` whose `start_server` is\nmodel-identity-aware (switching tier1↔tier2 restarts llama.cpp with the right\nGGUF; re-using a tier reuses the live process).\n\n`normalize_lang` (`translation.py:158`): `pt-BR`\u002F`pt_BR`→`pt`, `zh_Hant`→`zh`,\n`EN`→`en`; raises `ValueError` on a blank tag so a missing `targetLang` fails\nloudly instead of silently routing hosted.\n\n### Bilingual (stacked) subtitles\n\n`subtitles.translate({..., bilingual:true, order:\"original-first\"|\"translation-first\"})`\nstacks original + translation on two lines in one cue (`stack_bilingual`,\n`subtitles.py:405`; `stack_cue_text`, `subtitles.py:392`). The bilingual track is\nadded as a **NEW** track (source kept); monolingual replaces in place\n(`media_ops.py:151`). Translation cues are matched by `index` first, then\npositional, so a slight count mismatch degrades gracefully. Result `lang` is\n`\"\u003Corig>+\u003Ctrans>\"`, name defaults to `\"Bilingual (\u003Corig>\u002F\u003Ctrans>)\"`.\n\n---\n\n## Recipe B — generate a track from a transcript\n\n`generate(transcript)` → soft track (`subtitles.py:229`). `cues_from_transcript`\n(`subtitles.py:139`) makes one cue per segment, but **splits** any segment\nlonger than `max_chars=84` chars OR `max_duration=7.0` s on word boundaries\n(needs per-word timing) so no caption is a wall of text. Blank segments dropped.\n\n`generate_polished(transcript, settings=...)` (`subtitles.py:264`) is the\nhandler's default: after cue derivation it runs the WU9 caption-polish pass\n(Netflix CPS\u002FCPL\u002Fmin-gap timing + segmentation gate, plus optional punct\u002Fcasing,\nemphasis-keyword, profanity-mask stages — each skipped when its backend is\nabsent). `lang` is taken from `transcript.language` (else `\"und\"`).\n\nFor **karaoke-grade word timing**, refine the transcript first with\n`ctc_align.align_words(transcript, audio_path, settings=...)`\n(`ctc_align.py:395`) BEFORE generating, so cues carry tight per-word boundaries.\n\n---\n\n## Recipe C — export to SRT \u002F VTT \u002F ASS\n\n`export(track, fmt, out_path)` (`subtitles.py:774`) → writes UTF-8, returns the\npath. Round-trip parsers: `read_srt` \u002F `read_vtt` \u002F `read_ass` (and `load` \u002F\n`track_from_file`). Serializers reindex cues 1..N every time (`reindex`,\n`subtitles.py:116`).\n\nTimestamp forms (the #1 source of broken subs — they are NOT interchangeable):\n\n| Format | Helper | Form | Separator |\n|---|---|---|---|\n| SRT | `format_timestamp_srt` (`:477`) | `HH:MM:SS,mmm` | **comma**, 3-digit ms |\n| VTT | `format_timestamp_vtt` (`:483`) | `HH:MM:SS.mmm` | **dot**, 3-digit ms |\n| ASS | `format_timestamp_ass` (`:489`) | `H:MM:SS.cc`   | **dot**, **2-digit centiseconds**, 1-digit hours |\n\n`parse_timestamp` (`subtitles.py:499`) accepts comma OR dot and disambiguates\n2-digit (centiseconds, ASS) vs 3-digit (milliseconds) fractions **by length** —\nso a 2-digit SRT fraction would be mis-read as centiseconds. Always write 3\ndigits for SRT\u002FVTT.\n\n---\n\n## Recipe D — burn or soft-mux captions (caption.py)\n\n`caption.py` builds the ASS doc (`build_ass`, `:304`) sized to the export canvas\n(default 1080×1920 vertical) and renders via ffmpeg. The two argv builders:\n\n**Burn-in** (`build_burn_argv`, `caption.py:476`) — hardcodes pixels (re-encodes\nvideo, audio stream-copied):\n\n```\nffmpeg -hide_banner -nostdin -y -i \u003Cclip> \\\n  -vf subtitles='\u003Cescaped ass path>' \\\n  -c:a copy -progress pipe:1 -nostats \u003Cout>\n```\n\n**Soft-mux** (`build_softmux_argv`, `caption.py:507`) — selectable subtitle\nstream, video+audio stream-copied; **codec depends on the output extension**:\n\n```\nffmpeg -hide_banner -nostdin -y -i \u003Cclip> -i \u003Cass> \\\n  -map 0:v -map 0:a? -map 1:0 \\\n  -c:v copy -c:a copy -c:s \u003Cmov_text|ass> \\\n  -progress pipe:1 -nostats \u003Cout>\n```\n\n`scodec = \"mov_text\"` for `.mp4\u002F.m4v\u002F.mov`, else `\"ass\"` (`caption.py:519`).\n\n---\n\n## Recipe E — audio dub (TTS) path\n\n`tts.dub.start` (`features\u002Ftts\u002Fdub.py`) is a **batched** long job whose stage\nORDER is the frozen A4 contract (`run_dub_pipeline`, `dub.py:115`):\n\n1. **translate ALL cues** (batched) → then **`translator.free()`** (unload the MT\n   model — never interleave model swaps with synthesis).\n2. **synth ALL cues** (one engine session).\n3. **align each cue** (frozen recipe: target duration → re-synth → `atempo` ±15%\n   → pad) — `features\u002Ftts\u002Falign.py`.\n4. **concat** to a full-length WAV → encode AAC (`-c:a aac -b:a 192k`,\n   `build_encode_aac_argv`, `dub.py:216`) → **mux as an AudioTrack**.\n\nThe dub translator is the SAME T3 `TieredTranslator`, adapted to a text-based\n`translate(texts, target_lang, source_lang) -> texts` + `free()` seam\n(`ai_ops.py:_dub_translator`, `:400`). `free()` stops the shared llama server.\nIf `targetLang` is asked but no translator is wired, the job FAILS with a clear\nerror (never silently un-translated, `dub.py:146`).\n\n---\n\n## Pitfalls that actually bite\n\n1. **SRT comma vs VTT dot vs ASS centiseconds.** Do not hand-format. Use the\n   three `format_timestamp_*` helpers; never copy an SRT `,mmm` into a VTT\u002FASS\n   file. `parse_timestamp` keys ms-vs-cs off the digit count — a 2-digit SRT\n   fraction is silently mis-parsed as centiseconds (10× wrong).\n\n2. **WEBVTT header MUST have a trailing blank line.** `to_vtt` emits\n   `\"WEBVTT\\n\\n\"` (`subtitles.py:573`) — browsers and ffmpeg **reject** a header\n   with no blank line before the first cue. Hand-edited VTT that drops it fails\n   to load.\n\n3. **ASS override injection.** Raw `{...}` in cue text are ASS override codes —\n   a transcript line `{\\fake}` or `{\\an8}` could blank\u002Freposition the line.\n   `escape_ass_text` neutralizes braces. NOTE there are TWO escapers with\n   different (intentional) encodings: `caption.py:80` maps `{`→`\\{`, `}`→`\\}`\n   (render path); `subtitles.py:619` maps `{`→`\\(`, `}`→`\\)` (a brace-FREE,\n   *reversible* encoding so round-trip parse restores the literal). Both escape\n   `\\` FIRST so they never double-process. Don't \"simplify\" them to one.\n\n4. **Soft-mux codec by container.** MP4\u002FM4V\u002FMOV need `mov_text`; MKV\u002Fother keep\n   native `ass`. Forcing `-c:s ass` into an `.mp4` makes ffmpeg refuse the mux.\n   Burn-in (`-vf subtitles=`) sidesteps this but re-encodes the video.\n\n5. **Clip time re-basing.** When captioning a clip cut from t=120 s of the\n   source, cue times are against the ORIGINAL timeline. `caption.build_ass`\n   subtracts `source_start` (`rebase_cue_time`, `caption.py:122`) and **skips**\n   cues whose end ≤ start after re-base (they lie entirely before the clip,\n   `caption.py:442`). Pass the candidate's `sourceStart` or your first caption\n   shows at the wrong time \u002F never displays.\n\n6. **ffmpeg filter-path escaping (Windows).** Inside `-vf subtitles=`, `\\`, `:`,\n   and `'` are special — a `C:\\...` path breaks the filtergraph.\n   `_escape_filter_path` (`caption.py:461`) wraps in single quotes and escapes\n   backslash-first then `:` then `'`. Always pass the ASS via this builder, never\n   string-concat a Windows path into the filter.\n\n7. **Translation count mismatch.** The dub pipeline RAISES `DubError` if the\n   translator returns a different cue count (`dub.py:165`). `stack_bilingual`\n   tolerates a mismatch (index-then-positional match) — pick the right tool.\n\n8. **Offline gate.** The cloud (tier3 \u002F `useCloud`) translate path is refused\n   with a typed error when offline (`media_ops.py:122`, `_offline.guard_network`).\n   Local llama (tier1\u002Ftier2) and `ctc_align` degrade offline-safe; word\n   alignment returns the transcript UNCHANGED when the model asset is missing\n   AND offline (`ctc_align.py:438`), never raising.\n\n9. **RTL (Arabic\u002FHebrew) is NOT explicitly handled in the pipeline.** There is\n   no BiDi-mark insertion or RTL normalization anywhere in `subtitles.py` \u002F\n   `caption.py` — cue text is passed through verbatim. Rendering relies on libass\n   (which uses fribidi) for burn-in and on the player's BiDi for soft SRT\u002FVTT.\n   Practical consequence: `ar`\u002F`he` translate fine (tier1), but if a downstream\n   consumer doesn't run the Unicode BiDi algorithm, mixed RTL+digits\u002FLatin can\n   display reordered. If you need guaranteed RTL, verify with a burn-in (libass)\n   render, not a raw-text inspection.\n\n10. **Unknown language → hosted tier3 (needs a cloud key).** A target lang not in\n    `TIER1_LANGS`\u002F`TIER2_LANGS` routes to tier3 and will fail offline \u002F without\n    `cloudApiKey`. Check `route(lang)` before promising a local-only translation.\n\n11. **Immutability.** `translate`, `edit`, `stack_bilingual`,\n    `merge_word_times_into_transcript` all return NEW dicts. Don't expect the\n    input track to change in place — read the return value.\n\n12. **Cooperative cancel + progress.** `translate` polls `cancelled()` per cue\n    and emits `progress(pct, \"translated i\u002Ftotal\")` (`subtitles.py:369`). A\n    cancelled job returns the cues translated so far — partial, not empty.\n\n---\n\n## GPU + Hugging Face model notes\n\n- **MT (TranslateGemma GGUF, llama.cpp):** tier1 4B `translategemma-4b-it.Q4_K_M.gguf`\n  (`mradermacher\u002Ftranslategemma-4b-it-GGUF`, pinned commit\n  `35a7486e…`, sha256 `81200d03…`, 2.55 GB) and tier2 12B\n  `translategemma-12b-it.Q4_K_M.gguf` (pinned commit `fdf84c9f…`, sha256\n  `b7aac4b4…`, 7.58 GB) — URLs\u002Fhashes pinned in `translation.py:74-90`. Tier2 is\n  partial-offload (`TIER2_GPU_LAYERS=24`) for a 6 GB card and labelled `SLOW`.\n\n- **Forced alignment (karaoke word timing):** default\n  `MahmoudAshraf\u002Fmms-300m-1130-forced-aligner` (**CC-BY-NC-4.0**, 158-lang, fine\n  for a local desktop tool; revision pinned `49402e95…`). Commercial swap →\n  MIT wav2vec2 ids (`MIT_MODEL_IDS`, `ctc_align.py:108`) via\n  `settings['ctcModelId']`. Romanian opt-in `gigant\u002Fromanian-wav2vec2`\n  (Apache-2.0, `RO_MODEL_IDS`). Audio decoded to mono 16 kHz f32le via ffmpeg\n  (`ctc_align.py:338`).\n\n- **ASR (upstream cues):** `faster-whisper large-v3-turbo` on `cuda`\u002F`float16`,\n  with CPU fallback to model `small` \u002F `int8` (`transcribe.py:74-82`). On Windows\n  the pip-wheel CUDA DLL dirs must be registered or the first encode raises\n  `cublas64_12.dll not found` (`_register_cuda_dll_dirs`, `transcribe.py:43`).\n  GPU→CPU fallback surfaces a LOUD `job.progress` notice\n  (`GPU_FALLBACK_NOTICE`), not silent.\n\n---\n\n## Worked example\n\nGoal: take an English track, produce a Romanian translation, export Romanian SRT\n+ a bilingual ASS, and verify timing preserved.\n\n```python\nfrom media_studio.features import subtitles\nfrom media_studio.models import translation\n\n# 1. Build the tiered translator (handler does this via translation.get_translator;\n#    here we show the pure call). settings carries cloudApiKey\u002FlocalBaseUrl etc.\ntt = translation.get_translator(settings, runner=model_runner)\n\n# route(\"ro\") -> \"tier1\" (Romanian is in TIER1_LANGS) => local 4B GGUF, offline-safe.\nassert translation.route(\"ro\") == \"tier1\"\n\n# 2. Translate the track (timings\u002Findices preserved, new track, lang=\"ro\").\nro_track = tt.translate_track(en_track, \"ro\",\n                              progress=lambda p, m: print(p, m),\n                              cancelled=lambda: False)\n\n# 3. Verify timing preservation (cardinal correctness check).\nassert [(c[\"start\"], c[\"end\"]) for c in ro_track[\"cues\"]] == \\\n       [(c[\"start\"], c[\"end\"]) for c in en_track[\"cues\"]]\n\n# 4. Export Romanian SRT (HH:MM:SS,mmm) and a bilingual ASS.\nsubtitles.export(ro_track, \"srt\", \"out\u002Fro.srt\")\n\nbi = subtitles.stack_bilingual(en_track, ro_track, order=\"original-first\")\nsubtitles.export(bi, \"ass\", \"out\u002Fen-ro.ass\")   # lang=\"en+ro\"\n```\n\nBurn the bilingual ASS into a 9:16 clip cut from t=120 s of the source (note the\n`source_start` so captions line up):\n\n```python\nfrom media_studio.features.caption import CaptionEngine\nCaptionEngine(settings).render(\n    \"clip.mp4\", bi[\"cues\"], \"clip_subbed.mp4\",\n    burn=True, width=1080, height=1920, source_start=120.0,\n    total_sec=clip_seconds, on_progress=cb,\n)\n```\n\nSoft-mux instead (selectable track, no re-encode) — pick `.mkv` to keep native\nASS styling, or `.mp4` to force `mov_text` (styling flattened):\n\n```python\nCaptionEngine(settings).render(\"clip.mp4\", bi[\"cues\"], \"clip.mkv\", burn=False)\n```\n\nVerification, not vibes: round-trip the exported file\n(`subtitles.load(\"out\u002Fro.srt\")`) and assert the cue count + first\u002Flast\ntimestamps match `ro_track`; for RTL targets (`ar`\u002F`he`) confirm via a burn-in\nrender (libass\u002Ffribidi) rather than trusting raw-text order.\n","# Subtitle translate \u002F generate \u002F export (+ dub)\n\nThis skill is distilled from the REAL media-studio implementation. Every command,\nfield name, model id, and gotcha below is taken from these source files (read\nthem when in doubt):\n\n- `media_studio\u002Ffeatures\u002Fsubtitles.py` — the pure track logic: `generate`,\n  `edit`, `translate`, `export`, `stack_bilingual`, and the SRT\u002FVTT\u002FASS\n  serializers + parsers + timestamp helpers.\n- `media_studio\u002Fmodels\u002Ftranslation.py` — the **TieredTranslator** MT engine\n  (tier1\u002Ftier2 local GGUF + tier3 hosted), language routing, fallback chain.\n- `media_studio\u002Fhandlers\u002Fmedia_ops.py` — the RPC wire shapes\n  (`subtitles.generate\u002Fedit\u002Fexport\u002Ftranslate`).\n- `media_studio\u002Ffeatures\u002Ftts\u002Fdub.py` — the audio-dub pipeline (`tts.dub.start`).\n- `media_studio\u002Ffeatures\u002Fctc_align.py` — word-timing forced alignment (karaoke).\n- `media_studio\u002Ffeatures\u002Fcaption.py` — ASS document build + ffmpeg burn\u002Fsoft-mux argv.\n- `media_studio\u002Ffeatures\u002Ftranscribe.py` — faster-whisper (upstream source of cues).\n\nThe frozen schemas (CONTRACTS.md §3, identical on both sides):\n\n```\nWord          = {text, start, end}\nSegment       = {start, end, text, words:[Word]}\nTranscript    = {language, segments:[Segment], durationSec}\nCue           = {index, start, end, text}        # +optional speaker \u002F emphasis \u002F emoji\nSubtitleTrack = {id, lang, name, format, kind:\"soft\"|\"hard\", cues:[Cue]}\n```\n\n`FORMATS = (\"srt\", \"ass\", \"vtt\")` (`subtitles.py:44`). `\"ssa\"` is normalized to\n`\"ass\"` (`_normalize_format`, `subtitles.py:753`).\n\n---\n\n## RPC surface (what the UI actually calls)\n\nFrom `handlers\u002Fmedia_ops.py` — these are the wire methods, not internal helpers:\n\n| Method | Params | Returns | Sync? |\n|---|---|---|---|\n| `subtitles.generate` | `{videoId}` | `{track}` | direct (reads stored Transcript, calls `generate_polished`) |\n| `subtitles.edit` | `{trackId, cues}` | `{track}` | direct |\n| `subtitles.export` | `{trackId, format}` | `{path}` | direct |\n| `subtitles.translate` | `{trackId, targetLang, bilingual?, order?}` | `{jobId}` → `job.done {track}` | **long job** (progress streamed) |\n| `tts.dub.start` | `{videoId, trackId, engine, voice?, sampleId?, targetLang?}` | `{jobId}` → `{audioTrack, path}` | **long job** |\n\nNote: `subtitles.generate` (the handler) takes a `videoId` and loads the stored\ntranscript, then calls the PURE `generate_polished(transcript, settings=...)` —\nthe pure `subtitles.generate` takes a *Transcript*, not a videoId\n(`media_ops.py:35`). Speaker labels are prefixed when\n`captionSpeakerLabels` is on (`media_ops.py:60`, `format_speaker_prefix`).\n\n---\n\n## Recipe A — translate an existing track\n\nThe pure entry point is `subtitles.translate(track, target_lang, *, provider | translator, progress, cancelled)`\n(`subtitles.py:343`). It returns a **new** track with the same cue\ntimings\u002Findices, `lang` updated to `target_lang`; the input is never mutated.\n**Exactly one** translation seam must be supplied — an injected `translator`\n(`str -> str`) OR a `provider` (wrapped via `make_provider_translator`).\n\nIn production the handler does NOT use a single provider — it uses the\n**TieredTranslator** (`models\u002Ftranslation.py`) via\n`translator.translate_track(track, target_lang, progress=, cancelled=)`\n(`media_ops.py:137`). Cues are translated and timings preserved verbatim.\n\nPer-cue translation prompt (`subtitles.py:313` \u002F `translation.py:207`):\n\n```\nYou are a professional subtitle translator. Translate the user's text into\n{lang}. Reply with ONLY the translation — no quotes, no notes, no explanation.\nPreserve meaning and keep it concise enough to read as a subtitle.\n```\n\nBlank cues short-circuit to blank output with **no** model call\n(`make_provider_translator._translate`, `subtitles.py:330`), so empty cues stay\nempty and you do not pay for them.\n\n### The tiered MT engine (the actual translation model)\n\n`TieredTranslator` (`translation.py:247`) routes by target language:\n\n- **tier1** `TranslateGemma-4B-it Q4_K_M` (2.49 GB, fully GPU-resident) — fast\n  local default for the ~40 high\u002Fmid-resource langs in `TIER1_LANGS`\n  (`translation.py:102`: ar bg ca cs da de el en es … ro ru … zh).\n- **tier2** `TranslateGemma-12B-it Q4_K_M` (7.4 GB, **partial** offload,\n  `TIER2_GPU_LAYERS = 24` on a 6 GB card, labelled `SLOW` in progress) — the\n  low-resource\u002Fquality langs in `TIER2_LANGS` (bn gu is kn ml mr pa sw ta te ur zu).\n- **tier3** hosted OpenAI-compatible `CloudProvider` — everything outside the\n  table (`DEFAULT_TIER = TIER_HOSTED`), keyed on a non-empty `settings.cloudApiKey`.\n\n`route()` \u002F `fallback_chain()` (`translation.py:172`): the routed tier is tried\nfirst, then the remaining tiers in ascending order (tier1→tier2→tier3). On a\n**mid-batch** tier failure the WHOLE batch is retried on the next tier — never a\nmixed-tier patchwork (`translate`, `translation.py:302`). The local tiers reach\nthe GPU only through the injected `ModelRunner` whose `start_server` is\nmodel-identity-aware (switching tier1↔tier2 restarts llama.cpp with the right\nGGUF; re-using a tier reuses the live process).\n\n`normalize_lang` (`translation.py:158`): `pt-BR`\u002F`pt_BR`→`pt`, `zh_Hant`→`zh`,\n`EN`→`en`; raises `ValueError` on a blank tag so a missing `targetLang` fails\nloudly instead of silently routing hosted.\n\n### Bilingual (stacked) subtitles\n\n`subtitles.translate({..., bilingual:true, order:\"original-first\"|\"translation-first\"})`\nstacks original + translation on two lines in one cue (`stack_bilingual`,\n`subtitles.py:405`; `stack_cue_text`, `subtitles.py:392`). The bilingual track is\nadded as a **NEW** track (source kept); monolingual replaces in place\n(`media_ops.py:151`). Translation cues are matched by `index` first, then\npositional, so a slight count mismatch degrades gracefully. Result `lang` is\n`\"\u003Corig>+\u003Ctrans>\"`, name defaults to `\"Bilingual (\u003Corig>\u002F\u003Ctrans>)\"`.\n\n---\n\n## Recipe B — generate a track from a transcript\n\n`generate(transcript)` → soft track (`subtitles.py:229`). `cues_from_transcript`\n(`subtitles.py:139`) makes one cue per segment, but **splits** any segment\nlonger than `max_chars=84` chars OR `max_duration=7.0` s on word boundaries\n(needs per-word timing) so no caption is a wall of text. Blank segments dropped.\n\n`generate_polished(transcript, settings=...)` (`subtitles.py:264`) is the\nhandler's default: after cue derivation it runs the WU9 caption-polish pass\n(Netflix CPS\u002FCPL\u002Fmin-gap timing + segmentation gate, plus optional punct\u002Fcasing,\nemphasis-keyword, profanity-mask stages — each skipped when its backend is\nabsent). `lang` is taken from `transcript.language` (else `\"und\"`).\n\nFor **karaoke-grade word timing**, refine the transcript first with\n`ctc_align.align_words(transcript, audio_path, settings=...)`\n(`ctc_align.py:395`) BEFORE generating, so cues carry tight per-word boundaries.\n\n---\n\n## Recipe C — export to SRT \u002F VTT \u002F ASS\n\n`export(track, fmt, out_path)` (`subtitles.py:774`) → writes UTF-8, returns the\npath. Round-trip parsers: `read_srt` \u002F `read_vtt` \u002F `read_ass` (and `load` \u002F\n`track_from_file`). Serializers reindex cues 1..N every time (`reindex`,\n`subtitles.py:116`).\n\nTimestamp forms (the #1 source of broken subs — they are NOT interchangeable):\n\n| Format | Helper | Form | Separator |\n|---|---|---|---|\n| SRT | `format_timestamp_srt` (`:477`) | `HH:MM:SS,mmm` | **comma**, 3-digit ms |\n| VTT | `format_timestamp_vtt` (`:483`) | `HH:MM:SS.mmm` | **dot**, 3-digit ms |\n| ASS | `format_timestamp_ass` (`:489`) | `H:MM:SS.cc`   | **dot**, **2-digit centiseconds**, 1-digit hours |\n\n`parse_timestamp` (`subtitles.py:499`) accepts comma OR dot and disambiguates\n2-digit (centiseconds, ASS) vs 3-digit (milliseconds) fractions **by length** —\nso a 2-digit SRT fraction would be mis-read as centiseconds. Always write 3\ndigits for SRT\u002FVTT.\n\n---\n\n## Recipe D — burn or soft-mux captions (caption.py)\n\n`caption.py` builds the ASS doc (`build_ass`, `:304`) sized to the export canvas\n(default 1080×1920 vertical) and renders via ffmpeg. The two argv builders:\n\n**Burn-in** (`build_burn_argv`, `caption.py:476`) — hardcodes pixels (re-encodes\nvideo, audio stream-copied):\n\n```\nffmpeg -hide_banner -nostdin -y -i \u003Cclip> \\\n  -vf subtitles='\u003Cescaped ass path>' \\\n  -c:a copy -progress pipe:1 -nostats \u003Cout>\n```\n\n**Soft-mux** (`build_softmux_argv`, `caption.py:507`) — selectable subtitle\nstream, video+audio stream-copied; **codec depends on the output extension**:\n\n```\nffmpeg -hide_banner -nostdin -y -i \u003Cclip> -i \u003Cass> \\\n  -map 0:v -map 0:a? -map 1:0 \\\n  -c:v copy -c:a copy -c:s \u003Cmov_text|ass> \\\n  -progress pipe:1 -nostats \u003Cout>\n```\n\n`scodec = \"mov_text\"` for `.mp4\u002F.m4v\u002F.mov`, else `\"ass\"` (`caption.py:519`).\n\n---\n\n## Recipe E — audio dub (TTS) path\n\n`tts.dub.start` (`features\u002Ftts\u002Fdub.py`) is a **batched** long job whose stage\nORDER is the frozen A4 contract (`run_dub_pipeline`, `dub.py:115`):\n\n1. **translate ALL cues** (batched) → then **`translator.free()`** (unload the MT\n   model — never interleave model swaps with synthesis).\n2. **synth ALL cues** (one engine session).\n3. **align each cue** (frozen recipe: target duration → re-synth → `atempo` ±15%\n   → pad) — `features\u002Ftts\u002Falign.py`.\n4. **concat** to a full-length WAV → encode AAC (`-c:a aac -b:a 192k`,\n   `build_encode_aac_argv`, `dub.py:216`) → **mux as an AudioTrack**.\n\nThe dub translator is the SAME T3 `TieredTranslator`, adapted to a text-based\n`translate(texts, target_lang, source_lang) -> texts` + `free()` seam\n(`ai_ops.py:_dub_translator`, `:400`). `free()` stops the shared llama server.\nIf `targetLang` is asked but no translator is wired, the job FAILS with a clear\nerror (never silently un-translated, `dub.py:146`).\n\n---\n\n## Pitfalls that actually bite\n\n1. **SRT comma vs VTT dot vs ASS centiseconds.** Do not hand-format. Use the\n   three `format_timestamp_*` helpers; never copy an SRT `,mmm` into a VTT\u002FASS\n   file. `parse_timestamp` keys ms-vs-cs off the digit count — a 2-digit SRT\n   fraction is silently mis-parsed as centiseconds (10× wrong).\n\n2. **WEBVTT header MUST have a trailing blank line.** `to_vtt` emits\n   `\"WEBVTT\\n\\n\"` (`subtitles.py:573`) — browsers and ffmpeg **reject** a header\n   with no blank line before the first cue. Hand-edited VTT that drops it fails\n   to load.\n\n3. **ASS override injection.** Raw `{...}` in cue text are ASS override codes —\n   a transcript line `{\\fake}` or `{\\an8}` could blank\u002Freposition the line.\n   `escape_ass_text` neutralizes braces. NOTE there are TWO escapers with\n   different (intentional) encodings: `caption.py:80` maps `{`→`\\{`, `}`→`\\}`\n   (render path); `subtitles.py:619` maps `{`→`\\(`, `}`→`\\)` (a brace-FREE,\n   *reversible* encoding so round-trip parse restores the literal). Both escape\n   `\\` FIRST so they never double-process. Don't \"simplify\" them to one.\n\n4. **Soft-mux codec by container.** MP4\u002FM4V\u002FMOV need `mov_text`; MKV\u002Fother keep\n   native `ass`. Forcing `-c:s ass` into an `.mp4` makes ffmpeg refuse the mux.\n   Burn-in (`-vf subtitles=`) sidesteps this but re-encodes the video.\n\n5. **Clip time re-basing.** When captioning a clip cut from t=120 s of the\n   source, cue times are against the ORIGINAL timeline. `caption.build_ass`\n   subtracts `source_start` (`rebase_cue_time`, `caption.py:122`) and **skips**\n   cues whose end ≤ start after re-base (they lie entirely before the clip,\n   `caption.py:442`). Pass the candidate's `sourceStart` or your first caption\n   shows at the wrong time \u002F never displays.\n\n6. **ffmpeg filter-path escaping (Windows).** Inside `-vf subtitles=`, `\\`, `:`,\n   and `'` are special — a `C:\\...` path breaks the filtergraph.\n   `_escape_filter_path` (`caption.py:461`) wraps in single quotes and escapes\n   backslash-first then `:` then `'`. Always pass the ASS via this builder, never\n   string-concat a Windows path into the filter.\n\n7. **Translation count mismatch.** The dub pipeline RAISES `DubError` if the\n   translator returns a different cue count (`dub.py:165`). `stack_bilingual`\n   tolerates a mismatch (index-then-positional match) — pick the right tool.\n\n8. **Offline gate.** The cloud (tier3 \u002F `useCloud`) translate path is refused\n   with a typed error when offline (`media_ops.py:122`, `_offline.guard_network`).\n   Local llama (tier1\u002Ftier2) and `ctc_align` degrade offline-safe; word\n   alignment returns the transcript UNCHANGED when the model asset is missing\n   AND offline (`ctc_align.py:438`), never raising.\n\n9. **RTL (Arabic\u002FHebrew) is NOT explicitly handled in the pipeline.** There is\n   no BiDi-mark insertion or RTL normalization anywhere in `subtitles.py` \u002F\n   `caption.py` — cue text is passed through verbatim. Rendering relies on libass\n   (which uses fribidi) for burn-in and on the player's BiDi for soft SRT\u002FVTT.\n   Practical consequence: `ar`\u002F`he` translate fine (tier1), but if a downstream\n   consumer doesn't run the Unicode BiDi algorithm, mixed RTL+digits\u002FLatin can\n   display reordered. If you need guaranteed RTL, verify with a burn-in (libass)\n   render, not a raw-text inspection.\n\n10. **Unknown language → hosted tier3 (needs a cloud key).** A target lang not in\n    `TIER1_LANGS`\u002F`TIER2_LANGS` routes to tier3 and will fail offline \u002F without\n    `cloudApiKey`. Check `route(lang)` before promising a local-only translation.\n\n11. **Immutability.** `translate`, `edit`, `stack_bilingual`,\n    `merge_word_times_into_transcript` all return NEW dicts. Don't expect the\n    input track to change in place — read the return value.\n\n12. **Cooperative cancel + progress.** `translate` polls `cancelled()` per cue\n    and emits `progress(pct, \"translated i\u002Ftotal\")` (`subtitles.py:369`). A\n    cancelled job returns the cues translated so far — partial, not empty.\n\n---\n\n## GPU + Hugging Face model notes\n\n- **MT (TranslateGemma GGUF, llama.cpp):** tier1 4B `translategemma-4b-it.Q4_K_M.gguf`\n  (`mradermacher\u002Ftranslategemma-4b-it-GGUF`, pinned commit\n  `35a7486e…`, sha256 `81200d03…`, 2.55 GB) and tier2 12B\n  `translategemma-12b-it.Q4_K_M.gguf` (pinned commit `fdf84c9f…`, sha256\n  `b7aac4b4…`, 7.58 GB) — URLs\u002Fhashes pinned in `translation.py:74-90`. Tier2 is\n  partial-offload (`TIER2_GPU_LAYERS=24`) for a 6 GB card and labelled `SLOW`.\n\n- **Forced alignment (karaoke word timing):** default\n  `MahmoudAshraf\u002Fmms-300m-1130-forced-aligner` (**CC-BY-NC-4.0**, 158-lang, fine\n  for a local desktop tool; revision pinned `49402e95…`). Commercial swap →\n  MIT wav2vec2 ids (`MIT_MODEL_IDS`, `ctc_align.py:108`) via\n  `settings['ctcModelId']`. Romanian opt-in `gigant\u002Fromanian-wav2vec2`\n  (Apache-2.0, `RO_MODEL_IDS`). Audio decoded to mono 16 kHz f32le via ffmpeg\n  (`ctc_align.py:338`).\n\n- **ASR (upstream cues):** `faster-whisper large-v3-turbo` on `cuda`\u002F`float16`,\n  with CPU fallback to model `small` \u002F `int8` (`transcribe.py:74-82`). On Windows\n  the pip-wheel CUDA DLL dirs must be registered or the first encode raises\n  `cublas64_12.dll not found` (`_register_cuda_dll_dirs`, `transcribe.py:43`).\n  GPU→CPU fallback surfaces a LOUD `job.progress` notice\n  (`GPU_FALLBACK_NOTICE`), not silent.\n\n---\n\n## Worked example\n\nGoal: take an English track, produce a Romanian translation, export Romanian SRT\n+ a bilingual ASS, and verify timing preserved.\n\n```python\nfrom media_studio.features import subtitles\nfrom media_studio.models import translation\n\n# 1. Build the tiered translator (handler does this via translation.get_translator;\n#    here we show the pure call). settings carries cloudApiKey\u002FlocalBaseUrl etc.\ntt = translation.get_translator(settings, runner=model_runner)\n\n# route(\"ro\") -> \"tier1\" (Romanian is in TIER1_LANGS) => local 4B GGUF, offline-safe.\nassert translation.route(\"ro\") == \"tier1\"\n\n# 2. Translate the track (timings\u002Findices preserved, new track, lang=\"ro\").\nro_track = tt.translate_track(en_track, \"ro\",\n                              progress=lambda p, m: print(p, m),\n                              cancelled=lambda: False)\n\n# 3. Verify timing preservation (cardinal correctness check).\nassert [(c[\"start\"], c[\"end\"]) for c in ro_track[\"cues\"]] == \\\n       [(c[\"start\"], c[\"end\"]) for c in en_track[\"cues\"]]\n\n# 4. Export Romanian SRT (HH:MM:SS,mmm) and a bilingual ASS.\nsubtitles.export(ro_track, \"srt\", \"out\u002Fro.srt\")\n\nbi = subtitles.stack_bilingual(en_track, ro_track, order=\"original-first\")\nsubtitles.export(bi, \"ass\", \"out\u002Fen-ro.ass\")   # lang=\"en+ro\"\n```\n\nBurn the bilingual ASS into a 9:16 clip cut from t=120 s of the source (note the\n`source_start` so captions line up):\n\n```python\nfrom media_studio.features.caption import CaptionEngine\nCaptionEngine(settings).render(\n    \"clip.mp4\", bi[\"cues\"], \"clip_subbed.mp4\",\n    burn=True, width=1080, height=1920, source_start=120.0,\n    total_sec=clip_seconds, on_progress=cb,\n)\n```\n\nSoft-mux instead (selectable track, no re-encode) — pick `.mkv` to keep native\nASS styling, or `.mp4` to force `mov_text` (styling flattened):\n\n```python\nCaptionEngine(settings).render(\"clip.mp4\", bi[\"cues\"], \"clip.mkv\", burn=False)\n```\n\nVerification, not vibes: round-trip the exported file\n(`subtitles.load(\"out\u002Fro.srt\")`) and assert the cue count + first\u002Flast\ntimestamps match `ro_track`; for RTL targets (`ar`\u002F`he`) confirm via a burn-in\nrender (libass\u002Ffribidi) rather than trusting raw-text order.\n",{"name":5,"description":7},"main","a298c929b6cbd662e1fce54302ce520d5ceea1a5",".agents\u002Fskills\u002Fsubtitle-translate","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain","https:\u002F\u002Fgithub.com\u002FPrekzursil","2026-11-02 14:30:57",{"source":8,"skillName":5,"commands":51,"prompt":61,"zipAvailable":62},[52,55,58],{"tool":53,"command":54},"npx","npx skills add Prekzursil\u002Fagent-skills-toolchain --skill subtitle-translate",{"tool":56,"command":57},"bunx","bunx skills add Prekzursil\u002Fagent-skills-toolchain --skill subtitle-translate",{"tool":59,"command":60},"pnpm","pnpm dlx skills add Prekzursil\u002Fagent-skills-toolchain --skill subtitle-translate","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fsubtitle-translate\u002FSKILL.md",true,[],[65,86,114,141,155,171],{"skillNo":66,"name":67,"nameZh":6,"description":68,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":69,"categories":70,"keywords":71,"publishedAt":39},"01M6DF7D7478R55P46REYH4QAC","clarify-requirements","Resolve genuine pre-execution ambiguity by scanning repo context, generating 1-6 grounded options, and asking 1-3 focused questions via AskUserQuestion. NOT for mid-task use (injection risk); NOT when the goal is already unambiguous; use to disambiguate vague prompts before any file edits.","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002Ftools\u002Fworkspace-hooks\u002Fskills\u002Fclarify-requirements\u002FSKILL.md",[],[72,73,74,75,76,77,78,79,80,36,81,82,83,84,85],"clarify requirements","clarify","requirements","risk","options","task","prompts","goal","scanning","context","repo","injection","execution","file",{"skillNo":87,"name":88,"nameZh":6,"description":89,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":90,"categories":91,"keywords":95,"publishedAt":39},"01M6DF79CA4BHJS66J8EZDBPWJ","openmm-md","Run FREE, open-source, Windows-native GPU molecular dynamics via the openmm-md MCP (OpenMM + CUDA on the local NVIDIA GPU). Use for protein \u002F protein-ligand MD when you want a free stack or are on Windows without a Schrodinger license. Covers the full workflow (prepare, solvate, minimize, equilibrate, production, analyze, mmgbsa) AND an honest decision guide vs Schrodinger Desmond, including how to run Desmond legitimately on Linux or a cloud GPU with a real license. This is a deliberately LESSER free alternative and does NOT replace the schrodinger MCP tools. Do NOT use it to pick a free tool when a validated FEP+, WaterMap, IFD-MD, or Prime MM-GBSA result is actually required.","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002Ftools\u002Fmcp\u002Fopenmm-md-mcp\u002Fskill\u002Fopenmm-md\u002FSKILL.md",[92],{"categoryNo":93,"categoryName":94,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[96,97,98,99,100,101,102,103,104,105,106,107,108,109,36,110,111,38,112,113],"openmm md","how","decision","pick","linux","analyze","tool","stack","open","NVIDIA","gpu","license","workflow","windows","free","molecular dynamics","do","MCP",{"skillNo":115,"name":116,"nameZh":6,"description":117,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":118,"categories":119,"keywords":126,"publishedAt":39},"01M6DF76C1MG85VEWP8VZCBNMN","visual-review","Take screenshots of web pages and UI using Playwright for visual review and iteration","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002Ftools\u002Feditor\u002Fmetaswarm\u002Fskills\u002Fvisual-review\u002FSKILL.md",[120,123],{"categoryNo":121,"categoryName":122,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000003","演示文稿",{"categoryNo":124,"categoryName":125,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量",[127,128,129,130,131,132,133,134,135,136,97,137,138,139,140],"review","visual","visual review","pages","web","false","iteration","take","check","slides","ui","screenshot","playwright","screenshots",{"skillNo":142,"name":143,"nameZh":6,"description":144,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":145,"categories":146,"keywords":147,"publishedAt":39},"01M6DF73AQVCBZKEN0YXEX9FXP","status","Diagnostic status report — shows metaswarm installation state, project setup, and potential issues","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002Ftools\u002Feditor\u002Fmetaswarm\u002Fskills\u002Fstatus\u002FSKILL.md",[],[143,148,149,150,151,152,153,154],"setup","report","project","installation","project setup","issues","state",{"skillNo":156,"name":157,"nameZh":6,"description":158,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":159,"categories":160,"keywords":162,"publishedAt":39},"01M6DF6X6C2XB0P6GX76P7TTYV","start","Use when starting work on any task, when the user mentions metaswarm, or when the user wants to begin tracked development work","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002Ftools\u002Feditor\u002Fmetaswarm\u002Fskills\u002Fstart\u002FSKILL.md",[161],{"categoryNo":93,"categoryName":94,"taxonomyType":21,"taxonomyTypeDesc":22},[157,163,77,164,165,166,36,167,168,169,170],"start issue","issue","ready","work on issue","user","work","work on","development",{"skillNo":172,"name":148,"nameZh":6,"description":173,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":174,"categories":175,"keywords":176,"publishedAt":39},"01M6DF6QMJ9AJKFKHHKRT005PQ","Interactive project setup — detects your project, configures metaswarm, writes project-local files","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002Ftools\u002Feditor\u002Fmetaswarm\u002Fskills\u002Fsetup\u002FSKILL.md",[],[148,150,38,177,152,178],"files","interactive"]