@gitruck/cli 0.2.22 → 0.2.23
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 +526 -524
- package/dist/index.js +438 -120
- package/package.json +2 -2
- package/skills/gtrk-long2short/SKILL.md +68 -63
- package/skills/gtrk-tools/SKILL.md +22 -1
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
|
|
4200
|
+
import { join as join31 } from "node:path";
|
|
4201
4201
|
|
|
4202
4202
|
// src/lib/paths.ts
|
|
4203
4203
|
import { dirname, join } from "node:path";
|
|
@@ -6833,6 +6833,68 @@ function assertDurationConsistent(originalDuration, artifactAbs, ffmpegPath, tol
|
|
|
6833
6833
|
throw new Error(`抽出物时长(${got.toFixed(2)}s)与原片(${originalDuration.toFixed(2)}s)不一致(容差 ${tolSec}s),` + `疑似抽取异常,已中止上传`);
|
|
6834
6834
|
}
|
|
6835
6835
|
}
|
|
6836
|
+
function assFontNames(assText) {
|
|
6837
|
+
const names = new Set;
|
|
6838
|
+
for (const line2 of assText.split(/\r?\n/)) {
|
|
6839
|
+
if (!line2.startsWith("Style:"))
|
|
6840
|
+
continue;
|
|
6841
|
+
const font = line2.slice("Style:".length).split(",")[1]?.trim();
|
|
6842
|
+
if (font)
|
|
6843
|
+
names.add(font);
|
|
6844
|
+
}
|
|
6845
|
+
return [...names];
|
|
6846
|
+
}
|
|
6847
|
+
async function probeFontAvailable(font, ffmpegPath) {
|
|
6848
|
+
const { ffmpeg } = requireFfmpeg(ffmpegPath);
|
|
6849
|
+
try {
|
|
6850
|
+
await runFfmpeg(ffmpeg, [
|
|
6851
|
+
"-v",
|
|
6852
|
+
"error",
|
|
6853
|
+
"-f",
|
|
6854
|
+
"lavfi",
|
|
6855
|
+
"-i",
|
|
6856
|
+
"color=c=black:s=64x64:d=0.04",
|
|
6857
|
+
"-vf",
|
|
6858
|
+
`drawtext=font='${font}':text=A`,
|
|
6859
|
+
"-frames:v",
|
|
6860
|
+
"1",
|
|
6861
|
+
"-f",
|
|
6862
|
+
"null",
|
|
6863
|
+
"-"
|
|
6864
|
+
]);
|
|
6865
|
+
return true;
|
|
6866
|
+
} catch (e) {
|
|
6867
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
6868
|
+
if (/valid font|font.*not found|Fontconfig error/i.test(msg))
|
|
6869
|
+
return false;
|
|
6870
|
+
return null;
|
|
6871
|
+
}
|
|
6872
|
+
}
|
|
6873
|
+
async function burnSubtitle(videoAbs, assAbs, outAbs, opts = {}) {
|
|
6874
|
+
const { ffmpeg } = requireFfmpeg(opts.ffmpegPath);
|
|
6875
|
+
const filterPath = assAbs.replace(/\\/g, "/").replace(/:/g, "\\:");
|
|
6876
|
+
await runFfmpeg(ffmpeg, [
|
|
6877
|
+
"-y",
|
|
6878
|
+
"-v",
|
|
6879
|
+
"error",
|
|
6880
|
+
"-i",
|
|
6881
|
+
videoAbs,
|
|
6882
|
+
"-vf",
|
|
6883
|
+
`ass='${filterPath}'`,
|
|
6884
|
+
"-c:v",
|
|
6885
|
+
opts.codec ?? "libx264",
|
|
6886
|
+
"-crf",
|
|
6887
|
+
String(opts.crf ?? 18),
|
|
6888
|
+
"-preset",
|
|
6889
|
+
"medium",
|
|
6890
|
+
"-c:a",
|
|
6891
|
+
"copy",
|
|
6892
|
+
"-movflags",
|
|
6893
|
+
"+faststart",
|
|
6894
|
+
outAbs
|
|
6895
|
+
]);
|
|
6896
|
+
return outAbs;
|
|
6897
|
+
}
|
|
6836
6898
|
|
|
6837
6899
|
// src/lib/materialize.ts
|
|
6838
6900
|
import { join as join14, basename as basename5 } from "node:path";
|
|
@@ -7314,8 +7376,8 @@ async function runOralCut(input, opts) {
|
|
|
7314
7376
|
}
|
|
7315
7377
|
|
|
7316
7378
|
// src/commands/long2short.ts
|
|
7317
|
-
import { resolve as resolve6, join as
|
|
7318
|
-
import { mkdir as mkdir7, writeFile as
|
|
7379
|
+
import { resolve as resolve6, join as join18, dirname as dirname6, basename as basename8, extname as extname5 } from "node:path";
|
|
7380
|
+
import { mkdir as mkdir7, writeFile as writeFile8 } from "node:fs/promises";
|
|
7319
7381
|
import { existsSync as existsSync16 } from "node:fs";
|
|
7320
7382
|
|
|
7321
7383
|
// src/lib/clip-brief.ts
|
|
@@ -7445,10 +7507,110 @@ function renderClipsOverview(clips, ctx) {
|
|
|
7445
7507
|
`)}
|
|
7446
7508
|
`;
|
|
7447
7509
|
}
|
|
7510
|
+
var POLISH_LABEL = {
|
|
7511
|
+
split_screen: "智能分屏",
|
|
7512
|
+
camera: "克制运镜",
|
|
7513
|
+
speed: "整体调速",
|
|
7514
|
+
seam: "接缝过渡",
|
|
7515
|
+
subtitle: "智能字幕",
|
|
7516
|
+
subtitle_remap: "字幕时轴重映射",
|
|
7517
|
+
purify_source: "去除原字幕",
|
|
7518
|
+
unknown: "未具名润色项"
|
|
7519
|
+
};
|
|
7520
|
+
function renderProReport(clips, report, ctx) {
|
|
7521
|
+
const name = ctx.source.split(/[\/]/).pop() || ctx.source;
|
|
7522
|
+
const out = [`# ${name} · 长剪短精剪报告`, ""];
|
|
7523
|
+
out.push(`- 源片:\`${ctx.source}\``);
|
|
7524
|
+
out.push(`- 成片:${clips.length} 条`);
|
|
7525
|
+
const jc = report?.jump_cut;
|
|
7526
|
+
if (typeof jc === "boolean")
|
|
7527
|
+
out.push(`- 跳剪:${jc ? "开" : "关"}`);
|
|
7528
|
+
if (ctx.taskId)
|
|
7529
|
+
out.push(`- task_id:\`${ctx.taskId}\``);
|
|
7530
|
+
out.push("");
|
|
7531
|
+
out.push("| # | 标题 | 时长 | 评分 | 简介 |", "| --- | --- | --- | --- | --- |");
|
|
7532
|
+
for (const [i, clip] of clips.entries()) {
|
|
7533
|
+
const dur = clipDurationMs(clip);
|
|
7534
|
+
const score = num(clip.score);
|
|
7535
|
+
out.push(`| clip${i} | ${cell(str(clip.title) ?? "—")} | ${dur != null ? fmtTime(dur) : "—"} | ${score ?? "—"} | ${cell(str(clip.summary) ?? "—")} |`);
|
|
7536
|
+
}
|
|
7537
|
+
out.push("");
|
|
7538
|
+
const degraded = report?.degraded_items;
|
|
7539
|
+
const degRows = [];
|
|
7540
|
+
if (degraded && typeof degraded === "object" && !Array.isArray(degraded)) {
|
|
7541
|
+
for (const [k, v] of Object.entries(degraded)) {
|
|
7542
|
+
const names = list(v).map((x) => POLISH_LABEL[x] ?? x);
|
|
7543
|
+
if (names.length)
|
|
7544
|
+
degRows.push(`- **${k}**:${names.join("、")}`);
|
|
7545
|
+
}
|
|
7546
|
+
}
|
|
7547
|
+
if (degRows.length) {
|
|
7548
|
+
out.push("## ⚠️ 润色降级", "");
|
|
7549
|
+
out.push("以下片子照常出片,但列出的润色项**没有做成**——效果与预期不同,别当成完整成片直接发布。", "");
|
|
7550
|
+
out.push(...degRows, "");
|
|
7551
|
+
}
|
|
7552
|
+
for (const [i, clip] of clips.entries()) {
|
|
7553
|
+
const title = str(clip.title);
|
|
7554
|
+
out.push("---", "", `## clip${i}${title ? `「${title}」` : ""}`, "");
|
|
7555
|
+
const bits = [];
|
|
7556
|
+
const dur = clipDurationMs(clip);
|
|
7557
|
+
if (dur != null)
|
|
7558
|
+
bits.push(`时长 ${fmtTime(dur)}`);
|
|
7559
|
+
const segCount = clipSegmentCount(clip);
|
|
7560
|
+
if (segCount != null)
|
|
7561
|
+
bits.push(`${segCount} 个保留片段`);
|
|
7562
|
+
const span = sourceSpan(clip);
|
|
7563
|
+
if (span)
|
|
7564
|
+
bits.push(`源片 ${fmtTime(span[0])}–${fmtTime(span[1])}`);
|
|
7565
|
+
const f = ctx.clipFiles?.[i];
|
|
7566
|
+
if (f)
|
|
7567
|
+
bits.push(`成片 ${baseName(f)}`);
|
|
7568
|
+
if (bits.length)
|
|
7569
|
+
out.push(`- ${bits.join(" · ")}`, "");
|
|
7570
|
+
const score = num(clip.score);
|
|
7571
|
+
const reason = str(clip.score_reason);
|
|
7572
|
+
if (score != null || reason) {
|
|
7573
|
+
out.push("### 入选理由", "");
|
|
7574
|
+
out.push([score != null ? `**评分 ${score}**` : null, reason].filter(Boolean).join(" — "), "");
|
|
7575
|
+
}
|
|
7576
|
+
const summary = str(clip.summary);
|
|
7577
|
+
if (summary)
|
|
7578
|
+
out.push("### 简介", "", summary, "");
|
|
7579
|
+
const note = str(clip.jumpcut_note);
|
|
7580
|
+
if (note)
|
|
7581
|
+
out.push("### 跳剪", "", jumpcutText(note), "");
|
|
7582
|
+
const cats = [
|
|
7583
|
+
["主题", list(clip.themes)],
|
|
7584
|
+
["标签", list(clip.tags)],
|
|
7585
|
+
["类型", list(clip.genres)],
|
|
7586
|
+
["调性", list(clip.moods)]
|
|
7587
|
+
];
|
|
7588
|
+
const shown = cats.filter(([, v]) => v.length);
|
|
7589
|
+
if (shown.length) {
|
|
7590
|
+
out.push("### 分类与调性", "");
|
|
7591
|
+
for (const [k, v] of shown)
|
|
7592
|
+
out.push(`- ${k}:${v.join("、")}`);
|
|
7593
|
+
out.push("");
|
|
7594
|
+
}
|
|
7595
|
+
const hl = Array.isArray(clip.highlight_words) ? clip.highlight_words : [];
|
|
7596
|
+
const hlRows = hl.map((h) => ({ text: str(h?.text), at: num(h?.begin_time) })).filter((h) => h.text);
|
|
7597
|
+
if (hlRows.length) {
|
|
7598
|
+
out.push("### 高光词(源片时码)", "");
|
|
7599
|
+
for (const h of hlRows)
|
|
7600
|
+
out.push(`- ${h.at != null ? `${fmtTime(h.at)} ` : ""}「${h.text}」`);
|
|
7601
|
+
out.push("");
|
|
7602
|
+
}
|
|
7603
|
+
}
|
|
7604
|
+
return `${out.join(`
|
|
7605
|
+
`).replace(/\n{3,}/g, `
|
|
7606
|
+
|
|
7607
|
+
`).trimEnd()}
|
|
7608
|
+
`;
|
|
7609
|
+
}
|
|
7448
7610
|
|
|
7449
7611
|
// src/lib/tool-runner.ts
|
|
7450
|
-
import { resolve as resolve5, join as
|
|
7451
|
-
import { mkdir as mkdir6, writeFile as
|
|
7612
|
+
import { resolve as resolve5, join as join17, dirname as dirname5, basename as basename7, extname as extname4 } from "node:path";
|
|
7613
|
+
import { mkdir as mkdir6, writeFile as writeFile7, stat as stat5 } from "node:fs/promises";
|
|
7452
7614
|
import { createWriteStream, existsSync as existsSync15 } from "node:fs";
|
|
7453
7615
|
import { Readable as Readable2 } from "node:stream";
|
|
7454
7616
|
import { pipeline } from "node:stream/promises";
|
|
@@ -7534,11 +7696,13 @@ async function resolveToolPricing(priceKey, pricingContext, fetchFn = fetch) {
|
|
|
7534
7696
|
|
|
7535
7697
|
// src/lib/tool-descriptors.ts
|
|
7536
7698
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
7537
|
-
import {
|
|
7699
|
+
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";
|
|
7538
7701
|
var MULTI_INPUT_KINDS = new Set(["images", "videos"]);
|
|
7539
7702
|
var IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif", ".tif", ".tiff", ".heic", ".heif", ".avif"];
|
|
7540
7703
|
var VIDEO_EXTS = [".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v", ".flv", ".wmv", ".mpg", ".mpeg", ".ts", ".m2ts"];
|
|
7541
7704
|
var AUDIO_EXTS = [".mp3", ".wav", ".m4a", ".aac", ".flac", ".ogg", ".opus", ".wma"];
|
|
7705
|
+
var isAudioFile = (p) => AUDIO_EXTS.includes(extname3(p).toLowerCase());
|
|
7542
7706
|
var PUBLIC_VIDEO_EXTS = [
|
|
7543
7707
|
".mp4",
|
|
7544
7708
|
".avi",
|
|
@@ -8482,18 +8646,37 @@ var videoAiSubtitle = {
|
|
|
8482
8646
|
{ flag: "--subtitle-type <style>", desc: `字幕样式:${AI_SUBTITLE_TYPES.join("/")}(未传则用服务端默认)` },
|
|
8483
8647
|
{ flag: "--subtitle-color <color>", desc: `字幕颜色:${AI_SUBTITLE_COLORS.join("/")}(未传则用服务端默认)` }
|
|
8484
8648
|
],
|
|
8649
|
+
async preprocess(ctx) {
|
|
8650
|
+
const inputAbs = ctx.inputAbs;
|
|
8651
|
+
if (ctx.opts.needPure === true) {
|
|
8652
|
+
ctx.warn("--need-pure 需要画面(去原字幕是画面操作),本次整片上传;只要字幕文件时去掉该 flag 即可只传音频");
|
|
8653
|
+
return inputAbs;
|
|
8654
|
+
}
|
|
8655
|
+
if (isAudioFile(inputAbs))
|
|
8656
|
+
return inputAbs;
|
|
8657
|
+
const audio = await extractAudio(inputAbs, ctx.ffmpegPath);
|
|
8658
|
+
return audio;
|
|
8659
|
+
},
|
|
8485
8660
|
buildPayload(fileId, ctx) {
|
|
8486
8661
|
const language = ctx.opts.language == null ? "" : String(ctx.opts.language).trim();
|
|
8487
8662
|
if (!language)
|
|
8488
8663
|
throw new Error("--language 必填:请指定源语种代码(具体取值见云端 API 文档 / 服务端支持列表)");
|
|
8489
8664
|
const payload = { file_id: fileId, language };
|
|
8665
|
+
const inputAbs = ctx.inputAbs;
|
|
8666
|
+
if (inputAbs && !isAudioFile(inputAbs)) {
|
|
8667
|
+
try {
|
|
8668
|
+
const geo = probeGeometry(inputAbs, ctx.ffmpegPath);
|
|
8669
|
+
if (geo.width > 0 && geo.height > 0)
|
|
8670
|
+
payload.video_size = [geo.width, geo.height];
|
|
8671
|
+
} catch {
|
|
8672
|
+
ctx.warn("探原片几何失败,本次不回传 video_size(服务端将按 1920x1080 兜底出字幕)");
|
|
8673
|
+
}
|
|
8674
|
+
}
|
|
8490
8675
|
if (ctx.opts.translateLanguage != null) {
|
|
8491
8676
|
const t = String(ctx.opts.translateLanguage).trim();
|
|
8492
8677
|
if (t)
|
|
8493
8678
|
payload.translate_language = t;
|
|
8494
8679
|
}
|
|
8495
|
-
if (ctx.opts.needRender === true)
|
|
8496
|
-
payload.need_render = true;
|
|
8497
8680
|
if (ctx.opts.needPure === true)
|
|
8498
8681
|
payload.need_pure = true;
|
|
8499
8682
|
if (ctx.opts.subtitleType != null) {
|
|
@@ -8523,10 +8706,135 @@ var videoAiSubtitle = {
|
|
|
8523
8706
|
files.push({ url: pure, filename: `${ctx.baseName}-pure${extFromUrl(pure, ".mp4")}` });
|
|
8524
8707
|
return files;
|
|
8525
8708
|
},
|
|
8709
|
+
async postprocess(ctx, landed) {
|
|
8710
|
+
if (ctx.opts.needRender !== true)
|
|
8711
|
+
return;
|
|
8712
|
+
const inputAbs = ctx.inputAbs;
|
|
8713
|
+
if (isAudioFile(inputAbs)) {
|
|
8714
|
+
throw new Error("--need-render 需要视频输入:本次输入是音频文件,无画面可烧");
|
|
8715
|
+
}
|
|
8716
|
+
const ass = landed.find((p) => p.toLowerCase().endsWith(".ass"));
|
|
8717
|
+
if (!ass)
|
|
8718
|
+
throw new Error("未拉回 .ass 字幕文件,无法本地烧录");
|
|
8719
|
+
const fonts = assFontNames(await readFile5(ass, "utf8"));
|
|
8720
|
+
const missing = [];
|
|
8721
|
+
for (const f of fonts) {
|
|
8722
|
+
if (await probeFontAvailable(f, ctx.ffmpegPath) === false)
|
|
8723
|
+
missing.push(f);
|
|
8724
|
+
}
|
|
8725
|
+
if (missing.length) {
|
|
8726
|
+
throw new Error(`本机缺字幕模板所需字体:${missing.join("、")}。` + `装上后重跑即可(字幕文件已落地,无需重新提交任务)。CLI 不自带字体、也不用替代字体顶——` + `替代字体烧出来的成片观感与模板设计不符且难以察觉。`);
|
|
8727
|
+
}
|
|
8728
|
+
const out = join16(dirname4(ass), `${ctx.baseName}-subtitled.mp4`);
|
|
8729
|
+
await burnSubtitle(inputAbs, ass, out, { ffmpegPath: ctx.ffmpegPath });
|
|
8730
|
+
return [out];
|
|
8731
|
+
},
|
|
8526
8732
|
mapResult(out) {
|
|
8527
8733
|
return { summary: typeof out.summary === "string" ? out.summary : "", asr: out.asr ?? null };
|
|
8528
8734
|
}
|
|
8529
8735
|
};
|
|
8736
|
+
var PRO_DURATION_PREFS = ["auto", "short", "medium", "long"];
|
|
8737
|
+
var videoLong2ShortPro = {
|
|
8738
|
+
name: "video_long2short_pro",
|
|
8739
|
+
title: "长剪短·精剪(直接出片)",
|
|
8740
|
+
description: "长内容按语义抽多条高光短片并一键出成品:选段+跳剪+分屏内核,叠加模糊底画布适配/克制运镜/调速保音高/智能字幕。只出成片,不产工程文件——要可编辑工程请用 gtrk long2short(粗剪)。",
|
|
8741
|
+
kind: "cloud",
|
|
8742
|
+
input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
|
|
8743
|
+
priceKey: "video_long2short_pro",
|
|
8744
|
+
outputHint: "逐条高光成片 mp4 + 切片报告 clips.md",
|
|
8745
|
+
enabled: true,
|
|
8746
|
+
taskType: "video_long2short_pro",
|
|
8747
|
+
pollTimeoutMs: 4 * 60 * 60 * 1000,
|
|
8748
|
+
options: [
|
|
8749
|
+
{ flag: "--language <code>", desc: "源语种代码(精剪必填;取值以服务端支持列表为准)" },
|
|
8750
|
+
{ flag: "--output-language <code>", desc: "选段/文案的输出语种(未传则同源语种)" },
|
|
8751
|
+
{ flag: "--main-topic <text>", desc: "主题引导(影响选段偏好)" },
|
|
8752
|
+
{ flag: "--output-size <s>", desc: "成片画幅 9:16|16:9|1:1 或自定义 WxH(未传则服务端默认)" },
|
|
8753
|
+
{ flag: "--no-jump-cut", desc: "关闭跳剪(默认开:片内去水词/冗余,只删不重排)" },
|
|
8754
|
+
{ flag: "--duration-pref <p>", desc: `成片时长偏好 ${PRO_DURATION_PREFS.join("/")}(成片条数由内容语义决定、不可指定)` },
|
|
8755
|
+
{ flag: "--max-clip-sec <n>", desc: "单条成片时长安全上限(秒;未传则服务端默认)" },
|
|
8756
|
+
{ flag: "--split-screen", desc: "开启智能分屏(多人同框段合成分屏画面)" },
|
|
8757
|
+
{ flag: "--split-orientation <o>", desc: "分屏方向 auto|lr|tb(未传则服务端默认)" },
|
|
8758
|
+
{ flag: "--speed-factor <n>", desc: "整体调速倍率(保音高;未传则服务端默认)" },
|
|
8759
|
+
{ flag: "--no-camera-move", desc: "关闭克制运镜(默认开:亚像素推/拉)" },
|
|
8760
|
+
{ flag: "--no-subtitle", desc: "关闭智能字幕(默认开:成片内烧录字幕)" },
|
|
8761
|
+
{ flag: "--subtitle-translate-language <code>", desc: "字幕译文语种(未传则单语字幕)" }
|
|
8762
|
+
],
|
|
8763
|
+
buildPayload(fileId, ctx) {
|
|
8764
|
+
const o = ctx.opts;
|
|
8765
|
+
const language = o.language == null ? "" : String(o.language).trim();
|
|
8766
|
+
if (!language)
|
|
8767
|
+
throw new Error("--language 必填:请指定源语种代码(具体取值见云端 API 文档 / 服务端支持列表)");
|
|
8768
|
+
const p = { file_id: fileId, language };
|
|
8769
|
+
if (o.outputLanguage != null && String(o.outputLanguage).trim())
|
|
8770
|
+
p.output_language = String(o.outputLanguage).trim();
|
|
8771
|
+
if (o.mainTopic != null && String(o.mainTopic).trim())
|
|
8772
|
+
p.main_topic = String(o.mainTopic).trim();
|
|
8773
|
+
if (o.outputSize != null && String(o.outputSize).trim())
|
|
8774
|
+
p.output_size = String(o.outputSize).trim();
|
|
8775
|
+
if (o.jumpCut === false)
|
|
8776
|
+
p.jump_cut = false;
|
|
8777
|
+
const duration = {};
|
|
8778
|
+
if (o.durationPref != null && String(o.durationPref).trim())
|
|
8779
|
+
duration.pref = String(o.durationPref).trim();
|
|
8780
|
+
const mcs = parseCountFlag(o.maxClipSec, "--max-clip-sec");
|
|
8781
|
+
if (mcs != null)
|
|
8782
|
+
duration.max_clip_sec = mcs;
|
|
8783
|
+
if (Object.keys(duration).length)
|
|
8784
|
+
p.duration = duration;
|
|
8785
|
+
if (o.splitScreen === true) {
|
|
8786
|
+
const ss = { enable: true };
|
|
8787
|
+
if (o.splitOrientation != null && String(o.splitOrientation).trim())
|
|
8788
|
+
ss.orientation = String(o.splitOrientation).trim();
|
|
8789
|
+
p.split_screen = ss;
|
|
8790
|
+
}
|
|
8791
|
+
if (o.speedFactor != null) {
|
|
8792
|
+
const n = Number(o.speedFactor);
|
|
8793
|
+
if (!Number.isFinite(n))
|
|
8794
|
+
throw new Error(`--speed-factor 需要有限数值,拿到「${o.speedFactor}」`);
|
|
8795
|
+
p.speed = { factor: n };
|
|
8796
|
+
}
|
|
8797
|
+
if (o.cameraMove === false)
|
|
8798
|
+
p.camera_move = { enable: false };
|
|
8799
|
+
const sub = {};
|
|
8800
|
+
if (o.subtitle === false)
|
|
8801
|
+
sub.enable = false;
|
|
8802
|
+
if (o.subtitleTranslateLanguage != null && String(o.subtitleTranslateLanguage).trim()) {
|
|
8803
|
+
sub.translate_language = String(o.subtitleTranslateLanguage).trim();
|
|
8804
|
+
}
|
|
8805
|
+
if (Object.keys(sub).length)
|
|
8806
|
+
p.subtitle = sub;
|
|
8807
|
+
return p;
|
|
8808
|
+
},
|
|
8809
|
+
mapOutputs(out, _ctx) {
|
|
8810
|
+
const clips = Array.isArray(out.clips) ? out.clips : [];
|
|
8811
|
+
const files = [];
|
|
8812
|
+
for (const [i, clip] of clips.entries()) {
|
|
8813
|
+
const f = clip?.file;
|
|
8814
|
+
const url = typeof f?.download_url === "string" ? f.download_url : undefined;
|
|
8815
|
+
if (!url)
|
|
8816
|
+
continue;
|
|
8817
|
+
files.push({ url, filename: `clip${i}${extFromUrl(url, ".mp4")}` });
|
|
8818
|
+
}
|
|
8819
|
+
return files;
|
|
8820
|
+
},
|
|
8821
|
+
mapResult(out) {
|
|
8822
|
+
return { clips: out.clips ?? [], report: out.report ?? {} };
|
|
8823
|
+
},
|
|
8824
|
+
async postprocess(ctx, landed, out) {
|
|
8825
|
+
const clips = Array.isArray(out.clips) ? out.clips : [];
|
|
8826
|
+
if (!clips.length)
|
|
8827
|
+
return;
|
|
8828
|
+
const mp4s = landed.filter((p) => p.toLowerCase().endsWith(".mp4"));
|
|
8829
|
+
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");
|
|
8831
|
+
await writeFile6(dest, renderProReport(clips, out.report ?? {}, {
|
|
8832
|
+
source: ctx.inputAbs ?? ctx.baseName,
|
|
8833
|
+
clipFiles: byIndex
|
|
8834
|
+
}), "utf8");
|
|
8835
|
+
return [dest];
|
|
8836
|
+
}
|
|
8837
|
+
};
|
|
8530
8838
|
function parseCountFlag(v, flag) {
|
|
8531
8839
|
if (v == null)
|
|
8532
8840
|
return;
|
|
@@ -8818,6 +9126,7 @@ var TOOL_REGISTRY = [
|
|
|
8818
9126
|
imageClassicTemplate,
|
|
8819
9127
|
imageVerticalStitch,
|
|
8820
9128
|
videoSplitScreen,
|
|
9129
|
+
videoLong2ShortPro,
|
|
8821
9130
|
audioTtsClone,
|
|
8822
9131
|
mad
|
|
8823
9132
|
];
|
|
@@ -9040,9 +9349,9 @@ function resolveOutDir(descriptor, inputAbs, out) {
|
|
|
9040
9349
|
return resolve5(out);
|
|
9041
9350
|
if (inputAbs) {
|
|
9042
9351
|
const base = basename7(inputAbs, extname4(inputAbs));
|
|
9043
|
-
return
|
|
9352
|
+
return join17(dirname5(inputAbs), `${base}-${descriptor.name}`);
|
|
9044
9353
|
}
|
|
9045
|
-
return
|
|
9354
|
+
return join17(process.cwd(), `${descriptor.name}-${timestamp2()}`);
|
|
9046
9355
|
}
|
|
9047
9356
|
async function safeFingerprint(inputAbs) {
|
|
9048
9357
|
try {
|
|
@@ -9131,7 +9440,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9131
9440
|
}
|
|
9132
9441
|
await mkdir6(outDir, { recursive: true });
|
|
9133
9442
|
const fingerprint2 = inputList ? await Promise.all(inputList.map((p) => safeFingerprint(p))) : inputAbs ? await safeFingerprint(inputAbs) : undefined;
|
|
9134
|
-
await
|
|
9443
|
+
await writeFile7(join17(outDir, "task.json"), JSON.stringify({
|
|
9135
9444
|
tool: descriptor.name,
|
|
9136
9445
|
taskType,
|
|
9137
9446
|
taskId,
|
|
@@ -9153,7 +9462,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9153
9462
|
const errors = {};
|
|
9154
9463
|
const items = descriptor.mapOutputs ? descriptor.mapOutputs(outputResult, ctx) : [];
|
|
9155
9464
|
for (const it of items) {
|
|
9156
|
-
const dest =
|
|
9465
|
+
const dest = join17(outDir, it.filename);
|
|
9157
9466
|
try {
|
|
9158
9467
|
await deps.downloadStream(it.url, dest);
|
|
9159
9468
|
files.push(dest);
|
|
@@ -9161,11 +9470,20 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9161
9470
|
errors[it.filename] = e instanceof Error ? e.message : String(e);
|
|
9162
9471
|
}
|
|
9163
9472
|
}
|
|
9473
|
+
if (descriptor.postprocess) {
|
|
9474
|
+
try {
|
|
9475
|
+
const extra = await descriptor.postprocess(ctx, [...files], outputResult);
|
|
9476
|
+
if (Array.isArray(extra))
|
|
9477
|
+
files.push(...extra);
|
|
9478
|
+
} catch (e) {
|
|
9479
|
+
errors[`${descriptor.name}:postprocess`] = e instanceof Error ? e.message : String(e);
|
|
9480
|
+
}
|
|
9481
|
+
}
|
|
9164
9482
|
let resultFile;
|
|
9165
9483
|
const structured = descriptor.mapResult ? descriptor.mapResult(outputResult, ctx) : undefined;
|
|
9166
9484
|
if (structured != null) {
|
|
9167
|
-
resultFile =
|
|
9168
|
-
await
|
|
9485
|
+
resultFile = join17(outDir, "result-output.json");
|
|
9486
|
+
await writeFile7(resultFile, JSON.stringify(structured, null, 2));
|
|
9169
9487
|
}
|
|
9170
9488
|
if (items.length === 0 && resultFile == null) {
|
|
9171
9489
|
errors["output"] = "任务完成但未解析到任何产物(下载链接与结构化结果均为空,output_result 形态异常)";
|
|
@@ -9183,7 +9501,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9183
9501
|
...resultFile ? { resultFile } : {},
|
|
9184
9502
|
...Object.keys(errors).length ? { errors } : {}
|
|
9185
9503
|
};
|
|
9186
|
-
await
|
|
9504
|
+
await writeFile7(join17(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
|
|
9187
9505
|
return result;
|
|
9188
9506
|
}
|
|
9189
9507
|
|
|
@@ -9240,7 +9558,7 @@ async function copyClipDraftsToRoot(clipDirs, draftTarget, errors) {
|
|
|
9240
9558
|
let complete = 0;
|
|
9241
9559
|
let attempted = 0;
|
|
9242
9560
|
for (const [i, dir] of clipDirs.entries()) {
|
|
9243
|
-
const src =
|
|
9561
|
+
const src = join18(dir, "jianying");
|
|
9244
9562
|
if (!existsSync16(src)) {
|
|
9245
9563
|
paths.push(null);
|
|
9246
9564
|
continue;
|
|
@@ -9280,7 +9598,7 @@ async function runLong2Short(input, opts) {
|
|
|
9280
9598
|
if (!formats.includes("gtrk"))
|
|
9281
9599
|
formats.push("gtrk");
|
|
9282
9600
|
const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
|
|
9283
|
-
const outDir = resolve6(opts.out ??
|
|
9601
|
+
const outDir = resolve6(opts.out ?? join18(dirname6(inputAbs), `${projName}-long2short`));
|
|
9284
9602
|
const outName = basename8(outDir);
|
|
9285
9603
|
let draftRoot;
|
|
9286
9604
|
if (wantJianying) {
|
|
@@ -9290,7 +9608,7 @@ async function runLong2Short(input, opts) {
|
|
|
9290
9608
|
else
|
|
9291
9609
|
log.warn("没找到剪映草稿目录 → 将只产 draft_content.json、缺 meta。可加 --jianying-draft-dir <你的草稿目录> 重跑。");
|
|
9292
9610
|
}
|
|
9293
|
-
const draftTarget = draftRoot ?
|
|
9611
|
+
const draftTarget = draftRoot ? join18(draftRoot, outName) : undefined;
|
|
9294
9612
|
log.step(`▶ 长剪短:${basename8(inputAbs)}(${opts.splitScreen ? "720p 代理 · 智能分屏" : "纯选段 · 音频上传"},格式 ${formats.join("/")})`);
|
|
9295
9613
|
log.step("① 本地预处理(探几何 + 抽音频/720p 代理)…");
|
|
9296
9614
|
const geo = probeGeometry(inputAbs, opts.ffmpegPath);
|
|
@@ -9311,7 +9629,7 @@ async function runLong2Short(input, opts) {
|
|
|
9311
9629
|
const { taskId, fileId } = { taskId: submitted.taskId, fileId: submitted.fileId };
|
|
9312
9630
|
log.info(`task_id = ${taskId}`);
|
|
9313
9631
|
await mkdir7(outDir, { recursive: true });
|
|
9314
|
-
await
|
|
9632
|
+
await writeFile8(join18(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE2, fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
|
|
9315
9633
|
log.step("④ 云端处理中(选段/跳剪" + (opts.splitScreen ? "/分屏" : "") + ",每 5s 轮询)…");
|
|
9316
9634
|
const output = await pollToolTask(cfg, TASK_TYPE2, taskId, {
|
|
9317
9635
|
timeoutMs: POLL_TIMEOUT_MS,
|
|
@@ -9334,9 +9652,9 @@ async function runLong2Short(input, opts) {
|
|
|
9334
9652
|
continue;
|
|
9335
9653
|
}
|
|
9336
9654
|
if (!/^(?:[A-Za-z]:[\\/]|[\\/])/.test(dest))
|
|
9337
|
-
dest =
|
|
9655
|
+
dest = join18(dirname6(inputAbs), dest);
|
|
9338
9656
|
try {
|
|
9339
|
-
await mkdir7(
|
|
9657
|
+
await mkdir7(dirname6(dest), { recursive: true });
|
|
9340
9658
|
await download(url, dest);
|
|
9341
9659
|
splitLanded++;
|
|
9342
9660
|
} catch (e) {
|
|
@@ -9350,7 +9668,7 @@ async function runLong2Short(input, opts) {
|
|
|
9350
9668
|
log.step(`⑥ 逐 clip 拉回三方工程(共 ${clips.length} 条)…`);
|
|
9351
9669
|
const clipResults = [];
|
|
9352
9670
|
for (const [i, clip] of clips.entries()) {
|
|
9353
|
-
const clipDir =
|
|
9671
|
+
const clipDir = join18(outDir, `clip${i}`);
|
|
9354
9672
|
const { files: clipFiles, ...clipMeta } = clip;
|
|
9355
9673
|
try {
|
|
9356
9674
|
const r = await materializeResult({
|
|
@@ -9391,13 +9709,13 @@ async function runLong2Short(input, opts) {
|
|
|
9391
9709
|
for (const [i, cr] of clipResults.entries()) {
|
|
9392
9710
|
try {
|
|
9393
9711
|
await mkdir7(cr.dir, { recursive: true });
|
|
9394
|
-
await
|
|
9712
|
+
await writeFile8(join18(cr.dir, "clip.md"), renderClipBrief(clips[i], i, cr.files));
|
|
9395
9713
|
} catch (e) {
|
|
9396
9714
|
errors[`clip${i}:brief`] = e instanceof Error ? e.message : String(e);
|
|
9397
9715
|
}
|
|
9398
9716
|
}
|
|
9399
9717
|
try {
|
|
9400
|
-
await
|
|
9718
|
+
await writeFile8(join18(outDir, "clips.md"), renderClipsOverview(clips, {
|
|
9401
9719
|
source: inputAbs,
|
|
9402
9720
|
jumpCut: opts.jumpCut !== false,
|
|
9403
9721
|
splitMaterials: { landed: splitLanded, total: manifest.length },
|
|
@@ -9407,7 +9725,7 @@ async function runLong2Short(input, opts) {
|
|
|
9407
9725
|
} catch (e) {
|
|
9408
9726
|
errors["clips.md"] = e instanceof Error ? e.message : String(e);
|
|
9409
9727
|
}
|
|
9410
|
-
await
|
|
9728
|
+
await writeFile8(join18(outDir, "report.json"), JSON.stringify(output.report ?? {}, null, 2));
|
|
9411
9729
|
const ok = Object.keys(errors).length === 0 && clipResults.some((c3) => c3.ok);
|
|
9412
9730
|
const rootResult = {
|
|
9413
9731
|
ok,
|
|
@@ -9418,10 +9736,10 @@ async function runLong2Short(input, opts) {
|
|
|
9418
9736
|
outDir,
|
|
9419
9737
|
clips: clipResults,
|
|
9420
9738
|
splitMaterials: { landed: splitLanded, total: manifest.length },
|
|
9421
|
-
reportFile:
|
|
9739
|
+
reportFile: join18(outDir, "report.json"),
|
|
9422
9740
|
...Object.keys(errors).length ? { errors } : {}
|
|
9423
9741
|
};
|
|
9424
|
-
await
|
|
9742
|
+
await writeFile8(join18(outDir, "result.json"), JSON.stringify({ ...rootResult, finishedAt: new Date().toISOString() }, null, 2));
|
|
9425
9743
|
if (opts.json)
|
|
9426
9744
|
console.log(JSON.stringify(rootResult));
|
|
9427
9745
|
if (opts.open) {
|
|
@@ -9437,7 +9755,7 @@ async function runLong2Short(input, opts) {
|
|
|
9437
9755
|
}
|
|
9438
9756
|
|
|
9439
9757
|
// src/commands/oralcut-result.ts
|
|
9440
|
-
import { resolve as resolve7, join as
|
|
9758
|
+
import { resolve as resolve7, join as join19 } from "node:path";
|
|
9441
9759
|
var TASK_TYPE3 = "cli/video_oral_cut_for_cli";
|
|
9442
9760
|
function timestamp3() {
|
|
9443
9761
|
const d = new Date;
|
|
@@ -9471,7 +9789,7 @@ async function runOralCutResult(taskId, opts) {
|
|
|
9471
9789
|
const pct = got.progress != null ? ` ${Math.round(got.progress)}%` : "";
|
|
9472
9790
|
throw new Error(`任务尚未完成(当前 ${got.status || "未知"}${pct}),暂无法取回结果;请稍后再试。`);
|
|
9473
9791
|
}
|
|
9474
|
-
const outDir = resolve7(opts.out ??
|
|
9792
|
+
const outDir = resolve7(opts.out ?? join19(process.cwd(), `${taskId}-video-project-${timestamp3()}`));
|
|
9475
9793
|
const draftDir = resolveJianyingDraftDir(opts.jianyingDraftDir);
|
|
9476
9794
|
await materializeResult({
|
|
9477
9795
|
outDir,
|
|
@@ -9531,7 +9849,7 @@ function registerUpgrade(program2) {
|
|
|
9531
9849
|
}
|
|
9532
9850
|
|
|
9533
9851
|
// src/commands/render.ts
|
|
9534
|
-
import { resolve as resolve8, dirname as
|
|
9852
|
+
import { resolve as resolve8, dirname as dirname7, join as join20, basename as basename9, extname as extname6 } from "node:path";
|
|
9535
9853
|
import { existsSync as existsSync17 } from "node:fs";
|
|
9536
9854
|
function registerRender(program2) {
|
|
9537
9855
|
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) => {
|
|
@@ -9540,7 +9858,7 @@ function registerRender(program2) {
|
|
|
9540
9858
|
const gtrkAbs = resolve8(gtrk);
|
|
9541
9859
|
if (!existsSync17(gtrkAbs))
|
|
9542
9860
|
throw new Error(`gtrk 工程不存在:${gtrkAbs}`);
|
|
9543
|
-
const outMp4 = resolve8(opts.out ??
|
|
9861
|
+
const outMp4 = resolve8(opts.out ?? join20(dirname7(gtrkAbs), `${basename9(gtrkAbs, extname6(gtrkAbs))}.mp4`));
|
|
9544
9862
|
log.step(`▶ 本地渲染:${basename9(gtrkAbs)} → ${basename9(outMp4)}`);
|
|
9545
9863
|
const project = await readGtrkFile(gtrkAbs);
|
|
9546
9864
|
const result = await renderGtrk(project, outMp4, {
|
|
@@ -9556,7 +9874,7 @@ function registerRender(program2) {
|
|
|
9556
9874
|
log.tickEnd();
|
|
9557
9875
|
log.ok(`渲染完成:${outMp4}(${result.duration.toFixed(1)}s)`);
|
|
9558
9876
|
if (opts.open)
|
|
9559
|
-
openFolder(
|
|
9877
|
+
openFolder(dirname7(outMp4));
|
|
9560
9878
|
if (opts.json) {
|
|
9561
9879
|
console.log(JSON.stringify({ ok: true, output: outMp4, duration: result.duration }));
|
|
9562
9880
|
}
|
|
@@ -9564,14 +9882,14 @@ function registerRender(program2) {
|
|
|
9564
9882
|
}
|
|
9565
9883
|
|
|
9566
9884
|
// src/commands/split.ts
|
|
9567
|
-
import { resolve as resolve9, join as
|
|
9885
|
+
import { resolve as resolve9, join as join22, dirname as dirname9, basename as basename11 } from "node:path";
|
|
9568
9886
|
import { existsSync as existsSync18 } from "node:fs";
|
|
9569
|
-
import { readFile as
|
|
9887
|
+
import { readFile as readFile6, writeFile as writeFile9, mkdir as mkdir8 } from "node:fs/promises";
|
|
9570
9888
|
import { createHash } from "node:crypto";
|
|
9571
9889
|
|
|
9572
9890
|
// src/lib/gtrk-writeback.ts
|
|
9573
9891
|
import { readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "node:fs";
|
|
9574
|
-
import { dirname as
|
|
9892
|
+
import { dirname as dirname8, join as join21, basename as basename10 } from "node:path";
|
|
9575
9893
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
9576
9894
|
function readGtrk(path) {
|
|
9577
9895
|
const raw = readFileSync5(path, "utf8");
|
|
@@ -9598,7 +9916,7 @@ function writeStructMetaSplit(path, gtrk, splitObj, expectedMtimeMs) {
|
|
|
9598
9916
|
}
|
|
9599
9917
|
const nextStructMeta = { ...gtrk.struct_meta ?? {}, split: splitObj };
|
|
9600
9918
|
const next = { ...gtrk, struct_meta: nextStructMeta };
|
|
9601
|
-
const tmp =
|
|
9919
|
+
const tmp = join21(dirname8(path), `.${basename10(path)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
9602
9920
|
try {
|
|
9603
9921
|
writeFileSync2(tmp, JSON.stringify(next, null, 2));
|
|
9604
9922
|
renameSync(tmp, path);
|
|
@@ -9614,7 +9932,7 @@ function writeGtrkAtomic(path, next, expectedMtimeMs) {
|
|
|
9614
9932
|
if (cur !== expectedMtimeMs) {
|
|
9615
9933
|
throw new Error("工程文件在 matrix 运行期间被外部修改(保存冲突),已拒绝写入;请关闭客户端未保存的工程或重跑(plan 与已下载代理均保留)");
|
|
9616
9934
|
}
|
|
9617
|
-
const tmp =
|
|
9935
|
+
const tmp = join21(dirname8(path), `.${basename10(path)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
9618
9936
|
try {
|
|
9619
9937
|
writeFileSync2(tmp, JSON.stringify(next, null, 2));
|
|
9620
9938
|
renameSync(tmp, path);
|
|
@@ -9642,7 +9960,7 @@ function resolvePaths(opts) {
|
|
|
9642
9960
|
if (opts.gtrk) {
|
|
9643
9961
|
gtrkPath = resolve9(opts.gtrk);
|
|
9644
9962
|
} else if (project) {
|
|
9645
|
-
gtrkPath = firstExisting([
|
|
9963
|
+
gtrkPath = firstExisting([join22(project, "gtrk", "project.gtrk"), join22(project, "project.gtrk")]) ?? join22(project, "gtrk", "project.gtrk");
|
|
9646
9964
|
} else {
|
|
9647
9965
|
throw new Error("需 --project <目录> 或显式 --gtrk <path>");
|
|
9648
9966
|
}
|
|
@@ -9653,11 +9971,11 @@ function resolvePaths(opts) {
|
|
|
9653
9971
|
transcriptPath = resolve9(opts.transcript);
|
|
9654
9972
|
else if (project)
|
|
9655
9973
|
transcriptPath = firstExisting([
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
9974
|
+
join22(project, "transcript", "transcript.json"),
|
|
9975
|
+
join22(project, "json", "transcript.json"),
|
|
9976
|
+
join22(project, "transcript.json")
|
|
9659
9977
|
]);
|
|
9660
|
-
const baseDir = project ??
|
|
9978
|
+
const baseDir = project ?? dirname9(gtrkPath);
|
|
9661
9979
|
return { baseDir, gtrkPath, transcriptPath };
|
|
9662
9980
|
}
|
|
9663
9981
|
function slugify2(name) {
|
|
@@ -9665,7 +9983,7 @@ function slugify2(name) {
|
|
|
9665
9983
|
return s || "project";
|
|
9666
9984
|
}
|
|
9667
9985
|
async function loadTranscript(path) {
|
|
9668
|
-
const t = JSON.parse(await
|
|
9986
|
+
const t = JSON.parse(await readFile6(path, "utf8"));
|
|
9669
9987
|
if (!t || !Array.isArray(t.utterances) || typeof t.material_id !== "string" || typeof t.text_hash !== "string") {
|
|
9670
9988
|
throw new Error(`transcript.json 结构异常(缺 utterances/material_id/text_hash):${path}`);
|
|
9671
9989
|
}
|
|
@@ -9686,10 +10004,10 @@ async function runView(baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
9686
10004
|
const transcript = await loadTranscript(transcriptPath);
|
|
9687
10005
|
const { gtrk } = readGtrk(gtrkPath);
|
|
9688
10006
|
const view = projectTranscript(transcript, gtrk, { words: opts.words });
|
|
9689
|
-
const splitDir =
|
|
10007
|
+
const splitDir = join22(baseDir, "split");
|
|
9690
10008
|
await mkdir8(splitDir, { recursive: true });
|
|
9691
|
-
const viewPath =
|
|
9692
|
-
await
|
|
10009
|
+
const viewPath = join22(splitDir, "view.json");
|
|
10010
|
+
await writeFile9(viewPath, JSON.stringify(view, null, 2));
|
|
9693
10011
|
const dropped = view.utterances.filter((u) => u.dropped).length;
|
|
9694
10012
|
log.ok(`投影视图已生成:${viewPath}(${view.utterances.length} 条,其中 ${dropped} 条被剪)`);
|
|
9695
10013
|
const result = {
|
|
@@ -9711,7 +10029,7 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
9711
10029
|
if (!transcriptPath || !existsSync18(transcriptPath))
|
|
9712
10030
|
throw new Error(TRANSCRIPT_MISSING);
|
|
9713
10031
|
log.step("▶ 校验拆分稿并落地…");
|
|
9714
|
-
const doc = JSON.parse(await
|
|
10032
|
+
const doc = JSON.parse(await readFile6(splitdocPath, "utf8"));
|
|
9715
10033
|
const transcript = await loadTranscript(transcriptPath);
|
|
9716
10034
|
const { gtrk, mtimeMs } = readGtrk(gtrkPath);
|
|
9717
10035
|
assertGtrkV1(gtrk);
|
|
@@ -9745,14 +10063,14 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
9745
10063
|
}
|
|
9746
10064
|
});
|
|
9747
10065
|
writeStructMetaSplit(gtrkPath, gtrk, landing.split, mtimeMs);
|
|
9748
|
-
const splitDir =
|
|
10066
|
+
const splitDir = join22(baseDir, "split");
|
|
9749
10067
|
await mkdir8(splitDir, { recursive: true });
|
|
9750
|
-
const dispatchPath =
|
|
9751
|
-
await
|
|
10068
|
+
const dispatchPath = join22(splitDir, "dispatch.json");
|
|
10069
|
+
await writeFile9(dispatchPath, JSON.stringify(landing.dispatch, null, 2));
|
|
9752
10070
|
let mdPath = null;
|
|
9753
10071
|
if (opts.md) {
|
|
9754
|
-
mdPath =
|
|
9755
|
-
await
|
|
10072
|
+
mdPath = join22(splitDir, "visual-split.md");
|
|
10073
|
+
await writeFile9(mdPath, renderSplitMarkdown(doc, landing, { projectSlug, projectedAt }));
|
|
9756
10074
|
}
|
|
9757
10075
|
log.ok(`落地完成:${landing.split.beats.length}/${doc.beats.length} beat 落轨` + `(MG ${landing.dispatch.mg.length} · FILM_BROLL ${landing.dispatch.film_broll.length} · AI_DRAMA ${landing.dispatch.ai_drama.length})`);
|
|
9758
10076
|
for (const s of landing.skipped)
|
|
@@ -9783,9 +10101,9 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
9783
10101
|
}
|
|
9784
10102
|
|
|
9785
10103
|
// src/commands/matrix.ts
|
|
9786
|
-
import { resolve as resolve11, join as
|
|
10104
|
+
import { resolve as resolve11, join as join23, dirname as dirname10, basename as basename12 } from "node:path";
|
|
9787
10105
|
import { existsSync as existsSync20 } from "node:fs";
|
|
9788
|
-
import { readFile as
|
|
10106
|
+
import { readFile as readFile7, writeFile as writeFile10, mkdir as mkdir9, rename } from "node:fs/promises";
|
|
9789
10107
|
|
|
9790
10108
|
// src/lib/solid-png.ts
|
|
9791
10109
|
import { deflateSync } from "node:zlib";
|
|
@@ -10925,16 +11243,16 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
|
|
|
10925
11243
|
let baseDir;
|
|
10926
11244
|
if (opts.dispatch) {
|
|
10927
11245
|
dispatchPath = resolve11(opts.dispatch);
|
|
10928
|
-
baseDir =
|
|
11246
|
+
baseDir = dirname10(dirname10(dispatchPath));
|
|
10929
11247
|
} else if (opts.project) {
|
|
10930
11248
|
baseDir = resolve11(opts.project);
|
|
10931
|
-
dispatchPath =
|
|
11249
|
+
dispatchPath = join23(baseDir, "split", "dispatch.json");
|
|
10932
11250
|
} else {
|
|
10933
11251
|
throw new Error('需 --project <目录> 或显式 --dispatch <path>(ad-hoc 检索用:gtrk matrix search "<query>")');
|
|
10934
11252
|
}
|
|
10935
11253
|
if (!existsSync20(dispatchPath))
|
|
10936
11254
|
throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split <拆分稿> 落地派单)`);
|
|
10937
|
-
const dispatch = JSON.parse(await
|
|
11255
|
+
const dispatch = JSON.parse(await readFile7(dispatchPath, "utf8"));
|
|
10938
11256
|
const rawQueue = Array.isArray(dispatch.film_broll) ? dispatch.film_broll : [];
|
|
10939
11257
|
const earlyGtrkPath = locateGtrk(baseDir);
|
|
10940
11258
|
let earlyGtrk;
|
|
@@ -10997,10 +11315,10 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
|
|
|
10997
11315
|
columnId,
|
|
10998
11316
|
beats
|
|
10999
11317
|
});
|
|
11000
|
-
const splitDir =
|
|
11318
|
+
const splitDir = join23(baseDir, "split");
|
|
11001
11319
|
await mkdir9(splitDir, { recursive: true });
|
|
11002
|
-
const planPath =
|
|
11003
|
-
await
|
|
11320
|
+
const planPath = join23(splitDir, "broll-plan.json");
|
|
11321
|
+
await writeFile10(planPath, JSON.stringify(plan, null, 2));
|
|
11004
11322
|
log.ok(`候选清单已生成:${planPath}(${beats.length} beat · ${okCount}/${totalQueries} query 成功 · ${resultCount} 条候选)`);
|
|
11005
11323
|
log.info("清单只含引用不含素材:cover_url 可直接预览;url 带签名默认 24h 过期,过期重跑本命令即重签。");
|
|
11006
11324
|
const layN = parseLay(opts.lay);
|
|
@@ -11047,13 +11365,13 @@ function parseScoreFloor(raw) {
|
|
|
11047
11365
|
return SCORE_FLOOR_DEFAULT;
|
|
11048
11366
|
}
|
|
11049
11367
|
function locateGtrk(baseDir) {
|
|
11050
|
-
const cands = [
|
|
11368
|
+
const cands = [join23(baseDir, "gtrk", "project.gtrk"), join23(baseDir, "project.gtrk")];
|
|
11051
11369
|
return cands.find((p) => existsSync20(p));
|
|
11052
11370
|
}
|
|
11053
11371
|
async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRelay, reproj) {
|
|
11054
11372
|
const gtrkPath = locateGtrk(baseDir);
|
|
11055
11373
|
if (!gtrkPath) {
|
|
11056
|
-
log.warn(`未找到工程文件(${
|
|
11374
|
+
log.warn(`未找到工程文件(${join23(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——plan 已产出,可后续在有工程的目录重跑`);
|
|
11057
11375
|
return;
|
|
11058
11376
|
}
|
|
11059
11377
|
const { gtrk, mtimeMs } = readGtrk(gtrkPath);
|
|
@@ -11061,8 +11379,8 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
|
|
|
11061
11379
|
const { fills, clipIds } = planBeatFills(plan, layN, scoreFloor);
|
|
11062
11380
|
const slotCount = [...fills.values()].flat().reduce((n, s) => n + s.length, 0);
|
|
11063
11381
|
log.step(`▶ 候选铺轨(${layN} 轨 · 平铺 ${slotCount} 槽位 · ${clipIds.size} 个 clip,preview 代理)…`);
|
|
11064
|
-
const gtrkDir =
|
|
11065
|
-
const previewDir =
|
|
11382
|
+
const gtrkDir = dirname10(gtrkPath);
|
|
11383
|
+
const previewDir = join23(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
|
|
11066
11384
|
await mkdir9(previewDir, { recursive: true });
|
|
11067
11385
|
const prevSource = new Map;
|
|
11068
11386
|
const prevBroll = gtrk.struct_meta?.broll;
|
|
@@ -11085,7 +11403,7 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
|
|
|
11085
11403
|
if (!cand)
|
|
11086
11404
|
continue;
|
|
11087
11405
|
const rel = `${BROLL_PREVIEW_DIR}/${clipId}.mp4`;
|
|
11088
|
-
const abs =
|
|
11406
|
+
const abs = join23(gtrkDir, ...rel.split("/"));
|
|
11089
11407
|
if (existsSync20(abs)) {
|
|
11090
11408
|
const prev = prevSource.get(clipId);
|
|
11091
11409
|
if (prev !== "raw") {
|
|
@@ -11148,12 +11466,12 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
|
|
|
11148
11466
|
const canvas = gtrk.video_size;
|
|
11149
11467
|
const spec = { hex: BLACK_BED_HEX, width: canvas[0], height: canvas[1] };
|
|
11150
11468
|
const rel = solidRelPath(spec);
|
|
11151
|
-
const abs =
|
|
11469
|
+
const abs = join23(gtrkDir, ...rel.split("/"));
|
|
11152
11470
|
try {
|
|
11153
11471
|
if (!existsSync20(abs)) {
|
|
11154
|
-
await mkdir9(
|
|
11472
|
+
await mkdir9(dirname10(abs), { recursive: true });
|
|
11155
11473
|
const tmp = `${abs}.tmp-${process.pid}`;
|
|
11156
|
-
await
|
|
11474
|
+
await writeFile10(tmp, encodeSolidPng(spec));
|
|
11157
11475
|
await rename(tmp, abs);
|
|
11158
11476
|
}
|
|
11159
11477
|
} catch (e) {
|
|
@@ -11216,7 +11534,7 @@ async function downloadProxy(cand, absPath, opts = {}) {
|
|
|
11216
11534
|
if (previewUrl) {
|
|
11217
11535
|
const bytes = await tryFetch(previewUrl);
|
|
11218
11536
|
if (bytes) {
|
|
11219
|
-
await
|
|
11537
|
+
await writeFile10(absPath, bytes);
|
|
11220
11538
|
return "preview";
|
|
11221
11539
|
}
|
|
11222
11540
|
}
|
|
@@ -11224,7 +11542,7 @@ async function downloadProxy(cand, absPath, opts = {}) {
|
|
|
11224
11542
|
return null;
|
|
11225
11543
|
const raw = await tryFetch(cand.url);
|
|
11226
11544
|
if (raw) {
|
|
11227
|
-
await
|
|
11545
|
+
await writeFile10(absPath, raw);
|
|
11228
11546
|
log.warn(`clip ${cand.clip_id} 无 preview 代理,已回落原片(${(raw.length / 1048576).toFixed(1)}MB)`);
|
|
11229
11547
|
return "raw";
|
|
11230
11548
|
}
|
|
@@ -11247,7 +11565,7 @@ async function runAdhoc(query, cfg, tier, broll, overrides, columnId, opts) {
|
|
|
11247
11565
|
};
|
|
11248
11566
|
if (opts.out) {
|
|
11249
11567
|
const outPath = resolve11(opts.out);
|
|
11250
|
-
await
|
|
11568
|
+
await writeFile10(outPath, JSON.stringify({ query, recalled: data.recalled, results }, null, 2));
|
|
11251
11569
|
log.ok(`结果已落盘:${outPath}`);
|
|
11252
11570
|
result.outPath = outPath;
|
|
11253
11571
|
} else if (!opts.json) {
|
|
@@ -11266,9 +11584,9 @@ function slugify3(name) {
|
|
|
11266
11584
|
}
|
|
11267
11585
|
|
|
11268
11586
|
// src/commands/mg.ts
|
|
11269
|
-
import { resolve as resolve12, join as
|
|
11587
|
+
import { resolve as resolve12, join as join24, dirname as dirname11, basename as basename13 } from "node:path";
|
|
11270
11588
|
import { existsSync as existsSync21 } from "node:fs";
|
|
11271
|
-
import { readFile as
|
|
11589
|
+
import { readFile as readFile8, mkdir as mkdir10, copyFile } from "node:fs/promises";
|
|
11272
11590
|
|
|
11273
11591
|
// src/lib/mg-lint.ts
|
|
11274
11592
|
var CDN_OK = [/lib\.baomitu\.com/i, /cdnjs\.cloudflare\.com/i, /unpkg\.com/i];
|
|
@@ -12929,20 +13247,20 @@ async function runMg(words, opts) {
|
|
|
12929
13247
|
function resolveDispatch(opts) {
|
|
12930
13248
|
if (opts.dispatch) {
|
|
12931
13249
|
const dispatchPath = resolve12(opts.dispatch);
|
|
12932
|
-
return { dispatchPath, baseDir:
|
|
13250
|
+
return { dispatchPath, baseDir: dirname11(dirname11(dispatchPath)) };
|
|
12933
13251
|
}
|
|
12934
13252
|
if (opts.project) {
|
|
12935
13253
|
const baseDir = resolve12(opts.project);
|
|
12936
|
-
return { dispatchPath:
|
|
13254
|
+
return { dispatchPath: join24(baseDir, "split", "dispatch.json"), baseDir };
|
|
12937
13255
|
}
|
|
12938
13256
|
throw new Error("需 --project <目录> 或显式 --dispatch <path>");
|
|
12939
13257
|
}
|
|
12940
13258
|
function locateGtrk2(baseDir) {
|
|
12941
|
-
return [
|
|
13259
|
+
return [join24(baseDir, "gtrk", "project.gtrk"), join24(baseDir, "project.gtrk")].find((p) => existsSync21(p));
|
|
12942
13260
|
}
|
|
12943
13261
|
function locateSrcHtml(baseDir, compositionId) {
|
|
12944
13262
|
for (const d of MG_SRC_DIRS) {
|
|
12945
|
-
const p =
|
|
13263
|
+
const p = join24(baseDir, d, `${compositionId}.html`);
|
|
12946
13264
|
if (existsSync21(p))
|
|
12947
13265
|
return p;
|
|
12948
13266
|
}
|
|
@@ -12951,7 +13269,7 @@ function locateSrcHtml(baseDir, compositionId) {
|
|
|
12951
13269
|
async function readMgQueue(dispatchPath) {
|
|
12952
13270
|
if (!existsSync21(dispatchPath))
|
|
12953
13271
|
throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split 落地派单)`);
|
|
12954
|
-
const dispatch = JSON.parse(await
|
|
13272
|
+
const dispatch = JSON.parse(await readFile8(dispatchPath, "utf8"));
|
|
12955
13273
|
const queue = dispatch.mg ?? dispatch.rrv_mg;
|
|
12956
13274
|
return Array.isArray(queue) ? queue : [];
|
|
12957
13275
|
}
|
|
@@ -13025,10 +13343,10 @@ async function runLay(opts) {
|
|
|
13025
13343
|
const srcPath = locateSrcHtml(baseDir, q.composition_id);
|
|
13026
13344
|
if (!srcPath) {
|
|
13027
13345
|
skipped.push({ beat: q.beat, reason: "缺颗粒 HTML(未产出)" });
|
|
13028
|
-
log.warn(`${q.beat}:缺 ${
|
|
13346
|
+
log.warn(`${q.beat}:缺 ${join24(baseDir, MG_SRC_DIRS[0], `${q.composition_id}.html`)},跳过`);
|
|
13029
13347
|
continue;
|
|
13030
13348
|
}
|
|
13031
|
-
const html = await
|
|
13349
|
+
const html = await readFile8(srcPath, "utf8");
|
|
13032
13350
|
const category = typeof q.category === "string" ? q.category : undefined;
|
|
13033
13351
|
const slotDuration = Math.round((win.track_ed - win.track_st) * 1000) / 1000;
|
|
13034
13352
|
const lint = lintParticle(html, {
|
|
@@ -13073,7 +13391,7 @@ async function runLay(opts) {
|
|
|
13073
13391
|
});
|
|
13074
13392
|
}
|
|
13075
13393
|
if (!gtrkPath || !project) {
|
|
13076
|
-
log.warn(`未找到工程文件(${
|
|
13394
|
+
log.warn(`未找到工程文件(${join24(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——lint 已完成`);
|
|
13077
13395
|
return done(opts, {
|
|
13078
13396
|
ok: skipped.length === 0,
|
|
13079
13397
|
mode: "lay",
|
|
@@ -13090,7 +13408,7 @@ async function runLay(opts) {
|
|
|
13090
13408
|
});
|
|
13091
13409
|
}
|
|
13092
13410
|
const { gtrk, mtimeMs } = project;
|
|
13093
|
-
const gtrkDir =
|
|
13411
|
+
const gtrkDir = dirname11(gtrkPath);
|
|
13094
13412
|
const covered = new Set(items.map((it) => it.composition_id));
|
|
13095
13413
|
const orphans = laidBefore.filter((id) => !covered.has(id));
|
|
13096
13414
|
const inDispatch = new Set(allQueue.map((q) => q.composition_id));
|
|
@@ -13132,9 +13450,9 @@ async function runLay(opts) {
|
|
|
13132
13450
|
if (opts.replaceAll && orphans.length > 0) {
|
|
13133
13451
|
log.warn(`--replace-all:已显式授权重置整轨,轨上其余 ${orphans.length} 颗已铺颗粒将被剥离(不走增量保留)。`);
|
|
13134
13452
|
}
|
|
13135
|
-
await mkdir10(
|
|
13453
|
+
await mkdir10(join24(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
|
|
13136
13454
|
for (const it of items) {
|
|
13137
|
-
await copyFile(srcByComp.get(it.composition_id),
|
|
13455
|
+
await copyFile(srcByComp.get(it.composition_id), join24(gtrkDir, ...it.html_rel.split("/")));
|
|
13138
13456
|
}
|
|
13139
13457
|
const { next, summary, mg } = layMgTracks({ gtrk, items, generatedAt: new Date().toISOString(), keep });
|
|
13140
13458
|
const written = withTimecodeSource(next, "mg", reproj);
|
|
@@ -13178,7 +13496,7 @@ async function runLint(args, opts) {
|
|
|
13178
13496
|
const file = args[0];
|
|
13179
13497
|
if (!file)
|
|
13180
13498
|
throw new Error("用法:gtrk mg lint <particle.html> [--dispatch <path>]");
|
|
13181
|
-
const html = await
|
|
13499
|
+
const html = await readFile8(resolve12(file), "utf8");
|
|
13182
13500
|
const nameId = basename13(file).replace(/\.html?$/i, "");
|
|
13183
13501
|
let dispatchIds;
|
|
13184
13502
|
let slotDuration;
|
|
@@ -13249,9 +13567,9 @@ function done(opts, result) {
|
|
|
13249
13567
|
}
|
|
13250
13568
|
|
|
13251
13569
|
// src/lib/mad/mad.ts
|
|
13252
|
-
import { mkdir as mkdir13, writeFile as
|
|
13570
|
+
import { mkdir as mkdir13, writeFile as writeFile12 } from "node:fs/promises";
|
|
13253
13571
|
import { existsSync as existsSync24, statSync as statSync3 } from "node:fs";
|
|
13254
|
-
import { resolve as resolve13, join as
|
|
13572
|
+
import { resolve as resolve13, join as join28 } from "node:path";
|
|
13255
13573
|
|
|
13256
13574
|
// src/lib/convert/types.ts
|
|
13257
13575
|
function num2(v, d = 0) {
|
|
@@ -14031,7 +14349,7 @@ function madJsx(opts) {
|
|
|
14031
14349
|
|
|
14032
14350
|
// src/lib/mad/scan.ts
|
|
14033
14351
|
import { readdirSync, statSync as statSync2 } from "node:fs";
|
|
14034
|
-
import { extname as extname7, join as
|
|
14352
|
+
import { extname as extname7, join as join25 } from "node:path";
|
|
14035
14353
|
var VIDEO_EXTS2 = new Set(defaultExtsFor("video") ?? []);
|
|
14036
14354
|
function scanFolder(dirAbs, opts = {}) {
|
|
14037
14355
|
const probe = opts.probe ?? probeGeometry;
|
|
@@ -14042,7 +14360,7 @@ function scanFolder(dirAbs, opts = {}) {
|
|
|
14042
14360
|
} catch {
|
|
14043
14361
|
throw new Error(`素材文件夹不存在或无法读取:${dirAbs}`);
|
|
14044
14362
|
}
|
|
14045
|
-
const files = entries.filter((n) => VIDEO_EXTS2.has(extname7(n).toLowerCase())).map((n) =>
|
|
14363
|
+
const files = entries.filter((n) => VIDEO_EXTS2.has(extname7(n).toLowerCase())).map((n) => join25(dirAbs, n)).filter((p) => {
|
|
14046
14364
|
try {
|
|
14047
14365
|
return statSync2(p).isFile();
|
|
14048
14366
|
} catch {
|
|
@@ -14221,9 +14539,9 @@ function beatQuantized(natLens, analysis) {
|
|
|
14221
14539
|
|
|
14222
14540
|
// src/lib/mad/data.ts
|
|
14223
14541
|
import { createHash as createHash2 } from "node:crypto";
|
|
14224
|
-
import { mkdir as mkdir11, readFile as
|
|
14542
|
+
import { mkdir as mkdir11, readFile as readFile9, rename as rename2, rm, writeFile as writeFile11, readdir as readdir2 } from "node:fs/promises";
|
|
14225
14543
|
import { existsSync as existsSync22 } from "node:fs";
|
|
14226
|
-
import { join as
|
|
14544
|
+
import { join as join26 } from "node:path";
|
|
14227
14545
|
function madCacheDir() {
|
|
14228
14546
|
return homeFile("mad-cache");
|
|
14229
14547
|
}
|
|
@@ -14248,14 +14566,14 @@ async function fetchWithTimeout(fetchFn, url, timeoutMs) {
|
|
|
14248
14566
|
}
|
|
14249
14567
|
async function atomicWrite(dest, data) {
|
|
14250
14568
|
const tmp = `${dest}.tmp-${process.pid}-${Date.now()}`;
|
|
14251
|
-
await
|
|
14569
|
+
await writeFile11(tmp, data);
|
|
14252
14570
|
await rename2(tmp, dest);
|
|
14253
14571
|
}
|
|
14254
14572
|
async function verifyFile(path, sha256) {
|
|
14255
14573
|
if (!existsSync22(path))
|
|
14256
14574
|
return false;
|
|
14257
14575
|
try {
|
|
14258
|
-
const buf = await
|
|
14576
|
+
const buf = await readFile9(path);
|
|
14259
14577
|
return sha256Hex(buf) === sha256;
|
|
14260
14578
|
} catch {
|
|
14261
14579
|
return false;
|
|
@@ -14280,7 +14598,7 @@ async function cleanupOldVersions(cacheRoot, keepVersion, warn) {
|
|
|
14280
14598
|
for (const e of entries) {
|
|
14281
14599
|
const m = /^v(\d+)$/.exec(e);
|
|
14282
14600
|
if (m && Number(m[1]) !== keepVersion) {
|
|
14283
|
-
await rm(
|
|
14601
|
+
await rm(join26(cacheRoot, e), { recursive: true, force: true }).catch(() => {});
|
|
14284
14602
|
}
|
|
14285
14603
|
}
|
|
14286
14604
|
} catch {}
|
|
@@ -14289,7 +14607,7 @@ async function ensureMadData(opts, deps) {
|
|
|
14289
14607
|
const { cacheRoot, warn } = deps;
|
|
14290
14608
|
const timeout = deps.manifestTimeoutMs ?? 8000;
|
|
14291
14609
|
const mfUrl = deps.manifestUrl ?? manifestUrl();
|
|
14292
|
-
const snapshotPath =
|
|
14610
|
+
const snapshotPath = join26(cacheRoot, "manifest.json");
|
|
14293
14611
|
let manifest = null;
|
|
14294
14612
|
let online = false;
|
|
14295
14613
|
try {
|
|
@@ -14308,7 +14626,7 @@ async function ensureMadData(opts, deps) {
|
|
|
14308
14626
|
throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
|
|
14309
14627
|
}
|
|
14310
14628
|
try {
|
|
14311
|
-
manifest = validateManifest(JSON.parse(await
|
|
14629
|
+
manifest = validateManifest(JSON.parse(await readFile9(snapshotPath, "utf8")));
|
|
14312
14630
|
} catch {
|
|
14313
14631
|
throw new Error("本地 manifest 缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
|
|
14314
14632
|
}
|
|
@@ -14319,8 +14637,8 @@ async function ensureMadData(opts, deps) {
|
|
|
14319
14637
|
}
|
|
14320
14638
|
}
|
|
14321
14639
|
const version = manifest.version;
|
|
14322
|
-
const verDir =
|
|
14323
|
-
const poolPath =
|
|
14640
|
+
const verDir = join26(cacheRoot, `v${version}`);
|
|
14641
|
+
const poolPath = join26(verDir, "mad_pool.json");
|
|
14324
14642
|
const poolMeta = manifest.datasets.mad_pool;
|
|
14325
14643
|
const cacheValid = await verifyFile(poolPath, poolMeta.sha256);
|
|
14326
14644
|
const needDownload = !!opts.refresh || !cacheValid;
|
|
@@ -14350,7 +14668,7 @@ async function ensureMadData(opts, deps) {
|
|
|
14350
14668
|
}
|
|
14351
14669
|
let pool;
|
|
14352
14670
|
try {
|
|
14353
|
-
pool = JSON.parse(await
|
|
14671
|
+
pool = JSON.parse(await readFile9(poolPath, "utf8"));
|
|
14354
14672
|
if (!Array.isArray(pool))
|
|
14355
14673
|
throw new Error("mad_pool 非数组");
|
|
14356
14674
|
} catch (e) {
|
|
@@ -14361,11 +14679,11 @@ async function ensureMadData(opts, deps) {
|
|
|
14361
14679
|
|
|
14362
14680
|
// src/lib/mad/pool.ts
|
|
14363
14681
|
import { gunzipSync } from "node:zlib";
|
|
14364
|
-
import { mkdir as mkdir12, readFile as
|
|
14682
|
+
import { mkdir as mkdir12, readFile as readFile10 } from "node:fs/promises";
|
|
14365
14683
|
import { existsSync as existsSync23 } from "node:fs";
|
|
14366
|
-
import { join as
|
|
14684
|
+
import { join as join27 } from "node:path";
|
|
14367
14685
|
function shardPath(verDir, shard) {
|
|
14368
|
-
return
|
|
14686
|
+
return join27(verDir, "ir", `${shard}.json.gz`);
|
|
14369
14687
|
}
|
|
14370
14688
|
function decodeShard(gz) {
|
|
14371
14689
|
const json = gunzipSync(gz).toString("utf8");
|
|
@@ -14384,7 +14702,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
|
|
|
14384
14702
|
const path = shardPath(verDir, shard);
|
|
14385
14703
|
if (existsSync23(path)) {
|
|
14386
14704
|
try {
|
|
14387
|
-
const s2 = decodeShard(await
|
|
14705
|
+
const s2 = decodeShard(await readFile10(path));
|
|
14388
14706
|
memo.set(shard, s2);
|
|
14389
14707
|
return s2;
|
|
14390
14708
|
} catch {
|
|
@@ -14400,7 +14718,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
|
|
|
14400
14718
|
throw new Error(`IR 分片下载失败 HTTP ${res.status}:${url}`);
|
|
14401
14719
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
14402
14720
|
const s = decodeShard(buf);
|
|
14403
|
-
await mkdir12(
|
|
14721
|
+
await mkdir12(join27(verDir, "ir"), { recursive: true });
|
|
14404
14722
|
await atomicWrite(path, buf);
|
|
14405
14723
|
memo.set(shard, s);
|
|
14406
14724
|
return s;
|
|
@@ -14612,10 +14930,10 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
14612
14930
|
const header = madHeader(version, now.toISOString());
|
|
14613
14931
|
const bgm = level <= 2 && bgmForTrack ? { path: bgmForTrack, markers } : undefined;
|
|
14614
14932
|
const { jsx } = madJsx({ master, windows, header, bgm });
|
|
14615
|
-
const outDir = opts.out ? resolve13(opts.out) :
|
|
14933
|
+
const outDir = opts.out ? resolve13(opts.out) : join28(process.cwd(), `mad-${timestamp4(now)}`);
|
|
14616
14934
|
await mkdir13(outDir, { recursive: true });
|
|
14617
|
-
const jsxPath =
|
|
14618
|
-
await
|
|
14935
|
+
const jsxPath = join28(outDir, "mad.jsx");
|
|
14936
|
+
await writeFile12(jsxPath, jsx);
|
|
14619
14937
|
const result = {
|
|
14620
14938
|
ok: true,
|
|
14621
14939
|
tool: "mad",
|
|
@@ -14626,7 +14944,7 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
14626
14944
|
degradeLevel: level,
|
|
14627
14945
|
techniques: chosen.map((c3) => ({ uid: c3.entry.uid, pid: c3.entry.pid, cat: c3.entry.cat, t0: c3.entry.t0, t1: c3.entry.t1 }))
|
|
14628
14946
|
};
|
|
14629
|
-
await
|
|
14947
|
+
await writeFile12(join28(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: now.toISOString() }, null, 2));
|
|
14630
14948
|
warn(completionMessage(jsxPath, level));
|
|
14631
14949
|
return result;
|
|
14632
14950
|
}
|
|
@@ -14760,8 +15078,8 @@ async function runMadInTool(inputArg, opts) {
|
|
|
14760
15078
|
|
|
14761
15079
|
// src/commands/transcript.ts
|
|
14762
15080
|
import { existsSync as existsSync25 } from "node:fs";
|
|
14763
|
-
import { mkdir as mkdir14, rename as rename3, rm as rm2, stat as stat6, writeFile as
|
|
14764
|
-
import { basename as basename14, dirname as
|
|
15081
|
+
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";
|
|
14765
15083
|
|
|
14766
15084
|
// src/lib/transcript.ts
|
|
14767
15085
|
var AGENT_SUMMARY_PENDING = "<!-- gtrk:agent-summary-pending -->";
|
|
@@ -14911,16 +15229,16 @@ async function validateTranscriptInput(input) {
|
|
|
14911
15229
|
}
|
|
14912
15230
|
function resolveTranscriptOutput(inputAbs, out) {
|
|
14913
15231
|
const base = basename14(inputAbs, extname8(inputAbs));
|
|
14914
|
-
const output = out ? resolve14(out) :
|
|
15232
|
+
const output = out ? resolve14(out) : join29(dirname12(inputAbs), `${base}-transcript.md`);
|
|
14915
15233
|
if (extname8(output).toLowerCase() !== ".md")
|
|
14916
15234
|
throw new Error("--out 必须指向一个 .md 文件");
|
|
14917
15235
|
return output;
|
|
14918
15236
|
}
|
|
14919
15237
|
async function writeMarkdownAtomic(path, markdown) {
|
|
14920
|
-
await mkdir14(
|
|
15238
|
+
await mkdir14(dirname12(path), { recursive: true });
|
|
14921
15239
|
const temp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
14922
15240
|
try {
|
|
14923
|
-
await
|
|
15241
|
+
await writeFile13(temp, markdown, "utf8");
|
|
14924
15242
|
await rename3(temp, path);
|
|
14925
15243
|
} finally {
|
|
14926
15244
|
await rm2(temp, { force: true });
|
|
@@ -14994,8 +15312,8 @@ function registerTranscript(program2) {
|
|
|
14994
15312
|
}
|
|
14995
15313
|
|
|
14996
15314
|
// src/commands/music-visualizer.ts
|
|
14997
|
-
import { resolve as resolve15, join as
|
|
14998
|
-
import { mkdir as mkdir15, writeFile as
|
|
15315
|
+
import { resolve as resolve15, join as join30, dirname as dirname13, basename as basename15, extname as extname9 } from "node:path";
|
|
15316
|
+
import { mkdir as mkdir15, writeFile as writeFile14 } from "node:fs/promises";
|
|
14999
15317
|
import { existsSync as existsSync26 } from "node:fs";
|
|
15000
15318
|
var TASK_TYPE5 = "music_visualizer";
|
|
15001
15319
|
var PRICE_KEY2 = "music_visualizer";
|
|
@@ -15114,7 +15432,7 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
15114
15432
|
assertExt(coverAbs, IMAGE_EXTS2, "封面图");
|
|
15115
15433
|
const extraParams = parseExtraParams3(opts.param, opts.paramsJson);
|
|
15116
15434
|
const projName = basename15(audioAbs, extname9(audioAbs));
|
|
15117
|
-
const outDir = resolve15(opts.out ??
|
|
15435
|
+
const outDir = resolve15(opts.out ?? join30(dirname13(audioAbs), `${projName}-visualizer-${timestamp5()}`));
|
|
15118
15436
|
log.step(`▶ 音乐可视化:${basename15(audioAbs)}(模板 ${template}${bgAbs ? " · 背景" : ""}${coverAbs ? " · 封面" : ""})`);
|
|
15119
15437
|
let billingHint;
|
|
15120
15438
|
try {
|
|
@@ -15161,7 +15479,7 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
15161
15479
|
const { taskId } = submitted;
|
|
15162
15480
|
log.info(`task_id = ${taskId}`);
|
|
15163
15481
|
await mkdir15(outDir, { recursive: true });
|
|
15164
|
-
await
|
|
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));
|
|
15165
15483
|
log.step("③ 云端处理中(每 5s 轮询)…");
|
|
15166
15484
|
const result = await pollTask(cfg, TASK_TYPE5, taskId, (status, progress) => {
|
|
15167
15485
|
log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
|
|
@@ -15172,7 +15490,7 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
15172
15490
|
const files = [];
|
|
15173
15491
|
const errors = {};
|
|
15174
15492
|
if (url) {
|
|
15175
|
-
const dest =
|
|
15493
|
+
const dest = join30(outDir, `${projName}-visualizer${extFromUrl(url, ".mp4")}`);
|
|
15176
15494
|
try {
|
|
15177
15495
|
await downloadStream(url, dest);
|
|
15178
15496
|
files.push(dest);
|
|
@@ -15194,7 +15512,7 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
15194
15512
|
...Object.keys(errors).length ? { errors } : {},
|
|
15195
15513
|
finishedAt: new Date().toISOString()
|
|
15196
15514
|
};
|
|
15197
|
-
await
|
|
15515
|
+
await writeFile14(join30(outDir, "result.json"), JSON.stringify(resultJson, null, 2));
|
|
15198
15516
|
if (opts.json) {
|
|
15199
15517
|
process.stdout.write(`${JSON.stringify(resultJson)}
|
|
15200
15518
|
`);
|
|
@@ -15212,7 +15530,7 @@ try {
|
|
|
15212
15530
|
process.loadEnvFile?.();
|
|
15213
15531
|
} catch {}
|
|
15214
15532
|
migrateLegacyHome();
|
|
15215
|
-
var { version } = JSON.parse(readFileSync6(
|
|
15533
|
+
var { version } = JSON.parse(readFileSync6(join31(packageRoot(), "package.json"), "utf8"));
|
|
15216
15534
|
var program2 = new Command;
|
|
15217
15535
|
program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
|
|
15218
15536
|
registerInstall(program2);
|