Skip to content
Back to skills

Transcript To Srt Automator

ASecurity

Turn a timestamped transcript (JSON) into ready-to-burn SRT subtitles, chapter markers, and a keyword-driven highlight-clip plan with in/out offsets - all in the Python standard library. No ffmpeg required to generate the assets; optional ffmpeg command lines are emitted for the actual clipping step. Zero MCP.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 27, 2026
developmentpythonshell

Works with

  • cli
  • mcp

Security analysis

A100/100

Scanned September 27, 2026

npx -y skills add RMAzrim/opencode-artes --skill transcript-to-srt-automator --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Transcript To Srt Automator?

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

Security grade badge for Transcript To Srt Automator
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/rmazrim-transcript-to-srt-automator/badge)](https://www.skillsdirectory.com/skills/rmazrim-transcript-to-srt-automator)

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: Transcript to SRT Automator
description: Turn a timestamped transcript (JSON) into ready-to-burn SRT subtitles, chapter markers, and a keyword-driven highlight-clip plan with in/out offsets - all in the Python standard library. No ffmpeg required to generate the assets; optional ffmpeg command lines are emitted for the actual clipping step. Zero MCP.
metadata:
  source: skills/transcript-to-srt-automator/transcript-to-srt-automator.md
---

# Transcript to SRT Automator

Convert a machine transcript with timestamps into production-ready subtitle and clip-planning assets using only the standard library: an SRT file with correct cue timing, chapter markers derived from slide/topic boundaries, and a highlight-clip plan (start/end + label) from keyword + silence-heuristic scoring. ffmpeg is *not* needed to generate the assets — the module also emits copy-paste `ffmpeg` commands for the clipping step.

## 1. System Architecture & Prerequisites

- **Runtime**: CPython 3.9+ (`json`, `re`, `math`, `datetime`, `argparse`, `collections`). No whisper/srt libs, no ffmpeg binding, no MCP.
- **Input transcript schema**:
  ```json
  {
    "id": "pod-014",
    "duration_s": 3720,
    "segments": [
      {"start": 0.0, "end": 8.4, "speaker": "P1", "text": "Welcome to the show..."},
      {"start": 8.4, "end": 22.1, "speaker": "P2", "text": "Today we talk about ETL pipelines."}
    ]
  }
  ```
- **Outputs**:
  - `out.srt` — SRT subtitle file (one cue per segment, `HH:MM:SS,mmm`).
  - `chapters.json` — boundaries where speaker changes or a topic keyword appears.
  - `clips.json` — `[{label, start, end, reason, confidence}]` computed via keyword hits + gap heuristics.
  - `commands.sh` (optional) — ffmpeg cut commands reflecting clips.json.
- **Heuristics**: topic keyword list per clip desired → clip confidence = (keyword strength + gap margin). Segments must be monotonic; overlapping segments are clamped.

## 2. Input/Output Data Contracts

```
python srt_automator.py transcript.json --out subs.srt --chapters chapters.json \
    --clips clips.json --ffmpeg out.sh -k "ETL, pipeline" -c "keynote"
```

- `-k/--keywords` comma-separated; `-c/--chapter` per-topic; `--min-dur-s` clip floor (default 8 s); clock format `HH:MM:SS,mmm`.
- Exit `0` OK, `1` schema/segment validation, `2` input file missing/invalid JSON.

## 3. Production Reference Implementation

```python
#!/usr/bin/env python3
"""srt_automator.py - transcript to SRT + chapters + clip plan (pure stdlib)."""
import argparse
import json
import math
import re
import sys
from datetime import timedelta
from pathlib import Path


def srt_ts(seconds: float) -> str:
    if seconds < 0:
        seconds = 0.0
    ms = int(round((seconds - int(seconds)) * 1000))
    delta = timedelta(seconds=int(seconds))
    return f"{delta.seconds // 3600:02d}:{(delta.seconds % 3600) // 60:02d}:{delta.seconds % 60:02d},{ms:03d}"


def build_srt(segments: list[dict]) -> str:
    blocks = []
    for i, seg in enumerate(segments, 1):
        start = srt_ts(float(seg["start"]))
        end = srt_ts(float(seg["end"]))
        text = (seg.get("speaker", "?").strip() + ": " if seg.get("speaker") else "") + seg["text"].strip()
        blocks.append(f"{i}\n{start} --> {end}\n{text}\n")
    return "\n".join(blocks)


TOPIC_GAP_S = 30.0


def silence_gaps(segments: list[dict], min_gap: float = 1.5) -> list[float]:
    return [seg["start"] - prev["end"]
            for prev, seg in zip(segments, segments[1:])
            if seg["start"] - prev["end"] >= min_gap]


def chapters(segments: list[dict], chapter_words: list[str], min_dur: float = 60.0) -> dict:
    boundaries = []
    last = segments[0]["start"]
    for i, seg in enumerate(segments):
        topic_hit = any(w.lower() in seg["text"].lower() for w in chapter_words)
        speaker_change = i > 0 and seg["speaker"] != segments[i - 1]["speaker"]
        if (topic_hit or speaker_change) and seg["start"] - last >= min_dur:
            boundaries.append({"at": seg["start"], "label": f"{seg['speaker']} @ {seg['text'][:30]}"})
            last = seg["start"]
    return {"chapter_words": chapter_words, "count": len(boundaries), "boundaries": boundaries}


def clips(segments: list[dict], keywords: list[str], min_dur: float = 8.0) -> list[dict]:
    kws = [k.lower() for k in keywords]
    found = []
    for i, seg in enumerate(segments):
        text_l = seg["text"].lower()
        hits = [k for k in kws if k in text_l]
        if not hits:
            continue
        start = max(0.0, seg["start"] - 2.0)
        end = seg["end"] + min(4.0, 2.0)
        gap_bonus = 0.15 if any(g >= 1.5 for g in
                               [seg["start"] - segments[i - 1]["end"] if i > 0 else 0]) else 0.0
        confidence = round(min(0.99, 0.35 + 0.22 * len(hits) + gap_bonus), 2)
        if end - start < min_dur:
            end = start + min_dur
        found.append({"label": "|".join(hits), "start": round(start, 2),
                      "end": round(end, 2), "confidence": confidence,
                      "reason": f"{len(hits)} keyword hit(s)"})
    return found


def ffmpeg_commands(clip_list: list[dict], input_path: str = "input.mp4") -> list[str]:
    cmds = []
    for i, c in enumerate(clip_list, 1):
        cmds.append(
            f'ffmpeg -y -i "{input_path}" -ss {c["start"]} -to {c["end"]} '
            f'-c copy "clip_{i}_{c["label"].replace("|", "_")}.mp4"'
        )
    return cmds


def validate(segments: list[dict]) -> list[str]:
    errs = []
    if not segments:
        return ["no segments"]
    for i, seg in enumerate(segments):
        for key in ("start", "end", "text"):
            if key not in seg:
                errs.append(f"segment {i}: missing '{key}'")
        if i and seg["start"] < segments[i - 1]["end"]:
            errs.append(f"segment {i}: start < previous end (overlap)")
        if seg["end"] <= seg["start"]:
            errs.append(f"segment {i}: end <= start")
    return errs


def main(argv=None) -> int:
    ap = argparse.ArgumentParser(prog="srt_automator")
    ap.add_argument("transcript", type=Path)
    ap.add_argument("--out", default="subs.srt")
    ap.add_argument("--chapters", default=None)
    ap.add_argument("--clips", default=None)
    ap.add_argument("--ffmpeg", default=None)
    ap.add_argument("--input", default="input.mp4", help="media path for ffmpeg commands")
    ap.add_argument("-k", "--keywords", default="")
    ap.add_argument("-c", "--chapter", default="")
    ap.add_argument("--min-dur-s", type=float, default=8.0)
    args = ap.parse_args(argv)

    if not args.transcript.exists():
        print(f"[error] transcript not found: {args.transcript}", file=sys.stderr)
        return 2
    try:
        data = json.loads(args.transcript.read_text(encoding="utf-8"))
    except json.JSONDecodeError as exc:
        print(f"[error] invalid JSON: {exc}", file=sys.stderr)
        return 2
    segments = data.get("segments", [])
    errs = validate(segments)
    if errs:
        for e in errs:
            print(f"  - {e}")
        return 1

    Path(args.out).write_text(build_srt(segments), encoding="utf-8")
    print(f"srt written -> {args.out} ({len(segments)} cues)")
    if args.chapters:
        ch = chapters(segments, [w.strip() for w in args.chapter.split(",") if w.strip()])
        Path(args.chapters).write_text(json.dumps(ch, indent=2), encoding="utf-8")
        print(f"chapters written -> {args.chapters} ({int(ch['count'])} boundaries)")
    if args.clips:
        cl = clips(segments, [w.strip() for w in args.keywords.split(",") if w.strip()],
                   args.min_dur_s)
        Path(args.clips).write_text(json.dumps(cl, indent=2), encoding="utf-8")
        print(f"clips written -> {args.clips} ({len(cl)} highlights)")
        if args.ffmpeg:
            cmds = ffmpeg_commands(cl[:10], args.input)
            Path(args.ffmpeg).write_text("\n".join(cmds) + "\n", encoding="utf-8")
            print(f"ffmpeg commands -> {args.ffmpeg}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
```

## 4. Execution Protocol & Step-by-Step Workflow

1. **Prepare transcript**: export your STT/whisper transcript as `transcript.json` with the schema above.
2. **Generate SRT**: `python srt_automator.py transcript.json --out subs.srt` → playable subtitles, cue-indexed with `HH:MM:SS,mmm` bounds.
3. **Chapter markers**: `python srt_automator.py transcript.json --chapters ch.json -c "intro, recap, outro"` → speaker-change + topic-word boundaries (min 60 s spacing).
4. **Highlight clips**: `python srt_automator.py transcript.json --clips clips.json -k "ETL, pipeline, error" --ffmpeg cut.sh --input episode.mp4` → ranked highlight plan with confidence + ready-to-run ffmpeg commands.
5. **Inspect**: `clips.json` explains *why* each clip was chosen (`reason`, `confidence`); tune `--min-dur-s` to tighten or loosen segments.
6. **Burn & clip**: run `cut.sh` (requires ffmpeg on the host — the only external step, and it is optional) or pass the SRT to a video editor.

## 5. Edge Cases & Error Handling

- **Overlapping / out-of-order segments** → validation exits `1` listing each offending index; fix the transcript before SRT generation.
- **Missing `start`/`end`/`text`** → validation flags the exact segment; SRT never emits half-cues.
- **Zero keywords** → `clips` returns `[]` with a printed count `0` — never crashes.
- **Negative/zero-duration cues** → clamped to `>= 0` and `end > start` (min-dur floor applied).
- **Long clips** → hard cap of 10 ffmpeg commands to keep shell output readable; the full `clips.json` stays authoritative.
- **Non-UTF8 files** → `read_text` raises; wrap your producer (whisper) to output UTF-8 — the error surfaces with a clear traceback point.

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.