@remixmate/cli 0.9.61 → 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",
|
|
@@ -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
|