@gitruck/cli 0.2.22 → 1.0.0

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/index.js CHANGED
@@ -4197,7 +4197,7 @@ var {
4197
4197
 
4198
4198
  // src/index.ts
4199
4199
  import { readFileSync as readFileSync6 } from "node:fs";
4200
- import { join as join30 } from "node:path";
4200
+ import { join as join33 } from "node:path";
4201
4201
 
4202
4202
  // src/lib/paths.ts
4203
4203
  import { dirname, join } from "node:path";
@@ -4230,6 +4230,12 @@ function ffmpegDir() {
4230
4230
  function audioCacheDir() {
4231
4231
  return join(GITRUCK_HOME, "audio-cache");
4232
4232
  }
4233
+ function fontsDir() {
4234
+ return join(GITRUCK_HOME, "fonts");
4235
+ }
4236
+ function tmpDir() {
4237
+ return join(GITRUCK_HOME, "tmp");
4238
+ }
4233
4239
  var _migrated = false;
4234
4240
  function migrateLegacyHome() {
4235
4241
  if (_migrated)
@@ -5819,7 +5825,8 @@ import { existsSync as existsSync7 } from "node:fs";
5819
5825
  import { join as join7 } from "node:path";
5820
5826
  var isWin = process.platform === "win32";
5821
5827
  var bin = (base) => isWin ? `${base}.exe` : base;
5822
- var FFMPEG_INSTALL_HINT = `未找到 ffmpeg/ffprobe。请把二者放到 ${ffmpegDir()}(agent 可代办:先查本地确实缺失才拉,` + `面向国内用户优先国内加速站点——GitHub 代理 pass-through 拉 BtbN/gyan.dev 官方静态构建,或同合云自建镜像,` + `并做 sha256 校验),或用 --ffmpeg-path <目录> 指定已装位置。`;
5828
+ var FFMPEG_INSTALL_HINT = `未找到 ffmpeg/ffprobe。装它:gtrk deps install --ffmpeg` + `(从同合云镜像拉,自动过 sha256 校验,落 ${ffmpegDir()};agent 可代办)。
5829
+ ` + ` 也可自行把 ffmpeg/ffprobe 放到该目录,或用 --ffmpeg-path <目录> 指定已装位置。`;
5823
5830
  var _cache = new Map;
5824
5831
  function onSystemPath(cmd) {
5825
5832
  try {
@@ -6200,9 +6207,9 @@ function registerInstall(program2) {
6200
6207
  }
6201
6208
 
6202
6209
  // src/commands/oralcut.ts
6203
- import { resolve as resolve4, join as join15, dirname as dirname3, basename as basename6, extname as extname2 } from "node:path";
6210
+ import { resolve as resolve4, join as join16, dirname as dirname4, basename as basename7, extname as extname2 } from "node:path";
6204
6211
  import { mkdir as mkdir5, writeFile as writeFile5, readFile as readFile4 } from "node:fs/promises";
6205
- import { existsSync as existsSync13 } from "node:fs";
6212
+ import { existsSync as existsSync14 } from "node:fs";
6206
6213
 
6207
6214
  // src/lib/config.ts
6208
6215
  function loadConfig() {
@@ -6723,8 +6730,194 @@ async function uploadAndSubmitTask(cfg, path, taskType, buildPayload, options =
6723
6730
 
6724
6731
  // src/lib/media.ts
6725
6732
  import { mkdir as mkdir3, stat as stat4 } from "node:fs/promises";
6726
- import { existsSync as existsSync11 } from "node:fs";
6727
- import { basename as basename4, extname, join as join12 } from "node:path";
6733
+ import { closeSync, existsSync as existsSync12, openSync, readSync, readdirSync as readdirSync2 } from "node:fs";
6734
+ import { homedir as homedir3 } from "node:os";
6735
+ import { basename as basename5, extname, join as join13 } from "node:path";
6736
+
6737
+ // src/lib/runtime-assets.ts
6738
+ import { spawnSync as spawnSync4 } from "node:child_process";
6739
+ import { createHash } from "node:crypto";
6740
+ import { createWriteStream, existsSync as existsSync11, mkdirSync as mkdirSync4, chmodSync, renameSync, rmSync, readdirSync } from "node:fs";
6741
+ import { createReadStream as createReadStream2 } from "node:fs";
6742
+ import { once } from "node:events";
6743
+ import { pipeline } from "node:stream/promises";
6744
+ import { basename as basename4, dirname as dirname3, join as join12 } from "node:path";
6745
+ var MANIFEST_URL = "https://api.ai-mcn.tv:9000/broadcast/exe/ffmpeg/dist/manifest.json";
6746
+ var SUPPORTED_SCHEMA = 1;
6747
+ function assertHttps(url) {
6748
+ if (!/^https:\/\//i.test(url)) {
6749
+ throw new Error(`运行时资产下载 MUST 用 https,收到:${url}`);
6750
+ }
6751
+ }
6752
+ var DISTRIBUTED_PLATFORMS = ["win-x64", "linux-x64", "mac-x64", "mac-arm64"];
6753
+ function resolvePlatformKey(platform = process.platform, arch = process.arch) {
6754
+ const os = { win32: "win", linux: "linux", darwin: "mac" }[platform];
6755
+ const cpu = { x64: "x64", arm64: "arm64" }[arch];
6756
+ const key = os && cpu ? `${os}-${cpu}` : "";
6757
+ if (!key || !DISTRIBUTED_PLATFORMS.includes(key)) {
6758
+ throw new Error(`当前平台 ${platform}-${arch} 无对应的 ffmpeg 分发包(已分发:${DISTRIBUTED_PLATFORMS.join(" / ")})。` + `请自行安装 ffmpeg/ffprobe 到 ${ffmpegDir()},或用 --ffmpeg-path <目录> 指定已装位置。`);
6759
+ }
6760
+ return key;
6761
+ }
6762
+ function parseManifest(raw) {
6763
+ if (!raw || typeof raw !== "object")
6764
+ throw new Error("manifest 不是对象");
6765
+ const m = raw;
6766
+ if (typeof m.schema !== "number")
6767
+ throw new Error("manifest 缺 schema 字段");
6768
+ if (m.schema > SUPPORTED_SCHEMA) {
6769
+ throw new Error(`manifest schema=${m.schema} 高于本 CLI 支持的 ${SUPPORTED_SCHEMA},请先升级 CLI(gtrk upgrade)`);
6770
+ }
6771
+ if (!m.base || !m.ffmpeg || !m.font)
6772
+ throw new Error("manifest 缺 base/ffmpeg/font 字段");
6773
+ assertHttps(m.base);
6774
+ return m;
6775
+ }
6776
+ async function fetchManifest(url = MANIFEST_URL) {
6777
+ assertHttps(url);
6778
+ const r = await fetch(url);
6779
+ if (!r.ok)
6780
+ throw new Error(`取 manifest 失败:HTTP ${r.status} ${url}`);
6781
+ return parseManifest(await r.json());
6782
+ }
6783
+ async function sha256File(path) {
6784
+ const h = createHash("sha256");
6785
+ await pipeline(createReadStream2(path), h);
6786
+ return h.digest("hex");
6787
+ }
6788
+ function systemTarPath() {
6789
+ if (process.platform === "win32") {
6790
+ const sys = join12(process.env.SystemRoot || "C:\\Windows", "System32", "tar.exe");
6791
+ if (existsSync11(sys))
6792
+ return sys;
6793
+ }
6794
+ return "tar";
6795
+ }
6796
+ function hasSystemTar() {
6797
+ try {
6798
+ return spawnSync4(systemTarPath(), ["--version"], { stdio: "ignore" }).status === 0;
6799
+ } catch {
6800
+ return false;
6801
+ }
6802
+ }
6803
+ async function downloadVerified(url, expectSha256, onProgress) {
6804
+ assertHttps(url);
6805
+ mkdirSync4(tmpDir(), { recursive: true });
6806
+ const tmp = join12(tmpDir(), `dl-${Date.now()}-${url.split("/").pop() ?? "asset"}`);
6807
+ try {
6808
+ const r = await fetch(url);
6809
+ if (!r.ok)
6810
+ throw new Error(`下载失败:HTTP ${r.status} ${url}`);
6811
+ if (!r.body)
6812
+ throw new Error(`下载失败:响应无 body ${url}`);
6813
+ const total = Number(r.headers.get("content-length") ?? 0);
6814
+ let got = 0;
6815
+ const reader = r.body.getReader();
6816
+ const ws = createWriteStream(tmp);
6817
+ try {
6818
+ for (;; ) {
6819
+ const { done, value } = await reader.read();
6820
+ if (done)
6821
+ break;
6822
+ got += value.length;
6823
+ if (onProgress)
6824
+ onProgress(got, total);
6825
+ if (!ws.write(value))
6826
+ await once(ws, "drain");
6827
+ }
6828
+ } finally {
6829
+ ws.end();
6830
+ }
6831
+ await once(ws, "close");
6832
+ const actual = await sha256File(tmp);
6833
+ if (actual !== expectSha256) {
6834
+ rmSync(tmp, { force: true });
6835
+ throw new Error(`sha256 校验不通过,已丢弃下载物、未落地任何文件。
6836
+ 期望 ${expectSha256}
6837
+ 实得 ${actual}
6838
+ 来源 ${url}`);
6839
+ }
6840
+ return tmp;
6841
+ } catch (e) {
6842
+ rmSync(tmp, { force: true });
6843
+ throw e;
6844
+ }
6845
+ }
6846
+ function extractTar(archive, destDir) {
6847
+ if (!hasSystemTar()) {
6848
+ throw new Error(`未找到可用的系统 tar,无法解包 ${archive}。
6849
+ ` + `Windows 10+/macOS/Linux 通常自带;若确无,请手工解开该 .tar.xz 并把二进制放到 ${destDir}。`);
6850
+ }
6851
+ mkdirSync4(destDir, { recursive: true });
6852
+ const r = spawnSync4(systemTarPath(), ["-xf", basename4(archive), "-C", destDir], {
6853
+ cwd: dirname3(archive),
6854
+ encoding: "utf8"
6855
+ });
6856
+ if (r.status !== 0) {
6857
+ throw new Error(`tar 解包失败(code=${r.status}):${(r.stderr || "").slice(-300)}`);
6858
+ }
6859
+ }
6860
+ async function installFfmpeg(m, opts = {}) {
6861
+ const key = resolvePlatformKey();
6862
+ const entry = m.ffmpeg[key];
6863
+ if (!entry) {
6864
+ throw new Error(`manifest 中无 ${key} 的 ffmpeg 包(可用:${Object.keys(m.ffmpeg).join(" / ")})。` + `请自行安装到 ${ffmpegDir()} 或用 --ffmpeg-path。`);
6865
+ }
6866
+ const dest = ffmpegDir();
6867
+ const already = entry.members.every((f) => existsSync11(join12(dest, f)));
6868
+ if (already && !opts.force) {
6869
+ return { installed: false, reason: "已存在", detail: `${dest}(--force 可覆盖)` };
6870
+ }
6871
+ const tmp = await downloadVerified(`${m.base}/${entry.file}`, entry.sha256, opts.onProgress);
6872
+ try {
6873
+ const stage = join12(tmpDir(), `x-${Date.now()}`);
6874
+ extractTar(tmp, stage);
6875
+ for (const f of entry.members) {
6876
+ if (!existsSync11(join12(stage, f))) {
6877
+ rmSync(stage, { recursive: true, force: true });
6878
+ throw new Error(`解包结果缺成员 ${f},已中止,未改动 ${dest}`);
6879
+ }
6880
+ }
6881
+ mkdirSync4(dest, { recursive: true });
6882
+ for (const f of entry.members) {
6883
+ const target = join12(dest, f);
6884
+ rmSync(target, { force: true });
6885
+ renameSync(join12(stage, f), target);
6886
+ if (process.platform !== "win32")
6887
+ chmodSync(target, 493);
6888
+ }
6889
+ rmSync(stage, { recursive: true, force: true });
6890
+ return { installed: true, detail: `${entry.version} → ${dest}` };
6891
+ } finally {
6892
+ rmSync(tmp, { force: true });
6893
+ }
6894
+ }
6895
+ async function installFont(m, opts = {}) {
6896
+ const out = [];
6897
+ const dest = fontsDir();
6898
+ for (const [name, entry] of Object.entries(m.font)) {
6899
+ const target = join12(dest, entry.file);
6900
+ if (existsSync11(target) && !opts.force) {
6901
+ out.push({ installed: false, reason: "已存在", detail: `${name} → ${target}` });
6902
+ continue;
6903
+ }
6904
+ const tmp = await downloadVerified(entry.url, entry.sha256, opts.onProgress);
6905
+ mkdirSync4(dest, { recursive: true });
6906
+ rmSync(target, { force: true });
6907
+ renameSync(tmp, target);
6908
+ out.push({ installed: true, detail: `${name} → ${target}` });
6909
+ }
6910
+ return out;
6911
+ }
6912
+ function hasLocalFonts() {
6913
+ try {
6914
+ return readdirSync(fontsDir()).some((f) => /\.(otf|ttf|ttc|otc)$/i.test(f));
6915
+ } catch {
6916
+ return false;
6917
+ }
6918
+ }
6919
+
6920
+ // src/lib/media.ts
6728
6921
  function parseFps(rate) {
6729
6922
  if (typeof rate !== "string")
6730
6923
  return 0;
@@ -6772,14 +6965,14 @@ function probeDuration(path, ffmpegPath) {
6772
6965
  }
6773
6966
  async function artifactPath(inputAbs, ext) {
6774
6967
  const s = await stat4(inputAbs);
6775
- const base = basename4(inputAbs, extname(inputAbs));
6968
+ const base = basename5(inputAbs, extname(inputAbs));
6776
6969
  await mkdir3(audioCacheDir(), { recursive: true });
6777
- return join12(audioCacheDir(), `${base}.${s.size}_${Math.round(s.mtimeMs)}.${ext}`);
6970
+ return join13(audioCacheDir(), `${base}.${s.size}_${Math.round(s.mtimeMs)}.${ext}`);
6778
6971
  }
6779
6972
  async function extractAudio(inputAbs, ffmpegPath) {
6780
6973
  const { ffmpeg } = requireFfmpeg(ffmpegPath);
6781
6974
  const out = await artifactPath(inputAbs, "mp3");
6782
- if (existsSync11(out))
6975
+ if (existsSync12(out))
6783
6976
  return out;
6784
6977
  await runFfmpeg(ffmpeg, [
6785
6978
  "-y",
@@ -6803,7 +6996,7 @@ async function extractAudio(inputAbs, ffmpegPath) {
6803
6996
  async function compress720p(inputAbs, ffmpegPath) {
6804
6997
  const { ffmpeg } = requireFfmpeg(ffmpegPath);
6805
6998
  const out = await artifactPath(inputAbs, "720p.mp4");
6806
- if (existsSync11(out))
6999
+ if (existsSync12(out))
6807
7000
  return out;
6808
7001
  await runFfmpeg(ffmpeg, [
6809
7002
  "-y",
@@ -6833,16 +7026,230 @@ function assertDurationConsistent(originalDuration, artifactAbs, ffmpegPath, tol
6833
7026
  throw new Error(`抽出物时长(${got.toFixed(2)}s)与原片(${originalDuration.toFixed(2)}s)不一致(容差 ${tolSec}s),` + `疑似抽取异常,已中止上传`);
6834
7027
  }
6835
7028
  }
7029
+ function assFontNames(assText) {
7030
+ const names = new Set;
7031
+ for (const line2 of assText.split(/\r?\n/)) {
7032
+ if (!line2.startsWith("Style:"))
7033
+ continue;
7034
+ const font = line2.slice("Style:".length).split(",")[1]?.trim();
7035
+ if (font)
7036
+ names.add(font);
7037
+ }
7038
+ return [...names];
7039
+ }
7040
+ async function probeFontAvailable(font, ffmpegPath) {
7041
+ const { ffmpeg } = requireFfmpeg(ffmpegPath);
7042
+ try {
7043
+ await runFfmpeg(ffmpeg, [
7044
+ "-v",
7045
+ "error",
7046
+ "-f",
7047
+ "lavfi",
7048
+ "-i",
7049
+ "color=c=black:s=64x64:d=0.04",
7050
+ "-vf",
7051
+ `drawtext=font='${font}':text=A`,
7052
+ "-frames:v",
7053
+ "1",
7054
+ "-f",
7055
+ "null",
7056
+ "-"
7057
+ ]);
7058
+ return true;
7059
+ } catch (e) {
7060
+ const msg = e instanceof Error ? e.message : String(e);
7061
+ if (/valid font|font.*not found|Fontconfig error/i.test(msg))
7062
+ return false;
7063
+ return null;
7064
+ }
7065
+ }
7066
+ function assFilterPath(p) {
7067
+ return p.replace(/\\/g, "/").replace(/:/g, "\\:");
7068
+ }
7069
+ function nameTableNames(buf) {
7070
+ const out = [];
7071
+ if (buf.length < 6)
7072
+ return out;
7073
+ const count = buf.readUInt16BE(2);
7074
+ const strBase = buf.readUInt16BE(4);
7075
+ for (let i = 0;i < count; i++) {
7076
+ const r = 6 + i * 12;
7077
+ if (r + 12 > buf.length)
7078
+ break;
7079
+ const platformID = buf.readUInt16BE(r);
7080
+ const nameID = buf.readUInt16BE(r + 6);
7081
+ if (nameID !== 1 && nameID !== 4 && nameID !== 16)
7082
+ continue;
7083
+ const len = buf.readUInt16BE(r + 8);
7084
+ const off = strBase + buf.readUInt16BE(r + 10);
7085
+ if (off + len > buf.length)
7086
+ continue;
7087
+ const slice = buf.subarray(off, off + len);
7088
+ let s;
7089
+ if (platformID === 1) {
7090
+ s = slice.toString("latin1");
7091
+ } else {
7092
+ const even = slice.length % 2 === 0 ? slice : slice.subarray(0, slice.length - 1);
7093
+ s = Buffer.from(even).swap16().toString("utf16le");
7094
+ }
7095
+ const v = s.replace(/\0/g, "").trim();
7096
+ if (v)
7097
+ out.push(v);
7098
+ }
7099
+ return out;
7100
+ }
7101
+ function fontFileFamilies(path) {
7102
+ let fd = null;
7103
+ try {
7104
+ fd = openSync(path, "r");
7105
+ const head = Buffer.alloc(12);
7106
+ if (readSync(fd, head, 0, 12, 0) < 12)
7107
+ return [];
7108
+ const bases = [];
7109
+ if (head.toString("latin1", 0, 4) === "ttcf") {
7110
+ const n = head.readUInt32BE(8);
7111
+ const dir = Buffer.alloc(Math.min(n, 64) * 4);
7112
+ readSync(fd, dir, 0, dir.length, 12);
7113
+ for (let i = 0;i < dir.length / 4; i++)
7114
+ bases.push(dir.readUInt32BE(i * 4));
7115
+ } else {
7116
+ bases.push(0);
7117
+ }
7118
+ const out = [];
7119
+ for (const base of bases) {
7120
+ const h = base === 0 ? head : Buffer.alloc(12);
7121
+ if (base !== 0 && readSync(fd, h, 0, 12, base) < 12)
7122
+ continue;
7123
+ const numTables = h.readUInt16BE(4);
7124
+ if (!numTables || numTables > 512)
7125
+ continue;
7126
+ const dir = Buffer.alloc(numTables * 16);
7127
+ if (readSync(fd, dir, 0, dir.length, base + 12) < dir.length)
7128
+ continue;
7129
+ let nameOff = 0;
7130
+ let nameLen = 0;
7131
+ for (let i = 0;i < numTables; i++) {
7132
+ if (dir.toString("latin1", i * 16, i * 16 + 4) === "name") {
7133
+ nameOff = dir.readUInt32BE(i * 16 + 8);
7134
+ nameLen = dir.readUInt32BE(i * 16 + 12);
7135
+ break;
7136
+ }
7137
+ }
7138
+ if (!nameOff || !nameLen || nameLen > 1 << 20)
7139
+ continue;
7140
+ const nameBuf = Buffer.alloc(nameLen);
7141
+ if (readSync(fd, nameBuf, 0, nameLen, nameOff) < nameLen)
7142
+ continue;
7143
+ out.push(...nameTableNames(nameBuf));
7144
+ }
7145
+ return out;
7146
+ } catch {
7147
+ return [];
7148
+ } finally {
7149
+ if (fd !== null)
7150
+ try {
7151
+ closeSync(fd);
7152
+ } catch {}
7153
+ }
7154
+ }
7155
+ var FONT_EXT = /\.(otf|ttf|ttc|otc)$/i;
7156
+ function scanFontDir(dir, out, depth = 0) {
7157
+ if (depth > 3)
7158
+ return;
7159
+ let entries;
7160
+ try {
7161
+ entries = readdirSync2(dir, { withFileTypes: true });
7162
+ } catch {
7163
+ return;
7164
+ }
7165
+ for (const e of entries) {
7166
+ const p = join13(dir, e.name);
7167
+ if (e.isDirectory())
7168
+ scanFontDir(p, out, depth + 1);
7169
+ else if (FONT_EXT.test(e.name))
7170
+ for (const n of fontFileFamilies(p))
7171
+ out.add(n);
7172
+ }
7173
+ }
7174
+ var _famCache = null;
7175
+ function localFontFamilies() {
7176
+ if (_famCache)
7177
+ return _famCache;
7178
+ const out = new Set;
7179
+ scanFontDir(fontsDir(), out);
7180
+ _famCache = [...out];
7181
+ return _famCache;
7182
+ }
7183
+ function systemFontDirs() {
7184
+ const home = homedir3();
7185
+ if (process.platform === "win32") {
7186
+ return [
7187
+ join13(process.env.SystemRoot || "C:\\Windows", "Fonts"),
7188
+ join13(process.env.LOCALAPPDATA || join13(home, "AppData", "Local"), "Microsoft", "Windows", "Fonts")
7189
+ ];
7190
+ }
7191
+ if (process.platform === "darwin") {
7192
+ return ["/System/Library/Fonts", "/Library/Fonts", join13(home, "Library", "Fonts")];
7193
+ }
7194
+ return ["/usr/share/fonts", "/usr/local/share/fonts", join13(home, ".local/share/fonts"), join13(home, ".fonts")];
7195
+ }
7196
+ var _sysCache = null;
7197
+ function systemFontFamilies() {
7198
+ if (_sysCache)
7199
+ return _sysCache;
7200
+ const out = new Set;
7201
+ for (const d of systemFontDirs())
7202
+ scanFontDir(d, out);
7203
+ _sysCache = [...out];
7204
+ return _sysCache;
7205
+ }
7206
+ async function burnSubtitle(videoAbs, assAbs, outAbs, opts = {}) {
7207
+ const { ffmpeg } = requireFfmpeg(opts.ffmpegPath);
7208
+ let filter = `ass='${assFilterPath(assAbs)}'`;
7209
+ if (hasLocalFonts())
7210
+ filter += `:fontsdir='${assFilterPath(fontsDir())}'`;
7211
+ await runFfmpeg(ffmpeg, [
7212
+ "-y",
7213
+ "-v",
7214
+ "error",
7215
+ "-i",
7216
+ videoAbs,
7217
+ "-vf",
7218
+ filter,
7219
+ "-c:v",
7220
+ opts.codec ?? "libx264",
7221
+ "-crf",
7222
+ String(opts.crf ?? 18),
7223
+ "-preset",
7224
+ "medium",
7225
+ "-c:a",
7226
+ "copy",
7227
+ "-movflags",
7228
+ "+faststart",
7229
+ outAbs
7230
+ ]);
7231
+ return outAbs;
7232
+ }
7233
+ async function fontUsableForBurn(font, ffmpegPath) {
7234
+ const local = localFontFamilies();
7235
+ const sys = systemFontFamilies();
7236
+ if (local.includes(font) || sys.includes(font))
7237
+ return true;
7238
+ if (local.length === 0 && sys.length === 0) {
7239
+ return await probeFontAvailable(font, ffmpegPath) !== false;
7240
+ }
7241
+ return false;
7242
+ }
6836
7243
 
6837
7244
  // src/lib/materialize.ts
6838
- import { join as join14, basename as basename5 } from "node:path";
7245
+ import { join as join15, basename as basename6 } from "node:path";
6839
7246
  import { mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
6840
7247
 
6841
7248
  // src/lib/render.ts
6842
7249
  import { writeFile as writeFile3, unlink, readFile as readFile3 } from "node:fs/promises";
6843
- import { existsSync as existsSync12 } from "node:fs";
7250
+ import { existsSync as existsSync13 } from "node:fs";
6844
7251
  import { tmpdir } from "node:os";
6845
- import { join as join13 } from "node:path";
7252
+ import { join as join14 } from "node:path";
6846
7253
  var AUDIO_SAMPLE_RATE = 48000;
6847
7254
  var AUDIO_LAYOUT = "stereo";
6848
7255
  var DEFAULT_CRF = 18;
@@ -7000,7 +7407,7 @@ function materialPathsFromGtrk(gtrk) {
7000
7407
  continue;
7001
7408
  if (!m.path)
7002
7409
  throw new Error(`gtrk 素材 ${m.id} 缺 path(source_path),无法本地渲染`);
7003
- if (!existsSync12(m.path))
7410
+ if (!existsSync13(m.path))
7004
7411
  throw new Error(`gtrk 素材文件不存在:${m.path}`);
7005
7412
  map[String(m.id)] = m.path;
7006
7413
  }
@@ -7014,7 +7421,7 @@ async function renderGtrk(gtrk, outputPath, opts = {}) {
7014
7421
  const { ffmpeg } = requireFfmpeg(opts.ffmpegPath);
7015
7422
  const materialPaths = materialPathsFromGtrk(gtrk);
7016
7423
  const { inputs, graph, total } = buildFilterGraph(gtrk, materialPaths, { crf });
7017
- const filterFile = join13(tmpdir(), `gtrk-filter-${process.pid}-${inputs.length}.txt`);
7424
+ const filterFile = join14(tmpdir(), `gtrk-filter-${process.pid}-${inputs.length}.txt`);
7018
7425
  await writeFile3(filterFile, graph, "utf8");
7019
7426
  try {
7020
7427
  const args = ["-y"];
@@ -7052,7 +7459,7 @@ function gtrkSourceName(gtrk) {
7052
7459
  const p = gtrk.materials?.[0]?.path;
7053
7460
  if (!p)
7054
7461
  return;
7055
- const b = basename5(p);
7462
+ const b = basename6(p);
7056
7463
  const dot = b.lastIndexOf(".");
7057
7464
  return dot > 0 ? b.slice(0, dot) : b;
7058
7465
  }
@@ -7064,7 +7471,7 @@ async function materializeResult(opts) {
7064
7471
  throw new Error("任务无工程文件产物(检查 project_formats / 任务是否产出)");
7065
7472
  const errors = { ...output.errors ?? {} };
7066
7473
  await mkdir4(outDir, { recursive: true });
7067
- const resultPath = join14(outDir, "result.json");
7474
+ const resultPath = join15(outDir, "result.json");
7068
7475
  const writeResult = async (extra) => {
7069
7476
  const r = {
7070
7477
  ok: Object.keys(errors).length === 0,
@@ -7086,9 +7493,9 @@ async function materializeResult(opts) {
7086
7493
  const byFormat = {};
7087
7494
  for (const f of files) {
7088
7495
  const base = baseFormat(f.format);
7089
- const fmtDir = join14(outDir, base);
7496
+ const fmtDir = join15(outDir, base);
7090
7497
  await mkdir4(fmtDir, { recursive: true });
7091
- const dest = join14(fmtDir, f.filename);
7498
+ const dest = join15(fmtDir, f.filename);
7092
7499
  try {
7093
7500
  await dl(f.download_url, dest);
7094
7501
  (byFormat[base] ??= []).push(dest);
@@ -7104,9 +7511,9 @@ async function materializeResult(opts) {
7104
7511
  }
7105
7512
  let jianyingDraftPath = null;
7106
7513
  if (byFormat.jianying && opts.draftDir) {
7107
- const dest = join14(opts.draftDir, basename5(outDir));
7514
+ const dest = join15(opts.draftDir, basename6(outDir));
7108
7515
  try {
7109
- const landing = await copyJianyingDraft(join14(outDir, "jianying"), dest);
7516
+ const landing = await copyJianyingDraft(join15(outDir, "jianying"), dest);
7110
7517
  if (landing.complete) {
7111
7518
  jianyingDraftPath = dest;
7112
7519
  log.info(`剪映草稿已落到:${dest}`);
@@ -7128,7 +7535,7 @@ async function materializeResult(opts) {
7128
7535
  log.step("本地渲染成片(ffmpeg)…");
7129
7536
  const project = await readGtrkFile(gtrkPath);
7130
7537
  const name = opts.projName ?? gtrkSourceName(project) ?? taskId;
7131
- const outMp4 = join14(outDir, `${name}.mp4`);
7538
+ const outMp4 = join15(outDir, `${name}.mp4`);
7132
7539
  const r = await renderGtrk(project, outMp4, {
7133
7540
  crf: opts.crf != null ? Number(opts.crf) : undefined,
7134
7541
  codec: opts.codec,
@@ -7150,7 +7557,7 @@ async function materializeResult(opts) {
7150
7557
  log.step("三方打开(产物已就位,按需自取):");
7151
7558
  for (const base of Object.keys(byFormat)) {
7152
7559
  const meta = FORMAT_META[base];
7153
- const target = base === "jianying" ? jianyingDraftPath ?? join14(outDir, "jianying") : byFormat[base][0];
7560
+ const target = base === "jianying" ? jianyingDraftPath ?? join15(outDir, "jianying") : byFormat[base][0];
7154
7561
  console.log(` • ${meta?.label ?? base}:${meta?.openHint(target) ?? target}`);
7155
7562
  }
7156
7563
  if (rendered)
@@ -7218,18 +7625,18 @@ async function runOralCut(input, opts) {
7218
7625
  routeLogsToStderr();
7219
7626
  const cfg = loadConfig();
7220
7627
  const inputAbs = resolve4(input);
7221
- if (!existsSync13(inputAbs))
7628
+ if (!existsSync14(inputAbs))
7222
7629
  throw new Error(`毛片不存在:${inputAbs}`);
7223
- const projName = basename6(inputAbs, extname2(inputAbs));
7630
+ const projName = basename7(inputAbs, extname2(inputAbs));
7224
7631
  const formats = opts.formats.split(",").map((s) => s.trim()).filter(Boolean);
7225
7632
  if (opts.render && !formats.includes("gtrk"))
7226
7633
  formats.push("gtrk");
7227
7634
  const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
7228
- const outDir = resolve4(opts.out ?? join15(dirname3(inputAbs), `${projName}-video-project-${timestamp()}`));
7635
+ const outDir = resolve4(opts.out ?? join16(dirname4(inputAbs), `${projName}-video-project-${timestamp()}`));
7229
7636
  let scriptPath = opts.script ? resolve4(opts.script) : undefined;
7230
7637
  if (!scriptPath) {
7231
- const sibling = join15(dirname3(inputAbs), `${projName}.txt`);
7232
- if (existsSync13(sibling)) {
7638
+ const sibling = join16(dirname4(inputAbs), `${projName}.txt`);
7639
+ if (existsSync14(sibling)) {
7233
7640
  scriptPath = sibling;
7234
7641
  log.info(`自动识别到同名文稿:${sibling}(按有稿剪辑;不想用就改名或显式 --script)`);
7235
7642
  }
@@ -7243,14 +7650,14 @@ async function runOralCut(input, opts) {
7243
7650
  else
7244
7651
  log.warn("没找到剪映草稿目录 → 将只产 draft_content.json、缺 meta。可加 --jianying-draft-dir <你的草稿目录> 重跑。");
7245
7652
  }
7246
- log.step(`▶ 智能口播剪辑:${basename6(inputAbs)}(预设 ${opts.preset}${opts.visualAssist ? " · 视觉兜底(720p)" : ""},格式 ${formats.join("/")}${opts.render ? " · 本地渲染" : ""})`);
7653
+ log.step(`▶ 智能口播剪辑:${basename7(inputAbs)}(预设 ${opts.preset}${opts.visualAssist ? " · 视觉兜底(720p)" : ""},格式 ${formats.join("/")}${opts.render ? " · 本地渲染" : ""})`);
7247
7654
  const extraParams = parseExtraParams(opts.param, opts.paramsJson);
7248
7655
  log.step("① 本地预处理(探几何 + 抽音频/720p)…");
7249
7656
  const geo = probeGeometry(inputAbs, opts.ffmpegPath);
7250
7657
  log.info(`原片几何 ${geo.width}x${geo.height} @ ${geo.fps.toFixed(2)}fps · ${geo.duration.toFixed(1)}s`);
7251
7658
  const artifact = opts.visualAssist ? await compress720p(inputAbs, opts.ffmpegPath) : await extractAudio(inputAbs, opts.ffmpegPath);
7252
7659
  assertDurationConsistent(geo.duration, artifact, opts.ffmpegPath);
7253
- log.info(opts.visualAssist ? `已压 720p 代理(上传物):${basename6(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename6(artifact)}`);
7660
+ log.info(opts.visualAssist ? `已压 720p 代理(上传物):${basename7(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename7(artifact)}`);
7254
7661
  log.step("② 上传抽出物到云端…");
7255
7662
  const buildPayload = (fid) => {
7256
7663
  const p = {
@@ -7290,7 +7697,7 @@ async function runOralCut(input, opts) {
7290
7697
  const up = { fileId: submitted.fileId, cached: submitted.cached };
7291
7698
  log.info(`task_id = ${taskId}`);
7292
7699
  await mkdir5(outDir, { recursive: true });
7293
- await writeFile5(join15(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE, fileId: up.fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
7700
+ await writeFile5(join16(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE, fileId: up.fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
7294
7701
  log.step("④ 云端处理中(每 5s 轮询)…");
7295
7702
  const result = await pollTask(cfg, TASK_TYPE, taskId, (status, progress) => {
7296
7703
  log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
@@ -7314,9 +7721,9 @@ async function runOralCut(input, opts) {
7314
7721
  }
7315
7722
 
7316
7723
  // src/commands/long2short.ts
7317
- import { resolve as resolve6, join as join17, dirname as dirname5, basename as basename8, extname as extname5 } from "node:path";
7318
- import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
7319
- import { existsSync as existsSync16 } from "node:fs";
7724
+ import { resolve as resolve6, join as join19, dirname as dirname7, basename as basename9, extname as extname5 } from "node:path";
7725
+ import { mkdir as mkdir7, writeFile as writeFile8 } from "node:fs/promises";
7726
+ import { existsSync as existsSync17 } from "node:fs";
7320
7727
 
7321
7728
  // src/lib/clip-brief.ts
7322
7729
  var str = (v) => {
@@ -7445,13 +7852,113 @@ function renderClipsOverview(clips, ctx) {
7445
7852
  `)}
7446
7853
  `;
7447
7854
  }
7855
+ var POLISH_LABEL = {
7856
+ split_screen: "智能分屏",
7857
+ camera: "克制运镜",
7858
+ speed: "整体调速",
7859
+ seam: "接缝过渡",
7860
+ subtitle: "智能字幕",
7861
+ subtitle_remap: "字幕时轴重映射",
7862
+ purify_source: "去除原字幕",
7863
+ unknown: "未具名润色项"
7864
+ };
7865
+ function renderProReport(clips, report, ctx) {
7866
+ const name = ctx.source.split(/[\/]/).pop() || ctx.source;
7867
+ const out = [`# ${name} · 长剪短精剪报告`, ""];
7868
+ out.push(`- 源片:\`${ctx.source}\``);
7869
+ out.push(`- 成片:${clips.length} 条`);
7870
+ const jc = report?.jump_cut;
7871
+ if (typeof jc === "boolean")
7872
+ out.push(`- 跳剪:${jc ? "开" : "关"}`);
7873
+ if (ctx.taskId)
7874
+ out.push(`- task_id:\`${ctx.taskId}\``);
7875
+ out.push("");
7876
+ out.push("| # | 标题 | 时长 | 评分 | 简介 |", "| --- | --- | --- | --- | --- |");
7877
+ for (const [i, clip] of clips.entries()) {
7878
+ const dur = clipDurationMs(clip);
7879
+ const score = num(clip.score);
7880
+ out.push(`| clip${i} | ${cell(str(clip.title) ?? "—")} | ${dur != null ? fmtTime(dur) : "—"} | ${score ?? "—"} | ${cell(str(clip.summary) ?? "—")} |`);
7881
+ }
7882
+ out.push("");
7883
+ const degraded = report?.degraded_items;
7884
+ const degRows = [];
7885
+ if (degraded && typeof degraded === "object" && !Array.isArray(degraded)) {
7886
+ for (const [k, v] of Object.entries(degraded)) {
7887
+ const names = list(v).map((x) => POLISH_LABEL[x] ?? x);
7888
+ if (names.length)
7889
+ degRows.push(`- **${k}**:${names.join("、")}`);
7890
+ }
7891
+ }
7892
+ if (degRows.length) {
7893
+ out.push("## ⚠️ 润色降级", "");
7894
+ out.push("以下片子照常出片,但列出的润色项**没有做成**——效果与预期不同,别当成完整成片直接发布。", "");
7895
+ out.push(...degRows, "");
7896
+ }
7897
+ for (const [i, clip] of clips.entries()) {
7898
+ const title = str(clip.title);
7899
+ out.push("---", "", `## clip${i}${title ? `「${title}」` : ""}`, "");
7900
+ const bits = [];
7901
+ const dur = clipDurationMs(clip);
7902
+ if (dur != null)
7903
+ bits.push(`时长 ${fmtTime(dur)}`);
7904
+ const segCount = clipSegmentCount(clip);
7905
+ if (segCount != null)
7906
+ bits.push(`${segCount} 个保留片段`);
7907
+ const span = sourceSpan(clip);
7908
+ if (span)
7909
+ bits.push(`源片 ${fmtTime(span[0])}–${fmtTime(span[1])}`);
7910
+ const f = ctx.clipFiles?.[i];
7911
+ if (f)
7912
+ bits.push(`成片 ${baseName(f)}`);
7913
+ if (bits.length)
7914
+ out.push(`- ${bits.join(" · ")}`, "");
7915
+ const score = num(clip.score);
7916
+ const reason = str(clip.score_reason);
7917
+ if (score != null || reason) {
7918
+ out.push("### 入选理由", "");
7919
+ out.push([score != null ? `**评分 ${score}**` : null, reason].filter(Boolean).join(" — "), "");
7920
+ }
7921
+ const summary = str(clip.summary);
7922
+ if (summary)
7923
+ out.push("### 简介", "", summary, "");
7924
+ const note = str(clip.jumpcut_note);
7925
+ if (note)
7926
+ out.push("### 跳剪", "", jumpcutText(note), "");
7927
+ const cats = [
7928
+ ["主题", list(clip.themes)],
7929
+ ["标签", list(clip.tags)],
7930
+ ["类型", list(clip.genres)],
7931
+ ["调性", list(clip.moods)]
7932
+ ];
7933
+ const shown = cats.filter(([, v]) => v.length);
7934
+ if (shown.length) {
7935
+ out.push("### 分类与调性", "");
7936
+ for (const [k, v] of shown)
7937
+ out.push(`- ${k}:${v.join("、")}`);
7938
+ out.push("");
7939
+ }
7940
+ const hl = Array.isArray(clip.highlight_words) ? clip.highlight_words : [];
7941
+ const hlRows = hl.map((h) => ({ text: str(h?.text), at: num(h?.begin_time) })).filter((h) => h.text);
7942
+ if (hlRows.length) {
7943
+ out.push("### 高光词(源片时码)", "");
7944
+ for (const h of hlRows)
7945
+ out.push(`- ${h.at != null ? `${fmtTime(h.at)} ` : ""}「${h.text}」`);
7946
+ out.push("");
7947
+ }
7948
+ }
7949
+ return `${out.join(`
7950
+ `).replace(/\n{3,}/g, `
7951
+
7952
+ `).trimEnd()}
7953
+ `;
7954
+ }
7448
7955
 
7449
7956
  // src/lib/tool-runner.ts
7450
- import { resolve as resolve5, join as join16, dirname as dirname4, basename as basename7, extname as extname4 } from "node:path";
7451
- import { mkdir as mkdir6, writeFile as writeFile6, stat as stat5 } from "node:fs/promises";
7452
- import { createWriteStream, existsSync as existsSync15 } from "node:fs";
7957
+ import { resolve as resolve5, join as join18, dirname as dirname6, basename as basename8, extname as extname4 } from "node:path";
7958
+ import { mkdir as mkdir6, writeFile as writeFile7, stat as stat5 } from "node:fs/promises";
7959
+ import { createWriteStream as createWriteStream2, existsSync as existsSync16 } from "node:fs";
7453
7960
  import { Readable as Readable2 } from "node:stream";
7454
- import { pipeline } from "node:stream/promises";
7961
+ import { pipeline as pipeline2 } from "node:stream/promises";
7455
7962
 
7456
7963
  // src/lib/tool-pricing.ts
7457
7964
  var TOOL_PRICE_LIST_URL = "https://cloud.ai-mcn.tv/api/get_price_list";
@@ -7534,11 +8041,13 @@ async function resolveToolPricing(priceKey, pricingContext, fetchFn = fetch) {
7534
8041
 
7535
8042
  // src/lib/tool-descriptors.ts
7536
8043
  import { readFileSync as readFileSync4 } from "node:fs";
7537
- import { extname as extname3, resolve as resolvePath } from "node:path";
8044
+ import { readFile as readFile5, writeFile as writeFile6 } from "node:fs/promises";
8045
+ import { dirname as dirname5, extname as extname3, join as join17, resolve as resolvePath } from "node:path";
7538
8046
  var MULTI_INPUT_KINDS = new Set(["images", "videos"]);
7539
8047
  var IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif", ".tif", ".tiff", ".heic", ".heif", ".avif"];
7540
8048
  var VIDEO_EXTS = [".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v", ".flv", ".wmv", ".mpg", ".mpeg", ".ts", ".m2ts"];
7541
8049
  var AUDIO_EXTS = [".mp3", ".wav", ".m4a", ".aac", ".flac", ".ogg", ".opus", ".wma"];
8050
+ var isAudioFile = (p) => AUDIO_EXTS.includes(extname3(p).toLowerCase());
7542
8051
  var PUBLIC_VIDEO_EXTS = [
7543
8052
  ".mp4",
7544
8053
  ".avi",
@@ -8482,18 +8991,37 @@ var videoAiSubtitle = {
8482
8991
  { flag: "--subtitle-type <style>", desc: `字幕样式:${AI_SUBTITLE_TYPES.join("/")}(未传则用服务端默认)` },
8483
8992
  { flag: "--subtitle-color <color>", desc: `字幕颜色:${AI_SUBTITLE_COLORS.join("/")}(未传则用服务端默认)` }
8484
8993
  ],
8994
+ async preprocess(ctx) {
8995
+ const inputAbs = ctx.inputAbs;
8996
+ if (ctx.opts.needPure === true) {
8997
+ ctx.warn("--need-pure 需要画面(去原字幕是画面操作),本次整片上传;只要字幕文件时去掉该 flag 即可只传音频");
8998
+ return inputAbs;
8999
+ }
9000
+ if (isAudioFile(inputAbs))
9001
+ return inputAbs;
9002
+ const audio = await extractAudio(inputAbs, ctx.ffmpegPath);
9003
+ return audio;
9004
+ },
8485
9005
  buildPayload(fileId, ctx) {
8486
9006
  const language = ctx.opts.language == null ? "" : String(ctx.opts.language).trim();
8487
9007
  if (!language)
8488
9008
  throw new Error("--language 必填:请指定源语种代码(具体取值见云端 API 文档 / 服务端支持列表)");
8489
9009
  const payload = { file_id: fileId, language };
9010
+ const inputAbs = ctx.inputAbs;
9011
+ if (inputAbs && !isAudioFile(inputAbs)) {
9012
+ try {
9013
+ const geo = probeGeometry(inputAbs, ctx.ffmpegPath);
9014
+ if (geo.width > 0 && geo.height > 0)
9015
+ payload.video_size = [geo.width, geo.height];
9016
+ } catch {
9017
+ ctx.warn("探原片几何失败,本次不回传 video_size(服务端将按 1920x1080 兜底出字幕)");
9018
+ }
9019
+ }
8490
9020
  if (ctx.opts.translateLanguage != null) {
8491
9021
  const t = String(ctx.opts.translateLanguage).trim();
8492
9022
  if (t)
8493
9023
  payload.translate_language = t;
8494
9024
  }
8495
- if (ctx.opts.needRender === true)
8496
- payload.need_render = true;
8497
9025
  if (ctx.opts.needPure === true)
8498
9026
  payload.need_pure = true;
8499
9027
  if (ctx.opts.subtitleType != null) {
@@ -8523,10 +9051,136 @@ var videoAiSubtitle = {
8523
9051
  files.push({ url: pure, filename: `${ctx.baseName}-pure${extFromUrl(pure, ".mp4")}` });
8524
9052
  return files;
8525
9053
  },
9054
+ async postprocess(ctx, landed) {
9055
+ if (ctx.opts.needRender !== true)
9056
+ return;
9057
+ const inputAbs = ctx.inputAbs;
9058
+ if (isAudioFile(inputAbs)) {
9059
+ throw new Error("--need-render 需要视频输入:本次输入是音频文件,无画面可烧");
9060
+ }
9061
+ const ass = landed.find((p) => p.toLowerCase().endsWith(".ass"));
9062
+ if (!ass)
9063
+ throw new Error("未拉回 .ass 字幕文件,无法本地烧录");
9064
+ const fonts = assFontNames(await readFile5(ass, "utf8"));
9065
+ const missing = [];
9066
+ for (const f of fonts) {
9067
+ if (!await fontUsableForBurn(f, ctx.ffmpegPath))
9068
+ missing.push(f);
9069
+ }
9070
+ if (missing.length) {
9071
+ throw new Error(`本机缺字幕模板所需字体:${missing.join("、")}。` + `装它:gtrk deps install --font(落 ~/.gitruck/fonts,不写系统字体表)。
9072
+ ` + ` 装上后重跑即可(字幕文件已落地,无需重新提交任务)。不用替代字体顶——` + `替代字体烧出来的成片观感与模板设计不符且难以察觉。`);
9073
+ }
9074
+ const out = join17(dirname5(ass), `${ctx.baseName}-subtitled.mp4`);
9075
+ await burnSubtitle(inputAbs, ass, out, { ffmpegPath: ctx.ffmpegPath });
9076
+ return [out];
9077
+ },
8526
9078
  mapResult(out) {
8527
9079
  return { summary: typeof out.summary === "string" ? out.summary : "", asr: out.asr ?? null };
8528
9080
  }
8529
9081
  };
9082
+ var PRO_DURATION_PREFS = ["auto", "short", "medium", "long"];
9083
+ var videoLong2ShortPro = {
9084
+ name: "video_long2short_pro",
9085
+ title: "长剪短·精剪(直接出片)",
9086
+ description: "长内容按语义抽多条高光短片并一键出成品:选段+跳剪+分屏内核,叠加模糊底画布适配/克制运镜/调速保音高/智能字幕。只出成片,不产工程文件——要可编辑工程请用 gtrk long2short(粗剪)。",
9087
+ kind: "cloud",
9088
+ input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
9089
+ priceKey: "video_long2short_pro",
9090
+ outputHint: "逐条高光成片 mp4 + 切片报告 clips.md",
9091
+ enabled: true,
9092
+ taskType: "video_long2short_pro",
9093
+ pollTimeoutMs: 4 * 60 * 60 * 1000,
9094
+ options: [
9095
+ { flag: "--language <code>", desc: "源语种代码(精剪必填;取值以服务端支持列表为准)" },
9096
+ { flag: "--output-language <code>", desc: "选段/文案的输出语种(未传则同源语种)" },
9097
+ { flag: "--main-topic <text>", desc: "主题引导(影响选段偏好)" },
9098
+ { flag: "--output-size <s>", desc: "成片画幅 9:16|16:9|1:1 或自定义 WxH(未传则服务端默认)" },
9099
+ { flag: "--no-jump-cut", desc: "关闭跳剪(默认开:片内去水词/冗余,只删不重排)" },
9100
+ { flag: "--duration-pref <p>", desc: `成片时长偏好 ${PRO_DURATION_PREFS.join("/")}(成片条数由内容语义决定、不可指定)` },
9101
+ { flag: "--max-clip-sec <n>", desc: "单条成片时长安全上限(秒;未传则服务端默认)" },
9102
+ { flag: "--split-screen", desc: "开启智能分屏(多人同框段合成分屏画面)" },
9103
+ { flag: "--split-orientation <o>", desc: "分屏方向 auto|lr|tb(未传则服务端默认)" },
9104
+ { flag: "--speed-factor <n>", desc: "整体调速倍率(保音高;未传则服务端默认)" },
9105
+ { flag: "--no-camera-move", desc: "关闭克制运镜(默认开:亚像素推/拉)" },
9106
+ { flag: "--no-subtitle", desc: "关闭智能字幕(默认开:成片内烧录字幕)" },
9107
+ { flag: "--subtitle-translate-language <code>", desc: "字幕译文语种(未传则单语字幕)" }
9108
+ ],
9109
+ buildPayload(fileId, ctx) {
9110
+ const o = ctx.opts;
9111
+ const language = o.language == null ? "" : String(o.language).trim();
9112
+ if (!language)
9113
+ throw new Error("--language 必填:请指定源语种代码(具体取值见云端 API 文档 / 服务端支持列表)");
9114
+ const p = { file_id: fileId, language };
9115
+ if (o.outputLanguage != null && String(o.outputLanguage).trim())
9116
+ p.output_language = String(o.outputLanguage).trim();
9117
+ if (o.mainTopic != null && String(o.mainTopic).trim())
9118
+ p.main_topic = String(o.mainTopic).trim();
9119
+ if (o.outputSize != null && String(o.outputSize).trim())
9120
+ p.output_size = String(o.outputSize).trim();
9121
+ if (o.jumpCut === false)
9122
+ p.jump_cut = false;
9123
+ const duration = {};
9124
+ if (o.durationPref != null && String(o.durationPref).trim())
9125
+ duration.pref = String(o.durationPref).trim();
9126
+ const mcs = parseCountFlag(o.maxClipSec, "--max-clip-sec");
9127
+ if (mcs != null)
9128
+ duration.max_clip_sec = mcs;
9129
+ if (Object.keys(duration).length)
9130
+ p.duration = duration;
9131
+ if (o.splitScreen === true) {
9132
+ const ss = { enable: true };
9133
+ if (o.splitOrientation != null && String(o.splitOrientation).trim())
9134
+ ss.orientation = String(o.splitOrientation).trim();
9135
+ p.split_screen = ss;
9136
+ }
9137
+ if (o.speedFactor != null) {
9138
+ const n = Number(o.speedFactor);
9139
+ if (!Number.isFinite(n))
9140
+ throw new Error(`--speed-factor 需要有限数值,拿到「${o.speedFactor}」`);
9141
+ p.speed = { factor: n };
9142
+ }
9143
+ if (o.cameraMove === false)
9144
+ p.camera_move = { enable: false };
9145
+ const sub = {};
9146
+ if (o.subtitle === false)
9147
+ sub.enable = false;
9148
+ if (o.subtitleTranslateLanguage != null && String(o.subtitleTranslateLanguage).trim()) {
9149
+ sub.translate_language = String(o.subtitleTranslateLanguage).trim();
9150
+ }
9151
+ if (Object.keys(sub).length)
9152
+ p.subtitle = sub;
9153
+ return p;
9154
+ },
9155
+ mapOutputs(out, _ctx) {
9156
+ const clips = Array.isArray(out.clips) ? out.clips : [];
9157
+ const files = [];
9158
+ for (const [i, clip] of clips.entries()) {
9159
+ const f = clip?.file;
9160
+ const url = typeof f?.download_url === "string" ? f.download_url : undefined;
9161
+ if (!url)
9162
+ continue;
9163
+ files.push({ url, filename: `clip${i}${extFromUrl(url, ".mp4")}` });
9164
+ }
9165
+ return files;
9166
+ },
9167
+ mapResult(out) {
9168
+ return { clips: out.clips ?? [], report: out.report ?? {} };
9169
+ },
9170
+ async postprocess(ctx, landed, out) {
9171
+ const clips = Array.isArray(out.clips) ? out.clips : [];
9172
+ if (!clips.length)
9173
+ return;
9174
+ const mp4s = landed.filter((p) => p.toLowerCase().endsWith(".mp4"));
9175
+ const byIndex = clips.map((_c, i) => mp4s.find((p) => new RegExp(`clip${i}.[a-z0-9]+$`, "i").test(p)));
9176
+ const dest = join17(dirname5(landed[0] ?? "."), "clips.md");
9177
+ await writeFile6(dest, renderProReport(clips, out.report ?? {}, {
9178
+ source: ctx.inputAbs ?? ctx.baseName,
9179
+ clipFiles: byIndex
9180
+ }), "utf8");
9181
+ return [dest];
9182
+ }
9183
+ };
8530
9184
  function parseCountFlag(v, flag) {
8531
9185
  if (v == null)
8532
9186
  return;
@@ -8818,6 +9472,7 @@ var TOOL_REGISTRY = [
8818
9472
  imageClassicTemplate,
8819
9473
  imageVerticalStitch,
8820
9474
  videoSplitScreen,
9475
+ videoLong2ShortPro,
8821
9476
  audioTtsClone,
8822
9477
  mad
8823
9478
  ];
@@ -8918,7 +9573,7 @@ function validateToolInput(descriptor, inputAbs) {
8918
9573
  return;
8919
9574
  if (!inputAbs)
8920
9575
  throw new Error(`${descriptor.name} 需要输入${spec.kind === "directory" ? "目录" : "文件"}`);
8921
- if (!existsSync15(inputAbs))
9576
+ if (!existsSync16(inputAbs))
8922
9577
  throw new Error(`输入不存在:${inputAbs}`);
8923
9578
  if (spec.kind === "directory")
8924
9579
  return;
@@ -8935,13 +9590,13 @@ function validateToolInputs(descriptor, inputAbsList) {
8935
9590
  throw new Error(`${descriptor.name} 需要至少一个输入文件(可传多个,顺序即拼装顺序)`);
8936
9591
  const exts = descriptor.input.exts ?? defaultExtsFor(descriptor.input.kind);
8937
9592
  for (const p of inputAbsList) {
8938
- if (!existsSync15(p))
9593
+ if (!existsSync16(p))
8939
9594
  throw new Error(`输入不存在:${p}`);
8940
9595
  if (exts && exts.length) {
8941
9596
  const e = extname4(p).toLowerCase();
8942
9597
  if (!exts.includes(e)) {
8943
9598
  const noun = descriptor.input.kind === "videos" ? "视频" : "图片";
8944
- throw new Error(`${descriptor.name} 需要${noun}输入,但「${basename7(p)}」是「${e || "无扩展名"}」(支持:${exts.join(" ")})`);
9599
+ throw new Error(`${descriptor.name} 需要${noun}输入,但「${basename8(p)}」是「${e || "无扩展名"}」(支持:${exts.join(" ")})`);
8945
9600
  }
8946
9601
  }
8947
9602
  }
@@ -8959,7 +9614,7 @@ async function downloadStream(url, dest) {
8959
9614
  const res = await fetch(url);
8960
9615
  if (!res.ok || !res.body)
8961
9616
  throw new Error(`下载失败 HTTP ${res.status}:${url}`);
8962
- await pipeline(Readable2.fromWeb(res.body), createWriteStream(dest));
9617
+ await pipeline2(Readable2.fromWeb(res.body), createWriteStream2(dest));
8963
9618
  }
8964
9619
  async function pollToolTask(cfg, taskType, taskId, opts = {}) {
8965
9620
  const timeoutMs = opts.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
@@ -9039,10 +9694,10 @@ function resolveOutDir(descriptor, inputAbs, out) {
9039
9694
  if (out)
9040
9695
  return resolve5(out);
9041
9696
  if (inputAbs) {
9042
- const base = basename7(inputAbs, extname4(inputAbs));
9043
- return join16(dirname4(inputAbs), `${base}-${descriptor.name}`);
9697
+ const base = basename8(inputAbs, extname4(inputAbs));
9698
+ return join18(dirname6(inputAbs), `${base}-${descriptor.name}`);
9044
9699
  }
9045
- return join16(process.cwd(), `${descriptor.name}-${timestamp2()}`);
9700
+ return join18(process.cwd(), `${descriptor.name}-${timestamp2()}`);
9046
9701
  }
9047
9702
  async function safeFingerprint(inputAbs) {
9048
9703
  try {
@@ -9060,7 +9715,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9060
9715
  const isMulti = MULTI_INPUT_KINDS.has(descriptor.input.kind);
9061
9716
  const inputList = isMulti ? (Array.isArray(inputArg) ? inputArg : inputArg != null ? [inputArg] : []).map((p) => resolve5(p)) : undefined;
9062
9717
  const inputAbs = isMulti ? inputList[0] : typeof inputArg === "string" ? resolve5(inputArg) : undefined;
9063
- const baseName2 = inputAbs ? basename7(inputAbs, extname4(inputAbs)) : descriptor.name;
9718
+ const baseName2 = inputAbs ? basename8(inputAbs, extname4(inputAbs)) : descriptor.name;
9064
9719
  if (isMulti) {
9065
9720
  validateToolInputs(descriptor, inputList);
9066
9721
  } else {
@@ -9131,7 +9786,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9131
9786
  }
9132
9787
  await mkdir6(outDir, { recursive: true });
9133
9788
  const fingerprint2 = inputList ? await Promise.all(inputList.map((p) => safeFingerprint(p))) : inputAbs ? await safeFingerprint(inputAbs) : undefined;
9134
- await writeFile6(join16(outDir, "task.json"), JSON.stringify({
9789
+ await writeFile7(join18(outDir, "task.json"), JSON.stringify({
9135
9790
  tool: descriptor.name,
9136
9791
  taskType,
9137
9792
  taskId,
@@ -9153,7 +9808,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9153
9808
  const errors = {};
9154
9809
  const items = descriptor.mapOutputs ? descriptor.mapOutputs(outputResult, ctx) : [];
9155
9810
  for (const it of items) {
9156
- const dest = join16(outDir, it.filename);
9811
+ const dest = join18(outDir, it.filename);
9157
9812
  try {
9158
9813
  await deps.downloadStream(it.url, dest);
9159
9814
  files.push(dest);
@@ -9161,11 +9816,20 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9161
9816
  errors[it.filename] = e instanceof Error ? e.message : String(e);
9162
9817
  }
9163
9818
  }
9819
+ if (descriptor.postprocess) {
9820
+ try {
9821
+ const extra = await descriptor.postprocess(ctx, [...files], outputResult);
9822
+ if (Array.isArray(extra))
9823
+ files.push(...extra);
9824
+ } catch (e) {
9825
+ errors[`${descriptor.name}:postprocess`] = e instanceof Error ? e.message : String(e);
9826
+ }
9827
+ }
9164
9828
  let resultFile;
9165
9829
  const structured = descriptor.mapResult ? descriptor.mapResult(outputResult, ctx) : undefined;
9166
9830
  if (structured != null) {
9167
- resultFile = join16(outDir, "result-output.json");
9168
- await writeFile6(resultFile, JSON.stringify(structured, null, 2));
9831
+ resultFile = join18(outDir, "result-output.json");
9832
+ await writeFile7(resultFile, JSON.stringify(structured, null, 2));
9169
9833
  }
9170
9834
  if (items.length === 0 && resultFile == null) {
9171
9835
  errors["output"] = "任务完成但未解析到任何产物(下载链接与结构化结果均为空,output_result 形态异常)";
@@ -9183,7 +9847,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9183
9847
  ...resultFile ? { resultFile } : {},
9184
9848
  ...Object.keys(errors).length ? { errors } : {}
9185
9849
  };
9186
- await writeFile6(join16(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
9850
+ await writeFile7(join18(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
9187
9851
  return result;
9188
9852
  }
9189
9853
 
@@ -9240,8 +9904,8 @@ async function copyClipDraftsToRoot(clipDirs, draftTarget, errors) {
9240
9904
  let complete = 0;
9241
9905
  let attempted = 0;
9242
9906
  for (const [i, dir] of clipDirs.entries()) {
9243
- const src = join17(dir, "jianying");
9244
- if (!existsSync16(src)) {
9907
+ const src = join19(dir, "jianying");
9908
+ if (!existsSync17(src)) {
9245
9909
  paths.push(null);
9246
9910
  continue;
9247
9911
  }
@@ -9273,15 +9937,15 @@ async function runLong2Short(input, opts) {
9273
9937
  routeLogsToStderr();
9274
9938
  const cfg = loadConfig();
9275
9939
  const inputAbs = resolve6(input);
9276
- if (!existsSync16(inputAbs))
9940
+ if (!existsSync17(inputAbs))
9277
9941
  throw new Error(`毛片不存在:${inputAbs}`);
9278
- const projName = basename8(inputAbs, extname5(inputAbs));
9942
+ const projName = basename9(inputAbs, extname5(inputAbs));
9279
9943
  const formats = opts.formats.split(",").map((s) => s.trim()).filter(Boolean);
9280
9944
  if (!formats.includes("gtrk"))
9281
9945
  formats.push("gtrk");
9282
9946
  const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
9283
- const outDir = resolve6(opts.out ?? join17(dirname5(inputAbs), `${projName}-long2short`));
9284
- const outName = basename8(outDir);
9947
+ const outDir = resolve6(opts.out ?? join19(dirname7(inputAbs), `${projName}-long2short`));
9948
+ const outName = basename9(outDir);
9285
9949
  let draftRoot;
9286
9950
  if (wantJianying) {
9287
9951
  draftRoot = resolveJianyingDraftDir(opts.jianyingDraftDir);
@@ -9290,14 +9954,14 @@ async function runLong2Short(input, opts) {
9290
9954
  else
9291
9955
  log.warn("没找到剪映草稿目录 → 将只产 draft_content.json、缺 meta。可加 --jianying-draft-dir <你的草稿目录> 重跑。");
9292
9956
  }
9293
- const draftTarget = draftRoot ? join17(draftRoot, outName) : undefined;
9294
- log.step(`▶ 长剪短:${basename8(inputAbs)}(${opts.splitScreen ? "720p 代理 · 智能分屏" : "纯选段 · 音频上传"},格式 ${formats.join("/")})`);
9957
+ const draftTarget = draftRoot ? join19(draftRoot, outName) : undefined;
9958
+ log.step(`▶ 长剪短:${basename9(inputAbs)}(${opts.splitScreen ? "720p 代理 · 智能分屏" : "纯选段 · 音频上传"},格式 ${formats.join("/")})`);
9295
9959
  log.step("① 本地预处理(探几何 + 抽音频/720p 代理)…");
9296
9960
  const geo = probeGeometry(inputAbs, opts.ffmpegPath);
9297
9961
  log.info(`原片几何 ${geo.width}x${geo.height} @ ${geo.fps.toFixed(2)}fps · ${(geo.duration / 60).toFixed(1)}min`);
9298
9962
  const artifact = opts.splitScreen ? await compress720p(inputAbs, opts.ffmpegPath) : await extractAudio(inputAbs, opts.ffmpegPath);
9299
9963
  assertDurationConsistent(geo.duration, artifact, opts.ffmpegPath);
9300
- log.info(opts.splitScreen ? `已压 720p 代理(上传物):${basename8(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename8(artifact)}`);
9964
+ log.info(opts.splitScreen ? `已压 720p 代理(上传物):${basename9(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename9(artifact)}`);
9301
9965
  const payloadProbe = buildLong2ShortPayload("__dry_run__", opts, geo, inputAbs, formats, draftTarget);
9302
9966
  log.step("② 上传抽出物到云端…");
9303
9967
  const submitted = await uploadAndSubmitTask(cfg, artifact, TASK_TYPE2, (fid) => buildLong2ShortPayload(fid, opts, geo, inputAbs, formats, draftTarget), {
@@ -9311,7 +9975,7 @@ async function runLong2Short(input, opts) {
9311
9975
  const { taskId, fileId } = { taskId: submitted.taskId, fileId: submitted.fileId };
9312
9976
  log.info(`task_id = ${taskId}`);
9313
9977
  await mkdir7(outDir, { recursive: true });
9314
- await writeFile7(join17(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE2, fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
9978
+ await writeFile8(join19(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE2, fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
9315
9979
  log.step("④ 云端处理中(选段/跳剪" + (opts.splitScreen ? "/分屏" : "") + ",每 5s 轮询)…");
9316
9980
  const output = await pollToolTask(cfg, TASK_TYPE2, taskId, {
9317
9981
  timeoutMs: POLL_TIMEOUT_MS,
@@ -9334,14 +9998,14 @@ async function runLong2Short(input, opts) {
9334
9998
  continue;
9335
9999
  }
9336
10000
  if (!/^(?:[A-Za-z]:[\\/]|[\\/])/.test(dest))
9337
- dest = join17(dirname5(inputAbs), dest);
10001
+ dest = join19(dirname7(inputAbs), dest);
9338
10002
  try {
9339
- await mkdir7(dirname5(dest), { recursive: true });
10003
+ await mkdir7(dirname7(dest), { recursive: true });
9340
10004
  await download(url, dest);
9341
10005
  splitLanded++;
9342
10006
  } catch (e) {
9343
- errors[`split:${basename8(dest)}`] = e instanceof Error ? e.message : String(e);
9344
- log.warn(`分屏素材下载失败(${basename8(dest)}),对应工程中该素材将缺席`);
10007
+ errors[`split:${basename9(dest)}`] = e instanceof Error ? e.message : String(e);
10008
+ log.warn(`分屏素材下载失败(${basename9(dest)}),对应工程中该素材将缺席`);
9345
10009
  }
9346
10010
  }
9347
10011
  if (splitLanded)
@@ -9350,7 +10014,7 @@ async function runLong2Short(input, opts) {
9350
10014
  log.step(`⑥ 逐 clip 拉回三方工程(共 ${clips.length} 条)…`);
9351
10015
  const clipResults = [];
9352
10016
  for (const [i, clip] of clips.entries()) {
9353
- const clipDir = join17(outDir, `clip${i}`);
10017
+ const clipDir = join19(outDir, `clip${i}`);
9354
10018
  const { files: clipFiles, ...clipMeta } = clip;
9355
10019
  try {
9356
10020
  const r = await materializeResult({
@@ -9391,13 +10055,13 @@ async function runLong2Short(input, opts) {
9391
10055
  for (const [i, cr] of clipResults.entries()) {
9392
10056
  try {
9393
10057
  await mkdir7(cr.dir, { recursive: true });
9394
- await writeFile7(join17(cr.dir, "clip.md"), renderClipBrief(clips[i], i, cr.files));
10058
+ await writeFile8(join19(cr.dir, "clip.md"), renderClipBrief(clips[i], i, cr.files));
9395
10059
  } catch (e) {
9396
10060
  errors[`clip${i}:brief`] = e instanceof Error ? e.message : String(e);
9397
10061
  }
9398
10062
  }
9399
10063
  try {
9400
- await writeFile7(join17(outDir, "clips.md"), renderClipsOverview(clips, {
10064
+ await writeFile8(join19(outDir, "clips.md"), renderClipsOverview(clips, {
9401
10065
  source: inputAbs,
9402
10066
  jumpCut: opts.jumpCut !== false,
9403
10067
  splitMaterials: { landed: splitLanded, total: manifest.length },
@@ -9407,7 +10071,7 @@ async function runLong2Short(input, opts) {
9407
10071
  } catch (e) {
9408
10072
  errors["clips.md"] = e instanceof Error ? e.message : String(e);
9409
10073
  }
9410
- await writeFile7(join17(outDir, "report.json"), JSON.stringify(output.report ?? {}, null, 2));
10074
+ await writeFile8(join19(outDir, "report.json"), JSON.stringify(output.report ?? {}, null, 2));
9411
10075
  const ok = Object.keys(errors).length === 0 && clipResults.some((c3) => c3.ok);
9412
10076
  const rootResult = {
9413
10077
  ok,
@@ -9418,10 +10082,10 @@ async function runLong2Short(input, opts) {
9418
10082
  outDir,
9419
10083
  clips: clipResults,
9420
10084
  splitMaterials: { landed: splitLanded, total: manifest.length },
9421
- reportFile: join17(outDir, "report.json"),
10085
+ reportFile: join19(outDir, "report.json"),
9422
10086
  ...Object.keys(errors).length ? { errors } : {}
9423
10087
  };
9424
- await writeFile7(join17(outDir, "result.json"), JSON.stringify({ ...rootResult, finishedAt: new Date().toISOString() }, null, 2));
10088
+ await writeFile8(join19(outDir, "result.json"), JSON.stringify({ ...rootResult, finishedAt: new Date().toISOString() }, null, 2));
9425
10089
  if (opts.json)
9426
10090
  console.log(JSON.stringify(rootResult));
9427
10091
  if (opts.open) {
@@ -9437,7 +10101,7 @@ async function runLong2Short(input, opts) {
9437
10101
  }
9438
10102
 
9439
10103
  // src/commands/oralcut-result.ts
9440
- import { resolve as resolve7, join as join18 } from "node:path";
10104
+ import { resolve as resolve7, join as join20 } from "node:path";
9441
10105
  var TASK_TYPE3 = "cli/video_oral_cut_for_cli";
9442
10106
  function timestamp3() {
9443
10107
  const d = new Date;
@@ -9471,7 +10135,7 @@ async function runOralCutResult(taskId, opts) {
9471
10135
  const pct = got.progress != null ? ` ${Math.round(got.progress)}%` : "";
9472
10136
  throw new Error(`任务尚未完成(当前 ${got.status || "未知"}${pct}),暂无法取回结果;请稍后再试。`);
9473
10137
  }
9474
- const outDir = resolve7(opts.out ?? join18(process.cwd(), `${taskId}-video-project-${timestamp3()}`));
10138
+ const outDir = resolve7(opts.out ?? join20(process.cwd(), `${taskId}-video-project-${timestamp3()}`));
9475
10139
  const draftDir = resolveJianyingDraftDir(opts.jianyingDraftDir);
9476
10140
  await materializeResult({
9477
10141
  outDir,
@@ -9489,10 +10153,10 @@ async function runOralCutResult(taskId, opts) {
9489
10153
  }
9490
10154
 
9491
10155
  // src/commands/upgrade.ts
9492
- import { spawnSync as spawnSync4 } from "node:child_process";
10156
+ import { spawnSync as spawnSync5 } from "node:child_process";
9493
10157
  var CLIENT_UPGRADE = "irm https://api.ai-mcn.tv:9000/broadcast/exe/install.ps1 | iex";
9494
10158
  function run(cmd) {
9495
- const r = spawnSync4(cmd, { stdio: "inherit", shell: true });
10159
+ const r = spawnSync5(cmd, { stdio: "inherit", shell: true });
9496
10160
  return r.status ?? 1;
9497
10161
  }
9498
10162
  function registerUpgrade(program2) {
@@ -9531,17 +10195,17 @@ function registerUpgrade(program2) {
9531
10195
  }
9532
10196
 
9533
10197
  // src/commands/render.ts
9534
- import { resolve as resolve8, dirname as dirname6, join as join19, basename as basename9, extname as extname6 } from "node:path";
9535
- import { existsSync as existsSync17 } from "node:fs";
10198
+ import { resolve as resolve8, dirname as dirname8, join as join21, basename as basename10, extname as extname6 } from "node:path";
10199
+ import { existsSync as existsSync18 } from "node:fs";
9536
10200
  function registerRender(program2) {
9537
10201
  program2.command("render <gtrk>").description("本地渲染:gtrk 工程按 EDL 用本地 ffmpeg 渲染成片 mp4(素材取原片本地路径)").option("-o, --out <file>", "输出 mp4 路径(缺省 = <gtrk 同目录>/<gtrk 名>.mp4)").option("--crf <n>", "视频质量 CRF 14-28(越小越清晰/文件越大,默认 18)").option("--codec <c>", "视频编码(默认 h264)").option("--ffmpeg-path <dir>", "指定 ffmpeg/ffprobe 所在目录(缺省 ~/.gitruck/ffmpeg → 系统)").option("--no-open", "完成后不自动打开产物目录").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (gtrk, opts) => {
9538
10202
  if (opts.json)
9539
10203
  routeLogsToStderr();
9540
10204
  const gtrkAbs = resolve8(gtrk);
9541
- if (!existsSync17(gtrkAbs))
10205
+ if (!existsSync18(gtrkAbs))
9542
10206
  throw new Error(`gtrk 工程不存在:${gtrkAbs}`);
9543
- const outMp4 = resolve8(opts.out ?? join19(dirname6(gtrkAbs), `${basename9(gtrkAbs, extname6(gtrkAbs))}.mp4`));
9544
- log.step(`▶ 本地渲染:${basename9(gtrkAbs)} → ${basename9(outMp4)}`);
10207
+ const outMp4 = resolve8(opts.out ?? join21(dirname8(gtrkAbs), `${basename10(gtrkAbs, extname6(gtrkAbs))}.mp4`));
10208
+ log.step(`▶ 本地渲染:${basename10(gtrkAbs)} → ${basename10(outMp4)}`);
9545
10209
  const project = await readGtrkFile(gtrkAbs);
9546
10210
  const result = await renderGtrk(project, outMp4, {
9547
10211
  crf: opts.crf != null ? Number(opts.crf) : undefined,
@@ -9556,7 +10220,7 @@ function registerRender(program2) {
9556
10220
  log.tickEnd();
9557
10221
  log.ok(`渲染完成:${outMp4}(${result.duration.toFixed(1)}s)`);
9558
10222
  if (opts.open)
9559
- openFolder(dirname6(outMp4));
10223
+ openFolder(dirname8(outMp4));
9560
10224
  if (opts.json) {
9561
10225
  console.log(JSON.stringify({ ok: true, output: outMp4, duration: result.duration }));
9562
10226
  }
@@ -9564,14 +10228,14 @@ function registerRender(program2) {
9564
10228
  }
9565
10229
 
9566
10230
  // src/commands/split.ts
9567
- import { resolve as resolve9, join as join21, dirname as dirname8, basename as basename11 } from "node:path";
9568
- import { existsSync as existsSync18 } from "node:fs";
9569
- import { readFile as readFile5, writeFile as writeFile8, mkdir as mkdir8 } from "node:fs/promises";
9570
- import { createHash } from "node:crypto";
10231
+ import { resolve as resolve9, join as join23, dirname as dirname10, basename as basename12 } from "node:path";
10232
+ import { existsSync as existsSync19 } from "node:fs";
10233
+ import { readFile as readFile6, writeFile as writeFile9, mkdir as mkdir8 } from "node:fs/promises";
10234
+ import { createHash as createHash2 } from "node:crypto";
9571
10235
 
9572
10236
  // src/lib/gtrk-writeback.ts
9573
- import { readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "node:fs";
9574
- import { dirname as dirname7, join as join20, basename as basename10 } from "node:path";
10237
+ import { readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync2, renameSync as renameSync2, unlinkSync } from "node:fs";
10238
+ import { dirname as dirname9, join as join22, basename as basename11 } from "node:path";
9575
10239
  import { randomBytes as randomBytes2 } from "node:crypto";
9576
10240
  function readGtrk(path) {
9577
10241
  const raw = readFileSync5(path, "utf8");
@@ -9598,10 +10262,10 @@ function writeStructMetaSplit(path, gtrk, splitObj, expectedMtimeMs) {
9598
10262
  }
9599
10263
  const nextStructMeta = { ...gtrk.struct_meta ?? {}, split: splitObj };
9600
10264
  const next = { ...gtrk, struct_meta: nextStructMeta };
9601
- const tmp = join20(dirname7(path), `.${basename10(path)}.${randomBytes2(6).toString("hex")}.tmp`);
10265
+ const tmp = join22(dirname9(path), `.${basename11(path)}.${randomBytes2(6).toString("hex")}.tmp`);
9602
10266
  try {
9603
10267
  writeFileSync2(tmp, JSON.stringify(next, null, 2));
9604
- renameSync(tmp, path);
10268
+ renameSync2(tmp, path);
9605
10269
  } catch (e) {
9606
10270
  try {
9607
10271
  unlinkSync(tmp);
@@ -9614,10 +10278,10 @@ function writeGtrkAtomic(path, next, expectedMtimeMs) {
9614
10278
  if (cur !== expectedMtimeMs) {
9615
10279
  throw new Error("工程文件在 matrix 运行期间被外部修改(保存冲突),已拒绝写入;请关闭客户端未保存的工程或重跑(plan 与已下载代理均保留)");
9616
10280
  }
9617
- const tmp = join20(dirname7(path), `.${basename10(path)}.${randomBytes2(6).toString("hex")}.tmp`);
10281
+ const tmp = join22(dirname9(path), `.${basename11(path)}.${randomBytes2(6).toString("hex")}.tmp`);
9618
10282
  try {
9619
10283
  writeFileSync2(tmp, JSON.stringify(next, null, 2));
9620
- renameSync(tmp, path);
10284
+ renameSync2(tmp, path);
9621
10285
  } catch (e) {
9622
10286
  try {
9623
10287
  unlinkSync(tmp);
@@ -9634,7 +10298,7 @@ function registerSplit(program2) {
9634
10298
  });
9635
10299
  }
9636
10300
  function firstExisting(cands) {
9637
- return cands.find((p) => existsSync18(p));
10301
+ return cands.find((p) => existsSync19(p));
9638
10302
  }
9639
10303
  function resolvePaths(opts) {
9640
10304
  const project = opts.project ? resolve9(opts.project) : undefined;
@@ -9642,22 +10306,22 @@ function resolvePaths(opts) {
9642
10306
  if (opts.gtrk) {
9643
10307
  gtrkPath = resolve9(opts.gtrk);
9644
10308
  } else if (project) {
9645
- gtrkPath = firstExisting([join21(project, "gtrk", "project.gtrk"), join21(project, "project.gtrk")]) ?? join21(project, "gtrk", "project.gtrk");
10309
+ gtrkPath = firstExisting([join23(project, "gtrk", "project.gtrk"), join23(project, "project.gtrk")]) ?? join23(project, "gtrk", "project.gtrk");
9646
10310
  } else {
9647
10311
  throw new Error("需 --project <目录> 或显式 --gtrk <path>");
9648
10312
  }
9649
- if (!existsSync18(gtrkPath))
10313
+ if (!existsSync19(gtrkPath))
9650
10314
  throw new Error(`找不到工程文件:${gtrkPath}`);
9651
10315
  let transcriptPath;
9652
10316
  if (opts.transcript)
9653
10317
  transcriptPath = resolve9(opts.transcript);
9654
10318
  else if (project)
9655
10319
  transcriptPath = firstExisting([
9656
- join21(project, "transcript", "transcript.json"),
9657
- join21(project, "json", "transcript.json"),
9658
- join21(project, "transcript.json")
10320
+ join23(project, "transcript", "transcript.json"),
10321
+ join23(project, "json", "transcript.json"),
10322
+ join23(project, "transcript.json")
9659
10323
  ]);
9660
- const baseDir = project ?? dirname8(gtrkPath);
10324
+ const baseDir = project ?? dirname10(gtrkPath);
9661
10325
  return { baseDir, gtrkPath, transcriptPath };
9662
10326
  }
9663
10327
  function slugify2(name) {
@@ -9665,11 +10329,11 @@ function slugify2(name) {
9665
10329
  return s || "project";
9666
10330
  }
9667
10331
  async function loadTranscript(path) {
9668
- const t = JSON.parse(await readFile5(path, "utf8"));
10332
+ const t = JSON.parse(await readFile6(path, "utf8"));
9669
10333
  if (!t || !Array.isArray(t.utterances) || typeof t.material_id !== "string" || typeof t.text_hash !== "string") {
9670
10334
  throw new Error(`transcript.json 结构异常(缺 utterances/material_id/text_hash):${path}`);
9671
10335
  }
9672
- t.text_hash = createHash("sha256").update(t.utterances.map((u) => u.text ?? "").join(`
10336
+ t.text_hash = createHash2("sha256").update(t.utterances.map((u) => u.text ?? "").join(`
9673
10337
  `), "utf8").digest("hex");
9674
10338
  return t;
9675
10339
  }
@@ -9680,16 +10344,16 @@ async function runSplit(splitdoc, opts) {
9680
10344
  return splitdoc ? runLand(resolve9(splitdoc), baseDir, gtrkPath, transcriptPath, opts) : runView(baseDir, gtrkPath, transcriptPath, opts);
9681
10345
  }
9682
10346
  async function runView(baseDir, gtrkPath, transcriptPath, opts) {
9683
- if (!transcriptPath || !existsSync18(transcriptPath))
10347
+ if (!transcriptPath || !existsSync19(transcriptPath))
9684
10348
  throw new Error(TRANSCRIPT_MISSING);
9685
10349
  log.step("▶ 导出投影视图(transcript × 当刻 .gtrk)…");
9686
10350
  const transcript = await loadTranscript(transcriptPath);
9687
10351
  const { gtrk } = readGtrk(gtrkPath);
9688
10352
  const view = projectTranscript(transcript, gtrk, { words: opts.words });
9689
- const splitDir = join21(baseDir, "split");
10353
+ const splitDir = join23(baseDir, "split");
9690
10354
  await mkdir8(splitDir, { recursive: true });
9691
- const viewPath = join21(splitDir, "view.json");
9692
- await writeFile8(viewPath, JSON.stringify(view, null, 2));
10355
+ const viewPath = join23(splitDir, "view.json");
10356
+ await writeFile9(viewPath, JSON.stringify(view, null, 2));
9693
10357
  const dropped = view.utterances.filter((u) => u.dropped).length;
9694
10358
  log.ok(`投影视图已生成:${viewPath}(${view.utterances.length} 条,其中 ${dropped} 条被剪)`);
9695
10359
  const result = {
@@ -9706,12 +10370,12 @@ async function runView(baseDir, gtrkPath, transcriptPath, opts) {
9706
10370
  return result;
9707
10371
  }
9708
10372
  async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
9709
- if (!existsSync18(splitdocPath))
10373
+ if (!existsSync19(splitdocPath))
9710
10374
  throw new Error(`找不到拆分稿:${splitdocPath}`);
9711
- if (!transcriptPath || !existsSync18(transcriptPath))
10375
+ if (!transcriptPath || !existsSync19(transcriptPath))
9712
10376
  throw new Error(TRANSCRIPT_MISSING);
9713
10377
  log.step("▶ 校验拆分稿并落地…");
9714
- const doc = JSON.parse(await readFile5(splitdocPath, "utf8"));
10378
+ const doc = JSON.parse(await readFile6(splitdocPath, "utf8"));
9715
10379
  const transcript = await loadTranscript(transcriptPath);
9716
10380
  const { gtrk, mtimeMs } = readGtrk(gtrkPath);
9717
10381
  assertGtrkV1(gtrk);
@@ -9734,7 +10398,7 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
9734
10398
  }
9735
10399
  const projectedAt = new Date().toISOString();
9736
10400
  const view = projectTranscript(transcript, gtrk, { projectedAt });
9737
- const projectSlug = slugify2(basename11(baseDir));
10401
+ const projectSlug = slugify2(basename12(baseDir));
9738
10402
  const landing = buildLanding(doc, view, {
9739
10403
  utteranceIds: ctx.utteranceIds,
9740
10404
  projectSlug,
@@ -9745,14 +10409,14 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
9745
10409
  }
9746
10410
  });
9747
10411
  writeStructMetaSplit(gtrkPath, gtrk, landing.split, mtimeMs);
9748
- const splitDir = join21(baseDir, "split");
10412
+ const splitDir = join23(baseDir, "split");
9749
10413
  await mkdir8(splitDir, { recursive: true });
9750
- const dispatchPath = join21(splitDir, "dispatch.json");
9751
- await writeFile8(dispatchPath, JSON.stringify(landing.dispatch, null, 2));
10414
+ const dispatchPath = join23(splitDir, "dispatch.json");
10415
+ await writeFile9(dispatchPath, JSON.stringify(landing.dispatch, null, 2));
9752
10416
  let mdPath = null;
9753
10417
  if (opts.md) {
9754
- mdPath = join21(splitDir, "visual-split.md");
9755
- await writeFile8(mdPath, renderSplitMarkdown(doc, landing, { projectSlug, projectedAt }));
10418
+ mdPath = join23(splitDir, "visual-split.md");
10419
+ await writeFile9(mdPath, renderSplitMarkdown(doc, landing, { projectSlug, projectedAt }));
9756
10420
  }
9757
10421
  log.ok(`落地完成:${landing.split.beats.length}/${doc.beats.length} beat 落轨` + `(MG ${landing.dispatch.mg.length} · FILM_BROLL ${landing.dispatch.film_broll.length} · AI_DRAMA ${landing.dispatch.ai_drama.length})`);
9758
10422
  for (const s of landing.skipped)
@@ -9783,9 +10447,9 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
9783
10447
  }
9784
10448
 
9785
10449
  // src/commands/matrix.ts
9786
- import { resolve as resolve11, join as join22, dirname as dirname9, basename as basename12 } from "node:path";
9787
- import { existsSync as existsSync20 } from "node:fs";
9788
- import { readFile as readFile6, writeFile as writeFile9, mkdir as mkdir9, rename } from "node:fs/promises";
10450
+ import { resolve as resolve11, join as join24, dirname as dirname11, basename as basename13 } from "node:path";
10451
+ import { existsSync as existsSync21 } from "node:fs";
10452
+ import { readFile as readFile7, writeFile as writeFile10, mkdir as mkdir9, rename } from "node:fs/promises";
9789
10453
 
9790
10454
  // src/lib/solid-png.ts
9791
10455
  import { deflateSync } from "node:zlib";
@@ -10521,7 +11185,7 @@ function layBrollTracks(opts) {
10521
11185
  }
10522
11186
 
10523
11187
  // src/lib/material-integrity.ts
10524
- import { existsSync as existsSync19 } from "node:fs";
11188
+ import { existsSync as existsSync20 } from "node:fs";
10525
11189
  import { resolve as resolve10 } from "node:path";
10526
11190
  var INTEGRITY_LIST_CAP = 10;
10527
11191
  var r35 = (n) => Math.round(n * 1000) / 1000;
@@ -10582,7 +11246,7 @@ function collectMaterialRefs(gtrk) {
10582
11246
  return out;
10583
11247
  }
10584
11248
  function checkMaterialIntegrity(opts) {
10585
- const exists = opts.exists ?? ((p) => existsSync19(p));
11249
+ const exists = opts.exists ?? ((p) => existsSync20(p));
10586
11250
  const materials = Array.isArray(opts.gtrk.materials) ? opts.gtrk.materials : [];
10587
11251
  const refs = collectMaterialRefs(opts.gtrk);
10588
11252
  const counts = { relative: 0, absolute: 0, remote: 0, noPath: 0 };
@@ -10925,16 +11589,16 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
10925
11589
  let baseDir;
10926
11590
  if (opts.dispatch) {
10927
11591
  dispatchPath = resolve11(opts.dispatch);
10928
- baseDir = dirname9(dirname9(dispatchPath));
11592
+ baseDir = dirname11(dirname11(dispatchPath));
10929
11593
  } else if (opts.project) {
10930
11594
  baseDir = resolve11(opts.project);
10931
- dispatchPath = join22(baseDir, "split", "dispatch.json");
11595
+ dispatchPath = join24(baseDir, "split", "dispatch.json");
10932
11596
  } else {
10933
11597
  throw new Error('需 --project <目录> 或显式 --dispatch <path>(ad-hoc 检索用:gtrk matrix search "<query>")');
10934
11598
  }
10935
- if (!existsSync20(dispatchPath))
11599
+ if (!existsSync21(dispatchPath))
10936
11600
  throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split <拆分稿> 落地派单)`);
10937
- const dispatch = JSON.parse(await readFile6(dispatchPath, "utf8"));
11601
+ const dispatch = JSON.parse(await readFile7(dispatchPath, "utf8"));
10938
11602
  const rawQueue = Array.isArray(dispatch.film_broll) ? dispatch.film_broll : [];
10939
11603
  const earlyGtrkPath = locateGtrk(baseDir);
10940
11604
  let earlyGtrk;
@@ -10989,7 +11653,7 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
10989
11653
  if (totalQueries > 0 && okCount === 0) {
10990
11654
  throw new Error(`全部 ${totalQueries} 个 query 检索失败,未写入 plan(逐条原因见上方日志)`);
10991
11655
  }
10992
- const projectSlug = slugify3(basename12(baseDir));
11656
+ const projectSlug = slugify3(basename13(baseDir));
10993
11657
  const plan = buildPlan({
10994
11658
  generatedAt: new Date().toISOString(),
10995
11659
  memberType: tier,
@@ -10997,10 +11661,10 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
10997
11661
  columnId,
10998
11662
  beats
10999
11663
  });
11000
- const splitDir = join22(baseDir, "split");
11664
+ const splitDir = join24(baseDir, "split");
11001
11665
  await mkdir9(splitDir, { recursive: true });
11002
- const planPath = join22(splitDir, "broll-plan.json");
11003
- await writeFile9(planPath, JSON.stringify(plan, null, 2));
11666
+ const planPath = join24(splitDir, "broll-plan.json");
11667
+ await writeFile10(planPath, JSON.stringify(plan, null, 2));
11004
11668
  log.ok(`候选清单已生成:${planPath}(${beats.length} beat · ${okCount}/${totalQueries} query 成功 · ${resultCount} 条候选)`);
11005
11669
  log.info("清单只含引用不含素材:cover_url 可直接预览;url 带签名默认 24h 过期,过期重跑本命令即重签。");
11006
11670
  const layN = parseLay(opts.lay);
@@ -11047,13 +11711,13 @@ function parseScoreFloor(raw) {
11047
11711
  return SCORE_FLOOR_DEFAULT;
11048
11712
  }
11049
11713
  function locateGtrk(baseDir) {
11050
- const cands = [join22(baseDir, "gtrk", "project.gtrk"), join22(baseDir, "project.gtrk")];
11051
- return cands.find((p) => existsSync20(p));
11714
+ const cands = [join24(baseDir, "gtrk", "project.gtrk"), join24(baseDir, "project.gtrk")];
11715
+ return cands.find((p) => existsSync21(p));
11052
11716
  }
11053
11717
  async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRelay, reproj) {
11054
11718
  const gtrkPath = locateGtrk(baseDir);
11055
11719
  if (!gtrkPath) {
11056
- log.warn(`未找到工程文件(${join22(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——plan 已产出,可后续在有工程的目录重跑`);
11720
+ log.warn(`未找到工程文件(${join24(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——plan 已产出,可后续在有工程的目录重跑`);
11057
11721
  return;
11058
11722
  }
11059
11723
  const { gtrk, mtimeMs } = readGtrk(gtrkPath);
@@ -11061,8 +11725,8 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
11061
11725
  const { fills, clipIds } = planBeatFills(plan, layN, scoreFloor);
11062
11726
  const slotCount = [...fills.values()].flat().reduce((n, s) => n + s.length, 0);
11063
11727
  log.step(`▶ 候选铺轨(${layN} 轨 · 平铺 ${slotCount} 槽位 · ${clipIds.size} 个 clip,preview 代理)…`);
11064
- const gtrkDir = dirname9(gtrkPath);
11065
- const previewDir = join22(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
11728
+ const gtrkDir = dirname11(gtrkPath);
11729
+ const previewDir = join24(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
11066
11730
  await mkdir9(previewDir, { recursive: true });
11067
11731
  const prevSource = new Map;
11068
11732
  const prevBroll = gtrk.struct_meta?.broll;
@@ -11085,8 +11749,8 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
11085
11749
  if (!cand)
11086
11750
  continue;
11087
11751
  const rel = `${BROLL_PREVIEW_DIR}/${clipId}.mp4`;
11088
- const abs = join22(gtrkDir, ...rel.split("/"));
11089
- if (existsSync20(abs)) {
11752
+ const abs = join24(gtrkDir, ...rel.split("/"));
11753
+ if (existsSync21(abs)) {
11090
11754
  const prev = prevSource.get(clipId);
11091
11755
  if (prev !== "raw") {
11092
11756
  downloads.set(clipId, { rel, source: prev ?? "preview" });
@@ -11148,12 +11812,12 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
11148
11812
  const canvas = gtrk.video_size;
11149
11813
  const spec = { hex: BLACK_BED_HEX, width: canvas[0], height: canvas[1] };
11150
11814
  const rel = solidRelPath(spec);
11151
- const abs = join22(gtrkDir, ...rel.split("/"));
11815
+ const abs = join24(gtrkDir, ...rel.split("/"));
11152
11816
  try {
11153
- if (!existsSync20(abs)) {
11154
- await mkdir9(dirname9(abs), { recursive: true });
11817
+ if (!existsSync21(abs)) {
11818
+ await mkdir9(dirname11(abs), { recursive: true });
11155
11819
  const tmp = `${abs}.tmp-${process.pid}`;
11156
- await writeFile9(tmp, encodeSolidPng(spec));
11820
+ await writeFile10(tmp, encodeSolidPng(spec));
11157
11821
  await rename(tmp, abs);
11158
11822
  }
11159
11823
  } catch (e) {
@@ -11216,7 +11880,7 @@ async function downloadProxy(cand, absPath, opts = {}) {
11216
11880
  if (previewUrl) {
11217
11881
  const bytes = await tryFetch(previewUrl);
11218
11882
  if (bytes) {
11219
- await writeFile9(absPath, bytes);
11883
+ await writeFile10(absPath, bytes);
11220
11884
  return "preview";
11221
11885
  }
11222
11886
  }
@@ -11224,7 +11888,7 @@ async function downloadProxy(cand, absPath, opts = {}) {
11224
11888
  return null;
11225
11889
  const raw = await tryFetch(cand.url);
11226
11890
  if (raw) {
11227
- await writeFile9(absPath, raw);
11891
+ await writeFile10(absPath, raw);
11228
11892
  log.warn(`clip ${cand.clip_id} 无 preview 代理,已回落原片(${(raw.length / 1048576).toFixed(1)}MB)`);
11229
11893
  return "raw";
11230
11894
  }
@@ -11247,7 +11911,7 @@ async function runAdhoc(query, cfg, tier, broll, overrides, columnId, opts) {
11247
11911
  };
11248
11912
  if (opts.out) {
11249
11913
  const outPath = resolve11(opts.out);
11250
- await writeFile9(outPath, JSON.stringify({ query, recalled: data.recalled, results }, null, 2));
11914
+ await writeFile10(outPath, JSON.stringify({ query, recalled: data.recalled, results }, null, 2));
11251
11915
  log.ok(`结果已落盘:${outPath}`);
11252
11916
  result.outPath = outPath;
11253
11917
  } else if (!opts.json) {
@@ -11266,9 +11930,9 @@ function slugify3(name) {
11266
11930
  }
11267
11931
 
11268
11932
  // src/commands/mg.ts
11269
- import { resolve as resolve12, join as join23, dirname as dirname10, basename as basename13 } from "node:path";
11270
- import { existsSync as existsSync21 } from "node:fs";
11271
- import { readFile as readFile7, mkdir as mkdir10, copyFile } from "node:fs/promises";
11933
+ import { resolve as resolve12, join as join25, dirname as dirname12, basename as basename14 } from "node:path";
11934
+ import { existsSync as existsSync22 } from "node:fs";
11935
+ import { readFile as readFile8, mkdir as mkdir10, copyFile } from "node:fs/promises";
11272
11936
 
11273
11937
  // src/lib/mg-lint.ts
11274
11938
  var CDN_OK = [/lib\.baomitu\.com/i, /cdnjs\.cloudflare\.com/i, /unpkg\.com/i];
@@ -12929,29 +13593,29 @@ async function runMg(words, opts) {
12929
13593
  function resolveDispatch(opts) {
12930
13594
  if (opts.dispatch) {
12931
13595
  const dispatchPath = resolve12(opts.dispatch);
12932
- return { dispatchPath, baseDir: dirname10(dirname10(dispatchPath)) };
13596
+ return { dispatchPath, baseDir: dirname12(dirname12(dispatchPath)) };
12933
13597
  }
12934
13598
  if (opts.project) {
12935
13599
  const baseDir = resolve12(opts.project);
12936
- return { dispatchPath: join23(baseDir, "split", "dispatch.json"), baseDir };
13600
+ return { dispatchPath: join25(baseDir, "split", "dispatch.json"), baseDir };
12937
13601
  }
12938
13602
  throw new Error("需 --project <目录> 或显式 --dispatch <path>");
12939
13603
  }
12940
13604
  function locateGtrk2(baseDir) {
12941
- return [join23(baseDir, "gtrk", "project.gtrk"), join23(baseDir, "project.gtrk")].find((p) => existsSync21(p));
13605
+ return [join25(baseDir, "gtrk", "project.gtrk"), join25(baseDir, "project.gtrk")].find((p) => existsSync22(p));
12942
13606
  }
12943
13607
  function locateSrcHtml(baseDir, compositionId) {
12944
13608
  for (const d of MG_SRC_DIRS) {
12945
- const p = join23(baseDir, d, `${compositionId}.html`);
12946
- if (existsSync21(p))
13609
+ const p = join25(baseDir, d, `${compositionId}.html`);
13610
+ if (existsSync22(p))
12947
13611
  return p;
12948
13612
  }
12949
13613
  return;
12950
13614
  }
12951
13615
  async function readMgQueue(dispatchPath) {
12952
- if (!existsSync21(dispatchPath))
13616
+ if (!existsSync22(dispatchPath))
12953
13617
  throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split 落地派单)`);
12954
- const dispatch = JSON.parse(await readFile7(dispatchPath, "utf8"));
13618
+ const dispatch = JSON.parse(await readFile8(dispatchPath, "utf8"));
12955
13619
  const queue = dispatch.mg ?? dispatch.rrv_mg;
12956
13620
  return Array.isArray(queue) ? queue : [];
12957
13621
  }
@@ -13025,10 +13689,10 @@ async function runLay(opts) {
13025
13689
  const srcPath = locateSrcHtml(baseDir, q.composition_id);
13026
13690
  if (!srcPath) {
13027
13691
  skipped.push({ beat: q.beat, reason: "缺颗粒 HTML(未产出)" });
13028
- log.warn(`${q.beat}:缺 ${join23(baseDir, MG_SRC_DIRS[0], `${q.composition_id}.html`)},跳过`);
13692
+ log.warn(`${q.beat}:缺 ${join25(baseDir, MG_SRC_DIRS[0], `${q.composition_id}.html`)},跳过`);
13029
13693
  continue;
13030
13694
  }
13031
- const html = await readFile7(srcPath, "utf8");
13695
+ const html = await readFile8(srcPath, "utf8");
13032
13696
  const category = typeof q.category === "string" ? q.category : undefined;
13033
13697
  const slotDuration = Math.round((win.track_ed - win.track_st) * 1000) / 1000;
13034
13698
  const lint = lintParticle(html, {
@@ -13073,7 +13737,7 @@ async function runLay(opts) {
13073
13737
  });
13074
13738
  }
13075
13739
  if (!gtrkPath || !project) {
13076
- log.warn(`未找到工程文件(${join23(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——lint 已完成`);
13740
+ log.warn(`未找到工程文件(${join25(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——lint 已完成`);
13077
13741
  return done(opts, {
13078
13742
  ok: skipped.length === 0,
13079
13743
  mode: "lay",
@@ -13090,7 +13754,7 @@ async function runLay(opts) {
13090
13754
  });
13091
13755
  }
13092
13756
  const { gtrk, mtimeMs } = project;
13093
- const gtrkDir = dirname10(gtrkPath);
13757
+ const gtrkDir = dirname12(gtrkPath);
13094
13758
  const covered = new Set(items.map((it) => it.composition_id));
13095
13759
  const orphans = laidBefore.filter((id) => !covered.has(id));
13096
13760
  const inDispatch = new Set(allQueue.map((q) => q.composition_id));
@@ -13132,9 +13796,9 @@ async function runLay(opts) {
13132
13796
  if (opts.replaceAll && orphans.length > 0) {
13133
13797
  log.warn(`--replace-all:已显式授权重置整轨,轨上其余 ${orphans.length} 颗已铺颗粒将被剥离(不走增量保留)。`);
13134
13798
  }
13135
- await mkdir10(join23(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
13799
+ await mkdir10(join25(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
13136
13800
  for (const it of items) {
13137
- await copyFile(srcByComp.get(it.composition_id), join23(gtrkDir, ...it.html_rel.split("/")));
13801
+ await copyFile(srcByComp.get(it.composition_id), join25(gtrkDir, ...it.html_rel.split("/")));
13138
13802
  }
13139
13803
  const { next, summary, mg } = layMgTracks({ gtrk, items, generatedAt: new Date().toISOString(), keep });
13140
13804
  const written = withTimecodeSource(next, "mg", reproj);
@@ -13178,12 +13842,12 @@ async function runLint(args, opts) {
13178
13842
  const file = args[0];
13179
13843
  if (!file)
13180
13844
  throw new Error("用法:gtrk mg lint <particle.html> [--dispatch <path>]");
13181
- const html = await readFile7(resolve12(file), "utf8");
13182
- const nameId = basename13(file).replace(/\.html?$/i, "");
13845
+ const html = await readFile8(resolve12(file), "utf8");
13846
+ const nameId = basename14(file).replace(/\.html?$/i, "");
13183
13847
  let dispatchIds;
13184
13848
  let slotDuration;
13185
13849
  let compositionId;
13186
- if (opts.dispatch && existsSync21(resolve12(opts.dispatch))) {
13850
+ if (opts.dispatch && existsSync22(resolve12(opts.dispatch))) {
13187
13851
  const queue = await readMgQueue(resolve12(opts.dispatch));
13188
13852
  dispatchIds = queue.map((q) => q.composition_id);
13189
13853
  const byName = queue.find((q) => q.composition_id === nameId);
@@ -13207,7 +13871,7 @@ async function runLint(args, opts) {
13207
13871
  for (const vv of lint.violations)
13208
13872
  (vv.fatal ? log.err : log.warn)(`${vv.fatal ? "✗" : "·"} ${vv.law}: ${vv.msg}`);
13209
13873
  if (lint.ok)
13210
- log.ok(`lint 通过(${basename13(file)};opaque=${lint.opaque})`);
13874
+ log.ok(`lint 通过(${basename14(file)};opaque=${lint.opaque})`);
13211
13875
  else
13212
13876
  log.err(`lint 未过(${lint.violations.filter((v) => v.fatal).length} 项致命)`);
13213
13877
  const result = { mode: "lint", ...lint, ok: lint.ok };
@@ -13249,9 +13913,9 @@ function done(opts, result) {
13249
13913
  }
13250
13914
 
13251
13915
  // src/lib/mad/mad.ts
13252
- import { mkdir as mkdir13, writeFile as writeFile11 } from "node:fs/promises";
13253
- import { existsSync as existsSync24, statSync as statSync3 } from "node:fs";
13254
- import { resolve as resolve13, join as join27 } from "node:path";
13916
+ import { mkdir as mkdir13, writeFile as writeFile12 } from "node:fs/promises";
13917
+ import { existsSync as existsSync25, statSync as statSync3 } from "node:fs";
13918
+ import { resolve as resolve13, join as join29 } from "node:path";
13255
13919
 
13256
13920
  // src/lib/convert/types.ts
13257
13921
  function num2(v, d = 0) {
@@ -14030,19 +14694,19 @@ function madJsx(opts) {
14030
14694
  }
14031
14695
 
14032
14696
  // src/lib/mad/scan.ts
14033
- import { readdirSync, statSync as statSync2 } from "node:fs";
14034
- import { extname as extname7, join as join24 } from "node:path";
14697
+ import { readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
14698
+ import { extname as extname7, join as join26 } from "node:path";
14035
14699
  var VIDEO_EXTS2 = new Set(defaultExtsFor("video") ?? []);
14036
14700
  function scanFolder(dirAbs, opts = {}) {
14037
14701
  const probe = opts.probe ?? probeGeometry;
14038
14702
  const warn = opts.warn ?? (() => {});
14039
14703
  let entries;
14040
14704
  try {
14041
- entries = readdirSync(dirAbs);
14705
+ entries = readdirSync3(dirAbs);
14042
14706
  } catch {
14043
14707
  throw new Error(`素材文件夹不存在或无法读取:${dirAbs}`);
14044
14708
  }
14045
- const files = entries.filter((n) => VIDEO_EXTS2.has(extname7(n).toLowerCase())).map((n) => join24(dirAbs, n)).filter((p) => {
14709
+ const files = entries.filter((n) => VIDEO_EXTS2.has(extname7(n).toLowerCase())).map((n) => join26(dirAbs, n)).filter((p) => {
14046
14710
  try {
14047
14711
  return statSync2(p).isFile();
14048
14712
  } catch {
@@ -14220,10 +14884,10 @@ function beatQuantized(natLens, analysis) {
14220
14884
  }
14221
14885
 
14222
14886
  // src/lib/mad/data.ts
14223
- import { createHash as createHash2 } from "node:crypto";
14224
- import { mkdir as mkdir11, readFile as readFile8, rename as rename2, rm, writeFile as writeFile10, readdir as readdir2 } from "node:fs/promises";
14225
- import { existsSync as existsSync22 } from "node:fs";
14226
- import { join as join25 } from "node:path";
14887
+ import { createHash as createHash3 } from "node:crypto";
14888
+ import { mkdir as mkdir11, readFile as readFile9, rename as rename2, rm, writeFile as writeFile11, readdir as readdir2 } from "node:fs/promises";
14889
+ import { existsSync as existsSync23 } from "node:fs";
14890
+ import { join as join27 } from "node:path";
14227
14891
  function madCacheDir() {
14228
14892
  return homeFile("mad-cache");
14229
14893
  }
@@ -14235,7 +14899,7 @@ function manifestUrl() {
14235
14899
  }
14236
14900
  var REQUIRED_KEYS = ["mad_pool"];
14237
14901
  function sha256Hex(buf) {
14238
- return createHash2("sha256").update(buf).digest("hex");
14902
+ return createHash3("sha256").update(buf).digest("hex");
14239
14903
  }
14240
14904
  async function fetchWithTimeout(fetchFn, url, timeoutMs) {
14241
14905
  const ctrl = new AbortController;
@@ -14248,14 +14912,14 @@ async function fetchWithTimeout(fetchFn, url, timeoutMs) {
14248
14912
  }
14249
14913
  async function atomicWrite(dest, data) {
14250
14914
  const tmp = `${dest}.tmp-${process.pid}-${Date.now()}`;
14251
- await writeFile10(tmp, data);
14915
+ await writeFile11(tmp, data);
14252
14916
  await rename2(tmp, dest);
14253
14917
  }
14254
14918
  async function verifyFile(path, sha256) {
14255
- if (!existsSync22(path))
14919
+ if (!existsSync23(path))
14256
14920
  return false;
14257
14921
  try {
14258
- const buf = await readFile8(path);
14922
+ const buf = await readFile9(path);
14259
14923
  return sha256Hex(buf) === sha256;
14260
14924
  } catch {
14261
14925
  return false;
@@ -14280,7 +14944,7 @@ async function cleanupOldVersions(cacheRoot, keepVersion, warn) {
14280
14944
  for (const e of entries) {
14281
14945
  const m = /^v(\d+)$/.exec(e);
14282
14946
  if (m && Number(m[1]) !== keepVersion) {
14283
- await rm(join25(cacheRoot, e), { recursive: true, force: true }).catch(() => {});
14947
+ await rm(join27(cacheRoot, e), { recursive: true, force: true }).catch(() => {});
14284
14948
  }
14285
14949
  }
14286
14950
  } catch {}
@@ -14289,7 +14953,7 @@ async function ensureMadData(opts, deps) {
14289
14953
  const { cacheRoot, warn } = deps;
14290
14954
  const timeout = deps.manifestTimeoutMs ?? 8000;
14291
14955
  const mfUrl = deps.manifestUrl ?? manifestUrl();
14292
- const snapshotPath = join25(cacheRoot, "manifest.json");
14956
+ const snapshotPath = join27(cacheRoot, "manifest.json");
14293
14957
  let manifest = null;
14294
14958
  let online = false;
14295
14959
  try {
@@ -14304,11 +14968,11 @@ async function ensureMadData(opts, deps) {
14304
14968
  warn(`manifest 拉取失败(${e instanceof Error ? e.message : String(e)}),回退本地缓存`);
14305
14969
  }
14306
14970
  if (!manifest) {
14307
- if (!existsSync22(snapshotPath)) {
14971
+ if (!existsSync23(snapshotPath)) {
14308
14972
  throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
14309
14973
  }
14310
14974
  try {
14311
- manifest = validateManifest(JSON.parse(await readFile8(snapshotPath, "utf8")));
14975
+ manifest = validateManifest(JSON.parse(await readFile9(snapshotPath, "utf8")));
14312
14976
  } catch {
14313
14977
  throw new Error("本地 manifest 缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
14314
14978
  }
@@ -14319,14 +14983,14 @@ async function ensureMadData(opts, deps) {
14319
14983
  }
14320
14984
  }
14321
14985
  const version = manifest.version;
14322
- const verDir = join25(cacheRoot, `v${version}`);
14323
- const poolPath = join25(verDir, "mad_pool.json");
14986
+ const verDir = join27(cacheRoot, `v${version}`);
14987
+ const poolPath = join27(verDir, "mad_pool.json");
14324
14988
  const poolMeta = manifest.datasets.mad_pool;
14325
14989
  const cacheValid = await verifyFile(poolPath, poolMeta.sha256);
14326
14990
  const needDownload = !!opts.refresh || !cacheValid;
14327
14991
  if (needDownload) {
14328
14992
  if (!online) {
14329
- if (existsSync22(poolPath)) {
14993
+ if (existsSync23(poolPath)) {
14330
14994
  throw new Error("本地技法数据缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
14331
14995
  }
14332
14996
  throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
@@ -14350,7 +15014,7 @@ async function ensureMadData(opts, deps) {
14350
15014
  }
14351
15015
  let pool;
14352
15016
  try {
14353
- pool = JSON.parse(await readFile8(poolPath, "utf8"));
15017
+ pool = JSON.parse(await readFile9(poolPath, "utf8"));
14354
15018
  if (!Array.isArray(pool))
14355
15019
  throw new Error("mad_pool 非数组");
14356
15020
  } catch (e) {
@@ -14361,11 +15025,11 @@ async function ensureMadData(opts, deps) {
14361
15025
 
14362
15026
  // src/lib/mad/pool.ts
14363
15027
  import { gunzipSync } from "node:zlib";
14364
- import { mkdir as mkdir12, readFile as readFile9 } from "node:fs/promises";
14365
- import { existsSync as existsSync23 } from "node:fs";
14366
- import { join as join26 } from "node:path";
15028
+ import { mkdir as mkdir12, readFile as readFile10 } from "node:fs/promises";
15029
+ import { existsSync as existsSync24 } from "node:fs";
15030
+ import { join as join28 } from "node:path";
14367
15031
  function shardPath(verDir, shard) {
14368
- return join26(verDir, "ir", `${shard}.json.gz`);
15032
+ return join28(verDir, "ir", `${shard}.json.gz`);
14369
15033
  }
14370
15034
  function decodeShard(gz) {
14371
15035
  const json = gunzipSync(gz).toString("utf8");
@@ -14382,9 +15046,9 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
14382
15046
  if (cached)
14383
15047
  return cached;
14384
15048
  const path = shardPath(verDir, shard);
14385
- if (existsSync23(path)) {
15049
+ if (existsSync24(path)) {
14386
15050
  try {
14387
- const s2 = decodeShard(await readFile9(path));
15051
+ const s2 = decodeShard(await readFile10(path));
14388
15052
  memo.set(shard, s2);
14389
15053
  return s2;
14390
15054
  } catch {
@@ -14400,7 +15064,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
14400
15064
  throw new Error(`IR 分片下载失败 HTTP ${res.status}:${url}`);
14401
15065
  const buf = new Uint8Array(await res.arrayBuffer());
14402
15066
  const s = decodeShard(buf);
14403
- await mkdir12(join26(verDir, "ir"), { recursive: true });
15067
+ await mkdir12(join28(verDir, "ir"), { recursive: true });
14404
15068
  await atomicWrite(path, buf);
14405
15069
  memo.set(shard, s);
14406
15070
  return s;
@@ -14414,7 +15078,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
14414
15078
  return ir;
14415
15079
  },
14416
15080
  shardCached(shard) {
14417
- return memo.has(shard) || existsSync23(shardPath(verDir, shard));
15081
+ return memo.has(shard) || existsSync24(shardPath(verDir, shard));
14418
15082
  }
14419
15083
  };
14420
15084
  }
@@ -14498,7 +15162,7 @@ async function runMad(inputArg, opts, deps = {}) {
14498
15162
  if (!inputArg)
14499
15163
  throw new Error("用法:gtrk tool mad <素材文件夹> [--bgm 歌.mp3]");
14500
15164
  const dirAbs = resolve13(inputArg);
14501
- if (!existsSync24(dirAbs) || !statSync3(dirAbs).isDirectory()) {
15165
+ if (!existsSync25(dirAbs) || !statSync3(dirAbs).isDirectory()) {
14502
15166
  throw new Error(`素材文件夹不存在或不是目录:${dirAbs}`);
14503
15167
  }
14504
15168
  const { videos, orientation, skipped } = scanFolder(dirAbs, {
@@ -14612,10 +15276,10 @@ async function runMad(inputArg, opts, deps = {}) {
14612
15276
  const header = madHeader(version, now.toISOString());
14613
15277
  const bgm = level <= 2 && bgmForTrack ? { path: bgmForTrack, markers } : undefined;
14614
15278
  const { jsx } = madJsx({ master, windows, header, bgm });
14615
- const outDir = opts.out ? resolve13(opts.out) : join27(process.cwd(), `mad-${timestamp4(now)}`);
15279
+ const outDir = opts.out ? resolve13(opts.out) : join29(process.cwd(), `mad-${timestamp4(now)}`);
14616
15280
  await mkdir13(outDir, { recursive: true });
14617
- const jsxPath = join27(outDir, "mad.jsx");
14618
- await writeFile11(jsxPath, jsx);
15281
+ const jsxPath = join29(outDir, "mad.jsx");
15282
+ await writeFile12(jsxPath, jsx);
14619
15283
  const result = {
14620
15284
  ok: true,
14621
15285
  tool: "mad",
@@ -14626,7 +15290,7 @@ async function runMad(inputArg, opts, deps = {}) {
14626
15290
  degradeLevel: level,
14627
15291
  techniques: chosen.map((c3) => ({ uid: c3.entry.uid, pid: c3.entry.pid, cat: c3.entry.cat, t0: c3.entry.t0, t1: c3.entry.t1 }))
14628
15292
  };
14629
- await writeFile11(join27(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: now.toISOString() }, null, 2));
15293
+ await writeFile12(join29(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: now.toISOString() }, null, 2));
14630
15294
  warn(completionMessage(jsxPath, level));
14631
15295
  return result;
14632
15296
  }
@@ -14759,9 +15423,9 @@ async function runMadInTool(inputArg, opts) {
14759
15423
  }
14760
15424
 
14761
15425
  // src/commands/transcript.ts
14762
- import { existsSync as existsSync25 } from "node:fs";
14763
- import { mkdir as mkdir14, rename as rename3, rm as rm2, stat as stat6, writeFile as writeFile12 } from "node:fs/promises";
14764
- import { basename as basename14, dirname as dirname11, extname as extname8, join as join28, resolve as resolve14 } from "node:path";
15426
+ import { existsSync as existsSync26 } from "node:fs";
15427
+ import { mkdir as mkdir14, rename as rename3, rm as rm2, stat as stat6, writeFile as writeFile13 } from "node:fs/promises";
15428
+ import { basename as basename15, dirname as dirname13, extname as extname8, join as join30, resolve as resolve14 } from "node:path";
14765
15429
 
14766
15430
  // src/lib/transcript.ts
14767
15431
  var AGENT_SUMMARY_PENDING = "<!-- gtrk:agent-summary-pending -->";
@@ -14898,7 +15562,7 @@ async function validateTranscriptInput(input) {
14898
15562
  throw new Error("视频转文字稿仅支持本地视频文件,不支持 URL、平台视频地址或远端下载");
14899
15563
  }
14900
15564
  const inputAbs = resolve14(input);
14901
- if (!existsSync25(inputAbs))
15565
+ if (!existsSync26(inputAbs))
14902
15566
  throw new Error(`本地视频不存在:${inputAbs}`);
14903
15567
  const info = await stat6(inputAbs);
14904
15568
  if (!info.isFile())
@@ -14910,17 +15574,17 @@ async function validateTranscriptInput(input) {
14910
15574
  return inputAbs;
14911
15575
  }
14912
15576
  function resolveTranscriptOutput(inputAbs, out) {
14913
- const base = basename14(inputAbs, extname8(inputAbs));
14914
- const output = out ? resolve14(out) : join28(dirname11(inputAbs), `${base}-transcript.md`);
15577
+ const base = basename15(inputAbs, extname8(inputAbs));
15578
+ const output = out ? resolve14(out) : join30(dirname13(inputAbs), `${base}-transcript.md`);
14915
15579
  if (extname8(output).toLowerCase() !== ".md")
14916
15580
  throw new Error("--out 必须指向一个 .md 文件");
14917
15581
  return output;
14918
15582
  }
14919
15583
  async function writeMarkdownAtomic(path, markdown) {
14920
- await mkdir14(dirname11(path), { recursive: true });
15584
+ await mkdir14(dirname13(path), { recursive: true });
14921
15585
  const temp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
14922
15586
  try {
14923
- await writeFile12(temp, markdown, "utf8");
15587
+ await writeFile13(temp, markdown, "utf8");
14924
15588
  await rename3(temp, path);
14925
15589
  } finally {
14926
15590
  await rm2(temp, { force: true });
@@ -14933,8 +15597,8 @@ async function runTranscript(input, opts = {}, depsOverride) {
14933
15597
  const output = resolveTranscriptOutput(inputAbs, opts.out);
14934
15598
  const deps = buildDeps(depsOverride);
14935
15599
  const language = opts.lang?.trim() || "zh-CN";
14936
- const sourceName = basename14(inputAbs);
14937
- const title = basename14(inputAbs, extname8(inputAbs));
15600
+ const sourceName = basename15(inputAbs);
15601
+ const title = basename15(inputAbs, extname8(inputAbs));
14938
15602
  log.step(`▶ 视频转文字稿:${sourceName}`);
14939
15603
  log.step("① 本地探测视频…");
14940
15604
  const geometry = deps.probe(inputAbs, opts.ffmpegPath);
@@ -14946,7 +15610,7 @@ async function runTranscript(input, opts = {}, depsOverride) {
14946
15610
  log.step("② 本地抽取 16k 单声道音频(原视频不上传)…");
14947
15611
  const audio = await deps.extract(inputAbs, opts.ffmpegPath);
14948
15612
  deps.assertDuration(geometry.duration, audio, opts.ffmpegPath);
14949
- log.info(`上传物:${basename14(audio)}(仅音频衍生物)`);
15613
+ log.info(`上传物:${basename15(audio)}(仅音频衍生物)`);
14950
15614
  log.step("③ 上传音频并提交 ASR…");
14951
15615
  const payload = (fileId) => ({ file_id: fileId, language, word_level: true });
14952
15616
  const submitted = await uploadAndSubmitTask(deps.cfg, audio, TASK_TYPE4, payload, {
@@ -14994,9 +15658,9 @@ function registerTranscript(program2) {
14994
15658
  }
14995
15659
 
14996
15660
  // src/commands/music-visualizer.ts
14997
- import { resolve as resolve15, join as join29, dirname as dirname12, basename as basename15, extname as extname9 } from "node:path";
14998
- import { mkdir as mkdir15, writeFile as writeFile13 } from "node:fs/promises";
14999
- import { existsSync as existsSync26 } from "node:fs";
15661
+ import { resolve as resolve15, join as join31, dirname as dirname14, basename as basename16, extname as extname9 } from "node:path";
15662
+ import { mkdir as mkdir15, writeFile as writeFile14 } from "node:fs/promises";
15663
+ import { existsSync as existsSync27 } from "node:fs";
15000
15664
  var TASK_TYPE5 = "music_visualizer";
15001
15665
  var PRICE_KEY2 = "music_visualizer";
15002
15666
  var HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
@@ -15044,7 +15708,7 @@ function timestamp5() {
15044
15708
  return `${p(d.getFullYear() % 100)}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
15045
15709
  }
15046
15710
  function assertExt(pathAbs, exts, label) {
15047
- if (!existsSync26(pathAbs))
15711
+ if (!existsSync27(pathAbs))
15048
15712
  throw new Error(`${label}不存在:${pathAbs}`);
15049
15713
  const e = extname9(pathAbs).toLowerCase();
15050
15714
  if (exts.length && !exts.includes(e)) {
@@ -15113,9 +15777,9 @@ async function runMusicVisualizer(audio, opts) {
15113
15777
  if (coverAbs)
15114
15778
  assertExt(coverAbs, IMAGE_EXTS2, "封面图");
15115
15779
  const extraParams = parseExtraParams3(opts.param, opts.paramsJson);
15116
- const projName = basename15(audioAbs, extname9(audioAbs));
15117
- const outDir = resolve15(opts.out ?? join29(dirname12(audioAbs), `${projName}-visualizer-${timestamp5()}`));
15118
- log.step(`▶ 音乐可视化:${basename15(audioAbs)}(模板 ${template}${bgAbs ? " · 背景" : ""}${coverAbs ? " · 封面" : ""})`);
15780
+ const projName = basename16(audioAbs, extname9(audioAbs));
15781
+ const outDir = resolve15(opts.out ?? join31(dirname14(audioAbs), `${projName}-visualizer-${timestamp5()}`));
15782
+ log.step(`▶ 音乐可视化:${basename16(audioAbs)}(模板 ${template}${bgAbs ? " · 背景" : ""}${coverAbs ? " · 封面" : ""})`);
15119
15783
  let billingHint;
15120
15784
  try {
15121
15785
  billingHint = (await resolveToolPricing(PRICE_KEY2)).billingHint;
@@ -15161,7 +15825,7 @@ async function runMusicVisualizer(audio, opts) {
15161
15825
  const { taskId } = submitted;
15162
15826
  log.info(`task_id = ${taskId}`);
15163
15827
  await mkdir15(outDir, { recursive: true });
15164
- await writeFile13(join29(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE5, fileId: submitted.fileId, source: audioAbs, template, createdAt: new Date().toISOString() }, null, 2));
15828
+ await writeFile14(join31(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE5, fileId: submitted.fileId, source: audioAbs, template, createdAt: new Date().toISOString() }, null, 2));
15165
15829
  log.step("③ 云端处理中(每 5s 轮询)…");
15166
15830
  const result = await pollTask(cfg, TASK_TYPE5, taskId, (status, progress) => {
15167
15831
  log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
@@ -15172,7 +15836,7 @@ async function runMusicVisualizer(audio, opts) {
15172
15836
  const files = [];
15173
15837
  const errors = {};
15174
15838
  if (url) {
15175
- const dest = join29(outDir, `${projName}-visualizer${extFromUrl(url, ".mp4")}`);
15839
+ const dest = join31(outDir, `${projName}-visualizer${extFromUrl(url, ".mp4")}`);
15176
15840
  try {
15177
15841
  await downloadStream(url, dest);
15178
15842
  files.push(dest);
@@ -15194,7 +15858,7 @@ async function runMusicVisualizer(audio, opts) {
15194
15858
  ...Object.keys(errors).length ? { errors } : {},
15195
15859
  finishedAt: new Date().toISOString()
15196
15860
  };
15197
- await writeFile13(join29(outDir, "result.json"), JSON.stringify(resultJson, null, 2));
15861
+ await writeFile14(join31(outDir, "result.json"), JSON.stringify(resultJson, null, 2));
15198
15862
  if (opts.json) {
15199
15863
  process.stdout.write(`${JSON.stringify(resultJson)}
15200
15864
  `);
@@ -15207,12 +15871,121 @@ async function runMusicVisualizer(audio, opts) {
15207
15871
  log.ok(`完成。产物目录:${outDir}`);
15208
15872
  }
15209
15873
 
15874
+ // src/commands/deps.ts
15875
+ import { existsSync as existsSync28, readdirSync as readdirSync4, statSync as statSync4 } from "node:fs";
15876
+ import { join as join32 } from "node:path";
15877
+ function registerDeps(program2) {
15878
+ const deps = program2.command("deps").description("运行时资产:查看状态 / 显式安装 ffmpeg 与渲染字体");
15879
+ deps.command("status").description("查看 ffmpeg / 字体的当前来源、版本与授权信息").option("--ffmpeg-path <dir>", "显式 ffmpeg/ffprobe 目录").action(async (opts) => {
15880
+ await runStatus2(opts.ffmpegPath);
15881
+ });
15882
+ deps.command("install").description("从同合云镜像安装运行时资产(已存在则跳过)").option("--ffmpeg", "只装 ffmpeg/ffprobe").option("--font", "只装渲染字体").option("--force", "已存在也覆盖重装").action(async (opts) => {
15883
+ await runInstall(opts);
15884
+ });
15885
+ }
15886
+ function fmtMB(n) {
15887
+ return `${(n / 1048576).toFixed(1)} MB`;
15888
+ }
15889
+ async function runStatus2(ffmpegPath) {
15890
+ log.step("① ffmpeg / ffprobe");
15891
+ const res = resolveFfmpeg(ffmpegPath);
15892
+ if (res) {
15893
+ log.ok(`来源:${res.source}`);
15894
+ try {
15895
+ const cap = probeCapabilities(res);
15896
+ log.info(cap.version);
15897
+ log.info(`libx264 ${cap.hasLibx264 ? "✅" : "❌"} ass 滤镜 ${cap.filters.has("ass") ? "✅" : "❌"}`);
15898
+ } catch {
15899
+ log.warn("二进制存在但探测能力失败");
15900
+ }
15901
+ } else {
15902
+ log.warn(`未找到。装它:gtrk deps install --ffmpeg`);
15903
+ }
15904
+ log.step("② 渲染字体");
15905
+ const fdir = fontsDir();
15906
+ const fonts = existsSync28(fdir) ? readdirSync4(fdir).filter((f) => /\.(otf|ttf|ttc|otc)$/i.test(f)) : [];
15907
+ if (fonts.length) {
15908
+ log.ok(`${fdir}(${fonts.length} 个)`);
15909
+ for (const f of fonts)
15910
+ log.info(`${f} ${fmtMB(statSync4(join32(fdir, f)).size)}`);
15911
+ log.info("经 ass 滤镜 fontsdir 供给 libass,未安装进系统字体表");
15912
+ } else {
15913
+ log.warn(`${fdir} 无字体。装它:gtrk deps install --font`);
15914
+ log.info("(系统已装所需字体时也可直接用,两处任一命中即可)");
15915
+ }
15916
+ log.step("③ 分发物授权与源码");
15917
+ let m = null;
15918
+ try {
15919
+ m = await fetchManifest();
15920
+ } catch (e) {
15921
+ log.warn(`取 manifest 失败:${e instanceof Error ? e.message : String(e)}`);
15922
+ }
15923
+ if (m) {
15924
+ let key = "";
15925
+ try {
15926
+ key = resolvePlatformKey();
15927
+ } catch {}
15928
+ for (const [k, v] of Object.entries(m.ffmpeg)) {
15929
+ const mark = k === key ? "→" : " ";
15930
+ log.info(`${mark} ${k.padEnd(10)} ${v.version.padEnd(20)} ${v.license} 源码 ${v.source}`);
15931
+ }
15932
+ for (const [name, v] of Object.entries(m.font)) {
15933
+ log.info(` 字体 ${name} ${v.license}`);
15934
+ }
15935
+ log.info(`合规说明:${m.base}/SOURCE.md`);
15936
+ log.info("分发不附加任何使用限制(不限 gtrk 用户、不禁转发)");
15937
+ }
15938
+ }
15939
+ async function runInstall(opts) {
15940
+ const doFfmpeg = opts.ffmpeg || !opts.font;
15941
+ const doFont = opts.font || !opts.ffmpeg;
15942
+ log.step(`读 manifest:${MANIFEST_URL}`);
15943
+ const m = await fetchManifest();
15944
+ log.info(`schema ${m.schema},生成于 ${m.generated}`);
15945
+ let lastPct = -1;
15946
+ let lastAt = 0;
15947
+ const onProgress = (got, total) => {
15948
+ if (total <= 0)
15949
+ return;
15950
+ const pct = Math.floor(got / total * 100);
15951
+ const now = Date.now();
15952
+ if (pct < 100 && pct - lastPct < 5 && now - lastAt < 2000)
15953
+ return;
15954
+ lastPct = pct;
15955
+ lastAt = now;
15956
+ log.tick(`下载 ${fmtMB(got)} / ${fmtMB(total)}(${pct}%)`);
15957
+ };
15958
+ if (doFfmpeg) {
15959
+ const key = resolvePlatformKey();
15960
+ log.step(`① ffmpeg(${key})`);
15961
+ const r = await installFfmpeg(m, { force: opts.force, onProgress });
15962
+ log.tickEnd();
15963
+ if (r.installed)
15964
+ log.ok(`已安装 ${r.detail}`);
15965
+ else
15966
+ log.info(`跳过(${r.reason}):${r.detail}`);
15967
+ }
15968
+ if (doFont) {
15969
+ log.step("② 渲染字体");
15970
+ const rs = await installFont(m, { force: opts.force, onProgress });
15971
+ log.tickEnd();
15972
+ for (const r of rs) {
15973
+ if (r.installed)
15974
+ log.ok(`已安装 ${r.detail}`);
15975
+ else
15976
+ log.info(`跳过(${r.reason}):${r.detail}`);
15977
+ }
15978
+ log.info("未写系统字体表、未改注册表——烧录时经 ass 滤镜 fontsdir 供给");
15979
+ }
15980
+ log.ok(`完成。二进制在 ${ffmpegDir()},字体在 ${fontsDir()}`);
15981
+ }
15982
+
15210
15983
  // src/index.ts
15211
15984
  try {
15212
15985
  process.loadEnvFile?.();
15213
15986
  } catch {}
15214
15987
  migrateLegacyHome();
15215
- var { version } = JSON.parse(readFileSync6(join30(packageRoot(), "package.json"), "utf8"));
15988
+ var { version } = JSON.parse(readFileSync6(join33(packageRoot(), "package.json"), "utf8"));
15216
15989
  var program2 = new Command;
15217
15990
  program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
15218
15991
  registerInstall(program2);
@@ -15230,6 +16003,7 @@ registerMg(program2);
15230
16003
  registerTool(program2);
15231
16004
  registerTranscript(program2);
15232
16005
  registerMusicVisualizer(program2);
16006
+ registerDeps(program2);
15233
16007
  program2.parseAsync(process.argv).catch((e) => {
15234
16008
  console.error(`
15235
16009
  ❌ ${e instanceof Error ? e.message : String(e)}`);