@remixmate/cli 0.9.1 → 0.9.5
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/dist/manifest.json +2 -2
- package/package.json +1 -1
- package/skills/export-jianying/SKILL.md +9 -3
- package/skills/export-jianying/version.json +1 -1
- package/skills/gen-script/scripts/gen_script.py +146 -110
- package/skills/gen-script/skill.json +1 -1
- package/skills/gen-script/version.json +1 -1
- package/skills/render-video/SKILL.md +20 -1
- package/skills/render-video/scripts/render_video.py +47 -0
- package/skills/render-video/version.json +1 -1
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "AI media generation skills for Claude Code / Codex — 11 skills covering image, video, voice, digital human, web capture, script, template registry, rendering, Jianying export, and video deconstruction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -65,15 +65,21 @@ python3 <SkillDir>/scripts/gen_jianying_draft.py \
|
|
|
65
65
|
|
|
66
66
|
### Read from a RenderPlan file (fallback, single-user / local debugging)
|
|
67
67
|
|
|
68
|
-
When not using the database mode, read a local
|
|
68
|
+
When not using the database mode, read a local RenderPlan file:
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
71
|
python3 <SkillDir>/scripts/gen_jianying_draft.py \
|
|
72
|
-
--from-render-plan
|
|
72
|
+
--from-render-plan <path>.render-plan.json \
|
|
73
73
|
--title "My Video"
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
> ⚠️ Don't point `--from-render-plan` at a shared `output/render-plan.json`. That
|
|
77
|
+
> file is NOT produced by the default token-present pipeline (the plan goes to the
|
|
78
|
+
> DB under a jobId), so anything sitting there is likely a stale leftover from an
|
|
79
|
+
> unrelated run. Prefer the `--from-job-id` DB mode, or render with an explicit
|
|
80
|
+
> `--save-render-plan --render-plan-output <path>` first.
|
|
81
|
+
|
|
82
|
+
The script automatically extracts the following from the RenderPlan:
|
|
77
83
|
- Each scene's visual asset URL (image / video)
|
|
78
84
|
- TTS audio URL and duration
|
|
79
85
|
- Subtitle text
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "export-jianying",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "554",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V3",
|
|
6
6
|
"skillDescription": "剪映草稿生成技能,将素材URL打包为剪映可导入的草稿ZIP,支持从 RenderPlan 自动转换(调用 ab-api /file/generateJianYing)。\n\n当用户提到以下任何需求时,立即使用本 skill:\n- 导出剪映、剪映草稿、打包剪映、导入剪映\n- 将素材导出为剪映格式、生成剪映工程\n- 把视频/图片/音频打包成剪映草稿\n- 从 RenderPlan 导出剪映草稿\n\n即使用户没有明确说「剪映」,只要他们想要将素材打包为可在剪映中编辑的草稿格式,也要使用本 skill。"
|
|
7
7
|
}
|
|
@@ -56,13 +56,6 @@ try:
|
|
|
56
56
|
except ImportError:
|
|
57
57
|
_HAS_REGISTRY_LOADER = False
|
|
58
58
|
|
|
59
|
-
try:
|
|
60
|
-
# Centralized monorepo-path resolver living in template-registry/scripts/;
|
|
61
|
-
# the import works because the same sys.path.insert above pointed at it.
|
|
62
|
-
from template_paths import monorepo_template_src_dir as _monorepo_template_src_dir
|
|
63
|
-
except ImportError:
|
|
64
|
-
_monorepo_template_src_dir = None
|
|
65
|
-
|
|
66
59
|
VALID_PLATFORMS = ["douyin", "xiaohongshu", "bilibili", "wechat", "youtube", "generic"]
|
|
67
60
|
VALID_RATIOS = ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"]
|
|
68
61
|
VALID_PURPOSES = ["opening", "point", "example", "explanation", "highlight", "cta", "ending"]
|
|
@@ -419,23 +412,98 @@ def _template_needs_image(template_config: dict | None) -> bool:
|
|
|
419
412
|
|
|
420
413
|
|
|
421
414
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
422
|
-
# Template-as-Contract:
|
|
415
|
+
# Template-as-Contract: capabilities-driven DSL assembly
|
|
423
416
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
424
417
|
#
|
|
425
|
-
#
|
|
426
|
-
#
|
|
418
|
+
# 模板的所有差异都通过 template.json 的 ``capabilities`` 声明(registry 是单一数据源,
|
|
419
|
+
# 内置模板与个人 OSS 模板同源可达)。gen_script 把 capabilities 归一化成一份 contract,
|
|
420
|
+
# 再用同一条流水线组装 DSL —— 不再有 input-schema.json,也不再有 if template_id==X 分支。
|
|
421
|
+
#
|
|
422
|
+
# 归一化 contract 字段(全部可缺省,缺省值保证"未声明能力"的模板走通用图文旁白):
|
|
423
|
+
# - needs_narration bool capabilities.needsNarration,默认 True
|
|
424
|
+
# - scene_strategy str capabilities.sceneStrategy ∈ {arc, single, fixed},默认 arc
|
|
425
|
+
# - fixed_scenes list[dict] capabilities.fixedScenes(single/fixed 用;缺省单 opening)
|
|
426
|
+
# - payload_style str capabilities.payloadStyle ∈ {visual-overlay, slide, carousel-caption}
|
|
427
|
+
# - duration_strategy str capabilities.durationStrategy ∈ {explicit, fit-caption}
|
|
428
|
+
# - payload_defaults dict capabilities.payloadDefaults(carousel-caption 的轮播/字幕默认值)
|
|
429
|
+
# - primary_visual_type / default_layout 由 assetRequirements / capabilities 推导
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _resolve_contract(template_config: dict | None) -> dict:
|
|
433
|
+
"""把 template.json 的 capabilities 归一化成一份装配 contract。
|
|
434
|
+
|
|
435
|
+
宽容读取:capabilities 缺失 / 非 dict(历史脏数据可能是 ``[]``)一律按"未声明"
|
|
436
|
+
处理,回退到通用图文旁白(arc + visual-overlay + 旁白开),保证没有声明能力的
|
|
437
|
+
模板与无模板场景行为一致。
|
|
438
|
+
"""
|
|
439
|
+
caps = (template_config or {}).get("capabilities")
|
|
440
|
+
caps = caps if isinstance(caps, dict) else {}
|
|
441
|
+
|
|
442
|
+
needs_narration = caps.get("needsNarration", True) is not False
|
|
427
443
|
|
|
444
|
+
scene_strategy = caps.get("sceneStrategy")
|
|
445
|
+
if scene_strategy not in ("arc", "single", "fixed"):
|
|
446
|
+
scene_strategy = "arc"
|
|
428
447
|
|
|
429
|
-
|
|
430
|
-
|
|
448
|
+
primary_visual_type = _template_primary_visual_type(template_config)
|
|
449
|
+
|
|
450
|
+
payload_style = caps.get("payloadStyle")
|
|
451
|
+
if payload_style not in ("visual-overlay", "slide", "carousel-caption"):
|
|
452
|
+
payload_style = "slide" if primary_visual_type == "none" else "visual-overlay"
|
|
453
|
+
|
|
454
|
+
duration_strategy = caps.get("durationStrategy")
|
|
455
|
+
if duration_strategy not in ("explicit", "fit-caption"):
|
|
456
|
+
duration_strategy = "explicit"
|
|
457
|
+
|
|
458
|
+
fixed_scenes = caps.get("fixedScenes")
|
|
459
|
+
if not isinstance(fixed_scenes, list) or not fixed_scenes:
|
|
460
|
+
# single / fixed 未显式给 fixedScenes 时退化为单 opening 场景
|
|
461
|
+
fixed_scenes = [{"purpose": "opening"}] if scene_strategy in ("single", "fixed") else []
|
|
462
|
+
|
|
463
|
+
payload_defaults = caps.get("payloadDefaults")
|
|
464
|
+
payload_defaults = payload_defaults if isinstance(payload_defaults, dict) else {}
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
"needs_narration": needs_narration,
|
|
468
|
+
"scene_strategy": scene_strategy,
|
|
469
|
+
"fixed_scenes": fixed_scenes,
|
|
470
|
+
"payload_style": payload_style,
|
|
471
|
+
"duration_strategy": duration_strategy,
|
|
472
|
+
"payload_defaults": payload_defaults,
|
|
473
|
+
"primary_visual_type": primary_visual_type,
|
|
474
|
+
"default_layout": caps.get("defaultLayout"),
|
|
475
|
+
}
|
|
431
476
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
477
|
+
|
|
478
|
+
def _plan_contract_scenes(
|
|
479
|
+
contract: dict, topic: str, duration: int, scene_count: int | None, language: str
|
|
480
|
+
) -> list:
|
|
481
|
+
"""按 contract.scene_strategy 规划场景列表。
|
|
482
|
+
|
|
483
|
+
- arc : 沿用 plan_scenes 叙事弧(opening → point* → cta,最少 3 段)。
|
|
484
|
+
- single : 单场景(默认 opening),整段时长归一个场景。
|
|
485
|
+
- fixed : 按 capabilities.fixedScenes 顺序展开,时长均分。
|
|
486
|
+
|
|
487
|
+
single / fixed 完全绕开 arc 的"最少 3 段"下限 —— 单图 Ken Burns / 打字机卡片
|
|
488
|
+
这类模板由此能产出真正的单场景,而不再被强行补到 3 段。``--scenes`` 只对 arc 生效。
|
|
435
489
|
"""
|
|
436
|
-
if
|
|
437
|
-
return
|
|
438
|
-
|
|
490
|
+
if contract["scene_strategy"] == "arc":
|
|
491
|
+
return plan_scenes(topic, duration, scene_count, False, language=language)
|
|
492
|
+
|
|
493
|
+
fixed = contract["fixed_scenes"] or [{"purpose": "opening"}]
|
|
494
|
+
n = len(fixed)
|
|
495
|
+
per = max(1, round(duration / n))
|
|
496
|
+
plans = []
|
|
497
|
+
for i, fs in enumerate(fixed):
|
|
498
|
+
purpose = fs.get("purpose", "opening") if isinstance(fs, dict) else "opening"
|
|
499
|
+
if purpose == "opening":
|
|
500
|
+
label = _localize_label(language, "label_opening")
|
|
501
|
+
elif purpose == "cta":
|
|
502
|
+
label = _localize_label(language, "label_cta")
|
|
503
|
+
else:
|
|
504
|
+
label = _localize_label(language, "label_point", n=i)
|
|
505
|
+
plans.append({"purpose": purpose, "duration": per, "label": label})
|
|
506
|
+
return plans
|
|
439
507
|
|
|
440
508
|
|
|
441
509
|
def _plain_len(text: str) -> int:
|
|
@@ -503,27 +571,9 @@ def _estimate_fit_caption_duration(custom_payload: dict) -> float:
|
|
|
503
571
|
return recommended
|
|
504
572
|
|
|
505
573
|
|
|
506
|
-
def
|
|
507
|
-
"""Load input-schema.json for a template. Returns None if not found."""
|
|
508
|
-
tpl_dir = _locate_template_dir(template_id)
|
|
509
|
-
if not tpl_dir:
|
|
510
|
-
return None
|
|
511
|
-
schema_file = os.path.join(tpl_dir, "input-schema.json")
|
|
512
|
-
if not os.path.isfile(schema_file):
|
|
513
|
-
return None
|
|
514
|
-
try:
|
|
515
|
-
with open(schema_file, "r", encoding="utf-8") as f:
|
|
516
|
-
return json.load(f)
|
|
517
|
-
except (OSError, json.JSONDecodeError) as exc:
|
|
518
|
-
print(f"⚠️ failed to load input-schema.json for {template_id}: {exc}", file=sys.stderr)
|
|
519
|
-
return None
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
def _build_dsl_from_input_schema(
|
|
574
|
+
def _build_carousel_caption_dsl(
|
|
523
575
|
*,
|
|
524
576
|
template_id: str,
|
|
525
|
-
template_config: dict,
|
|
526
|
-
input_schema: dict,
|
|
527
577
|
topic: str,
|
|
528
578
|
headline: str,
|
|
529
579
|
subheadline: str,
|
|
@@ -534,22 +584,19 @@ def _build_dsl_from_input_schema(
|
|
|
534
584
|
style: str,
|
|
535
585
|
ratio: str,
|
|
536
586
|
resolution: str,
|
|
537
|
-
voice_id: str,
|
|
538
587
|
output_language: str,
|
|
539
588
|
resolved_voice_id: str,
|
|
589
|
+
narration_enabled: bool,
|
|
590
|
+
payload_defaults: dict,
|
|
591
|
+
duration_strategy: str | None,
|
|
540
592
|
) -> dict:
|
|
541
|
-
"""
|
|
593
|
+
"""carousel-caption 装配(spotlight-card 类:单场景、图/视频轮播 + 打字机)。
|
|
542
594
|
|
|
543
|
-
|
|
544
|
-
|
|
595
|
+
所有差异来自归一化 contract(capabilities.payloadDefaults / durationStrategy /
|
|
596
|
+
needsNarration),不再读 input-schema.json,也没有 if template_id==X 分支。
|
|
597
|
+
轮播素材由调用方在 carousel_items 里直接给 URL(existing 资产),不触发 AI 生图。
|
|
545
598
|
"""
|
|
546
|
-
defaults =
|
|
547
|
-
|
|
548
|
-
# 旁白开关:部分模板(如 spotlight-card)是打字机 / 画面驱动,刻意无配音。
|
|
549
|
-
# 由 input-schema.json 的 narration.enabled 声明,缺省 True 保持其它 schema
|
|
550
|
-
# 模板的向后兼容。关闭时不产出 gen-voice 音频资产,scene 也不带 audio.narration,
|
|
551
|
-
# 这样 prepare_video_assets 不会触发 TTS、render_video 也不会按 TTS 拉长场景时长。
|
|
552
|
-
narration_enabled = bool(input_schema.get("narration", {}).get("enabled", True))
|
|
599
|
+
defaults = payload_defaults or {}
|
|
553
600
|
|
|
554
601
|
# ── Build assets from carousel_items ──────────────────────────────────
|
|
555
602
|
assets = []
|
|
@@ -620,15 +667,9 @@ def _build_dsl_from_input_schema(
|
|
|
620
667
|
label="", topic=topic, duration=duration)
|
|
621
668
|
|
|
622
669
|
# ── Effective scene duration ──────────────────────────────────────────
|
|
623
|
-
# fit-caption
|
|
624
|
-
# 时长由打字机自然节奏决定,而不是盲取调用方传入的 targetDuration。
|
|
670
|
+
# fit-caption 策略:时长由打字机自然节奏决定,而不是盲取调用方传入的 targetDuration。
|
|
625
671
|
# 这是 spotlight-card 这类打字机/画面驱动模板的核心——没有旁白来"撑"时长,
|
|
626
672
|
# 必须由 caption 自己定,否则文字打完后画面会静止到 targetDuration。
|
|
627
|
-
scene_strategy = input_schema.get("sceneStrategy", {})
|
|
628
|
-
fixed_scenes = scene_strategy.get("fixedScenes", [])
|
|
629
|
-
duration_strategy = (
|
|
630
|
-
fixed_scenes[0].get("durationStrategy") if fixed_scenes else None
|
|
631
|
-
)
|
|
632
673
|
effective_duration = duration
|
|
633
674
|
if duration_strategy == "fit-caption":
|
|
634
675
|
est = _estimate_fit_caption_duration(custom_payload)
|
|
@@ -754,9 +795,9 @@ def build_dsl(
|
|
|
754
795
|
When template_id is provided, loads the template config and adapts the DSL
|
|
755
796
|
structure to match the template's assetRequirements and scene patterns.
|
|
756
797
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
798
|
+
模板差异全部来自归一化 contract(capabilities):payloadStyle=carousel-caption
|
|
799
|
+
走单场景轮播+打字机装配;其余按 sceneStrategy(arc/single/fixed)+ visual-overlay/
|
|
800
|
+
slide 走统一场景循环。无 input-schema.json、无 if template_id==X 分支。
|
|
760
801
|
|
|
761
802
|
headline / subheadline 让作者显式提供画面上的"短主标题 + 副标题"。
|
|
762
803
|
DSL 元字段命名约定:
|
|
@@ -767,51 +808,17 @@ def build_dsl(
|
|
|
767
808
|
注意:subtitle(CC 字幕)由 global.subtitle 与 render-plan.subtitleSegments
|
|
768
809
|
单独承载,与 subheadline 完全是两个东西,命名上刻意分开避免歧义。
|
|
769
810
|
"""
|
|
770
|
-
# Load template config
|
|
811
|
+
# Load template config(registry 单一数据源)→ 归一化 contract
|
|
771
812
|
template_config = _load_template_config(template_id) if template_id else None
|
|
813
|
+
contract = _resolve_contract(template_config)
|
|
772
814
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
# Resolve language + voice (same logic as generic path)
|
|
778
|
-
output_language, template_default_voice_id = _resolve_template_language_meta(template_config)
|
|
779
|
-
resolved_voice_id = _resolve_voice_id(voice_id, template_default_voice_id, output_language)
|
|
780
|
-
if resolved_voice_id != voice_id:
|
|
781
|
-
print(
|
|
782
|
-
f"ℹ️ voice resolution: cli={voice_id!r} → resolved={resolved_voice_id!r} "
|
|
783
|
-
f"(templateDefault={template_default_voice_id!r}, language={output_language})",
|
|
784
|
-
file=sys.stderr,
|
|
785
|
-
)
|
|
786
|
-
effective_headline = (headline or "").strip() or topic
|
|
787
|
-
effective_subheadline = (subheadline or "").strip()
|
|
788
|
-
print(
|
|
789
|
-
f"ℹ️ using input-schema driven assembly for template {template_id}",
|
|
790
|
-
file=sys.stderr,
|
|
791
|
-
)
|
|
792
|
-
return _build_dsl_from_input_schema(
|
|
793
|
-
template_id=template_id,
|
|
794
|
-
template_config=template_config,
|
|
795
|
-
input_schema=input_schema,
|
|
796
|
-
topic=topic,
|
|
797
|
-
headline=effective_headline,
|
|
798
|
-
subheadline=effective_subheadline,
|
|
799
|
-
carousel_items=carousel_items or [],
|
|
800
|
-
caption_lines=caption_lines or [],
|
|
801
|
-
duration=duration,
|
|
802
|
-
platform=platform,
|
|
803
|
-
style=style,
|
|
804
|
-
ratio=ratio,
|
|
805
|
-
resolution=resolution,
|
|
806
|
-
voice_id=voice_id,
|
|
807
|
-
output_language=output_language,
|
|
808
|
-
resolved_voice_id=resolved_voice_id,
|
|
809
|
-
)
|
|
810
|
-
primary_visual_type = _template_primary_visual_type(template_config)
|
|
811
|
-
has_visual = primary_visual_type in ("image", "video")
|
|
815
|
+
primary_visual_type = contract["primary_visual_type"]
|
|
816
|
+
payload_style = contract["payload_style"]
|
|
817
|
+
# visual-overlay 模板才生成视觉素材并用 visuals.background;slide / carousel-caption 不走。
|
|
818
|
+
has_visual = payload_style == "visual-overlay"
|
|
812
819
|
# 旁白是模板能力(capabilities.needsNarration)。声明为 false 的模板(纯视觉 /
|
|
813
820
|
# BGM-only,如单图 Ken Burns)不分配 gen-voice 素材、场景不写 audio.narration。
|
|
814
|
-
needs_narration =
|
|
821
|
+
needs_narration = contract["needs_narration"]
|
|
815
822
|
|
|
816
823
|
# ── Resolve template-driven output language + voice ────────────────────
|
|
817
824
|
# outputLanguage is owned by the template (template.json). Missing/invalid
|
|
@@ -825,11 +832,37 @@ def build_dsl(
|
|
|
825
832
|
file=sys.stderr,
|
|
826
833
|
)
|
|
827
834
|
|
|
828
|
-
# 缺省值:headline 退回 topic
|
|
835
|
+
# 缺省值:headline 退回 topic,subheadline 默认空字符串
|
|
829
836
|
effective_headline = (headline or "").strip() or topic
|
|
830
837
|
effective_subheadline = (subheadline or "").strip()
|
|
831
838
|
|
|
832
|
-
|
|
839
|
+
# ── carousel-caption 模板(单场景、图/视频轮播 + 打字机)单独装配并直接返回 ──
|
|
840
|
+
if payload_style == "carousel-caption":
|
|
841
|
+
print(
|
|
842
|
+
f"ℹ️ contract: payloadStyle=carousel-caption for template {template_id}",
|
|
843
|
+
file=sys.stderr,
|
|
844
|
+
)
|
|
845
|
+
return _build_carousel_caption_dsl(
|
|
846
|
+
template_id=template_id,
|
|
847
|
+
topic=topic,
|
|
848
|
+
headline=effective_headline,
|
|
849
|
+
subheadline=effective_subheadline,
|
|
850
|
+
carousel_items=carousel_items or [],
|
|
851
|
+
caption_lines=caption_lines or [],
|
|
852
|
+
duration=duration,
|
|
853
|
+
platform=platform,
|
|
854
|
+
style=style,
|
|
855
|
+
ratio=ratio,
|
|
856
|
+
resolution=resolution,
|
|
857
|
+
output_language=output_language,
|
|
858
|
+
resolved_voice_id=resolved_voice_id,
|
|
859
|
+
narration_enabled=needs_narration,
|
|
860
|
+
payload_defaults=contract["payload_defaults"],
|
|
861
|
+
duration_strategy=contract["duration_strategy"],
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
# ── 其余模板:统一场景规划(arc 叙事弧 / single / fixed)+ 统一装配循环 ──────
|
|
865
|
+
scene_plans = _plan_contract_scenes(contract, topic, duration, scene_count, output_language)
|
|
833
866
|
|
|
834
867
|
assets = []
|
|
835
868
|
scenes = []
|
|
@@ -847,8 +880,8 @@ def build_dsl(
|
|
|
847
880
|
if needs_narration else ""
|
|
848
881
|
)
|
|
849
882
|
|
|
850
|
-
#
|
|
851
|
-
if primary_visual_type == "image":
|
|
883
|
+
# 仅 visual-overlay 模板生成视觉素材(slide 无视觉素材,carousel-caption 已提前返回)
|
|
884
|
+
if has_visual and primary_visual_type == "image":
|
|
852
885
|
if stub_image_url:
|
|
853
886
|
assets.append({
|
|
854
887
|
"assetId": visual_asset_id,
|
|
@@ -877,7 +910,7 @@ def build_dsl(
|
|
|
877
910
|
"status": "planned",
|
|
878
911
|
"payload": img_payload,
|
|
879
912
|
})
|
|
880
|
-
elif primary_visual_type == "video":
|
|
913
|
+
elif has_visual and primary_visual_type == "video":
|
|
881
914
|
if stub_video_url:
|
|
882
915
|
assets.append({
|
|
883
916
|
"assetId": visual_asset_id,
|
|
@@ -1036,8 +1069,10 @@ def build_dsl(
|
|
|
1036
1069
|
"aspectRatio": ratio,
|
|
1037
1070
|
"resolution": resolution,
|
|
1038
1071
|
"fps": 30,
|
|
1072
|
+
# 无配音模板:关闭 CC 字幕(字幕段是 narration→TTS 的派生物),并省略
|
|
1073
|
+
# global.narration,避免下游误判存在旁白。
|
|
1039
1074
|
"subtitle": {"enabled": needs_narration, "style": "bottom"},
|
|
1040
|
-
"narration": {"voiceId": resolved_voice_id, "speed": 1.0},
|
|
1075
|
+
**({"narration": {"voiceId": resolved_voice_id, "speed": 1.0}} if needs_narration else {}),
|
|
1041
1076
|
"bgm": {"enabled": True, "volume": 0.12},
|
|
1042
1077
|
},
|
|
1043
1078
|
"assets": assets,
|
|
@@ -1110,9 +1145,9 @@ Examples:
|
|
|
1110
1145
|
default=None,
|
|
1111
1146
|
help="Media URL for the template's carousel/gallery. Can be repeated: "
|
|
1112
1147
|
"--carousel-items url1 --carousel-items url2. "
|
|
1113
|
-
"For templates
|
|
1114
|
-
"are placed directly into
|
|
1115
|
-
"bypassing AI image generation entirely.",
|
|
1148
|
+
"For carousel-caption templates (capabilities.payloadStyle=carousel-caption, "
|
|
1149
|
+
"e.g. spotlight-card), these URLs are placed directly into "
|
|
1150
|
+
"customPayload.carousel.items as existing assets, bypassing AI image generation entirely.",
|
|
1116
1151
|
)
|
|
1117
1152
|
parser.add_argument(
|
|
1118
1153
|
"--caption-lines",
|
|
@@ -1120,8 +1155,9 @@ Examples:
|
|
|
1120
1155
|
default=None,
|
|
1121
1156
|
help="Bottom typewriter text line. Can be repeated: "
|
|
1122
1157
|
"--caption-lines 'line1' --caption-lines 'line2'. "
|
|
1123
|
-
"For templates
|
|
1124
|
-
"into customPayload.caption.lines.
|
|
1158
|
+
"For carousel-caption templates (capabilities.payloadStyle=carousel-caption, "
|
|
1159
|
+
"e.g. spotlight-card), these are placed into customPayload.caption.lines. "
|
|
1160
|
+
"Supports **emphasis** syntax.",
|
|
1125
1161
|
)
|
|
1126
1162
|
|
|
1127
1163
|
args = parser.parse_args()
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"carousel_items": {
|
|
28
28
|
"type": "array",
|
|
29
29
|
"items": { "type": "string" },
|
|
30
|
-
"description": "Media URLs for the template's image/video carousel (e.g. spotlight-card's middle carousel). When provided together with a template_id
|
|
30
|
+
"description": "Media URLs for the template's image/video carousel (e.g. spotlight-card's middle carousel). When provided together with a template_id whose capabilities.payloadStyle=carousel-caption, these URLs are placed directly into customPayload.carousel.items as existing assets — NO AI image generation is triggered. **Must** pass when the user explicitly provides image/video URLs for carousel-style templates (spotlight-card, etc.). Each element is a full URL string."
|
|
31
31
|
},
|
|
32
32
|
"caption_lines": {
|
|
33
33
|
"type": "array",
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "gen-script",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "474",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V11",
|
|
6
6
|
"skillDescription": "视频脚本生成技能,将用户主题转化为结构化 Video DSL(JSON),描述视频的完整结构、素材需求与叙事逻辑。\n\n当用户提到以下任何需求时,立即使用本 skill:\n- 帮我写视频脚本、生成视频脚本、视频策划、写分镜脚本\n- 做一个短视频、帮我规划视频内容、生成视频 DSL\n- 把主题转成视频结构、视频内容规划\n\n即使用户没有明确说「生成 DSL」,只要他们想要把一个主题变成视频内容结构,也要使用本 skill。"
|
|
7
7
|
}
|
|
@@ -73,9 +73,28 @@ python3 <SkillDir>/scripts/render_video.py --job-id <jobId>
|
|
|
73
73
|
When the DB is unavailable, the script accepts a pre-generated RenderPlan file:
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
|
-
python3 <SkillDir>/scripts/render_video.py --render-plan
|
|
76
|
+
python3 <SkillDir>/scripts/render_video.py --render-plan <path>.render-plan.json
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
> ⚠️ **Do NOT blindly point `--render-plan` at a shared `output/render-plan.json`.**
|
|
80
|
+
> Under the default token-present path, `prepare_video_assets` / `render_video`
|
|
81
|
+
> persist the plan to the **database (jobId)** only — they do **not** write
|
|
82
|
+
> `output/render-plan.json`. Any file sitting there is likely a leftover from a
|
|
83
|
+
> previous, unrelated run (the `output/` dir is gitignored scratch), and rendering
|
|
84
|
+
> it silently produces the wrong video.
|
|
85
|
+
>
|
|
86
|
+
> To get a RenderPlan file on disk, generate it explicitly:
|
|
87
|
+
> ```bash
|
|
88
|
+
> python3 <SkillDir>/scripts/render_video.py --dsl video.dsl.json --template-id <id> \
|
|
89
|
+
> --resolve-only --save-render-plan --render-plan-output video.render-plan.json
|
|
90
|
+
> ```
|
|
91
|
+
> Or skip the file entirely and render in one shot:
|
|
92
|
+
> `render_video.py --dsl video.dsl.json --template-id <id>`.
|
|
93
|
+
>
|
|
94
|
+
> On load, the script now prints a `↳ plan: templateId=… composition=… duration=… title=…`
|
|
95
|
+
> summary, and warns if the plan file is older than a `--dsl` you also passed —
|
|
96
|
+
> check that line matches what you intend before the render proceeds.
|
|
97
|
+
|
|
79
98
|
### Render and upload to Alibaba Cloud OSS (default behavior)
|
|
80
99
|
|
|
81
100
|
```bash
|
|
@@ -1856,6 +1856,35 @@ def auto_bind_template(dsl: dict, template_id: str) -> dict:
|
|
|
1856
1856
|
return binding
|
|
1857
1857
|
|
|
1858
1858
|
|
|
1859
|
+
def _log_render_plan_summary(render_plan: dict) -> None:
|
|
1860
|
+
"""Print a one-line identity summary of a loaded RenderPlan.
|
|
1861
|
+
|
|
1862
|
+
The two ways to feed an existing plan (``--render-plan <file>`` and
|
|
1863
|
+
``--job-id <int>``) used to print only the *source* (path / id), never the
|
|
1864
|
+
*content*. When a stale ``output/render-plan.json`` from a previous, unrelated
|
|
1865
|
+
run got picked up, the mismatch was rendered silently. Surfacing
|
|
1866
|
+
templateId / title / duration / scene count here makes a wrong plan obvious
|
|
1867
|
+
at a glance before any render time is spent.
|
|
1868
|
+
"""
|
|
1869
|
+
try:
|
|
1870
|
+
template_id = render_plan.get("templateId", "?")
|
|
1871
|
+
title = render_plan.get("title", "")
|
|
1872
|
+
cfg = render_plan.get("renderConfig", {}) or {}
|
|
1873
|
+
fps = cfg.get("fps") or 30
|
|
1874
|
+
total_frames = cfg.get("totalFrames")
|
|
1875
|
+
scenes = len(render_plan.get("timeline", []) or [])
|
|
1876
|
+
comp = (render_plan.get("remotionProps", {}) or {}).get("compositionId", "?")
|
|
1877
|
+
dur = f"{total_frames / fps:.1f}s/{total_frames}f" if total_frames else "?"
|
|
1878
|
+
LogPrint(
|
|
1879
|
+
f" ↳ plan: templateId={template_id} composition={comp} "
|
|
1880
|
+
f"duration={dur} scenes={scenes}" + (f' title=\"{title}\"' if title else ""),
|
|
1881
|
+
file=sys.stderr,
|
|
1882
|
+
)
|
|
1883
|
+
except Exception:
|
|
1884
|
+
# A summary is a convenience, never a hard dependency of rendering.
|
|
1885
|
+
pass
|
|
1886
|
+
|
|
1887
|
+
|
|
1859
1888
|
def main():
|
|
1860
1889
|
parser = argparse.ArgumentParser(
|
|
1861
1890
|
description="Video render tool — DSL + TemplateBinding → Remotion video.",
|
|
@@ -1944,12 +1973,30 @@ Examples:
|
|
|
1944
1973
|
sys.exit(1)
|
|
1945
1974
|
render_plan = json.loads(render_plan_str)
|
|
1946
1975
|
LogPrint(f"✅ RenderPlan loaded (jobId={args.job_id})", file=sys.stderr)
|
|
1976
|
+
_log_render_plan_summary(render_plan)
|
|
1947
1977
|
elif args.render_plan:
|
|
1948
1978
|
if not os.path.exists(args.render_plan):
|
|
1949
1979
|
LogPrint(f"❌ file does not exist: {args.render_plan}", file=sys.stderr)
|
|
1950
1980
|
sys.exit(1)
|
|
1951
1981
|
render_plan = load_json(args.render_plan)
|
|
1952
1982
|
LogPrint(f"📋 loaded existing RenderPlan: {args.render_plan}", file=sys.stderr)
|
|
1983
|
+
_log_render_plan_summary(render_plan)
|
|
1984
|
+
# 陈旧文件防护:output/render-plan.json 等共享文件名常被上一次别的项目
|
|
1985
|
+
# 的运行残留覆盖(output/ 是 gitignored 调试目录)。当用户同时给了 --dsl
|
|
1986
|
+
# 用以表明"我想渲这个 DSL",但磁盘上的 plan 比 DSL 还旧时,几乎可以肯定
|
|
1987
|
+
# 加载到的是过期 plan —— 明确警告而不是静默渲染错的东西。
|
|
1988
|
+
if args.dsl and os.path.exists(args.dsl):
|
|
1989
|
+
try:
|
|
1990
|
+
if os.path.getmtime(args.render_plan) < os.path.getmtime(args.dsl):
|
|
1991
|
+
LogPrint(
|
|
1992
|
+
f"⚠️ STALE RenderPlan? '{args.render_plan}' is OLDER than the DSL "
|
|
1993
|
+
f"'{args.dsl}'. You may be rendering a leftover plan from a previous "
|
|
1994
|
+
"run. Regenerate with --dsl ... --template-id ... (drop --render-plan) "
|
|
1995
|
+
"if the summary above doesn't match what you expect.",
|
|
1996
|
+
file=sys.stderr,
|
|
1997
|
+
)
|
|
1998
|
+
except OSError:
|
|
1999
|
+
pass
|
|
1953
2000
|
else:
|
|
1954
2001
|
if not args.dsl and not args.dsl_json:
|
|
1955
2002
|
LogPrint(
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "render-video",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "473",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V19",
|
|
6
6
|
"skillDescription": "Final-render skill (Phase 3 of the two-phase video pipeline). Loads a persisted RenderPlan by job_id and drives Remotion to produce the final video. Assets must already be generated via prepare_video_assets."
|
|
7
7
|
}
|