arkaos 4.31.0 → 4.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/THE-ARKAOS-GUIDE.md +1 -1
  3. package/VERSION +1 -1
  4. package/arka/SKILL.md +1 -1
  5. package/config/claude-agents/brand-director.md +1 -1
  6. package/config/claude-agents/content-strategist.md +1 -1
  7. package/config/claude-agents/frontend-dev.md +1 -1
  8. package/config/claude-agents/marketing-director.md +1 -1
  9. package/config/claude-agents/scriptwriter.md +1 -1
  10. package/config/claude-agents/trends-analyst.md +1 -1
  11. package/config/claude-agents/video-producer.md +1 -1
  12. package/config/skills-curated.yaml +2 -1
  13. package/config/skills-provenance.yaml +6 -0
  14. package/core/keys.py +17 -5
  15. package/departments/brand/agents/brand-director.yaml +1 -0
  16. package/departments/content/agents/content-strategist.yaml +1 -0
  17. package/departments/content/agents/production/trends-analyst.yaml +1 -0
  18. package/departments/content/agents/production/video-producer.yaml +1 -0
  19. package/departments/content/agents/scriptwriter.yaml +1 -0
  20. package/departments/dev/agents/frontend-dev.yaml +1 -0
  21. package/departments/dev/skills/animated-website/SKILL.md +14 -1
  22. package/departments/dev/skills/watch/SKILL.md +162 -0
  23. package/departments/dev/skills/watch/references/claude-video.LICENSE +21 -0
  24. package/departments/dev/skills/watch/scripts/config.py +145 -0
  25. package/departments/dev/skills/watch/scripts/download.py +179 -0
  26. package/departments/dev/skills/watch/scripts/frames.py +762 -0
  27. package/departments/dev/skills/watch/scripts/setup.py +373 -0
  28. package/departments/dev/skills/watch/scripts/transcribe.py +95 -0
  29. package/departments/dev/skills/watch/scripts/watch.py +531 -0
  30. package/departments/dev/skills/watch/scripts/whisper.py +466 -0
  31. package/departments/marketing/agents/marketing-director.yaml +1 -0
  32. package/departments/marketing/skills/ad-creative/SKILL.md +5 -0
  33. package/harness/codex/AGENTS.md +1 -1
  34. package/harness/copilot/copilot-instructions.md +1 -1
  35. package/harness/cursor/rules/arkaos.mdc +2 -2
  36. package/harness/gemini/GEMINI.md +1 -1
  37. package/harness/opencode/AGENTS.md +1 -1
  38. package/harness/zed/.rules +1 -1
  39. package/installer/doctor.js +38 -4
  40. package/knowledge/agents-registry-v2.json +8 -1
  41. package/knowledge/skills-manifest.json +14 -1
  42. package/package.json +1 -1
  43. package/pyproject.toml +1 -1
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env python3
2
+ """Download a video via yt-dlp, or resolve a local file path.
3
+
4
+ Also fetches subtitles (manual first, then auto-generated) in VTT format so
5
+ transcribe.py can parse them without needing Whisper.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ from pathlib import Path
14
+ from urllib.parse import urlparse
15
+
16
+ VIDEO_EXTS = {".mp4", ".mkv", ".webm", ".mov", ".m4v", ".avi", ".flv", ".wmv"}
17
+
18
+
19
+ def is_url(source: str) -> bool:
20
+ if source.startswith("-"):
21
+ return False
22
+ parsed = urlparse(source)
23
+ return parsed.scheme in ("http", "https") and bool(parsed.netloc)
24
+
25
+
26
+ def resolve_local(path: str) -> dict:
27
+ p = Path(path).expanduser().resolve()
28
+ if not p.exists():
29
+ raise SystemExit(f"File not found: {p}")
30
+ if p.suffix.lower() not in VIDEO_EXTS:
31
+ print(
32
+ f"[watch] warning: {p.suffix} is not a known video extension, proceeding anyway",
33
+ file=sys.stderr,
34
+ )
35
+ return {
36
+ "video_path": str(p),
37
+ "subtitle_path": None,
38
+ "info": {"title": p.name, "url": str(p)},
39
+ "downloaded": False,
40
+ }
41
+
42
+
43
+ def _pick_subtitle(out_dir: Path) -> Path | None:
44
+ candidates = sorted(out_dir.glob("video*.vtt"))
45
+ if not candidates:
46
+ return None
47
+ preferred = [
48
+ c for c in candidates
49
+ if any(marker in c.name for marker in (".en.", ".en-US.", ".en-GB.", ".en-orig."))
50
+ ]
51
+ return preferred[0] if preferred else candidates[0]
52
+
53
+
54
+ def _pick_video(out_dir: Path) -> Path | None:
55
+ for ext in (".mp4", ".mkv", ".webm", ".mov", ".m4a", ".mp3", ".opus"):
56
+ for candidate in out_dir.glob(f"video*{ext}"):
57
+ return candidate
58
+ for candidate in out_dir.glob("video.*"):
59
+ if candidate.suffix.lower() in VIDEO_EXTS:
60
+ return candidate
61
+ return None
62
+
63
+
64
+ def fetch_captions(url: str, out_dir: Path) -> dict:
65
+ """Fetch metadata and best available VTT captions without downloading video."""
66
+ if shutil.which("yt-dlp") is None:
67
+ raise SystemExit("yt-dlp is not installed. Install with: brew install yt-dlp")
68
+
69
+ out_dir.mkdir(parents=True, exist_ok=True)
70
+ output_template = str(out_dir / "video.%(ext)s")
71
+ cmd = [
72
+ "yt-dlp",
73
+ "--skip-download",
74
+ "--write-info-json",
75
+ "--write-subs",
76
+ "--write-auto-subs",
77
+ "--sub-langs", "en.*",
78
+ "--sub-format", "vtt",
79
+ "--convert-subs", "vtt",
80
+ "--no-playlist",
81
+ "--ignore-errors",
82
+ "-o", output_template,
83
+ "--",
84
+ url,
85
+ ]
86
+ subprocess.run(cmd, stdout=sys.stderr, stderr=sys.stderr)
87
+ subtitle = _pick_subtitle(out_dir)
88
+ info = _read_info(out_dir / "video.info.json", url)
89
+ return {
90
+ "video_path": None,
91
+ "subtitle_path": str(subtitle) if subtitle else None,
92
+ "info": info or {"url": url},
93
+ "downloaded": False,
94
+ }
95
+
96
+
97
+ def _read_info(info_path: Path, url: str) -> dict:
98
+ info: dict = {}
99
+ if info_path.exists():
100
+ try:
101
+ raw = json.loads(info_path.read_text(encoding="utf-8"))
102
+ info = {
103
+ "title": raw.get("title"),
104
+ "uploader": raw.get("uploader") or raw.get("channel"),
105
+ "duration": raw.get("duration"),
106
+ "url": raw.get("webpage_url") or url,
107
+ }
108
+ except Exception as exc:
109
+ print(f"[watch] info.json parse failed: {exc}", file=sys.stderr)
110
+ info = {"url": url}
111
+ return info
112
+
113
+
114
+ def download_url(
115
+ url: str,
116
+ out_dir: Path,
117
+ audio_only: bool = False,
118
+ ) -> dict:
119
+ if shutil.which("yt-dlp") is None:
120
+ raise SystemExit("yt-dlp is not installed. Install with: brew install yt-dlp")
121
+
122
+ out_dir.mkdir(parents=True, exist_ok=True)
123
+ output_template = str(out_dir / "video.%(ext)s")
124
+
125
+ fmt = "ba/bestaudio" if audio_only else "bv*[height<=720]+ba/b[height<=720]/bv+ba/b"
126
+ cmd = [
127
+ "yt-dlp",
128
+ "-N", "8",
129
+ "-f", fmt,
130
+ "--merge-output-format", "mp4",
131
+ "--write-info-json",
132
+ "--write-subs",
133
+ "--write-auto-subs",
134
+ "--sub-langs", "en.*",
135
+ "--sub-format", "vtt",
136
+ "--convert-subs", "vtt",
137
+ "--no-playlist",
138
+ "--ignore-errors",
139
+ "-o", output_template,
140
+ "--",
141
+ url,
142
+ ]
143
+
144
+ # yt-dlp may exit non-zero if a subtitle variant fails (e.g. 429) even when
145
+ # the video itself downloaded fine. Treat "video file present" as success.
146
+ result = subprocess.run(cmd, stdout=sys.stderr, stderr=sys.stderr)
147
+ video = _pick_video(out_dir)
148
+ if video is None:
149
+ raise SystemExit(
150
+ f"yt-dlp did not produce a video file in {out_dir} (exit {result.returncode})"
151
+ )
152
+
153
+ subtitle = _pick_subtitle(out_dir)
154
+ info = _read_info(out_dir / "video.info.json", url)
155
+
156
+ return {
157
+ "video_path": str(video),
158
+ "subtitle_path": str(subtitle) if subtitle else None,
159
+ "info": info or {"url": url},
160
+ "downloaded": True,
161
+ }
162
+
163
+
164
+ def download(
165
+ source: str,
166
+ out_dir: Path,
167
+ audio_only: bool = False,
168
+ ) -> dict:
169
+ if is_url(source):
170
+ return download_url(source, out_dir, audio_only=audio_only)
171
+ return resolve_local(source)
172
+
173
+
174
+ if __name__ == "__main__":
175
+ if len(sys.argv) < 3:
176
+ print("usage: download.py <url-or-path> <out-dir>", file=sys.stderr)
177
+ raise SystemExit(2)
178
+ result = download(sys.argv[1], Path(sys.argv[2]))
179
+ print(json.dumps(result, indent=2))