-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmake.py
More file actions
108 lines (90 loc) · 4.82 KB
/
Copy pathmake.py
File metadata and controls
108 lines (90 loc) · 4.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
"""Editor-OS — the full loop: drop a folder + a brief, get a finished video.
python make.py "C:/path/to/assets" --brief "emotional mental-health short for reels" -o out/final.mp4
The folder should contain your video clips and your narration audio (either in
`Video Clips/` + `Voiceover/` subfolders, or mixed — they're split by type and
ordered naturally). The brain transcribes the narration, picks a per-scene mood
grade, pairs clips to narration, and emits an EDL; the compiler renders it.
The EDL is written next to the output so you can tweak and re-render with
`edit.py render`.
"""
from __future__ import annotations
import argparse
import json
import sys
from collections import Counter
from pathlib import Path
from brain import ingest, planner
from editor import edl as edl_mod
from editor import music as music_mod
from editor.compiler import compile_edl
from editor.ffmpeg_runner import TOOLS
def _global_mood(edl: dict) -> str:
grades = [c.get("grade") for c in edl["video"] if c.get("grade")]
salient = [g for g in grades if g != "neutral"]
counts = Counter(salient or grades or ["neutral"])
return counts.most_common(1)[0][0]
def main() -> int:
ap = argparse.ArgumentParser(description="Editor-OS — folder + brief -> finished video")
ap.add_argument("root", nargs="?", help="Folder containing clips + narration")
ap.add_argument("--clips", help="Explicit clips folder (overrides discovery)")
ap.add_argument("--narration", help="Explicit narration folder (overrides discovery)")
ap.add_argument("--brief", default="", help="One-line description / goal")
ap.add_argument("-o", "--output", required=True, help="Output .mp4")
ap.add_argument("--aspect", default="9:16", choices=["9:16", "16:9", "1:1", "4:5"])
ap.add_argument("--fps", type=int, default=30)
ap.add_argument("--transition", default="fade")
ap.add_argument("--music", default=None, help="Music file (else library/none)")
ap.add_argument("--sfx-whoosh", default=None, help="Whoosh audio file to place at every transition")
ap.add_argument("--captions", default="karaoke", choices=["karaoke", "none"], help="Caption style (none for clips with baked-in text)")
ap.add_argument("--model", default="base", help="faster-whisper caption model size")
ap.add_argument("--grades", default=None, help="Comma list of per-scene look overrides (agent-brain arc), e.g. neutral,neutral_punch,neutral,cool_sad,warm_hopeful")
ap.add_argument("--emit-only", action="store_true", help="Write the EDL, do not render")
args = ap.parse_args()
TOOLS.check()
# 1. Discover + order + pair
if args.clips and args.narration:
vids = ingest._find(Path(args.clips), ingest.VIDEO_EXT)
auds = ingest._find(Path(args.narration), ingest.AUDIO_EXT)
elif args.root:
vids, auds = ingest.discover(args.root)
else:
ap.error("give a root folder, or both --clips and --narration")
if not vids or not auds:
print(f"ERROR: found {len(vids)} clips and {len(auds)} narrations — need at least one of each.")
return 1
pairs = ingest.pair(vids, auds)
print(f"ingested {len(pairs)} scene(s) from {args.root or args.clips}")
# 2. Plan -> EDL (per-scene grades from narration mood; optional agent override)
grades_override = [g.strip() for g in args.grades.split(",")] if args.grades else None
whoosh = Path(args.sfx_whoosh).resolve() if args.sfx_whoosh else None
edl = planner.plan(pairs, args.brief, aspect=args.aspect, fps=args.fps,
transition=args.transition, caption_model=args.model,
grades_override=grades_override, captions=args.captions,
sfx_whoosh=whoosh)
# 3. Resolve a mood-matched music bed (global mood); inject if found
out = Path(args.output).resolve()
mood = _global_mood(edl)
music_arg = str(Path(args.music).resolve()) if args.music else None
mpath, mnote = music_mod.resolve_music(mood, music_arg, cache_dir=out.parent)
if mpath:
edl["sources"]["m1"] = {"path": str(Path(mpath).resolve()), "kind": "audio"}
edl["music"] = {"source": "m1", "gain_db": -18, "duck": True}
print(f" music ({mood}): {mnote}")
# 4. Write the editable EDL next to the output
out.parent.mkdir(parents=True, exist_ok=True)
edl_path = out.with_suffix(".edl.json")
edl_path.write_text(json.dumps(edl, indent=2), encoding="utf-8")
print(f" EDL -> {edl_path}")
# 5. Validate + normalize + render
edl_mod.validate(edl, base_dir=out.parent, check_files=True)
norm = edl_mod.normalize(edl, base_dir=out.parent)
if args.emit_only:
print("emit-only: skipping render.")
return 0
report = compile_edl(norm, out)
print("\ndone:")
for k, v in report.items():
print(f" {k}: {v}")
return 0
if __name__ == "__main__":
sys.exit(main())