Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 50 additions & 50 deletions bun.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
"validate-skills": "bash -c 'for dir in skills/*/; do uvx --from skills-ref agentskills validate $dir; done'"
},
"devDependencies": {
"oxfmt": "^0.59.0",
"oxlint": "^1.74.0",
"oxlint-tsgolint": "^0.25.0",
"oxfmt": "^0.61.0",
"oxlint": "^1.76.0",
"oxlint-tsgolint": "^7.0.2001",
"typescript": "^7.0.2"
}
}
24 changes: 13 additions & 11 deletions skills/multilingual-caption-video/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ license: "GPL-3.0-or-Later"
compatibility: Requires uv and FFmpeg with libass and libx264 support.
metadata:
author: o-az
version: "1.1.1"
version: "1.2.0"
---

# multilingual-caption-video
Expand All @@ -27,7 +27,7 @@ Every bundled Python entrypoint has PEP 723 inline script metadata, requires Pyt
- `scripts/transcribe.py` detects the spoken language, transcribes the video, and emits timestamped JSON.
- `scripts/style_captions.py` samples the future subtitle band and selects a readable, stable style for each cue.
- `scripts/make_ass.py` converts translated caption JSON into styled ASS subtitles.
- `scripts/deliver.py` copies a verified MP4 to the operating system's Downloads directory using a safe, collision-free filename.
- `scripts/deliver.py` copies a verified MP4 to the operating system's Downloads directory using a short, collision-free language-code/timestamp filename.
- `scripts/preferences.py` reads and, only with explicit user consent, saves sparse JSON preferences.
- `scripts/cleanup.py` creates marked work directories and safely schedules or cancels their deletion.

Expand Down Expand Up @@ -116,14 +116,16 @@ For a local file, resolve its absolute path and verify it is a regular video fil
SOURCE="$(uv run --script "$SKILL_ROOT/scripts/download.py" "<VIDEO_URL>" "$WORK_DIR")"
```

For URLs whose path ends in `.mp4` or `.mov`, the downloader streams the response directly and validates each redirect host. For other URLs, it validates the initial page host and delegates to yt-dlp with playlists and live streams disabled. Platform extraction is best-effort and may fail when a site is unsupported, requires authentication, or changes its interface. Both paths write only inside the marked work directory and report the final local path. Platform downloads retain yt-dlp's title and media ID so file delivery can derive a meaningful original stem. Probe the source:
For URLs whose path ends in `.mp4` or `.mov`, the downloader streams the response directly and validates each redirect host. For other URLs, it validates the initial page host and delegates to yt-dlp with playlists and live streams disabled. Platform extraction is best-effort and may fail when a site is unsupported, requires authentication, or changes its interface. Both paths write only inside the marked work directory and report the final local path. Platform downloads retain yt-dlp's title and media ID. Probe the source, including any existing title metadata:

```bash
ffprobe -v error -show_entries format=duration:stream=codec_type,codec_name,width,height:stream_side_data=rotation -of json "<SOURCE>"
ffprobe -v error -show_entries format=duration:format_tags=title:stream=codec_type,codec_name,width,height:stream_side_data=rotation -of json "<SOURCE>"
```

Stop with a clear error when the source has no video stream or no audio stream.

Resolve `ORIGINAL_TITLE` for the output metadata. Prefer a non-empty `format.tags.title` from the probe. Otherwise use the local source filename stem, the decoded direct-URL filename stem, or the yt-dlp title in the downloaded filename with its final ` [<media-id>]` removed. Preserve spaces, emoji, and other Unicode in this metadata value; do not include it in the delivered filename.

### 4. Detect the source language and transcribe

```bash
Expand Down Expand Up @@ -180,19 +182,19 @@ The default ASS region uses 12 pixels of padding on each horizontal edge, so wra

### 7. Burn captions into a new MP4

Run `ffmpeg` from the work directory so the subtitle filter receives a simple path:
Run `ffmpeg` from the work directory so the subtitle filter receives a simple path. Add the original video title to the MP4 `title` metadata field:

```bash
cd "$WORK_DIR"
ffmpeg -y -i "<SOURCE>" -vf "ass=captions.ass" -c:v libx264 -crf 18 -preset medium -c:a aac -b:a 192k -movflags +faststart captioned.mp4
ffmpeg -y -i "<SOURCE>" -vf "ass=captions.ass" -c:v libx264 -crf 18 -preset medium -c:a aac -b:a 192k -metadata "title=$ORIGINAL_TITLE" -movflags +faststart captioned.mp4
```

### 8. Verify visually and structurally

Probe the result and compare its duration and displayed dimensions with the source:

```bash
ffprobe -v error -show_entries format=duration:stream=codec_type,codec_name,width,height:stream_side_data=rotation -of json "$WORK_DIR/captioned.mp4"
ffprobe -v error -show_entries format=duration:format_tags=title:stream=codec_type,codec_name,width,height:stream_side_data=rotation -of json "$WORK_DIR/captioned.mp4"
```

Account for display rotation when comparing dimensions. FFmpeg normally autorotates and normalizes a rotated input while rendering, so a source stored as `1920x1080` with a 90-degree display matrix correctly becomes a `1080x1920` output without that matrix. Treat those as matching displayed dimensions.
Expand All @@ -205,17 +207,17 @@ ffmpeg -y -ss <SPEECH_TIMESTAMP> -i "$WORK_DIR/captioned.mp4" -frames:v 1 -vf "s

Before sending each preview to an image or vision tool, check its byte size against that tool's upload limit. If the limit is unknown, keep the preview below 1 MiB. If it is too large, reduce the dimensions or JPEG quality, then check again; never invoke the inspection tool with a known-oversized image.

Confirm that captions are present, correctly shaped, legible, inside the safe area, and no more than two lines; adaptive text or background colors remain readable without flickering within a cue; video and audio both play; duration and displayed dimensions match the source; and the source remains unchanged. Fix the caption data or style and render again when verification fails.
Confirm that captions are present, correctly shaped, legible, inside the safe area, and no more than two lines; adaptive text or background colors remain readable without flickering within a cue; video and audio both play; duration and displayed dimensions match the source; the output `title` metadata equals `ORIGINAL_TITLE`; and the source remains unchanged. Fix the caption data or style and render again when verification fails.

### 9. Deliver the file

Copy the verified MP4 to the operating system's Downloads directory, then deliver that permanent file without uploading it. Pass the local source's original path or the downloaded source's yt-dlp-derived path as `<ORIGINAL_NAME>`, and use the target language's lowercase ISO or BCP 47 code for `<LANGUAGE_CODE>`:
Copy the verified MP4 to the operating system's Downloads directory, then deliver that permanent file without uploading it. Use the target language's lowercase ISO or BCP 47 code for `<LANGUAGE_CODE>`:

```bash
DELIVERED="$(uv run --script "$SKILL_ROOT/scripts/deliver.py" "$WORK_DIR/captioned.mp4" "<ORIGINAL_NAME>" "<LANGUAGE_CODE>")"
DELIVERED="$(uv run --script "$SKILL_ROOT/scripts/deliver.py" "$WORK_DIR/captioned.mp4" "<LANGUAGE_CODE>")"
```

The resulting name is `YYYYMMDD-<original-stem>-<language-code>-subtitles.mp4`. The script sanitizes the original stem and uses `-2`, `-3`, and so on when a name already exists; it never overwrites another file. It uses the Windows Downloads known folder with the Desktop as its Windows fallback, the configured XDG Downloads directory on Linux when available, and `~/Downloads` otherwise. If that directory cannot be created or written, report the issue and fall back to the platform's normal file-delivery or attachment capability from the work directory.
The resulting name is `<language-code>_YYYY-MM-DD_HH.mm.ss.mp4`, using the local delivery time, for example `es_2026-07-31_02.52.58.mp4`. If that exact second already exists, the script advances the filename timestamp one second at a time until an unused name is available; it never overwrites another file. It uses the Windows Downloads known folder with the Desktop as its Windows fallback, the configured XDG Downloads directory on Linux when available, and `~/Downloads` otherwise. If that directory cannot be created or written, report the issue and fall back to the platform's normal file-delivery or attachment capability from the work directory.

Return the permanent path printed by `deliver.py`, not the temporary work-directory MP4. Include the target language, and do not claim completion without a successful probe and visual inspection.

Expand Down
42 changes: 11 additions & 31 deletions skills/multilingual-caption-video/scripts/deliver.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import re
import shutil
import sys
import unicodedata
from pathlib import Path

LANGUAGE_CODE = re.compile(r"[a-z]{2,3}(?:-[a-z0-9]{2,8})*")
Expand Down Expand Up @@ -56,21 +55,6 @@ def default_downloads_directory() -> Path:
return Path.home() / "Downloads"


def sanitize_stem(original_name: str) -> str:
basename = original_name.replace("\\", "/").rsplit("/", 1)[-1]
stem = Path(basename).stem
normalized = unicodedata.normalize("NFKC", stem)
cleaned = "".join(
"-"
if char in '<>:"/\\|?*' or unicodedata.category(char)[0] == "C"
else char
for char in normalized
)
cleaned = re.sub(r"\s+", "-", cleaned)
cleaned = re.sub(r"-+", "-", cleaned).strip(" .-")
return cleaned[:120].rstrip(" .-") or "video"


def normalized_language_code(language: str) -> str:
code = language.strip().replace("_", "-").lower()
if not LANGUAGE_CODE.fullmatch(code):
Expand All @@ -80,11 +64,10 @@ def normalized_language_code(language: str) -> str:

def deliver_video(
video: Path,
original_name: str,
language: str,
*,
destination: Path | None = None,
today: datetime.date | None = None,
timestamp: datetime.datetime | None = None,
) -> Path:
video = video.resolve(strict=True)
if not video.is_file() or video.suffix.lower() != ".mp4":
Expand All @@ -98,18 +81,17 @@ def deliver_video(
if not destination.is_dir():
raise ValueError("Delivery destination must be a directory")

day = (
datetime.datetime.now(tz=datetime.UTC).date()
if today is None
else today
delivered_at = (
datetime.datetime.now().astimezone() if timestamp is None else timestamp
)
base = (
f"{day:%Y%m%d}-{sanitize_stem(original_name)}-"
f"{normalized_language_code(language)}-subtitles"
)
for number in range(1, 10_000):
suffix = "" if number == 1 else f"-{number}"
output = destination / f"{base}{suffix}.mp4"
language_code = normalized_language_code(language)
for second_offset in range(10_000):
candidate_time = delivered_at + datetime.timedelta(
seconds=second_offset
)
output = destination / (
f"{language_code}_{candidate_time:%Y-%m-%d_%H.%M.%S}.mp4"
)
try:
descriptor = os.open(
output, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644
Expand All @@ -134,14 +116,12 @@ def main() -> None:
description="Copy a verified captioned MP4 to the Downloads directory."
)
parser.add_argument("video", type=Path)
parser.add_argument("original_name")
parser.add_argument("language", help="ISO or BCP 47 language code")
parser.add_argument("--destination", type=Path)
args = parser.parse_args()
print(
deliver_video(
args.video,
args.original_name,
args.language,
destination=args.destination,
)
Expand Down
3 changes: 2 additions & 1 deletion tests/multilingual-caption-video/evals.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"Samples the subtitle region at multiple points per cue and selects one stable high-contrast or translucent-background style for that cue",
"Checks preview byte size against the vision tool limit and resizes or recompresses it before inspection when necessary",
"Visually inspects the rendered captions before delivery",
"Stores the original video title in the delivered MP4 title metadata instead of its filename",
"Copies the generated MP4 to the operating system's Downloads directory",
"Reports the actual font used and offers font, size, or subtitle-style changes",
"Announces and schedules cleanup only after successful delivery"
Expand All @@ -32,7 +33,7 @@
"Uses a single low-resolution video pass to assign stable per-cue styles based on the subtitle region's luminance and variation",
"Keeps visual-inspection previews below the image tool's upload-size limit",
"Probes and visually checks the final MP4 before delivery",
"Copies file deliveries to the operating system's Downloads directory with a sanitized YYYYMMDD-original-stem-language-code-subtitles.mp4 name without overwriting an existing file",
"Copies file deliveries to the operating system's Downloads directory with a lowercase language-code_YYYY-MM-DD_HH.mm.ss.mp4 name without overwriting an existing file",
"Reports the selected font and offers font, size, or subtitle-style changes",
"Deletes only the marked generated work directory after the cleanup grace period while retaining the delivered Downloads file"
]
Expand Down
15 changes: 5 additions & 10 deletions tests/multilingual-caption-video/test_caption_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import sys
import tempfile
import time
from datetime import date
from datetime import UTC, datetime
from http.client import HTTPMessage
from pathlib import Path
from types import SimpleNamespace
Expand All @@ -33,7 +33,6 @@
default_downloads_directory,
deliver_video,
normalized_language_code,
sanitize_stem,
)
from download import (
LiveStreamFilter,
Expand Down Expand Up @@ -310,8 +309,6 @@ def fake_popen(*args: object, **kwargs: object) -> FakeFfmpegProcess:
direct_download.assert_not_called()
platform_download.assert_called_once()

assert sanitize_stem("/tmp/My unsafe: video?.mov") == "My-unsafe-video"
assert sanitize_stem(r"C:\Videos\قصيدة جميلة.mp4") == "قصيدة-جميلة"
assert normalized_language_code("EN_us") == "en-us"
try:
normalized_language_code("English")
Expand Down Expand Up @@ -339,21 +336,19 @@ def fake_popen(*args: object, **kwargs: object) -> FakeFfmpegProcess:
downloads = delivery_directory / "Downloads"
delivered = deliver_video(
rendered,
"/tmp/My Interview.mov",
"AR",
destination=downloads,
today=date(2026, 7, 21),
timestamp=datetime(2026, 7, 31, 2, 52, 58, tzinfo=UTC),
)
assert delivered.name == "20260721-My-Interview-ar-subtitles.mp4"
assert delivered.name == "ar_2026-07-31_02.52.58.mp4"
assert delivered.read_bytes() == b"video"
duplicate = deliver_video(
rendered,
"/tmp/My Interview.mov",
"ar",
destination=downloads,
today=date(2026, 7, 21),
timestamp=datetime(2026, 7, 31, 2, 52, 58, tzinfo=UTC),
)
assert duplicate.name == "20260721-My-Interview-ar-subtitles-2.mp4"
assert duplicate.name == "ar_2026-07-31_02.52.59.mp4"
assert delivered.read_bytes() == b"video"

cleanup_script = SCRIPTS / "cleanup.py"
Expand Down