@remixmate/cli 0.1.2 → 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/README.md +46 -0
- package/README.zh-CN.md +22 -0
- package/dist/auth/auth-lock.d.ts +26 -0
- package/dist/auth/auth-lock.js +100 -0
- package/dist/auth/auto-login.d.ts +20 -0
- package/dist/auth/auto-login.js +66 -0
- package/dist/auth/commands.d.ts +8 -0
- package/dist/auth/commands.js +130 -0
- package/dist/auth/credential-store.d.ts +44 -0
- package/dist/auth/credential-store.js +126 -0
- package/dist/auth/device-flow-runner.d.ts +43 -0
- package/dist/auth/device-flow-runner.js +62 -0
- package/dist/auth/device-flow.d.ts +52 -0
- package/dist/auth/device-flow.js +115 -0
- package/dist/auth/environment.d.ts +25 -0
- package/dist/auth/environment.js +48 -0
- package/dist/auth/resolve.d.ts +30 -0
- package/dist/auth/resolve.js +44 -0
- package/dist/cli.js +11 -0
- package/dist/handlers/gen-digital-human.js +1 -1
- package/dist/handlers/gen-image.js +1 -1
- package/dist/handlers/gen-video.js +1 -1
- package/dist/handlers/gen-voice.js +2 -2
- package/dist/http.d.ts +9 -5
- package/dist/http.js +24 -10
- package/dist/manifest.json +3 -3
- package/dist/runner.d.ts +1 -1
- package/package.json +1 -1
- package/skills/export-jianying/SKILL.md +9 -3
- package/skills/export-jianying/version.json +1 -1
- package/skills/gen-digital-human/SKILL.md +11 -11
- package/skills/gen-digital-human/version.json +1 -1
- package/skills/gen-image/SKILL.md +6 -6
- package/skills/gen-image/version.json +1 -1
- package/skills/gen-script/scripts/gen_script.py +211 -140
- package/skills/gen-script/skill.json +1 -1
- package/skills/gen-script/version.json +1 -1
- package/skills/gen-video/SKILL.md +6 -6
- package/skills/gen-video/version.json +1 -1
- package/skills/gen-voice/SKILL.md +6 -6
- package/skills/gen-voice/version.json +1 -1
- package/skills/render-video/SKILL.md +20 -1
- package/skills/render-video/scripts/remote_renderer_client.py +9 -7
- package/skills/render-video/scripts/render_video.py +119 -15
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/README.md +12 -13
- package/skills/template-registry/SKILL.md +11 -12
- package/skills/template-registry/scripts/registry_loader.py +117 -96
- package/skills/template-registry/scripts/render_job_client.py +12 -0
- package/skills/template-registry/skill.json +1 -1
- package/skills/template-registry/version.json +1 -1
- package/skills/template-registry/video_dsl/runtime/dsl_validator.py +2 -2
- package/skills/web-capture/skill.json +0 -1
- package/skills/web-capture/version.json +1 -1
|
@@ -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"]
|
|
@@ -322,9 +315,9 @@ def generate_image_prompt(
|
|
|
322
315
|
def _load_template_config(template_id: str) -> dict | None:
|
|
323
316
|
"""Load template definition for the given template ID.
|
|
324
317
|
|
|
325
|
-
|
|
326
|
-
1. 通过 registry_loader 从
|
|
327
|
-
|
|
318
|
+
解析顺序:
|
|
319
|
+
1. 通过 registry_loader 从 ab-api(单一数据源,URL 取
|
|
320
|
+
VIDEO_TEMPLATE_REGISTRY_URL 或按 MM_API_BASE_URL 推导)加载,按 templateId 匹配。
|
|
328
321
|
2. 兜底:旧路径 template-registry/video_dsl/templates/<id>/template.json
|
|
329
322
|
(仅在仓库还残留旧目录时使用)。
|
|
330
323
|
返回完整 template dict(含 supportedAspectRatios / assetRequirements / slotMapping 等),
|
|
@@ -384,6 +377,29 @@ def _template_primary_visual_type(template_config: dict | None) -> str:
|
|
|
384
377
|
return "none"
|
|
385
378
|
|
|
386
379
|
|
|
380
|
+
def _template_needs_narration(template_config: dict | None) -> bool:
|
|
381
|
+
"""模板是否需要旁白(TTS)。
|
|
382
|
+
|
|
383
|
+
默认 True —— 保持历史行为:每个场景挂一个 gen-voice 素材 + 写入
|
|
384
|
+
``audio.narration`` 骨架,由 agent 后续填真实文案。
|
|
385
|
+
|
|
386
|
+
模板可通过 ``capabilities.needsNarration=false`` 声明"纯视觉 / 无旁白"
|
|
387
|
+
(如单图 Ken Burns、BGM-only 展示)。此时 gen_script:
|
|
388
|
+
- 不生成 gen-voice 素材;
|
|
389
|
+
- 场景不写 ``audio.narration``。
|
|
390
|
+
于是骨架的 narrationSceneCount=0,ab-agent 的 prepare_video_assets 预校验
|
|
391
|
+
会直接放行,不再强制用户为每个场景填旁白(与 spotlight-card 等无配音模板
|
|
392
|
+
走的是同一条 pass-through 路径)。
|
|
393
|
+
|
|
394
|
+
兼容历史脏数据:部分模板的 ``capabilities`` 是空列表 ``[]``(而非 dict),
|
|
395
|
+
统一按"未声明"处理 → 返回 True。仅当显式为 JSON ``false`` 时才关闭旁白。
|
|
396
|
+
"""
|
|
397
|
+
caps = (template_config or {}).get("capabilities")
|
|
398
|
+
if not isinstance(caps, dict):
|
|
399
|
+
return True
|
|
400
|
+
return caps.get("needsNarration", True) is not False
|
|
401
|
+
|
|
402
|
+
|
|
387
403
|
def _template_needs_image(template_config: dict | None) -> bool:
|
|
388
404
|
"""Check if a template requires image assets.
|
|
389
405
|
|
|
@@ -396,23 +412,98 @@ def _template_needs_image(template_config: dict | None) -> bool:
|
|
|
396
412
|
|
|
397
413
|
|
|
398
414
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
399
|
-
# Template-as-Contract:
|
|
415
|
+
# Template-as-Contract: capabilities-driven DSL assembly
|
|
400
416
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
401
417
|
#
|
|
402
|
-
#
|
|
403
|
-
#
|
|
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
|
|
404
443
|
|
|
444
|
+
scene_strategy = caps.get("sceneStrategy")
|
|
445
|
+
if scene_strategy not in ("arc", "single", "fixed"):
|
|
446
|
+
scene_strategy = "arc"
|
|
405
447
|
|
|
406
|
-
|
|
407
|
-
|
|
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
|
+
}
|
|
408
476
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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 生效。
|
|
412
489
|
"""
|
|
413
|
-
if
|
|
414
|
-
return
|
|
415
|
-
|
|
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
|
|
416
507
|
|
|
417
508
|
|
|
418
509
|
def _plain_len(text: str) -> int:
|
|
@@ -480,27 +571,9 @@ def _estimate_fit_caption_duration(custom_payload: dict) -> float:
|
|
|
480
571
|
return recommended
|
|
481
572
|
|
|
482
573
|
|
|
483
|
-
def
|
|
484
|
-
"""Load input-schema.json for a template. Returns None if not found."""
|
|
485
|
-
tpl_dir = _locate_template_dir(template_id)
|
|
486
|
-
if not tpl_dir:
|
|
487
|
-
return None
|
|
488
|
-
schema_file = os.path.join(tpl_dir, "input-schema.json")
|
|
489
|
-
if not os.path.isfile(schema_file):
|
|
490
|
-
return None
|
|
491
|
-
try:
|
|
492
|
-
with open(schema_file, "r", encoding="utf-8") as f:
|
|
493
|
-
return json.load(f)
|
|
494
|
-
except (OSError, json.JSONDecodeError) as exc:
|
|
495
|
-
print(f"⚠️ failed to load input-schema.json for {template_id}: {exc}", file=sys.stderr)
|
|
496
|
-
return None
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
def _build_dsl_from_input_schema(
|
|
574
|
+
def _build_carousel_caption_dsl(
|
|
500
575
|
*,
|
|
501
576
|
template_id: str,
|
|
502
|
-
template_config: dict,
|
|
503
|
-
input_schema: dict,
|
|
504
577
|
topic: str,
|
|
505
578
|
headline: str,
|
|
506
579
|
subheadline: str,
|
|
@@ -511,22 +584,19 @@ def _build_dsl_from_input_schema(
|
|
|
511
584
|
style: str,
|
|
512
585
|
ratio: str,
|
|
513
586
|
resolution: str,
|
|
514
|
-
voice_id: str,
|
|
515
587
|
output_language: str,
|
|
516
588
|
resolved_voice_id: str,
|
|
589
|
+
narration_enabled: bool,
|
|
590
|
+
payload_defaults: dict,
|
|
591
|
+
duration_strategy: str | None,
|
|
517
592
|
) -> dict:
|
|
518
|
-
"""
|
|
593
|
+
"""carousel-caption 装配(spotlight-card 类:单场景、图/视频轮播 + 打字机)。
|
|
519
594
|
|
|
520
|
-
|
|
521
|
-
|
|
595
|
+
所有差异来自归一化 contract(capabilities.payloadDefaults / durationStrategy /
|
|
596
|
+
needsNarration),不再读 input-schema.json,也没有 if template_id==X 分支。
|
|
597
|
+
轮播素材由调用方在 carousel_items 里直接给 URL(existing 资产),不触发 AI 生图。
|
|
522
598
|
"""
|
|
523
|
-
defaults =
|
|
524
|
-
|
|
525
|
-
# 旁白开关:部分模板(如 spotlight-card)是打字机 / 画面驱动,刻意无配音。
|
|
526
|
-
# 由 input-schema.json 的 narration.enabled 声明,缺省 True 保持其它 schema
|
|
527
|
-
# 模板的向后兼容。关闭时不产出 gen-voice 音频资产,scene 也不带 audio.narration,
|
|
528
|
-
# 这样 prepare_video_assets 不会触发 TTS、render_video 也不会按 TTS 拉长场景时长。
|
|
529
|
-
narration_enabled = bool(input_schema.get("narration", {}).get("enabled", True))
|
|
599
|
+
defaults = payload_defaults or {}
|
|
530
600
|
|
|
531
601
|
# ── Build assets from carousel_items ──────────────────────────────────
|
|
532
602
|
assets = []
|
|
@@ -597,15 +667,9 @@ def _build_dsl_from_input_schema(
|
|
|
597
667
|
label="", topic=topic, duration=duration)
|
|
598
668
|
|
|
599
669
|
# ── Effective scene duration ──────────────────────────────────────────
|
|
600
|
-
# fit-caption
|
|
601
|
-
# 时长由打字机自然节奏决定,而不是盲取调用方传入的 targetDuration。
|
|
670
|
+
# fit-caption 策略:时长由打字机自然节奏决定,而不是盲取调用方传入的 targetDuration。
|
|
602
671
|
# 这是 spotlight-card 这类打字机/画面驱动模板的核心——没有旁白来"撑"时长,
|
|
603
672
|
# 必须由 caption 自己定,否则文字打完后画面会静止到 targetDuration。
|
|
604
|
-
scene_strategy = input_schema.get("sceneStrategy", {})
|
|
605
|
-
fixed_scenes = scene_strategy.get("fixedScenes", [])
|
|
606
|
-
duration_strategy = (
|
|
607
|
-
fixed_scenes[0].get("durationStrategy") if fixed_scenes else None
|
|
608
|
-
)
|
|
609
673
|
effective_duration = duration
|
|
610
674
|
if duration_strategy == "fit-caption":
|
|
611
675
|
est = _estimate_fit_caption_duration(custom_payload)
|
|
@@ -731,9 +795,9 @@ def build_dsl(
|
|
|
731
795
|
When template_id is provided, loads the template config and adapts the DSL
|
|
732
796
|
structure to match the template's assetRequirements and scene patterns.
|
|
733
797
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
798
|
+
模板差异全部来自归一化 contract(capabilities):payloadStyle=carousel-caption
|
|
799
|
+
走单场景轮播+打字机装配;其余按 sceneStrategy(arc/single/fixed)+ visual-overlay/
|
|
800
|
+
slide 走统一场景循环。无 input-schema.json、无 if template_id==X 分支。
|
|
737
801
|
|
|
738
802
|
headline / subheadline 让作者显式提供画面上的"短主标题 + 副标题"。
|
|
739
803
|
DSL 元字段命名约定:
|
|
@@ -744,48 +808,17 @@ def build_dsl(
|
|
|
744
808
|
注意:subtitle(CC 字幕)由 global.subtitle 与 render-plan.subtitleSegments
|
|
745
809
|
单独承载,与 subheadline 完全是两个东西,命名上刻意分开避免歧义。
|
|
746
810
|
"""
|
|
747
|
-
# Load template config
|
|
811
|
+
# Load template config(registry 单一数据源)→ 归一化 contract
|
|
748
812
|
template_config = _load_template_config(template_id) if template_id else None
|
|
813
|
+
contract = _resolve_contract(template_config)
|
|
749
814
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
if resolved_voice_id != voice_id:
|
|
758
|
-
print(
|
|
759
|
-
f"ℹ️ voice resolution: cli={voice_id!r} → resolved={resolved_voice_id!r} "
|
|
760
|
-
f"(templateDefault={template_default_voice_id!r}, language={output_language})",
|
|
761
|
-
file=sys.stderr,
|
|
762
|
-
)
|
|
763
|
-
effective_headline = (headline or "").strip() or topic
|
|
764
|
-
effective_subheadline = (subheadline or "").strip()
|
|
765
|
-
print(
|
|
766
|
-
f"ℹ️ using input-schema driven assembly for template {template_id}",
|
|
767
|
-
file=sys.stderr,
|
|
768
|
-
)
|
|
769
|
-
return _build_dsl_from_input_schema(
|
|
770
|
-
template_id=template_id,
|
|
771
|
-
template_config=template_config,
|
|
772
|
-
input_schema=input_schema,
|
|
773
|
-
topic=topic,
|
|
774
|
-
headline=effective_headline,
|
|
775
|
-
subheadline=effective_subheadline,
|
|
776
|
-
carousel_items=carousel_items or [],
|
|
777
|
-
caption_lines=caption_lines or [],
|
|
778
|
-
duration=duration,
|
|
779
|
-
platform=platform,
|
|
780
|
-
style=style,
|
|
781
|
-
ratio=ratio,
|
|
782
|
-
resolution=resolution,
|
|
783
|
-
voice_id=voice_id,
|
|
784
|
-
output_language=output_language,
|
|
785
|
-
resolved_voice_id=resolved_voice_id,
|
|
786
|
-
)
|
|
787
|
-
primary_visual_type = _template_primary_visual_type(template_config)
|
|
788
|
-
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"
|
|
819
|
+
# 旁白是模板能力(capabilities.needsNarration)。声明为 false 的模板(纯视觉 /
|
|
820
|
+
# BGM-only,如单图 Ken Burns)不分配 gen-voice 素材、场景不写 audio.narration。
|
|
821
|
+
needs_narration = contract["needs_narration"]
|
|
789
822
|
|
|
790
823
|
# ── Resolve template-driven output language + voice ────────────────────
|
|
791
824
|
# outputLanguage is owned by the template (template.json). Missing/invalid
|
|
@@ -799,11 +832,37 @@ def build_dsl(
|
|
|
799
832
|
file=sys.stderr,
|
|
800
833
|
)
|
|
801
834
|
|
|
802
|
-
# 缺省值:headline 退回 topic
|
|
835
|
+
# 缺省值:headline 退回 topic,subheadline 默认空字符串
|
|
803
836
|
effective_headline = (headline or "").strip() or topic
|
|
804
837
|
effective_subheadline = (subheadline or "").strip()
|
|
805
838
|
|
|
806
|
-
|
|
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)
|
|
807
866
|
|
|
808
867
|
assets = []
|
|
809
868
|
scenes = []
|
|
@@ -815,11 +874,14 @@ def build_dsl(
|
|
|
815
874
|
visual_asset_id = f"video-{scene_id}"
|
|
816
875
|
else:
|
|
817
876
|
visual_asset_id = f"img-{scene_id}"
|
|
818
|
-
narration_asset_id = f"narration-{scene_id}"
|
|
819
|
-
narration_text =
|
|
877
|
+
narration_asset_id = f"narration-{scene_id}" if needs_narration else None
|
|
878
|
+
narration_text = (
|
|
879
|
+
estimate_narration_text(plan["label"], plan["purpose"], plan["duration"], topic, language=output_language)
|
|
880
|
+
if needs_narration else ""
|
|
881
|
+
)
|
|
820
882
|
|
|
821
|
-
#
|
|
822
|
-
if primary_visual_type == "image":
|
|
883
|
+
# 仅 visual-overlay 模板生成视觉素材(slide 无视觉素材,carousel-caption 已提前返回)
|
|
884
|
+
if has_visual and primary_visual_type == "image":
|
|
823
885
|
if stub_image_url:
|
|
824
886
|
assets.append({
|
|
825
887
|
"assetId": visual_asset_id,
|
|
@@ -848,7 +910,7 @@ def build_dsl(
|
|
|
848
910
|
"status": "planned",
|
|
849
911
|
"payload": img_payload,
|
|
850
912
|
})
|
|
851
|
-
elif primary_visual_type == "video":
|
|
913
|
+
elif has_visual and primary_visual_type == "video":
|
|
852
914
|
if stub_video_url:
|
|
853
915
|
assets.append({
|
|
854
916
|
"assetId": visual_asset_id,
|
|
@@ -881,15 +943,17 @@ def build_dsl(
|
|
|
881
943
|
# narration text 不再在 audio asset 的 payload 里冗余存放——
|
|
882
944
|
# 唯一来源是下游 scenes[].audio.narration.text,render_video 在
|
|
883
945
|
# 调用 TTS skill 前会按 assetRef 回查 scene 文本注入。
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
"
|
|
891
|
-
|
|
892
|
-
|
|
946
|
+
# 模板声明 needsNarration=false 时,完全不生成 gen-voice 素材。
|
|
947
|
+
if needs_narration:
|
|
948
|
+
assets.append({
|
|
949
|
+
"assetId": narration_asset_id,
|
|
950
|
+
"type": "audio",
|
|
951
|
+
"source": "gen-voice",
|
|
952
|
+
"status": "planned",
|
|
953
|
+
"payload": {
|
|
954
|
+
"voiceId": resolved_voice_id,
|
|
955
|
+
},
|
|
956
|
+
})
|
|
893
957
|
|
|
894
958
|
if has_visual:
|
|
895
959
|
layout = "text-overlay" if plan["purpose"] in ("opening", "cta") else "full-visual"
|
|
@@ -916,33 +980,35 @@ def build_dsl(
|
|
|
916
980
|
if effective_subheadline:
|
|
917
981
|
text_layers.append({"role": "subheadline", "content": effective_subheadline, "animation": "fade-in"})
|
|
918
982
|
|
|
919
|
-
|
|
983
|
+
scene = {
|
|
920
984
|
"id": scene_id,
|
|
921
985
|
"purpose": plan["purpose"],
|
|
922
986
|
"duration": plan["duration"],
|
|
923
987
|
"layout": layout,
|
|
924
988
|
"visuals": {"background": {"assetRef": visual_asset_id}},
|
|
925
|
-
"
|
|
989
|
+
"textLayers": text_layers,
|
|
990
|
+
"animationHints": {
|
|
991
|
+
"entrance": "fade",
|
|
992
|
+
"motion": "kenburns-in" if idx % 2 == 0 else "kenburns-out",
|
|
993
|
+
},
|
|
994
|
+
}
|
|
995
|
+
if needs_narration:
|
|
996
|
+
scene["audio"] = {
|
|
926
997
|
"narration": {
|
|
927
998
|
"text": narration_text,
|
|
928
999
|
"assetRef": narration_asset_id,
|
|
929
1000
|
# 骨架标记:scenes[].audio.narration.text 同样需要被真实旁白替换
|
|
930
1001
|
"needsFill": True,
|
|
931
1002
|
},
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
"animationHints": {
|
|
935
|
-
"entrance": "fade",
|
|
936
|
-
"motion": "kenburns-in" if idx % 2 == 0 else "kenburns-out",
|
|
937
|
-
},
|
|
938
|
-
})
|
|
1003
|
+
}
|
|
1004
|
+
scenes.append(scene)
|
|
939
1005
|
else:
|
|
940
1006
|
# The "no-visual" branch labels each scene with a layout hint.
|
|
941
1007
|
# Historical fallback was hardcoded "html-slide" — when no template
|
|
942
1008
|
# was specified, every audio-only scene defaulted to html-slide
|
|
943
1009
|
# styling. We replace the hardcoded fallback chain with:
|
|
944
1010
|
# 1. template.capabilities.defaultLayout ⇐ the template tells
|
|
945
|
-
#
|
|
1011
|
+
# remixmate what layout name it wants on its no-visual scenes
|
|
946
1012
|
# 2. template_id ⇐ legacy: pass id as
|
|
947
1013
|
# layout name (existing behavior when no capability declared)
|
|
948
1014
|
# 3. "html-slide" ⇐ absolute fallback
|
|
@@ -966,22 +1032,24 @@ def build_dsl(
|
|
|
966
1032
|
|
|
967
1033
|
custom_payload = _build_custom_payload(plan["purpose"], topic, plan["label"], idx, len(scene_plans), language=output_language)
|
|
968
1034
|
|
|
969
|
-
|
|
1035
|
+
scene = {
|
|
970
1036
|
"id": scene_id,
|
|
971
1037
|
"purpose": plan["purpose"],
|
|
972
1038
|
"duration": plan["duration"],
|
|
973
1039
|
"layout": layout,
|
|
974
|
-
"
|
|
1040
|
+
"textLayers": text_layers,
|
|
1041
|
+
"customPayload": custom_payload,
|
|
1042
|
+
}
|
|
1043
|
+
if needs_narration:
|
|
1044
|
+
scene["audio"] = {
|
|
975
1045
|
"narration": {
|
|
976
1046
|
"text": narration_text,
|
|
977
1047
|
"assetRef": narration_asset_id,
|
|
978
1048
|
# 骨架标记:scenes[].audio.narration.text 同样需要被真实旁白替换
|
|
979
1049
|
"needsFill": True,
|
|
980
1050
|
},
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
"customPayload": custom_payload,
|
|
984
|
-
})
|
|
1051
|
+
}
|
|
1052
|
+
scenes.append(scene)
|
|
985
1053
|
|
|
986
1054
|
dsl = {
|
|
987
1055
|
"version": "v1alpha1",
|
|
@@ -1001,8 +1069,10 @@ def build_dsl(
|
|
|
1001
1069
|
"aspectRatio": ratio,
|
|
1002
1070
|
"resolution": resolution,
|
|
1003
1071
|
"fps": 30,
|
|
1004
|
-
|
|
1005
|
-
|
|
1072
|
+
# 无配音模板:关闭 CC 字幕(字幕段是 narration→TTS 的派生物),并省略
|
|
1073
|
+
# global.narration,避免下游误判存在旁白。
|
|
1074
|
+
"subtitle": {"enabled": needs_narration, "style": "bottom"},
|
|
1075
|
+
**({"narration": {"voiceId": resolved_voice_id, "speed": 1.0}} if needs_narration else {}),
|
|
1006
1076
|
"bgm": {"enabled": True, "volume": 0.12},
|
|
1007
1077
|
},
|
|
1008
1078
|
"assets": assets,
|
|
@@ -1075,9 +1145,9 @@ Examples:
|
|
|
1075
1145
|
default=None,
|
|
1076
1146
|
help="Media URL for the template's carousel/gallery. Can be repeated: "
|
|
1077
1147
|
"--carousel-items url1 --carousel-items url2. "
|
|
1078
|
-
"For templates
|
|
1079
|
-
"are placed directly into
|
|
1080
|
-
"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.",
|
|
1081
1151
|
)
|
|
1082
1152
|
parser.add_argument(
|
|
1083
1153
|
"--caption-lines",
|
|
@@ -1085,8 +1155,9 @@ Examples:
|
|
|
1085
1155
|
default=None,
|
|
1086
1156
|
help="Bottom typewriter text line. Can be repeated: "
|
|
1087
1157
|
"--caption-lines 'line1' --caption-lines 'line2'. "
|
|
1088
|
-
"For templates
|
|
1089
|
-
"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.",
|
|
1090
1161
|
)
|
|
1091
1162
|
|
|
1092
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
|
}
|
|
@@ -19,7 +19,7 @@ triggers:
|
|
|
19
19
|
|
|
20
20
|
Wraps ab-api's `POST /model/genVideo` (the same endpoint the web "Lingchuang AI Video" tool uses), authenticated with the **Tianyan privateToken**, routed through LiteLLM to **Seedance** or **Veo**. Generation is async — the handler submits the task and polls `/model/getVideoStatus` until completion.
|
|
21
21
|
|
|
22
|
-
> This skill was migrated from a Python script to an
|
|
22
|
+
> This skill was migrated from a Python script to an remixmate CLI HTTP handler (`entry.type: http`). The agent invocation is unchanged (same tool name `gen_video`, same params as in `skill.json`); local repro goes through `remixmate gen-video ...`.
|
|
23
23
|
|
|
24
24
|
## Models
|
|
25
25
|
|
|
@@ -72,7 +72,7 @@ No skill-local env file — the executing process inherits the system environmen
|
|
|
72
72
|
### Seedance text-to-video (default model)
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
|
-
|
|
75
|
+
remixmate gen-video \
|
|
76
76
|
--prompt "<video description>" \
|
|
77
77
|
--duration 5 \
|
|
78
78
|
--ratio "16:9"
|
|
@@ -83,7 +83,7 @@ ab-skill gen-video \
|
|
|
83
83
|
Frame images accept local file paths, HTTPS URLs, or data URIs (local files are base64-encoded into a data URI).
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
|
-
|
|
86
|
+
remixmate gen-video \
|
|
87
87
|
--prompt "<transition description>" \
|
|
88
88
|
--first-frame ./start.png \
|
|
89
89
|
--last-frame ./end.png \
|
|
@@ -94,7 +94,7 @@ ab-skill gen-video \
|
|
|
94
94
|
### Veo 3.1 high-resolution
|
|
95
95
|
|
|
96
96
|
```bash
|
|
97
|
-
|
|
97
|
+
remixmate gen-video \
|
|
98
98
|
--model veo \
|
|
99
99
|
--prompt "<video description>" \
|
|
100
100
|
--duration 8 \
|
|
@@ -105,7 +105,7 @@ ab-skill gen-video \
|
|
|
105
105
|
### Veo 3.1 Fast for rapid iteration
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
|
-
|
|
108
|
+
remixmate gen-video \
|
|
109
109
|
--model veo-fast \
|
|
110
110
|
--prompt "<video description>" \
|
|
111
111
|
--duration 6
|
|
@@ -116,7 +116,7 @@ ab-skill gen-video \
|
|
|
116
116
|
Pass `--reference` multiple times (Veo only, up to 3).
|
|
117
117
|
|
|
118
118
|
```bash
|
|
119
|
-
|
|
119
|
+
remixmate gen-video \
|
|
120
120
|
--model veo \
|
|
121
121
|
--prompt "<video description>" \
|
|
122
122
|
--reference ./ref1.png \
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "gen-video",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "339",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V8",
|
|
6
6
|
"skillDescription": "AI 生视频技能,根据文字描述生成素材视频(调用 ab-api /model/genVideo,支持 Seedance 与 Veo)。\n\n当用户提到以下任何需求时,立即使用本 skill:\n- AI 生视频、文生视频、文字生成视频、生成一段视频、AI 制作视频\n- 使用 doubao / 豆包 / seedance、Veo、Google 等生成视频\n- 用户提供视频提示词并希望生成视频\n- 图生视频、首帧生成视频、参考图生成视频\n\n即使用户没有明确说「使用 AI」,只要他们想要根据描述生成视频,也要使用本 skill。"
|
|
7
7
|
}
|
|
@@ -34,7 +34,7 @@ There is no skill-local env file — the executing process inherits the system e
|
|
|
34
34
|
|
|
35
35
|
## Operations
|
|
36
36
|
|
|
37
|
-
> This skill was migrated from a Python script to an
|
|
37
|
+
> This skill was migrated from a Python script to an remixmate CLI HTTP handler (`entry.type: http`). The agent invocation is unchanged (same tool name `gen_voice`, same params as in `skill.json`); local repro goes through `remixmate gen-voice ...`. The legacy `--download` flag has been removed — audio URLs are persisted in the cloud and play directly.
|
|
38
38
|
|
|
39
39
|
1. **Text**: confirm what to synthesize. Punctuation drives pauses (commas short, periods long).
|
|
40
40
|
2. **Voice**: if the user prefers a specific voice, run `--list-voices` first and pick a matching id.
|
|
@@ -43,13 +43,13 @@ There is no skill-local env file — the executing process inherits the system e
|
|
|
43
43
|
### List available voices
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
|
|
46
|
+
remixmate gen-voice --list-voices
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
To inspect the local language-tagged fallback catalog used by the voice resolver (no remote API call), add `--local`:
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
|
-
|
|
52
|
+
remixmate gen-voice --list-voices --local
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
The local catalog prints one voice per line as `<voice-id>\t<lang>\t<display-name>`.
|
|
@@ -57,13 +57,13 @@ The local catalog prints one voice per line as `<voice-id>\t<lang>\t<display-nam
|
|
|
57
57
|
### Default synthesis (URL output)
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
|
|
60
|
+
remixmate gen-voice --text "<text-to-synthesize>"
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
### With voice + speed
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
|
|
66
|
+
remixmate gen-voice \
|
|
67
67
|
--text "<text-to-synthesize>" \
|
|
68
68
|
--voice-id "female-shaonv" \
|
|
69
69
|
--speed 1.2
|
|
@@ -72,7 +72,7 @@ ab-skill gen-voice \
|
|
|
72
72
|
### JSON output (with subtitle timestamps)
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
|
-
|
|
75
|
+
remixmate gen-voice --text "<text-to-synthesize>" --json-output
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
## Common CLI flags
|