@remixmate/cli 0.9.72 → 0.9.74
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/LICENSE +45 -21
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/argv.js +1 -51
- package/dist/auth/auth-lock.js +1 -100
- package/dist/auth/auto-login.js +7 -75
- package/dist/auth/commands.js +40 -290
- package/dist/auth/credential-store.js +1 -143
- package/dist/auth/device-flow-runner.js +13 -110
- package/dist/auth/device-flow.js +1 -132
- package/dist/auth/ensure.js +1 -62
- package/dist/auth/environment.js +1 -69
- package/dist/auth/pending-store.js +1 -45
- package/dist/auth/resolve.js +1 -44
- package/dist/billing.js +3 -76
- package/dist/capabilities.js +1 -75
- package/dist/cli.js +39 -211
- package/dist/diagnostics.js +2 -70
- package/dist/doctor.js +9 -188
- package/dist/errors.js +1 -33
- package/dist/exec.js +4 -47
- package/dist/handlers/gen-digital-human.js +33 -264
- package/dist/handlers/gen-image.js +9 -113
- package/dist/handlers/gen-post.js +15 -192
- package/dist/handlers/gen-video.js +7 -185
- package/dist/handlers/gen-voice.js +19 -133
- package/dist/handlers/index.js +1 -20
- package/dist/handlers/shared.js +1 -66
- package/dist/handlers/template-init.js +1 -21
- package/dist/http.js +3 -155
- package/dist/index.js +1 -8
- package/dist/install.js +40 -448
- package/dist/manifest.json +44 -3
- package/dist/progress.js +2 -15
- package/dist/project/commands.js +34 -156
- package/dist/project/host.js +1 -151
- package/dist/project/resolve.js +1 -65
- package/dist/project/store.js +2 -97
- package/dist/project/take.js +1 -106
- package/dist/project/trace.js +5 -278
- package/dist/registry.js +3 -70
- package/dist/runner.js +7 -225
- package/dist/skill-schema.js +1 -194
- package/dist/template/args.js +1 -69
- package/dist/template/check.js +2 -30
- package/dist/template/commands.js +34 -163
- package/dist/template/constants.js +1 -46
- package/dist/template/init.js +5 -133
- package/dist/template/list.js +1 -46
- package/dist/template/preview.js +7 -318
- package/dist/template/proc.js +1 -43
- package/dist/template/project.js +6 -161
- package/dist/template/publish.js +5 -185
- package/dist/template/render-client.js +2 -79
- package/dist/template/upgrade.js +4 -123
- package/dist/text.js +1 -15
- package/package.json +2 -35
- package/skills/doc-parse/scripts/extract_via_render.py +32 -32
- package/skills/export-jianying/scripts/gen_jianying_draft.py +188 -188
- package/skills/gen-script/SKILL.md +23 -1
- package/skills/gen-script/scripts/gen_script.py +644 -551
- package/skills/gen-script/skill.json +12 -0
- package/skills/gen-script/version.json +1 -1
- package/skills/prepare-video-assets/scripts/prepare_video_assets.py +21 -21
- package/skills/render-video/scripts/_chrome_vendor.py +17 -17
- package/skills/render-video/scripts/_digital_human_segments.py +34 -34
- package/skills/render-video/scripts/_video_probe.py +33 -33
- package/skills/render-video/scripts/_vod_polling.py +13 -13
- package/skills/render-video/scripts/remote_renderer_client.py +42 -42
- package/skills/render-video/scripts/render_video.py +639 -639
- package/skills/render-video/scripts/required_props.py +37 -37
- package/skills/render-video/scripts/upload_video.py +35 -35
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/SKILL.md +16 -0
- package/skills/template-registry/scripts/countdown_spec.py +92 -92
- package/skills/template-registry/scripts/doc_extract.py +40 -40
- package/skills/template-registry/scripts/list_templates.py +303 -191
- package/skills/template-registry/scripts/log_diagnostics.py +1 -1
- package/skills/template-registry/scripts/match_template.py +87 -87
- package/skills/template-registry/scripts/payload_hydrator.py +89 -89
- package/skills/template-registry/scripts/registry_loader.py +159 -159
- package/skills/template-registry/scripts/render_job_client.py +75 -75
- package/skills/template-registry/scripts/showcase_loader.py +44 -44
- package/skills/template-registry/skill.json +4 -1
- package/skills/template-registry/version.json +1 -1
- package/skills/template-registry/video_dsl/runtime/__init__.py +8 -8
- package/skills/template-registry/video_dsl/runtime/dsl_validator.py +329 -329
- package/skills/template-registry/video_dsl/runtime/narration_budget.py +36 -36
- package/skills/template-registry/video_dsl/runtime/payload_contract.py +7 -7
- package/skills/template-registry/video_dsl/runtime/prompt_enhancer.py +98 -98
- package/skills/template-registry/video_dsl/runtime/timeline_compiler.py +127 -127
- package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +14 -1
- package/skills/video-parser/scripts/analyze_video.py +173 -173
- package/skills/video-parser/scripts/deconstruct_video.py +91 -91
- package/skills/video-parser/scripts/parse_via_render.py +21 -21
- package/skills/web-screenshot/scripts/_media_screenshot/__init__.py +4 -4
- package/skills/web-screenshot/scripts/_media_screenshot/bootstrap.py +7 -7
- package/skills/web-screenshot/scripts/_media_screenshot/browser.py +4 -4
- package/skills/web-screenshot/scripts/_media_screenshot/cli_args.py +4 -4
- package/skills/web-screenshot/scripts/_media_screenshot/js_loader.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/overlay.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/reader.py +40 -40
- package/skills/web-screenshot/scripts/_media_screenshot/recording.py +11 -11
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/__init__.py +8 -8
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/arrow.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/caption.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/click.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/code_lines.py +5 -5
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/focus.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/highlight.py +3 -3
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/hover.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/redact.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/scroll.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/sequence.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/title_card.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/type.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/wait.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/zoom.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/screenshot.py +15 -15
- package/skills/web-screenshot/scripts/_media_screenshot/storyboard.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/template.py +11 -11
- package/skills/web-screenshot/scripts/_media_screenshot/trim.py +10 -10
- package/skills/web-screenshot/scripts/_media_screenshot/urlguard.py +22 -22
- package/skills/web-screenshot/scripts/_media_upload.py +33 -33
- package/skills/web-screenshot/scripts/read_page.py +15 -15
- package/skills/web-screenshot/scripts/record.py +26 -26
- package/skills/web-screenshot/scripts/screenshot.py +5 -5
- package/dist/argv.d.ts +0 -15
- package/dist/auth/auth-lock.d.ts +0 -26
- package/dist/auth/auto-login.d.ts +0 -20
- package/dist/auth/commands.d.ts +0 -8
- package/dist/auth/credential-store.d.ts +0 -60
- package/dist/auth/device-flow-runner.d.ts +0 -58
- package/dist/auth/device-flow.d.ts +0 -52
- package/dist/auth/ensure.d.ts +0 -55
- package/dist/auth/environment.d.ts +0 -38
- package/dist/auth/pending-store.d.ts +0 -33
- package/dist/auth/resolve.d.ts +0 -30
- package/dist/billing.d.ts +0 -44
- package/dist/capabilities.d.ts +0 -63
- package/dist/cli.d.ts +0 -13
- package/dist/diagnostics.d.ts +0 -24
- package/dist/doctor.d.ts +0 -22
- package/dist/errors.d.ts +0 -29
- package/dist/exec.d.ts +0 -18
- package/dist/handlers/gen-digital-human.d.ts +0 -17
- package/dist/handlers/gen-image.d.ts +0 -20
- package/dist/handlers/gen-post.d.ts +0 -22
- package/dist/handlers/gen-video.d.ts +0 -19
- package/dist/handlers/gen-voice.d.ts +0 -26
- package/dist/handlers/index.d.ts +0 -20
- package/dist/handlers/shared.d.ts +0 -26
- package/dist/handlers/template-init.d.ts +0 -10
- package/dist/http.d.ts +0 -72
- package/dist/index.d.ts +0 -9
- package/dist/install.d.ts +0 -71
- package/dist/progress.d.ts +0 -23
- package/dist/project/commands.d.ts +0 -19
- package/dist/project/host.d.ts +0 -91
- package/dist/project/resolve.d.ts +0 -43
- package/dist/project/store.d.ts +0 -31
- package/dist/project/take.d.ts +0 -50
- package/dist/project/trace.d.ts +0 -98
- package/dist/registry.d.ts +0 -30
- package/dist/runner.d.ts +0 -39
- package/dist/skill-schema.d.ts +0 -138
- package/dist/template/args.d.ts +0 -25
- package/dist/template/check.d.ts +0 -23
- package/dist/template/commands.d.ts +0 -25
- package/dist/template/constants.d.ts +0 -39
- package/dist/template/init.d.ts +0 -28
- package/dist/template/list.d.ts +0 -23
- package/dist/template/preview.d.ts +0 -60
- package/dist/template/proc.d.ts +0 -26
- package/dist/template/project.d.ts +0 -47
- package/dist/template/publish.d.ts +0 -43
- package/dist/template/render-client.d.ts +0 -39
- package/dist/template/upgrade.d.ts +0 -44
- package/dist/text.d.ts +0 -8
|
@@ -56,6 +56,16 @@
|
|
|
56
56
|
"type": "string",
|
|
57
57
|
"description": "Template id (from template_registry). The template owns outputLanguage and may also declare a defaultVoiceId; both flow into the produced DSL."
|
|
58
58
|
},
|
|
59
|
+
"template_source": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"enum": ["user-explicit", "user-confirmed", "auto"],
|
|
62
|
+
"description": "How template_id was chosen; recorded at meta.templateSelection.source. **Always pass it with template_id.** `user-explicit`: the user named this template — its id or name, a pasted template detail-page prompt or link, or it was settled earlier in this conversation. `user-confirmed`: the user did not name one, you offered candidates and the user picked or approved this one in the chat. `auto`: you picked it yourself and the user has not confirmed — gen_script refuses, so instead of calling it, show the candidates and end the turn. Never mark a template user-confirmed that the user has not seen and approved."
|
|
63
|
+
},
|
|
64
|
+
"template_candidates": {
|
|
65
|
+
"type": "array",
|
|
66
|
+
"items": { "type": "string" },
|
|
67
|
+
"description": "The template ids you offered the user, recommended first; recorded at meta.templateSelection.candidates. Pass with template_source=user-confirmed — template_id must then be one of them (a template the user named outside the shortlist is user-explicit)."
|
|
68
|
+
},
|
|
59
69
|
"source_document_url": {
|
|
60
70
|
"type": "string",
|
|
61
71
|
"description": "Source document URL (.pptx) for a template that builds its per-scene picture from that document (capabilities.payloadHydration). **Must** pass it for such a template: it is recorded at meta.sourceDocument, and prepare_video_assets then fetches the layout itself and fills the customPayload field the template declares (payloadHydration.payloadField) on every scene. Pass the same URL you gave doc_parse. You are NOT expected to author or even read that layout data — one page is hundreds of KB, so it never travels through the conversation; without this reference the DSL is rejected downstream after you have already written all the narration."
|
|
@@ -128,6 +138,8 @@
|
|
|
128
138
|
"variant",
|
|
129
139
|
"background_preset",
|
|
130
140
|
"template_id",
|
|
141
|
+
"template_source",
|
|
142
|
+
"template_candidates",
|
|
131
143
|
"source_document_url",
|
|
132
144
|
"headline",
|
|
133
145
|
"subheadline",
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "gen-script",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "474",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V19",
|
|
6
6
|
"skillDescription": "Video-script generation skill. Turns a user-supplied topic into a structured Video DSL (JSON) that describes the full video — scene structure, asset requirements, and narrative flow.\n\nUse this skill as soon as the user mentions any of these intents:\n- Write a video script, generate a video script, plan a video, write storyboards\n- Create a short video, plan video content, generate a Video DSL\n- Turn a topic into a video structure / video content plan\n\nEven when the user does not say \"generate the DSL\", use this skill whenever they want to turn a topic into a structured video plan.\n\n⚠️ Stop-and-confirm gate: after this skill returns a DSL, show the full script and wait for the user's explicit confirmation. Never call `prepare_video_assets` in the same turn."
|
|
7
7
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Thin wrapper that delegates to the canonical render_video.py with --resolve-only forced on.
|
|
3
2
|
|
|
4
|
-
The two-phase video pipeline shares one Python implementation:
|
|
5
3
|
|
|
6
|
-
prepare_video_assets → render_video.py --resolve-only ... (this file)
|
|
7
|
-
render_video → render_video.py ... (sibling skill)
|
|
8
4
|
|
|
9
|
-
By exec'ing the canonical script we get verbatim stdout / stderr / exit-code / signal forwarding
|
|
10
|
-
without any subprocess wrapper layer. The agent layer's stdout parsing for the
|
|
11
|
-
"📦 render job jobId: N" line therefore continues to work unchanged.
|
|
12
5
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
21
|
import os
|
|
@@ -23,14 +23,14 @@ import sys
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def _canonical_script_path() -> str:
|
|
26
|
-
"""Resolve the absolute path to the render-video skill's render_video.py.
|
|
27
26
|
|
|
28
|
-
Layout (relative to this file):
|
|
29
|
-
apps/ab-skill/skills/prepare-video-assets/scripts/prepare_video_assets.py <-- HERE
|
|
30
|
-
apps/ab-skill/skills/render-video/scripts/render_video.py <-- target
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
34
|
here = os.path.dirname(os.path.abspath(__file__))
|
|
35
35
|
return os.path.normpath(
|
|
36
36
|
os.path.join(here, "..", "..", "render-video", "scripts", "render_video.py")
|
|
@@ -40,7 +40,7 @@ def _canonical_script_path() -> str:
|
|
|
40
40
|
def main() -> None:
|
|
41
41
|
script = _canonical_script_path()
|
|
42
42
|
if not os.path.isfile(script):
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
print(
|
|
45
45
|
f"❌ prepare_video_assets wrapper could not locate the canonical render script.\n"
|
|
46
46
|
f" Expected at: {script}\n"
|
|
@@ -48,7 +48,7 @@ def main() -> None:
|
|
|
48
48
|
file=sys.stderr,
|
|
49
49
|
)
|
|
50
50
|
sys.exit(2)
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
forwarded = ["--resolve-only", *sys.argv[1:]]
|
|
53
53
|
os.execv(sys.executable, [sys.executable, script, *forwarded])
|
|
54
54
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"""Chrome Headless vendor sync — separated from render_video.py.
|
|
2
1
|
|
|
3
|
-
把 Chrome 下载/解压/共享缓存这块"部署/运维相关"的逻辑收拢到一个文件,让
|
|
4
|
-
render_video.py 只剩"调度 + 渲染"。日后如果要把这块搬到 ab-render Dockerfile
|
|
5
|
-
或独立 prepare 脚本,只需要换掉这个模块的实现,render_video 不用动。
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import builtins
|
|
@@ -13,7 +13,7 @@ import json
|
|
|
13
13
|
import os
|
|
14
14
|
import platform
|
|
15
15
|
import shutil
|
|
16
|
-
import subprocess
|
|
16
|
+
import subprocess
|
|
17
17
|
import sys
|
|
18
18
|
import tempfile
|
|
19
19
|
import urllib.error
|
|
@@ -29,14 +29,14 @@ def _now_iso() -> str:
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def _log(msg: str) -> None:
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
now = datetime.now()
|
|
34
34
|
stamp = now.strftime("%Y%m%d %H%M%S") + f":{now.microsecond // 1000:03d}"
|
|
35
35
|
builtins.print(f"[{stamp}] {msg}", file=sys.stderr)
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def _detect_remotion_chrome_platform() -> Optional[str]:
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
system = platform.system()
|
|
41
41
|
machine = platform.machine().lower()
|
|
42
42
|
if system == "Darwin":
|
|
@@ -118,7 +118,7 @@ def _chrome_headless_share_zip_path(share_root: str, rem_platform: str) -> str:
|
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
def _share_chrome_zip_ready(share_root: str, rem_platform: str, expected_version: str) -> bool:
|
|
121
|
-
|
|
121
|
+
|
|
122
122
|
version_path = os.path.join(share_root, "VERSION")
|
|
123
123
|
if not os.path.isfile(version_path):
|
|
124
124
|
return False
|
|
@@ -136,7 +136,7 @@ def _share_chrome_zip_ready(share_root: str, rem_platform: str, expected_version
|
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
def _extract_chrome_headless_zip_to_dest(zip_path: str, dest_root: str, rem_platform: str) -> None:
|
|
139
|
-
|
|
139
|
+
|
|
140
140
|
platform_dest = os.path.join(dest_root, rem_platform)
|
|
141
141
|
if os.path.isdir(platform_dest):
|
|
142
142
|
shutil.rmtree(platform_dest)
|
|
@@ -146,13 +146,13 @@ def _extract_chrome_headless_zip_to_dest(zip_path: str, dest_root: str, rem_plat
|
|
|
146
146
|
|
|
147
147
|
|
|
148
148
|
def sync_chrome_headless_vendor(renderer_dir: str, render_plan: dict) -> None:
|
|
149
|
-
"""
|
|
150
|
-
共享目录(默认 /tmp/agent-share/chrome-headless)只存 VERSION + chrome-headless-shell-<平台>.zip;
|
|
151
|
-
若 zip 与版本匹配则本地解压到 node_modules/.remotion/chrome-headless-shell/,否则从 OSS 下载,
|
|
152
|
-
成功后把 zip 写入共享目录(不存解压后的目录树)。
|
|
153
149
|
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
156
|
if os.environ.get("REMOTION_SKIP_VENDOR_CHROME", "").strip().lower() in ("1", "true", "yes"):
|
|
157
157
|
render_plan["logs"].append({
|
|
158
158
|
"phase": "render",
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
"""数字人连续段:分段、驱动音频规划、起播帧注入。
|
|
2
1
|
|
|
3
|
-
一个 ``source=gen-digital-human`` 的 avatar 资产被**相邻**几个场景的
|
|
4
|
-
``props.avatarAssetId`` 同时引用 = 一个连续段:生成一段视频,人物跨场景不重启。
|
|
5
|
-
要让第 k 个场景的第 0 帧正好对上视频里的对应位置,驱动音频必须是「段内各场景
|
|
6
|
-
旁白、每段补静音到场景最终时长」拼成的一条——所以数字人只能在**排轴之后**生成
|
|
7
|
-
(场景时长由真实 TTS 时长决定,见 render_video.adjust_timeline_to_audio)。
|
|
8
2
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
@@ -21,10 +21,10 @@ from typing import Optional
|
|
|
21
21
|
|
|
22
22
|
DIGITAL_HUMAN_SOURCE = "gen-digital-human"
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
MIN_DRIVE_SEC = 5.0
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
_SAMPLE_RATE = 44100
|
|
29
29
|
|
|
30
30
|
|
|
@@ -42,11 +42,11 @@ def digital_human_asset_ids(render_plan: dict) -> set[str]:
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
def collect_segments(render_plan: dict) -> dict[str, list[int]]:
|
|
45
|
-
"""avatar 资产 id → 引用它的 timeline 下标(按出场顺序)。
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
50
|
dh_ids = digital_human_asset_ids(render_plan)
|
|
51
51
|
segments: dict[str, list[int]] = {}
|
|
52
52
|
for idx, entry in enumerate(render_plan.get("timeline", [])):
|
|
@@ -69,11 +69,11 @@ def segment_role(pos: int, count: int) -> str:
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def inject_segment_props(render_plan: dict, segments: dict[str, list[int]]) -> list[str]:
|
|
72
|
-
"""给段内每个场景写 ``avatarOffsetFrames`` / ``avatarSegmentRole``。
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
77
|
warnings: list[str] = []
|
|
78
78
|
timeline = render_plan.get("timeline", [])
|
|
79
79
|
for asset_id, indices in segments.items():
|
|
@@ -94,11 +94,11 @@ def inject_segment_props(render_plan: dict, segments: dict[str, list[int]]) -> l
|
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
def plan_drive_pieces(render_plan: dict, indices: list[int]) -> list[dict]:
|
|
97
|
-
"""段内每个场景一块:``{"sceneId", "url" | None, "durationSec"}``。
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
102
|
fps = (render_plan.get("renderConfig") or {}).get("fps") or 30
|
|
103
103
|
assets = {a.get("assetId"): a for a in render_plan.get("resolvedAssets", [])}
|
|
104
104
|
timeline = render_plan.get("timeline", [])
|
|
@@ -119,10 +119,10 @@ def plan_drive_pieces(render_plan: dict, indices: list[int]) -> list[dict]:
|
|
|
119
119
|
|
|
120
120
|
|
|
121
121
|
def direct_drive_url(render_plan: dict, indices: list[int]) -> Optional[str]:
|
|
122
|
-
"""单场景段、且旁白本身就够长时,直接拿旁白音频驱动,不必拼接。
|
|
123
122
|
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
126
|
if len(indices) != 1:
|
|
127
127
|
return None
|
|
128
128
|
entry = render_plan.get("timeline", [])[indices[0]]
|
|
@@ -137,11 +137,11 @@ def direct_drive_url(render_plan: dict, indices: list[int]) -> Optional[str]:
|
|
|
137
137
|
|
|
138
138
|
|
|
139
139
|
def split_segment_per_scene(render_plan: dict, asset_id: str, indices: list[int]) -> list[str]:
|
|
140
|
-
"""把一个连续段拆成每场景一段(拿不到 ffmpeg 拼接时的降级)。
|
|
141
140
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
145
|
assets = render_plan.get("resolvedAssets", [])
|
|
146
146
|
original = next((a for a in assets if a.get("assetId") == asset_id), None)
|
|
147
147
|
if original is None:
|
|
@@ -160,7 +160,7 @@ def split_segment_per_scene(render_plan: dict, asset_id: str, indices: list[int]
|
|
|
160
160
|
|
|
161
161
|
|
|
162
162
|
def build_concat_command(pieces: list[dict], out_path: str, ffmpeg: str = "ffmpeg") -> list[str]:
|
|
163
|
-
|
|
163
|
+
|
|
164
164
|
cmd = [ffmpeg, "-y", "-loglevel", "error"]
|
|
165
165
|
filters: list[str] = []
|
|
166
166
|
labels: list[str] = []
|
|
@@ -190,7 +190,7 @@ def find_ffmpeg() -> Optional[str]:
|
|
|
190
190
|
|
|
191
191
|
|
|
192
192
|
def concat_drive_audio(pieces: list[dict], out_dir: str, name: str, ffmpeg: str, timeout: int = 300) -> str:
|
|
193
|
-
|
|
193
|
+
|
|
194
194
|
os.makedirs(out_dir, exist_ok=True)
|
|
195
195
|
safe = "".join(ch if ch.isalnum() or ch in "-_." else "_" for ch in name)
|
|
196
196
|
out_path = os.path.join(out_dir, f"dh-drive-{safe}.mp3")
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
"""Video duration probe — separated from render_video.py.
|
|
2
1
|
|
|
3
|
-
screen-walkthrough 这种"按 TTS 时间轴 + 视频素材池"的模板需要知道远端视频的
|
|
4
|
-
真实时长才能选出正确的 adaptStrategy(fit-exact / trim-and-speed / freeze-tail
|
|
5
|
-
/ static-fallback)。这里给出三条策略,按速度排序:
|
|
6
2
|
|
|
7
|
-
1. 通过 mediaId(从 CDN URL 中切出)查 ab-api /file/get
|
|
8
|
-
2. HEAD 请求按 Content-Length 估算(粗略)
|
|
9
|
-
3. 下载前 2 MB 用 ffprobe 解析(精确但贵)
|
|
10
3
|
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import builtins
|
|
@@ -31,12 +31,12 @@ def _log(msg: str) -> None:
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def _extract_media_id_from_url(url: str) -> Optional[str]:
|
|
34
|
-
"""Extract mediaId (vodVideoId) from CDN URL.
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
40
|
try:
|
|
41
41
|
from urllib.parse import urlparse
|
|
42
42
|
parsed = urlparse(url)
|
|
@@ -51,10 +51,10 @@ def _extract_media_id_from_url(url: str) -> Optional[str]:
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def _query_duration_from_api(url: str) -> Optional[float]:
|
|
54
|
-
"""Query ab-api /file/get by mediaId to get the precise duration from database.
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
58
|
media_id = _extract_media_id_from_url(url)
|
|
59
59
|
if not media_id:
|
|
60
60
|
return None
|
|
@@ -99,15 +99,15 @@ def _query_duration_from_api(url: str) -> Optional[float]:
|
|
|
99
99
|
|
|
100
100
|
|
|
101
101
|
def probe_video_duration(url: str) -> Optional[float]:
|
|
102
|
-
"""Probe the duration (in seconds) of a remote video URL.
|
|
103
102
|
|
|
104
|
-
Strategy (ordered by priority):
|
|
105
|
-
1. Query ab-api /file/get by mediaId extracted from CDN URL (fastest, most accurate).
|
|
106
|
-
2. HEAD request to estimate from Content-Length.
|
|
107
|
-
3. Download first 2MB + ffprobe (if available).
|
|
108
103
|
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
111
|
api_duration = _query_duration_from_api(url)
|
|
112
112
|
if api_duration is not None:
|
|
113
113
|
return api_duration
|
|
@@ -178,16 +178,16 @@ def probe_video_duration(url: str) -> Optional[float]:
|
|
|
178
178
|
|
|
179
179
|
|
|
180
180
|
def probe_video_meta(url: str, timeout: float = 20.0) -> dict:
|
|
181
|
-
"""Probe a remote video's pixel size and duration with ffprobe (reads the URL directly).
|
|
182
181
|
|
|
183
|
-
Returns ``{"width": int, "height": int, "duration_ms": int}`` with only the fields it
|
|
184
|
-
could read; ``{}`` when ffprobe is missing or the probe fails. Callers treat every
|
|
185
|
-
field as optional.
|
|
186
182
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
191
|
try:
|
|
192
192
|
result = subprocess.run(
|
|
193
193
|
[
|
|
@@ -212,7 +212,7 @@ def probe_video_meta(url: str, timeout: float = 20.0) -> dict:
|
|
|
212
212
|
s = streams[0]
|
|
213
213
|
w, h = s.get("width"), s.get("height")
|
|
214
214
|
if isinstance(w, int) and isinstance(h, int) and w > 0 and h > 0:
|
|
215
|
-
|
|
215
|
+
|
|
216
216
|
rotation = 0
|
|
217
217
|
for side in s.get("side_data_list") or []:
|
|
218
218
|
if isinstance(side.get("rotation"), (int, float)):
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
"""VOD playback URL polling — separated from render_video.py.
|
|
2
1
|
|
|
3
|
-
ab-render 通过远程渲染服务把视频上传到阿里云 VOD 后,会立即返回一个
|
|
4
|
-
`vod://` 协议地址 + fileId。但前端需要的是 CDN 加速地址(`http://cdn.aibearer.com/...`),
|
|
5
|
-
这要等阿里云转码回调到达 ab-api、回写 `file.fileUrl` 之后才能拿到。
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import builtins
|
|
@@ -33,13 +33,13 @@ def poll_vod_playback_url(
|
|
|
33
33
|
max_wait: float = 60.0,
|
|
34
34
|
interval: float = 3.0,
|
|
35
35
|
) -> Optional[str]:
|
|
36
|
-
"""轮询 ab-api /file/get,等待 VOD CDN URL 就绪。
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
43
|
api_base = os.environ.get(
|
|
44
44
|
"MM_API_BASE_URL",
|
|
45
45
|
os.environ.get("MM_BACKEND_API_URL", "https://api.remixmate.ai/api"),
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
26
|
|
|
27
27
|
from __future__ import annotations
|
|
28
28
|
|
|
@@ -61,16 +61,16 @@ TERMINAL_FAILURE = {STATUS_FAILED, STATUS_CANCELLED}
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
class RemoteRenderError(RuntimeError):
|
|
64
|
-
|
|
64
|
+
pass
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
class RemoteRenderTimeout(RemoteRenderError):
|
|
68
|
-
"""轮询超时 —— 任务**没有失败**,只是我们不等了。
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
74
|
|
|
75
75
|
def __init__(self, message: str, task_id: str):
|
|
76
76
|
super().__init__(message)
|
|
@@ -144,7 +144,7 @@ def start_render(
|
|
|
144
144
|
conversation_id: Optional[str] = None,
|
|
145
145
|
path: str = "/render",
|
|
146
146
|
) -> str:
|
|
147
|
-
|
|
147
|
+
|
|
148
148
|
result = _request(
|
|
149
149
|
"POST",
|
|
150
150
|
path,
|
|
@@ -174,16 +174,16 @@ def poll_render(
|
|
|
174
174
|
adaptive_interval: bool = False,
|
|
175
175
|
status_path: str = "/renderStatus",
|
|
176
176
|
) -> dict:
|
|
177
|
-
"""POST <status_path>(默认 /renderStatus,私有模板传 /renderDraftStatus)轮询直到完成/失败/超时。
|
|
178
177
|
|
|
179
|
-
成功时返回 data dict(至少包含 fileUrl),否则抛 RemoteRenderError。
|
|
180
|
-
on_progress 回调在每次成功请求后触发,参数为完整 data dict。
|
|
181
178
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
187
|
start = time.monotonic()
|
|
188
188
|
consecutive_errors = 0
|
|
189
189
|
current_progress = 0.0
|
|
@@ -233,7 +233,7 @@ def poll_render(
|
|
|
233
233
|
if on_progress is not None:
|
|
234
234
|
try:
|
|
235
235
|
on_progress(data)
|
|
236
|
-
except Exception:
|
|
236
|
+
except Exception:
|
|
237
237
|
pass
|
|
238
238
|
|
|
239
239
|
status = str(data.get("status") or "").lower()
|
|
@@ -243,11 +243,11 @@ def poll_render(
|
|
|
243
243
|
err = data.get("error") or data.get("errorMsg") or data.get("message") or "unknown"
|
|
244
244
|
raise RemoteRenderError(f"remote render failed ({status}): {err}")
|
|
245
245
|
|
|
246
|
-
|
|
246
|
+
|
|
247
247
|
current_progress = float(data.get("progress") or 0.0)
|
|
248
248
|
if adaptive_interval:
|
|
249
249
|
if current_progress < 0.2:
|
|
250
|
-
|
|
250
|
+
|
|
251
251
|
sleep_time = max(interval * 0.5, 1.0)
|
|
252
252
|
elif current_progress >= 0.8:
|
|
253
253
|
sleep_time = max(interval * 0.6, 1.0)
|