videonut 1.2.7 → 1.3.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 +272 -272
- package/USER_GUIDE.md +90 -90
- package/agents/core/eic.md +771 -771
- package/agents/creative/director.md +246 -246
- package/agents/creative/scriptwriter.md +207 -207
- package/agents/research/investigator.md +394 -394
- package/agents/technical/archivist.md +288 -288
- package/agents/technical/scavenger.md +247 -247
- package/bin/videonut.js +37 -21
- package/config.yaml +61 -61
- package/docs/scriptwriter.md +42 -42
- package/file_validator.py +186 -186
- package/memory/short_term/asset_manifest.md +64 -64
- package/memory/short_term/investigation_dossier.md +31 -31
- package/memory/short_term/master_script.md +51 -51
- package/package.json +61 -64
- package/requirements.txt +8 -8
- package/setup.js +33 -15
- package/tools/check_env.py +76 -76
- package/tools/downloaders/caption_reader.py +237 -237
- package/tools/downloaders/clip_grabber.py +82 -82
- package/tools/downloaders/image_grabber.py +105 -105
- package/tools/downloaders/pdf_reader.py +163 -163
- package/tools/downloaders/screenshotter.py +58 -58
- package/tools/downloaders/web_reader.py +69 -69
- package/tools/validators/link_checker.py +45 -45
- package/workflow_orchestrator.py +336 -336
- package/.claude/commands/archivist.toml +0 -12
- package/.claude/commands/director.toml +0 -12
- package/.claude/commands/eic.toml +0 -12
- package/.claude/commands/investigator.toml +0 -12
- package/.claude/commands/prompt.toml +0 -12
- package/.claude/commands/scavenger.toml +0 -12
- package/.claude/commands/scout.toml +0 -12
- package/.claude/commands/scriptwriter.toml +0 -12
- package/.claude/commands/seo.toml +0 -12
- package/.claude/commands/thumbnail.toml +0 -12
- package/.claude/commands/topic_scout.toml +0 -12
- package/.gemini/commands/archivist.toml +0 -12
- package/.gemini/commands/director.toml +0 -12
- package/.gemini/commands/eic.toml +0 -12
- package/.gemini/commands/investigator.toml +0 -12
- package/.gemini/commands/prompt.toml +0 -12
- package/.gemini/commands/scavenger.toml +0 -12
- package/.gemini/commands/scout.toml +0 -12
- package/.gemini/commands/scriptwriter.toml +0 -12
- package/.gemini/commands/seo.toml +0 -12
- package/.gemini/commands/thumbnail.toml +0 -12
- package/.gemini/commands/topic_scout.toml +0 -12
- package/.qwen/commands/archivist.toml +0 -12
- package/.qwen/commands/director.toml +0 -12
- package/.qwen/commands/eic.toml +0 -12
- package/.qwen/commands/investigator.toml +0 -12
- package/.qwen/commands/prompt.toml +0 -12
- package/.qwen/commands/scavenger.toml +0 -12
- package/.qwen/commands/scout.toml +0 -12
- package/.qwen/commands/scriptwriter.toml +0 -12
- package/.qwen/commands/seo.toml +0 -12
- package/.qwen/commands/thumbnail.toml +0 -12
- package/.qwen/commands/topic_scout.toml +0 -12
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
|
-
import subprocess
|
|
4
|
-
import argparse
|
|
5
|
-
|
|
6
|
-
def download_clip(url, start_time, end_time, output_path, ffmpeg_path):
|
|
7
|
-
"""
|
|
8
|
-
Downloads a specific clip from a YouTube video using yt-dlp.
|
|
9
|
-
"""
|
|
10
|
-
# Ensure output directory exists if it's not the current directory
|
|
11
|
-
dir_name = os.path.dirname(output_path)
|
|
12
|
-
if dir_name:
|
|
13
|
-
os.makedirs(dir_name, exist_ok=True)
|
|
14
|
-
|
|
15
|
-
# Construct the yt-dlp command
|
|
16
|
-
# --download-sections "*start-end" downloads only that range
|
|
17
|
-
# --force-keyframes-at-cuts ensures precise cutting (requires ffmpeg)
|
|
18
|
-
cmd = [
|
|
19
|
-
"yt-dlp",
|
|
20
|
-
"--verbose",
|
|
21
|
-
"--download-sections", f"*{start_time}-{end_time}",
|
|
22
|
-
"--force-keyframes-at-cuts",
|
|
23
|
-
"--ffmpeg-location", ffmpeg_path,
|
|
24
|
-
"-o", output_path,
|
|
25
|
-
url
|
|
26
|
-
]
|
|
27
|
-
|
|
28
|
-
print(f"Executing: ", ' '.join(cmd))
|
|
29
|
-
|
|
30
|
-
try:
|
|
31
|
-
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
32
|
-
print("Download successful.")
|
|
33
|
-
print(result.stdout)
|
|
34
|
-
|
|
35
|
-
# Validate that the file was created and has content
|
|
36
|
-
if os.path.exists(output_path):
|
|
37
|
-
file_size = os.path.getsize(output_path)
|
|
38
|
-
if file_size == 0:
|
|
39
|
-
print(f"Error: Downloaded file is empty: {output_path}")
|
|
40
|
-
sys.exit(1)
|
|
41
|
-
else:
|
|
42
|
-
print(f"File validation: {output_path} created with size {file_size} bytes")
|
|
43
|
-
else:
|
|
44
|
-
print(f"Error: Downloaded file does not exist: {output_path}")
|
|
45
|
-
sys.exit(1)
|
|
46
|
-
|
|
47
|
-
except subprocess.CalledProcessError as e:
|
|
48
|
-
print("Error during download:")
|
|
49
|
-
print(e.stderr)
|
|
50
|
-
sys.exit(1)
|
|
51
|
-
except FileNotFoundError:
|
|
52
|
-
print("Error: yt-dlp not found. Please install it (pip install yt-dlp) and ensure it's in your PATH.")
|
|
53
|
-
sys.exit(1)
|
|
54
|
-
|
|
55
|
-
if __name__ == "__main__":
|
|
56
|
-
parser = argparse.ArgumentParser(description="Download a video clip.")
|
|
57
|
-
parser.add_argument("--url", required=True, help="Video URL")
|
|
58
|
-
parser.add_argument("--start", required=True, help="Start time (e.g., 00:00:10 or 10)")
|
|
59
|
-
parser.add_argument("--end", required=True, help="End time (e.g., 00:00:20 or 20)")
|
|
60
|
-
parser.add_argument("--output", required=True, help="Output file path")
|
|
61
|
-
|
|
62
|
-
# Try to find ffmpeg in system PATH first
|
|
63
|
-
import shutil
|
|
64
|
-
import platform
|
|
65
|
-
default_ffmpeg = shutil.which("ffmpeg")
|
|
66
|
-
if not default_ffmpeg:
|
|
67
|
-
# Fallback to local bin folder relative to this script
|
|
68
|
-
# Assumes structure: tools/downloaders/clip_grabber.py -> tools/bin/ffmpeg.exe (Windows) or tools/bin/ffmpeg (Unix)
|
|
69
|
-
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
70
|
-
# Determine the appropriate executable name based on the platform
|
|
71
|
-
ffmpeg_exe = "ffmpeg.exe" if platform.system().lower() == "windows" else "ffmpeg"
|
|
72
|
-
default_ffmpeg = os.path.join(base_dir, "bin", ffmpeg_exe)
|
|
73
|
-
|
|
74
|
-
# If the fallback path doesn't exist, warn the user
|
|
75
|
-
if not os.path.exists(default_ffmpeg):
|
|
76
|
-
print(f"Warning: ffmpeg not found in PATH or at expected location: {default_ffmpeg}")
|
|
77
|
-
print("Please install ffmpeg or place it in the tools/bin/ directory.")
|
|
78
|
-
|
|
79
|
-
parser.add_argument("--ffmpeg", default=default_ffmpeg, help="Path to ffmpeg executable")
|
|
80
|
-
|
|
81
|
-
args = parser.parse_args()
|
|
82
|
-
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import subprocess
|
|
4
|
+
import argparse
|
|
5
|
+
|
|
6
|
+
def download_clip(url, start_time, end_time, output_path, ffmpeg_path):
|
|
7
|
+
"""
|
|
8
|
+
Downloads a specific clip from a YouTube video using yt-dlp.
|
|
9
|
+
"""
|
|
10
|
+
# Ensure output directory exists if it's not the current directory
|
|
11
|
+
dir_name = os.path.dirname(output_path)
|
|
12
|
+
if dir_name:
|
|
13
|
+
os.makedirs(dir_name, exist_ok=True)
|
|
14
|
+
|
|
15
|
+
# Construct the yt-dlp command
|
|
16
|
+
# --download-sections "*start-end" downloads only that range
|
|
17
|
+
# --force-keyframes-at-cuts ensures precise cutting (requires ffmpeg)
|
|
18
|
+
cmd = [
|
|
19
|
+
"yt-dlp",
|
|
20
|
+
"--verbose",
|
|
21
|
+
"--download-sections", f"*{start_time}-{end_time}",
|
|
22
|
+
"--force-keyframes-at-cuts",
|
|
23
|
+
"--ffmpeg-location", ffmpeg_path,
|
|
24
|
+
"-o", output_path,
|
|
25
|
+
url
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
print(f"Executing: ", ' '.join(cmd))
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
32
|
+
print("Download successful.")
|
|
33
|
+
print(result.stdout)
|
|
34
|
+
|
|
35
|
+
# Validate that the file was created and has content
|
|
36
|
+
if os.path.exists(output_path):
|
|
37
|
+
file_size = os.path.getsize(output_path)
|
|
38
|
+
if file_size == 0:
|
|
39
|
+
print(f"Error: Downloaded file is empty: {output_path}")
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
else:
|
|
42
|
+
print(f"File validation: {output_path} created with size {file_size} bytes")
|
|
43
|
+
else:
|
|
44
|
+
print(f"Error: Downloaded file does not exist: {output_path}")
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
except subprocess.CalledProcessError as e:
|
|
48
|
+
print("Error during download:")
|
|
49
|
+
print(e.stderr)
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
except FileNotFoundError:
|
|
52
|
+
print("Error: yt-dlp not found. Please install it (pip install yt-dlp) and ensure it's in your PATH.")
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
parser = argparse.ArgumentParser(description="Download a video clip.")
|
|
57
|
+
parser.add_argument("--url", required=True, help="Video URL")
|
|
58
|
+
parser.add_argument("--start", required=True, help="Start time (e.g., 00:00:10 or 10)")
|
|
59
|
+
parser.add_argument("--end", required=True, help="End time (e.g., 00:00:20 or 20)")
|
|
60
|
+
parser.add_argument("--output", required=True, help="Output file path")
|
|
61
|
+
|
|
62
|
+
# Try to find ffmpeg in system PATH first
|
|
63
|
+
import shutil
|
|
64
|
+
import platform
|
|
65
|
+
default_ffmpeg = shutil.which("ffmpeg")
|
|
66
|
+
if not default_ffmpeg:
|
|
67
|
+
# Fallback to local bin folder relative to this script
|
|
68
|
+
# Assumes structure: tools/downloaders/clip_grabber.py -> tools/bin/ffmpeg.exe (Windows) or tools/bin/ffmpeg (Unix)
|
|
69
|
+
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
70
|
+
# Determine the appropriate executable name based on the platform
|
|
71
|
+
ffmpeg_exe = "ffmpeg.exe" if platform.system().lower() == "windows" else "ffmpeg"
|
|
72
|
+
default_ffmpeg = os.path.join(base_dir, "bin", ffmpeg_exe)
|
|
73
|
+
|
|
74
|
+
# If the fallback path doesn't exist, warn the user
|
|
75
|
+
if not os.path.exists(default_ffmpeg):
|
|
76
|
+
print(f"Warning: ffmpeg not found in PATH or at expected location: {default_ffmpeg}")
|
|
77
|
+
print("Please install ffmpeg or place it in the tools/bin/ directory.")
|
|
78
|
+
|
|
79
|
+
parser.add_argument("--ffmpeg", default=default_ffmpeg, help="Path to ffmpeg executable")
|
|
80
|
+
|
|
81
|
+
args = parser.parse_args()
|
|
82
|
+
|
|
83
83
|
download_clip(args.url, args.start, args.end, args.output, args.ffmpeg)
|
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
|
-
import requests
|
|
4
|
-
import argparse
|
|
5
|
-
import mimetypes
|
|
6
|
-
from urllib.parse import urlparse
|
|
7
|
-
|
|
8
|
-
def is_safe_image_type(content_type, url):
|
|
9
|
-
"""
|
|
10
|
-
Check if the content type is a safe image type.
|
|
11
|
-
"""
|
|
12
|
-
safe_types = {
|
|
13
|
-
'image/jpeg', 'image/jpg', 'image/png', 'image/gif',
|
|
14
|
-
'image/webp', 'image/bmp', 'image/svg+xml', 'image/tiff'
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
# Check content-type header
|
|
18
|
-
if content_type and content_type.lower() in safe_types:
|
|
19
|
-
return True
|
|
20
|
-
|
|
21
|
-
# Fallback: check file extension from URL
|
|
22
|
-
parsed_url = urlparse(url)
|
|
23
|
-
file_ext = os.path.splitext(parsed_url.path)[1].lower()
|
|
24
|
-
mime_type, _ = mimetypes.guess_type(f"dummy{file_ext}")
|
|
25
|
-
|
|
26
|
-
if mime_type and mime_type in safe_types:
|
|
27
|
-
return True
|
|
28
|
-
|
|
29
|
-
return False
|
|
30
|
-
|
|
31
|
-
def get_file_size(response):
|
|
32
|
-
"""
|
|
33
|
-
Get the file size from the response headers.
|
|
34
|
-
"""
|
|
35
|
-
content_length = response.headers.get('content-length')
|
|
36
|
-
if content_length:
|
|
37
|
-
return int(content_length)
|
|
38
|
-
return 0
|
|
39
|
-
|
|
40
|
-
def download_image(url, output_path):
|
|
41
|
-
"""
|
|
42
|
-
Downloads an image from a URL with security validation.
|
|
43
|
-
"""
|
|
44
|
-
# Ensure output directory exists if it's not the current directory
|
|
45
|
-
dir_name = os.path.dirname(output_path)
|
|
46
|
-
if dir_name:
|
|
47
|
-
os.makedirs(dir_name, exist_ok=True)
|
|
48
|
-
|
|
49
|
-
headers = {
|
|
50
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try:
|
|
54
|
-
print(f"Downloading image from: {url}")
|
|
55
|
-
|
|
56
|
-
# First, make a HEAD request to check content type and size
|
|
57
|
-
head_response = requests.head(url, headers=headers, timeout=10)
|
|
58
|
-
content_type = head_response.headers.get('content-type', '').lower()
|
|
59
|
-
|
|
60
|
-
# Validate content type
|
|
61
|
-
if not is_safe_image_type(content_type, url):
|
|
62
|
-
print(f"Security Error: Content type '{content_type}' is not a safe image type.")
|
|
63
|
-
sys.exit(1)
|
|
64
|
-
|
|
65
|
-
# Check file size (limit to 50MB)
|
|
66
|
-
file_size = get_file_size(head_response)
|
|
67
|
-
if file_size > 50 * 1024 * 1024: # 50MB
|
|
68
|
-
print(f"Security Error: File size {file_size} bytes exceeds 50MB limit.")
|
|
69
|
-
sys.exit(1)
|
|
70
|
-
|
|
71
|
-
# Actually download the file
|
|
72
|
-
response = requests.get(url, headers=headers, stream=True, timeout=10)
|
|
73
|
-
response.raise_for_status()
|
|
74
|
-
|
|
75
|
-
# Double-check content type after download
|
|
76
|
-
downloaded_content_type = response.headers.get('content-type', '').lower()
|
|
77
|
-
if not is_safe_image_type(downloaded_content_type, url):
|
|
78
|
-
print(f"Security Error: Downloaded content type '{downloaded_content_type}' is not a safe image type.")
|
|
79
|
-
sys.exit(1)
|
|
80
|
-
|
|
81
|
-
# Write file in chunks with size validation
|
|
82
|
-
total_size = 0
|
|
83
|
-
with open(output_path, 'wb') as f:
|
|
84
|
-
for chunk in response.iter_content(chunk_size=8192):
|
|
85
|
-
if chunk: # Filter out keep-alive chunks
|
|
86
|
-
total_size += len(chunk)
|
|
87
|
-
if total_size > 50 * 1024 * 1024: # 50MB limit
|
|
88
|
-
print(f"Security Error: Downloaded file exceeds 50MB limit.")
|
|
89
|
-
os.remove(output_path) # Clean up partial file
|
|
90
|
-
sys.exit(1)
|
|
91
|
-
f.write(chunk)
|
|
92
|
-
|
|
93
|
-
print(f"Successfully saved to {output_path}")
|
|
94
|
-
|
|
95
|
-
except Exception as e:
|
|
96
|
-
print(f"Failed to download image: {e}")
|
|
97
|
-
sys.exit(1)
|
|
98
|
-
|
|
99
|
-
if __name__ == "__main__":
|
|
100
|
-
parser = argparse.ArgumentParser(description="Download an image.")
|
|
101
|
-
parser.add_argument("--url", required=True, help="Image URL")
|
|
102
|
-
parser.add_argument("--output", required=True, help="Output file path")
|
|
103
|
-
|
|
104
|
-
args = parser.parse_args()
|
|
105
|
-
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import requests
|
|
4
|
+
import argparse
|
|
5
|
+
import mimetypes
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
def is_safe_image_type(content_type, url):
|
|
9
|
+
"""
|
|
10
|
+
Check if the content type is a safe image type.
|
|
11
|
+
"""
|
|
12
|
+
safe_types = {
|
|
13
|
+
'image/jpeg', 'image/jpg', 'image/png', 'image/gif',
|
|
14
|
+
'image/webp', 'image/bmp', 'image/svg+xml', 'image/tiff'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Check content-type header
|
|
18
|
+
if content_type and content_type.lower() in safe_types:
|
|
19
|
+
return True
|
|
20
|
+
|
|
21
|
+
# Fallback: check file extension from URL
|
|
22
|
+
parsed_url = urlparse(url)
|
|
23
|
+
file_ext = os.path.splitext(parsed_url.path)[1].lower()
|
|
24
|
+
mime_type, _ = mimetypes.guess_type(f"dummy{file_ext}")
|
|
25
|
+
|
|
26
|
+
if mime_type and mime_type in safe_types:
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def get_file_size(response):
|
|
32
|
+
"""
|
|
33
|
+
Get the file size from the response headers.
|
|
34
|
+
"""
|
|
35
|
+
content_length = response.headers.get('content-length')
|
|
36
|
+
if content_length:
|
|
37
|
+
return int(content_length)
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
def download_image(url, output_path):
|
|
41
|
+
"""
|
|
42
|
+
Downloads an image from a URL with security validation.
|
|
43
|
+
"""
|
|
44
|
+
# Ensure output directory exists if it's not the current directory
|
|
45
|
+
dir_name = os.path.dirname(output_path)
|
|
46
|
+
if dir_name:
|
|
47
|
+
os.makedirs(dir_name, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
headers = {
|
|
50
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
print(f"Downloading image from: {url}")
|
|
55
|
+
|
|
56
|
+
# First, make a HEAD request to check content type and size
|
|
57
|
+
head_response = requests.head(url, headers=headers, timeout=10)
|
|
58
|
+
content_type = head_response.headers.get('content-type', '').lower()
|
|
59
|
+
|
|
60
|
+
# Validate content type
|
|
61
|
+
if not is_safe_image_type(content_type, url):
|
|
62
|
+
print(f"Security Error: Content type '{content_type}' is not a safe image type.")
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
# Check file size (limit to 50MB)
|
|
66
|
+
file_size = get_file_size(head_response)
|
|
67
|
+
if file_size > 50 * 1024 * 1024: # 50MB
|
|
68
|
+
print(f"Security Error: File size {file_size} bytes exceeds 50MB limit.")
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
# Actually download the file
|
|
72
|
+
response = requests.get(url, headers=headers, stream=True, timeout=10)
|
|
73
|
+
response.raise_for_status()
|
|
74
|
+
|
|
75
|
+
# Double-check content type after download
|
|
76
|
+
downloaded_content_type = response.headers.get('content-type', '').lower()
|
|
77
|
+
if not is_safe_image_type(downloaded_content_type, url):
|
|
78
|
+
print(f"Security Error: Downloaded content type '{downloaded_content_type}' is not a safe image type.")
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
|
|
81
|
+
# Write file in chunks with size validation
|
|
82
|
+
total_size = 0
|
|
83
|
+
with open(output_path, 'wb') as f:
|
|
84
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
85
|
+
if chunk: # Filter out keep-alive chunks
|
|
86
|
+
total_size += len(chunk)
|
|
87
|
+
if total_size > 50 * 1024 * 1024: # 50MB limit
|
|
88
|
+
print(f"Security Error: Downloaded file exceeds 50MB limit.")
|
|
89
|
+
os.remove(output_path) # Clean up partial file
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
f.write(chunk)
|
|
92
|
+
|
|
93
|
+
print(f"Successfully saved to {output_path}")
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
print(f"Failed to download image: {e}")
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
parser = argparse.ArgumentParser(description="Download an image.")
|
|
101
|
+
parser.add_argument("--url", required=True, help="Image URL")
|
|
102
|
+
parser.add_argument("--output", required=True, help="Output file path")
|
|
103
|
+
|
|
104
|
+
args = parser.parse_args()
|
|
105
|
+
|
|
106
106
|
download_image(args.url, args.output)
|