Skip to content
Back to skills

Auto Subtitles

ASecurity

Auto-generate styled hardcoded subtitles from speech for any video. Whisper (large-v3) transcribes audio with word-level timestamps, splits into semantic chunks of 2-5 words, burns into video with a custom font and configurable colored plate. Works for any language Whisper supports. Use when the user wants automatic subtitles, voice-to-text caption burn-in, hardcoded subs for social media (TikTok, Reels, Shorts), brand-styled captions, automatic transcription with timing. Triggers — "auto sub...

  • 5 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
content-marketingpythongobash

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 3 files and shows the line behind each finding

Scanned November 7, 2026

npx -y skills add Wanfeng1028/muse --skill auto-subtitles --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Auto Subtitles?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Auto Subtitles
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/wanfeng1028-auto-subtitles/badge)](https://www.skillsdirectory.com/skills/wanfeng1028-auto-subtitles)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: auto-subtitles
version: 1.0.0
description: Auto-generate styled hardcoded subtitles from speech for any video. Whisper (large-v3) transcribes audio with word-level timestamps, splits into semantic chunks of 2-5 words, burns into video with a custom font and configurable colored plate. Works for any language Whisper supports. Use when the user wants automatic subtitles, voice-to-text caption burn-in, hardcoded subs for social media (TikTok, Reels, Shorts), brand-styled captions, automatic transcription with timing. Triggers — "auto subtitles", "burn subtitles", "voice to subtitles", "hardcoded captions", "вшить субтитры", "автотитры", "субтитры по голосу", "распознай речь и сделай титры", "субтитры с таймингом", "TikTok subtitles", "Reels captions".
---

# Auto-Subtitles — voice-driven hardcoded captions

End-to-end pipeline: video file → Whisper transcription with word timestamps → smart semantic chunking → ASS subtitle styling → ffmpeg burn-in.

## When to use

- TikTok / Reels / Shorts captions (most viewers watch muted)
- YouTube hardcoded subs for translation
- Brand video content where editor (CapCut) doesn't preserve subtitle styles
- Course/lecture recordings — auto-captions in any language

## Required from user

Ask for these before generating anything:

1. **Path to video file(s)** — `.mp4`, `.mov`, `.mkv`, etc.
2. **Language** — `ru`, `en`, `es`, `fr`, `de`, `zh`, `auto`, etc.
3. **Font** — path to a `.otf` / `.ttf` file. If user doesn't have one, suggest free options: Inter, Roboto, Montserrat, Bebas Neue. Drop in any local font file path.
4. **Frame size** — usually `1080×1920` (vertical / 9:16), `1920×1080` (horizontal), `1080×1080` (square)
5. **Plate position** — top or bottom. Default: top (doesn't cover faces if speaker is centered)
6. **Brand colors** (optional) — text color, plate color, accent color. Default: white text on `rgba(0,0,0,0.55)` plate
7. **Glossary** (optional but recommended) — list of names, brands, technical terms to seed Whisper for better accuracy

## Tech stack

- **mlx-whisper** for Apple Silicon (`pip install mlx-whisper`) — 5-10× faster than vanilla whisper
- **openai-whisper** fallback for non-Mac (`pip install openai-whisper`)
- **ffmpeg** with libass — `brew install ffmpeg` / `apt install ffmpeg`
- **Python 3.9+**

## Step 1 — Transcription with word timestamps

```bash
# Apple Silicon (fast)
mlx_whisper --model mlx-community/whisper-large-v3-mlx \
  --language ru \
  --output-format json \
  --word-timestamps True \
  --initial-prompt "BrandName ProductName customTerm1 customTerm2" \
  --output-dir transcripts/ \
  video.mp4

# Cross-platform
whisper video.mp4 --model large-v3 --language ru \
  --output_format json --word_timestamps True \
  --initial_prompt "..." --output_dir transcripts/
```

**Word-level timestamps are critical** — without them subtitles drift 0.5-1 sec from speech.

## Step 2 — Smart chunking + burn-in

```python
"""
burn-subs.py — voice-driven subtitle burn-in.
Usage:
  python3 burn-subs.py <video> <whisper.json> [output.mp4]

Config: edit constants at top of file.
"""
import sys, re, json, subprocess
from pathlib import Path

# ─── CONFIG (edit these) ───
FONT_NAME    = "Inter"                          # font family (must match what's in FONTS_DIR)
FONT_SIZE    = 60                                # 60pt good for 1080×1920; scale proportionally for other sizes
FONTS_DIR    = "/path/to/your/fonts"            # folder containing .otf/.ttf files
VIDEO_W      = 1080
VIDEO_H      = 1920
STRIP_Y      = 140                               # plate top edge (px from video top)
STRIP_H      = 140                               # plate height
PLATE_COLOR  = "black@0.55"                      # ffmpeg color/alpha
TEXT_COLOR   = "&H00FFFFFF"                      # ASS color (BGR hex, &H + alpha + BBGGRR). FFFFFF = white
OUTLINE_COLOR = "&H00000000"                     # black outline for legibility
OUTLINE_WIDTH = 2

MIN_WORDS, MAX_WORDS, MAX_CHARS = 2, 5, 32
PAUSE_THRESHOLD = 0.40                           # break on pauses ≥ 400ms
PUNCT_HARD = ".!?…"                              # hard break after
PUNCT_SOFT = ",;:—–"                             # soft break if ≥ MIN_WORDS

# Whisper hallucinations to skip (common training-data artifacts)
HALLUCINATIONS = re.compile(
    r'(?i)(subtitles\s+by|субтитры\s+(?:созда|подготов|сделал)|dimatorzok|'
    r'thank\s+you\s+for\s+watching|please\s+subscribe|продолжение\s+следует)'
)

def ms_to_ass(ms):
    h = ms // 3600000; ms %= 3600000
    m = ms // 60000; ms %= 60000
    s = ms // 1000; cs = (ms % 1000) // 10
    return f"{h}:{m:02d}:{s:02d}.{cs:02d}"

def smart_chunks(data):
    words = []
    for seg in data.get("segments", []):
        if HALLUCINATIONS.search(seg.get("text", "")):
            continue
        for w in seg.get("words", []):
            tok = w.get("word", "").strip()
            if tok:
                words.append({"text": tok, "start": w["start"], "end": w["end"]})
    chunks, cur, cur_chars = [], [], 0
    def flush():
        nonlocal cur, cur_chars
        if not cur: return
        chunks.append({
            "start": int(cur[0]["start"]*1000),
            "end":   int(cur[-1]["end"]*1000),
            "text":  " ".join(w["text"] for w in cur)
        })
        cur, cur_chars = [], 0
    for i, w in enumerate(words):
        cur.append(w); cur_chars += len(w["text"]) + (1 if len(cur) > 1 else 0)
        last = w["text"][-1] if w["text"] else ""
        nxt = words[i+1] if i+1 < len(words) else None
        pause = (nxt["start"] - w["end"]) if nxt else 999
        force = False
        if len(cur) >= MAX_WORDS: force = True
        elif cur_chars >= MAX_CHARS and len(cur) >= MIN_WORDS: force = True
        elif last in PUNCT_HARD: force = True
        elif last in PUNCT_SOFT and len(cur) >= MIN_WORDS: force = True
        elif pause >= PAUSE_THRESHOLD and len(cur) >= MIN_WORDS: force = True
        elif nxt is None: force = True
        if force: flush()
    return chunks

def write_ass(cues, path):
    plate_center = STRIP_Y + STRIP_H // 2
    header = f"""[Script Info]
ScriptType: v4.00+
PlayResX: {VIDEO_W}
PlayResY: {VIDEO_H}
WrapStyle: 2
ScaledBorderAndShadow: yes

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,{FONT_NAME},{FONT_SIZE},{TEXT_COLOR},&H000000FF,{OUTLINE_COLOR},&H80000000,-1,0,0,0,100,100,0,0,1,{OUTLINE_WIDTH},0,5,30,30,0,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
"""
    lines = []
    for c in cues:
        txt = f"{{\\pos({VIDEO_W//2},{plate_center})}}" + c["text"]
        lines.append(f"Dialogue: 0,{ms_to_ass(c['start'])},{ms_to_ass(c['end'])},Default,,0,0,0,,{txt}")
    path.write_text(header + "\n".join(lines), encoding="utf-8")

def burn(video, ass, out):
    vf = (f"drawbox=x=0:y={STRIP_Y}:w=iw:h={STRIP_H}:color={PLATE_COLOR}:t=fill,"
          f"subtitles={ass}:fontsdir={FONTS_DIR}")
    subprocess.check_call([
        "ffmpeg","-y","-loglevel","error","-stats",
        "-i", str(video), "-vf", vf,
        "-c:v","libx264","-preset","fast","-crf","20",
        "-c:a","copy","-movflags","+faststart", str(out)
    ])

def main():
    video = Path(sys.argv[1])
    js    = Path(sys.argv[2])
    out   = Path(sys.argv[3]) if len(sys.argv) > 3 else video.with_name(video.stem + "_subs.mp4")
    ass   = video.with_suffix(".ass")
    cues  = smart_chunks(json.loads(js.read_text(encoding="utf-8")))
    print(f"chunks: {len(cues)}")
    write_ass(cues, ass)
    burn(video, ass, out)
    ass.unlink()
    print(f"done: {out}")

if __name__ == "__main__":
    main()
```

## Step 3 — Batch processing

```bash
for v in *.mp4; do
  mlx_whisper --model mlx-community/whisper-large-v3-mlx \
    --language ru --output-format json --word-timestamps True \
    --output-dir transcripts/ "$v"
  python3 burn-subs.py "$v" "transcripts/${v%.mp4}.json" "out/${v%.mp4}_subs.mp4"
done
```

## Defaults by aspect ratio

| Frame | Font size | Plate y | Plate h | Max chars |
|---|---|---|---|---|
| 1080×1920 (9:16) | 60 | 140 | 140 | 32 |
| 1920×1080 (16:9) | 56 | 880 | 130 | 50 |
| 1080×1080 (1:1) | 56 | 80 | 130 | 36 |

## Best practices

1. **Always use word-level timestamps** — without them, subtitles drift 0.5-1s from speech
2. **Smart chunks beat fixed N-words** — break on punctuation + pauses, not arbitrary word count
3. **Filter hallucinations** — Whisper invents "Subtitles by X" / "thank you for watching" in silence
4. **Glossary helps** — pass `--initial-prompt` with brand names, jargon, person names
5. **Plate height ≤ 15% of video height** — keeps the subject visible
6. **Font size = 5-7% of video width** — readable on phones without dominating
7. **Outline at least 2px** — works on any background even without plate
8. **Test on 1 video first** — adjust position/size before batch processing all files

## Output format options

Beyond burn-in MP4:

| Format | When | How |
|---|---|---|
| Burned-in MP4 (default) | Final upload, CapCut won't strip style | as above |
| Standalone .srt next to video | User wants to edit in CapCut/Premiere | mlx_whisper `--output-format srt` |
| ProRes 4444 .mov overlay with alpha | Drag-n-drop over edited video | render frames separately, encode with `-c:v prores_ks -profile:v 4444 -pix_fmt yuva444p10le` |
| ASS file separately | Advanced editing in Aegisub | save .ass without burning |

## Pre-delivery checklist

- [ ] Font loaded correctly (not falling back to Arial)
- [ ] No hallucinations in output (search for trained phrases)
- [ ] Subtitles sync with voice (check first/last word in a few cues)
- [ ] Text doesn't overflow video width
- [ ] Plate position doesn't cover key visual elements
- [ ] Audio preserved (`-c:a copy` keeps original quality)

Files in this skill

  • LICENSE1 KB
  • README.md65 B
  • SKILL.md9.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.