@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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.9.61",
4
- "generatedAt": "2026-09-30T06:42:11.032Z",
3
+ "version": "0.9.62",
4
+ "generatedAt": "2026-09-30T08:05:32.139Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "doc-parse",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixmate/cli",
3
- "version": "0.9.61",
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
- return {"status": "generated", "url": line}
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