429f3143e5
Move the skill into skills/watch/ so SKILL.md and its scripts/ runtime are
siblings inside one folder. `npx skills add` (Codex/Cursor/Copilot/agents) now
copies a working skill as a unit; previously it grabbed the root SKILL.md alone
and left scripts/ behind, so the skill was dead on arrival on every non-Claude
host. Mirrors the layout last30days-skill adopted for the same reason.
- skills/watch/{SKILL.md,scripts/}: self-contained skill folder
- SKILL.md: resolve a harness-agnostic $SKILL_DIR (the dir it was Read from)
instead of the Claude-Code-only ${CLAUDE_SKILL_DIR}; guard + 19 call sites
- drop commands/watch.md: /watch derives from frontmatter (name + user-invocable)
- .codex-plugin/plugin.json: full manifest with "skills": "./skills/" + interface
- add .agents/plugins/marketplace.json, AGENTS.md, CLAUDE.md, .skillignore
- build-skill.sh: archive the skills/watch subtree (one SKILL.md, no zip -d)
- fix paths in tests, hooks hint, .gitattributes, release.yml
- relocate dev-sync.sh to repo root and fix REPO_ROOT
- README: content-ideas structure, npx skills install, star history
Verified: 37/37 tests pass; npx skills add bundles the full scripts/ runtime;
manifests valid; versions synced at 0.1.3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
84 lines
2.7 KiB
Python
84 lines
2.7 KiB
Python
"""Shared pytest fixtures: ffmpeg-synthesized clips and scripts/ on sys.path."""
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
# Make the bundled scripts importable (mirrors watch.py's sys.path insert).
|
|
SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "skills" / "watch" / "scripts"
|
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
|
|
# 14 visually distinct fills → 14 abrupt cuts → x264 emits a keyframe per cut.
|
|
COLORS = [
|
|
"red", "green", "blue", "white", "black", "yellow", "cyan",
|
|
"magenta", "gray", "orange", "purple", "brown", "navy", "olive",
|
|
]
|
|
|
|
|
|
def _run(cmd: list[str]) -> None:
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"ffmpeg failed: {' '.join(cmd)}\n{result.stderr}")
|
|
|
|
|
|
def build_cut_clip(
|
|
path: Path,
|
|
n: int = 14,
|
|
seg: float = 0.4,
|
|
size: str = "320x240",
|
|
fps: int = 10,
|
|
) -> None:
|
|
"""Concatenate ``n`` solid-color segments into one clip with ``n`` cuts.
|
|
|
|
Each color change is a hard scene cut, so the scene selector finds ~n-1
|
|
changes. x264's own scenecut detection is unreliable on flat fills, so we
|
|
force a keyframe at every ``seg`` boundary — giving ~n real keyframes for
|
|
the keyframe engine to find.
|
|
"""
|
|
inputs: list[str] = []
|
|
for i in range(n):
|
|
color = COLORS[i % len(COLORS)]
|
|
inputs += ["-f", "lavfi", "-t", str(seg), "-i", f"color=c={color}:s={size}:r={fps}"]
|
|
streams = "".join(f"[{i}:v]" for i in range(n))
|
|
filt = f"{streams}concat=n={n}:v=1:a=0[out]"
|
|
_run([
|
|
"ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
|
|
*inputs,
|
|
"-filter_complex", filt, "-map", "[out]",
|
|
"-c:v", "libx264", "-pix_fmt", "yuv420p",
|
|
"-force_key_frames", f"expr:gte(t,n_forced*{seg})",
|
|
str(path),
|
|
])
|
|
|
|
|
|
def build_static_clip(
|
|
path: Path,
|
|
duration: float = 3.0,
|
|
size: str = "320x240",
|
|
fps: int = 10,
|
|
) -> None:
|
|
"""One solid color: 1 keyframe, no scene changes → triggers both fallbacks."""
|
|
_run([
|
|
"ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
|
|
"-f", "lavfi", "-t", str(duration), "-i", f"color=c=blue:s={size}:r={fps}",
|
|
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-g", "600",
|
|
str(path),
|
|
])
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def cut_clip(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
|
path = tmp_path_factory.mktemp("clips") / "cuts.mp4"
|
|
build_cut_clip(path)
|
|
return path
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def static_clip(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
|
path = tmp_path_factory.mktemp("clips") / "static.mp4"
|
|
build_static_clip(path)
|
|
return path
|