@gitruck/cli 0.2.23 → 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 join31 } 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",
@@ -6870,9 +7063,151 @@ async function probeFontAvailable(font, ffmpegPath) {
6870
7063
  return null;
6871
7064
  }
6872
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
+ }
6873
7206
  async function burnSubtitle(videoAbs, assAbs, outAbs, opts = {}) {
6874
7207
  const { ffmpeg } = requireFfmpeg(opts.ffmpegPath);
6875
- const filterPath = assAbs.replace(/\\/g, "/").replace(/:/g, "\\:");
7208
+ let filter = `ass='${assFilterPath(assAbs)}'`;
7209
+ if (hasLocalFonts())
7210
+ filter += `:fontsdir='${assFilterPath(fontsDir())}'`;
6876
7211
  await runFfmpeg(ffmpeg, [
6877
7212
  "-y",
6878
7213
  "-v",
@@ -6880,7 +7215,7 @@ async function burnSubtitle(videoAbs, assAbs, outAbs, opts = {}) {
6880
7215
  "-i",
6881
7216
  videoAbs,
6882
7217
  "-vf",
6883
- `ass='${filterPath}'`,
7218
+ filter,
6884
7219
  "-c:v",
6885
7220
  opts.codec ?? "libx264",
6886
7221
  "-crf",
@@ -6895,16 +7230,26 @@ async function burnSubtitle(videoAbs, assAbs, outAbs, opts = {}) {
6895
7230
  ]);
6896
7231
  return outAbs;
6897
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
+ }
6898
7243
 
6899
7244
  // src/lib/materialize.ts
6900
- import { join as join14, basename as basename5 } from "node:path";
7245
+ import { join as join15, basename as basename6 } from "node:path";
6901
7246
  import { mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
6902
7247
 
6903
7248
  // src/lib/render.ts
6904
7249
  import { writeFile as writeFile3, unlink, readFile as readFile3 } from "node:fs/promises";
6905
- import { existsSync as existsSync12 } from "node:fs";
7250
+ import { existsSync as existsSync13 } from "node:fs";
6906
7251
  import { tmpdir } from "node:os";
6907
- import { join as join13 } from "node:path";
7252
+ import { join as join14 } from "node:path";
6908
7253
  var AUDIO_SAMPLE_RATE = 48000;
6909
7254
  var AUDIO_LAYOUT = "stereo";
6910
7255
  var DEFAULT_CRF = 18;
@@ -7062,7 +7407,7 @@ function materialPathsFromGtrk(gtrk) {
7062
7407
  continue;
7063
7408
  if (!m.path)
7064
7409
  throw new Error(`gtrk 素材 ${m.id} 缺 path(source_path),无法本地渲染`);
7065
- if (!existsSync12(m.path))
7410
+ if (!existsSync13(m.path))
7066
7411
  throw new Error(`gtrk 素材文件不存在:${m.path}`);
7067
7412
  map[String(m.id)] = m.path;
7068
7413
  }
@@ -7076,7 +7421,7 @@ async function renderGtrk(gtrk, outputPath, opts = {}) {
7076
7421
  const { ffmpeg } = requireFfmpeg(opts.ffmpegPath);
7077
7422
  const materialPaths = materialPathsFromGtrk(gtrk);
7078
7423
  const { inputs, graph, total } = buildFilterGraph(gtrk, materialPaths, { crf });
7079
- const filterFile = join13(tmpdir(), `gtrk-filter-${process.pid}-${inputs.length}.txt`);
7424
+ const filterFile = join14(tmpdir(), `gtrk-filter-${process.pid}-${inputs.length}.txt`);
7080
7425
  await writeFile3(filterFile, graph, "utf8");
7081
7426
  try {
7082
7427
  const args = ["-y"];
@@ -7114,7 +7459,7 @@ function gtrkSourceName(gtrk) {
7114
7459
  const p = gtrk.materials?.[0]?.path;
7115
7460
  if (!p)
7116
7461
  return;
7117
- const b = basename5(p);
7462
+ const b = basename6(p);
7118
7463
  const dot = b.lastIndexOf(".");
7119
7464
  return dot > 0 ? b.slice(0, dot) : b;
7120
7465
  }
@@ -7126,7 +7471,7 @@ async function materializeResult(opts) {
7126
7471
  throw new Error("任务无工程文件产物(检查 project_formats / 任务是否产出)");
7127
7472
  const errors = { ...output.errors ?? {} };
7128
7473
  await mkdir4(outDir, { recursive: true });
7129
- const resultPath = join14(outDir, "result.json");
7474
+ const resultPath = join15(outDir, "result.json");
7130
7475
  const writeResult = async (extra) => {
7131
7476
  const r = {
7132
7477
  ok: Object.keys(errors).length === 0,
@@ -7148,9 +7493,9 @@ async function materializeResult(opts) {
7148
7493
  const byFormat = {};
7149
7494
  for (const f of files) {
7150
7495
  const base = baseFormat(f.format);
7151
- const fmtDir = join14(outDir, base);
7496
+ const fmtDir = join15(outDir, base);
7152
7497
  await mkdir4(fmtDir, { recursive: true });
7153
- const dest = join14(fmtDir, f.filename);
7498
+ const dest = join15(fmtDir, f.filename);
7154
7499
  try {
7155
7500
  await dl(f.download_url, dest);
7156
7501
  (byFormat[base] ??= []).push(dest);
@@ -7166,9 +7511,9 @@ async function materializeResult(opts) {
7166
7511
  }
7167
7512
  let jianyingDraftPath = null;
7168
7513
  if (byFormat.jianying && opts.draftDir) {
7169
- const dest = join14(opts.draftDir, basename5(outDir));
7514
+ const dest = join15(opts.draftDir, basename6(outDir));
7170
7515
  try {
7171
- const landing = await copyJianyingDraft(join14(outDir, "jianying"), dest);
7516
+ const landing = await copyJianyingDraft(join15(outDir, "jianying"), dest);
7172
7517
  if (landing.complete) {
7173
7518
  jianyingDraftPath = dest;
7174
7519
  log.info(`剪映草稿已落到:${dest}`);
@@ -7190,7 +7535,7 @@ async function materializeResult(opts) {
7190
7535
  log.step("本地渲染成片(ffmpeg)…");
7191
7536
  const project = await readGtrkFile(gtrkPath);
7192
7537
  const name = opts.projName ?? gtrkSourceName(project) ?? taskId;
7193
- const outMp4 = join14(outDir, `${name}.mp4`);
7538
+ const outMp4 = join15(outDir, `${name}.mp4`);
7194
7539
  const r = await renderGtrk(project, outMp4, {
7195
7540
  crf: opts.crf != null ? Number(opts.crf) : undefined,
7196
7541
  codec: opts.codec,
@@ -7212,7 +7557,7 @@ async function materializeResult(opts) {
7212
7557
  log.step("三方打开(产物已就位,按需自取):");
7213
7558
  for (const base of Object.keys(byFormat)) {
7214
7559
  const meta = FORMAT_META[base];
7215
- const target = base === "jianying" ? jianyingDraftPath ?? join14(outDir, "jianying") : byFormat[base][0];
7560
+ const target = base === "jianying" ? jianyingDraftPath ?? join15(outDir, "jianying") : byFormat[base][0];
7216
7561
  console.log(` • ${meta?.label ?? base}:${meta?.openHint(target) ?? target}`);
7217
7562
  }
7218
7563
  if (rendered)
@@ -7280,18 +7625,18 @@ async function runOralCut(input, opts) {
7280
7625
  routeLogsToStderr();
7281
7626
  const cfg = loadConfig();
7282
7627
  const inputAbs = resolve4(input);
7283
- if (!existsSync13(inputAbs))
7628
+ if (!existsSync14(inputAbs))
7284
7629
  throw new Error(`毛片不存在:${inputAbs}`);
7285
- const projName = basename6(inputAbs, extname2(inputAbs));
7630
+ const projName = basename7(inputAbs, extname2(inputAbs));
7286
7631
  const formats = opts.formats.split(",").map((s) => s.trim()).filter(Boolean);
7287
7632
  if (opts.render && !formats.includes("gtrk"))
7288
7633
  formats.push("gtrk");
7289
7634
  const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
7290
- 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()}`));
7291
7636
  let scriptPath = opts.script ? resolve4(opts.script) : undefined;
7292
7637
  if (!scriptPath) {
7293
- const sibling = join15(dirname3(inputAbs), `${projName}.txt`);
7294
- if (existsSync13(sibling)) {
7638
+ const sibling = join16(dirname4(inputAbs), `${projName}.txt`);
7639
+ if (existsSync14(sibling)) {
7295
7640
  scriptPath = sibling;
7296
7641
  log.info(`自动识别到同名文稿:${sibling}(按有稿剪辑;不想用就改名或显式 --script)`);
7297
7642
  }
@@ -7305,14 +7650,14 @@ async function runOralCut(input, opts) {
7305
7650
  else
7306
7651
  log.warn("没找到剪映草稿目录 → 将只产 draft_content.json、缺 meta。可加 --jianying-draft-dir <你的草稿目录> 重跑。");
7307
7652
  }
7308
- 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 ? " · 本地渲染" : ""})`);
7309
7654
  const extraParams = parseExtraParams(opts.param, opts.paramsJson);
7310
7655
  log.step("① 本地预处理(探几何 + 抽音频/720p)…");
7311
7656
  const geo = probeGeometry(inputAbs, opts.ffmpegPath);
7312
7657
  log.info(`原片几何 ${geo.width}x${geo.height} @ ${geo.fps.toFixed(2)}fps · ${geo.duration.toFixed(1)}s`);
7313
7658
  const artifact = opts.visualAssist ? await compress720p(inputAbs, opts.ffmpegPath) : await extractAudio(inputAbs, opts.ffmpegPath);
7314
7659
  assertDurationConsistent(geo.duration, artifact, opts.ffmpegPath);
7315
- log.info(opts.visualAssist ? `已压 720p 代理(上传物):${basename6(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename6(artifact)}`);
7660
+ log.info(opts.visualAssist ? `已压 720p 代理(上传物):${basename7(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename7(artifact)}`);
7316
7661
  log.step("② 上传抽出物到云端…");
7317
7662
  const buildPayload = (fid) => {
7318
7663
  const p = {
@@ -7352,7 +7697,7 @@ async function runOralCut(input, opts) {
7352
7697
  const up = { fileId: submitted.fileId, cached: submitted.cached };
7353
7698
  log.info(`task_id = ${taskId}`);
7354
7699
  await mkdir5(outDir, { recursive: true });
7355
- 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));
7356
7701
  log.step("④ 云端处理中(每 5s 轮询)…");
7357
7702
  const result = await pollTask(cfg, TASK_TYPE, taskId, (status, progress) => {
7358
7703
  log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
@@ -7376,9 +7721,9 @@ async function runOralCut(input, opts) {
7376
7721
  }
7377
7722
 
7378
7723
  // src/commands/long2short.ts
7379
- import { resolve as resolve6, join as join18, dirname as dirname6, basename as basename8, extname as extname5 } from "node:path";
7724
+ import { resolve as resolve6, join as join19, dirname as dirname7, basename as basename9, extname as extname5 } from "node:path";
7380
7725
  import { mkdir as mkdir7, writeFile as writeFile8 } from "node:fs/promises";
7381
- import { existsSync as existsSync16 } from "node:fs";
7726
+ import { existsSync as existsSync17 } from "node:fs";
7382
7727
 
7383
7728
  // src/lib/clip-brief.ts
7384
7729
  var str = (v) => {
@@ -7609,11 +7954,11 @@ function renderProReport(clips, report, ctx) {
7609
7954
  }
7610
7955
 
7611
7956
  // src/lib/tool-runner.ts
7612
- import { resolve as resolve5, join as join17, dirname as dirname5, basename as basename7, extname as extname4 } from "node:path";
7957
+ import { resolve as resolve5, join as join18, dirname as dirname6, basename as basename8, extname as extname4 } from "node:path";
7613
7958
  import { mkdir as mkdir6, writeFile as writeFile7, stat as stat5 } from "node:fs/promises";
7614
- import { createWriteStream, existsSync as existsSync15 } from "node:fs";
7959
+ import { createWriteStream as createWriteStream2, existsSync as existsSync16 } from "node:fs";
7615
7960
  import { Readable as Readable2 } from "node:stream";
7616
- import { pipeline } from "node:stream/promises";
7961
+ import { pipeline as pipeline2 } from "node:stream/promises";
7617
7962
 
7618
7963
  // src/lib/tool-pricing.ts
7619
7964
  var TOOL_PRICE_LIST_URL = "https://cloud.ai-mcn.tv/api/get_price_list";
@@ -7697,7 +8042,7 @@ async function resolveToolPricing(priceKey, pricingContext, fetchFn = fetch) {
7697
8042
  // src/lib/tool-descriptors.ts
7698
8043
  import { readFileSync as readFileSync4 } from "node:fs";
7699
8044
  import { readFile as readFile5, writeFile as writeFile6 } from "node:fs/promises";
7700
- import { dirname as dirname4, extname as extname3, join as join16, resolve as resolvePath } from "node:path";
8045
+ import { dirname as dirname5, extname as extname3, join as join17, resolve as resolvePath } from "node:path";
7701
8046
  var MULTI_INPUT_KINDS = new Set(["images", "videos"]);
7702
8047
  var IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif", ".tif", ".tiff", ".heic", ".heif", ".avif"];
7703
8048
  var VIDEO_EXTS = [".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v", ".flv", ".wmv", ".mpg", ".mpeg", ".ts", ".m2ts"];
@@ -8719,13 +9064,14 @@ var videoAiSubtitle = {
8719
9064
  const fonts = assFontNames(await readFile5(ass, "utf8"));
8720
9065
  const missing = [];
8721
9066
  for (const f of fonts) {
8722
- if (await probeFontAvailable(f, ctx.ffmpegPath) === false)
9067
+ if (!await fontUsableForBurn(f, ctx.ffmpegPath))
8723
9068
  missing.push(f);
8724
9069
  }
8725
9070
  if (missing.length) {
8726
- throw new Error(`本机缺字幕模板所需字体:${missing.join("、")}。` + `装上后重跑即可(字幕文件已落地,无需重新提交任务)。CLI 不自带字体、也不用替代字体顶——` + `替代字体烧出来的成片观感与模板设计不符且难以察觉。`);
9071
+ throw new Error(`本机缺字幕模板所需字体:${missing.join("、")}。` + `装它:gtrk deps install --font(落 ~/.gitruck/fonts,不写系统字体表)。
9072
+ ` + ` 装上后重跑即可(字幕文件已落地,无需重新提交任务)。不用替代字体顶——` + `替代字体烧出来的成片观感与模板设计不符且难以察觉。`);
8727
9073
  }
8728
- const out = join16(dirname4(ass), `${ctx.baseName}-subtitled.mp4`);
9074
+ const out = join17(dirname5(ass), `${ctx.baseName}-subtitled.mp4`);
8729
9075
  await burnSubtitle(inputAbs, ass, out, { ffmpegPath: ctx.ffmpegPath });
8730
9076
  return [out];
8731
9077
  },
@@ -8827,7 +9173,7 @@ var videoLong2ShortPro = {
8827
9173
  return;
8828
9174
  const mp4s = landed.filter((p) => p.toLowerCase().endsWith(".mp4"));
8829
9175
  const byIndex = clips.map((_c, i) => mp4s.find((p) => new RegExp(`clip${i}.[a-z0-9]+$`, "i").test(p)));
8830
- const dest = join16(dirname4(landed[0] ?? "."), "clips.md");
9176
+ const dest = join17(dirname5(landed[0] ?? "."), "clips.md");
8831
9177
  await writeFile6(dest, renderProReport(clips, out.report ?? {}, {
8832
9178
  source: ctx.inputAbs ?? ctx.baseName,
8833
9179
  clipFiles: byIndex
@@ -9227,7 +9573,7 @@ function validateToolInput(descriptor, inputAbs) {
9227
9573
  return;
9228
9574
  if (!inputAbs)
9229
9575
  throw new Error(`${descriptor.name} 需要输入${spec.kind === "directory" ? "目录" : "文件"}`);
9230
- if (!existsSync15(inputAbs))
9576
+ if (!existsSync16(inputAbs))
9231
9577
  throw new Error(`输入不存在:${inputAbs}`);
9232
9578
  if (spec.kind === "directory")
9233
9579
  return;
@@ -9244,13 +9590,13 @@ function validateToolInputs(descriptor, inputAbsList) {
9244
9590
  throw new Error(`${descriptor.name} 需要至少一个输入文件(可传多个,顺序即拼装顺序)`);
9245
9591
  const exts = descriptor.input.exts ?? defaultExtsFor(descriptor.input.kind);
9246
9592
  for (const p of inputAbsList) {
9247
- if (!existsSync15(p))
9593
+ if (!existsSync16(p))
9248
9594
  throw new Error(`输入不存在:${p}`);
9249
9595
  if (exts && exts.length) {
9250
9596
  const e = extname4(p).toLowerCase();
9251
9597
  if (!exts.includes(e)) {
9252
9598
  const noun = descriptor.input.kind === "videos" ? "视频" : "图片";
9253
- throw new Error(`${descriptor.name} 需要${noun}输入,但「${basename7(p)}」是「${e || "无扩展名"}」(支持:${exts.join(" ")})`);
9599
+ throw new Error(`${descriptor.name} 需要${noun}输入,但「${basename8(p)}」是「${e || "无扩展名"}」(支持:${exts.join(" ")})`);
9254
9600
  }
9255
9601
  }
9256
9602
  }
@@ -9268,7 +9614,7 @@ async function downloadStream(url, dest) {
9268
9614
  const res = await fetch(url);
9269
9615
  if (!res.ok || !res.body)
9270
9616
  throw new Error(`下载失败 HTTP ${res.status}:${url}`);
9271
- await pipeline(Readable2.fromWeb(res.body), createWriteStream(dest));
9617
+ await pipeline2(Readable2.fromWeb(res.body), createWriteStream2(dest));
9272
9618
  }
9273
9619
  async function pollToolTask(cfg, taskType, taskId, opts = {}) {
9274
9620
  const timeoutMs = opts.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
@@ -9348,10 +9694,10 @@ function resolveOutDir(descriptor, inputAbs, out) {
9348
9694
  if (out)
9349
9695
  return resolve5(out);
9350
9696
  if (inputAbs) {
9351
- const base = basename7(inputAbs, extname4(inputAbs));
9352
- return join17(dirname5(inputAbs), `${base}-${descriptor.name}`);
9697
+ const base = basename8(inputAbs, extname4(inputAbs));
9698
+ return join18(dirname6(inputAbs), `${base}-${descriptor.name}`);
9353
9699
  }
9354
- return join17(process.cwd(), `${descriptor.name}-${timestamp2()}`);
9700
+ return join18(process.cwd(), `${descriptor.name}-${timestamp2()}`);
9355
9701
  }
9356
9702
  async function safeFingerprint(inputAbs) {
9357
9703
  try {
@@ -9369,7 +9715,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9369
9715
  const isMulti = MULTI_INPUT_KINDS.has(descriptor.input.kind);
9370
9716
  const inputList = isMulti ? (Array.isArray(inputArg) ? inputArg : inputArg != null ? [inputArg] : []).map((p) => resolve5(p)) : undefined;
9371
9717
  const inputAbs = isMulti ? inputList[0] : typeof inputArg === "string" ? resolve5(inputArg) : undefined;
9372
- const baseName2 = inputAbs ? basename7(inputAbs, extname4(inputAbs)) : descriptor.name;
9718
+ const baseName2 = inputAbs ? basename8(inputAbs, extname4(inputAbs)) : descriptor.name;
9373
9719
  if (isMulti) {
9374
9720
  validateToolInputs(descriptor, inputList);
9375
9721
  } else {
@@ -9440,7 +9786,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9440
9786
  }
9441
9787
  await mkdir6(outDir, { recursive: true });
9442
9788
  const fingerprint2 = inputList ? await Promise.all(inputList.map((p) => safeFingerprint(p))) : inputAbs ? await safeFingerprint(inputAbs) : undefined;
9443
- await writeFile7(join17(outDir, "task.json"), JSON.stringify({
9789
+ await writeFile7(join18(outDir, "task.json"), JSON.stringify({
9444
9790
  tool: descriptor.name,
9445
9791
  taskType,
9446
9792
  taskId,
@@ -9462,7 +9808,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9462
9808
  const errors = {};
9463
9809
  const items = descriptor.mapOutputs ? descriptor.mapOutputs(outputResult, ctx) : [];
9464
9810
  for (const it of items) {
9465
- const dest = join17(outDir, it.filename);
9811
+ const dest = join18(outDir, it.filename);
9466
9812
  try {
9467
9813
  await deps.downloadStream(it.url, dest);
9468
9814
  files.push(dest);
@@ -9482,7 +9828,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9482
9828
  let resultFile;
9483
9829
  const structured = descriptor.mapResult ? descriptor.mapResult(outputResult, ctx) : undefined;
9484
9830
  if (structured != null) {
9485
- resultFile = join17(outDir, "result-output.json");
9831
+ resultFile = join18(outDir, "result-output.json");
9486
9832
  await writeFile7(resultFile, JSON.stringify(structured, null, 2));
9487
9833
  }
9488
9834
  if (items.length === 0 && resultFile == null) {
@@ -9501,7 +9847,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9501
9847
  ...resultFile ? { resultFile } : {},
9502
9848
  ...Object.keys(errors).length ? { errors } : {}
9503
9849
  };
9504
- await writeFile7(join17(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));
9505
9851
  return result;
9506
9852
  }
9507
9853
 
@@ -9558,8 +9904,8 @@ async function copyClipDraftsToRoot(clipDirs, draftTarget, errors) {
9558
9904
  let complete = 0;
9559
9905
  let attempted = 0;
9560
9906
  for (const [i, dir] of clipDirs.entries()) {
9561
- const src = join18(dir, "jianying");
9562
- if (!existsSync16(src)) {
9907
+ const src = join19(dir, "jianying");
9908
+ if (!existsSync17(src)) {
9563
9909
  paths.push(null);
9564
9910
  continue;
9565
9911
  }
@@ -9591,15 +9937,15 @@ async function runLong2Short(input, opts) {
9591
9937
  routeLogsToStderr();
9592
9938
  const cfg = loadConfig();
9593
9939
  const inputAbs = resolve6(input);
9594
- if (!existsSync16(inputAbs))
9940
+ if (!existsSync17(inputAbs))
9595
9941
  throw new Error(`毛片不存在:${inputAbs}`);
9596
- const projName = basename8(inputAbs, extname5(inputAbs));
9942
+ const projName = basename9(inputAbs, extname5(inputAbs));
9597
9943
  const formats = opts.formats.split(",").map((s) => s.trim()).filter(Boolean);
9598
9944
  if (!formats.includes("gtrk"))
9599
9945
  formats.push("gtrk");
9600
9946
  const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
9601
- const outDir = resolve6(opts.out ?? join18(dirname6(inputAbs), `${projName}-long2short`));
9602
- const outName = basename8(outDir);
9947
+ const outDir = resolve6(opts.out ?? join19(dirname7(inputAbs), `${projName}-long2short`));
9948
+ const outName = basename9(outDir);
9603
9949
  let draftRoot;
9604
9950
  if (wantJianying) {
9605
9951
  draftRoot = resolveJianyingDraftDir(opts.jianyingDraftDir);
@@ -9608,14 +9954,14 @@ async function runLong2Short(input, opts) {
9608
9954
  else
9609
9955
  log.warn("没找到剪映草稿目录 → 将只产 draft_content.json、缺 meta。可加 --jianying-draft-dir <你的草稿目录> 重跑。");
9610
9956
  }
9611
- const draftTarget = draftRoot ? join18(draftRoot, outName) : undefined;
9612
- 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("/")})`);
9613
9959
  log.step("① 本地预处理(探几何 + 抽音频/720p 代理)…");
9614
9960
  const geo = probeGeometry(inputAbs, opts.ffmpegPath);
9615
9961
  log.info(`原片几何 ${geo.width}x${geo.height} @ ${geo.fps.toFixed(2)}fps · ${(geo.duration / 60).toFixed(1)}min`);
9616
9962
  const artifact = opts.splitScreen ? await compress720p(inputAbs, opts.ffmpegPath) : await extractAudio(inputAbs, opts.ffmpegPath);
9617
9963
  assertDurationConsistent(geo.duration, artifact, opts.ffmpegPath);
9618
- log.info(opts.splitScreen ? `已压 720p 代理(上传物):${basename8(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename8(artifact)}`);
9964
+ log.info(opts.splitScreen ? `已压 720p 代理(上传物):${basename9(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename9(artifact)}`);
9619
9965
  const payloadProbe = buildLong2ShortPayload("__dry_run__", opts, geo, inputAbs, formats, draftTarget);
9620
9966
  log.step("② 上传抽出物到云端…");
9621
9967
  const submitted = await uploadAndSubmitTask(cfg, artifact, TASK_TYPE2, (fid) => buildLong2ShortPayload(fid, opts, geo, inputAbs, formats, draftTarget), {
@@ -9629,7 +9975,7 @@ async function runLong2Short(input, opts) {
9629
9975
  const { taskId, fileId } = { taskId: submitted.taskId, fileId: submitted.fileId };
9630
9976
  log.info(`task_id = ${taskId}`);
9631
9977
  await mkdir7(outDir, { recursive: true });
9632
- await writeFile8(join18(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));
9633
9979
  log.step("④ 云端处理中(选段/跳剪" + (opts.splitScreen ? "/分屏" : "") + ",每 5s 轮询)…");
9634
9980
  const output = await pollToolTask(cfg, TASK_TYPE2, taskId, {
9635
9981
  timeoutMs: POLL_TIMEOUT_MS,
@@ -9652,14 +9998,14 @@ async function runLong2Short(input, opts) {
9652
9998
  continue;
9653
9999
  }
9654
10000
  if (!/^(?:[A-Za-z]:[\\/]|[\\/])/.test(dest))
9655
- dest = join18(dirname6(inputAbs), dest);
10001
+ dest = join19(dirname7(inputAbs), dest);
9656
10002
  try {
9657
- await mkdir7(dirname6(dest), { recursive: true });
10003
+ await mkdir7(dirname7(dest), { recursive: true });
9658
10004
  await download(url, dest);
9659
10005
  splitLanded++;
9660
10006
  } catch (e) {
9661
- errors[`split:${basename8(dest)}`] = e instanceof Error ? e.message : String(e);
9662
- log.warn(`分屏素材下载失败(${basename8(dest)}),对应工程中该素材将缺席`);
10007
+ errors[`split:${basename9(dest)}`] = e instanceof Error ? e.message : String(e);
10008
+ log.warn(`分屏素材下载失败(${basename9(dest)}),对应工程中该素材将缺席`);
9663
10009
  }
9664
10010
  }
9665
10011
  if (splitLanded)
@@ -9668,7 +10014,7 @@ async function runLong2Short(input, opts) {
9668
10014
  log.step(`⑥ 逐 clip 拉回三方工程(共 ${clips.length} 条)…`);
9669
10015
  const clipResults = [];
9670
10016
  for (const [i, clip] of clips.entries()) {
9671
- const clipDir = join18(outDir, `clip${i}`);
10017
+ const clipDir = join19(outDir, `clip${i}`);
9672
10018
  const { files: clipFiles, ...clipMeta } = clip;
9673
10019
  try {
9674
10020
  const r = await materializeResult({
@@ -9709,13 +10055,13 @@ async function runLong2Short(input, opts) {
9709
10055
  for (const [i, cr] of clipResults.entries()) {
9710
10056
  try {
9711
10057
  await mkdir7(cr.dir, { recursive: true });
9712
- await writeFile8(join18(cr.dir, "clip.md"), renderClipBrief(clips[i], i, cr.files));
10058
+ await writeFile8(join19(cr.dir, "clip.md"), renderClipBrief(clips[i], i, cr.files));
9713
10059
  } catch (e) {
9714
10060
  errors[`clip${i}:brief`] = e instanceof Error ? e.message : String(e);
9715
10061
  }
9716
10062
  }
9717
10063
  try {
9718
- await writeFile8(join18(outDir, "clips.md"), renderClipsOverview(clips, {
10064
+ await writeFile8(join19(outDir, "clips.md"), renderClipsOverview(clips, {
9719
10065
  source: inputAbs,
9720
10066
  jumpCut: opts.jumpCut !== false,
9721
10067
  splitMaterials: { landed: splitLanded, total: manifest.length },
@@ -9725,7 +10071,7 @@ async function runLong2Short(input, opts) {
9725
10071
  } catch (e) {
9726
10072
  errors["clips.md"] = e instanceof Error ? e.message : String(e);
9727
10073
  }
9728
- await writeFile8(join18(outDir, "report.json"), JSON.stringify(output.report ?? {}, null, 2));
10074
+ await writeFile8(join19(outDir, "report.json"), JSON.stringify(output.report ?? {}, null, 2));
9729
10075
  const ok = Object.keys(errors).length === 0 && clipResults.some((c3) => c3.ok);
9730
10076
  const rootResult = {
9731
10077
  ok,
@@ -9736,10 +10082,10 @@ async function runLong2Short(input, opts) {
9736
10082
  outDir,
9737
10083
  clips: clipResults,
9738
10084
  splitMaterials: { landed: splitLanded, total: manifest.length },
9739
- reportFile: join18(outDir, "report.json"),
10085
+ reportFile: join19(outDir, "report.json"),
9740
10086
  ...Object.keys(errors).length ? { errors } : {}
9741
10087
  };
9742
- await writeFile8(join18(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));
9743
10089
  if (opts.json)
9744
10090
  console.log(JSON.stringify(rootResult));
9745
10091
  if (opts.open) {
@@ -9755,7 +10101,7 @@ async function runLong2Short(input, opts) {
9755
10101
  }
9756
10102
 
9757
10103
  // src/commands/oralcut-result.ts
9758
- import { resolve as resolve7, join as join19 } from "node:path";
10104
+ import { resolve as resolve7, join as join20 } from "node:path";
9759
10105
  var TASK_TYPE3 = "cli/video_oral_cut_for_cli";
9760
10106
  function timestamp3() {
9761
10107
  const d = new Date;
@@ -9789,7 +10135,7 @@ async function runOralCutResult(taskId, opts) {
9789
10135
  const pct = got.progress != null ? ` ${Math.round(got.progress)}%` : "";
9790
10136
  throw new Error(`任务尚未完成(当前 ${got.status || "未知"}${pct}),暂无法取回结果;请稍后再试。`);
9791
10137
  }
9792
- const outDir = resolve7(opts.out ?? join19(process.cwd(), `${taskId}-video-project-${timestamp3()}`));
10138
+ const outDir = resolve7(opts.out ?? join20(process.cwd(), `${taskId}-video-project-${timestamp3()}`));
9793
10139
  const draftDir = resolveJianyingDraftDir(opts.jianyingDraftDir);
9794
10140
  await materializeResult({
9795
10141
  outDir,
@@ -9807,10 +10153,10 @@ async function runOralCutResult(taskId, opts) {
9807
10153
  }
9808
10154
 
9809
10155
  // src/commands/upgrade.ts
9810
- import { spawnSync as spawnSync4 } from "node:child_process";
10156
+ import { spawnSync as spawnSync5 } from "node:child_process";
9811
10157
  var CLIENT_UPGRADE = "irm https://api.ai-mcn.tv:9000/broadcast/exe/install.ps1 | iex";
9812
10158
  function run(cmd) {
9813
- const r = spawnSync4(cmd, { stdio: "inherit", shell: true });
10159
+ const r = spawnSync5(cmd, { stdio: "inherit", shell: true });
9814
10160
  return r.status ?? 1;
9815
10161
  }
9816
10162
  function registerUpgrade(program2) {
@@ -9849,17 +10195,17 @@ function registerUpgrade(program2) {
9849
10195
  }
9850
10196
 
9851
10197
  // src/commands/render.ts
9852
- import { resolve as resolve8, dirname as dirname7, join as join20, basename as basename9, extname as extname6 } from "node:path";
9853
- 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";
9854
10200
  function registerRender(program2) {
9855
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) => {
9856
10202
  if (opts.json)
9857
10203
  routeLogsToStderr();
9858
10204
  const gtrkAbs = resolve8(gtrk);
9859
- if (!existsSync17(gtrkAbs))
10205
+ if (!existsSync18(gtrkAbs))
9860
10206
  throw new Error(`gtrk 工程不存在:${gtrkAbs}`);
9861
- const outMp4 = resolve8(opts.out ?? join20(dirname7(gtrkAbs), `${basename9(gtrkAbs, extname6(gtrkAbs))}.mp4`));
9862
- 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)}`);
9863
10209
  const project = await readGtrkFile(gtrkAbs);
9864
10210
  const result = await renderGtrk(project, outMp4, {
9865
10211
  crf: opts.crf != null ? Number(opts.crf) : undefined,
@@ -9874,7 +10220,7 @@ function registerRender(program2) {
9874
10220
  log.tickEnd();
9875
10221
  log.ok(`渲染完成:${outMp4}(${result.duration.toFixed(1)}s)`);
9876
10222
  if (opts.open)
9877
- openFolder(dirname7(outMp4));
10223
+ openFolder(dirname8(outMp4));
9878
10224
  if (opts.json) {
9879
10225
  console.log(JSON.stringify({ ok: true, output: outMp4, duration: result.duration }));
9880
10226
  }
@@ -9882,14 +10228,14 @@ function registerRender(program2) {
9882
10228
  }
9883
10229
 
9884
10230
  // src/commands/split.ts
9885
- import { resolve as resolve9, join as join22, dirname as dirname9, basename as basename11 } from "node:path";
9886
- import { existsSync as existsSync18 } from "node:fs";
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";
9887
10233
  import { readFile as readFile6, writeFile as writeFile9, mkdir as mkdir8 } from "node:fs/promises";
9888
- import { createHash } from "node:crypto";
10234
+ import { createHash as createHash2 } from "node:crypto";
9889
10235
 
9890
10236
  // src/lib/gtrk-writeback.ts
9891
- import { readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "node:fs";
9892
- import { dirname as dirname8, join as join21, 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";
9893
10239
  import { randomBytes as randomBytes2 } from "node:crypto";
9894
10240
  function readGtrk(path) {
9895
10241
  const raw = readFileSync5(path, "utf8");
@@ -9916,10 +10262,10 @@ function writeStructMetaSplit(path, gtrk, splitObj, expectedMtimeMs) {
9916
10262
  }
9917
10263
  const nextStructMeta = { ...gtrk.struct_meta ?? {}, split: splitObj };
9918
10264
  const next = { ...gtrk, struct_meta: nextStructMeta };
9919
- const tmp = join21(dirname8(path), `.${basename10(path)}.${randomBytes2(6).toString("hex")}.tmp`);
10265
+ const tmp = join22(dirname9(path), `.${basename11(path)}.${randomBytes2(6).toString("hex")}.tmp`);
9920
10266
  try {
9921
10267
  writeFileSync2(tmp, JSON.stringify(next, null, 2));
9922
- renameSync(tmp, path);
10268
+ renameSync2(tmp, path);
9923
10269
  } catch (e) {
9924
10270
  try {
9925
10271
  unlinkSync(tmp);
@@ -9932,10 +10278,10 @@ function writeGtrkAtomic(path, next, expectedMtimeMs) {
9932
10278
  if (cur !== expectedMtimeMs) {
9933
10279
  throw new Error("工程文件在 matrix 运行期间被外部修改(保存冲突),已拒绝写入;请关闭客户端未保存的工程或重跑(plan 与已下载代理均保留)");
9934
10280
  }
9935
- const tmp = join21(dirname8(path), `.${basename10(path)}.${randomBytes2(6).toString("hex")}.tmp`);
10281
+ const tmp = join22(dirname9(path), `.${basename11(path)}.${randomBytes2(6).toString("hex")}.tmp`);
9936
10282
  try {
9937
10283
  writeFileSync2(tmp, JSON.stringify(next, null, 2));
9938
- renameSync(tmp, path);
10284
+ renameSync2(tmp, path);
9939
10285
  } catch (e) {
9940
10286
  try {
9941
10287
  unlinkSync(tmp);
@@ -9952,7 +10298,7 @@ function registerSplit(program2) {
9952
10298
  });
9953
10299
  }
9954
10300
  function firstExisting(cands) {
9955
- return cands.find((p) => existsSync18(p));
10301
+ return cands.find((p) => existsSync19(p));
9956
10302
  }
9957
10303
  function resolvePaths(opts) {
9958
10304
  const project = opts.project ? resolve9(opts.project) : undefined;
@@ -9960,22 +10306,22 @@ function resolvePaths(opts) {
9960
10306
  if (opts.gtrk) {
9961
10307
  gtrkPath = resolve9(opts.gtrk);
9962
10308
  } else if (project) {
9963
- gtrkPath = firstExisting([join22(project, "gtrk", "project.gtrk"), join22(project, "project.gtrk")]) ?? join22(project, "gtrk", "project.gtrk");
10309
+ gtrkPath = firstExisting([join23(project, "gtrk", "project.gtrk"), join23(project, "project.gtrk")]) ?? join23(project, "gtrk", "project.gtrk");
9964
10310
  } else {
9965
10311
  throw new Error("需 --project <目录> 或显式 --gtrk <path>");
9966
10312
  }
9967
- if (!existsSync18(gtrkPath))
10313
+ if (!existsSync19(gtrkPath))
9968
10314
  throw new Error(`找不到工程文件:${gtrkPath}`);
9969
10315
  let transcriptPath;
9970
10316
  if (opts.transcript)
9971
10317
  transcriptPath = resolve9(opts.transcript);
9972
10318
  else if (project)
9973
10319
  transcriptPath = firstExisting([
9974
- join22(project, "transcript", "transcript.json"),
9975
- join22(project, "json", "transcript.json"),
9976
- join22(project, "transcript.json")
10320
+ join23(project, "transcript", "transcript.json"),
10321
+ join23(project, "json", "transcript.json"),
10322
+ join23(project, "transcript.json")
9977
10323
  ]);
9978
- const baseDir = project ?? dirname9(gtrkPath);
10324
+ const baseDir = project ?? dirname10(gtrkPath);
9979
10325
  return { baseDir, gtrkPath, transcriptPath };
9980
10326
  }
9981
10327
  function slugify2(name) {
@@ -9987,7 +10333,7 @@ async function loadTranscript(path) {
9987
10333
  if (!t || !Array.isArray(t.utterances) || typeof t.material_id !== "string" || typeof t.text_hash !== "string") {
9988
10334
  throw new Error(`transcript.json 结构异常(缺 utterances/material_id/text_hash):${path}`);
9989
10335
  }
9990
- 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(`
9991
10337
  `), "utf8").digest("hex");
9992
10338
  return t;
9993
10339
  }
@@ -9998,15 +10344,15 @@ async function runSplit(splitdoc, opts) {
9998
10344
  return splitdoc ? runLand(resolve9(splitdoc), baseDir, gtrkPath, transcriptPath, opts) : runView(baseDir, gtrkPath, transcriptPath, opts);
9999
10345
  }
10000
10346
  async function runView(baseDir, gtrkPath, transcriptPath, opts) {
10001
- if (!transcriptPath || !existsSync18(transcriptPath))
10347
+ if (!transcriptPath || !existsSync19(transcriptPath))
10002
10348
  throw new Error(TRANSCRIPT_MISSING);
10003
10349
  log.step("▶ 导出投影视图(transcript × 当刻 .gtrk)…");
10004
10350
  const transcript = await loadTranscript(transcriptPath);
10005
10351
  const { gtrk } = readGtrk(gtrkPath);
10006
10352
  const view = projectTranscript(transcript, gtrk, { words: opts.words });
10007
- const splitDir = join22(baseDir, "split");
10353
+ const splitDir = join23(baseDir, "split");
10008
10354
  await mkdir8(splitDir, { recursive: true });
10009
- const viewPath = join22(splitDir, "view.json");
10355
+ const viewPath = join23(splitDir, "view.json");
10010
10356
  await writeFile9(viewPath, JSON.stringify(view, null, 2));
10011
10357
  const dropped = view.utterances.filter((u) => u.dropped).length;
10012
10358
  log.ok(`投影视图已生成:${viewPath}(${view.utterances.length} 条,其中 ${dropped} 条被剪)`);
@@ -10024,9 +10370,9 @@ async function runView(baseDir, gtrkPath, transcriptPath, opts) {
10024
10370
  return result;
10025
10371
  }
10026
10372
  async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
10027
- if (!existsSync18(splitdocPath))
10373
+ if (!existsSync19(splitdocPath))
10028
10374
  throw new Error(`找不到拆分稿:${splitdocPath}`);
10029
- if (!transcriptPath || !existsSync18(transcriptPath))
10375
+ if (!transcriptPath || !existsSync19(transcriptPath))
10030
10376
  throw new Error(TRANSCRIPT_MISSING);
10031
10377
  log.step("▶ 校验拆分稿并落地…");
10032
10378
  const doc = JSON.parse(await readFile6(splitdocPath, "utf8"));
@@ -10052,7 +10398,7 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
10052
10398
  }
10053
10399
  const projectedAt = new Date().toISOString();
10054
10400
  const view = projectTranscript(transcript, gtrk, { projectedAt });
10055
- const projectSlug = slugify2(basename11(baseDir));
10401
+ const projectSlug = slugify2(basename12(baseDir));
10056
10402
  const landing = buildLanding(doc, view, {
10057
10403
  utteranceIds: ctx.utteranceIds,
10058
10404
  projectSlug,
@@ -10063,13 +10409,13 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
10063
10409
  }
10064
10410
  });
10065
10411
  writeStructMetaSplit(gtrkPath, gtrk, landing.split, mtimeMs);
10066
- const splitDir = join22(baseDir, "split");
10412
+ const splitDir = join23(baseDir, "split");
10067
10413
  await mkdir8(splitDir, { recursive: true });
10068
- const dispatchPath = join22(splitDir, "dispatch.json");
10414
+ const dispatchPath = join23(splitDir, "dispatch.json");
10069
10415
  await writeFile9(dispatchPath, JSON.stringify(landing.dispatch, null, 2));
10070
10416
  let mdPath = null;
10071
10417
  if (opts.md) {
10072
- mdPath = join22(splitDir, "visual-split.md");
10418
+ mdPath = join23(splitDir, "visual-split.md");
10073
10419
  await writeFile9(mdPath, renderSplitMarkdown(doc, landing, { projectSlug, projectedAt }));
10074
10420
  }
10075
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})`);
@@ -10101,8 +10447,8 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
10101
10447
  }
10102
10448
 
10103
10449
  // src/commands/matrix.ts
10104
- import { resolve as resolve11, join as join23, dirname as dirname10, basename as basename12 } from "node:path";
10105
- import { existsSync as existsSync20 } from "node:fs";
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";
10106
10452
  import { readFile as readFile7, writeFile as writeFile10, mkdir as mkdir9, rename } from "node:fs/promises";
10107
10453
 
10108
10454
  // src/lib/solid-png.ts
@@ -10839,7 +11185,7 @@ function layBrollTracks(opts) {
10839
11185
  }
10840
11186
 
10841
11187
  // src/lib/material-integrity.ts
10842
- import { existsSync as existsSync19 } from "node:fs";
11188
+ import { existsSync as existsSync20 } from "node:fs";
10843
11189
  import { resolve as resolve10 } from "node:path";
10844
11190
  var INTEGRITY_LIST_CAP = 10;
10845
11191
  var r35 = (n) => Math.round(n * 1000) / 1000;
@@ -10900,7 +11246,7 @@ function collectMaterialRefs(gtrk) {
10900
11246
  return out;
10901
11247
  }
10902
11248
  function checkMaterialIntegrity(opts) {
10903
- const exists = opts.exists ?? ((p) => existsSync19(p));
11249
+ const exists = opts.exists ?? ((p) => existsSync20(p));
10904
11250
  const materials = Array.isArray(opts.gtrk.materials) ? opts.gtrk.materials : [];
10905
11251
  const refs = collectMaterialRefs(opts.gtrk);
10906
11252
  const counts = { relative: 0, absolute: 0, remote: 0, noPath: 0 };
@@ -11243,14 +11589,14 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
11243
11589
  let baseDir;
11244
11590
  if (opts.dispatch) {
11245
11591
  dispatchPath = resolve11(opts.dispatch);
11246
- baseDir = dirname10(dirname10(dispatchPath));
11592
+ baseDir = dirname11(dirname11(dispatchPath));
11247
11593
  } else if (opts.project) {
11248
11594
  baseDir = resolve11(opts.project);
11249
- dispatchPath = join23(baseDir, "split", "dispatch.json");
11595
+ dispatchPath = join24(baseDir, "split", "dispatch.json");
11250
11596
  } else {
11251
11597
  throw new Error('需 --project <目录> 或显式 --dispatch <path>(ad-hoc 检索用:gtrk matrix search "<query>")');
11252
11598
  }
11253
- if (!existsSync20(dispatchPath))
11599
+ if (!existsSync21(dispatchPath))
11254
11600
  throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split <拆分稿> 落地派单)`);
11255
11601
  const dispatch = JSON.parse(await readFile7(dispatchPath, "utf8"));
11256
11602
  const rawQueue = Array.isArray(dispatch.film_broll) ? dispatch.film_broll : [];
@@ -11307,7 +11653,7 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
11307
11653
  if (totalQueries > 0 && okCount === 0) {
11308
11654
  throw new Error(`全部 ${totalQueries} 个 query 检索失败,未写入 plan(逐条原因见上方日志)`);
11309
11655
  }
11310
- const projectSlug = slugify3(basename12(baseDir));
11656
+ const projectSlug = slugify3(basename13(baseDir));
11311
11657
  const plan = buildPlan({
11312
11658
  generatedAt: new Date().toISOString(),
11313
11659
  memberType: tier,
@@ -11315,9 +11661,9 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
11315
11661
  columnId,
11316
11662
  beats
11317
11663
  });
11318
- const splitDir = join23(baseDir, "split");
11664
+ const splitDir = join24(baseDir, "split");
11319
11665
  await mkdir9(splitDir, { recursive: true });
11320
- const planPath = join23(splitDir, "broll-plan.json");
11666
+ const planPath = join24(splitDir, "broll-plan.json");
11321
11667
  await writeFile10(planPath, JSON.stringify(plan, null, 2));
11322
11668
  log.ok(`候选清单已生成:${planPath}(${beats.length} beat · ${okCount}/${totalQueries} query 成功 · ${resultCount} 条候选)`);
11323
11669
  log.info("清单只含引用不含素材:cover_url 可直接预览;url 带签名默认 24h 过期,过期重跑本命令即重签。");
@@ -11365,13 +11711,13 @@ function parseScoreFloor(raw) {
11365
11711
  return SCORE_FLOOR_DEFAULT;
11366
11712
  }
11367
11713
  function locateGtrk(baseDir) {
11368
- const cands = [join23(baseDir, "gtrk", "project.gtrk"), join23(baseDir, "project.gtrk")];
11369
- 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));
11370
11716
  }
11371
11717
  async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRelay, reproj) {
11372
11718
  const gtrkPath = locateGtrk(baseDir);
11373
11719
  if (!gtrkPath) {
11374
- log.warn(`未找到工程文件(${join23(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——plan 已产出,可后续在有工程的目录重跑`);
11720
+ log.warn(`未找到工程文件(${join24(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——plan 已产出,可后续在有工程的目录重跑`);
11375
11721
  return;
11376
11722
  }
11377
11723
  const { gtrk, mtimeMs } = readGtrk(gtrkPath);
@@ -11379,8 +11725,8 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
11379
11725
  const { fills, clipIds } = planBeatFills(plan, layN, scoreFloor);
11380
11726
  const slotCount = [...fills.values()].flat().reduce((n, s) => n + s.length, 0);
11381
11727
  log.step(`▶ 候选铺轨(${layN} 轨 · 平铺 ${slotCount} 槽位 · ${clipIds.size} 个 clip,preview 代理)…`);
11382
- const gtrkDir = dirname10(gtrkPath);
11383
- const previewDir = join23(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
11728
+ const gtrkDir = dirname11(gtrkPath);
11729
+ const previewDir = join24(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
11384
11730
  await mkdir9(previewDir, { recursive: true });
11385
11731
  const prevSource = new Map;
11386
11732
  const prevBroll = gtrk.struct_meta?.broll;
@@ -11403,8 +11749,8 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
11403
11749
  if (!cand)
11404
11750
  continue;
11405
11751
  const rel = `${BROLL_PREVIEW_DIR}/${clipId}.mp4`;
11406
- const abs = join23(gtrkDir, ...rel.split("/"));
11407
- if (existsSync20(abs)) {
11752
+ const abs = join24(gtrkDir, ...rel.split("/"));
11753
+ if (existsSync21(abs)) {
11408
11754
  const prev = prevSource.get(clipId);
11409
11755
  if (prev !== "raw") {
11410
11756
  downloads.set(clipId, { rel, source: prev ?? "preview" });
@@ -11466,10 +11812,10 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
11466
11812
  const canvas = gtrk.video_size;
11467
11813
  const spec = { hex: BLACK_BED_HEX, width: canvas[0], height: canvas[1] };
11468
11814
  const rel = solidRelPath(spec);
11469
- const abs = join23(gtrkDir, ...rel.split("/"));
11815
+ const abs = join24(gtrkDir, ...rel.split("/"));
11470
11816
  try {
11471
- if (!existsSync20(abs)) {
11472
- await mkdir9(dirname10(abs), { recursive: true });
11817
+ if (!existsSync21(abs)) {
11818
+ await mkdir9(dirname11(abs), { recursive: true });
11473
11819
  const tmp = `${abs}.tmp-${process.pid}`;
11474
11820
  await writeFile10(tmp, encodeSolidPng(spec));
11475
11821
  await rename(tmp, abs);
@@ -11584,8 +11930,8 @@ function slugify3(name) {
11584
11930
  }
11585
11931
 
11586
11932
  // src/commands/mg.ts
11587
- import { resolve as resolve12, join as join24, dirname as dirname11, basename as basename13 } from "node:path";
11588
- import { existsSync as existsSync21 } from "node:fs";
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";
11589
11935
  import { readFile as readFile8, mkdir as mkdir10, copyFile } from "node:fs/promises";
11590
11936
 
11591
11937
  // src/lib/mg-lint.ts
@@ -13247,27 +13593,27 @@ async function runMg(words, opts) {
13247
13593
  function resolveDispatch(opts) {
13248
13594
  if (opts.dispatch) {
13249
13595
  const dispatchPath = resolve12(opts.dispatch);
13250
- return { dispatchPath, baseDir: dirname11(dirname11(dispatchPath)) };
13596
+ return { dispatchPath, baseDir: dirname12(dirname12(dispatchPath)) };
13251
13597
  }
13252
13598
  if (opts.project) {
13253
13599
  const baseDir = resolve12(opts.project);
13254
- return { dispatchPath: join24(baseDir, "split", "dispatch.json"), baseDir };
13600
+ return { dispatchPath: join25(baseDir, "split", "dispatch.json"), baseDir };
13255
13601
  }
13256
13602
  throw new Error("需 --project <目录> 或显式 --dispatch <path>");
13257
13603
  }
13258
13604
  function locateGtrk2(baseDir) {
13259
- return [join24(baseDir, "gtrk", "project.gtrk"), join24(baseDir, "project.gtrk")].find((p) => existsSync21(p));
13605
+ return [join25(baseDir, "gtrk", "project.gtrk"), join25(baseDir, "project.gtrk")].find((p) => existsSync22(p));
13260
13606
  }
13261
13607
  function locateSrcHtml(baseDir, compositionId) {
13262
13608
  for (const d of MG_SRC_DIRS) {
13263
- const p = join24(baseDir, d, `${compositionId}.html`);
13264
- if (existsSync21(p))
13609
+ const p = join25(baseDir, d, `${compositionId}.html`);
13610
+ if (existsSync22(p))
13265
13611
  return p;
13266
13612
  }
13267
13613
  return;
13268
13614
  }
13269
13615
  async function readMgQueue(dispatchPath) {
13270
- if (!existsSync21(dispatchPath))
13616
+ if (!existsSync22(dispatchPath))
13271
13617
  throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split 落地派单)`);
13272
13618
  const dispatch = JSON.parse(await readFile8(dispatchPath, "utf8"));
13273
13619
  const queue = dispatch.mg ?? dispatch.rrv_mg;
@@ -13343,7 +13689,7 @@ async function runLay(opts) {
13343
13689
  const srcPath = locateSrcHtml(baseDir, q.composition_id);
13344
13690
  if (!srcPath) {
13345
13691
  skipped.push({ beat: q.beat, reason: "缺颗粒 HTML(未产出)" });
13346
- log.warn(`${q.beat}:缺 ${join24(baseDir, MG_SRC_DIRS[0], `${q.composition_id}.html`)},跳过`);
13692
+ log.warn(`${q.beat}:缺 ${join25(baseDir, MG_SRC_DIRS[0], `${q.composition_id}.html`)},跳过`);
13347
13693
  continue;
13348
13694
  }
13349
13695
  const html = await readFile8(srcPath, "utf8");
@@ -13391,7 +13737,7 @@ async function runLay(opts) {
13391
13737
  });
13392
13738
  }
13393
13739
  if (!gtrkPath || !project) {
13394
- log.warn(`未找到工程文件(${join24(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——lint 已完成`);
13740
+ log.warn(`未找到工程文件(${join25(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——lint 已完成`);
13395
13741
  return done(opts, {
13396
13742
  ok: skipped.length === 0,
13397
13743
  mode: "lay",
@@ -13408,7 +13754,7 @@ async function runLay(opts) {
13408
13754
  });
13409
13755
  }
13410
13756
  const { gtrk, mtimeMs } = project;
13411
- const gtrkDir = dirname11(gtrkPath);
13757
+ const gtrkDir = dirname12(gtrkPath);
13412
13758
  const covered = new Set(items.map((it) => it.composition_id));
13413
13759
  const orphans = laidBefore.filter((id) => !covered.has(id));
13414
13760
  const inDispatch = new Set(allQueue.map((q) => q.composition_id));
@@ -13450,9 +13796,9 @@ async function runLay(opts) {
13450
13796
  if (opts.replaceAll && orphans.length > 0) {
13451
13797
  log.warn(`--replace-all:已显式授权重置整轨,轨上其余 ${orphans.length} 颗已铺颗粒将被剥离(不走增量保留)。`);
13452
13798
  }
13453
- await mkdir10(join24(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
13799
+ await mkdir10(join25(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
13454
13800
  for (const it of items) {
13455
- await copyFile(srcByComp.get(it.composition_id), join24(gtrkDir, ...it.html_rel.split("/")));
13801
+ await copyFile(srcByComp.get(it.composition_id), join25(gtrkDir, ...it.html_rel.split("/")));
13456
13802
  }
13457
13803
  const { next, summary, mg } = layMgTracks({ gtrk, items, generatedAt: new Date().toISOString(), keep });
13458
13804
  const written = withTimecodeSource(next, "mg", reproj);
@@ -13497,11 +13843,11 @@ async function runLint(args, opts) {
13497
13843
  if (!file)
13498
13844
  throw new Error("用法:gtrk mg lint <particle.html> [--dispatch <path>]");
13499
13845
  const html = await readFile8(resolve12(file), "utf8");
13500
- const nameId = basename13(file).replace(/\.html?$/i, "");
13846
+ const nameId = basename14(file).replace(/\.html?$/i, "");
13501
13847
  let dispatchIds;
13502
13848
  let slotDuration;
13503
13849
  let compositionId;
13504
- if (opts.dispatch && existsSync21(resolve12(opts.dispatch))) {
13850
+ if (opts.dispatch && existsSync22(resolve12(opts.dispatch))) {
13505
13851
  const queue = await readMgQueue(resolve12(opts.dispatch));
13506
13852
  dispatchIds = queue.map((q) => q.composition_id);
13507
13853
  const byName = queue.find((q) => q.composition_id === nameId);
@@ -13525,7 +13871,7 @@ async function runLint(args, opts) {
13525
13871
  for (const vv of lint.violations)
13526
13872
  (vv.fatal ? log.err : log.warn)(`${vv.fatal ? "✗" : "·"} ${vv.law}: ${vv.msg}`);
13527
13873
  if (lint.ok)
13528
- log.ok(`lint 通过(${basename13(file)};opaque=${lint.opaque})`);
13874
+ log.ok(`lint 通过(${basename14(file)};opaque=${lint.opaque})`);
13529
13875
  else
13530
13876
  log.err(`lint 未过(${lint.violations.filter((v) => v.fatal).length} 项致命)`);
13531
13877
  const result = { mode: "lint", ...lint, ok: lint.ok };
@@ -13568,8 +13914,8 @@ function done(opts, result) {
13568
13914
 
13569
13915
  // src/lib/mad/mad.ts
13570
13916
  import { mkdir as mkdir13, writeFile as writeFile12 } from "node:fs/promises";
13571
- import { existsSync as existsSync24, statSync as statSync3 } from "node:fs";
13572
- import { resolve as resolve13, join as join28 } from "node:path";
13917
+ import { existsSync as existsSync25, statSync as statSync3 } from "node:fs";
13918
+ import { resolve as resolve13, join as join29 } from "node:path";
13573
13919
 
13574
13920
  // src/lib/convert/types.ts
13575
13921
  function num2(v, d = 0) {
@@ -14348,19 +14694,19 @@ function madJsx(opts) {
14348
14694
  }
14349
14695
 
14350
14696
  // src/lib/mad/scan.ts
14351
- import { readdirSync, statSync as statSync2 } from "node:fs";
14352
- import { extname as extname7, join as join25 } 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";
14353
14699
  var VIDEO_EXTS2 = new Set(defaultExtsFor("video") ?? []);
14354
14700
  function scanFolder(dirAbs, opts = {}) {
14355
14701
  const probe = opts.probe ?? probeGeometry;
14356
14702
  const warn = opts.warn ?? (() => {});
14357
14703
  let entries;
14358
14704
  try {
14359
- entries = readdirSync(dirAbs);
14705
+ entries = readdirSync3(dirAbs);
14360
14706
  } catch {
14361
14707
  throw new Error(`素材文件夹不存在或无法读取:${dirAbs}`);
14362
14708
  }
14363
- const files = entries.filter((n) => VIDEO_EXTS2.has(extname7(n).toLowerCase())).map((n) => join25(dirAbs, n)).filter((p) => {
14709
+ const files = entries.filter((n) => VIDEO_EXTS2.has(extname7(n).toLowerCase())).map((n) => join26(dirAbs, n)).filter((p) => {
14364
14710
  try {
14365
14711
  return statSync2(p).isFile();
14366
14712
  } catch {
@@ -14538,10 +14884,10 @@ function beatQuantized(natLens, analysis) {
14538
14884
  }
14539
14885
 
14540
14886
  // src/lib/mad/data.ts
14541
- import { createHash as createHash2 } from "node:crypto";
14887
+ import { createHash as createHash3 } from "node:crypto";
14542
14888
  import { mkdir as mkdir11, readFile as readFile9, rename as rename2, rm, writeFile as writeFile11, readdir as readdir2 } from "node:fs/promises";
14543
- import { existsSync as existsSync22 } from "node:fs";
14544
- import { join as join26 } from "node:path";
14889
+ import { existsSync as existsSync23 } from "node:fs";
14890
+ import { join as join27 } from "node:path";
14545
14891
  function madCacheDir() {
14546
14892
  return homeFile("mad-cache");
14547
14893
  }
@@ -14553,7 +14899,7 @@ function manifestUrl() {
14553
14899
  }
14554
14900
  var REQUIRED_KEYS = ["mad_pool"];
14555
14901
  function sha256Hex(buf) {
14556
- return createHash2("sha256").update(buf).digest("hex");
14902
+ return createHash3("sha256").update(buf).digest("hex");
14557
14903
  }
14558
14904
  async function fetchWithTimeout(fetchFn, url, timeoutMs) {
14559
14905
  const ctrl = new AbortController;
@@ -14570,7 +14916,7 @@ async function atomicWrite(dest, data) {
14570
14916
  await rename2(tmp, dest);
14571
14917
  }
14572
14918
  async function verifyFile(path, sha256) {
14573
- if (!existsSync22(path))
14919
+ if (!existsSync23(path))
14574
14920
  return false;
14575
14921
  try {
14576
14922
  const buf = await readFile9(path);
@@ -14598,7 +14944,7 @@ async function cleanupOldVersions(cacheRoot, keepVersion, warn) {
14598
14944
  for (const e of entries) {
14599
14945
  const m = /^v(\d+)$/.exec(e);
14600
14946
  if (m && Number(m[1]) !== keepVersion) {
14601
- await rm(join26(cacheRoot, e), { recursive: true, force: true }).catch(() => {});
14947
+ await rm(join27(cacheRoot, e), { recursive: true, force: true }).catch(() => {});
14602
14948
  }
14603
14949
  }
14604
14950
  } catch {}
@@ -14607,7 +14953,7 @@ async function ensureMadData(opts, deps) {
14607
14953
  const { cacheRoot, warn } = deps;
14608
14954
  const timeout = deps.manifestTimeoutMs ?? 8000;
14609
14955
  const mfUrl = deps.manifestUrl ?? manifestUrl();
14610
- const snapshotPath = join26(cacheRoot, "manifest.json");
14956
+ const snapshotPath = join27(cacheRoot, "manifest.json");
14611
14957
  let manifest = null;
14612
14958
  let online = false;
14613
14959
  try {
@@ -14622,7 +14968,7 @@ async function ensureMadData(opts, deps) {
14622
14968
  warn(`manifest 拉取失败(${e instanceof Error ? e.message : String(e)}),回退本地缓存`);
14623
14969
  }
14624
14970
  if (!manifest) {
14625
- if (!existsSync22(snapshotPath)) {
14971
+ if (!existsSync23(snapshotPath)) {
14626
14972
  throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
14627
14973
  }
14628
14974
  try {
@@ -14637,14 +14983,14 @@ async function ensureMadData(opts, deps) {
14637
14983
  }
14638
14984
  }
14639
14985
  const version = manifest.version;
14640
- const verDir = join26(cacheRoot, `v${version}`);
14641
- const poolPath = join26(verDir, "mad_pool.json");
14986
+ const verDir = join27(cacheRoot, `v${version}`);
14987
+ const poolPath = join27(verDir, "mad_pool.json");
14642
14988
  const poolMeta = manifest.datasets.mad_pool;
14643
14989
  const cacheValid = await verifyFile(poolPath, poolMeta.sha256);
14644
14990
  const needDownload = !!opts.refresh || !cacheValid;
14645
14991
  if (needDownload) {
14646
14992
  if (!online) {
14647
- if (existsSync22(poolPath)) {
14993
+ if (existsSync23(poolPath)) {
14648
14994
  throw new Error("本地技法数据缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
14649
14995
  }
14650
14996
  throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
@@ -14680,10 +15026,10 @@ async function ensureMadData(opts, deps) {
14680
15026
  // src/lib/mad/pool.ts
14681
15027
  import { gunzipSync } from "node:zlib";
14682
15028
  import { mkdir as mkdir12, readFile as readFile10 } from "node:fs/promises";
14683
- import { existsSync as existsSync23 } from "node:fs";
14684
- import { join as join27 } from "node:path";
15029
+ import { existsSync as existsSync24 } from "node:fs";
15030
+ import { join as join28 } from "node:path";
14685
15031
  function shardPath(verDir, shard) {
14686
- return join27(verDir, "ir", `${shard}.json.gz`);
15032
+ return join28(verDir, "ir", `${shard}.json.gz`);
14687
15033
  }
14688
15034
  function decodeShard(gz) {
14689
15035
  const json = gunzipSync(gz).toString("utf8");
@@ -14700,7 +15046,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
14700
15046
  if (cached)
14701
15047
  return cached;
14702
15048
  const path = shardPath(verDir, shard);
14703
- if (existsSync23(path)) {
15049
+ if (existsSync24(path)) {
14704
15050
  try {
14705
15051
  const s2 = decodeShard(await readFile10(path));
14706
15052
  memo.set(shard, s2);
@@ -14718,7 +15064,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
14718
15064
  throw new Error(`IR 分片下载失败 HTTP ${res.status}:${url}`);
14719
15065
  const buf = new Uint8Array(await res.arrayBuffer());
14720
15066
  const s = decodeShard(buf);
14721
- await mkdir12(join27(verDir, "ir"), { recursive: true });
15067
+ await mkdir12(join28(verDir, "ir"), { recursive: true });
14722
15068
  await atomicWrite(path, buf);
14723
15069
  memo.set(shard, s);
14724
15070
  return s;
@@ -14732,7 +15078,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
14732
15078
  return ir;
14733
15079
  },
14734
15080
  shardCached(shard) {
14735
- return memo.has(shard) || existsSync23(shardPath(verDir, shard));
15081
+ return memo.has(shard) || existsSync24(shardPath(verDir, shard));
14736
15082
  }
14737
15083
  };
14738
15084
  }
@@ -14816,7 +15162,7 @@ async function runMad(inputArg, opts, deps = {}) {
14816
15162
  if (!inputArg)
14817
15163
  throw new Error("用法:gtrk tool mad <素材文件夹> [--bgm 歌.mp3]");
14818
15164
  const dirAbs = resolve13(inputArg);
14819
- if (!existsSync24(dirAbs) || !statSync3(dirAbs).isDirectory()) {
15165
+ if (!existsSync25(dirAbs) || !statSync3(dirAbs).isDirectory()) {
14820
15166
  throw new Error(`素材文件夹不存在或不是目录:${dirAbs}`);
14821
15167
  }
14822
15168
  const { videos, orientation, skipped } = scanFolder(dirAbs, {
@@ -14930,9 +15276,9 @@ async function runMad(inputArg, opts, deps = {}) {
14930
15276
  const header = madHeader(version, now.toISOString());
14931
15277
  const bgm = level <= 2 && bgmForTrack ? { path: bgmForTrack, markers } : undefined;
14932
15278
  const { jsx } = madJsx({ master, windows, header, bgm });
14933
- const outDir = opts.out ? resolve13(opts.out) : join28(process.cwd(), `mad-${timestamp4(now)}`);
15279
+ const outDir = opts.out ? resolve13(opts.out) : join29(process.cwd(), `mad-${timestamp4(now)}`);
14934
15280
  await mkdir13(outDir, { recursive: true });
14935
- const jsxPath = join28(outDir, "mad.jsx");
15281
+ const jsxPath = join29(outDir, "mad.jsx");
14936
15282
  await writeFile12(jsxPath, jsx);
14937
15283
  const result = {
14938
15284
  ok: true,
@@ -14944,7 +15290,7 @@ async function runMad(inputArg, opts, deps = {}) {
14944
15290
  degradeLevel: level,
14945
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 }))
14946
15292
  };
14947
- await writeFile12(join28(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));
14948
15294
  warn(completionMessage(jsxPath, level));
14949
15295
  return result;
14950
15296
  }
@@ -15077,9 +15423,9 @@ async function runMadInTool(inputArg, opts) {
15077
15423
  }
15078
15424
 
15079
15425
  // src/commands/transcript.ts
15080
- import { existsSync as existsSync25 } from "node:fs";
15426
+ import { existsSync as existsSync26 } from "node:fs";
15081
15427
  import { mkdir as mkdir14, rename as rename3, rm as rm2, stat as stat6, writeFile as writeFile13 } from "node:fs/promises";
15082
- import { basename as basename14, dirname as dirname12, extname as extname8, join as join29, resolve as resolve14 } from "node:path";
15428
+ import { basename as basename15, dirname as dirname13, extname as extname8, join as join30, resolve as resolve14 } from "node:path";
15083
15429
 
15084
15430
  // src/lib/transcript.ts
15085
15431
  var AGENT_SUMMARY_PENDING = "<!-- gtrk:agent-summary-pending -->";
@@ -15216,7 +15562,7 @@ async function validateTranscriptInput(input) {
15216
15562
  throw new Error("视频转文字稿仅支持本地视频文件,不支持 URL、平台视频地址或远端下载");
15217
15563
  }
15218
15564
  const inputAbs = resolve14(input);
15219
- if (!existsSync25(inputAbs))
15565
+ if (!existsSync26(inputAbs))
15220
15566
  throw new Error(`本地视频不存在:${inputAbs}`);
15221
15567
  const info = await stat6(inputAbs);
15222
15568
  if (!info.isFile())
@@ -15228,14 +15574,14 @@ async function validateTranscriptInput(input) {
15228
15574
  return inputAbs;
15229
15575
  }
15230
15576
  function resolveTranscriptOutput(inputAbs, out) {
15231
- const base = basename14(inputAbs, extname8(inputAbs));
15232
- const output = out ? resolve14(out) : join29(dirname12(inputAbs), `${base}-transcript.md`);
15577
+ const base = basename15(inputAbs, extname8(inputAbs));
15578
+ const output = out ? resolve14(out) : join30(dirname13(inputAbs), `${base}-transcript.md`);
15233
15579
  if (extname8(output).toLowerCase() !== ".md")
15234
15580
  throw new Error("--out 必须指向一个 .md 文件");
15235
15581
  return output;
15236
15582
  }
15237
15583
  async function writeMarkdownAtomic(path, markdown) {
15238
- await mkdir14(dirname12(path), { recursive: true });
15584
+ await mkdir14(dirname13(path), { recursive: true });
15239
15585
  const temp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
15240
15586
  try {
15241
15587
  await writeFile13(temp, markdown, "utf8");
@@ -15251,8 +15597,8 @@ async function runTranscript(input, opts = {}, depsOverride) {
15251
15597
  const output = resolveTranscriptOutput(inputAbs, opts.out);
15252
15598
  const deps = buildDeps(depsOverride);
15253
15599
  const language = opts.lang?.trim() || "zh-CN";
15254
- const sourceName = basename14(inputAbs);
15255
- const title = basename14(inputAbs, extname8(inputAbs));
15600
+ const sourceName = basename15(inputAbs);
15601
+ const title = basename15(inputAbs, extname8(inputAbs));
15256
15602
  log.step(`▶ 视频转文字稿:${sourceName}`);
15257
15603
  log.step("① 本地探测视频…");
15258
15604
  const geometry = deps.probe(inputAbs, opts.ffmpegPath);
@@ -15264,7 +15610,7 @@ async function runTranscript(input, opts = {}, depsOverride) {
15264
15610
  log.step("② 本地抽取 16k 单声道音频(原视频不上传)…");
15265
15611
  const audio = await deps.extract(inputAbs, opts.ffmpegPath);
15266
15612
  deps.assertDuration(geometry.duration, audio, opts.ffmpegPath);
15267
- log.info(`上传物:${basename14(audio)}(仅音频衍生物)`);
15613
+ log.info(`上传物:${basename15(audio)}(仅音频衍生物)`);
15268
15614
  log.step("③ 上传音频并提交 ASR…");
15269
15615
  const payload = (fileId) => ({ file_id: fileId, language, word_level: true });
15270
15616
  const submitted = await uploadAndSubmitTask(deps.cfg, audio, TASK_TYPE4, payload, {
@@ -15312,9 +15658,9 @@ function registerTranscript(program2) {
15312
15658
  }
15313
15659
 
15314
15660
  // src/commands/music-visualizer.ts
15315
- import { resolve as resolve15, join as join30, dirname as dirname13, basename as basename15, extname as extname9 } from "node:path";
15661
+ import { resolve as resolve15, join as join31, dirname as dirname14, basename as basename16, extname as extname9 } from "node:path";
15316
15662
  import { mkdir as mkdir15, writeFile as writeFile14 } from "node:fs/promises";
15317
- import { existsSync as existsSync26 } from "node:fs";
15663
+ import { existsSync as existsSync27 } from "node:fs";
15318
15664
  var TASK_TYPE5 = "music_visualizer";
15319
15665
  var PRICE_KEY2 = "music_visualizer";
15320
15666
  var HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
@@ -15362,7 +15708,7 @@ function timestamp5() {
15362
15708
  return `${p(d.getFullYear() % 100)}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
15363
15709
  }
15364
15710
  function assertExt(pathAbs, exts, label) {
15365
- if (!existsSync26(pathAbs))
15711
+ if (!existsSync27(pathAbs))
15366
15712
  throw new Error(`${label}不存在:${pathAbs}`);
15367
15713
  const e = extname9(pathAbs).toLowerCase();
15368
15714
  if (exts.length && !exts.includes(e)) {
@@ -15431,9 +15777,9 @@ async function runMusicVisualizer(audio, opts) {
15431
15777
  if (coverAbs)
15432
15778
  assertExt(coverAbs, IMAGE_EXTS2, "封面图");
15433
15779
  const extraParams = parseExtraParams3(opts.param, opts.paramsJson);
15434
- const projName = basename15(audioAbs, extname9(audioAbs));
15435
- const outDir = resolve15(opts.out ?? join30(dirname13(audioAbs), `${projName}-visualizer-${timestamp5()}`));
15436
- 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 ? " · 封面" : ""})`);
15437
15783
  let billingHint;
15438
15784
  try {
15439
15785
  billingHint = (await resolveToolPricing(PRICE_KEY2)).billingHint;
@@ -15479,7 +15825,7 @@ async function runMusicVisualizer(audio, opts) {
15479
15825
  const { taskId } = submitted;
15480
15826
  log.info(`task_id = ${taskId}`);
15481
15827
  await mkdir15(outDir, { recursive: true });
15482
- await writeFile14(join30(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));
15483
15829
  log.step("③ 云端处理中(每 5s 轮询)…");
15484
15830
  const result = await pollTask(cfg, TASK_TYPE5, taskId, (status, progress) => {
15485
15831
  log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
@@ -15490,7 +15836,7 @@ async function runMusicVisualizer(audio, opts) {
15490
15836
  const files = [];
15491
15837
  const errors = {};
15492
15838
  if (url) {
15493
- const dest = join30(outDir, `${projName}-visualizer${extFromUrl(url, ".mp4")}`);
15839
+ const dest = join31(outDir, `${projName}-visualizer${extFromUrl(url, ".mp4")}`);
15494
15840
  try {
15495
15841
  await downloadStream(url, dest);
15496
15842
  files.push(dest);
@@ -15512,7 +15858,7 @@ async function runMusicVisualizer(audio, opts) {
15512
15858
  ...Object.keys(errors).length ? { errors } : {},
15513
15859
  finishedAt: new Date().toISOString()
15514
15860
  };
15515
- await writeFile14(join30(outDir, "result.json"), JSON.stringify(resultJson, null, 2));
15861
+ await writeFile14(join31(outDir, "result.json"), JSON.stringify(resultJson, null, 2));
15516
15862
  if (opts.json) {
15517
15863
  process.stdout.write(`${JSON.stringify(resultJson)}
15518
15864
  `);
@@ -15525,12 +15871,121 @@ async function runMusicVisualizer(audio, opts) {
15525
15871
  log.ok(`完成。产物目录:${outDir}`);
15526
15872
  }
15527
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
+
15528
15983
  // src/index.ts
15529
15984
  try {
15530
15985
  process.loadEnvFile?.();
15531
15986
  } catch {}
15532
15987
  migrateLegacyHome();
15533
- var { version } = JSON.parse(readFileSync6(join31(packageRoot(), "package.json"), "utf8"));
15988
+ var { version } = JSON.parse(readFileSync6(join33(packageRoot(), "package.json"), "utf8"));
15534
15989
  var program2 = new Command;
15535
15990
  program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
15536
15991
  registerInstall(program2);
@@ -15548,6 +16003,7 @@ registerMg(program2);
15548
16003
  registerTool(program2);
15549
16004
  registerTranscript(program2);
15550
16005
  registerMusicVisualizer(program2);
16006
+ registerDeps(program2);
15551
16007
  program2.parseAsync(process.argv).catch((e) => {
15552
16008
  console.error(`
15553
16009
  ❌ ${e instanceof Error ? e.message : String(e)}`);