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.
- package/README.md +1 -1
- package/THE-ARKAOS-GUIDE.md +1 -1
- package/VERSION +1 -1
- package/arka/SKILL.md +1 -1
- package/config/claude-agents/brand-director.md +1 -1
- package/config/claude-agents/content-strategist.md +1 -1
- package/config/claude-agents/frontend-dev.md +1 -1
- package/config/claude-agents/marketing-director.md +1 -1
- package/config/claude-agents/scriptwriter.md +1 -1
- package/config/claude-agents/trends-analyst.md +1 -1
- package/config/claude-agents/video-producer.md +1 -1
- package/config/skills-curated.yaml +2 -1
- package/config/skills-provenance.yaml +6 -0
- package/core/keys.py +17 -5
- package/departments/brand/agents/brand-director.yaml +1 -0
- package/departments/content/agents/content-strategist.yaml +1 -0
- package/departments/content/agents/production/trends-analyst.yaml +1 -0
- package/departments/content/agents/production/video-producer.yaml +1 -0
- package/departments/content/agents/scriptwriter.yaml +1 -0
- package/departments/dev/agents/frontend-dev.yaml +1 -0
- package/departments/dev/skills/animated-website/SKILL.md +14 -1
- package/departments/dev/skills/watch/SKILL.md +162 -0
- package/departments/dev/skills/watch/references/claude-video.LICENSE +21 -0
- package/departments/dev/skills/watch/scripts/config.py +145 -0
- package/departments/dev/skills/watch/scripts/download.py +179 -0
- package/departments/dev/skills/watch/scripts/frames.py +762 -0
- package/departments/dev/skills/watch/scripts/setup.py +373 -0
- package/departments/dev/skills/watch/scripts/transcribe.py +95 -0
- package/departments/dev/skills/watch/scripts/watch.py +531 -0
- package/departments/dev/skills/watch/scripts/whisper.py +466 -0
- package/departments/marketing/agents/marketing-director.yaml +1 -0
- package/departments/marketing/skills/ad-creative/SKILL.md +5 -0
- package/harness/codex/AGENTS.md +1 -1
- package/harness/copilot/copilot-instructions.md +1 -1
- package/harness/cursor/rules/arkaos.mdc +2 -2
- package/harness/gemini/GEMINI.md +1 -1
- package/harness/opencode/AGENTS.md +1 -1
- package/harness/zed/.rules +1 -1
- package/installer/doctor.js +38 -4
- package/knowledge/agents-registry-v2.json +8 -1
- package/knowledge/skills-manifest.json +14 -1
- package/package.json +1 -1
- 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))
|