@gitruck/cli 0.2.19 → 0.2.20

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/README.md CHANGED
@@ -414,6 +414,7 @@ gtrk transcript "D:/素材/采访视频.mp4" --lang zh-CN --out "D:/文字稿/
414
414
  | `video_motion_cut` | 单条本地视频 | 运镜/高光片段结构 `result-output.json`(结构化数据,非下载文件) | 运行前实时查询 | 已上线 |
415
415
  | `video_speaker_detect` | 单条本地视频;可选 `--language`/`--max-faces-per-frame`/`--detect-body`/`--track-sample-fps`(重 GPU) | 可见说话人结构 `result-output.json`(时基以服务端输出为准) | 运行前实时查询 | 已上线 |
416
416
  | `video_face_track` | 单条本地视频;可选 `--sample-fps`/`--max-faces`/`--min-face-ratio`/`--enable-body-match`/`--similarity-threshold`;`time_ranges` 走 `--params-json`(重 GPU) | 人物 ID/时间段/轨迹结构 `result-output.json`(时基以服务端输出为准) | 运行前实时查询 | 已上线 |
417
+ | `audio_tts_clone` | **无文件**:`--text`/`--text-file` 二选一(≤2000 字)+ `--speaker` 必填;可选语言/格式/语速/切分法 | 配音音频 wav/mp3(按文本字数折算分钟计费) | 运行前实时查询 | 已上线 |
417
418
  | `video_ai_subtitle` | 单条视频或音频;`--language <码>` 必填;可选 `--translate-language`、`--need-render`、`--need-pure`、`--subtitle-type`、`--subtitle-color` | `.ass` 字幕 + 可选烧录/去字幕 `.mp4` + `result-output.json`(摘要 + 字级时间轴) | 运行前实时查询 | 已上线 |
418
419
  | `audio_separation` | 单条音频;可选 `--mode fast|turbo` | 人声与伴奏音频(按实际返回可为一项或两项) | 运行前实时查询 | 已上线 |
419
420
  | `audio_speaker_split` | 单条音频;可选 `--only-struct` | 各说话人 `.wav` 分轨 + `spoken_list` 时间线(`result-output.json`) | 运行前实时查询 | 已上线 |
@@ -467,6 +468,8 @@ gtrk transcript "D:/素材/采访视频.mp4" --lang zh-CN --out "D:/文字稿/
467
468
  - `gtrk tool video_split_screen a.mp4 b.mp4 --clips-json '[{"input":0,"begin_time_ms":0,"end_time_ms":5000},{"input":1,"crop":{"x":0.1,"y":0,"width":0.8,"height":1}}]'` — 精确档:按 0 起索引指定每段毫秒区间与归一化裁剪框;同一文件可多条目出多窗口。
468
469
  - `gtrk tool video_speaker_detect ./talk.mp4 --language zh-CN` — 检测画面里谁在何时说话,出结构化 JSON(重 GPU)。
469
470
  - `gtrk tool video_face_track ./talk.mp4 --params-json '{"time_ranges":[{"begin_time":0,"end_time":30000}]}'` — 人脸追踪/身份聚类,可限定时间段(**单位毫秒**;重 GPU)。
471
+ - `gtrk tool audio_tts_clone --text "欢迎收听本期节目" --speaker narrator` — 文字转配音音频(音色列表见官网文档)。
472
+ - `gtrk tool audio_tts_clone --text-file 稿子.txt --speaker sweet_female --output-format mp3` — 长文合成,缺省跟随音色调好的语速与切分参数。
470
473
  - `gtrk tool mad ./素材 [--bgm 歌.mp3] [--duration 20] [--seed 42] [--refresh] [--json]` — 一键剪 MAD:扫素材文件夹 → 自动选技法 → 单一 `.jsx`(AE 2020+ 跑一遍出母合成成片工程)。`--seed` 可复现;`result.json` 记 seed/数据版本/降级档位/选中技法。
471
474
  - 通用:`--out <dir>` 覆盖产物目录、`--param k=v`(可重复)/`--params-json '<对象>'` 透传云端参数、`--reupload` 忽略上传缓存、`--json` 机读、`--ffmpeg-path <dir>` 指定 ffmpeg 目录。
472
475
  - cloud 型工具缺 Key → 报错引导 `gtrk init`。产物下载失败(如链接过期 404)→ `result.json` 记 `errors`、`ok=false`、`task.json` 保留可凭 `taskId` 恢复。
package/dist/index.js CHANGED
@@ -4196,7 +4196,7 @@ var {
4196
4196
  } = import__.default;
4197
4197
 
4198
4198
  // src/index.ts
4199
- import { readFileSync as readFileSync5 } from "node:fs";
4199
+ import { readFileSync as readFileSync6 } from "node:fs";
4200
4200
  import { join as join30 } from "node:path";
4201
4201
 
4202
4202
  // src/lib/paths.ts
@@ -7286,12 +7286,12 @@ async function runOralCut(input, opts) {
7286
7286
  // src/commands/long2short.ts
7287
7287
  import { resolve as resolve6, join as join17, dirname as dirname5, basename as basename8, extname as extname5 } from "node:path";
7288
7288
  import { mkdir as mkdir6, writeFile as writeFile7, cp as cp2 } from "node:fs/promises";
7289
- import { existsSync as existsSync15 } from "node:fs";
7289
+ import { existsSync as existsSync16 } from "node:fs";
7290
7290
 
7291
7291
  // src/lib/tool-runner.ts
7292
7292
  import { resolve as resolve5, join as join16, dirname as dirname4, basename as basename7, extname as extname4 } from "node:path";
7293
7293
  import { mkdir as mkdir5, writeFile as writeFile6, stat as stat5 } from "node:fs/promises";
7294
- import { createWriteStream, existsSync as existsSync14 } from "node:fs";
7294
+ import { createWriteStream, existsSync as existsSync15 } from "node:fs";
7295
7295
  import { Readable as Readable2 } from "node:stream";
7296
7296
  import { pipeline } from "node:stream/promises";
7297
7297
 
@@ -7375,7 +7375,8 @@ async function resolveToolPricing(priceKey, pricingContext, fetchFn = fetch) {
7375
7375
  }
7376
7376
 
7377
7377
  // src/lib/tool-descriptors.ts
7378
- import { extname as extname3 } from "node:path";
7378
+ import { readFileSync as readFileSync4 } from "node:fs";
7379
+ import { extname as extname3, resolve as resolvePath } from "node:path";
7379
7380
  var MULTI_INPUT_KINDS = new Set(["images", "videos"]);
7380
7381
  var IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif", ".tif", ".tiff", ".heic", ".heif", ".avif"];
7381
7382
  var VIDEO_EXTS = [".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v", ".flv", ".wmv", ".mpg", ".mpeg", ".ts", ".m2ts"];
@@ -8191,7 +8192,7 @@ var audioStretch = {
8191
8192
  taskType: "audio_stretch",
8192
8193
  options: [
8193
8194
  { flag: "--semitones <n>", desc: "变调半音(升正降负;未传则不变调)" },
8194
- { flag: "--speed <n>", desc: "变速倍率(必须 > 0;未传则不变速)" }
8195
+ { flag: "--speed <n>", desc: "变速/语速倍率(各工具范围与缺省见各自说明)" }
8195
8196
  ],
8196
8197
  buildPayload(fileId, ctx) {
8197
8198
  const payload = { file_id: fileId };
@@ -8549,6 +8550,66 @@ var videoSplitScreen = {
8549
8550
  return url ? [{ url, filename: `${ctx.baseName}-splitscreen${extFromUrl(url, ".mp4")}` }] : [];
8550
8551
  }
8551
8552
  };
8553
+ var audioTtsClone = {
8554
+ name: "audio_tts_clone",
8555
+ title: "声音克隆配音",
8556
+ description: "输入一段文字,用指定音色合成配音音频(--text 短文本或 --text-file 文本文件二选一,≤2000 字)。按文本字数折算分钟计费。",
8557
+ kind: "cloud",
8558
+ input: { kind: "none" },
8559
+ priceKey: "audio_tts_clone",
8560
+ outputHint: "配音音频(wav/mp3)",
8561
+ enabled: true,
8562
+ taskType: "audio_tts_clone",
8563
+ options: [
8564
+ { flag: "--text <text>", desc: "待合成的短文本(与 --text-file 二选一)" },
8565
+ { flag: "--text-file <file>", desc: "待合成的文本文件(UTF-8;与 --text 二选一)" },
8566
+ { flag: "--speaker <code>", desc: "音色代号(必填;可用列表见官网文档「可用音色」,传错时报错会列出全部可用项)" },
8567
+ { flag: "--text-lang <lang>", desc: "文本语言(未传则服务端默认 zh)" },
8568
+ { flag: "--output-format <fmt>", desc: "输出音频格式 wav/mp3(未传则服务端默认 wav)" },
8569
+ { flag: "--split-method <m>", desc: "长文切分法 cut0~cut5(未传则跟随该音色调好的参数)" },
8570
+ { flag: "--batch-size <n>", desc: "分段并发批大小 1~16(未传则服务端默认)" }
8571
+ ],
8572
+ buildPayloadNone(ctx) {
8573
+ const o = ctx.opts;
8574
+ const hasText = typeof o.text === "string" && o.text.trim() !== "";
8575
+ const hasFile = typeof o.textFile === "string" && o.textFile.trim() !== "";
8576
+ if (hasText && hasFile)
8577
+ throw new Error("--text 与 --text-file 只能给其一");
8578
+ if (!hasText && !hasFile)
8579
+ throw new Error("需要 --text <短文本> 或 --text-file <文本文件>(UTF-8)之一");
8580
+ const text = hasText ? o.text : readFileSync4(resolvePath(o.textFile), "utf8");
8581
+ if (!text.trim())
8582
+ throw new Error("文本内容为空");
8583
+ if (!(typeof o.speaker === "string" && o.speaker.trim())) {
8584
+ throw new Error("audio_tts_clone 需要 --speaker <音色代号>(可用列表见官网文档;传错时服务端报错会列出可用项)");
8585
+ }
8586
+ const p = { text, speaker: o.speaker };
8587
+ if (typeof o.textLang === "string")
8588
+ p.text_lang = o.textLang;
8589
+ if (typeof o.outputFormat === "string")
8590
+ p.output_format = o.outputFormat;
8591
+ const speed = parseNumFlag(o.speed, "--speed");
8592
+ if (speed != null)
8593
+ p.speed = speed;
8594
+ if (typeof o.splitMethod === "string")
8595
+ p.text_split_method = o.splitMethod;
8596
+ const bs = parseNumFlag(o.batchSize, "--batch-size");
8597
+ if (bs != null) {
8598
+ if (!Number.isInteger(bs))
8599
+ throw new Error(`--batch-size 需要整数,拿到「${String(o.batchSize)}」`);
8600
+ p.batch_size = bs;
8601
+ }
8602
+ return p;
8603
+ },
8604
+ mapOutputs(out, ctx) {
8605
+ const url = pickUrl(out, ["download_url"]);
8606
+ if (!url)
8607
+ return [];
8608
+ const fallback = typeof ctx.opts.outputFormat === "string" ? `.${ctx.opts.outputFormat}` : ".wav";
8609
+ const speaker = typeof ctx.opts.speaker === "string" && ctx.opts.speaker.trim() ? ctx.opts.speaker : "voice";
8610
+ return [{ url, filename: `tts-${speaker}${extFromUrl(url, fallback)}` }];
8611
+ }
8612
+ };
8552
8613
  var mad = {
8553
8614
  name: "mad",
8554
8615
  title: "一键剪 MAD",
@@ -8599,6 +8660,7 @@ var TOOL_REGISTRY = [
8599
8660
  imageClassicTemplate,
8600
8661
  imageVerticalStitch,
8601
8662
  videoSplitScreen,
8663
+ audioTtsClone,
8602
8664
  mad
8603
8665
  ];
8604
8666
  function findTool(name, registry = TOOL_REGISTRY) {
@@ -8635,6 +8697,19 @@ function validateRegistry(registry = TOOL_REGISTRY) {
8635
8697
  } else if (d.buildPayloadMulti) {
8636
8698
  throw new Error(`非多文件工具不得声明 buildPayloadMulti:「${d.name}」`);
8637
8699
  }
8700
+ if (d.buildPayloadNone) {
8701
+ if (d.input.kind !== "none")
8702
+ throw new Error(`非 none 工具不得声明 buildPayloadNone:「${d.name}」`);
8703
+ if (d.buildPayload || d.buildPayloadMulti) {
8704
+ throw new Error(`buildPayloadNone 与其他 payload 钩子互斥:「${d.name}」`);
8705
+ }
8706
+ }
8707
+ if (d.kind === "cloud" && d.input.kind === "none") {
8708
+ const paths = (d.preprocess ? 1 : 0) + (d.buildPayloadNone ? 1 : 0);
8709
+ if (paths !== 1) {
8710
+ throw new Error(`none 工具须声明 preprocess 或 buildPayloadNone 恰一:「${d.name}」`);
8711
+ }
8712
+ }
8638
8713
  }
8639
8714
  }
8640
8715
 
@@ -8685,7 +8760,7 @@ function validateToolInput(descriptor, inputAbs) {
8685
8760
  return;
8686
8761
  if (!inputAbs)
8687
8762
  throw new Error(`${descriptor.name} 需要输入${spec.kind === "directory" ? "目录" : "文件"}`);
8688
- if (!existsSync14(inputAbs))
8763
+ if (!existsSync15(inputAbs))
8689
8764
  throw new Error(`输入不存在:${inputAbs}`);
8690
8765
  if (spec.kind === "directory")
8691
8766
  return;
@@ -8702,7 +8777,7 @@ function validateToolInputs(descriptor, inputAbsList) {
8702
8777
  throw new Error(`${descriptor.name} 需要至少一个输入文件(可传多个,顺序即拼装顺序)`);
8703
8778
  const exts = descriptor.input.exts ?? defaultExtsFor(descriptor.input.kind);
8704
8779
  for (const p of inputAbsList) {
8705
- if (!existsSync14(p))
8780
+ if (!existsSync15(p))
8706
8781
  throw new Error(`输入不存在:${p}`);
8707
8782
  if (exts && exts.length) {
8708
8783
  const e = extname4(p).toLowerCase();
@@ -8849,11 +8924,13 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
8849
8924
  const outDir = resolveOutDir(descriptor, inputAbs, opts.out);
8850
8925
  if (isMulti)
8851
8926
  descriptor.buildPayloadMulti(inputList.map(() => "__dry_run__"), ctx);
8927
+ const isNoneDirect = descriptor.input.kind === "none" && !!descriptor.buildPayloadNone;
8852
8928
  let uploadPath = inputAbs;
8853
8929
  if (!isMulti && descriptor.preprocess)
8854
8930
  uploadPath = await descriptor.preprocess(ctx);
8855
- if (!uploadPath)
8856
- throw new Error(`${descriptor.name} 缺上传物(input=none 的 cloud 型工具需 preprocess 产上传物)`);
8931
+ if (!uploadPath && !isNoneDirect) {
8932
+ throw new Error(`${descriptor.name} 缺上传物(input=none 的 cloud 型工具需 preprocess 产上传物或声明 buildPayloadNone)`);
8933
+ }
8857
8934
  let billingHint;
8858
8935
  try {
8859
8936
  billingHint = (await (deps.resolvePricing ?? resolveToolPricing)(descriptor.priceKey, descriptor.pricingContext)).billingHint;
@@ -8865,7 +8942,11 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
8865
8942
  let taskId;
8866
8943
  let fileId;
8867
8944
  let fileIds;
8868
- if (isMulti) {
8945
+ if (isNoneDirect) {
8946
+ const p = descriptor.buildPayloadNone(ctx);
8947
+ mergeParams(p, extraParams);
8948
+ taskId = await deps.submitTask(deps.cfg, taskType, p);
8949
+ } else if (isMulti) {
8869
8950
  const buildMulti = (fids) => {
8870
8951
  const p = descriptor.buildPayloadMulti(fids, ctx);
8871
8952
  mergeParams(p, extraParams);
@@ -9006,7 +9087,7 @@ async function runLong2Short(input, opts) {
9006
9087
  routeLogsToStderr();
9007
9088
  const cfg = loadConfig();
9008
9089
  const inputAbs = resolve6(input);
9009
- if (!existsSync15(inputAbs))
9090
+ if (!existsSync16(inputAbs))
9010
9091
  throw new Error(`毛片不存在:${inputAbs}`);
9011
9092
  const projName = basename8(inputAbs, extname5(inputAbs));
9012
9093
  const formats = opts.formats.split(",").map((s) => s.trim()).filter(Boolean);
@@ -9108,7 +9189,7 @@ async function runLong2Short(input, opts) {
9108
9189
  if (draftRoot) {
9109
9190
  for (const [i, cr] of clipResults.entries()) {
9110
9191
  const src = join17(cr.dir, "jianying");
9111
- if (!existsSync15(src))
9192
+ if (!existsSync16(src))
9112
9193
  continue;
9113
9194
  const dest = `${draftTarget}_clip${i}`;
9114
9195
  try {
@@ -9245,13 +9326,13 @@ function registerUpgrade(program2) {
9245
9326
 
9246
9327
  // src/commands/render.ts
9247
9328
  import { resolve as resolve8, dirname as dirname6, join as join19, basename as basename9, extname as extname6 } from "node:path";
9248
- import { existsSync as existsSync16 } from "node:fs";
9329
+ import { existsSync as existsSync17 } from "node:fs";
9249
9330
  function registerRender(program2) {
9250
9331
  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) => {
9251
9332
  if (opts.json)
9252
9333
  routeLogsToStderr();
9253
9334
  const gtrkAbs = resolve8(gtrk);
9254
- if (!existsSync16(gtrkAbs))
9335
+ if (!existsSync17(gtrkAbs))
9255
9336
  throw new Error(`gtrk 工程不存在:${gtrkAbs}`);
9256
9337
  const outMp4 = resolve8(opts.out ?? join19(dirname6(gtrkAbs), `${basename9(gtrkAbs, extname6(gtrkAbs))}.mp4`));
9257
9338
  log.step(`▶ 本地渲染:${basename9(gtrkAbs)} → ${basename9(outMp4)}`);
@@ -9278,16 +9359,16 @@ function registerRender(program2) {
9278
9359
 
9279
9360
  // src/commands/split.ts
9280
9361
  import { resolve as resolve9, join as join21, dirname as dirname8, basename as basename11 } from "node:path";
9281
- import { existsSync as existsSync17 } from "node:fs";
9362
+ import { existsSync as existsSync18 } from "node:fs";
9282
9363
  import { readFile as readFile5, writeFile as writeFile8, mkdir as mkdir7 } from "node:fs/promises";
9283
9364
  import { createHash } from "node:crypto";
9284
9365
 
9285
9366
  // src/lib/gtrk-writeback.ts
9286
- import { readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "node:fs";
9367
+ import { readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "node:fs";
9287
9368
  import { dirname as dirname7, join as join20, basename as basename10 } from "node:path";
9288
9369
  import { randomBytes as randomBytes2 } from "node:crypto";
9289
9370
  function readGtrk(path) {
9290
- const raw = readFileSync4(path, "utf8");
9371
+ const raw = readFileSync5(path, "utf8");
9291
9372
  let gtrk;
9292
9373
  try {
9293
9374
  gtrk = JSON.parse(raw);
@@ -9347,7 +9428,7 @@ function registerSplit(program2) {
9347
9428
  });
9348
9429
  }
9349
9430
  function firstExisting(cands) {
9350
- return cands.find((p) => existsSync17(p));
9431
+ return cands.find((p) => existsSync18(p));
9351
9432
  }
9352
9433
  function resolvePaths(opts) {
9353
9434
  const project = opts.project ? resolve9(opts.project) : undefined;
@@ -9359,7 +9440,7 @@ function resolvePaths(opts) {
9359
9440
  } else {
9360
9441
  throw new Error("需 --project <目录> 或显式 --gtrk <path>");
9361
9442
  }
9362
- if (!existsSync17(gtrkPath))
9443
+ if (!existsSync18(gtrkPath))
9363
9444
  throw new Error(`找不到工程文件:${gtrkPath}`);
9364
9445
  let transcriptPath;
9365
9446
  if (opts.transcript)
@@ -9393,7 +9474,7 @@ async function runSplit(splitdoc, opts) {
9393
9474
  return splitdoc ? runLand(resolve9(splitdoc), baseDir, gtrkPath, transcriptPath, opts) : runView(baseDir, gtrkPath, transcriptPath, opts);
9394
9475
  }
9395
9476
  async function runView(baseDir, gtrkPath, transcriptPath, opts) {
9396
- if (!transcriptPath || !existsSync17(transcriptPath))
9477
+ if (!transcriptPath || !existsSync18(transcriptPath))
9397
9478
  throw new Error(TRANSCRIPT_MISSING);
9398
9479
  log.step("▶ 导出投影视图(transcript × 当刻 .gtrk)…");
9399
9480
  const transcript = await loadTranscript(transcriptPath);
@@ -9419,9 +9500,9 @@ async function runView(baseDir, gtrkPath, transcriptPath, opts) {
9419
9500
  return result;
9420
9501
  }
9421
9502
  async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
9422
- if (!existsSync17(splitdocPath))
9503
+ if (!existsSync18(splitdocPath))
9423
9504
  throw new Error(`找不到拆分稿:${splitdocPath}`);
9424
- if (!transcriptPath || !existsSync17(transcriptPath))
9505
+ if (!transcriptPath || !existsSync18(transcriptPath))
9425
9506
  throw new Error(TRANSCRIPT_MISSING);
9426
9507
  log.step("▶ 校验拆分稿并落地…");
9427
9508
  const doc = JSON.parse(await readFile5(splitdocPath, "utf8"));
@@ -9497,7 +9578,7 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
9497
9578
 
9498
9579
  // src/commands/matrix.ts
9499
9580
  import { resolve as resolve11, join as join22, dirname as dirname9, basename as basename12 } from "node:path";
9500
- import { existsSync as existsSync19 } from "node:fs";
9581
+ import { existsSync as existsSync20 } from "node:fs";
9501
9582
  import { readFile as readFile6, writeFile as writeFile9, mkdir as mkdir8, rename } from "node:fs/promises";
9502
9583
 
9503
9584
  // src/lib/solid-png.ts
@@ -10234,7 +10315,7 @@ function layBrollTracks(opts) {
10234
10315
  }
10235
10316
 
10236
10317
  // src/lib/material-integrity.ts
10237
- import { existsSync as existsSync18 } from "node:fs";
10318
+ import { existsSync as existsSync19 } from "node:fs";
10238
10319
  import { resolve as resolve10 } from "node:path";
10239
10320
  var INTEGRITY_LIST_CAP = 10;
10240
10321
  var r35 = (n) => Math.round(n * 1000) / 1000;
@@ -10295,7 +10376,7 @@ function collectMaterialRefs(gtrk) {
10295
10376
  return out;
10296
10377
  }
10297
10378
  function checkMaterialIntegrity(opts) {
10298
- const exists = opts.exists ?? ((p) => existsSync18(p));
10379
+ const exists = opts.exists ?? ((p) => existsSync19(p));
10299
10380
  const materials = Array.isArray(opts.gtrk.materials) ? opts.gtrk.materials : [];
10300
10381
  const refs = collectMaterialRefs(opts.gtrk);
10301
10382
  const counts = { relative: 0, absolute: 0, remote: 0, noPath: 0 };
@@ -10645,7 +10726,7 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
10645
10726
  } else {
10646
10727
  throw new Error('需 --project <目录> 或显式 --dispatch <path>(ad-hoc 检索用:gtrk matrix search "<query>")');
10647
10728
  }
10648
- if (!existsSync19(dispatchPath))
10729
+ if (!existsSync20(dispatchPath))
10649
10730
  throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split <拆分稿> 落地派单)`);
10650
10731
  const dispatch = JSON.parse(await readFile6(dispatchPath, "utf8"));
10651
10732
  const rawQueue = Array.isArray(dispatch.film_broll) ? dispatch.film_broll : [];
@@ -10761,7 +10842,7 @@ function parseScoreFloor(raw) {
10761
10842
  }
10762
10843
  function locateGtrk(baseDir) {
10763
10844
  const cands = [join22(baseDir, "gtrk", "project.gtrk"), join22(baseDir, "project.gtrk")];
10764
- return cands.find((p) => existsSync19(p));
10845
+ return cands.find((p) => existsSync20(p));
10765
10846
  }
10766
10847
  async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRelay, reproj) {
10767
10848
  const gtrkPath = locateGtrk(baseDir);
@@ -10799,7 +10880,7 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
10799
10880
  continue;
10800
10881
  const rel = `${BROLL_PREVIEW_DIR}/${clipId}.mp4`;
10801
10882
  const abs = join22(gtrkDir, ...rel.split("/"));
10802
- if (existsSync19(abs)) {
10883
+ if (existsSync20(abs)) {
10803
10884
  const prev = prevSource.get(clipId);
10804
10885
  if (prev !== "raw") {
10805
10886
  downloads.set(clipId, { rel, source: prev ?? "preview" });
@@ -10863,7 +10944,7 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
10863
10944
  const rel = solidRelPath(spec);
10864
10945
  const abs = join22(gtrkDir, ...rel.split("/"));
10865
10946
  try {
10866
- if (!existsSync19(abs)) {
10947
+ if (!existsSync20(abs)) {
10867
10948
  await mkdir8(dirname9(abs), { recursive: true });
10868
10949
  const tmp = `${abs}.tmp-${process.pid}`;
10869
10950
  await writeFile9(tmp, encodeSolidPng(spec));
@@ -10980,7 +11061,7 @@ function slugify3(name) {
10980
11061
 
10981
11062
  // src/commands/mg.ts
10982
11063
  import { resolve as resolve12, join as join23, dirname as dirname10, basename as basename13 } from "node:path";
10983
- import { existsSync as existsSync20 } from "node:fs";
11064
+ import { existsSync as existsSync21 } from "node:fs";
10984
11065
  import { readFile as readFile7, mkdir as mkdir9, copyFile } from "node:fs/promises";
10985
11066
 
10986
11067
  // src/lib/mg-lint.ts
@@ -12506,18 +12587,18 @@ function resolveDispatch(opts) {
12506
12587
  throw new Error("需 --project <目录> 或显式 --dispatch <path>");
12507
12588
  }
12508
12589
  function locateGtrk2(baseDir) {
12509
- return [join23(baseDir, "gtrk", "project.gtrk"), join23(baseDir, "project.gtrk")].find((p) => existsSync20(p));
12590
+ return [join23(baseDir, "gtrk", "project.gtrk"), join23(baseDir, "project.gtrk")].find((p) => existsSync21(p));
12510
12591
  }
12511
12592
  function locateSrcHtml(baseDir, compositionId) {
12512
12593
  for (const d of MG_SRC_DIRS) {
12513
12594
  const p = join23(baseDir, d, `${compositionId}.html`);
12514
- if (existsSync20(p))
12595
+ if (existsSync21(p))
12515
12596
  return p;
12516
12597
  }
12517
12598
  return;
12518
12599
  }
12519
12600
  async function readMgQueue(dispatchPath) {
12520
- if (!existsSync20(dispatchPath))
12601
+ if (!existsSync21(dispatchPath))
12521
12602
  throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split 落地派单)`);
12522
12603
  const dispatch = JSON.parse(await readFile7(dispatchPath, "utf8"));
12523
12604
  const queue = dispatch.mg ?? dispatch.rrv_mg;
@@ -12751,7 +12832,7 @@ async function runLint(args, opts) {
12751
12832
  let dispatchIds;
12752
12833
  let slotDuration;
12753
12834
  let compositionId;
12754
- if (opts.dispatch && existsSync20(resolve12(opts.dispatch))) {
12835
+ if (opts.dispatch && existsSync21(resolve12(opts.dispatch))) {
12755
12836
  const queue = await readMgQueue(resolve12(opts.dispatch));
12756
12837
  dispatchIds = queue.map((q) => q.composition_id);
12757
12838
  const byName = queue.find((q) => q.composition_id === nameId);
@@ -12818,7 +12899,7 @@ function done(opts, result) {
12818
12899
 
12819
12900
  // src/lib/mad/mad.ts
12820
12901
  import { mkdir as mkdir12, writeFile as writeFile11 } from "node:fs/promises";
12821
- import { existsSync as existsSync23, statSync as statSync3 } from "node:fs";
12902
+ import { existsSync as existsSync24, statSync as statSync3 } from "node:fs";
12822
12903
  import { resolve as resolve13, join as join27 } from "node:path";
12823
12904
 
12824
12905
  // src/lib/convert/types.ts
@@ -13790,7 +13871,7 @@ function beatQuantized(natLens, analysis) {
13790
13871
  // src/lib/mad/data.ts
13791
13872
  import { createHash as createHash2 } from "node:crypto";
13792
13873
  import { mkdir as mkdir10, readFile as readFile8, rename as rename2, rm, writeFile as writeFile10, readdir } from "node:fs/promises";
13793
- import { existsSync as existsSync21 } from "node:fs";
13874
+ import { existsSync as existsSync22 } from "node:fs";
13794
13875
  import { join as join25 } from "node:path";
13795
13876
  function madCacheDir() {
13796
13877
  return homeFile("mad-cache");
@@ -13820,7 +13901,7 @@ async function atomicWrite(dest, data) {
13820
13901
  await rename2(tmp, dest);
13821
13902
  }
13822
13903
  async function verifyFile(path, sha256) {
13823
- if (!existsSync21(path))
13904
+ if (!existsSync22(path))
13824
13905
  return false;
13825
13906
  try {
13826
13907
  const buf = await readFile8(path);
@@ -13872,7 +13953,7 @@ async function ensureMadData(opts, deps) {
13872
13953
  warn(`manifest 拉取失败(${e instanceof Error ? e.message : String(e)}),回退本地缓存`);
13873
13954
  }
13874
13955
  if (!manifest) {
13875
- if (!existsSync21(snapshotPath)) {
13956
+ if (!existsSync22(snapshotPath)) {
13876
13957
  throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
13877
13958
  }
13878
13959
  try {
@@ -13894,7 +13975,7 @@ async function ensureMadData(opts, deps) {
13894
13975
  const needDownload = !!opts.refresh || !cacheValid;
13895
13976
  if (needDownload) {
13896
13977
  if (!online) {
13897
- if (existsSync21(poolPath)) {
13978
+ if (existsSync22(poolPath)) {
13898
13979
  throw new Error("本地技法数据缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
13899
13980
  }
13900
13981
  throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
@@ -13930,7 +14011,7 @@ async function ensureMadData(opts, deps) {
13930
14011
  // src/lib/mad/pool.ts
13931
14012
  import { gunzipSync } from "node:zlib";
13932
14013
  import { mkdir as mkdir11, readFile as readFile9 } from "node:fs/promises";
13933
- import { existsSync as existsSync22 } from "node:fs";
14014
+ import { existsSync as existsSync23 } from "node:fs";
13934
14015
  import { join as join26 } from "node:path";
13935
14016
  function shardPath(verDir, shard) {
13936
14017
  return join26(verDir, "ir", `${shard}.json.gz`);
@@ -13950,7 +14031,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
13950
14031
  if (cached)
13951
14032
  return cached;
13952
14033
  const path = shardPath(verDir, shard);
13953
- if (existsSync22(path)) {
14034
+ if (existsSync23(path)) {
13954
14035
  try {
13955
14036
  const s2 = decodeShard(await readFile9(path));
13956
14037
  memo.set(shard, s2);
@@ -13982,7 +14063,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
13982
14063
  return ir;
13983
14064
  },
13984
14065
  shardCached(shard) {
13985
- return memo.has(shard) || existsSync22(shardPath(verDir, shard));
14066
+ return memo.has(shard) || existsSync23(shardPath(verDir, shard));
13986
14067
  }
13987
14068
  };
13988
14069
  }
@@ -14066,7 +14147,7 @@ async function runMad(inputArg, opts, deps = {}) {
14066
14147
  if (!inputArg)
14067
14148
  throw new Error("用法:gtrk tool mad <素材文件夹> [--bgm 歌.mp3]");
14068
14149
  const dirAbs = resolve13(inputArg);
14069
- if (!existsSync23(dirAbs) || !statSync3(dirAbs).isDirectory()) {
14150
+ if (!existsSync24(dirAbs) || !statSync3(dirAbs).isDirectory()) {
14070
14151
  throw new Error(`素材文件夹不存在或不是目录:${dirAbs}`);
14071
14152
  }
14072
14153
  const { videos, orientation, skipped } = scanFolder(dirAbs, {
@@ -14327,7 +14408,7 @@ async function runMadInTool(inputArg, opts) {
14327
14408
  }
14328
14409
 
14329
14410
  // src/commands/transcript.ts
14330
- import { existsSync as existsSync24 } from "node:fs";
14411
+ import { existsSync as existsSync25 } from "node:fs";
14331
14412
  import { mkdir as mkdir13, rename as rename3, rm as rm2, stat as stat6, writeFile as writeFile12 } from "node:fs/promises";
14332
14413
  import { basename as basename14, dirname as dirname11, extname as extname8, join as join28, resolve as resolve14 } from "node:path";
14333
14414
 
@@ -14466,7 +14547,7 @@ async function validateTranscriptInput(input) {
14466
14547
  throw new Error("视频转文字稿仅支持本地视频文件,不支持 URL、平台视频地址或远端下载");
14467
14548
  }
14468
14549
  const inputAbs = resolve14(input);
14469
- if (!existsSync24(inputAbs))
14550
+ if (!existsSync25(inputAbs))
14470
14551
  throw new Error(`本地视频不存在:${inputAbs}`);
14471
14552
  const info = await stat6(inputAbs);
14472
14553
  if (!info.isFile())
@@ -14564,7 +14645,7 @@ function registerTranscript(program2) {
14564
14645
  // src/commands/music-visualizer.ts
14565
14646
  import { resolve as resolve15, join as join29, dirname as dirname12, basename as basename15, extname as extname9 } from "node:path";
14566
14647
  import { mkdir as mkdir14, writeFile as writeFile13 } from "node:fs/promises";
14567
- import { existsSync as existsSync25 } from "node:fs";
14648
+ import { existsSync as existsSync26 } from "node:fs";
14568
14649
  var TASK_TYPE5 = "music_visualizer";
14569
14650
  var PRICE_KEY2 = "music_visualizer";
14570
14651
  var HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
@@ -14612,7 +14693,7 @@ function timestamp5() {
14612
14693
  return `${p(d.getFullYear() % 100)}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
14613
14694
  }
14614
14695
  function assertExt(pathAbs, exts, label) {
14615
- if (!existsSync25(pathAbs))
14696
+ if (!existsSync26(pathAbs))
14616
14697
  throw new Error(`${label}不存在:${pathAbs}`);
14617
14698
  const e = extname9(pathAbs).toLowerCase();
14618
14699
  if (exts.length && !exts.includes(e)) {
@@ -14780,7 +14861,7 @@ try {
14780
14861
  process.loadEnvFile?.();
14781
14862
  } catch {}
14782
14863
  migrateLegacyHome();
14783
- var { version } = JSON.parse(readFileSync5(join30(packageRoot(), "package.json"), "utf8"));
14864
+ var { version } = JSON.parse(readFileSync6(join30(packageRoot(), "package.json"), "utf8"));
14784
14865
  var program2 = new Command;
14785
14866
  program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
14786
14867
  registerInstall(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitruck/cli",
3
- "version": "0.2.19",
3
+ "version": "0.2.20",
4
4
  "description": "同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通。",
5
5
  "license": "MIT",
6
6
  "author": "Gitruck (同合云)",
@@ -46,6 +46,7 @@ description: gtrk 单点工具与媒体转换能力的调用向导,覆盖 `gtr
46
46
  | `image_to_live` | 「图片变 LivePhoto / 静图微动 / 让照片动起来」 | 单张图片 | 微动视频 `.mp4`(产物是视频) | 运行前实时查询 | 未开放(上游生成能力暂未开放,恢复后重新上架) |
47
47
  | `image_classic_template` | 「做封面 / 智能拼图 / 标题配图出封面图」 | **多张图片**(可传多个路径,文件顺序即拼装顺序);**`--main-title <标题>` 必填**;可选 `--sub-title`、`--template-mode`、`--aspect`、`--quality`、`--output-pic-count`(服务端钳制 ≤20)、`--output-text-count`(≤20)、`--title-layout` | 封面/拼图成品(text/pic/render 三组、各组可空可多张) | 运行前实时查询 | 已上线 |
48
48
  | `image_vertical_stitch` | 「拼长图 / 多图竖拼 / 截图接成一张长图」 | **多张图片**(可传多个路径,顺序=自上而下拼接顺序) | 一张垂直拼接长图 | 运行前实时查询 | 已上线 |
49
+ | `audio_tts_clone` | 「配音 / 文字转语音 / TTS / 用 xx 音色念这段 / 把这段稿子合成音频」 | **无文件**:`--text <短文本>` 或 `--text-file <txt>` 二选一(≤2000 字,服务端为真相)+ **`--speaker <音色代号>` 必填**(可用音色见官网文档,传错时报错会列出全部可用项);可选 `--text-lang`/`--output-format wav\|mp3`/`--speed`/`--split-method`/`--batch-size`(speed/切分法缺省跟随该音色调好的参数)。**计费按文本字数折算分钟(约 200 字/分钟)** | 配音音频(wav/mp3) | 运行前实时查询 | 已上线 |
49
50
  | `mad` | 「一键剪 MAD / 素材文件夹自动出卡点成片 / 自动选技法 / 给这堆素材出个 AE 工程」 | 一个素材文件夹(3~10 条视频)+ 可选 `--bgm 歌.mp3` | AE 母合成工程 `.jsx`(仅支持 AE) | 仅 `--bgm` 触发实时查价 | 已上线 |
50
51
 
51
52
  > 清单随 CLI 版本增补。**动手前先跑 `gtrk tool list --json` 拿最新清单**,别凭记忆——新工具、状态变化都在那里。