lai-caption · diff

git:20260417.89d056b to git:20260428.4ab9c84

110 added, 226 removed. Audit A to A.

---
name: lai-caption
- description: Use when converting between caption/subtitle formats or adjusting timing. Supports 30+ formats including SRT, VTT, ASS, TTML, JSON, TextGrid, LRC, and NLE formats (Premiere, FCPXML). Use this skill whenever the user needs to change subtitle format, shift caption timing, add ASS styling, create karaoke effects, or export captions for video editing software. Also triggers on "convert captions", "转换字幕格式", "change format", "shift timing", "SRT to VTT", "ASS styling", "转换成SRT", "make ASS subtitles", "karaoke effect", "导入Premiere", even if they just say "I have an SRT and need a VTT" or "shift all timestamps by 2 seconds".
+ description: Convert between 30+ caption/subtitle formats (SRT, VTT, ASS, JSON, TextGrid, LRC, FCPXML, Premiere, …) and shift timing. Trigger on "convert captions", "SRT to VTT", "转换字幕格式", "shift timing", "ASS styling", "karaoke effect", "导入Premiere", or any caption-format question. Do NOT trigger to fix timing accuracy (`/lai-align`) or translate (`/lai-translate`).
allowed-tools: Read, Bash(laicap:*), Bash(lai:*)
---
# Caption Format Conversion
- Convert between 30+ caption/subtitle formats with optional timing adjustment
- and ASS styling. Preserves text content, timing, speaker labels, and
- word-level alignment data where the target format supports it.
-
- **CLI syntax note**: LattifAI uses `nemo_run` configuration syntax. Options use
- `key=value` format (e.g., `caption.ass.font_size=48`), NOT traditional flags.
- This applies to all `lai` subcommands.
-
- ## When to Use
-
- - Convert between caption formats (SRT to VTT, JSON to ASS, etc.)
- - Shift timing (offset all timestamps by N seconds)
- - Apply ASS styling (fonts, colors, karaoke effects)
- - Prepare captions for specific platforms or players
- - Export aligned JSON to human-readable formats
+ Convert, shift, and style caption files. Format is auto-detected from file extension.
- ## When NOT to Use
+ `laicap-convert` and `lai caption convert` are **the same command** — the former is the shortcut entry-point. For pipeline / non-interactive use, add `--direct -Y` (direct execution, skip confirmation).
- - Need to fix timing accuracy -- use `/lai-align` instead
- - Need to generate captions from audio -- use `/lai-transcribe`
- - Need translation -- use `/lai-translate`
+ ## Convert
- ## CLI Usage
+ `laicap-convert` (== `lai caption convert`) takes two positional args: `input_path` and `output_path`. All styling/rendering keys are **flat top-level config** (`render.*`, `ass.*`), **not** `caption.*`.
- ### Format Conversion
+ All examples assume a single `<base>` (media stem or YouTube ID) reused across the pipeline; outputs land in the current directory.
```bash
- laicap-convert input.srt output.vtt
+ # <base> = podcast
+ laicap-convert podcast.srt podcast.vtt
```
- Full command form:
+ Common pairs:
```bash
- lai caption convert input.srt output.vtt
+ laicap-convert podcast.aligned.json podcast.srt # JSON → playback formats
+ laicap-convert podcast.srt podcast.ass # basic → styled
+ laicap-convert podcast.transcript.md podcast.srt # Gemini markdown → SRT
+ laicap-convert podcast.aligned.json podcast.TextGrid # → Praat
```
- Format is auto-detected from file extension.
+ Add `-Y` for non-interactive runs. Use `input_format=srt` (top-level flag) to override auto-detection when the extension is wrong.
- ### Common Conversions
+ ## Shift Timing
```bash
- # SRT to VTT (for web playback)
- laicap-convert video.srt video.vtt
-
- # JSON to SRT (for players)
- laicap-convert aligned.json aligned.srt
-
- # SRT to ASS (for styled subtitles)
- laicap-convert video.srt video.ass
-
- # JSON to TextGrid (for Praat linguistic analysis)
- laicap-convert aligned.json aligned.TextGrid
-
- # Gemini MD to SRT
- laicap-convert transcript.md transcript.srt
-
- # Any format to JSON (preserve all data)
- laicap-convert caption.srt caption.json
+ laicap-shift podcast.srt podcast.shifted.srt 2.5 # forward 2.5 s
+ laicap-shift podcast.srt podcast.shifted.srt -1.0 # backward 1 s
```
- ### Timing Shift
+ ## ASS Styling
- Offset all timestamps by a fixed amount:
+ Top-level `ass.*` keys (see `lai caption convert --help` for the full list):
```bash
- laicap-shift input.srt output.srt 2.5 # Shift forward 2.5 seconds
- laicap-shift input.srt output.srt -1.0 # Shift backward 1 second
+ laicap-convert podcast.srt podcast.ass \
+ ass.font_name="Noto Sans CJK SC" \
+ ass.font_size=48 \
+ ass.primary_color="#FFFFFF"
```
- Full command form:
+ ### Karaoke (per-word highlighting)
+ Requires word-level JSON input (produced by `/lai-align` with `caption.render.word_level=true`, or `/lai-youtube` with the same flag — the upstream flag populates `words` arrays in the JSON).
+
```bash
- lai caption shift input.srt output.srt 2.5
+ laicap-convert podcast.aligned.json podcast.karaoke.ass \
+ ass.karaoke_effect=sweep \
+ ass.karaoke_color_scheme=azure-gold
```
- ### ASS Styling Options
-
- When outputting to ASS format:
+ > **`render.word_level` scope** — default is word-scope (per-word `\k` / `\kf` / `\ko` emission). Only pass `render.word_level=false` to switch to line-scope (a single override at event start, whole text block animates together — CapCut-style "fade in"). You don't need `render.word_level=true` to enable karaoke; `ass.karaoke_effect` alone does it.
- ```bash
- laicap-convert input.srt output.ass \
- caption.ass.font_name="Noto Sans CJK SC" \
- caption.ass.font_size=48 \
- caption.ass.primary_color="#FFFFFF" \
- caption.ass.outline_color="#000000"
- ```
+ - `ass.karaoke_effect`: `sweep` (classic karaoke fill), `instant` (hard on/off), `outline` (outline-only highlight)
+ - `ass.karaoke_color_scheme` (12 presets, each tunes primary/secondary/outline/back):
+ `azure-gold`, `sakura-purple`, `mint-ocean`, `gardenia-green`, `sunset-warm`,
+ `prussian-elegant`, `burgundy-classic`, `langgan-spring`, `mars-teal`,
+ `spring-field`, `navy-pink`, `apricot-dark`
+ - `ass.kinetic_style` (orthogonal per-word animation, 15 options grouped by feel):
+ - **Impact**: `bounce`, `pop`, `shake`, `pulse`, `swing`
+ - **Smooth**: `fade`, `zoom`, `rise`, `typewriter`, `blur_in`
+ - **Stylized**: `glow`, `neon`, `wave`, `flicker`, `stagger`
### Bilingual ASS
+ Format selection, typography conventions (line order / color / font-size ratio), and four scenario recipes (learning SRT / social ASS / karaoke / dual-track upload) all live in **`/lai-translate` §Bilingual Delivery Guide** — don't duplicate. Minimal one-liner for a bilingual JSON from `/lai-translate`:
+
```bash
- laicap-convert bilingual.srt bilingual.ass \
- caption.ass.style=bilingual \
- caption.ass.line1_color="#00FF00" \
- caption.ass.line2_color="#FFFF00"
+ # `<base>.translated.json` is the merged JSON (source + translation) from
+ # `/lai-translate merge.py --bilingual`.
+ laicap-convert podcast.translated.json podcast.zh.translated.ass \
+ ass.primary_color="#FFFFFF" \
+ ass.translation_color="#FFC209"
```
- ### Karaoke Effect (Word-Level)
-
- Requires word-level timing data (from alignment JSON):
+ Add `ass.karaoke_effect=sweep ass.karaoke_color_scheme=...` for per-word karaoke on the source line.
- ```bash
- laicap-convert aligned.json karaoke.ass \
- caption.ass.karaoke_effect=true
- ```
+ ### Speaker Color
- ### Speaker-Colored ASS
+ `ass.speaker_color=...` paints dialogue per speaker (needs `/lai-diarize` output in the source). Accepts:
- Different colors per speaker:
+ - `""` — disabled (default)
+ - `"auto"` — 10-color LattifAI palette (cycles for >10 speakers)
+ - `"#RRGGBB,#RRGGBB,..."` — CSV of explicit colors, one per speaker in appearance order
```bash
- laicap-convert diarized.json speakers.ass \
- caption.render.include_speaker_in_text=true \
- caption.ass.speaker_color=true
+ # Host = cyan-blue, Guest = pink — short CSV palette
+ laicap-convert diarized.json out.ass \
+ render.include_speaker_in_text=true \
+ ass.speaker_color="#658AE4,#F7C3D9"
```
- ## Supported Formats
-
- ### Standard Subtitle Formats
-
- | Format | Extension | Read | Write | Notes |
- |--------|-----------|------|-------|-------|
- | SubRip | `.srt` | Yes | Yes | Most universal format |
- | WebVTT | `.vtt` | Yes | Yes | Web standard, supports styling |
- | ASS/SSA | `.ass`, `.ssa` | Yes | Yes | Rich styling, karaoke |
- | LRC | `.lrc` | Yes | Yes | Lyrics format |
- | Plain text | `.txt` | Yes | Yes | Text only, no timing |
- | Markdown | `.md` | Yes | Yes | Gemini-style with timestamps |
-
- ### Data / Interchange Formats
-
- | Format | Extension | Read | Write | Notes |
- |--------|-----------|------|-------|-------|
- | JSON | `.json` | Yes | Yes | Full data (word-level, speakers) |
- | TSV | `.tsv` | Yes | Yes | Tab-separated |
- | CSV | `.csv` | Yes | Yes | Comma-separated |
- | Audacity | `.aud` | Yes | Yes | Audacity label track |
-
- ### Linguistic Formats
-
- | Format | Extension | Read | Write | Notes |
- |--------|-----------|------|-------|-------|
- | TextGrid (Praat) | `.TextGrid` | Yes | Yes | Phonetic analysis |
- | TTML | `.ttml`, `.xml` | Yes | Yes | W3C timed text |
-
- ### NLE (Non-Linear Editing) Formats
-
- | Format | Extension | Read | Write | Notes |
- |--------|-----------|------|-------|-------|
- | FCPXML | `.fcpxml` | Yes | Yes | Final Cut Pro |
- | Premiere XML | `.prproj.xml` | No | Yes | Adobe Premiere |
-
- ## Parameters
-
- ### Convert
-
- | Parameter | CLI Syntax | Default | Description |
- |-----------|-----------|---------|-------------|
- | Input | positional arg 1 | required | Source caption file |
- | Output | positional arg 2 | required | Target caption file |
- | Input format | `caption.input_format=X` | auto | Override format detection |
- | Word-level | `caption.render.word_level=true` | `false` | Include word timing |
- | Include speaker | `caption.render.include_speaker_in_text=true` | `false` | Embed speaker labels |
- | Translation first | `caption.render.translation_first=true` | `false` | Show translation above source |
-
- ### Shift
-
- | Parameter | CLI Syntax | Default | Description |
- |-----------|-----------|---------|-------------|
- | Input | positional arg 1 | required | Source caption file |
- | Output | positional arg 2 | required | Target caption file |
- | Offset | positional arg 3 | required | Seconds to shift (positive or negative) |
-
- ### ASS Options
-
- | Parameter | CLI Syntax | Default | Description |
- |-----------|-----------|---------|-------------|
- | Font | `caption.ass.font_name=X` | system default | Font family |
- | Font size | `caption.ass.font_size=N` | 24 | Font size |
- | Primary color | `caption.ass.primary_color=#HEX` | `#FFFFFF` | Text color |
- | Outline color | `caption.ass.outline_color=#HEX` | `#000000` | Outline color |
- | Karaoke | `caption.ass.karaoke_effect=true` | `false` | Karaoke highlighting |
- | Speaker color | `caption.ass.speaker_color=true` | `false` | Per-speaker coloring |
- | Bilingual style | `caption.ass.style=bilingual` | default | Dual-line layout |
-
- ## Workflow
-
- ### Step 1 -- Identify source format
+ ## Broadcast-Grade Profiles (standardization.*)
- Read the input file to determine its format and content:
- - File extension for format detection
- - Check for word-level data (JSON with `alignment` or `words` field)
- - Check for speaker labels
- - Check for bilingual content
+ `standardization.*` reflows segments to meet CPL/CPS/duration rules before writing. Useful for delivering SRT/VTT to Netflix-class platforms or tighter YouTube specs.
- ### Step 2 -- Choose target format
+ Fields (all top-level):
- | Need | Recommended Format |
- |------|--------------------|
- | Universal playback | `.srt` |
- | Web embedding | `.vtt` |
- | Styled subtitles | `.ass` |
- | Downstream processing | `.json` |
- | Linguistic analysis | `.TextGrid` |
- | Video editing (FCP) | `.fcpxml` |
- | Lyrics display | `.lrc` |
+ - `standardization.min_duration` / `max_duration` — segment duration bounds (s)
+ - `standardization.min_gap` — minimum inter-segment gap (s)
+ - `standardization.max_lines` / `max_chars_per_line` — line wrapping limits
+ - `standardization.optimal_cps` — target characters-per-second for readability
+ - `standardization.start_margin` / `end_margin` — pre/post roll per segment
+ - `standardization.margin_collision_mode` — `trim` (default), `drop`, …
- ### Step 3 -- Run conversion
+ Ready-made profiles:
```bash
- laicap-convert <input> <output> [options]
- ```
-
- ### Step 4 -- Verify output
-
- Read the first few segments to confirm:
- - Text preserved correctly
- - Timing intact
- - Speaker labels present (if expected)
- - Styling applied (for ASS)
-
- ## Data Preservation
-
- When converting between formats, data preservation varies:
+ # Netflix-ish: 42 CPL × 2 lines, 0.8-7 s
+ laicap-convert diarized.json out.netflix.srt \
+ standardization.min_duration=0.8 \
+ standardization.max_duration=7.0 \
+ standardization.min_gap=0.08 \
+ standardization.max_lines=2 \
+ standardization.max_chars_per_line=42 \
+ standardization.start_margin=0.05 \
+ standardization.end_margin=0.15
- | Data | JSON | SRT | VTT | ASS | TextGrid | TXT |
- |------|------|-----|-----|-----|----------|-----|
- | Text | Yes | Yes | Yes | Yes | Yes | Yes |
- | Timing | Yes | Yes | Yes | Yes | Yes | No |
- | Speakers | Yes | Partial | Yes | Yes | Yes | No |
- | Word-level | Yes | No | No | Karaoke | Yes | No |
- | Translation | Yes | Bilingual | Bilingual | Bilingual | No | No |
- | Styling | No | No | Basic | Full | No | No |
+ # YouTube-ish: shorter cues, narrower lines
+ laicap-convert diarized.json out.youtube.srt \
+ standardization.min_duration=0.5 \
+ standardization.max_duration=5.0 \
+ standardization.max_chars_per_line=35 \
+ standardization.start_margin=0.03 \
+ standardization.end_margin=0.10
+ ```
- **Rule**: Convert to JSON first to preserve maximum data. Then convert
- from JSON to the target format for delivery.
+ `start_margin` / `end_margin` also benefit karaoke exports (give lyrics breathing room) — combine with the karaoke recipe above.
- ## Error Handling
+ ## Supported Formats
- | Condition | Action |
- |-----------|--------|
- | Unknown input format | Specify with `caption.input_format=srt` |
- | Empty file | Check file has content |
- | Encoding error | Ensure file is UTF-8 |
- | Missing timing data (for SRT/VTT) | Input may be plain text. Add timing via `/lai-align` |
- | Karaoke requested but no word-level | Align first with `caption.render.word_level=true` |
+ | Category | Formats (read & write unless noted) |
+ |----------|-------------------------------------|
+ | Standard | `.srt`, `.vtt`, `.ass` / `.ssa`, `.lrc`, `.txt`, `.md` |
+ | Data | `.json`, `.tsv`, `.csv`, `.aud` |
+ | Linguistic | `.TextGrid`, `.ttml` / `.xml` |
+ | NLE | `.fcpxml`; `.prproj.xml` (write-only) |
- ## Related Skills
+ Full list: `lai caption convert --help`.
- | Skill | Use When |
- |-------|----------|
- | `/lai-align` | Need precise timing before conversion |
- | `/lai-transcribe` | Need to generate captions first |
- | `/lai-translate` | Need translation before bilingual conversion |
- | `/lai-diarize` | Need speaker labels before speaker-colored output |
+ ### Data Preservation
- ### Common Workflow Chains
+ Convert to `.json` first to keep word-level timing, speakers, and translations — then fan out to delivery formats.
- ```bash
- # Align -> Convert to multiple formats
- lai alignment align audio.mp3 caption.srt aligned.json \
- caption.render.word_level=true
- laicap-convert aligned.json aligned.srt
- laicap-convert aligned.json aligned.vtt
- laicap-convert aligned.json karaoke.ass caption.ass.karaoke_effect=true
+ - **Word-level timing**: JSON, TextGrid, ASS-karaoke only
+ - **Speakers**: JSON, VTT, ASS, TextGrid (partial in SRT)
+ - **Full styling**: ASS only
- # Shift + Convert
- laicap-shift input.srt shifted.srt -2.0
- laicap-convert shifted.srt shifted.vtt
+ ## Common Issues
- # JSON -> bilingual ASS after translation
- laicap-convert translated.json bilingual.ass \
- caption.ass.style=bilingual \
- caption.ass.line1_color="#FFFFFF" \
- caption.ass.line2_color="#FFD700"
- ```
+ | Problem | Fix |
+ |---------|-----|
+ | Unknown input format | Specify `input_format=srt` (top-level flag, not `caption.input_format`) |
+ | Encoding error | Re-save the file as UTF-8 |
+ | Karaoke has no highlighting | Source JSON needs `words` arrays — re-run `/lai-align` with `caption.render.word_level=true`, then convert with just `ass.karaoke_effect=sweep` (no `render.word_level` needed; default is word-scope) |
+ | `No parameter named 'caption'` / `'input'` | For `laicap-convert`, styling keys are flat (`render.*`, `ass.*`) and input/output are positional (`input_path`/`output_path`) — there is no `caption.*` namespace here |
+ | Plain text missing timing | Add timing via `/lai-align` first |
- ## Non-Goals
+ ## Related Skills
- - Does NOT fix timing accuracy -- use `/lai-align`
- - Does NOT translate text -- use `/lai-translate`
- - Does NOT generate captions from audio -- use `/lai-transcribe`
- - Does NOT edit caption text content
- - Does NOT merge multiple caption files into one
+ - `/lai-align` — fix timing accuracy (conversion doesn't change timing)
+ - `/lai-transcribe` — generate captions from audio first
+ - `/lai-translate` — translate before bilingual conversion
+ - `/lai-diarize` — add speaker labels for speaker-colored output