@remixmate/cli 0.12.8 ā 0.13.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 +7 -2
- package/README.zh-CN.md +1 -1
- package/dist/manifest.json +21 -8
- package/dist/template/preview.js +7 -7
- package/dist/template/publish.js +8 -8
- package/dist/template/scene-props.js +1 -0
- package/dist/template/still-cases.js +1 -1
- package/package.json +1 -1
- package/skills/export-jianying/scripts/gen_jianying_draft.py +38 -33
- package/skills/export-jianying/version.json +1 -1
- package/skills/gen-script/SKILL.md +15 -13
- package/skills/gen-script/scripts/gen_script.py +906 -547
- package/skills/gen-script/skill.json +19 -6
- package/skills/gen-script/version.json +1 -1
- package/skills/render-video/scripts/_video_probe.py +0 -1
- package/skills/render-video/scripts/remote_renderer_client.py +9 -7
- package/skills/render-video/scripts/render_video.py +553 -391
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/SKILL.md +1 -1
- package/skills/template-registry/scripts/match_template.py +2 -2
- package/skills/template-registry/scripts/payload_hydrator.py +9 -1
- package/skills/template-registry/scripts/render_job_client.py +4 -0
- package/skills/template-registry/version.json +1 -1
- package/skills/template-registry/video_dsl/runtime/dsl_validator.py +50 -24
- package/skills/template-registry/video_dsl/schema/render-plan-v1alpha1.json +7 -1
- package/skills/video-parser/scripts/analyze_video.py +2 -0
- package/skills/video-parser/version.json +1 -1
|
@@ -37,7 +37,7 @@ import time
|
|
|
37
37
|
import urllib.error
|
|
38
38
|
import urllib.request
|
|
39
39
|
from datetime import datetime, timezone
|
|
40
|
-
from typing import Optional
|
|
40
|
+
from typing import Callable, Optional
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
|
|
@@ -700,15 +700,10 @@ def validate_and_fix_render_plan(render_plan: dict) -> list:
|
|
|
700
700
|
if asset.get("duration") is None and needs_video_duration:
|
|
701
701
|
|
|
702
702
|
|
|
703
|
+
|
|
703
704
|
fallback_dur = None
|
|
704
705
|
for entry in render_plan.get("timeline", []):
|
|
705
|
-
|
|
706
|
-
refs = (
|
|
707
|
-
props.get("videoAssetId"),
|
|
708
|
-
props.get("backgroundAssetId"),
|
|
709
|
-
props.get("imageAssetId"),
|
|
710
|
-
)
|
|
711
|
-
if asset.get("assetId") in refs:
|
|
706
|
+
if _props_reference(entry.get("props") or {}, asset.get("assetId")):
|
|
712
707
|
fallback_dur = entry.get("durationFrames", 150) / fps
|
|
713
708
|
break
|
|
714
709
|
if fallback_dur:
|
|
@@ -785,6 +780,19 @@ def _probe_video_meta(url: str) -> dict:
|
|
|
785
780
|
return probe_video_meta(url)
|
|
786
781
|
|
|
787
782
|
|
|
783
|
+
def _props_reference(value, asset_id) -> bool:
|
|
784
|
+
|
|
785
|
+
if not asset_id:
|
|
786
|
+
return False
|
|
787
|
+
if isinstance(value, str):
|
|
788
|
+
return value == asset_id
|
|
789
|
+
if isinstance(value, dict):
|
|
790
|
+
return any(_props_reference(v, asset_id) for v in value.values())
|
|
791
|
+
if isinstance(value, list):
|
|
792
|
+
return any(_props_reference(v, asset_id) for v in value)
|
|
793
|
+
return False
|
|
794
|
+
|
|
795
|
+
|
|
788
796
|
def build_render_plan(dsl: dict, binding: dict) -> dict:
|
|
789
797
|
|
|
790
798
|
fps = dsl.get("global", {}).get("fps", 30)
|
|
@@ -881,6 +889,7 @@ def build_render_plan(dsl: dict, binding: dict) -> dict:
|
|
|
881
889
|
|
|
882
890
|
|
|
883
891
|
|
|
892
|
+
|
|
884
893
|
meta_by_url: dict = {}
|
|
885
894
|
for aid, asset in assets_by_id.items():
|
|
886
895
|
url = asset.get("url")
|
|
@@ -988,6 +997,10 @@ def build_render_plan(dsl: dict, binding: dict) -> dict:
|
|
|
988
997
|
"subtitleAtSec": subtitle_at_sec,
|
|
989
998
|
"transition": {"type": scene_trans_type, "durationFrames": scene_trans_dur},
|
|
990
999
|
}
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
if isinstance(scene.get("textLayers"), list) and scene["textLayers"]:
|
|
1003
|
+
entry["textLayers"] = scene["textLayers"]
|
|
991
1004
|
timeline.append(entry)
|
|
992
1005
|
current_frame += duration_frames
|
|
993
1006
|
|
|
@@ -1617,8 +1630,6 @@ def _auto_highlight_map(
|
|
|
1617
1630
|
|
|
1618
1631
|
|
|
1619
1632
|
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
1633
|
if not narration_asset:
|
|
1623
1634
|
return
|
|
1624
1635
|
line_count = narration_asset.get("narrationLineCount")
|
|
@@ -1879,17 +1890,6 @@ def adjust_timeline_to_audio(render_plan: dict) -> dict:
|
|
|
1879
1890
|
|
|
1880
1891
|
entry.pop("layers", None)
|
|
1881
1892
|
|
|
1882
|
-
|
|
1883
|
-
slides = entry.get("props", {}).get("slides")
|
|
1884
|
-
if slides and isinstance(slides, list) and len(slides) > 0:
|
|
1885
|
-
scene_dur = entry["durationFrames"]
|
|
1886
|
-
per_slide = scene_dur // len(slides)
|
|
1887
|
-
remainder = scene_dur - per_slide * len(slides)
|
|
1888
|
-
for si, slide in enumerate(slides):
|
|
1889
|
-
if not isinstance(slide, dict):
|
|
1890
|
-
continue
|
|
1891
|
-
slide["durationFrames"] = per_slide + (1 if si < remainder else 0)
|
|
1892
|
-
|
|
1893
1893
|
current_frame += entry["durationFrames"]
|
|
1894
1894
|
|
|
1895
1895
|
render_plan["renderConfig"]["totalFrames"] = current_frame
|
|
@@ -2137,8 +2137,6 @@ def render_with_remote_api(
|
|
|
2137
2137
|
|
|
2138
2138
|
|
|
2139
2139
|
|
|
2140
|
-
import remote_renderer_client
|
|
2141
|
-
|
|
2142
2140
|
render_plan["status"] = "rendering"
|
|
2143
2141
|
render_plan["renderMode"] = "remote"
|
|
2144
2142
|
render_plan["logs"].append({
|
|
@@ -2147,6 +2145,30 @@ def render_with_remote_api(
|
|
|
2147
2145
|
"timestamp": now_iso(),
|
|
2148
2146
|
})
|
|
2149
2147
|
|
|
2148
|
+
request = _remote_render_request(render_plan, output_path, upload_title)
|
|
2149
|
+
submitted = _submit_or_resume_remote_task(
|
|
2150
|
+
render_plan, request["payload"], request["render_path"], request["status_path"],
|
|
2151
|
+
private_token=private_token, conversation_id=conversation_id, job_id=job_id,
|
|
2152
|
+
)
|
|
2153
|
+
if submitted is None:
|
|
2154
|
+
return False
|
|
2155
|
+
task_id, status_path = submitted
|
|
2156
|
+
finished, result = _poll_remote_task(
|
|
2157
|
+
render_plan, task_id, status_path,
|
|
2158
|
+
private_token=private_token, poll_timeout=poll_timeout, poll_interval=poll_interval, job_id=job_id,
|
|
2159
|
+
)
|
|
2160
|
+
if not finished:
|
|
2161
|
+
return False
|
|
2162
|
+
return _apply_remote_result(
|
|
2163
|
+
render_plan, result,
|
|
2164
|
+
effective_title=request["effective_title"],
|
|
2165
|
+
cover_composition_id=request["cover_composition_id"],
|
|
2166
|
+
private_token=private_token,
|
|
2167
|
+
)
|
|
2168
|
+
|
|
2169
|
+
|
|
2170
|
+
def _remote_render_request(render_plan: dict, output_path: str, upload_title: Optional[str]) -> dict:
|
|
2171
|
+
|
|
2150
2172
|
https_assets = []
|
|
2151
2173
|
for asset in render_plan.get("resolvedAssets", []):
|
|
2152
2174
|
asset = asset.copy()
|
|
@@ -2260,6 +2282,27 @@ def render_with_remote_api(
|
|
|
2260
2282
|
LogPrint(f" size: {config.get('width')}x{config.get('height')}", file=sys.stderr)
|
|
2261
2283
|
LogPrint(f" total frames: {total_frames}", file=sys.stderr)
|
|
2262
2284
|
sys.stderr.flush()
|
|
2285
|
+
return {
|
|
2286
|
+
"payload": payload,
|
|
2287
|
+
"render_path": render_path,
|
|
2288
|
+
"status_path": status_path,
|
|
2289
|
+
"effective_title": effective_title,
|
|
2290
|
+
"cover_composition_id": cover_composition_id,
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
|
|
2294
|
+
def _submit_or_resume_remote_task(
|
|
2295
|
+
render_plan: dict,
|
|
2296
|
+
payload: dict,
|
|
2297
|
+
render_path: str,
|
|
2298
|
+
status_path: str,
|
|
2299
|
+
*,
|
|
2300
|
+
private_token: str,
|
|
2301
|
+
conversation_id: Optional[str],
|
|
2302
|
+
job_id: Optional[int],
|
|
2303
|
+
) -> Optional[tuple[str, str]]:
|
|
2304
|
+
|
|
2305
|
+
import remote_renderer_client
|
|
2263
2306
|
|
|
2264
2307
|
|
|
2265
2308
|
|
|
@@ -2290,7 +2333,7 @@ def render_with_remote_api(
|
|
|
2290
2333
|
"message": f"remote start_render failed: {exc}",
|
|
2291
2334
|
"timestamp": now_iso(),
|
|
2292
2335
|
})
|
|
2293
|
-
return
|
|
2336
|
+
return None
|
|
2294
2337
|
|
|
2295
2338
|
LogPrint(f" ā
task submitted, taskId={task_id}", file=sys.stderr)
|
|
2296
2339
|
render_plan["remoteTaskId"] = task_id
|
|
@@ -2306,6 +2349,11 @@ def render_with_remote_api(
|
|
|
2306
2349
|
|
|
2307
2350
|
_persist_plan_checkpoint(render_plan, job_id, private_token)
|
|
2308
2351
|
|
|
2352
|
+
return task_id, status_path
|
|
2353
|
+
|
|
2354
|
+
|
|
2355
|
+
def _remote_progress_reporter(render_plan: dict, task_id: str) -> Callable[[dict], None]:
|
|
2356
|
+
|
|
2309
2357
|
last_progress = -1.0
|
|
2310
2358
|
last_status: Optional[str] = None
|
|
2311
2359
|
last_stage: Optional[str] = None
|
|
@@ -2364,6 +2412,24 @@ def render_with_remote_api(
|
|
|
2364
2412
|
last_stage = stage
|
|
2365
2413
|
last_emit_at = now
|
|
2366
2414
|
|
|
2415
|
+
return _on_progress
|
|
2416
|
+
|
|
2417
|
+
|
|
2418
|
+
def _poll_remote_task(
|
|
2419
|
+
render_plan: dict,
|
|
2420
|
+
task_id: str,
|
|
2421
|
+
status_path: str,
|
|
2422
|
+
*,
|
|
2423
|
+
private_token: str,
|
|
2424
|
+
poll_timeout: float,
|
|
2425
|
+
poll_interval: float,
|
|
2426
|
+
job_id: Optional[int],
|
|
2427
|
+
) -> tuple[bool, Optional[dict]]:
|
|
2428
|
+
|
|
2429
|
+
import remote_renderer_client
|
|
2430
|
+
|
|
2431
|
+
_on_progress = _remote_progress_reporter(render_plan, task_id)
|
|
2432
|
+
|
|
2367
2433
|
|
|
2368
2434
|
|
|
2369
2435
|
|
|
@@ -2411,7 +2477,7 @@ def render_with_remote_api(
|
|
|
2411
2477
|
f"ēØåäøäøŖ job_id åč°äøę¬” render_video å³åÆē»äøļ¼äøä¼éę°ęø²ęć",
|
|
2412
2478
|
flush=True,
|
|
2413
2479
|
)
|
|
2414
|
-
return False
|
|
2480
|
+
return False, None
|
|
2415
2481
|
except Exception as exc:
|
|
2416
2482
|
LogPrint(f"ā remote render polling failed: {exc}", file=sys.stderr)
|
|
2417
2483
|
render_plan["status"] = "failed"
|
|
@@ -2426,10 +2492,21 @@ def render_with_remote_api(
|
|
|
2426
2492
|
"message": f"remote poll_render failed: {exc}",
|
|
2427
2493
|
"timestamp": now_iso(),
|
|
2428
2494
|
})
|
|
2429
|
-
return False
|
|
2495
|
+
return False, None
|
|
2430
2496
|
finally:
|
|
2431
2497
|
|
|
2432
2498
|
signal.signal(signal.SIGINT, prev_sigint)
|
|
2499
|
+
return True, result
|
|
2500
|
+
|
|
2501
|
+
|
|
2502
|
+
def _apply_remote_result(
|
|
2503
|
+
render_plan: dict,
|
|
2504
|
+
result: Optional[dict],
|
|
2505
|
+
*,
|
|
2506
|
+
effective_title: str,
|
|
2507
|
+
cover_composition_id: Optional[str],
|
|
2508
|
+
private_token: str,
|
|
2509
|
+
) -> bool:
|
|
2433
2510
|
|
|
2434
2511
|
file_url =(result or {}).get("fileUrl") or (result or {}).get("videoUrl")
|
|
2435
2512
|
if not file_url:
|
|
@@ -2597,9 +2674,7 @@ def _log_render_plan_summary(render_plan: dict) -> None:
|
|
|
2597
2674
|
pass
|
|
2598
2675
|
|
|
2599
2676
|
|
|
2600
|
-
def
|
|
2601
|
-
|
|
2602
|
-
atexit.register(print_billing_footer)
|
|
2677
|
+
def _build_arg_parser() -> argparse.ArgumentParser:
|
|
2603
2678
|
parser = argparse.ArgumentParser(
|
|
2604
2679
|
description="Video render tool ā DSL + TemplateBinding ā Remotion video.",
|
|
2605
2680
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
@@ -2672,154 +2747,164 @@ Examples:
|
|
|
2672
2747
|
default=None,
|
|
2673
2748
|
help="Test mode: every source=gen-video asset is short-circuited to this URL, no gen-video API call (env STUB_VIDEO_URL works too, but the explicit CLI flag is preferred to avoid cross-session leakage).",
|
|
2674
2749
|
)
|
|
2750
|
+
return parser
|
|
2675
2751
|
|
|
2676
|
-
args = parser.parse_args()
|
|
2677
2752
|
|
|
2678
|
-
|
|
2753
|
+
def _load_plan_from_job(job_id: int, private_token: str) -> dict:
|
|
2679
2754
|
|
|
2680
2755
|
|
|
2681
|
-
|
|
2682
|
-
|
|
2756
|
+
LogPrint(f"š loading RenderPlan from database (jobId={job_id})...", file=sys.stderr)
|
|
2757
|
+
if not private_token:
|
|
2758
|
+
LogPrint("ā --job-id mode requires PRIV_TOKEN or --priv-token", file=sys.stderr)
|
|
2759
|
+
sys.exit(1)
|
|
2683
2760
|
|
|
2684
2761
|
|
|
2685
|
-
if args.save_job and not private_token:
|
|
2686
|
-
LogPrint("ā¹ļø PRIV_TOKEN not configured; --save-job auto-disabled (will not write to database)", file=sys.stderr)
|
|
2687
|
-
args.save_job = False
|
|
2688
2762
|
|
|
2689
|
-
|
|
2763
|
+
from render_job_client import get_plan as rjc_get_plan
|
|
2764
|
+
try:
|
|
2765
|
+
render_plan_str = rjc_get_plan(job_id, private_token)
|
|
2766
|
+
except RuntimeError as e:
|
|
2767
|
+
LogPrint(f"ā failed to fetch RenderPlan: {e}", file=sys.stderr)
|
|
2768
|
+
sys.exit(1)
|
|
2769
|
+
render_plan = json.loads(render_plan_str)
|
|
2770
|
+
LogPrint(f"ā
RenderPlan loaded (jobId={job_id})", file=sys.stderr)
|
|
2771
|
+
_log_render_plan_summary(render_plan)
|
|
2772
|
+
return render_plan
|
|
2690
2773
|
|
|
2691
|
-
LogPrint(f"š loading RenderPlan from database (jobId={args.job_id})...", file=sys.stderr)
|
|
2692
|
-
if not private_token:
|
|
2693
|
-
LogPrint("ā --job-id mode requires PRIV_TOKEN or --priv-token", file=sys.stderr)
|
|
2694
|
-
sys.exit(1)
|
|
2695
2774
|
|
|
2775
|
+
def _load_plan_from_file(plan_path: str, dsl_path: str | None) -> dict:
|
|
2696
2776
|
|
|
2777
|
+
if not os.path.exists(plan_path):
|
|
2778
|
+
LogPrint(f"ā file does not exist: {plan_path}", file=sys.stderr)
|
|
2779
|
+
sys.exit(1)
|
|
2780
|
+
render_plan = load_json(plan_path)
|
|
2781
|
+
LogPrint(f"š loaded existing RenderPlan: {plan_path}", file=sys.stderr)
|
|
2782
|
+
_log_render_plan_summary(render_plan)
|
|
2697
2783
|
|
|
2698
|
-
|
|
2784
|
+
|
|
2785
|
+
|
|
2786
|
+
|
|
2787
|
+
if dsl_path and os.path.exists(dsl_path):
|
|
2699
2788
|
try:
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
render_plan = load_json(args.render_plan)
|
|
2712
|
-
LogPrint(f"š loaded existing RenderPlan: {args.render_plan}", file=sys.stderr)
|
|
2713
|
-
_log_render_plan_summary(render_plan)
|
|
2789
|
+
if os.path.getmtime(plan_path) < os.path.getmtime(dsl_path):
|
|
2790
|
+
LogPrint(
|
|
2791
|
+
f"ā ļø STALE RenderPlan? '{plan_path}' is OLDER than the DSL "
|
|
2792
|
+
f"'{dsl_path}'. You may be rendering a leftover plan from a previous "
|
|
2793
|
+
"run. Regenerate with --dsl ... --template-id ... (drop --render-plan) "
|
|
2794
|
+
"if the summary above doesn't match what you expect.",
|
|
2795
|
+
file=sys.stderr,
|
|
2796
|
+
)
|
|
2797
|
+
except OSError:
|
|
2798
|
+
pass
|
|
2799
|
+
return render_plan
|
|
2714
2800
|
|
|
2715
2801
|
|
|
2802
|
+
def _parse_dsl_json(dsl_json: str) -> dict:
|
|
2716
2803
|
|
|
2717
2804
|
|
|
2718
|
-
if args.dsl and os.path.exists(args.dsl):
|
|
2719
|
-
try:
|
|
2720
|
-
if os.path.getmtime(args.render_plan) < os.path.getmtime(args.dsl):
|
|
2721
|
-
LogPrint(
|
|
2722
|
-
f"ā ļø STALE RenderPlan? '{args.render_plan}' is OLDER than the DSL "
|
|
2723
|
-
f"'{args.dsl}'. You may be rendering a leftover plan from a previous "
|
|
2724
|
-
"run. Regenerate with --dsl ... --template-id ... (drop --render-plan) "
|
|
2725
|
-
"if the summary above doesn't match what you expect.",
|
|
2726
|
-
file=sys.stderr,
|
|
2727
|
-
)
|
|
2728
|
-
except OSError:
|
|
2729
|
-
pass
|
|
2730
|
-
else:
|
|
2731
|
-
if not args.dsl and not args.dsl_json:
|
|
2732
|
-
LogPrint(
|
|
2733
|
-
"ā render_video needs a DSL source. You provided neither --dsl/--dsl-json "
|
|
2734
|
-
"nor --render-plan/--job-id.\n"
|
|
2735
|
-
" Typical flow: 1) gen_script ā DSL skeleton 2) prepare_video_assets "
|
|
2736
|
-
"(returns a job_id) 3) render_video with job_id=<int>.\n"
|
|
2737
|
-
" Or pass the DSL inline: render_video --dsl-json '<json>' --template-id <id>.",
|
|
2738
|
-
file=sys.stderr,
|
|
2739
|
-
)
|
|
2740
|
-
sys.exit(1)
|
|
2741
2805
|
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2806
|
+
|
|
2807
|
+
|
|
2808
|
+
|
|
2809
|
+
|
|
2810
|
+
try:
|
|
2811
|
+
dsl = json.loads(dsl_json)
|
|
2812
|
+
except json.JSONDecodeError:
|
|
2813
|
+
dsl = None
|
|
2814
|
+
if dsl is None:
|
|
2815
|
+
try:
|
|
2816
|
+
|
|
2817
|
+
_dsl_str = dsl_json
|
|
2818
|
+
|
|
2819
|
+
_dsl_str = _dsl_str.replace('\u201c', '"').replace('\u201d', '"')
|
|
2820
|
+
_dsl_str = _dsl_str.replace('\u2018', "'").replace('\u2019', "'")
|
|
2821
|
+
|
|
2822
|
+
_dsl_str = _dsl_str.replace('\uff1a', ':')
|
|
2823
|
+
_dsl_str = _dsl_str.replace('\uff0c', ',')
|
|
2824
|
+
_dsl_str = _dsl_str.replace('\uff08', '(')
|
|
2825
|
+
_dsl_str = _dsl_str.replace('\uff09', ')')
|
|
2826
|
+
_dsl_str = _dsl_str.replace('\u3010', '[')
|
|
2827
|
+
_dsl_str = _dsl_str.replace('\u3011', ']')
|
|
2828
|
+
_dsl_str = _dsl_str.replace('\uff3b', '[')
|
|
2829
|
+
_dsl_str = _dsl_str.replace('\uff3d', ']')
|
|
2830
|
+
_dsl_str = _dsl_str.replace('\uff5b', '{')
|
|
2831
|
+
_dsl_str = _dsl_str.replace('\uff5d', '}')
|
|
2832
|
+
dsl = json.loads(_dsl_str)
|
|
2833
|
+
LogPrint("ā ļø --dsl-json was not valid JSON; parsed after normalizing full-width punctuation", file=sys.stderr)
|
|
2834
|
+
except json.JSONDecodeError as e:
|
|
2835
|
+
LogPrint(f"ā --dsl-json parse failed: {e}", file=sys.stderr)
|
|
2748
2836
|
sys.exit(1)
|
|
2837
|
+
return dsl
|
|
2749
2838
|
|
|
2750
2839
|
|
|
2751
|
-
|
|
2840
|
+
def _load_dsl(args) -> dict:
|
|
2752
2841
|
|
|
2842
|
+
if not args.dsl and not args.dsl_json:
|
|
2843
|
+
LogPrint(
|
|
2844
|
+
"ā render_video needs a DSL source. You provided neither --dsl/--dsl-json "
|
|
2845
|
+
"nor --render-plan/--job-id.\n"
|
|
2846
|
+
" Typical flow: 1) gen_script ā DSL skeleton 2) prepare_video_assets "
|
|
2847
|
+
"(returns a job_id) 3) render_video with job_id=<int>.\n"
|
|
2848
|
+
" Or pass the DSL inline: render_video --dsl-json '<json>' --template-id <id>.",
|
|
2849
|
+
file=sys.stderr,
|
|
2850
|
+
)
|
|
2851
|
+
sys.exit(1)
|
|
2753
2852
|
|
|
2853
|
+
if not args.template_id:
|
|
2854
|
+
LogPrint(
|
|
2855
|
+
"ā render_video needs --template-id when rendering from --dsl/--dsl-json "
|
|
2856
|
+
"(binding is computed in-memory from the template id; --binding is no longer supported).",
|
|
2857
|
+
file=sys.stderr,
|
|
2858
|
+
)
|
|
2859
|
+
sys.exit(1)
|
|
2754
2860
|
|
|
2755
2861
|
|
|
2862
|
+
if args.dsl_json:
|
|
2863
|
+
dsl = _parse_dsl_json(args.dsl_json)
|
|
2864
|
+
else:
|
|
2865
|
+
if not os.path.exists(args.dsl):
|
|
2866
|
+
LogPrint(f"ā file does not exist: {args.dsl}", file=sys.stderr)
|
|
2867
|
+
sys.exit(1)
|
|
2868
|
+
dsl = load_json(args.dsl)
|
|
2756
2869
|
|
|
2757
2870
|
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
dsl = None
|
|
2762
|
-
if dsl is None:
|
|
2763
|
-
try:
|
|
2871
|
+
if "version" not in dsl:
|
|
2872
|
+
dsl["version"] = "v1alpha1"
|
|
2873
|
+
LogPrint("ā ļø DSL missing version field; auto-filled with v1alpha1", file=sys.stderr)
|
|
2764
2874
|
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
_dsl_str = _dsl_str.replace('\u201c', '"').replace('\u201d', '"')
|
|
2768
|
-
_dsl_str = _dsl_str.replace('\u2018', "'").replace('\u2019', "'")
|
|
2769
|
-
|
|
2770
|
-
_dsl_str = _dsl_str.replace('\uff1a', ':')
|
|
2771
|
-
_dsl_str = _dsl_str.replace('\uff0c', ',')
|
|
2772
|
-
_dsl_str = _dsl_str.replace('\uff08', '(')
|
|
2773
|
-
_dsl_str = _dsl_str.replace('\uff09', ')')
|
|
2774
|
-
_dsl_str = _dsl_str.replace('\u3010', '[')
|
|
2775
|
-
_dsl_str = _dsl_str.replace('\u3011', ']')
|
|
2776
|
-
_dsl_str = _dsl_str.replace('\uff3b', '[')
|
|
2777
|
-
_dsl_str = _dsl_str.replace('\uff3d', ']')
|
|
2778
|
-
_dsl_str = _dsl_str.replace('\uff5b', '{')
|
|
2779
|
-
_dsl_str = _dsl_str.replace('\uff5d', '}')
|
|
2780
|
-
dsl = json.loads(_dsl_str)
|
|
2781
|
-
LogPrint("ā ļø --dsl-json was not valid JSON; parsed after normalizing full-width punctuation", file=sys.stderr)
|
|
2782
|
-
except json.JSONDecodeError as e:
|
|
2783
|
-
LogPrint(f"ā --dsl-json parse failed: {e}", file=sys.stderr)
|
|
2784
|
-
sys.exit(1)
|
|
2785
|
-
else:
|
|
2786
|
-
if not os.path.exists(args.dsl):
|
|
2787
|
-
LogPrint(f"ā file does not exist: {args.dsl}", file=sys.stderr)
|
|
2788
|
-
sys.exit(1)
|
|
2789
|
-
dsl = load_json(args.dsl)
|
|
2875
|
+
return dsl
|
|
2790
2876
|
|
|
2791
2877
|
|
|
2792
|
-
|
|
2793
|
-
dsl["version"] = "v1alpha1"
|
|
2794
|
-
LogPrint("ā ļø DSL missing version field; auto-filled with v1alpha1", file=sys.stderr)
|
|
2878
|
+
def _reject_skeleton_dsl(dsl: dict) -> None:
|
|
2795
2879
|
|
|
2796
2880
|
|
|
2797
2881
|
|
|
2882
|
+
_skeleton_offenders: list[str] = []
|
|
2883
|
+
_skel_markers = ("[éŖØę¶å¾
唫å
]", "[skeleton placeholder]", "čæęÆéč¦ęæę¢ēå ä½ęę”")
|
|
2884
|
+
for _scene in dsl.get("scenes", []) or []:
|
|
2885
|
+
_nar = ((_scene.get("audio") or {}).get("narration") or {})
|
|
2886
|
+
_sid = _scene.get("id", "?")
|
|
2887
|
+
if _nar.get("needsFill") is True:
|
|
2888
|
+
_skeleton_offenders.append(f"scene {_sid}: narration.needsFill=true")
|
|
2889
|
+
continue
|
|
2890
|
+
_t = _nar.get("text") or ""
|
|
2891
|
+
if any(m in _t for m in _skel_markers):
|
|
2892
|
+
_skeleton_offenders.append(f"scene {_sid}: narration.text still has the skeleton marker")
|
|
2893
|
+
if _skeleton_offenders:
|
|
2894
|
+
LogPrint("ā DSL narration is still the gen_script skeleton and was not replaced with real content; refusing to continue:", file=sys.stderr)
|
|
2895
|
+
for _off in _skeleton_offenders[:5]:
|
|
2896
|
+
LogPrint(f" - {_off}", file=sys.stderr)
|
|
2897
|
+
if len(_skeleton_offenders) > 5:
|
|
2898
|
+
LogPrint(f" ... ({len(_skeleton_offenders)} total)", file=sys.stderr)
|
|
2899
|
+
LogPrint(
|
|
2900
|
+
" Fill audio.narration.text with the real narration for every scene "
|
|
2901
|
+
"and remove the needsFill field, then retry.",
|
|
2902
|
+
file=sys.stderr,
|
|
2903
|
+
)
|
|
2904
|
+
sys.exit(2)
|
|
2798
2905
|
|
|
2799
|
-
_skeleton_offenders: list[str] = []
|
|
2800
|
-
_skel_markers = ("[éŖØę¶å¾
唫å
]", "[skeleton placeholder]", "čæęÆéč¦ęæę¢ēå ä½ęę”")
|
|
2801
|
-
for _scene in dsl.get("scenes", []) or []:
|
|
2802
|
-
_nar = ((_scene.get("audio") or {}).get("narration") or {})
|
|
2803
|
-
_sid = _scene.get("id", "?")
|
|
2804
|
-
if _nar.get("needsFill") is True:
|
|
2805
|
-
_skeleton_offenders.append(f"scene {_sid}: narration.needsFill=true")
|
|
2806
|
-
continue
|
|
2807
|
-
_t = _nar.get("text") or ""
|
|
2808
|
-
if any(m in _t for m in _skel_markers):
|
|
2809
|
-
_skeleton_offenders.append(f"scene {_sid}: narration.text still has the skeleton marker")
|
|
2810
|
-
if _skeleton_offenders:
|
|
2811
|
-
LogPrint("ā DSL narration is still the gen_script skeleton and was not replaced with real content; refusing to continue:", file=sys.stderr)
|
|
2812
|
-
for _off in _skeleton_offenders[:5]:
|
|
2813
|
-
LogPrint(f" - {_off}", file=sys.stderr)
|
|
2814
|
-
if len(_skeleton_offenders) > 5:
|
|
2815
|
-
LogPrint(f" ... ({len(_skeleton_offenders)} total)", file=sys.stderr)
|
|
2816
|
-
LogPrint(
|
|
2817
|
-
" Fill audio.narration.text with the real narration for every scene "
|
|
2818
|
-
"and remove the needsFill field, then retry.",
|
|
2819
|
-
file=sys.stderr,
|
|
2820
|
-
)
|
|
2821
|
-
sys.exit(2)
|
|
2822
2906
|
|
|
2907
|
+
def _plan_from_dsl(dsl: dict, args) -> tuple[dict, dict]:
|
|
2823
2908
|
|
|
2824
2909
|
|
|
2825
2910
|
|
|
@@ -2830,44 +2915,49 @@ Examples:
|
|
|
2830
2915
|
|
|
2831
2916
|
|
|
2832
2917
|
|
|
2833
|
-
_hydrate_dsl_payload(dsl, args.template_id, args.source_document_url)
|
|
2834
2918
|
|
|
2919
|
+
_hydrate_dsl_payload(dsl, args.template_id, args.source_document_url)
|
|
2835
2920
|
|
|
2836
2921
|
|
|
2837
|
-
_compose_image_prompts(dsl, args.template_id)
|
|
2838
2922
|
|
|
2839
|
-
|
|
2840
|
-
if errors:
|
|
2841
|
-
LogPrint("ā DSL validation failed:", file=sys.stderr)
|
|
2842
|
-
for err in errors:
|
|
2843
|
-
LogPrint(f" - {err}", file=sys.stderr)
|
|
2844
|
-
sys.exit(1)
|
|
2923
|
+
_compose_image_prompts(dsl, args.template_id)
|
|
2845
2924
|
|
|
2925
|
+
errors = validate_dsl(dsl)
|
|
2926
|
+
if errors:
|
|
2927
|
+
LogPrint("ā DSL validation failed:", file=sys.stderr)
|
|
2928
|
+
for err in errors:
|
|
2929
|
+
LogPrint(f" - {err}", file=sys.stderr)
|
|
2930
|
+
sys.exit(1)
|
|
2846
2931
|
|
|
2847
2932
|
|
|
2848
2933
|
|
|
2849
2934
|
|
|
2850
2935
|
|
|
2851
|
-
binding = auto_bind_template(dsl, args.template_id)
|
|
2852
2936
|
|
|
2853
|
-
|
|
2854
|
-
render_plan = build_render_plan(dsl, binding)
|
|
2855
|
-
render_plan["_dsl_assets"] = dsl.get("assets", [])
|
|
2937
|
+
binding = auto_bind_template(dsl, args.template_id)
|
|
2856
2938
|
|
|
2939
|
+
LogPrint(f"š Building RenderPlan...", file=sys.stderr)
|
|
2940
|
+
render_plan = build_render_plan(dsl, binding)
|
|
2941
|
+
render_plan["_dsl_assets"] = dsl.get("assets", [])
|
|
2942
|
+
return render_plan, binding
|
|
2857
2943
|
|
|
2858
2944
|
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2945
|
+
def _validate_saved_plan(render_plan: dict) -> None:
|
|
2946
|
+
|
|
2947
|
+
|
|
2948
|
+
from registry_loader import get_template
|
|
2949
|
+
from required_props import validate_required_props
|
|
2950
|
+
try:
|
|
2951
|
+
template = get_template(render_plan.get("templateId", ""), include_all_statuses=True)
|
|
2952
|
+
if not template:
|
|
2953
|
+
raise ValueError("Cannot validate RenderPlan: template metadata unavailable")
|
|
2954
|
+
validate_required_props(template, render_plan.get("timeline", []))
|
|
2955
|
+
except ValueError as exc:
|
|
2956
|
+
LogPrint(f"ā {exc}", file=sys.stderr)
|
|
2957
|
+
sys.exit(2)
|
|
2870
2958
|
|
|
2959
|
+
|
|
2960
|
+
def _reject_bad_asset_refs(render_plan: dict) -> None:
|
|
2871
2961
|
from video_dsl.runtime.payload_contract import plan_asset_errors
|
|
2872
2962
|
reference_errors = plan_asset_errors(render_plan)
|
|
2873
2963
|
if reference_errors:
|
|
@@ -2876,212 +2966,197 @@ Examples:
|
|
|
2876
2966
|
LogPrint(f" - {error}", file=sys.stderr)
|
|
2877
2967
|
sys.exit(2)
|
|
2878
2968
|
|
|
2879
|
-
if not args.skip_asset_resolve:
|
|
2880
2969
|
|
|
2970
|
+
def _resolve_plan_assets(render_plan: dict, args, private_token: str) -> dict:
|
|
2881
2971
|
|
|
2882
2972
|
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
pending_count = sum(1 for a in render_plan["resolvedAssets"] if a["status"] == "pending")
|
|
2907
|
-
if pending_count > 0:
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
if not private_token:
|
|
2912
|
-
LogPrint("ā asset generation needs an auth token, but PRIV_TOKEN env var is not set and --priv-token was not passed", file=sys.stderr)
|
|
2913
|
-
LogPrint(" configure it one of these ways:", file=sys.stderr)
|
|
2914
|
-
LogPrint(" 1. export PRIV_TOKEN=<your-token>", file=sys.stderr)
|
|
2915
|
-
LogPrint(" 2. python3 render_video.py --priv-token <your-token> ...", file=sys.stderr)
|
|
2916
|
-
sys.exit(1)
|
|
2917
|
-
|
|
2918
|
-
LogPrint(f"š Resolving {pending_count} missing asset(s)...", file=sys.stderr)
|
|
2919
|
-
render_plan = resolve_assets(
|
|
2920
|
-
render_plan,
|
|
2921
|
-
private_token=private_token,
|
|
2922
|
-
max_retries=args.max_asset_retries,
|
|
2923
|
-
timeout=args.asset_timeout,
|
|
2924
|
-
defer_digital_human=True,
|
|
2925
|
-
)
|
|
2973
|
+
|
|
2974
|
+
|
|
2975
|
+
stub_image_url = args.stub_image_url
|
|
2976
|
+
stub_video_url = args.stub_video_url
|
|
2977
|
+
if stub_image_url is None:
|
|
2978
|
+
env_v = os.environ.get("STUB_IMAGE_URL", "")
|
|
2979
|
+
if env_v:
|
|
2980
|
+
LogPrint(f"ā ļø detected env var STUB_IMAGE_URL={env_v}; using it as the image stub URL (pass --stub-image-url explicitly or unset the env var)", file=sys.stderr)
|
|
2981
|
+
stub_image_url = env_v
|
|
2982
|
+
if stub_video_url is None:
|
|
2983
|
+
env_v = os.environ.get("STUB_VIDEO_URL", "")
|
|
2984
|
+
if env_v:
|
|
2985
|
+
LogPrint(f"ā ļø detected env var STUB_VIDEO_URL={env_v}; using it as the video stub URL (pass --stub-video-url explicitly or unset the env var)", file=sys.stderr)
|
|
2986
|
+
stub_video_url = env_v
|
|
2987
|
+
|
|
2988
|
+
if stub_image_url or stub_video_url:
|
|
2989
|
+
stubbed = apply_stub_urls(
|
|
2990
|
+
render_plan,
|
|
2991
|
+
stub_image_url=stub_image_url,
|
|
2992
|
+
stub_video_url=stub_video_url,
|
|
2993
|
+
)
|
|
2994
|
+
if stubbed:
|
|
2995
|
+
LogPrint(f"š§Ŗ Stub mode: {stubbed} asset(s) using a stub URL; generation API skipped", file=sys.stderr)
|
|
2926
2996
|
else:
|
|
2927
|
-
LogPrint(f"
|
|
2928
|
-
|
|
2997
|
+
LogPrint(f"š§Ŗ Stub mode is on but no matching assets were found (no pending gen-image / gen-video resources)", file=sys.stderr)
|
|
2998
|
+
pending_count = sum(1 for a in render_plan["resolvedAssets"] if a["status"] == "pending")
|
|
2999
|
+
if pending_count > 0:
|
|
3000
|
+
|
|
2929
3001
|
|
|
2930
|
-
if not args.skip_asset_resolve:
|
|
2931
|
-
render_plan = adjust_timeline_to_audio(render_plan)
|
|
2932
3002
|
|
|
3003
|
+
if not private_token:
|
|
3004
|
+
LogPrint("ā asset generation needs an auth token, but PRIV_TOKEN env var is not set and --priv-token was not passed", file=sys.stderr)
|
|
3005
|
+
LogPrint(" configure it one of these ways:", file=sys.stderr)
|
|
3006
|
+
LogPrint(" 1. export PRIV_TOKEN=<your-token>", file=sys.stderr)
|
|
3007
|
+
LogPrint(" 2. python3 render_video.py --priv-token <your-token> ...", file=sys.stderr)
|
|
3008
|
+
sys.exit(1)
|
|
2933
3009
|
|
|
2934
|
-
|
|
3010
|
+
LogPrint(f"š Resolving {pending_count} missing asset(s)...", file=sys.stderr)
|
|
3011
|
+
render_plan = resolve_assets(
|
|
2935
3012
|
render_plan,
|
|
2936
3013
|
private_token=private_token,
|
|
2937
3014
|
max_retries=args.max_asset_retries,
|
|
2938
3015
|
timeout=args.asset_timeout,
|
|
3016
|
+
defer_digital_human=True,
|
|
2939
3017
|
)
|
|
3018
|
+
else:
|
|
3019
|
+
LogPrint(f"ā
all assets ready", file=sys.stderr)
|
|
3020
|
+
render_plan["status"] = "assets-ready"
|
|
2940
3021
|
|
|
2941
|
-
render_plan
|
|
3022
|
+
render_plan = adjust_timeline_to_audio(render_plan)
|
|
2942
3023
|
|
|
2943
|
-
output_path = os.path.join(OUTPUT_DIR, "video.mp4")
|
|
2944
|
-
rp_output = args.render_plan_output or os.path.join(OUTPUT_DIR, "render-plan.json")
|
|
2945
3024
|
|
|
3025
|
+
render_plan = resolve_digital_human_segments(
|
|
3026
|
+
render_plan,
|
|
3027
|
+
private_token=private_token,
|
|
3028
|
+
max_retries=args.max_asset_retries,
|
|
3029
|
+
timeout=args.asset_timeout,
|
|
3030
|
+
)
|
|
3031
|
+
return render_plan
|
|
2946
3032
|
|
|
2947
|
-
if args.save_render_plan or not args.save_job:
|
|
2948
|
-
validate_and_fix_render_plan(render_plan)
|
|
2949
|
-
save_json(render_plan, rp_output)
|
|
2950
|
-
LogPrint(f"š¾ RenderPlan saved: {rp_output}", file=sys.stderr)
|
|
2951
3033
|
|
|
3034
|
+
def _persist_render_plan(render_plan: dict, args, private_token: str, dsl: dict | None, binding: dict | None) -> None:
|
|
2952
3035
|
|
|
2953
3036
|
|
|
3037
|
+
validate_and_fix_render_plan(render_plan)
|
|
2954
3038
|
|
|
2955
|
-
|
|
3039
|
+
from render_job_client import create_job as rjc_create_job, save_plan as rjc_save_plan
|
|
3040
|
+
render_plan_json_str = json.dumps(render_plan, ensure_ascii=False)
|
|
2956
3041
|
|
|
2957
|
-
|
|
3042
|
+
_job_id = args.job_id
|
|
3043
|
+
if not _job_id:
|
|
2958
3044
|
|
|
2959
|
-
from render_job_client import create_job as rjc_create_job, save_plan as rjc_save_plan
|
|
2960
|
-
render_plan_json_str = json.dumps(render_plan, ensure_ascii=False)
|
|
2961
3045
|
|
|
2962
|
-
|
|
2963
|
-
|
|
3046
|
+
_dsl_obj = dsl
|
|
3047
|
+
_binding_obj = binding
|
|
3048
|
+
_meta_summary: dict = {}
|
|
2964
3049
|
|
|
2965
3050
|
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
3051
|
+
if _dsl_obj:
|
|
3052
|
+
_src_meta = (_dsl_obj.get("meta") or {})
|
|
3053
|
+
for _k in ("title", "topic", "platform", "templateId", "targetDuration"):
|
|
3054
|
+
_v = _src_meta.get(_k)
|
|
3055
|
+
if _v:
|
|
3056
|
+
_meta_summary[_k] = _v
|
|
3057
|
+
_ratio = (_dsl_obj.get("global") or {}).get("aspectRatio")
|
|
3058
|
+
if _ratio:
|
|
3059
|
+
_meta_summary["aspectRatio"] = _ratio
|
|
3060
|
+
else:
|
|
3061
|
+
for _k in ("title", "templateId", "targetDuration"):
|
|
3062
|
+
_v = render_plan.get(_k)
|
|
3063
|
+
if _v:
|
|
3064
|
+
_meta_summary[_k] = _v
|
|
3065
|
+
_rc = render_plan.get("renderConfig") or {}
|
|
3066
|
+
if _rc.get("width") and _rc.get("height"):
|
|
3067
|
+
_meta_summary["resolution"] = f"{_rc['width']}x{_rc['height']}"
|
|
3068
|
+
if _binding_obj and isinstance(_binding_obj, dict):
|
|
3069
|
+
_tid = _binding_obj.get("templateId")
|
|
3070
|
+
if _tid and "templateId" not in _meta_summary:
|
|
3071
|
+
_meta_summary["templateId"] = _tid
|
|
3072
|
+
_dsl_str = json.dumps(_dsl_obj, ensure_ascii=False) if _dsl_obj else ""
|
|
3073
|
+
try:
|
|
3074
|
+
_job_id = rjc_create_job(
|
|
3075
|
+
private_token,
|
|
3076
|
+
dsl=_dsl_str,
|
|
3077
|
+
dsl_meta=_meta_summary or None,
|
|
3078
|
+
)
|
|
3079
|
+
LogPrint(f"ā
render job created: jobId={_job_id}", file=sys.stderr)
|
|
3080
|
+
except RuntimeError as e:
|
|
3081
|
+
LogPrint(f"ā ļø failed to create render job (--save-job auto-disabled, rendering continues): {e}", file=sys.stderr)
|
|
3082
|
+
args.save_job = False
|
|
3083
|
+
_job_id = None
|
|
3084
|
+
if args.save_job and _job_id:
|
|
3085
|
+
try:
|
|
3086
|
+
rjc_save_plan(_job_id, render_plan_json_str, private_token)
|
|
3087
|
+
LogPrint(f"ā
RenderPlan saved to database: jobId={_job_id}", file=sys.stderr)
|
|
2970
3088
|
|
|
3089
|
+
args.job_id = _job_id
|
|
3090
|
+
print(f"\nš¦ render job jobId: {_job_id}")
|
|
3091
|
+
except RuntimeError as e:
|
|
3092
|
+
LogPrint(f"ā ļø failed to save RenderPlan to database (--save-job auto-disabled, rendering continues): {e}", file=sys.stderr)
|
|
3093
|
+
args.save_job = False
|
|
2971
3094
|
|
|
2972
|
-
if _dsl_obj:
|
|
2973
|
-
_src_meta = (_dsl_obj.get("meta") or {})
|
|
2974
|
-
for _k in ("title", "topic", "platform", "templateId", "targetDuration"):
|
|
2975
|
-
_v = _src_meta.get(_k)
|
|
2976
|
-
if _v:
|
|
2977
|
-
_meta_summary[_k] = _v
|
|
2978
|
-
_ratio = (_dsl_obj.get("global") or {}).get("aspectRatio")
|
|
2979
|
-
if _ratio:
|
|
2980
|
-
_meta_summary["aspectRatio"] = _ratio
|
|
2981
|
-
else:
|
|
2982
|
-
for _k in ("title", "templateId", "targetDuration"):
|
|
2983
|
-
_v = render_plan.get(_k)
|
|
2984
|
-
if _v:
|
|
2985
|
-
_meta_summary[_k] = _v
|
|
2986
|
-
_rc = render_plan.get("renderConfig") or {}
|
|
2987
|
-
if _rc.get("width") and _rc.get("height"):
|
|
2988
|
-
_meta_summary["resolution"] = f"{_rc['width']}x{_rc['height']}"
|
|
2989
|
-
if _binding_obj and isinstance(_binding_obj, dict):
|
|
2990
|
-
_tid = _binding_obj.get("templateId")
|
|
2991
|
-
if _tid and "templateId" not in _meta_summary:
|
|
2992
|
-
_meta_summary["templateId"] = _tid
|
|
2993
|
-
_dsl_str = json.dumps(_dsl_obj, ensure_ascii=False) if _dsl_obj else ""
|
|
2994
|
-
try:
|
|
2995
|
-
_job_id = rjc_create_job(
|
|
2996
|
-
private_token,
|
|
2997
|
-
dsl=_dsl_str,
|
|
2998
|
-
dsl_meta=_meta_summary or None,
|
|
2999
|
-
)
|
|
3000
|
-
LogPrint(f"ā
render job created: jobId={_job_id}", file=sys.stderr)
|
|
3001
|
-
except RuntimeError as e:
|
|
3002
|
-
LogPrint(f"ā ļø failed to create render job (--save-job auto-disabled, rendering continues): {e}", file=sys.stderr)
|
|
3003
|
-
args.save_job = False
|
|
3004
|
-
_job_id = None
|
|
3005
|
-
if args.save_job and _job_id:
|
|
3006
|
-
try:
|
|
3007
|
-
rjc_save_plan(_job_id, render_plan_json_str, private_token)
|
|
3008
|
-
LogPrint(f"ā
RenderPlan saved to database: jobId={_job_id}", file=sys.stderr)
|
|
3009
3095
|
|
|
3010
|
-
|
|
3011
|
-
print(f"\nš¦ render job jobId: {_job_id}")
|
|
3012
|
-
except RuntimeError as e:
|
|
3013
|
-
LogPrint(f"ā ļø failed to save RenderPlan to database (--save-job auto-disabled, rendering continues): {e}", file=sys.stderr)
|
|
3014
|
-
args.save_job = False
|
|
3096
|
+
def _print_resolve_only_summary(render_plan: dict) -> int:
|
|
3015
3097
|
|
|
3016
|
-
if
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
LogPrint(f"\nš Asset resolution finished (resolve-only mode): total {total}, generated {generated}, failed {failed}", file=sys.stderr)
|
|
3098
|
+
generated = sum(1 for a in render_plan["resolvedAssets"] if a["status"] == "generated")
|
|
3099
|
+
failed = sum(1 for a in render_plan["resolvedAssets"] if a["status"] == "failed")
|
|
3100
|
+
total = len(render_plan["resolvedAssets"])
|
|
3101
|
+
LogPrint(f"\nš Asset resolution finished (resolve-only mode): total {total}, generated {generated}, failed {failed}", file=sys.stderr)
|
|
3021
3102
|
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3103
|
+
fps = render_plan.get("renderConfig", {}).get("fps", 30)
|
|
3104
|
+
target_dur = render_plan.get("targetDuration")
|
|
3105
|
+
actual_dur = render_plan.get("renderConfig", {}).get("totalDuration", 0)
|
|
3025
3106
|
|
|
3026
3107
|
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
avatar_assets = [a for a in render_plan.get("resolvedAssets", []) if a.get("type") == "avatar"]
|
|
3062
|
-
avatar_scenes: dict = {}
|
|
3063
|
-
for entry in render_plan.get("timeline", []):
|
|
3064
|
-
ref = (entry.get("props") or {}).get("avatarAssetId")
|
|
3065
|
-
if ref:
|
|
3066
|
-
avatar_scenes.setdefault(ref, []).append(entry.get("sceneId", "?"))
|
|
3067
|
-
if avatar_assets:
|
|
3068
|
-
print(f"\nš§ Digital-human segments:")
|
|
3069
|
-
for asset in avatar_assets:
|
|
3070
|
-
scenes_on = ", ".join(avatar_scenes.get(asset["assetId"], [])) or "(no scene)"
|
|
3071
|
-
print(f" {asset['assetId']:<25} {asset.get('status', '?'):<10} scenes: {scenes_on} {asset.get('url', '')}")
|
|
3072
|
-
else:
|
|
3073
|
-
print(f"\nš§ Digital-human segments: none (this render has no presenter)")
|
|
3108
|
+
print(f"\nš Asset resolution finished: total {total}, generated {generated}, failed {failed}")
|
|
3109
|
+
if target_dur:
|
|
3110
|
+
diff = actual_dur - target_dur
|
|
3111
|
+
sign = "+" if diff >= 0 else ""
|
|
3112
|
+
print(f"ā± Actual duration: {actual_dur}s (target {target_dur}s, {sign}{diff:.1f}s)")
|
|
3113
|
+
else:
|
|
3114
|
+
print(f"ā± Actual duration: {actual_dur}s")
|
|
3115
|
+
|
|
3116
|
+
print(f"\nš¬ Scene timeline:")
|
|
3117
|
+
for entry in render_plan.get("timeline", []):
|
|
3118
|
+
scene_dur = round(entry.get("durationFrames", 0) / fps, 1)
|
|
3119
|
+
scene_id = entry.get("sceneId", "?")
|
|
3120
|
+
print(f" {scene_id:<25} {scene_dur:>6.1f}s")
|
|
3121
|
+
|
|
3122
|
+
audio_assets = [a for a in render_plan.get("resolvedAssets", [])
|
|
3123
|
+
if a.get("type") == "audio" and a.get("status") == "generated"]
|
|
3124
|
+
if audio_assets:
|
|
3125
|
+
print(f"\nš TTS audio:")
|
|
3126
|
+
for asset in audio_assets:
|
|
3127
|
+
dur_s = round((asset.get("duration") or 0) / 1000, 1)
|
|
3128
|
+
url = asset.get("url", "")
|
|
3129
|
+
print(f" {asset['assetId']:<25} {dur_s:>6.1f}s {url}")
|
|
3130
|
+
|
|
3131
|
+
image_assets = [a for a in render_plan.get("resolvedAssets", [])
|
|
3132
|
+
if a.get("type") == "image" and a.get("status") == "generated"]
|
|
3133
|
+
if image_assets:
|
|
3134
|
+
print(f"\nš¼ Image assets:")
|
|
3135
|
+
for asset in image_assets:
|
|
3136
|
+
url = asset.get("url", "")
|
|
3137
|
+
print(f" {asset['assetId']:<25} {url}")
|
|
3074
3138
|
|
|
3075
3139
|
|
|
3076
|
-
if failed > 0:
|
|
3077
|
-
sys.exit(1)
|
|
3078
|
-
return
|
|
3079
3140
|
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3141
|
+
|
|
3142
|
+
avatar_assets = [a for a in render_plan.get("resolvedAssets", []) if a.get("type") == "avatar"]
|
|
3143
|
+
avatar_scenes: dict = {}
|
|
3144
|
+
for entry in render_plan.get("timeline", []):
|
|
3145
|
+
ref = (entry.get("props") or {}).get("avatarAssetId")
|
|
3146
|
+
if ref:
|
|
3147
|
+
avatar_scenes.setdefault(ref, []).append(entry.get("sceneId", "?"))
|
|
3148
|
+
if avatar_assets:
|
|
3149
|
+
print(f"\nš§ Digital-human segments:")
|
|
3150
|
+
for asset in avatar_assets:
|
|
3151
|
+
scenes_on = ", ".join(avatar_scenes.get(asset["assetId"], [])) or "(no scene)"
|
|
3152
|
+
print(f" {asset['assetId']:<25} {asset.get('status', '?'):<10} scenes: {scenes_on} {asset.get('url', '')}")
|
|
3153
|
+
else:
|
|
3154
|
+
print(f"\nš§ Digital-human segments: none (this render has no presenter)")
|
|
3155
|
+
|
|
3156
|
+
return failed
|
|
3157
|
+
|
|
3158
|
+
|
|
3159
|
+
def _render(render_plan: dict, args, private_token: str, output_path: str) -> tuple[bool, str, str]:
|
|
3085
3160
|
|
|
3086
3161
|
render_mode = args.renderer
|
|
3087
3162
|
upload_title_hint = (
|
|
@@ -3114,6 +3189,75 @@ Examples:
|
|
|
3114
3189
|
LogPrint(f"š» Using local render mode", file=sys.stderr)
|
|
3115
3190
|
render_plan["renderMode"] = "local"
|
|
3116
3191
|
success = render_with_local_cli(render_plan, output_path)
|
|
3192
|
+
return success, render_mode, upload_title_hint
|
|
3193
|
+
|
|
3194
|
+
|
|
3195
|
+
def _print_remote_playback(render_plan: dict, upload_info: dict, remote_url: str, playback_url: str) -> None:
|
|
3196
|
+
|
|
3197
|
+
|
|
3198
|
+
effective_playback = playback_url or remote_url
|
|
3199
|
+
if effective_playback and not effective_playback.startswith("vod://"):
|
|
3200
|
+
print(f"\nš¬ Video playback URL: {effective_playback}")
|
|
3201
|
+
|
|
3202
|
+
|
|
3203
|
+
if upload_info.get("coverUrl"):
|
|
3204
|
+
print(f"š¼ļø Cover image URL: {upload_info['coverUrl']}")
|
|
3205
|
+
|
|
3206
|
+
|
|
3207
|
+
|
|
3208
|
+
|
|
3209
|
+
|
|
3210
|
+
|
|
3211
|
+
|
|
3212
|
+
|
|
3213
|
+
|
|
3214
|
+
|
|
3215
|
+
|
|
3216
|
+
|
|
3217
|
+
asset = {"url": effective_playback}
|
|
3218
|
+
cover = upload_info.get("coverUrl")
|
|
3219
|
+
if cover:
|
|
3220
|
+
asset["coverUrl"] = cover
|
|
3221
|
+
total_duration = (render_plan.get("renderConfig") or {}).get("totalDuration")
|
|
3222
|
+
if isinstance(total_duration, (int, float)) and total_duration > 0:
|
|
3223
|
+
asset["durationSec"] = total_duration
|
|
3224
|
+
print("__render_video_asset__ " + json.dumps(asset, ensure_ascii=False))
|
|
3225
|
+
|
|
3226
|
+
|
|
3227
|
+
def _upload_local_render(output_path: str, upload_title_hint: str, manifest: dict, manifest_path: str | None) -> None:
|
|
3228
|
+
|
|
3229
|
+
try:
|
|
3230
|
+
from upload_video import upload_media_file
|
|
3231
|
+
|
|
3232
|
+
upload_title = upload_title_hint
|
|
3233
|
+
LogPrint(f"\nš¤ Uploading video to OSS...", file=sys.stderr)
|
|
3234
|
+
upload_result = upload_media_file(
|
|
3235
|
+
file_path=output_path,
|
|
3236
|
+
title=upload_title,
|
|
3237
|
+
role="final_video",
|
|
3238
|
+
retention="output",
|
|
3239
|
+
)
|
|
3240
|
+
manifest["upload"] = {
|
|
3241
|
+
"fileUrl": upload_result["fileUrl"],
|
|
3242
|
+
"title": upload_title,
|
|
3243
|
+
"uploadedAt": now_iso(),
|
|
3244
|
+
|
|
3245
|
+
"fileId": upload_result.get("fileId"),
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
if upload_result.get("fileUrl"):
|
|
3249
|
+
print(f"\nš¬ Video playback URL: {upload_result['fileUrl']}")
|
|
3250
|
+
if manifest_path:
|
|
3251
|
+
save_json(manifest, manifest_path)
|
|
3252
|
+
LogPrint(f" Manifest updated: {manifest_path}", file=sys.stderr)
|
|
3253
|
+
except Exception as e:
|
|
3254
|
+
LogPrint(f"\nā ļø upload failed (the video stays local): {e}", file=sys.stderr)
|
|
3255
|
+
|
|
3256
|
+
|
|
3257
|
+
def _report_render(
|
|
3258
|
+
render_plan: dict, args, private_token: str, success: bool, render_mode: str,
|
|
3259
|
+
upload_title_hint: str, output_path: str, rp_output: str,
|
|
3260
|
+
) -> None:
|
|
3117
3261
|
|
|
3118
3262
|
|
|
3119
3263
|
if not args.save_job:
|
|
@@ -3145,34 +3289,7 @@ Examples:
|
|
|
3145
3289
|
LogPrint(f" Remote video: {remote_url}", file=sys.stderr)
|
|
3146
3290
|
if playback_url:
|
|
3147
3291
|
LogPrint(f" Playback URL: {playback_url}", file=sys.stderr)
|
|
3148
|
-
|
|
3149
|
-
effective_playback = playback_url or remote_url
|
|
3150
|
-
if effective_playback and not effective_playback.startswith("vod://"):
|
|
3151
|
-
print(f"\nš¬ Video playback URL: {effective_playback}")
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
if upload_info.get("coverUrl"):
|
|
3155
|
-
print(f"š¼ļø Cover image URL: {upload_info['coverUrl']}")
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
asset = {"url": effective_playback}
|
|
3169
|
-
cover = upload_info.get("coverUrl")
|
|
3170
|
-
if cover:
|
|
3171
|
-
asset["coverUrl"] = cover
|
|
3172
|
-
total_duration = (render_plan.get("renderConfig") or {}).get("totalDuration")
|
|
3173
|
-
if isinstance(total_duration, (int, float)) and total_duration > 0:
|
|
3174
|
-
asset["durationSec"] = total_duration
|
|
3175
|
-
print("__render_video_asset__ " + json.dumps(asset, ensure_ascii=False))
|
|
3292
|
+
_print_remote_playback(render_plan, upload_info, remote_url, playback_url)
|
|
3176
3293
|
else:
|
|
3177
3294
|
LogPrint(f" Video: {output_path}", file=sys.stderr)
|
|
3178
3295
|
if args.save_job:
|
|
@@ -3189,32 +3306,7 @@ Examples:
|
|
|
3189
3306
|
save_json(manifest, manifest_path)
|
|
3190
3307
|
LogPrint(f" Manifest updated (remote fileUrl): {manifest_path}", file=sys.stderr)
|
|
3191
3308
|
elif args.upload and not args.no_upload:
|
|
3192
|
-
|
|
3193
|
-
from upload_video import upload_media_file
|
|
3194
|
-
|
|
3195
|
-
upload_title = upload_title_hint
|
|
3196
|
-
LogPrint(f"\nš¤ Uploading video to OSS...", file=sys.stderr)
|
|
3197
|
-
upload_result = upload_media_file(
|
|
3198
|
-
file_path=output_path,
|
|
3199
|
-
title=upload_title,
|
|
3200
|
-
role="final_video",
|
|
3201
|
-
retention="output",
|
|
3202
|
-
)
|
|
3203
|
-
manifest["upload"] = {
|
|
3204
|
-
"fileUrl": upload_result["fileUrl"],
|
|
3205
|
-
"title": upload_title,
|
|
3206
|
-
"uploadedAt": now_iso(),
|
|
3207
|
-
|
|
3208
|
-
"fileId": upload_result.get("fileId"),
|
|
3209
|
-
}
|
|
3210
|
-
|
|
3211
|
-
if upload_result.get("fileUrl"):
|
|
3212
|
-
print(f"\nš¬ Video playback URL: {upload_result['fileUrl']}")
|
|
3213
|
-
if manifest_path:
|
|
3214
|
-
save_json(manifest, manifest_path)
|
|
3215
|
-
LogPrint(f" Manifest updated: {manifest_path}", file=sys.stderr)
|
|
3216
|
-
except Exception as e:
|
|
3217
|
-
LogPrint(f"\nā ļø upload failed (the video stays local): {e}", file=sys.stderr)
|
|
3309
|
+
_upload_local_render(output_path, upload_title_hint, manifest, manifest_path)
|
|
3218
3310
|
|
|
3219
3311
|
|
|
3220
3312
|
if args.save_job and args.job_id:
|
|
@@ -3229,5 +3321,75 @@ Examples:
|
|
|
3229
3321
|
sys.exit(1)
|
|
3230
3322
|
|
|
3231
3323
|
|
|
3324
|
+
def main():
|
|
3325
|
+
|
|
3326
|
+
atexit.register(print_billing_footer)
|
|
3327
|
+
args = _build_arg_parser().parse_args()
|
|
3328
|
+
|
|
3329
|
+
private_token = args.priv_token or os.environ.get("PRIV_TOKEN", "")
|
|
3330
|
+
|
|
3331
|
+
|
|
3332
|
+
if args.priv_token:
|
|
3333
|
+
os.environ["PRIV_TOKEN"] = private_token
|
|
3334
|
+
|
|
3335
|
+
|
|
3336
|
+
if args.save_job and not private_token:
|
|
3337
|
+
LogPrint("ā¹ļø PRIV_TOKEN not configured; --save-job auto-disabled (will not write to database)", file=sys.stderr)
|
|
3338
|
+
args.save_job = False
|
|
3339
|
+
|
|
3340
|
+
dsl = binding = None
|
|
3341
|
+
if args.job_id is not None and args.job_id > 0:
|
|
3342
|
+
render_plan = _load_plan_from_job(args.job_id, private_token)
|
|
3343
|
+
elif args.render_plan:
|
|
3344
|
+
render_plan = _load_plan_from_file(args.render_plan, args.dsl)
|
|
3345
|
+
else:
|
|
3346
|
+
dsl = _load_dsl(args)
|
|
3347
|
+
_reject_skeleton_dsl(dsl)
|
|
3348
|
+
render_plan, binding = _plan_from_dsl(dsl, args)
|
|
3349
|
+
|
|
3350
|
+
if args.job_id or args.render_plan:
|
|
3351
|
+
_validate_saved_plan(render_plan)
|
|
3352
|
+
_reject_bad_asset_refs(render_plan)
|
|
3353
|
+
|
|
3354
|
+
if not args.skip_asset_resolve:
|
|
3355
|
+
render_plan = _resolve_plan_assets(render_plan, args, private_token)
|
|
3356
|
+
|
|
3357
|
+
render_plan.pop("_dsl_assets", None)
|
|
3358
|
+
|
|
3359
|
+
output_path = os.path.join(OUTPUT_DIR, "video.mp4")
|
|
3360
|
+
rp_output = args.render_plan_output or os.path.join(OUTPUT_DIR, "render-plan.json")
|
|
3361
|
+
|
|
3362
|
+
|
|
3363
|
+
if args.save_render_plan or not args.save_job:
|
|
3364
|
+
validate_and_fix_render_plan(render_plan)
|
|
3365
|
+
save_json(render_plan, rp_output)
|
|
3366
|
+
LogPrint(f"š¾ RenderPlan saved: {rp_output}", file=sys.stderr)
|
|
3367
|
+
|
|
3368
|
+
|
|
3369
|
+
|
|
3370
|
+
|
|
3371
|
+
if args.save_job:
|
|
3372
|
+
_persist_render_plan(render_plan, args, private_token, dsl, binding)
|
|
3373
|
+
if not args.save_job and not args.save_render_plan:
|
|
3374
|
+
|
|
3375
|
+
|
|
3376
|
+
save_json(render_plan, rp_output)
|
|
3377
|
+
LogPrint(f"š¾ RenderPlan saved: {rp_output}", file=sys.stderr)
|
|
3378
|
+
|
|
3379
|
+
if args.resolve_only:
|
|
3380
|
+
if _print_resolve_only_summary(render_plan) > 0:
|
|
3381
|
+
sys.exit(1)
|
|
3382
|
+
return
|
|
3383
|
+
|
|
3384
|
+
if render_plan["status"] == "failed":
|
|
3385
|
+
LogPrint("ā asset resolution had failures; cannot continue rendering", file=sys.stderr)
|
|
3386
|
+
if not args.save_job:
|
|
3387
|
+
save_json(render_plan, rp_output)
|
|
3388
|
+
sys.exit(1)
|
|
3389
|
+
|
|
3390
|
+
success, render_mode, upload_title_hint = _render(render_plan, args, private_token, output_path)
|
|
3391
|
+
_report_render(render_plan, args, private_token, success, render_mode, upload_title_hint, output_path, rp_output)
|
|
3392
|
+
|
|
3393
|
+
|
|
3232
3394
|
if __name__ == "__main__":
|
|
3233
3395
|
main()
|