feat: implement vertical crop, local LLM deepseek-v4-flash, multi-threading, word-level pink box highlight subtitles, and Nextcloud scan sync
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
"""CLI entry point.
|
||||
|
||||
Usage:
|
||||
python main.py "https://www.youtube.com/watch?v=..." \
|
||||
--num-clips 3 --aspect-ratio 9:16
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
# Windows uses 'charmap' by default, which can't encode Unicode characters
|
||||
# like →. Reconfigure stdout/stderr to UTF-8 so output works on all platforms.
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
if hasattr(sys.stderr, "reconfigure"):
|
||||
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
from shorts_generator import generate_shorts
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="AI YouTube Shorts Generator")
|
||||
parser.add_argument("url", help="YouTube URL, file:// URL, or local file path")
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=["api", "local"],
|
||||
default="api",
|
||||
help="api (default, MuAPI) or local (remote URL, file://, or local path + faster-whisper + LLM provider + ffmpeg).",
|
||||
)
|
||||
parser.add_argument("--num-clips", type=int, default=3, help="How many shorts to render (default: 3)")
|
||||
parser.add_argument("--aspect-ratio", default="9:16", help="Output aspect ratio (default: 9:16)")
|
||||
parser.add_argument("--format", default="720", help="Source download resolution: 360 / 480 / 720 / 1080 (default: 720)")
|
||||
parser.add_argument("--language", default=None, help="Force Whisper language code, e.g. 'en' (default: auto-detect)")
|
||||
parser.add_argument("--output-json", default=None, help="Write the full result JSON to this path")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
result = generate_shorts(
|
||||
youtube_url=args.url,
|
||||
num_clips=args.num_clips,
|
||||
aspect_ratio=args.aspect_ratio,
|
||||
download_format=args.format,
|
||||
language=args.language,
|
||||
mode=args.mode,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"\nFAILED: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print("\n" + "=" * 72)
|
||||
print(f"Mode: {result.get('mode', args.mode)}")
|
||||
print(f"Source video: {result['source_video_url']}")
|
||||
print(f"Highlights: {len(result['highlights'])} candidates → kept top {len(result['shorts'])}")
|
||||
print("=" * 72)
|
||||
for i, s in enumerate(result["shorts"], 1):
|
||||
print(f"\n#{i} score={s.get('score')} {s.get('start_time'):.1f}s → {s.get('end_time'):.1f}s")
|
||||
print(f" title: {s.get('title')}")
|
||||
print(f" hook: {s.get('hook_sentence')}")
|
||||
if s.get("clip_url"):
|
||||
print(f" clip: {s['clip_url']}")
|
||||
else:
|
||||
print(f" clip: FAILED ({s.get('error')})")
|
||||
|
||||
if args.output_json:
|
||||
with open(args.output_json, "w") as f:
|
||||
json.dump(result, f, indent=2)
|
||||
print(f"\nFull JSON written to {args.output_json}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user