@remixmate/cli 0.9.60 → 0.9.62
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
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.62",
|
|
4
4
|
"description": "AI media generation skills for Claude Code / Codex — 12 skills covering image, video, voice, digital human, web screenshot, web recording, script, template registry, rendering, Jianying export, and video deconstruction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "prepare-video-assets",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "476",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V7",
|
|
6
6
|
"skillDescription": "Asset-preparation skill: resolves and generates every asset (image / audio / video) referenced by a Video DSL, persists a RenderPlan to the database, and returns a `job_id` for the subsequent `render_video` call.\n\nUse this skill as soon as the user mentions any of these intents:\n- Generate / prepare video assets, resolve assets, render-ready\n- \"Make me a video about X\" (the agent calls gen_script → prepare_video_assets → render_video)\n- Regenerate one asset (image / audio) for a specific scene\n\nNext step: after the user confirms the resolved assets, call `render_video` with the `job_id` returned by this skill.\n\n⚠️ Stop-and-confirm gate: this skill runs only after the user has confirmed the script, and after it returns you must show the resolved assets and wait for the user's explicit confirmation. Never call `render_video` in the same turn."
|
|
7
7
|
}
|
|
@@ -175,3 +175,58 @@ def probe_video_duration(url: str) -> Optional[float]:
|
|
|
175
175
|
return content_length_estimate
|
|
176
176
|
|
|
177
177
|
return None
|
|
178
|
+
|
|
179
|
+
|
|
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
|
+
|
|
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
|
+
|
|
187
|
+
为什么要:数字人盒子的宽高比缺省按 3:4 算,而克隆素材多是 9:16 左右的竖版
|
|
188
|
+
(实测 720×1264)。资产上没有宽高时,模板用 3:4 的盒子 object-fit: cover 裁掉
|
|
189
|
+
人物的头顶和下半身(conv_XKTQxEBLdZy3i_jIB7IG)。
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
result = subprocess.run(
|
|
193
|
+
[
|
|
194
|
+
"ffprobe", "-v", "quiet", "-print_format", "json",
|
|
195
|
+
"-select_streams", "v:0", "-show_streams", "-show_format", url,
|
|
196
|
+
],
|
|
197
|
+
capture_output=True, text=True, timeout=timeout, stdin=subprocess.DEVNULL,
|
|
198
|
+
)
|
|
199
|
+
except (FileNotFoundError, OSError, subprocess.TimeoutExpired) as e:
|
|
200
|
+
_log(f" ⚠️ video meta probe unavailable: {type(e).__name__}")
|
|
201
|
+
return {}
|
|
202
|
+
if result.returncode != 0:
|
|
203
|
+
return {}
|
|
204
|
+
try:
|
|
205
|
+
info = json.loads(result.stdout or "{}")
|
|
206
|
+
except json.JSONDecodeError:
|
|
207
|
+
return {}
|
|
208
|
+
|
|
209
|
+
meta: dict = {}
|
|
210
|
+
streams = info.get("streams") or []
|
|
211
|
+
if streams:
|
|
212
|
+
s = streams[0]
|
|
213
|
+
w, h = s.get("width"), s.get("height")
|
|
214
|
+
if isinstance(w, int) and isinstance(h, int) and w > 0 and h > 0:
|
|
215
|
+
# 手机素材常带 90° / 270° 旋转元数据:播放器按旋转后的方向显示。
|
|
216
|
+
rotation = 0
|
|
217
|
+
for side in s.get("side_data_list") or []:
|
|
218
|
+
if isinstance(side.get("rotation"), (int, float)):
|
|
219
|
+
rotation = int(side["rotation"])
|
|
220
|
+
tags_rot = (s.get("tags") or {}).get("rotate")
|
|
221
|
+
if tags_rot and str(tags_rot).lstrip("-").isdigit():
|
|
222
|
+
rotation = int(tags_rot)
|
|
223
|
+
if abs(rotation) % 180 == 90:
|
|
224
|
+
w, h = h, w
|
|
225
|
+
meta["width"], meta["height"] = w, h
|
|
226
|
+
try:
|
|
227
|
+
dur = float((info.get("format") or {}).get("duration") or 0)
|
|
228
|
+
if dur > 0:
|
|
229
|
+
meta["duration_ms"] = int(round(dur * 1000))
|
|
230
|
+
except (TypeError, ValueError):
|
|
231
|
+
pass
|
|
232
|
+
return meta
|
|
@@ -1182,7 +1182,16 @@ def resolve_asset_digital_human(asset: dict, private_token: str, timeout: int) -
|
|
|
1182
1182
|
for line in reversed(result.stdout.strip().split("\n")):
|
|
1183
1183
|
line = line.strip()
|
|
1184
1184
|
if line.startswith("http"):
|
|
1185
|
-
|
|
1185
|
+
out = {"status": "generated", "url": line}
|
|
1186
|
+
# 宽高 / 时长写回资产:模板按它定盒子比例(缺省 3:4 会裁人),
|
|
1187
|
+
# 按时长决定 onEnd=hide/loop 何时生效。探测失败不影响生成结果。
|
|
1188
|
+
from _video_probe import probe_video_meta
|
|
1189
|
+
meta = probe_video_meta(line)
|
|
1190
|
+
if meta.get("width") and meta.get("height"):
|
|
1191
|
+
out["width"], out["height"] = meta["width"], meta["height"]
|
|
1192
|
+
if meta.get("duration_ms"):
|
|
1193
|
+
out["duration_ms"] = meta["duration_ms"]
|
|
1194
|
+
return out
|
|
1186
1195
|
return {"status": "failed", "error": result.stderr.strip()[:1000]}
|
|
1187
1196
|
except subprocess.TimeoutExpired:
|
|
1188
1197
|
return {"status": "failed", "error": f"Digital human generation timed out after {dh_timeout}s"}
|
|
@@ -1263,6 +1272,11 @@ def _resolve_single_asset(
|
|
|
1263
1272
|
asset["generatedBy"]["completedAt"] = now_iso()
|
|
1264
1273
|
if result.get("audio_length_ms") is not None:
|
|
1265
1274
|
asset["duration"] = result["audio_length_ms"]
|
|
1275
|
+
# 视频类资产(数字人):ms 时长 + 像素尺寸,模板的 ResolvedAsset 契约同名字段。
|
|
1276
|
+
if result.get("duration_ms") is not None:
|
|
1277
|
+
asset["duration"] = result["duration_ms"]
|
|
1278
|
+
if result.get("width") and result.get("height"):
|
|
1279
|
+
asset["width"], asset["height"] = result["width"], result["height"]
|
|
1266
1280
|
if result.get("subtitles"):
|
|
1267
1281
|
asset["ttsSubtitles"] = result["subtitles"]
|
|
1268
1282
|
# Propagate structured-narration metadata onto the resolved
|
|
@@ -2950,6 +2964,23 @@ Examples:
|
|
|
2950
2964
|
for asset in image_assets:
|
|
2951
2965
|
url = asset.get("url", "")
|
|
2952
2966
|
print(f" {asset['assetId']:<25} {url}")
|
|
2967
|
+
|
|
2968
|
+
# 数字人段也要列出来:模型按这里写成片总结。线上出过 DSL 里没有数字人、
|
|
2969
|
+
# 模型却照着自己的意图汇报「数字人出镜」的事(conv_lvFaCClP7HrW5gDOZU1e),
|
|
2970
|
+
# 没有这一节时它无从核对。没有数字人时明说,而不是什么都不打。
|
|
2971
|
+
avatar_assets = [a for a in render_plan.get("resolvedAssets", []) if a.get("type") == "avatar"]
|
|
2972
|
+
avatar_scenes: dict = {}
|
|
2973
|
+
for entry in render_plan.get("timeline", []):
|
|
2974
|
+
ref = (entry.get("props") or {}).get("avatarAssetId")
|
|
2975
|
+
if ref:
|
|
2976
|
+
avatar_scenes.setdefault(ref, []).append(entry.get("sceneId", "?"))
|
|
2977
|
+
if avatar_assets:
|
|
2978
|
+
print(f"\n🧑 Digital-human segments:")
|
|
2979
|
+
for asset in avatar_assets:
|
|
2980
|
+
scenes_on = ", ".join(avatar_scenes.get(asset["assetId"], [])) or "(no scene)"
|
|
2981
|
+
print(f" {asset['assetId']:<25} {asset.get('status', '?'):<10} scenes: {scenes_on} {asset.get('url', '')}")
|
|
2982
|
+
else:
|
|
2983
|
+
print(f"\n🧑 Digital-human segments: none (this render has no presenter)")
|
|
2953
2984
|
# ─────────────────────────────────────────────────────────────────────
|
|
2954
2985
|
|
|
2955
2986
|
if failed > 0:
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "render-video",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "473",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V35",
|
|
6
6
|
"skillDescription": "Final-render skill: loads a persisted RenderPlan by `job_id` and drives the Remotion engine to produce the final video.\n\nUse this skill as soon as the user mentions any of these intents (after assets are already prepared):\n- Render the video, composite the video, export the video\n- Turn the prepared assets into the final clip\n- Render with Remotion\n\nPrerequisite: assets must already be generated via `prepare_video_assets`. This skill never resolves or regenerates assets — pass it a `job_id` from a previous `prepare_video_assets` call.\n\n⚠️ Stop-and-confirm gate: never call this skill until the user has explicitly confirmed the assets prepared by `prepare_video_assets`. If those assets were prepared in the current turn and the user has not replied since, stop and ask instead of rendering."
|
|
7
7
|
}
|