|
@@ -1,6 +1,7 @@
|
|
|
import { Command } from "commander";
|
|
import { Command } from "commander";
|
|
|
-import { runPipeline, type PipelineConfig } from "@pipeline/core";
|
|
|
|
|
|
|
+import { runPipeline, resolvePublishConfig, type PipelineConfig } from "@pipeline/core";
|
|
|
import { PLATFORM_PRESETS, TEMPLATE_TYPES, PLATFORM_PRESET_KEYS } from "@pipeline/shared";
|
|
import { PLATFORM_PRESETS, TEMPLATE_TYPES, PLATFORM_PRESET_KEYS } from "@pipeline/shared";
|
|
|
|
|
+import { resolveOutputDir } from "@pipeline/shared/node";
|
|
|
import { getCollector, listCollectorNames } from "@pipeline/collect";
|
|
import { getCollector, listCollectorNames } from "@pipeline/collect";
|
|
|
import { readFileSync, existsSync } from "node:fs";
|
|
import { readFileSync, existsSync } from "node:fs";
|
|
|
import { resolve, dirname } from "node:path";
|
|
import { resolve, dirname } from "node:path";
|
|
@@ -8,13 +9,15 @@ import { parse as parseYaml } from "yaml";
|
|
|
|
|
|
|
|
// Commander --no-tts sets opts.tts = false (not opts.noTts)
|
|
// Commander --no-tts sets opts.tts = false (not opts.noTts)
|
|
|
const skipTts = (opts: any) => opts.tts === false;
|
|
const skipTts = (opts: any) => opts.tts === false;
|
|
|
|
|
+// Commander --no-publish sets opts.publish = false
|
|
|
|
|
+const skipPublish = (opts: any) => opts.publish === false;
|
|
|
|
|
|
|
|
export const renderCommand = new Command("render")
|
|
export const renderCommand = new Command("render")
|
|
|
.description("Generate video from text, markdown, or structured JSON input")
|
|
.description("Generate video from text, markdown, or structured JSON input")
|
|
|
.argument("[input]", "Input file path (JSON, markdown, or plain text). Required unless --source is used.")
|
|
.argument("[input]", "Input file path (JSON, markdown, or plain text). Required unless --source is used.")
|
|
|
.requiredOption("-t, --template <type>", `Template: ${TEMPLATE_TYPES.join("|")}`)
|
|
.requiredOption("-t, --template <type>", `Template: ${TEMPLATE_TYPES.join("|")}`)
|
|
|
.option("-p, --platform <preset>", "Platform preset", "bilibili")
|
|
.option("-p, --platform <preset>", "Platform preset", "bilibili")
|
|
|
- .option("-o, --output <dir>", "Output directory", "./output")
|
|
|
|
|
|
|
+ .option("-o, --output <dir>", "Output directory (default: config output.dir or ./output)")
|
|
|
.option("--assets-dir <dir>", "Image assets directory (defaults to JSON file location)")
|
|
.option("--assets-dir <dir>", "Image assets directory (defaults to JSON file location)")
|
|
|
.option("--config <path>", "Config file path")
|
|
.option("--config <path>", "Config file path")
|
|
|
.option("-v, --verbose", "Verbose logging")
|
|
.option("-v, --verbose", "Verbose logging")
|
|
@@ -38,6 +41,7 @@ export const renderCommand = new Command("render")
|
|
|
.option("--tts-format <format>", "TTS audio format (mp3|wav|pcm)")
|
|
.option("--tts-format <format>", "TTS audio format (mp3|wav|pcm)")
|
|
|
.option("--no-tts", "Skip TTS (generate silent video)")
|
|
.option("--no-tts", "Skip TTS (generate silent video)")
|
|
|
.option("--channel-name <name>", "Channel name displayed in video watermark")
|
|
.option("--channel-name <name>", "Channel name displayed in video watermark")
|
|
|
|
|
+ .option("--no-publish", "Skip OSS upload + Feishu notification")
|
|
|
|
|
|
|
|
// Alignment
|
|
// Alignment
|
|
|
.option("--alignment <mode>", "Timestamp alignment: whisper|native")
|
|
.option("--alignment <mode>", "Timestamp alignment: whisper|native")
|
|
@@ -106,8 +110,17 @@ export const renderCommand = new Command("render")
|
|
|
const assetsRoot = resolve(projectRoot, "assets");
|
|
const assetsRoot = resolve(projectRoot, "assets");
|
|
|
|
|
|
|
|
const noTts = skipTts(opts);
|
|
const noTts = skipTts(opts);
|
|
|
|
|
+ const noPublish = skipPublish(opts);
|
|
|
const flags: string[] = [];
|
|
const flags: string[] = [];
|
|
|
if (noTts) flags.push("no-tts");
|
|
if (noTts) flags.push("no-tts");
|
|
|
|
|
+ if (noPublish) flags.push("no-publish");
|
|
|
|
|
+
|
|
|
|
|
+ // Unified output directory (honors OUTPUT_DIR env > config > ./output).
|
|
|
|
|
+ const outputDir = resolveOutputDir(opts.output || config?.output?.dir);
|
|
|
|
|
+ const retentionDays = Number(
|
|
|
|
|
+ process.env.OUTPUT_RETENTION_DAYS ?? config?.output?.retentionDays ?? 30
|
|
|
|
|
+ );
|
|
|
|
|
+ const publish = noPublish ? undefined : resolvePublishConfig(config);
|
|
|
|
|
|
|
|
const pipelineConfig: PipelineConfig = {
|
|
const pipelineConfig: PipelineConfig = {
|
|
|
branding: {
|
|
branding: {
|
|
@@ -126,8 +139,11 @@ export const renderCommand = new Command("render")
|
|
|
speed: opts.ttsSpeed || config?.tts?.speed || 1.0,
|
|
speed: opts.ttsSpeed || config?.tts?.speed || 1.0,
|
|
|
},
|
|
},
|
|
|
output: {
|
|
output: {
|
|
|
- dir: resolve(opts.output || config?.output?.dir || "./output"),
|
|
|
|
|
|
|
+ dir: outputDir,
|
|
|
|
|
+ retentionDays,
|
|
|
},
|
|
},
|
|
|
|
|
+ publish,
|
|
|
|
|
+ skipPublish: noPublish,
|
|
|
assets: { root: assetsRoot, inputDir },
|
|
assets: { root: assetsRoot, inputDir },
|
|
|
templates: { entryPoint: templatesEntry },
|
|
templates: { entryPoint: templatesEntry },
|
|
|
skipTts: noTts,
|
|
skipTts: noTts,
|
|
@@ -135,7 +151,7 @@ export const renderCommand = new Command("render")
|
|
|
alignment: buildAlignmentConfig(opts, config),
|
|
alignment: buildAlignmentConfig(opts, config),
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const stages = ["parse", "tts", "assets", "compose", "render", "export"];
|
|
|
|
|
|
|
+ const stages = ["parse", "tts", "assets", "compose", "render", "export", "publish"];
|
|
|
|
|
|
|
|
console.log(`\nPipeline: generating ${template} video for ${platforms.join(", ")}`);
|
|
console.log(`\nPipeline: generating ${template} video for ${platforms.join(", ")}`);
|
|
|
console.log(`Input: ${text.length} chars${flags.length ? " | Flags: " + flags.join(", ") : ""}\n`);
|
|
console.log(`Input: ${text.length} chars${flags.length ? " | Flags: " + flags.join(", ") : ""}\n`);
|
|
@@ -147,7 +163,7 @@ export const renderCommand = new Command("render")
|
|
|
platforms: platforms as any,
|
|
platforms: platforms as any,
|
|
|
ttsProvider: providerName as any,
|
|
ttsProvider: providerName as any,
|
|
|
voiceId: pipelineConfig.tts.voiceId,
|
|
voiceId: pipelineConfig.tts.voiceId,
|
|
|
- outputPath: resolve(opts.output || "./output"),
|
|
|
|
|
|
|
+ outputPath: outputDir,
|
|
|
source: opts.source,
|
|
source: opts.source,
|
|
|
},
|
|
},
|
|
|
pipelineConfig,
|
|
pipelineConfig,
|
|
@@ -162,15 +178,16 @@ export const renderCommand = new Command("render")
|
|
|
compose: "Composing scenes",
|
|
compose: "Composing scenes",
|
|
|
render: "Rendering video",
|
|
render: "Rendering video",
|
|
|
export: "Exporting",
|
|
export: "Exporting",
|
|
|
|
|
+ publish: "Publishing (OSS + Feishu)",
|
|
|
};
|
|
};
|
|
|
const stepNum = stages.indexOf(name as string) + 1;
|
|
const stepNum = stages.indexOf(name as string) + 1;
|
|
|
- console.log(` [${stepNum}/6] ${labels[name] || name}${platformLabel}...`);
|
|
|
|
|
|
|
+ console.log(` [${stepNum}/${stages.length}] ${labels[name] || name}${platformLabel}...`);
|
|
|
},
|
|
},
|
|
|
onStageComplete: (stage) => {
|
|
onStageComplete: (stage) => {
|
|
|
const [name, plat] = stage.split(":");
|
|
const [name, plat] = stage.split(":");
|
|
|
const platformLabel = plat ? ` [${plat}]` : "";
|
|
const platformLabel = plat ? ` [${plat}]` : "";
|
|
|
const stepNum = stages.indexOf(name as string) + 1;
|
|
const stepNum = stages.indexOf(name as string) + 1;
|
|
|
- console.log(` [${stepNum}/6] ${name}${platformLabel} done`);
|
|
|
|
|
|
|
+ console.log(` [${stepNum}/${stages.length}] ${name}${platformLabel} done`);
|
|
|
},
|
|
},
|
|
|
onError: (stage, error) => {
|
|
onError: (stage, error) => {
|
|
|
console.error(`\n Error in ${stage}: ${error.message}`);
|
|
console.error(`\n Error in ${stage}: ${error.message}`);
|
|
@@ -186,6 +203,10 @@ export const renderCommand = new Command("render")
|
|
|
console.log(`\nCompleted! Job ID: ${job.id}`);
|
|
console.log(`\nCompleted! Job ID: ${job.id}`);
|
|
|
for (const file of job.exported.files) {
|
|
for (const file of job.exported.files) {
|
|
|
console.log(` -> ${file.filePath} (${(file.fileSizeBytes / 1024 / 1024).toFixed(1)}MB, ${file.width}x${file.height})`);
|
|
console.log(` -> ${file.filePath} (${(file.fileSizeBytes / 1024 / 1024).toFixed(1)}MB, ${file.width}x${file.height})`);
|
|
|
|
|
+ if (file.ossUrl) console.log(` oss: ${file.ossUrl}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (job.publishError) {
|
|
|
|
|
+ console.error(`\nPublish warning: ${job.publishError}`);
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
console.error(`\nFailed: ${job.error}`);
|
|
console.error(`\nFailed: ${job.error}`);
|