@gitruck/cli 0.2.23 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT.md +14 -0
- package/README.md +8 -0
- package/dist/index.js +649 -193
- package/package.json +2 -2
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 join33 } from "node:path";
|
|
4201
4201
|
|
|
4202
4202
|
// src/lib/paths.ts
|
|
4203
4203
|
import { dirname, join } from "node:path";
|
|
@@ -4230,6 +4230,12 @@ function ffmpegDir() {
|
|
|
4230
4230
|
function audioCacheDir() {
|
|
4231
4231
|
return join(GITRUCK_HOME, "audio-cache");
|
|
4232
4232
|
}
|
|
4233
|
+
function fontsDir() {
|
|
4234
|
+
return join(GITRUCK_HOME, "fonts");
|
|
4235
|
+
}
|
|
4236
|
+
function tmpDir() {
|
|
4237
|
+
return join(GITRUCK_HOME, "tmp");
|
|
4238
|
+
}
|
|
4233
4239
|
var _migrated = false;
|
|
4234
4240
|
function migrateLegacyHome() {
|
|
4235
4241
|
if (_migrated)
|
|
@@ -5819,7 +5825,8 @@ import { existsSync as existsSync7 } from "node:fs";
|
|
|
5819
5825
|
import { join as join7 } from "node:path";
|
|
5820
5826
|
var isWin = process.platform === "win32";
|
|
5821
5827
|
var bin = (base) => isWin ? `${base}.exe` : base;
|
|
5822
|
-
var FFMPEG_INSTALL_HINT = `未找到 ffmpeg/ffprobe
|
|
5828
|
+
var FFMPEG_INSTALL_HINT = `未找到 ffmpeg/ffprobe。装它:gtrk deps install --ffmpeg` + `(从同合云镜像拉,自动过 sha256 校验,落 ${ffmpegDir()};agent 可代办)。
|
|
5829
|
+
` + ` 也可自行把 ffmpeg/ffprobe 放到该目录,或用 --ffmpeg-path <目录> 指定已装位置。`;
|
|
5823
5830
|
var _cache = new Map;
|
|
5824
5831
|
function onSystemPath(cmd) {
|
|
5825
5832
|
try {
|
|
@@ -6200,9 +6207,9 @@ function registerInstall(program2) {
|
|
|
6200
6207
|
}
|
|
6201
6208
|
|
|
6202
6209
|
// src/commands/oralcut.ts
|
|
6203
|
-
import { resolve as resolve4, join as
|
|
6210
|
+
import { resolve as resolve4, join as join16, dirname as dirname4, basename as basename7, extname as extname2 } from "node:path";
|
|
6204
6211
|
import { mkdir as mkdir5, writeFile as writeFile5, readFile as readFile4 } from "node:fs/promises";
|
|
6205
|
-
import { existsSync as
|
|
6212
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
6206
6213
|
|
|
6207
6214
|
// src/lib/config.ts
|
|
6208
6215
|
function loadConfig() {
|
|
@@ -6723,8 +6730,194 @@ async function uploadAndSubmitTask(cfg, path, taskType, buildPayload, options =
|
|
|
6723
6730
|
|
|
6724
6731
|
// src/lib/media.ts
|
|
6725
6732
|
import { mkdir as mkdir3, stat as stat4 } from "node:fs/promises";
|
|
6726
|
-
import { existsSync as
|
|
6727
|
-
import {
|
|
6733
|
+
import { closeSync, existsSync as existsSync12, openSync, readSync, readdirSync as readdirSync2 } from "node:fs";
|
|
6734
|
+
import { homedir as homedir3 } from "node:os";
|
|
6735
|
+
import { basename as basename5, extname, join as join13 } from "node:path";
|
|
6736
|
+
|
|
6737
|
+
// src/lib/runtime-assets.ts
|
|
6738
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
6739
|
+
import { createHash } from "node:crypto";
|
|
6740
|
+
import { createWriteStream, existsSync as existsSync11, mkdirSync as mkdirSync4, chmodSync, renameSync, rmSync, readdirSync } from "node:fs";
|
|
6741
|
+
import { createReadStream as createReadStream2 } from "node:fs";
|
|
6742
|
+
import { once } from "node:events";
|
|
6743
|
+
import { pipeline } from "node:stream/promises";
|
|
6744
|
+
import { basename as basename4, dirname as dirname3, join as join12 } from "node:path";
|
|
6745
|
+
var MANIFEST_URL = "https://api.ai-mcn.tv:9000/broadcast/exe/ffmpeg/dist/manifest.json";
|
|
6746
|
+
var SUPPORTED_SCHEMA = 1;
|
|
6747
|
+
function assertHttps(url) {
|
|
6748
|
+
if (!/^https:\/\//i.test(url)) {
|
|
6749
|
+
throw new Error(`运行时资产下载 MUST 用 https,收到:${url}`);
|
|
6750
|
+
}
|
|
6751
|
+
}
|
|
6752
|
+
var DISTRIBUTED_PLATFORMS = ["win-x64", "linux-x64", "mac-x64", "mac-arm64"];
|
|
6753
|
+
function resolvePlatformKey(platform = process.platform, arch = process.arch) {
|
|
6754
|
+
const os = { win32: "win", linux: "linux", darwin: "mac" }[platform];
|
|
6755
|
+
const cpu = { x64: "x64", arm64: "arm64" }[arch];
|
|
6756
|
+
const key = os && cpu ? `${os}-${cpu}` : "";
|
|
6757
|
+
if (!key || !DISTRIBUTED_PLATFORMS.includes(key)) {
|
|
6758
|
+
throw new Error(`当前平台 ${platform}-${arch} 无对应的 ffmpeg 分发包(已分发:${DISTRIBUTED_PLATFORMS.join(" / ")})。` + `请自行安装 ffmpeg/ffprobe 到 ${ffmpegDir()},或用 --ffmpeg-path <目录> 指定已装位置。`);
|
|
6759
|
+
}
|
|
6760
|
+
return key;
|
|
6761
|
+
}
|
|
6762
|
+
function parseManifest(raw) {
|
|
6763
|
+
if (!raw || typeof raw !== "object")
|
|
6764
|
+
throw new Error("manifest 不是对象");
|
|
6765
|
+
const m = raw;
|
|
6766
|
+
if (typeof m.schema !== "number")
|
|
6767
|
+
throw new Error("manifest 缺 schema 字段");
|
|
6768
|
+
if (m.schema > SUPPORTED_SCHEMA) {
|
|
6769
|
+
throw new Error(`manifest schema=${m.schema} 高于本 CLI 支持的 ${SUPPORTED_SCHEMA},请先升级 CLI(gtrk upgrade)`);
|
|
6770
|
+
}
|
|
6771
|
+
if (!m.base || !m.ffmpeg || !m.font)
|
|
6772
|
+
throw new Error("manifest 缺 base/ffmpeg/font 字段");
|
|
6773
|
+
assertHttps(m.base);
|
|
6774
|
+
return m;
|
|
6775
|
+
}
|
|
6776
|
+
async function fetchManifest(url = MANIFEST_URL) {
|
|
6777
|
+
assertHttps(url);
|
|
6778
|
+
const r = await fetch(url);
|
|
6779
|
+
if (!r.ok)
|
|
6780
|
+
throw new Error(`取 manifest 失败:HTTP ${r.status} ${url}`);
|
|
6781
|
+
return parseManifest(await r.json());
|
|
6782
|
+
}
|
|
6783
|
+
async function sha256File(path) {
|
|
6784
|
+
const h = createHash("sha256");
|
|
6785
|
+
await pipeline(createReadStream2(path), h);
|
|
6786
|
+
return h.digest("hex");
|
|
6787
|
+
}
|
|
6788
|
+
function systemTarPath() {
|
|
6789
|
+
if (process.platform === "win32") {
|
|
6790
|
+
const sys = join12(process.env.SystemRoot || "C:\\Windows", "System32", "tar.exe");
|
|
6791
|
+
if (existsSync11(sys))
|
|
6792
|
+
return sys;
|
|
6793
|
+
}
|
|
6794
|
+
return "tar";
|
|
6795
|
+
}
|
|
6796
|
+
function hasSystemTar() {
|
|
6797
|
+
try {
|
|
6798
|
+
return spawnSync4(systemTarPath(), ["--version"], { stdio: "ignore" }).status === 0;
|
|
6799
|
+
} catch {
|
|
6800
|
+
return false;
|
|
6801
|
+
}
|
|
6802
|
+
}
|
|
6803
|
+
async function downloadVerified(url, expectSha256, onProgress) {
|
|
6804
|
+
assertHttps(url);
|
|
6805
|
+
mkdirSync4(tmpDir(), { recursive: true });
|
|
6806
|
+
const tmp = join12(tmpDir(), `dl-${Date.now()}-${url.split("/").pop() ?? "asset"}`);
|
|
6807
|
+
try {
|
|
6808
|
+
const r = await fetch(url);
|
|
6809
|
+
if (!r.ok)
|
|
6810
|
+
throw new Error(`下载失败:HTTP ${r.status} ${url}`);
|
|
6811
|
+
if (!r.body)
|
|
6812
|
+
throw new Error(`下载失败:响应无 body ${url}`);
|
|
6813
|
+
const total = Number(r.headers.get("content-length") ?? 0);
|
|
6814
|
+
let got = 0;
|
|
6815
|
+
const reader = r.body.getReader();
|
|
6816
|
+
const ws = createWriteStream(tmp);
|
|
6817
|
+
try {
|
|
6818
|
+
for (;; ) {
|
|
6819
|
+
const { done, value } = await reader.read();
|
|
6820
|
+
if (done)
|
|
6821
|
+
break;
|
|
6822
|
+
got += value.length;
|
|
6823
|
+
if (onProgress)
|
|
6824
|
+
onProgress(got, total);
|
|
6825
|
+
if (!ws.write(value))
|
|
6826
|
+
await once(ws, "drain");
|
|
6827
|
+
}
|
|
6828
|
+
} finally {
|
|
6829
|
+
ws.end();
|
|
6830
|
+
}
|
|
6831
|
+
await once(ws, "close");
|
|
6832
|
+
const actual = await sha256File(tmp);
|
|
6833
|
+
if (actual !== expectSha256) {
|
|
6834
|
+
rmSync(tmp, { force: true });
|
|
6835
|
+
throw new Error(`sha256 校验不通过,已丢弃下载物、未落地任何文件。
|
|
6836
|
+
期望 ${expectSha256}
|
|
6837
|
+
实得 ${actual}
|
|
6838
|
+
来源 ${url}`);
|
|
6839
|
+
}
|
|
6840
|
+
return tmp;
|
|
6841
|
+
} catch (e) {
|
|
6842
|
+
rmSync(tmp, { force: true });
|
|
6843
|
+
throw e;
|
|
6844
|
+
}
|
|
6845
|
+
}
|
|
6846
|
+
function extractTar(archive, destDir) {
|
|
6847
|
+
if (!hasSystemTar()) {
|
|
6848
|
+
throw new Error(`未找到可用的系统 tar,无法解包 ${archive}。
|
|
6849
|
+
` + `Windows 10+/macOS/Linux 通常自带;若确无,请手工解开该 .tar.xz 并把二进制放到 ${destDir}。`);
|
|
6850
|
+
}
|
|
6851
|
+
mkdirSync4(destDir, { recursive: true });
|
|
6852
|
+
const r = spawnSync4(systemTarPath(), ["-xf", basename4(archive), "-C", destDir], {
|
|
6853
|
+
cwd: dirname3(archive),
|
|
6854
|
+
encoding: "utf8"
|
|
6855
|
+
});
|
|
6856
|
+
if (r.status !== 0) {
|
|
6857
|
+
throw new Error(`tar 解包失败(code=${r.status}):${(r.stderr || "").slice(-300)}`);
|
|
6858
|
+
}
|
|
6859
|
+
}
|
|
6860
|
+
async function installFfmpeg(m, opts = {}) {
|
|
6861
|
+
const key = resolvePlatformKey();
|
|
6862
|
+
const entry = m.ffmpeg[key];
|
|
6863
|
+
if (!entry) {
|
|
6864
|
+
throw new Error(`manifest 中无 ${key} 的 ffmpeg 包(可用:${Object.keys(m.ffmpeg).join(" / ")})。` + `请自行安装到 ${ffmpegDir()} 或用 --ffmpeg-path。`);
|
|
6865
|
+
}
|
|
6866
|
+
const dest = ffmpegDir();
|
|
6867
|
+
const already = entry.members.every((f) => existsSync11(join12(dest, f)));
|
|
6868
|
+
if (already && !opts.force) {
|
|
6869
|
+
return { installed: false, reason: "已存在", detail: `${dest}(--force 可覆盖)` };
|
|
6870
|
+
}
|
|
6871
|
+
const tmp = await downloadVerified(`${m.base}/${entry.file}`, entry.sha256, opts.onProgress);
|
|
6872
|
+
try {
|
|
6873
|
+
const stage = join12(tmpDir(), `x-${Date.now()}`);
|
|
6874
|
+
extractTar(tmp, stage);
|
|
6875
|
+
for (const f of entry.members) {
|
|
6876
|
+
if (!existsSync11(join12(stage, f))) {
|
|
6877
|
+
rmSync(stage, { recursive: true, force: true });
|
|
6878
|
+
throw new Error(`解包结果缺成员 ${f},已中止,未改动 ${dest}`);
|
|
6879
|
+
}
|
|
6880
|
+
}
|
|
6881
|
+
mkdirSync4(dest, { recursive: true });
|
|
6882
|
+
for (const f of entry.members) {
|
|
6883
|
+
const target = join12(dest, f);
|
|
6884
|
+
rmSync(target, { force: true });
|
|
6885
|
+
renameSync(join12(stage, f), target);
|
|
6886
|
+
if (process.platform !== "win32")
|
|
6887
|
+
chmodSync(target, 493);
|
|
6888
|
+
}
|
|
6889
|
+
rmSync(stage, { recursive: true, force: true });
|
|
6890
|
+
return { installed: true, detail: `${entry.version} → ${dest}` };
|
|
6891
|
+
} finally {
|
|
6892
|
+
rmSync(tmp, { force: true });
|
|
6893
|
+
}
|
|
6894
|
+
}
|
|
6895
|
+
async function installFont(m, opts = {}) {
|
|
6896
|
+
const out = [];
|
|
6897
|
+
const dest = fontsDir();
|
|
6898
|
+
for (const [name, entry] of Object.entries(m.font)) {
|
|
6899
|
+
const target = join12(dest, entry.file);
|
|
6900
|
+
if (existsSync11(target) && !opts.force) {
|
|
6901
|
+
out.push({ installed: false, reason: "已存在", detail: `${name} → ${target}` });
|
|
6902
|
+
continue;
|
|
6903
|
+
}
|
|
6904
|
+
const tmp = await downloadVerified(entry.url, entry.sha256, opts.onProgress);
|
|
6905
|
+
mkdirSync4(dest, { recursive: true });
|
|
6906
|
+
rmSync(target, { force: true });
|
|
6907
|
+
renameSync(tmp, target);
|
|
6908
|
+
out.push({ installed: true, detail: `${name} → ${target}` });
|
|
6909
|
+
}
|
|
6910
|
+
return out;
|
|
6911
|
+
}
|
|
6912
|
+
function hasLocalFonts() {
|
|
6913
|
+
try {
|
|
6914
|
+
return readdirSync(fontsDir()).some((f) => /\.(otf|ttf|ttc|otc)$/i.test(f));
|
|
6915
|
+
} catch {
|
|
6916
|
+
return false;
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
|
|
6920
|
+
// src/lib/media.ts
|
|
6728
6921
|
function parseFps(rate) {
|
|
6729
6922
|
if (typeof rate !== "string")
|
|
6730
6923
|
return 0;
|
|
@@ -6772,14 +6965,14 @@ function probeDuration(path, ffmpegPath) {
|
|
|
6772
6965
|
}
|
|
6773
6966
|
async function artifactPath(inputAbs, ext) {
|
|
6774
6967
|
const s = await stat4(inputAbs);
|
|
6775
|
-
const base =
|
|
6968
|
+
const base = basename5(inputAbs, extname(inputAbs));
|
|
6776
6969
|
await mkdir3(audioCacheDir(), { recursive: true });
|
|
6777
|
-
return
|
|
6970
|
+
return join13(audioCacheDir(), `${base}.${s.size}_${Math.round(s.mtimeMs)}.${ext}`);
|
|
6778
6971
|
}
|
|
6779
6972
|
async function extractAudio(inputAbs, ffmpegPath) {
|
|
6780
6973
|
const { ffmpeg } = requireFfmpeg(ffmpegPath);
|
|
6781
6974
|
const out = await artifactPath(inputAbs, "mp3");
|
|
6782
|
-
if (
|
|
6975
|
+
if (existsSync12(out))
|
|
6783
6976
|
return out;
|
|
6784
6977
|
await runFfmpeg(ffmpeg, [
|
|
6785
6978
|
"-y",
|
|
@@ -6803,7 +6996,7 @@ async function extractAudio(inputAbs, ffmpegPath) {
|
|
|
6803
6996
|
async function compress720p(inputAbs, ffmpegPath) {
|
|
6804
6997
|
const { ffmpeg } = requireFfmpeg(ffmpegPath);
|
|
6805
6998
|
const out = await artifactPath(inputAbs, "720p.mp4");
|
|
6806
|
-
if (
|
|
6999
|
+
if (existsSync12(out))
|
|
6807
7000
|
return out;
|
|
6808
7001
|
await runFfmpeg(ffmpeg, [
|
|
6809
7002
|
"-y",
|
|
@@ -6870,9 +7063,151 @@ async function probeFontAvailable(font, ffmpegPath) {
|
|
|
6870
7063
|
return null;
|
|
6871
7064
|
}
|
|
6872
7065
|
}
|
|
7066
|
+
function assFilterPath(p) {
|
|
7067
|
+
return p.replace(/\\/g, "/").replace(/:/g, "\\:");
|
|
7068
|
+
}
|
|
7069
|
+
function nameTableNames(buf) {
|
|
7070
|
+
const out = [];
|
|
7071
|
+
if (buf.length < 6)
|
|
7072
|
+
return out;
|
|
7073
|
+
const count = buf.readUInt16BE(2);
|
|
7074
|
+
const strBase = buf.readUInt16BE(4);
|
|
7075
|
+
for (let i = 0;i < count; i++) {
|
|
7076
|
+
const r = 6 + i * 12;
|
|
7077
|
+
if (r + 12 > buf.length)
|
|
7078
|
+
break;
|
|
7079
|
+
const platformID = buf.readUInt16BE(r);
|
|
7080
|
+
const nameID = buf.readUInt16BE(r + 6);
|
|
7081
|
+
if (nameID !== 1 && nameID !== 4 && nameID !== 16)
|
|
7082
|
+
continue;
|
|
7083
|
+
const len = buf.readUInt16BE(r + 8);
|
|
7084
|
+
const off = strBase + buf.readUInt16BE(r + 10);
|
|
7085
|
+
if (off + len > buf.length)
|
|
7086
|
+
continue;
|
|
7087
|
+
const slice = buf.subarray(off, off + len);
|
|
7088
|
+
let s;
|
|
7089
|
+
if (platformID === 1) {
|
|
7090
|
+
s = slice.toString("latin1");
|
|
7091
|
+
} else {
|
|
7092
|
+
const even = slice.length % 2 === 0 ? slice : slice.subarray(0, slice.length - 1);
|
|
7093
|
+
s = Buffer.from(even).swap16().toString("utf16le");
|
|
7094
|
+
}
|
|
7095
|
+
const v = s.replace(/\0/g, "").trim();
|
|
7096
|
+
if (v)
|
|
7097
|
+
out.push(v);
|
|
7098
|
+
}
|
|
7099
|
+
return out;
|
|
7100
|
+
}
|
|
7101
|
+
function fontFileFamilies(path) {
|
|
7102
|
+
let fd = null;
|
|
7103
|
+
try {
|
|
7104
|
+
fd = openSync(path, "r");
|
|
7105
|
+
const head = Buffer.alloc(12);
|
|
7106
|
+
if (readSync(fd, head, 0, 12, 0) < 12)
|
|
7107
|
+
return [];
|
|
7108
|
+
const bases = [];
|
|
7109
|
+
if (head.toString("latin1", 0, 4) === "ttcf") {
|
|
7110
|
+
const n = head.readUInt32BE(8);
|
|
7111
|
+
const dir = Buffer.alloc(Math.min(n, 64) * 4);
|
|
7112
|
+
readSync(fd, dir, 0, dir.length, 12);
|
|
7113
|
+
for (let i = 0;i < dir.length / 4; i++)
|
|
7114
|
+
bases.push(dir.readUInt32BE(i * 4));
|
|
7115
|
+
} else {
|
|
7116
|
+
bases.push(0);
|
|
7117
|
+
}
|
|
7118
|
+
const out = [];
|
|
7119
|
+
for (const base of bases) {
|
|
7120
|
+
const h = base === 0 ? head : Buffer.alloc(12);
|
|
7121
|
+
if (base !== 0 && readSync(fd, h, 0, 12, base) < 12)
|
|
7122
|
+
continue;
|
|
7123
|
+
const numTables = h.readUInt16BE(4);
|
|
7124
|
+
if (!numTables || numTables > 512)
|
|
7125
|
+
continue;
|
|
7126
|
+
const dir = Buffer.alloc(numTables * 16);
|
|
7127
|
+
if (readSync(fd, dir, 0, dir.length, base + 12) < dir.length)
|
|
7128
|
+
continue;
|
|
7129
|
+
let nameOff = 0;
|
|
7130
|
+
let nameLen = 0;
|
|
7131
|
+
for (let i = 0;i < numTables; i++) {
|
|
7132
|
+
if (dir.toString("latin1", i * 16, i * 16 + 4) === "name") {
|
|
7133
|
+
nameOff = dir.readUInt32BE(i * 16 + 8);
|
|
7134
|
+
nameLen = dir.readUInt32BE(i * 16 + 12);
|
|
7135
|
+
break;
|
|
7136
|
+
}
|
|
7137
|
+
}
|
|
7138
|
+
if (!nameOff || !nameLen || nameLen > 1 << 20)
|
|
7139
|
+
continue;
|
|
7140
|
+
const nameBuf = Buffer.alloc(nameLen);
|
|
7141
|
+
if (readSync(fd, nameBuf, 0, nameLen, nameOff) < nameLen)
|
|
7142
|
+
continue;
|
|
7143
|
+
out.push(...nameTableNames(nameBuf));
|
|
7144
|
+
}
|
|
7145
|
+
return out;
|
|
7146
|
+
} catch {
|
|
7147
|
+
return [];
|
|
7148
|
+
} finally {
|
|
7149
|
+
if (fd !== null)
|
|
7150
|
+
try {
|
|
7151
|
+
closeSync(fd);
|
|
7152
|
+
} catch {}
|
|
7153
|
+
}
|
|
7154
|
+
}
|
|
7155
|
+
var FONT_EXT = /\.(otf|ttf|ttc|otc)$/i;
|
|
7156
|
+
function scanFontDir(dir, out, depth = 0) {
|
|
7157
|
+
if (depth > 3)
|
|
7158
|
+
return;
|
|
7159
|
+
let entries;
|
|
7160
|
+
try {
|
|
7161
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
7162
|
+
} catch {
|
|
7163
|
+
return;
|
|
7164
|
+
}
|
|
7165
|
+
for (const e of entries) {
|
|
7166
|
+
const p = join13(dir, e.name);
|
|
7167
|
+
if (e.isDirectory())
|
|
7168
|
+
scanFontDir(p, out, depth + 1);
|
|
7169
|
+
else if (FONT_EXT.test(e.name))
|
|
7170
|
+
for (const n of fontFileFamilies(p))
|
|
7171
|
+
out.add(n);
|
|
7172
|
+
}
|
|
7173
|
+
}
|
|
7174
|
+
var _famCache = null;
|
|
7175
|
+
function localFontFamilies() {
|
|
7176
|
+
if (_famCache)
|
|
7177
|
+
return _famCache;
|
|
7178
|
+
const out = new Set;
|
|
7179
|
+
scanFontDir(fontsDir(), out);
|
|
7180
|
+
_famCache = [...out];
|
|
7181
|
+
return _famCache;
|
|
7182
|
+
}
|
|
7183
|
+
function systemFontDirs() {
|
|
7184
|
+
const home = homedir3();
|
|
7185
|
+
if (process.platform === "win32") {
|
|
7186
|
+
return [
|
|
7187
|
+
join13(process.env.SystemRoot || "C:\\Windows", "Fonts"),
|
|
7188
|
+
join13(process.env.LOCALAPPDATA || join13(home, "AppData", "Local"), "Microsoft", "Windows", "Fonts")
|
|
7189
|
+
];
|
|
7190
|
+
}
|
|
7191
|
+
if (process.platform === "darwin") {
|
|
7192
|
+
return ["/System/Library/Fonts", "/Library/Fonts", join13(home, "Library", "Fonts")];
|
|
7193
|
+
}
|
|
7194
|
+
return ["/usr/share/fonts", "/usr/local/share/fonts", join13(home, ".local/share/fonts"), join13(home, ".fonts")];
|
|
7195
|
+
}
|
|
7196
|
+
var _sysCache = null;
|
|
7197
|
+
function systemFontFamilies() {
|
|
7198
|
+
if (_sysCache)
|
|
7199
|
+
return _sysCache;
|
|
7200
|
+
const out = new Set;
|
|
7201
|
+
for (const d of systemFontDirs())
|
|
7202
|
+
scanFontDir(d, out);
|
|
7203
|
+
_sysCache = [...out];
|
|
7204
|
+
return _sysCache;
|
|
7205
|
+
}
|
|
6873
7206
|
async function burnSubtitle(videoAbs, assAbs, outAbs, opts = {}) {
|
|
6874
7207
|
const { ffmpeg } = requireFfmpeg(opts.ffmpegPath);
|
|
6875
|
-
|
|
7208
|
+
let filter = `ass='${assFilterPath(assAbs)}'`;
|
|
7209
|
+
if (hasLocalFonts())
|
|
7210
|
+
filter += `:fontsdir='${assFilterPath(fontsDir())}'`;
|
|
6876
7211
|
await runFfmpeg(ffmpeg, [
|
|
6877
7212
|
"-y",
|
|
6878
7213
|
"-v",
|
|
@@ -6880,7 +7215,7 @@ async function burnSubtitle(videoAbs, assAbs, outAbs, opts = {}) {
|
|
|
6880
7215
|
"-i",
|
|
6881
7216
|
videoAbs,
|
|
6882
7217
|
"-vf",
|
|
6883
|
-
|
|
7218
|
+
filter,
|
|
6884
7219
|
"-c:v",
|
|
6885
7220
|
opts.codec ?? "libx264",
|
|
6886
7221
|
"-crf",
|
|
@@ -6895,16 +7230,26 @@ async function burnSubtitle(videoAbs, assAbs, outAbs, opts = {}) {
|
|
|
6895
7230
|
]);
|
|
6896
7231
|
return outAbs;
|
|
6897
7232
|
}
|
|
7233
|
+
async function fontUsableForBurn(font, ffmpegPath) {
|
|
7234
|
+
const local = localFontFamilies();
|
|
7235
|
+
const sys = systemFontFamilies();
|
|
7236
|
+
if (local.includes(font) || sys.includes(font))
|
|
7237
|
+
return true;
|
|
7238
|
+
if (local.length === 0 && sys.length === 0) {
|
|
7239
|
+
return await probeFontAvailable(font, ffmpegPath) !== false;
|
|
7240
|
+
}
|
|
7241
|
+
return false;
|
|
7242
|
+
}
|
|
6898
7243
|
|
|
6899
7244
|
// src/lib/materialize.ts
|
|
6900
|
-
import { join as
|
|
7245
|
+
import { join as join15, basename as basename6 } from "node:path";
|
|
6901
7246
|
import { mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
|
|
6902
7247
|
|
|
6903
7248
|
// src/lib/render.ts
|
|
6904
7249
|
import { writeFile as writeFile3, unlink, readFile as readFile3 } from "node:fs/promises";
|
|
6905
|
-
import { existsSync as
|
|
7250
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
6906
7251
|
import { tmpdir } from "node:os";
|
|
6907
|
-
import { join as
|
|
7252
|
+
import { join as join14 } from "node:path";
|
|
6908
7253
|
var AUDIO_SAMPLE_RATE = 48000;
|
|
6909
7254
|
var AUDIO_LAYOUT = "stereo";
|
|
6910
7255
|
var DEFAULT_CRF = 18;
|
|
@@ -7062,7 +7407,7 @@ function materialPathsFromGtrk(gtrk) {
|
|
|
7062
7407
|
continue;
|
|
7063
7408
|
if (!m.path)
|
|
7064
7409
|
throw new Error(`gtrk 素材 ${m.id} 缺 path(source_path),无法本地渲染`);
|
|
7065
|
-
if (!
|
|
7410
|
+
if (!existsSync13(m.path))
|
|
7066
7411
|
throw new Error(`gtrk 素材文件不存在:${m.path}`);
|
|
7067
7412
|
map[String(m.id)] = m.path;
|
|
7068
7413
|
}
|
|
@@ -7076,7 +7421,7 @@ async function renderGtrk(gtrk, outputPath, opts = {}) {
|
|
|
7076
7421
|
const { ffmpeg } = requireFfmpeg(opts.ffmpegPath);
|
|
7077
7422
|
const materialPaths = materialPathsFromGtrk(gtrk);
|
|
7078
7423
|
const { inputs, graph, total } = buildFilterGraph(gtrk, materialPaths, { crf });
|
|
7079
|
-
const filterFile =
|
|
7424
|
+
const filterFile = join14(tmpdir(), `gtrk-filter-${process.pid}-${inputs.length}.txt`);
|
|
7080
7425
|
await writeFile3(filterFile, graph, "utf8");
|
|
7081
7426
|
try {
|
|
7082
7427
|
const args = ["-y"];
|
|
@@ -7114,7 +7459,7 @@ function gtrkSourceName(gtrk) {
|
|
|
7114
7459
|
const p = gtrk.materials?.[0]?.path;
|
|
7115
7460
|
if (!p)
|
|
7116
7461
|
return;
|
|
7117
|
-
const b =
|
|
7462
|
+
const b = basename6(p);
|
|
7118
7463
|
const dot = b.lastIndexOf(".");
|
|
7119
7464
|
return dot > 0 ? b.slice(0, dot) : b;
|
|
7120
7465
|
}
|
|
@@ -7126,7 +7471,7 @@ async function materializeResult(opts) {
|
|
|
7126
7471
|
throw new Error("任务无工程文件产物(检查 project_formats / 任务是否产出)");
|
|
7127
7472
|
const errors = { ...output.errors ?? {} };
|
|
7128
7473
|
await mkdir4(outDir, { recursive: true });
|
|
7129
|
-
const resultPath =
|
|
7474
|
+
const resultPath = join15(outDir, "result.json");
|
|
7130
7475
|
const writeResult = async (extra) => {
|
|
7131
7476
|
const r = {
|
|
7132
7477
|
ok: Object.keys(errors).length === 0,
|
|
@@ -7148,9 +7493,9 @@ async function materializeResult(opts) {
|
|
|
7148
7493
|
const byFormat = {};
|
|
7149
7494
|
for (const f of files) {
|
|
7150
7495
|
const base = baseFormat(f.format);
|
|
7151
|
-
const fmtDir =
|
|
7496
|
+
const fmtDir = join15(outDir, base);
|
|
7152
7497
|
await mkdir4(fmtDir, { recursive: true });
|
|
7153
|
-
const dest =
|
|
7498
|
+
const dest = join15(fmtDir, f.filename);
|
|
7154
7499
|
try {
|
|
7155
7500
|
await dl(f.download_url, dest);
|
|
7156
7501
|
(byFormat[base] ??= []).push(dest);
|
|
@@ -7166,9 +7511,9 @@ async function materializeResult(opts) {
|
|
|
7166
7511
|
}
|
|
7167
7512
|
let jianyingDraftPath = null;
|
|
7168
7513
|
if (byFormat.jianying && opts.draftDir) {
|
|
7169
|
-
const dest =
|
|
7514
|
+
const dest = join15(opts.draftDir, basename6(outDir));
|
|
7170
7515
|
try {
|
|
7171
|
-
const landing = await copyJianyingDraft(
|
|
7516
|
+
const landing = await copyJianyingDraft(join15(outDir, "jianying"), dest);
|
|
7172
7517
|
if (landing.complete) {
|
|
7173
7518
|
jianyingDraftPath = dest;
|
|
7174
7519
|
log.info(`剪映草稿已落到:${dest}`);
|
|
@@ -7190,7 +7535,7 @@ async function materializeResult(opts) {
|
|
|
7190
7535
|
log.step("本地渲染成片(ffmpeg)…");
|
|
7191
7536
|
const project = await readGtrkFile(gtrkPath);
|
|
7192
7537
|
const name = opts.projName ?? gtrkSourceName(project) ?? taskId;
|
|
7193
|
-
const outMp4 =
|
|
7538
|
+
const outMp4 = join15(outDir, `${name}.mp4`);
|
|
7194
7539
|
const r = await renderGtrk(project, outMp4, {
|
|
7195
7540
|
crf: opts.crf != null ? Number(opts.crf) : undefined,
|
|
7196
7541
|
codec: opts.codec,
|
|
@@ -7212,7 +7557,7 @@ async function materializeResult(opts) {
|
|
|
7212
7557
|
log.step("三方打开(产物已就位,按需自取):");
|
|
7213
7558
|
for (const base of Object.keys(byFormat)) {
|
|
7214
7559
|
const meta = FORMAT_META[base];
|
|
7215
|
-
const target = base === "jianying" ? jianyingDraftPath ??
|
|
7560
|
+
const target = base === "jianying" ? jianyingDraftPath ?? join15(outDir, "jianying") : byFormat[base][0];
|
|
7216
7561
|
console.log(` • ${meta?.label ?? base}:${meta?.openHint(target) ?? target}`);
|
|
7217
7562
|
}
|
|
7218
7563
|
if (rendered)
|
|
@@ -7280,18 +7625,18 @@ async function runOralCut(input, opts) {
|
|
|
7280
7625
|
routeLogsToStderr();
|
|
7281
7626
|
const cfg = loadConfig();
|
|
7282
7627
|
const inputAbs = resolve4(input);
|
|
7283
|
-
if (!
|
|
7628
|
+
if (!existsSync14(inputAbs))
|
|
7284
7629
|
throw new Error(`毛片不存在:${inputAbs}`);
|
|
7285
|
-
const projName =
|
|
7630
|
+
const projName = basename7(inputAbs, extname2(inputAbs));
|
|
7286
7631
|
const formats = opts.formats.split(",").map((s) => s.trim()).filter(Boolean);
|
|
7287
7632
|
if (opts.render && !formats.includes("gtrk"))
|
|
7288
7633
|
formats.push("gtrk");
|
|
7289
7634
|
const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
|
|
7290
|
-
const outDir = resolve4(opts.out ??
|
|
7635
|
+
const outDir = resolve4(opts.out ?? join16(dirname4(inputAbs), `${projName}-video-project-${timestamp()}`));
|
|
7291
7636
|
let scriptPath = opts.script ? resolve4(opts.script) : undefined;
|
|
7292
7637
|
if (!scriptPath) {
|
|
7293
|
-
const sibling =
|
|
7294
|
-
if (
|
|
7638
|
+
const sibling = join16(dirname4(inputAbs), `${projName}.txt`);
|
|
7639
|
+
if (existsSync14(sibling)) {
|
|
7295
7640
|
scriptPath = sibling;
|
|
7296
7641
|
log.info(`自动识别到同名文稿:${sibling}(按有稿剪辑;不想用就改名或显式 --script)`);
|
|
7297
7642
|
}
|
|
@@ -7305,14 +7650,14 @@ async function runOralCut(input, opts) {
|
|
|
7305
7650
|
else
|
|
7306
7651
|
log.warn("没找到剪映草稿目录 → 将只产 draft_content.json、缺 meta。可加 --jianying-draft-dir <你的草稿目录> 重跑。");
|
|
7307
7652
|
}
|
|
7308
|
-
log.step(`▶ 智能口播剪辑:${
|
|
7653
|
+
log.step(`▶ 智能口播剪辑:${basename7(inputAbs)}(预设 ${opts.preset}${opts.visualAssist ? " · 视觉兜底(720p)" : ""},格式 ${formats.join("/")}${opts.render ? " · 本地渲染" : ""})`);
|
|
7309
7654
|
const extraParams = parseExtraParams(opts.param, opts.paramsJson);
|
|
7310
7655
|
log.step("① 本地预处理(探几何 + 抽音频/720p)…");
|
|
7311
7656
|
const geo = probeGeometry(inputAbs, opts.ffmpegPath);
|
|
7312
7657
|
log.info(`原片几何 ${geo.width}x${geo.height} @ ${geo.fps.toFixed(2)}fps · ${geo.duration.toFixed(1)}s`);
|
|
7313
7658
|
const artifact = opts.visualAssist ? await compress720p(inputAbs, opts.ffmpegPath) : await extractAudio(inputAbs, opts.ffmpegPath);
|
|
7314
7659
|
assertDurationConsistent(geo.duration, artifact, opts.ffmpegPath);
|
|
7315
|
-
log.info(opts.visualAssist ? `已压 720p 代理(上传物):${
|
|
7660
|
+
log.info(opts.visualAssist ? `已压 720p 代理(上传物):${basename7(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename7(artifact)}`);
|
|
7316
7661
|
log.step("② 上传抽出物到云端…");
|
|
7317
7662
|
const buildPayload = (fid) => {
|
|
7318
7663
|
const p = {
|
|
@@ -7352,7 +7697,7 @@ async function runOralCut(input, opts) {
|
|
|
7352
7697
|
const up = { fileId: submitted.fileId, cached: submitted.cached };
|
|
7353
7698
|
log.info(`task_id = ${taskId}`);
|
|
7354
7699
|
await mkdir5(outDir, { recursive: true });
|
|
7355
|
-
await writeFile5(
|
|
7700
|
+
await writeFile5(join16(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE, fileId: up.fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
|
|
7356
7701
|
log.step("④ 云端处理中(每 5s 轮询)…");
|
|
7357
7702
|
const result = await pollTask(cfg, TASK_TYPE, taskId, (status, progress) => {
|
|
7358
7703
|
log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
|
|
@@ -7376,9 +7721,9 @@ async function runOralCut(input, opts) {
|
|
|
7376
7721
|
}
|
|
7377
7722
|
|
|
7378
7723
|
// src/commands/long2short.ts
|
|
7379
|
-
import { resolve as resolve6, join as
|
|
7724
|
+
import { resolve as resolve6, join as join19, dirname as dirname7, basename as basename9, extname as extname5 } from "node:path";
|
|
7380
7725
|
import { mkdir as mkdir7, writeFile as writeFile8 } from "node:fs/promises";
|
|
7381
|
-
import { existsSync as
|
|
7726
|
+
import { existsSync as existsSync17 } from "node:fs";
|
|
7382
7727
|
|
|
7383
7728
|
// src/lib/clip-brief.ts
|
|
7384
7729
|
var str = (v) => {
|
|
@@ -7609,11 +7954,11 @@ function renderProReport(clips, report, ctx) {
|
|
|
7609
7954
|
}
|
|
7610
7955
|
|
|
7611
7956
|
// src/lib/tool-runner.ts
|
|
7612
|
-
import { resolve as resolve5, join as
|
|
7957
|
+
import { resolve as resolve5, join as join18, dirname as dirname6, basename as basename8, extname as extname4 } from "node:path";
|
|
7613
7958
|
import { mkdir as mkdir6, writeFile as writeFile7, stat as stat5 } from "node:fs/promises";
|
|
7614
|
-
import { createWriteStream, existsSync as
|
|
7959
|
+
import { createWriteStream as createWriteStream2, existsSync as existsSync16 } from "node:fs";
|
|
7615
7960
|
import { Readable as Readable2 } from "node:stream";
|
|
7616
|
-
import { pipeline } from "node:stream/promises";
|
|
7961
|
+
import { pipeline as pipeline2 } from "node:stream/promises";
|
|
7617
7962
|
|
|
7618
7963
|
// src/lib/tool-pricing.ts
|
|
7619
7964
|
var TOOL_PRICE_LIST_URL = "https://cloud.ai-mcn.tv/api/get_price_list";
|
|
@@ -7697,7 +8042,7 @@ async function resolveToolPricing(priceKey, pricingContext, fetchFn = fetch) {
|
|
|
7697
8042
|
// src/lib/tool-descriptors.ts
|
|
7698
8043
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
7699
8044
|
import { readFile as readFile5, writeFile as writeFile6 } from "node:fs/promises";
|
|
7700
|
-
import { dirname as
|
|
8045
|
+
import { dirname as dirname5, extname as extname3, join as join17, resolve as resolvePath } from "node:path";
|
|
7701
8046
|
var MULTI_INPUT_KINDS = new Set(["images", "videos"]);
|
|
7702
8047
|
var IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif", ".tif", ".tiff", ".heic", ".heif", ".avif"];
|
|
7703
8048
|
var VIDEO_EXTS = [".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v", ".flv", ".wmv", ".mpg", ".mpeg", ".ts", ".m2ts"];
|
|
@@ -8719,13 +9064,14 @@ var videoAiSubtitle = {
|
|
|
8719
9064
|
const fonts = assFontNames(await readFile5(ass, "utf8"));
|
|
8720
9065
|
const missing = [];
|
|
8721
9066
|
for (const f of fonts) {
|
|
8722
|
-
if (await
|
|
9067
|
+
if (!await fontUsableForBurn(f, ctx.ffmpegPath))
|
|
8723
9068
|
missing.push(f);
|
|
8724
9069
|
}
|
|
8725
9070
|
if (missing.length) {
|
|
8726
|
-
throw new Error(`本机缺字幕模板所需字体:${missing.join("、")}。` +
|
|
9071
|
+
throw new Error(`本机缺字幕模板所需字体:${missing.join("、")}。` + `装它:gtrk deps install --font(落 ~/.gitruck/fonts,不写系统字体表)。
|
|
9072
|
+
` + ` 装上后重跑即可(字幕文件已落地,无需重新提交任务)。不用替代字体顶——` + `替代字体烧出来的成片观感与模板设计不符且难以察觉。`);
|
|
8727
9073
|
}
|
|
8728
|
-
const out =
|
|
9074
|
+
const out = join17(dirname5(ass), `${ctx.baseName}-subtitled.mp4`);
|
|
8729
9075
|
await burnSubtitle(inputAbs, ass, out, { ffmpegPath: ctx.ffmpegPath });
|
|
8730
9076
|
return [out];
|
|
8731
9077
|
},
|
|
@@ -8827,7 +9173,7 @@ var videoLong2ShortPro = {
|
|
|
8827
9173
|
return;
|
|
8828
9174
|
const mp4s = landed.filter((p) => p.toLowerCase().endsWith(".mp4"));
|
|
8829
9175
|
const byIndex = clips.map((_c, i) => mp4s.find((p) => new RegExp(`clip${i}.[a-z0-9]+$`, "i").test(p)));
|
|
8830
|
-
const dest =
|
|
9176
|
+
const dest = join17(dirname5(landed[0] ?? "."), "clips.md");
|
|
8831
9177
|
await writeFile6(dest, renderProReport(clips, out.report ?? {}, {
|
|
8832
9178
|
source: ctx.inputAbs ?? ctx.baseName,
|
|
8833
9179
|
clipFiles: byIndex
|
|
@@ -9227,7 +9573,7 @@ function validateToolInput(descriptor, inputAbs) {
|
|
|
9227
9573
|
return;
|
|
9228
9574
|
if (!inputAbs)
|
|
9229
9575
|
throw new Error(`${descriptor.name} 需要输入${spec.kind === "directory" ? "目录" : "文件"}`);
|
|
9230
|
-
if (!
|
|
9576
|
+
if (!existsSync16(inputAbs))
|
|
9231
9577
|
throw new Error(`输入不存在:${inputAbs}`);
|
|
9232
9578
|
if (spec.kind === "directory")
|
|
9233
9579
|
return;
|
|
@@ -9244,13 +9590,13 @@ function validateToolInputs(descriptor, inputAbsList) {
|
|
|
9244
9590
|
throw new Error(`${descriptor.name} 需要至少一个输入文件(可传多个,顺序即拼装顺序)`);
|
|
9245
9591
|
const exts = descriptor.input.exts ?? defaultExtsFor(descriptor.input.kind);
|
|
9246
9592
|
for (const p of inputAbsList) {
|
|
9247
|
-
if (!
|
|
9593
|
+
if (!existsSync16(p))
|
|
9248
9594
|
throw new Error(`输入不存在:${p}`);
|
|
9249
9595
|
if (exts && exts.length) {
|
|
9250
9596
|
const e = extname4(p).toLowerCase();
|
|
9251
9597
|
if (!exts.includes(e)) {
|
|
9252
9598
|
const noun = descriptor.input.kind === "videos" ? "视频" : "图片";
|
|
9253
|
-
throw new Error(`${descriptor.name} 需要${noun}输入,但「${
|
|
9599
|
+
throw new Error(`${descriptor.name} 需要${noun}输入,但「${basename8(p)}」是「${e || "无扩展名"}」(支持:${exts.join(" ")})`);
|
|
9254
9600
|
}
|
|
9255
9601
|
}
|
|
9256
9602
|
}
|
|
@@ -9268,7 +9614,7 @@ async function downloadStream(url, dest) {
|
|
|
9268
9614
|
const res = await fetch(url);
|
|
9269
9615
|
if (!res.ok || !res.body)
|
|
9270
9616
|
throw new Error(`下载失败 HTTP ${res.status}:${url}`);
|
|
9271
|
-
await
|
|
9617
|
+
await pipeline2(Readable2.fromWeb(res.body), createWriteStream2(dest));
|
|
9272
9618
|
}
|
|
9273
9619
|
async function pollToolTask(cfg, taskType, taskId, opts = {}) {
|
|
9274
9620
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
|
|
@@ -9348,10 +9694,10 @@ function resolveOutDir(descriptor, inputAbs, out) {
|
|
|
9348
9694
|
if (out)
|
|
9349
9695
|
return resolve5(out);
|
|
9350
9696
|
if (inputAbs) {
|
|
9351
|
-
const base =
|
|
9352
|
-
return
|
|
9697
|
+
const base = basename8(inputAbs, extname4(inputAbs));
|
|
9698
|
+
return join18(dirname6(inputAbs), `${base}-${descriptor.name}`);
|
|
9353
9699
|
}
|
|
9354
|
-
return
|
|
9700
|
+
return join18(process.cwd(), `${descriptor.name}-${timestamp2()}`);
|
|
9355
9701
|
}
|
|
9356
9702
|
async function safeFingerprint(inputAbs) {
|
|
9357
9703
|
try {
|
|
@@ -9369,7 +9715,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9369
9715
|
const isMulti = MULTI_INPUT_KINDS.has(descriptor.input.kind);
|
|
9370
9716
|
const inputList = isMulti ? (Array.isArray(inputArg) ? inputArg : inputArg != null ? [inputArg] : []).map((p) => resolve5(p)) : undefined;
|
|
9371
9717
|
const inputAbs = isMulti ? inputList[0] : typeof inputArg === "string" ? resolve5(inputArg) : undefined;
|
|
9372
|
-
const baseName2 = inputAbs ?
|
|
9718
|
+
const baseName2 = inputAbs ? basename8(inputAbs, extname4(inputAbs)) : descriptor.name;
|
|
9373
9719
|
if (isMulti) {
|
|
9374
9720
|
validateToolInputs(descriptor, inputList);
|
|
9375
9721
|
} else {
|
|
@@ -9440,7 +9786,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9440
9786
|
}
|
|
9441
9787
|
await mkdir6(outDir, { recursive: true });
|
|
9442
9788
|
const fingerprint2 = inputList ? await Promise.all(inputList.map((p) => safeFingerprint(p))) : inputAbs ? await safeFingerprint(inputAbs) : undefined;
|
|
9443
|
-
await writeFile7(
|
|
9789
|
+
await writeFile7(join18(outDir, "task.json"), JSON.stringify({
|
|
9444
9790
|
tool: descriptor.name,
|
|
9445
9791
|
taskType,
|
|
9446
9792
|
taskId,
|
|
@@ -9462,7 +9808,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9462
9808
|
const errors = {};
|
|
9463
9809
|
const items = descriptor.mapOutputs ? descriptor.mapOutputs(outputResult, ctx) : [];
|
|
9464
9810
|
for (const it of items) {
|
|
9465
|
-
const dest =
|
|
9811
|
+
const dest = join18(outDir, it.filename);
|
|
9466
9812
|
try {
|
|
9467
9813
|
await deps.downloadStream(it.url, dest);
|
|
9468
9814
|
files.push(dest);
|
|
@@ -9482,7 +9828,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9482
9828
|
let resultFile;
|
|
9483
9829
|
const structured = descriptor.mapResult ? descriptor.mapResult(outputResult, ctx) : undefined;
|
|
9484
9830
|
if (structured != null) {
|
|
9485
|
-
resultFile =
|
|
9831
|
+
resultFile = join18(outDir, "result-output.json");
|
|
9486
9832
|
await writeFile7(resultFile, JSON.stringify(structured, null, 2));
|
|
9487
9833
|
}
|
|
9488
9834
|
if (items.length === 0 && resultFile == null) {
|
|
@@ -9501,7 +9847,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9501
9847
|
...resultFile ? { resultFile } : {},
|
|
9502
9848
|
...Object.keys(errors).length ? { errors } : {}
|
|
9503
9849
|
};
|
|
9504
|
-
await writeFile7(
|
|
9850
|
+
await writeFile7(join18(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
|
|
9505
9851
|
return result;
|
|
9506
9852
|
}
|
|
9507
9853
|
|
|
@@ -9558,8 +9904,8 @@ async function copyClipDraftsToRoot(clipDirs, draftTarget, errors) {
|
|
|
9558
9904
|
let complete = 0;
|
|
9559
9905
|
let attempted = 0;
|
|
9560
9906
|
for (const [i, dir] of clipDirs.entries()) {
|
|
9561
|
-
const src =
|
|
9562
|
-
if (!
|
|
9907
|
+
const src = join19(dir, "jianying");
|
|
9908
|
+
if (!existsSync17(src)) {
|
|
9563
9909
|
paths.push(null);
|
|
9564
9910
|
continue;
|
|
9565
9911
|
}
|
|
@@ -9591,15 +9937,15 @@ async function runLong2Short(input, opts) {
|
|
|
9591
9937
|
routeLogsToStderr();
|
|
9592
9938
|
const cfg = loadConfig();
|
|
9593
9939
|
const inputAbs = resolve6(input);
|
|
9594
|
-
if (!
|
|
9940
|
+
if (!existsSync17(inputAbs))
|
|
9595
9941
|
throw new Error(`毛片不存在:${inputAbs}`);
|
|
9596
|
-
const projName =
|
|
9942
|
+
const projName = basename9(inputAbs, extname5(inputAbs));
|
|
9597
9943
|
const formats = opts.formats.split(",").map((s) => s.trim()).filter(Boolean);
|
|
9598
9944
|
if (!formats.includes("gtrk"))
|
|
9599
9945
|
formats.push("gtrk");
|
|
9600
9946
|
const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
|
|
9601
|
-
const outDir = resolve6(opts.out ??
|
|
9602
|
-
const outName =
|
|
9947
|
+
const outDir = resolve6(opts.out ?? join19(dirname7(inputAbs), `${projName}-long2short`));
|
|
9948
|
+
const outName = basename9(outDir);
|
|
9603
9949
|
let draftRoot;
|
|
9604
9950
|
if (wantJianying) {
|
|
9605
9951
|
draftRoot = resolveJianyingDraftDir(opts.jianyingDraftDir);
|
|
@@ -9608,14 +9954,14 @@ async function runLong2Short(input, opts) {
|
|
|
9608
9954
|
else
|
|
9609
9955
|
log.warn("没找到剪映草稿目录 → 将只产 draft_content.json、缺 meta。可加 --jianying-draft-dir <你的草稿目录> 重跑。");
|
|
9610
9956
|
}
|
|
9611
|
-
const draftTarget = draftRoot ?
|
|
9612
|
-
log.step(`▶ 长剪短:${
|
|
9957
|
+
const draftTarget = draftRoot ? join19(draftRoot, outName) : undefined;
|
|
9958
|
+
log.step(`▶ 长剪短:${basename9(inputAbs)}(${opts.splitScreen ? "720p 代理 · 智能分屏" : "纯选段 · 音频上传"},格式 ${formats.join("/")})`);
|
|
9613
9959
|
log.step("① 本地预处理(探几何 + 抽音频/720p 代理)…");
|
|
9614
9960
|
const geo = probeGeometry(inputAbs, opts.ffmpegPath);
|
|
9615
9961
|
log.info(`原片几何 ${geo.width}x${geo.height} @ ${geo.fps.toFixed(2)}fps · ${(geo.duration / 60).toFixed(1)}min`);
|
|
9616
9962
|
const artifact = opts.splitScreen ? await compress720p(inputAbs, opts.ffmpegPath) : await extractAudio(inputAbs, opts.ffmpegPath);
|
|
9617
9963
|
assertDurationConsistent(geo.duration, artifact, opts.ffmpegPath);
|
|
9618
|
-
log.info(opts.splitScreen ? `已压 720p 代理(上传物):${
|
|
9964
|
+
log.info(opts.splitScreen ? `已压 720p 代理(上传物):${basename9(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename9(artifact)}`);
|
|
9619
9965
|
const payloadProbe = buildLong2ShortPayload("__dry_run__", opts, geo, inputAbs, formats, draftTarget);
|
|
9620
9966
|
log.step("② 上传抽出物到云端…");
|
|
9621
9967
|
const submitted = await uploadAndSubmitTask(cfg, artifact, TASK_TYPE2, (fid) => buildLong2ShortPayload(fid, opts, geo, inputAbs, formats, draftTarget), {
|
|
@@ -9629,7 +9975,7 @@ async function runLong2Short(input, opts) {
|
|
|
9629
9975
|
const { taskId, fileId } = { taskId: submitted.taskId, fileId: submitted.fileId };
|
|
9630
9976
|
log.info(`task_id = ${taskId}`);
|
|
9631
9977
|
await mkdir7(outDir, { recursive: true });
|
|
9632
|
-
await writeFile8(
|
|
9978
|
+
await writeFile8(join19(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE2, fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
|
|
9633
9979
|
log.step("④ 云端处理中(选段/跳剪" + (opts.splitScreen ? "/分屏" : "") + ",每 5s 轮询)…");
|
|
9634
9980
|
const output = await pollToolTask(cfg, TASK_TYPE2, taskId, {
|
|
9635
9981
|
timeoutMs: POLL_TIMEOUT_MS,
|
|
@@ -9652,14 +9998,14 @@ async function runLong2Short(input, opts) {
|
|
|
9652
9998
|
continue;
|
|
9653
9999
|
}
|
|
9654
10000
|
if (!/^(?:[A-Za-z]:[\\/]|[\\/])/.test(dest))
|
|
9655
|
-
dest =
|
|
10001
|
+
dest = join19(dirname7(inputAbs), dest);
|
|
9656
10002
|
try {
|
|
9657
|
-
await mkdir7(
|
|
10003
|
+
await mkdir7(dirname7(dest), { recursive: true });
|
|
9658
10004
|
await download(url, dest);
|
|
9659
10005
|
splitLanded++;
|
|
9660
10006
|
} catch (e) {
|
|
9661
|
-
errors[`split:${
|
|
9662
|
-
log.warn(`分屏素材下载失败(${
|
|
10007
|
+
errors[`split:${basename9(dest)}`] = e instanceof Error ? e.message : String(e);
|
|
10008
|
+
log.warn(`分屏素材下载失败(${basename9(dest)}),对应工程中该素材将缺席`);
|
|
9663
10009
|
}
|
|
9664
10010
|
}
|
|
9665
10011
|
if (splitLanded)
|
|
@@ -9668,7 +10014,7 @@ async function runLong2Short(input, opts) {
|
|
|
9668
10014
|
log.step(`⑥ 逐 clip 拉回三方工程(共 ${clips.length} 条)…`);
|
|
9669
10015
|
const clipResults = [];
|
|
9670
10016
|
for (const [i, clip] of clips.entries()) {
|
|
9671
|
-
const clipDir =
|
|
10017
|
+
const clipDir = join19(outDir, `clip${i}`);
|
|
9672
10018
|
const { files: clipFiles, ...clipMeta } = clip;
|
|
9673
10019
|
try {
|
|
9674
10020
|
const r = await materializeResult({
|
|
@@ -9709,13 +10055,13 @@ async function runLong2Short(input, opts) {
|
|
|
9709
10055
|
for (const [i, cr] of clipResults.entries()) {
|
|
9710
10056
|
try {
|
|
9711
10057
|
await mkdir7(cr.dir, { recursive: true });
|
|
9712
|
-
await writeFile8(
|
|
10058
|
+
await writeFile8(join19(cr.dir, "clip.md"), renderClipBrief(clips[i], i, cr.files));
|
|
9713
10059
|
} catch (e) {
|
|
9714
10060
|
errors[`clip${i}:brief`] = e instanceof Error ? e.message : String(e);
|
|
9715
10061
|
}
|
|
9716
10062
|
}
|
|
9717
10063
|
try {
|
|
9718
|
-
await writeFile8(
|
|
10064
|
+
await writeFile8(join19(outDir, "clips.md"), renderClipsOverview(clips, {
|
|
9719
10065
|
source: inputAbs,
|
|
9720
10066
|
jumpCut: opts.jumpCut !== false,
|
|
9721
10067
|
splitMaterials: { landed: splitLanded, total: manifest.length },
|
|
@@ -9725,7 +10071,7 @@ async function runLong2Short(input, opts) {
|
|
|
9725
10071
|
} catch (e) {
|
|
9726
10072
|
errors["clips.md"] = e instanceof Error ? e.message : String(e);
|
|
9727
10073
|
}
|
|
9728
|
-
await writeFile8(
|
|
10074
|
+
await writeFile8(join19(outDir, "report.json"), JSON.stringify(output.report ?? {}, null, 2));
|
|
9729
10075
|
const ok = Object.keys(errors).length === 0 && clipResults.some((c3) => c3.ok);
|
|
9730
10076
|
const rootResult = {
|
|
9731
10077
|
ok,
|
|
@@ -9736,10 +10082,10 @@ async function runLong2Short(input, opts) {
|
|
|
9736
10082
|
outDir,
|
|
9737
10083
|
clips: clipResults,
|
|
9738
10084
|
splitMaterials: { landed: splitLanded, total: manifest.length },
|
|
9739
|
-
reportFile:
|
|
10085
|
+
reportFile: join19(outDir, "report.json"),
|
|
9740
10086
|
...Object.keys(errors).length ? { errors } : {}
|
|
9741
10087
|
};
|
|
9742
|
-
await writeFile8(
|
|
10088
|
+
await writeFile8(join19(outDir, "result.json"), JSON.stringify({ ...rootResult, finishedAt: new Date().toISOString() }, null, 2));
|
|
9743
10089
|
if (opts.json)
|
|
9744
10090
|
console.log(JSON.stringify(rootResult));
|
|
9745
10091
|
if (opts.open) {
|
|
@@ -9755,7 +10101,7 @@ async function runLong2Short(input, opts) {
|
|
|
9755
10101
|
}
|
|
9756
10102
|
|
|
9757
10103
|
// src/commands/oralcut-result.ts
|
|
9758
|
-
import { resolve as resolve7, join as
|
|
10104
|
+
import { resolve as resolve7, join as join20 } from "node:path";
|
|
9759
10105
|
var TASK_TYPE3 = "cli/video_oral_cut_for_cli";
|
|
9760
10106
|
function timestamp3() {
|
|
9761
10107
|
const d = new Date;
|
|
@@ -9789,7 +10135,7 @@ async function runOralCutResult(taskId, opts) {
|
|
|
9789
10135
|
const pct = got.progress != null ? ` ${Math.round(got.progress)}%` : "";
|
|
9790
10136
|
throw new Error(`任务尚未完成(当前 ${got.status || "未知"}${pct}),暂无法取回结果;请稍后再试。`);
|
|
9791
10137
|
}
|
|
9792
|
-
const outDir = resolve7(opts.out ??
|
|
10138
|
+
const outDir = resolve7(opts.out ?? join20(process.cwd(), `${taskId}-video-project-${timestamp3()}`));
|
|
9793
10139
|
const draftDir = resolveJianyingDraftDir(opts.jianyingDraftDir);
|
|
9794
10140
|
await materializeResult({
|
|
9795
10141
|
outDir,
|
|
@@ -9807,10 +10153,10 @@ async function runOralCutResult(taskId, opts) {
|
|
|
9807
10153
|
}
|
|
9808
10154
|
|
|
9809
10155
|
// src/commands/upgrade.ts
|
|
9810
|
-
import { spawnSync as
|
|
10156
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
9811
10157
|
var CLIENT_UPGRADE = "irm https://api.ai-mcn.tv:9000/broadcast/exe/install.ps1 | iex";
|
|
9812
10158
|
function run(cmd) {
|
|
9813
|
-
const r =
|
|
10159
|
+
const r = spawnSync5(cmd, { stdio: "inherit", shell: true });
|
|
9814
10160
|
return r.status ?? 1;
|
|
9815
10161
|
}
|
|
9816
10162
|
function registerUpgrade(program2) {
|
|
@@ -9849,17 +10195,17 @@ function registerUpgrade(program2) {
|
|
|
9849
10195
|
}
|
|
9850
10196
|
|
|
9851
10197
|
// src/commands/render.ts
|
|
9852
|
-
import { resolve as resolve8, dirname as
|
|
9853
|
-
import { existsSync as
|
|
10198
|
+
import { resolve as resolve8, dirname as dirname8, join as join21, basename as basename10, extname as extname6 } from "node:path";
|
|
10199
|
+
import { existsSync as existsSync18 } from "node:fs";
|
|
9854
10200
|
function registerRender(program2) {
|
|
9855
10201
|
program2.command("render <gtrk>").description("本地渲染:gtrk 工程按 EDL 用本地 ffmpeg 渲染成片 mp4(素材取原片本地路径)").option("-o, --out <file>", "输出 mp4 路径(缺省 = <gtrk 同目录>/<gtrk 名>.mp4)").option("--crf <n>", "视频质量 CRF 14-28(越小越清晰/文件越大,默认 18)").option("--codec <c>", "视频编码(默认 h264)").option("--ffmpeg-path <dir>", "指定 ffmpeg/ffprobe 所在目录(缺省 ~/.gitruck/ffmpeg → 系统)").option("--no-open", "完成后不自动打开产物目录").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (gtrk, opts) => {
|
|
9856
10202
|
if (opts.json)
|
|
9857
10203
|
routeLogsToStderr();
|
|
9858
10204
|
const gtrkAbs = resolve8(gtrk);
|
|
9859
|
-
if (!
|
|
10205
|
+
if (!existsSync18(gtrkAbs))
|
|
9860
10206
|
throw new Error(`gtrk 工程不存在:${gtrkAbs}`);
|
|
9861
|
-
const outMp4 = resolve8(opts.out ??
|
|
9862
|
-
log.step(`▶ 本地渲染:${
|
|
10207
|
+
const outMp4 = resolve8(opts.out ?? join21(dirname8(gtrkAbs), `${basename10(gtrkAbs, extname6(gtrkAbs))}.mp4`));
|
|
10208
|
+
log.step(`▶ 本地渲染:${basename10(gtrkAbs)} → ${basename10(outMp4)}`);
|
|
9863
10209
|
const project = await readGtrkFile(gtrkAbs);
|
|
9864
10210
|
const result = await renderGtrk(project, outMp4, {
|
|
9865
10211
|
crf: opts.crf != null ? Number(opts.crf) : undefined,
|
|
@@ -9874,7 +10220,7 @@ function registerRender(program2) {
|
|
|
9874
10220
|
log.tickEnd();
|
|
9875
10221
|
log.ok(`渲染完成:${outMp4}(${result.duration.toFixed(1)}s)`);
|
|
9876
10222
|
if (opts.open)
|
|
9877
|
-
openFolder(
|
|
10223
|
+
openFolder(dirname8(outMp4));
|
|
9878
10224
|
if (opts.json) {
|
|
9879
10225
|
console.log(JSON.stringify({ ok: true, output: outMp4, duration: result.duration }));
|
|
9880
10226
|
}
|
|
@@ -9882,14 +10228,14 @@ function registerRender(program2) {
|
|
|
9882
10228
|
}
|
|
9883
10229
|
|
|
9884
10230
|
// src/commands/split.ts
|
|
9885
|
-
import { resolve as resolve9, join as
|
|
9886
|
-
import { existsSync as
|
|
10231
|
+
import { resolve as resolve9, join as join23, dirname as dirname10, basename as basename12 } from "node:path";
|
|
10232
|
+
import { existsSync as existsSync19 } from "node:fs";
|
|
9887
10233
|
import { readFile as readFile6, writeFile as writeFile9, mkdir as mkdir8 } from "node:fs/promises";
|
|
9888
|
-
import { createHash } from "node:crypto";
|
|
10234
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
9889
10235
|
|
|
9890
10236
|
// src/lib/gtrk-writeback.ts
|
|
9891
|
-
import { readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "node:fs";
|
|
9892
|
-
import { dirname as
|
|
10237
|
+
import { readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync2, renameSync as renameSync2, unlinkSync } from "node:fs";
|
|
10238
|
+
import { dirname as dirname9, join as join22, basename as basename11 } from "node:path";
|
|
9893
10239
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
9894
10240
|
function readGtrk(path) {
|
|
9895
10241
|
const raw = readFileSync5(path, "utf8");
|
|
@@ -9916,10 +10262,10 @@ function writeStructMetaSplit(path, gtrk, splitObj, expectedMtimeMs) {
|
|
|
9916
10262
|
}
|
|
9917
10263
|
const nextStructMeta = { ...gtrk.struct_meta ?? {}, split: splitObj };
|
|
9918
10264
|
const next = { ...gtrk, struct_meta: nextStructMeta };
|
|
9919
|
-
const tmp =
|
|
10265
|
+
const tmp = join22(dirname9(path), `.${basename11(path)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
9920
10266
|
try {
|
|
9921
10267
|
writeFileSync2(tmp, JSON.stringify(next, null, 2));
|
|
9922
|
-
|
|
10268
|
+
renameSync2(tmp, path);
|
|
9923
10269
|
} catch (e) {
|
|
9924
10270
|
try {
|
|
9925
10271
|
unlinkSync(tmp);
|
|
@@ -9932,10 +10278,10 @@ function writeGtrkAtomic(path, next, expectedMtimeMs) {
|
|
|
9932
10278
|
if (cur !== expectedMtimeMs) {
|
|
9933
10279
|
throw new Error("工程文件在 matrix 运行期间被外部修改(保存冲突),已拒绝写入;请关闭客户端未保存的工程或重跑(plan 与已下载代理均保留)");
|
|
9934
10280
|
}
|
|
9935
|
-
const tmp =
|
|
10281
|
+
const tmp = join22(dirname9(path), `.${basename11(path)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
9936
10282
|
try {
|
|
9937
10283
|
writeFileSync2(tmp, JSON.stringify(next, null, 2));
|
|
9938
|
-
|
|
10284
|
+
renameSync2(tmp, path);
|
|
9939
10285
|
} catch (e) {
|
|
9940
10286
|
try {
|
|
9941
10287
|
unlinkSync(tmp);
|
|
@@ -9952,7 +10298,7 @@ function registerSplit(program2) {
|
|
|
9952
10298
|
});
|
|
9953
10299
|
}
|
|
9954
10300
|
function firstExisting(cands) {
|
|
9955
|
-
return cands.find((p) =>
|
|
10301
|
+
return cands.find((p) => existsSync19(p));
|
|
9956
10302
|
}
|
|
9957
10303
|
function resolvePaths(opts) {
|
|
9958
10304
|
const project = opts.project ? resolve9(opts.project) : undefined;
|
|
@@ -9960,22 +10306,22 @@ function resolvePaths(opts) {
|
|
|
9960
10306
|
if (opts.gtrk) {
|
|
9961
10307
|
gtrkPath = resolve9(opts.gtrk);
|
|
9962
10308
|
} else if (project) {
|
|
9963
|
-
gtrkPath = firstExisting([
|
|
10309
|
+
gtrkPath = firstExisting([join23(project, "gtrk", "project.gtrk"), join23(project, "project.gtrk")]) ?? join23(project, "gtrk", "project.gtrk");
|
|
9964
10310
|
} else {
|
|
9965
10311
|
throw new Error("需 --project <目录> 或显式 --gtrk <path>");
|
|
9966
10312
|
}
|
|
9967
|
-
if (!
|
|
10313
|
+
if (!existsSync19(gtrkPath))
|
|
9968
10314
|
throw new Error(`找不到工程文件:${gtrkPath}`);
|
|
9969
10315
|
let transcriptPath;
|
|
9970
10316
|
if (opts.transcript)
|
|
9971
10317
|
transcriptPath = resolve9(opts.transcript);
|
|
9972
10318
|
else if (project)
|
|
9973
10319
|
transcriptPath = firstExisting([
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
10320
|
+
join23(project, "transcript", "transcript.json"),
|
|
10321
|
+
join23(project, "json", "transcript.json"),
|
|
10322
|
+
join23(project, "transcript.json")
|
|
9977
10323
|
]);
|
|
9978
|
-
const baseDir = project ??
|
|
10324
|
+
const baseDir = project ?? dirname10(gtrkPath);
|
|
9979
10325
|
return { baseDir, gtrkPath, transcriptPath };
|
|
9980
10326
|
}
|
|
9981
10327
|
function slugify2(name) {
|
|
@@ -9987,7 +10333,7 @@ async function loadTranscript(path) {
|
|
|
9987
10333
|
if (!t || !Array.isArray(t.utterances) || typeof t.material_id !== "string" || typeof t.text_hash !== "string") {
|
|
9988
10334
|
throw new Error(`transcript.json 结构异常(缺 utterances/material_id/text_hash):${path}`);
|
|
9989
10335
|
}
|
|
9990
|
-
t.text_hash =
|
|
10336
|
+
t.text_hash = createHash2("sha256").update(t.utterances.map((u) => u.text ?? "").join(`
|
|
9991
10337
|
`), "utf8").digest("hex");
|
|
9992
10338
|
return t;
|
|
9993
10339
|
}
|
|
@@ -9998,15 +10344,15 @@ async function runSplit(splitdoc, opts) {
|
|
|
9998
10344
|
return splitdoc ? runLand(resolve9(splitdoc), baseDir, gtrkPath, transcriptPath, opts) : runView(baseDir, gtrkPath, transcriptPath, opts);
|
|
9999
10345
|
}
|
|
10000
10346
|
async function runView(baseDir, gtrkPath, transcriptPath, opts) {
|
|
10001
|
-
if (!transcriptPath || !
|
|
10347
|
+
if (!transcriptPath || !existsSync19(transcriptPath))
|
|
10002
10348
|
throw new Error(TRANSCRIPT_MISSING);
|
|
10003
10349
|
log.step("▶ 导出投影视图(transcript × 当刻 .gtrk)…");
|
|
10004
10350
|
const transcript = await loadTranscript(transcriptPath);
|
|
10005
10351
|
const { gtrk } = readGtrk(gtrkPath);
|
|
10006
10352
|
const view = projectTranscript(transcript, gtrk, { words: opts.words });
|
|
10007
|
-
const splitDir =
|
|
10353
|
+
const splitDir = join23(baseDir, "split");
|
|
10008
10354
|
await mkdir8(splitDir, { recursive: true });
|
|
10009
|
-
const viewPath =
|
|
10355
|
+
const viewPath = join23(splitDir, "view.json");
|
|
10010
10356
|
await writeFile9(viewPath, JSON.stringify(view, null, 2));
|
|
10011
10357
|
const dropped = view.utterances.filter((u) => u.dropped).length;
|
|
10012
10358
|
log.ok(`投影视图已生成:${viewPath}(${view.utterances.length} 条,其中 ${dropped} 条被剪)`);
|
|
@@ -10024,9 +10370,9 @@ async function runView(baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
10024
10370
|
return result;
|
|
10025
10371
|
}
|
|
10026
10372
|
async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
10027
|
-
if (!
|
|
10373
|
+
if (!existsSync19(splitdocPath))
|
|
10028
10374
|
throw new Error(`找不到拆分稿:${splitdocPath}`);
|
|
10029
|
-
if (!transcriptPath || !
|
|
10375
|
+
if (!transcriptPath || !existsSync19(transcriptPath))
|
|
10030
10376
|
throw new Error(TRANSCRIPT_MISSING);
|
|
10031
10377
|
log.step("▶ 校验拆分稿并落地…");
|
|
10032
10378
|
const doc = JSON.parse(await readFile6(splitdocPath, "utf8"));
|
|
@@ -10052,7 +10398,7 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
10052
10398
|
}
|
|
10053
10399
|
const projectedAt = new Date().toISOString();
|
|
10054
10400
|
const view = projectTranscript(transcript, gtrk, { projectedAt });
|
|
10055
|
-
const projectSlug = slugify2(
|
|
10401
|
+
const projectSlug = slugify2(basename12(baseDir));
|
|
10056
10402
|
const landing = buildLanding(doc, view, {
|
|
10057
10403
|
utteranceIds: ctx.utteranceIds,
|
|
10058
10404
|
projectSlug,
|
|
@@ -10063,13 +10409,13 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
10063
10409
|
}
|
|
10064
10410
|
});
|
|
10065
10411
|
writeStructMetaSplit(gtrkPath, gtrk, landing.split, mtimeMs);
|
|
10066
|
-
const splitDir =
|
|
10412
|
+
const splitDir = join23(baseDir, "split");
|
|
10067
10413
|
await mkdir8(splitDir, { recursive: true });
|
|
10068
|
-
const dispatchPath =
|
|
10414
|
+
const dispatchPath = join23(splitDir, "dispatch.json");
|
|
10069
10415
|
await writeFile9(dispatchPath, JSON.stringify(landing.dispatch, null, 2));
|
|
10070
10416
|
let mdPath = null;
|
|
10071
10417
|
if (opts.md) {
|
|
10072
|
-
mdPath =
|
|
10418
|
+
mdPath = join23(splitDir, "visual-split.md");
|
|
10073
10419
|
await writeFile9(mdPath, renderSplitMarkdown(doc, landing, { projectSlug, projectedAt }));
|
|
10074
10420
|
}
|
|
10075
10421
|
log.ok(`落地完成:${landing.split.beats.length}/${doc.beats.length} beat 落轨` + `(MG ${landing.dispatch.mg.length} · FILM_BROLL ${landing.dispatch.film_broll.length} · AI_DRAMA ${landing.dispatch.ai_drama.length})`);
|
|
@@ -10101,8 +10447,8 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
10101
10447
|
}
|
|
10102
10448
|
|
|
10103
10449
|
// src/commands/matrix.ts
|
|
10104
|
-
import { resolve as resolve11, join as
|
|
10105
|
-
import { existsSync as
|
|
10450
|
+
import { resolve as resolve11, join as join24, dirname as dirname11, basename as basename13 } from "node:path";
|
|
10451
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
10106
10452
|
import { readFile as readFile7, writeFile as writeFile10, mkdir as mkdir9, rename } from "node:fs/promises";
|
|
10107
10453
|
|
|
10108
10454
|
// src/lib/solid-png.ts
|
|
@@ -10839,7 +11185,7 @@ function layBrollTracks(opts) {
|
|
|
10839
11185
|
}
|
|
10840
11186
|
|
|
10841
11187
|
// src/lib/material-integrity.ts
|
|
10842
|
-
import { existsSync as
|
|
11188
|
+
import { existsSync as existsSync20 } from "node:fs";
|
|
10843
11189
|
import { resolve as resolve10 } from "node:path";
|
|
10844
11190
|
var INTEGRITY_LIST_CAP = 10;
|
|
10845
11191
|
var r35 = (n) => Math.round(n * 1000) / 1000;
|
|
@@ -10900,7 +11246,7 @@ function collectMaterialRefs(gtrk) {
|
|
|
10900
11246
|
return out;
|
|
10901
11247
|
}
|
|
10902
11248
|
function checkMaterialIntegrity(opts) {
|
|
10903
|
-
const exists = opts.exists ?? ((p) =>
|
|
11249
|
+
const exists = opts.exists ?? ((p) => existsSync20(p));
|
|
10904
11250
|
const materials = Array.isArray(opts.gtrk.materials) ? opts.gtrk.materials : [];
|
|
10905
11251
|
const refs = collectMaterialRefs(opts.gtrk);
|
|
10906
11252
|
const counts = { relative: 0, absolute: 0, remote: 0, noPath: 0 };
|
|
@@ -11243,14 +11589,14 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
|
|
|
11243
11589
|
let baseDir;
|
|
11244
11590
|
if (opts.dispatch) {
|
|
11245
11591
|
dispatchPath = resolve11(opts.dispatch);
|
|
11246
|
-
baseDir =
|
|
11592
|
+
baseDir = dirname11(dirname11(dispatchPath));
|
|
11247
11593
|
} else if (opts.project) {
|
|
11248
11594
|
baseDir = resolve11(opts.project);
|
|
11249
|
-
dispatchPath =
|
|
11595
|
+
dispatchPath = join24(baseDir, "split", "dispatch.json");
|
|
11250
11596
|
} else {
|
|
11251
11597
|
throw new Error('需 --project <目录> 或显式 --dispatch <path>(ad-hoc 检索用:gtrk matrix search "<query>")');
|
|
11252
11598
|
}
|
|
11253
|
-
if (!
|
|
11599
|
+
if (!existsSync21(dispatchPath))
|
|
11254
11600
|
throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split <拆分稿> 落地派单)`);
|
|
11255
11601
|
const dispatch = JSON.parse(await readFile7(dispatchPath, "utf8"));
|
|
11256
11602
|
const rawQueue = Array.isArray(dispatch.film_broll) ? dispatch.film_broll : [];
|
|
@@ -11307,7 +11653,7 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
|
|
|
11307
11653
|
if (totalQueries > 0 && okCount === 0) {
|
|
11308
11654
|
throw new Error(`全部 ${totalQueries} 个 query 检索失败,未写入 plan(逐条原因见上方日志)`);
|
|
11309
11655
|
}
|
|
11310
|
-
const projectSlug = slugify3(
|
|
11656
|
+
const projectSlug = slugify3(basename13(baseDir));
|
|
11311
11657
|
const plan = buildPlan({
|
|
11312
11658
|
generatedAt: new Date().toISOString(),
|
|
11313
11659
|
memberType: tier,
|
|
@@ -11315,9 +11661,9 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
|
|
|
11315
11661
|
columnId,
|
|
11316
11662
|
beats
|
|
11317
11663
|
});
|
|
11318
|
-
const splitDir =
|
|
11664
|
+
const splitDir = join24(baseDir, "split");
|
|
11319
11665
|
await mkdir9(splitDir, { recursive: true });
|
|
11320
|
-
const planPath =
|
|
11666
|
+
const planPath = join24(splitDir, "broll-plan.json");
|
|
11321
11667
|
await writeFile10(planPath, JSON.stringify(plan, null, 2));
|
|
11322
11668
|
log.ok(`候选清单已生成:${planPath}(${beats.length} beat · ${okCount}/${totalQueries} query 成功 · ${resultCount} 条候选)`);
|
|
11323
11669
|
log.info("清单只含引用不含素材:cover_url 可直接预览;url 带签名默认 24h 过期,过期重跑本命令即重签。");
|
|
@@ -11365,13 +11711,13 @@ function parseScoreFloor(raw) {
|
|
|
11365
11711
|
return SCORE_FLOOR_DEFAULT;
|
|
11366
11712
|
}
|
|
11367
11713
|
function locateGtrk(baseDir) {
|
|
11368
|
-
const cands = [
|
|
11369
|
-
return cands.find((p) =>
|
|
11714
|
+
const cands = [join24(baseDir, "gtrk", "project.gtrk"), join24(baseDir, "project.gtrk")];
|
|
11715
|
+
return cands.find((p) => existsSync21(p));
|
|
11370
11716
|
}
|
|
11371
11717
|
async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRelay, reproj) {
|
|
11372
11718
|
const gtrkPath = locateGtrk(baseDir);
|
|
11373
11719
|
if (!gtrkPath) {
|
|
11374
|
-
log.warn(`未找到工程文件(${
|
|
11720
|
+
log.warn(`未找到工程文件(${join24(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——plan 已产出,可后续在有工程的目录重跑`);
|
|
11375
11721
|
return;
|
|
11376
11722
|
}
|
|
11377
11723
|
const { gtrk, mtimeMs } = readGtrk(gtrkPath);
|
|
@@ -11379,8 +11725,8 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
|
|
|
11379
11725
|
const { fills, clipIds } = planBeatFills(plan, layN, scoreFloor);
|
|
11380
11726
|
const slotCount = [...fills.values()].flat().reduce((n, s) => n + s.length, 0);
|
|
11381
11727
|
log.step(`▶ 候选铺轨(${layN} 轨 · 平铺 ${slotCount} 槽位 · ${clipIds.size} 个 clip,preview 代理)…`);
|
|
11382
|
-
const gtrkDir =
|
|
11383
|
-
const previewDir =
|
|
11728
|
+
const gtrkDir = dirname11(gtrkPath);
|
|
11729
|
+
const previewDir = join24(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
|
|
11384
11730
|
await mkdir9(previewDir, { recursive: true });
|
|
11385
11731
|
const prevSource = new Map;
|
|
11386
11732
|
const prevBroll = gtrk.struct_meta?.broll;
|
|
@@ -11403,8 +11749,8 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
|
|
|
11403
11749
|
if (!cand)
|
|
11404
11750
|
continue;
|
|
11405
11751
|
const rel = `${BROLL_PREVIEW_DIR}/${clipId}.mp4`;
|
|
11406
|
-
const abs =
|
|
11407
|
-
if (
|
|
11752
|
+
const abs = join24(gtrkDir, ...rel.split("/"));
|
|
11753
|
+
if (existsSync21(abs)) {
|
|
11408
11754
|
const prev = prevSource.get(clipId);
|
|
11409
11755
|
if (prev !== "raw") {
|
|
11410
11756
|
downloads.set(clipId, { rel, source: prev ?? "preview" });
|
|
@@ -11466,10 +11812,10 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
|
|
|
11466
11812
|
const canvas = gtrk.video_size;
|
|
11467
11813
|
const spec = { hex: BLACK_BED_HEX, width: canvas[0], height: canvas[1] };
|
|
11468
11814
|
const rel = solidRelPath(spec);
|
|
11469
|
-
const abs =
|
|
11815
|
+
const abs = join24(gtrkDir, ...rel.split("/"));
|
|
11470
11816
|
try {
|
|
11471
|
-
if (!
|
|
11472
|
-
await mkdir9(
|
|
11817
|
+
if (!existsSync21(abs)) {
|
|
11818
|
+
await mkdir9(dirname11(abs), { recursive: true });
|
|
11473
11819
|
const tmp = `${abs}.tmp-${process.pid}`;
|
|
11474
11820
|
await writeFile10(tmp, encodeSolidPng(spec));
|
|
11475
11821
|
await rename(tmp, abs);
|
|
@@ -11584,8 +11930,8 @@ function slugify3(name) {
|
|
|
11584
11930
|
}
|
|
11585
11931
|
|
|
11586
11932
|
// src/commands/mg.ts
|
|
11587
|
-
import { resolve as resolve12, join as
|
|
11588
|
-
import { existsSync as
|
|
11933
|
+
import { resolve as resolve12, join as join25, dirname as dirname12, basename as basename14 } from "node:path";
|
|
11934
|
+
import { existsSync as existsSync22 } from "node:fs";
|
|
11589
11935
|
import { readFile as readFile8, mkdir as mkdir10, copyFile } from "node:fs/promises";
|
|
11590
11936
|
|
|
11591
11937
|
// src/lib/mg-lint.ts
|
|
@@ -13247,27 +13593,27 @@ async function runMg(words, opts) {
|
|
|
13247
13593
|
function resolveDispatch(opts) {
|
|
13248
13594
|
if (opts.dispatch) {
|
|
13249
13595
|
const dispatchPath = resolve12(opts.dispatch);
|
|
13250
|
-
return { dispatchPath, baseDir:
|
|
13596
|
+
return { dispatchPath, baseDir: dirname12(dirname12(dispatchPath)) };
|
|
13251
13597
|
}
|
|
13252
13598
|
if (opts.project) {
|
|
13253
13599
|
const baseDir = resolve12(opts.project);
|
|
13254
|
-
return { dispatchPath:
|
|
13600
|
+
return { dispatchPath: join25(baseDir, "split", "dispatch.json"), baseDir };
|
|
13255
13601
|
}
|
|
13256
13602
|
throw new Error("需 --project <目录> 或显式 --dispatch <path>");
|
|
13257
13603
|
}
|
|
13258
13604
|
function locateGtrk2(baseDir) {
|
|
13259
|
-
return [
|
|
13605
|
+
return [join25(baseDir, "gtrk", "project.gtrk"), join25(baseDir, "project.gtrk")].find((p) => existsSync22(p));
|
|
13260
13606
|
}
|
|
13261
13607
|
function locateSrcHtml(baseDir, compositionId) {
|
|
13262
13608
|
for (const d of MG_SRC_DIRS) {
|
|
13263
|
-
const p =
|
|
13264
|
-
if (
|
|
13609
|
+
const p = join25(baseDir, d, `${compositionId}.html`);
|
|
13610
|
+
if (existsSync22(p))
|
|
13265
13611
|
return p;
|
|
13266
13612
|
}
|
|
13267
13613
|
return;
|
|
13268
13614
|
}
|
|
13269
13615
|
async function readMgQueue(dispatchPath) {
|
|
13270
|
-
if (!
|
|
13616
|
+
if (!existsSync22(dispatchPath))
|
|
13271
13617
|
throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split 落地派单)`);
|
|
13272
13618
|
const dispatch = JSON.parse(await readFile8(dispatchPath, "utf8"));
|
|
13273
13619
|
const queue = dispatch.mg ?? dispatch.rrv_mg;
|
|
@@ -13343,7 +13689,7 @@ async function runLay(opts) {
|
|
|
13343
13689
|
const srcPath = locateSrcHtml(baseDir, q.composition_id);
|
|
13344
13690
|
if (!srcPath) {
|
|
13345
13691
|
skipped.push({ beat: q.beat, reason: "缺颗粒 HTML(未产出)" });
|
|
13346
|
-
log.warn(`${q.beat}:缺 ${
|
|
13692
|
+
log.warn(`${q.beat}:缺 ${join25(baseDir, MG_SRC_DIRS[0], `${q.composition_id}.html`)},跳过`);
|
|
13347
13693
|
continue;
|
|
13348
13694
|
}
|
|
13349
13695
|
const html = await readFile8(srcPath, "utf8");
|
|
@@ -13391,7 +13737,7 @@ async function runLay(opts) {
|
|
|
13391
13737
|
});
|
|
13392
13738
|
}
|
|
13393
13739
|
if (!gtrkPath || !project) {
|
|
13394
|
-
log.warn(`未找到工程文件(${
|
|
13740
|
+
log.warn(`未找到工程文件(${join25(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——lint 已完成`);
|
|
13395
13741
|
return done(opts, {
|
|
13396
13742
|
ok: skipped.length === 0,
|
|
13397
13743
|
mode: "lay",
|
|
@@ -13408,7 +13754,7 @@ async function runLay(opts) {
|
|
|
13408
13754
|
});
|
|
13409
13755
|
}
|
|
13410
13756
|
const { gtrk, mtimeMs } = project;
|
|
13411
|
-
const gtrkDir =
|
|
13757
|
+
const gtrkDir = dirname12(gtrkPath);
|
|
13412
13758
|
const covered = new Set(items.map((it) => it.composition_id));
|
|
13413
13759
|
const orphans = laidBefore.filter((id) => !covered.has(id));
|
|
13414
13760
|
const inDispatch = new Set(allQueue.map((q) => q.composition_id));
|
|
@@ -13450,9 +13796,9 @@ async function runLay(opts) {
|
|
|
13450
13796
|
if (opts.replaceAll && orphans.length > 0) {
|
|
13451
13797
|
log.warn(`--replace-all:已显式授权重置整轨,轨上其余 ${orphans.length} 颗已铺颗粒将被剥离(不走增量保留)。`);
|
|
13452
13798
|
}
|
|
13453
|
-
await mkdir10(
|
|
13799
|
+
await mkdir10(join25(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
|
|
13454
13800
|
for (const it of items) {
|
|
13455
|
-
await copyFile(srcByComp.get(it.composition_id),
|
|
13801
|
+
await copyFile(srcByComp.get(it.composition_id), join25(gtrkDir, ...it.html_rel.split("/")));
|
|
13456
13802
|
}
|
|
13457
13803
|
const { next, summary, mg } = layMgTracks({ gtrk, items, generatedAt: new Date().toISOString(), keep });
|
|
13458
13804
|
const written = withTimecodeSource(next, "mg", reproj);
|
|
@@ -13497,11 +13843,11 @@ async function runLint(args, opts) {
|
|
|
13497
13843
|
if (!file)
|
|
13498
13844
|
throw new Error("用法:gtrk mg lint <particle.html> [--dispatch <path>]");
|
|
13499
13845
|
const html = await readFile8(resolve12(file), "utf8");
|
|
13500
|
-
const nameId =
|
|
13846
|
+
const nameId = basename14(file).replace(/\.html?$/i, "");
|
|
13501
13847
|
let dispatchIds;
|
|
13502
13848
|
let slotDuration;
|
|
13503
13849
|
let compositionId;
|
|
13504
|
-
if (opts.dispatch &&
|
|
13850
|
+
if (opts.dispatch && existsSync22(resolve12(opts.dispatch))) {
|
|
13505
13851
|
const queue = await readMgQueue(resolve12(opts.dispatch));
|
|
13506
13852
|
dispatchIds = queue.map((q) => q.composition_id);
|
|
13507
13853
|
const byName = queue.find((q) => q.composition_id === nameId);
|
|
@@ -13525,7 +13871,7 @@ async function runLint(args, opts) {
|
|
|
13525
13871
|
for (const vv of lint.violations)
|
|
13526
13872
|
(vv.fatal ? log.err : log.warn)(`${vv.fatal ? "✗" : "·"} ${vv.law}: ${vv.msg}`);
|
|
13527
13873
|
if (lint.ok)
|
|
13528
|
-
log.ok(`lint 通过(${
|
|
13874
|
+
log.ok(`lint 通过(${basename14(file)};opaque=${lint.opaque})`);
|
|
13529
13875
|
else
|
|
13530
13876
|
log.err(`lint 未过(${lint.violations.filter((v) => v.fatal).length} 项致命)`);
|
|
13531
13877
|
const result = { mode: "lint", ...lint, ok: lint.ok };
|
|
@@ -13568,8 +13914,8 @@ function done(opts, result) {
|
|
|
13568
13914
|
|
|
13569
13915
|
// src/lib/mad/mad.ts
|
|
13570
13916
|
import { mkdir as mkdir13, writeFile as writeFile12 } from "node:fs/promises";
|
|
13571
|
-
import { existsSync as
|
|
13572
|
-
import { resolve as resolve13, join as
|
|
13917
|
+
import { existsSync as existsSync25, statSync as statSync3 } from "node:fs";
|
|
13918
|
+
import { resolve as resolve13, join as join29 } from "node:path";
|
|
13573
13919
|
|
|
13574
13920
|
// src/lib/convert/types.ts
|
|
13575
13921
|
function num2(v, d = 0) {
|
|
@@ -14348,19 +14694,19 @@ function madJsx(opts) {
|
|
|
14348
14694
|
}
|
|
14349
14695
|
|
|
14350
14696
|
// src/lib/mad/scan.ts
|
|
14351
|
-
import { readdirSync, statSync as statSync2 } from "node:fs";
|
|
14352
|
-
import { extname as extname7, join as
|
|
14697
|
+
import { readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
|
|
14698
|
+
import { extname as extname7, join as join26 } from "node:path";
|
|
14353
14699
|
var VIDEO_EXTS2 = new Set(defaultExtsFor("video") ?? []);
|
|
14354
14700
|
function scanFolder(dirAbs, opts = {}) {
|
|
14355
14701
|
const probe = opts.probe ?? probeGeometry;
|
|
14356
14702
|
const warn = opts.warn ?? (() => {});
|
|
14357
14703
|
let entries;
|
|
14358
14704
|
try {
|
|
14359
|
-
entries =
|
|
14705
|
+
entries = readdirSync3(dirAbs);
|
|
14360
14706
|
} catch {
|
|
14361
14707
|
throw new Error(`素材文件夹不存在或无法读取:${dirAbs}`);
|
|
14362
14708
|
}
|
|
14363
|
-
const files = entries.filter((n) => VIDEO_EXTS2.has(extname7(n).toLowerCase())).map((n) =>
|
|
14709
|
+
const files = entries.filter((n) => VIDEO_EXTS2.has(extname7(n).toLowerCase())).map((n) => join26(dirAbs, n)).filter((p) => {
|
|
14364
14710
|
try {
|
|
14365
14711
|
return statSync2(p).isFile();
|
|
14366
14712
|
} catch {
|
|
@@ -14538,10 +14884,10 @@ function beatQuantized(natLens, analysis) {
|
|
|
14538
14884
|
}
|
|
14539
14885
|
|
|
14540
14886
|
// src/lib/mad/data.ts
|
|
14541
|
-
import { createHash as
|
|
14887
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
14542
14888
|
import { mkdir as mkdir11, readFile as readFile9, rename as rename2, rm, writeFile as writeFile11, readdir as readdir2 } from "node:fs/promises";
|
|
14543
|
-
import { existsSync as
|
|
14544
|
-
import { join as
|
|
14889
|
+
import { existsSync as existsSync23 } from "node:fs";
|
|
14890
|
+
import { join as join27 } from "node:path";
|
|
14545
14891
|
function madCacheDir() {
|
|
14546
14892
|
return homeFile("mad-cache");
|
|
14547
14893
|
}
|
|
@@ -14553,7 +14899,7 @@ function manifestUrl() {
|
|
|
14553
14899
|
}
|
|
14554
14900
|
var REQUIRED_KEYS = ["mad_pool"];
|
|
14555
14901
|
function sha256Hex(buf) {
|
|
14556
|
-
return
|
|
14902
|
+
return createHash3("sha256").update(buf).digest("hex");
|
|
14557
14903
|
}
|
|
14558
14904
|
async function fetchWithTimeout(fetchFn, url, timeoutMs) {
|
|
14559
14905
|
const ctrl = new AbortController;
|
|
@@ -14570,7 +14916,7 @@ async function atomicWrite(dest, data) {
|
|
|
14570
14916
|
await rename2(tmp, dest);
|
|
14571
14917
|
}
|
|
14572
14918
|
async function verifyFile(path, sha256) {
|
|
14573
|
-
if (!
|
|
14919
|
+
if (!existsSync23(path))
|
|
14574
14920
|
return false;
|
|
14575
14921
|
try {
|
|
14576
14922
|
const buf = await readFile9(path);
|
|
@@ -14598,7 +14944,7 @@ async function cleanupOldVersions(cacheRoot, keepVersion, warn) {
|
|
|
14598
14944
|
for (const e of entries) {
|
|
14599
14945
|
const m = /^v(\d+)$/.exec(e);
|
|
14600
14946
|
if (m && Number(m[1]) !== keepVersion) {
|
|
14601
|
-
await rm(
|
|
14947
|
+
await rm(join27(cacheRoot, e), { recursive: true, force: true }).catch(() => {});
|
|
14602
14948
|
}
|
|
14603
14949
|
}
|
|
14604
14950
|
} catch {}
|
|
@@ -14607,7 +14953,7 @@ async function ensureMadData(opts, deps) {
|
|
|
14607
14953
|
const { cacheRoot, warn } = deps;
|
|
14608
14954
|
const timeout = deps.manifestTimeoutMs ?? 8000;
|
|
14609
14955
|
const mfUrl = deps.manifestUrl ?? manifestUrl();
|
|
14610
|
-
const snapshotPath =
|
|
14956
|
+
const snapshotPath = join27(cacheRoot, "manifest.json");
|
|
14611
14957
|
let manifest = null;
|
|
14612
14958
|
let online = false;
|
|
14613
14959
|
try {
|
|
@@ -14622,7 +14968,7 @@ async function ensureMadData(opts, deps) {
|
|
|
14622
14968
|
warn(`manifest 拉取失败(${e instanceof Error ? e.message : String(e)}),回退本地缓存`);
|
|
14623
14969
|
}
|
|
14624
14970
|
if (!manifest) {
|
|
14625
|
-
if (!
|
|
14971
|
+
if (!existsSync23(snapshotPath)) {
|
|
14626
14972
|
throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
|
|
14627
14973
|
}
|
|
14628
14974
|
try {
|
|
@@ -14637,14 +14983,14 @@ async function ensureMadData(opts, deps) {
|
|
|
14637
14983
|
}
|
|
14638
14984
|
}
|
|
14639
14985
|
const version = manifest.version;
|
|
14640
|
-
const verDir =
|
|
14641
|
-
const poolPath =
|
|
14986
|
+
const verDir = join27(cacheRoot, `v${version}`);
|
|
14987
|
+
const poolPath = join27(verDir, "mad_pool.json");
|
|
14642
14988
|
const poolMeta = manifest.datasets.mad_pool;
|
|
14643
14989
|
const cacheValid = await verifyFile(poolPath, poolMeta.sha256);
|
|
14644
14990
|
const needDownload = !!opts.refresh || !cacheValid;
|
|
14645
14991
|
if (needDownload) {
|
|
14646
14992
|
if (!online) {
|
|
14647
|
-
if (
|
|
14993
|
+
if (existsSync23(poolPath)) {
|
|
14648
14994
|
throw new Error("本地技法数据缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
|
|
14649
14995
|
}
|
|
14650
14996
|
throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
|
|
@@ -14680,10 +15026,10 @@ async function ensureMadData(opts, deps) {
|
|
|
14680
15026
|
// src/lib/mad/pool.ts
|
|
14681
15027
|
import { gunzipSync } from "node:zlib";
|
|
14682
15028
|
import { mkdir as mkdir12, readFile as readFile10 } from "node:fs/promises";
|
|
14683
|
-
import { existsSync as
|
|
14684
|
-
import { join as
|
|
15029
|
+
import { existsSync as existsSync24 } from "node:fs";
|
|
15030
|
+
import { join as join28 } from "node:path";
|
|
14685
15031
|
function shardPath(verDir, shard) {
|
|
14686
|
-
return
|
|
15032
|
+
return join28(verDir, "ir", `${shard}.json.gz`);
|
|
14687
15033
|
}
|
|
14688
15034
|
function decodeShard(gz) {
|
|
14689
15035
|
const json = gunzipSync(gz).toString("utf8");
|
|
@@ -14700,7 +15046,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
|
|
|
14700
15046
|
if (cached)
|
|
14701
15047
|
return cached;
|
|
14702
15048
|
const path = shardPath(verDir, shard);
|
|
14703
|
-
if (
|
|
15049
|
+
if (existsSync24(path)) {
|
|
14704
15050
|
try {
|
|
14705
15051
|
const s2 = decodeShard(await readFile10(path));
|
|
14706
15052
|
memo.set(shard, s2);
|
|
@@ -14718,7 +15064,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
|
|
|
14718
15064
|
throw new Error(`IR 分片下载失败 HTTP ${res.status}:${url}`);
|
|
14719
15065
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
14720
15066
|
const s = decodeShard(buf);
|
|
14721
|
-
await mkdir12(
|
|
15067
|
+
await mkdir12(join28(verDir, "ir"), { recursive: true });
|
|
14722
15068
|
await atomicWrite(path, buf);
|
|
14723
15069
|
memo.set(shard, s);
|
|
14724
15070
|
return s;
|
|
@@ -14732,7 +15078,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
|
|
|
14732
15078
|
return ir;
|
|
14733
15079
|
},
|
|
14734
15080
|
shardCached(shard) {
|
|
14735
|
-
return memo.has(shard) ||
|
|
15081
|
+
return memo.has(shard) || existsSync24(shardPath(verDir, shard));
|
|
14736
15082
|
}
|
|
14737
15083
|
};
|
|
14738
15084
|
}
|
|
@@ -14816,7 +15162,7 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
14816
15162
|
if (!inputArg)
|
|
14817
15163
|
throw new Error("用法:gtrk tool mad <素材文件夹> [--bgm 歌.mp3]");
|
|
14818
15164
|
const dirAbs = resolve13(inputArg);
|
|
14819
|
-
if (!
|
|
15165
|
+
if (!existsSync25(dirAbs) || !statSync3(dirAbs).isDirectory()) {
|
|
14820
15166
|
throw new Error(`素材文件夹不存在或不是目录:${dirAbs}`);
|
|
14821
15167
|
}
|
|
14822
15168
|
const { videos, orientation, skipped } = scanFolder(dirAbs, {
|
|
@@ -14930,9 +15276,9 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
14930
15276
|
const header = madHeader(version, now.toISOString());
|
|
14931
15277
|
const bgm = level <= 2 && bgmForTrack ? { path: bgmForTrack, markers } : undefined;
|
|
14932
15278
|
const { jsx } = madJsx({ master, windows, header, bgm });
|
|
14933
|
-
const outDir = opts.out ? resolve13(opts.out) :
|
|
15279
|
+
const outDir = opts.out ? resolve13(opts.out) : join29(process.cwd(), `mad-${timestamp4(now)}`);
|
|
14934
15280
|
await mkdir13(outDir, { recursive: true });
|
|
14935
|
-
const jsxPath =
|
|
15281
|
+
const jsxPath = join29(outDir, "mad.jsx");
|
|
14936
15282
|
await writeFile12(jsxPath, jsx);
|
|
14937
15283
|
const result = {
|
|
14938
15284
|
ok: true,
|
|
@@ -14944,7 +15290,7 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
14944
15290
|
degradeLevel: level,
|
|
14945
15291
|
techniques: chosen.map((c3) => ({ uid: c3.entry.uid, pid: c3.entry.pid, cat: c3.entry.cat, t0: c3.entry.t0, t1: c3.entry.t1 }))
|
|
14946
15292
|
};
|
|
14947
|
-
await writeFile12(
|
|
15293
|
+
await writeFile12(join29(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: now.toISOString() }, null, 2));
|
|
14948
15294
|
warn(completionMessage(jsxPath, level));
|
|
14949
15295
|
return result;
|
|
14950
15296
|
}
|
|
@@ -15077,9 +15423,9 @@ async function runMadInTool(inputArg, opts) {
|
|
|
15077
15423
|
}
|
|
15078
15424
|
|
|
15079
15425
|
// src/commands/transcript.ts
|
|
15080
|
-
import { existsSync as
|
|
15426
|
+
import { existsSync as existsSync26 } from "node:fs";
|
|
15081
15427
|
import { mkdir as mkdir14, rename as rename3, rm as rm2, stat as stat6, writeFile as writeFile13 } from "node:fs/promises";
|
|
15082
|
-
import { basename as
|
|
15428
|
+
import { basename as basename15, dirname as dirname13, extname as extname8, join as join30, resolve as resolve14 } from "node:path";
|
|
15083
15429
|
|
|
15084
15430
|
// src/lib/transcript.ts
|
|
15085
15431
|
var AGENT_SUMMARY_PENDING = "<!-- gtrk:agent-summary-pending -->";
|
|
@@ -15216,7 +15562,7 @@ async function validateTranscriptInput(input) {
|
|
|
15216
15562
|
throw new Error("视频转文字稿仅支持本地视频文件,不支持 URL、平台视频地址或远端下载");
|
|
15217
15563
|
}
|
|
15218
15564
|
const inputAbs = resolve14(input);
|
|
15219
|
-
if (!
|
|
15565
|
+
if (!existsSync26(inputAbs))
|
|
15220
15566
|
throw new Error(`本地视频不存在:${inputAbs}`);
|
|
15221
15567
|
const info = await stat6(inputAbs);
|
|
15222
15568
|
if (!info.isFile())
|
|
@@ -15228,14 +15574,14 @@ async function validateTranscriptInput(input) {
|
|
|
15228
15574
|
return inputAbs;
|
|
15229
15575
|
}
|
|
15230
15576
|
function resolveTranscriptOutput(inputAbs, out) {
|
|
15231
|
-
const base =
|
|
15232
|
-
const output = out ? resolve14(out) :
|
|
15577
|
+
const base = basename15(inputAbs, extname8(inputAbs));
|
|
15578
|
+
const output = out ? resolve14(out) : join30(dirname13(inputAbs), `${base}-transcript.md`);
|
|
15233
15579
|
if (extname8(output).toLowerCase() !== ".md")
|
|
15234
15580
|
throw new Error("--out 必须指向一个 .md 文件");
|
|
15235
15581
|
return output;
|
|
15236
15582
|
}
|
|
15237
15583
|
async function writeMarkdownAtomic(path, markdown) {
|
|
15238
|
-
await mkdir14(
|
|
15584
|
+
await mkdir14(dirname13(path), { recursive: true });
|
|
15239
15585
|
const temp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
15240
15586
|
try {
|
|
15241
15587
|
await writeFile13(temp, markdown, "utf8");
|
|
@@ -15251,8 +15597,8 @@ async function runTranscript(input, opts = {}, depsOverride) {
|
|
|
15251
15597
|
const output = resolveTranscriptOutput(inputAbs, opts.out);
|
|
15252
15598
|
const deps = buildDeps(depsOverride);
|
|
15253
15599
|
const language = opts.lang?.trim() || "zh-CN";
|
|
15254
|
-
const sourceName =
|
|
15255
|
-
const title =
|
|
15600
|
+
const sourceName = basename15(inputAbs);
|
|
15601
|
+
const title = basename15(inputAbs, extname8(inputAbs));
|
|
15256
15602
|
log.step(`▶ 视频转文字稿:${sourceName}`);
|
|
15257
15603
|
log.step("① 本地探测视频…");
|
|
15258
15604
|
const geometry = deps.probe(inputAbs, opts.ffmpegPath);
|
|
@@ -15264,7 +15610,7 @@ async function runTranscript(input, opts = {}, depsOverride) {
|
|
|
15264
15610
|
log.step("② 本地抽取 16k 单声道音频(原视频不上传)…");
|
|
15265
15611
|
const audio = await deps.extract(inputAbs, opts.ffmpegPath);
|
|
15266
15612
|
deps.assertDuration(geometry.duration, audio, opts.ffmpegPath);
|
|
15267
|
-
log.info(`上传物:${
|
|
15613
|
+
log.info(`上传物:${basename15(audio)}(仅音频衍生物)`);
|
|
15268
15614
|
log.step("③ 上传音频并提交 ASR…");
|
|
15269
15615
|
const payload = (fileId) => ({ file_id: fileId, language, word_level: true });
|
|
15270
15616
|
const submitted = await uploadAndSubmitTask(deps.cfg, audio, TASK_TYPE4, payload, {
|
|
@@ -15312,9 +15658,9 @@ function registerTranscript(program2) {
|
|
|
15312
15658
|
}
|
|
15313
15659
|
|
|
15314
15660
|
// src/commands/music-visualizer.ts
|
|
15315
|
-
import { resolve as resolve15, join as
|
|
15661
|
+
import { resolve as resolve15, join as join31, dirname as dirname14, basename as basename16, extname as extname9 } from "node:path";
|
|
15316
15662
|
import { mkdir as mkdir15, writeFile as writeFile14 } from "node:fs/promises";
|
|
15317
|
-
import { existsSync as
|
|
15663
|
+
import { existsSync as existsSync27 } from "node:fs";
|
|
15318
15664
|
var TASK_TYPE5 = "music_visualizer";
|
|
15319
15665
|
var PRICE_KEY2 = "music_visualizer";
|
|
15320
15666
|
var HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
@@ -15362,7 +15708,7 @@ function timestamp5() {
|
|
|
15362
15708
|
return `${p(d.getFullYear() % 100)}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
15363
15709
|
}
|
|
15364
15710
|
function assertExt(pathAbs, exts, label) {
|
|
15365
|
-
if (!
|
|
15711
|
+
if (!existsSync27(pathAbs))
|
|
15366
15712
|
throw new Error(`${label}不存在:${pathAbs}`);
|
|
15367
15713
|
const e = extname9(pathAbs).toLowerCase();
|
|
15368
15714
|
if (exts.length && !exts.includes(e)) {
|
|
@@ -15431,9 +15777,9 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
15431
15777
|
if (coverAbs)
|
|
15432
15778
|
assertExt(coverAbs, IMAGE_EXTS2, "封面图");
|
|
15433
15779
|
const extraParams = parseExtraParams3(opts.param, opts.paramsJson);
|
|
15434
|
-
const projName =
|
|
15435
|
-
const outDir = resolve15(opts.out ??
|
|
15436
|
-
log.step(`▶ 音乐可视化:${
|
|
15780
|
+
const projName = basename16(audioAbs, extname9(audioAbs));
|
|
15781
|
+
const outDir = resolve15(opts.out ?? join31(dirname14(audioAbs), `${projName}-visualizer-${timestamp5()}`));
|
|
15782
|
+
log.step(`▶ 音乐可视化:${basename16(audioAbs)}(模板 ${template}${bgAbs ? " · 背景" : ""}${coverAbs ? " · 封面" : ""})`);
|
|
15437
15783
|
let billingHint;
|
|
15438
15784
|
try {
|
|
15439
15785
|
billingHint = (await resolveToolPricing(PRICE_KEY2)).billingHint;
|
|
@@ -15479,7 +15825,7 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
15479
15825
|
const { taskId } = submitted;
|
|
15480
15826
|
log.info(`task_id = ${taskId}`);
|
|
15481
15827
|
await mkdir15(outDir, { recursive: true });
|
|
15482
|
-
await writeFile14(
|
|
15828
|
+
await writeFile14(join31(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE5, fileId: submitted.fileId, source: audioAbs, template, createdAt: new Date().toISOString() }, null, 2));
|
|
15483
15829
|
log.step("③ 云端处理中(每 5s 轮询)…");
|
|
15484
15830
|
const result = await pollTask(cfg, TASK_TYPE5, taskId, (status, progress) => {
|
|
15485
15831
|
log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
|
|
@@ -15490,7 +15836,7 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
15490
15836
|
const files = [];
|
|
15491
15837
|
const errors = {};
|
|
15492
15838
|
if (url) {
|
|
15493
|
-
const dest =
|
|
15839
|
+
const dest = join31(outDir, `${projName}-visualizer${extFromUrl(url, ".mp4")}`);
|
|
15494
15840
|
try {
|
|
15495
15841
|
await downloadStream(url, dest);
|
|
15496
15842
|
files.push(dest);
|
|
@@ -15512,7 +15858,7 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
15512
15858
|
...Object.keys(errors).length ? { errors } : {},
|
|
15513
15859
|
finishedAt: new Date().toISOString()
|
|
15514
15860
|
};
|
|
15515
|
-
await writeFile14(
|
|
15861
|
+
await writeFile14(join31(outDir, "result.json"), JSON.stringify(resultJson, null, 2));
|
|
15516
15862
|
if (opts.json) {
|
|
15517
15863
|
process.stdout.write(`${JSON.stringify(resultJson)}
|
|
15518
15864
|
`);
|
|
@@ -15525,12 +15871,121 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
15525
15871
|
log.ok(`完成。产物目录:${outDir}`);
|
|
15526
15872
|
}
|
|
15527
15873
|
|
|
15874
|
+
// src/commands/deps.ts
|
|
15875
|
+
import { existsSync as existsSync28, readdirSync as readdirSync4, statSync as statSync4 } from "node:fs";
|
|
15876
|
+
import { join as join32 } from "node:path";
|
|
15877
|
+
function registerDeps(program2) {
|
|
15878
|
+
const deps = program2.command("deps").description("运行时资产:查看状态 / 显式安装 ffmpeg 与渲染字体");
|
|
15879
|
+
deps.command("status").description("查看 ffmpeg / 字体的当前来源、版本与授权信息").option("--ffmpeg-path <dir>", "显式 ffmpeg/ffprobe 目录").action(async (opts) => {
|
|
15880
|
+
await runStatus2(opts.ffmpegPath);
|
|
15881
|
+
});
|
|
15882
|
+
deps.command("install").description("从同合云镜像安装运行时资产(已存在则跳过)").option("--ffmpeg", "只装 ffmpeg/ffprobe").option("--font", "只装渲染字体").option("--force", "已存在也覆盖重装").action(async (opts) => {
|
|
15883
|
+
await runInstall(opts);
|
|
15884
|
+
});
|
|
15885
|
+
}
|
|
15886
|
+
function fmtMB(n) {
|
|
15887
|
+
return `${(n / 1048576).toFixed(1)} MB`;
|
|
15888
|
+
}
|
|
15889
|
+
async function runStatus2(ffmpegPath) {
|
|
15890
|
+
log.step("① ffmpeg / ffprobe");
|
|
15891
|
+
const res = resolveFfmpeg(ffmpegPath);
|
|
15892
|
+
if (res) {
|
|
15893
|
+
log.ok(`来源:${res.source}`);
|
|
15894
|
+
try {
|
|
15895
|
+
const cap = probeCapabilities(res);
|
|
15896
|
+
log.info(cap.version);
|
|
15897
|
+
log.info(`libx264 ${cap.hasLibx264 ? "✅" : "❌"} ass 滤镜 ${cap.filters.has("ass") ? "✅" : "❌"}`);
|
|
15898
|
+
} catch {
|
|
15899
|
+
log.warn("二进制存在但探测能力失败");
|
|
15900
|
+
}
|
|
15901
|
+
} else {
|
|
15902
|
+
log.warn(`未找到。装它:gtrk deps install --ffmpeg`);
|
|
15903
|
+
}
|
|
15904
|
+
log.step("② 渲染字体");
|
|
15905
|
+
const fdir = fontsDir();
|
|
15906
|
+
const fonts = existsSync28(fdir) ? readdirSync4(fdir).filter((f) => /\.(otf|ttf|ttc|otc)$/i.test(f)) : [];
|
|
15907
|
+
if (fonts.length) {
|
|
15908
|
+
log.ok(`${fdir}(${fonts.length} 个)`);
|
|
15909
|
+
for (const f of fonts)
|
|
15910
|
+
log.info(`${f} ${fmtMB(statSync4(join32(fdir, f)).size)}`);
|
|
15911
|
+
log.info("经 ass 滤镜 fontsdir 供给 libass,未安装进系统字体表");
|
|
15912
|
+
} else {
|
|
15913
|
+
log.warn(`${fdir} 无字体。装它:gtrk deps install --font`);
|
|
15914
|
+
log.info("(系统已装所需字体时也可直接用,两处任一命中即可)");
|
|
15915
|
+
}
|
|
15916
|
+
log.step("③ 分发物授权与源码");
|
|
15917
|
+
let m = null;
|
|
15918
|
+
try {
|
|
15919
|
+
m = await fetchManifest();
|
|
15920
|
+
} catch (e) {
|
|
15921
|
+
log.warn(`取 manifest 失败:${e instanceof Error ? e.message : String(e)}`);
|
|
15922
|
+
}
|
|
15923
|
+
if (m) {
|
|
15924
|
+
let key = "";
|
|
15925
|
+
try {
|
|
15926
|
+
key = resolvePlatformKey();
|
|
15927
|
+
} catch {}
|
|
15928
|
+
for (const [k, v] of Object.entries(m.ffmpeg)) {
|
|
15929
|
+
const mark = k === key ? "→" : " ";
|
|
15930
|
+
log.info(`${mark} ${k.padEnd(10)} ${v.version.padEnd(20)} ${v.license} 源码 ${v.source}`);
|
|
15931
|
+
}
|
|
15932
|
+
for (const [name, v] of Object.entries(m.font)) {
|
|
15933
|
+
log.info(` 字体 ${name} ${v.license}`);
|
|
15934
|
+
}
|
|
15935
|
+
log.info(`合规说明:${m.base}/SOURCE.md`);
|
|
15936
|
+
log.info("分发不附加任何使用限制(不限 gtrk 用户、不禁转发)");
|
|
15937
|
+
}
|
|
15938
|
+
}
|
|
15939
|
+
async function runInstall(opts) {
|
|
15940
|
+
const doFfmpeg = opts.ffmpeg || !opts.font;
|
|
15941
|
+
const doFont = opts.font || !opts.ffmpeg;
|
|
15942
|
+
log.step(`读 manifest:${MANIFEST_URL}`);
|
|
15943
|
+
const m = await fetchManifest();
|
|
15944
|
+
log.info(`schema ${m.schema},生成于 ${m.generated}`);
|
|
15945
|
+
let lastPct = -1;
|
|
15946
|
+
let lastAt = 0;
|
|
15947
|
+
const onProgress = (got, total) => {
|
|
15948
|
+
if (total <= 0)
|
|
15949
|
+
return;
|
|
15950
|
+
const pct = Math.floor(got / total * 100);
|
|
15951
|
+
const now = Date.now();
|
|
15952
|
+
if (pct < 100 && pct - lastPct < 5 && now - lastAt < 2000)
|
|
15953
|
+
return;
|
|
15954
|
+
lastPct = pct;
|
|
15955
|
+
lastAt = now;
|
|
15956
|
+
log.tick(`下载 ${fmtMB(got)} / ${fmtMB(total)}(${pct}%)`);
|
|
15957
|
+
};
|
|
15958
|
+
if (doFfmpeg) {
|
|
15959
|
+
const key = resolvePlatformKey();
|
|
15960
|
+
log.step(`① ffmpeg(${key})`);
|
|
15961
|
+
const r = await installFfmpeg(m, { force: opts.force, onProgress });
|
|
15962
|
+
log.tickEnd();
|
|
15963
|
+
if (r.installed)
|
|
15964
|
+
log.ok(`已安装 ${r.detail}`);
|
|
15965
|
+
else
|
|
15966
|
+
log.info(`跳过(${r.reason}):${r.detail}`);
|
|
15967
|
+
}
|
|
15968
|
+
if (doFont) {
|
|
15969
|
+
log.step("② 渲染字体");
|
|
15970
|
+
const rs = await installFont(m, { force: opts.force, onProgress });
|
|
15971
|
+
log.tickEnd();
|
|
15972
|
+
for (const r of rs) {
|
|
15973
|
+
if (r.installed)
|
|
15974
|
+
log.ok(`已安装 ${r.detail}`);
|
|
15975
|
+
else
|
|
15976
|
+
log.info(`跳过(${r.reason}):${r.detail}`);
|
|
15977
|
+
}
|
|
15978
|
+
log.info("未写系统字体表、未改注册表——烧录时经 ass 滤镜 fontsdir 供给");
|
|
15979
|
+
}
|
|
15980
|
+
log.ok(`完成。二进制在 ${ffmpegDir()},字体在 ${fontsDir()}`);
|
|
15981
|
+
}
|
|
15982
|
+
|
|
15528
15983
|
// src/index.ts
|
|
15529
15984
|
try {
|
|
15530
15985
|
process.loadEnvFile?.();
|
|
15531
15986
|
} catch {}
|
|
15532
15987
|
migrateLegacyHome();
|
|
15533
|
-
var { version } = JSON.parse(readFileSync6(
|
|
15988
|
+
var { version } = JSON.parse(readFileSync6(join33(packageRoot(), "package.json"), "utf8"));
|
|
15534
15989
|
var program2 = new Command;
|
|
15535
15990
|
program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
|
|
15536
15991
|
registerInstall(program2);
|
|
@@ -15548,6 +16003,7 @@ registerMg(program2);
|
|
|
15548
16003
|
registerTool(program2);
|
|
15549
16004
|
registerTranscript(program2);
|
|
15550
16005
|
registerMusicVisualizer(program2);
|
|
16006
|
+
registerDeps(program2);
|
|
15551
16007
|
program2.parseAsync(process.argv).catch((e) => {
|
|
15552
16008
|
console.error(`
|
|
15553
16009
|
❌ ${e instanceof Error ? e.message : String(e)}`);
|