vidpipe 1.3.14 → 1.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1191 -105
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +460 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -538,6 +538,11 @@ function resolveConfig(cliOptions = {}) {
|
|
|
538
538
|
process.env.SKIP_VISUAL_ENHANCEMENT,
|
|
539
539
|
false
|
|
540
540
|
),
|
|
541
|
+
SKIP_INTRO_OUTRO: resolveBoolean(
|
|
542
|
+
cliOptions.introOutro === void 0 ? void 0 : !cliOptions.introOutro,
|
|
543
|
+
process.env.SKIP_INTRO_OUTRO,
|
|
544
|
+
false
|
|
545
|
+
),
|
|
541
546
|
LATE_API_KEY: resolveString(
|
|
542
547
|
cliOptions.lateApiKey,
|
|
543
548
|
process.env.LATE_API_KEY,
|
|
@@ -742,15 +747,16 @@ var init_types = __esm({
|
|
|
742
747
|
{ stage: "visual-enhancement" /* VisualEnhancement */, name: "Visual Enhancement", stageNumber: 4 },
|
|
743
748
|
{ stage: "captions" /* Captions */, name: "Captions", stageNumber: 5 },
|
|
744
749
|
{ stage: "caption-burn" /* CaptionBurn */, name: "Caption Burn", stageNumber: 6 },
|
|
745
|
-
{ stage: "
|
|
746
|
-
{ stage: "
|
|
747
|
-
{ stage: "
|
|
748
|
-
{ stage: "
|
|
749
|
-
{ stage: "
|
|
750
|
-
{ stage: "
|
|
751
|
-
{ stage: "
|
|
752
|
-
{ stage: "
|
|
753
|
-
{ stage: "
|
|
750
|
+
{ stage: "intro-outro" /* IntroOutro */, name: "Intro/Outro", stageNumber: 7 },
|
|
751
|
+
{ stage: "shorts" /* Shorts */, name: "Shorts", stageNumber: 8 },
|
|
752
|
+
{ stage: "medium-clips" /* MediumClips */, name: "Medium Clips", stageNumber: 9 },
|
|
753
|
+
{ stage: "chapters" /* Chapters */, name: "Chapters", stageNumber: 10 },
|
|
754
|
+
{ stage: "summary" /* Summary */, name: "Summary", stageNumber: 11 },
|
|
755
|
+
{ stage: "social-media" /* SocialMedia */, name: "Social Media", stageNumber: 12 },
|
|
756
|
+
{ stage: "short-posts" /* ShortPosts */, name: "Short Posts", stageNumber: 13 },
|
|
757
|
+
{ stage: "medium-clip-posts" /* MediumClipPosts */, name: "Medium Clip Posts", stageNumber: 14 },
|
|
758
|
+
{ stage: "queue-build" /* QueueBuild */, name: "Queue Build", stageNumber: 15 },
|
|
759
|
+
{ stage: "blog" /* Blog */, name: "Blog", stageNumber: 16 }
|
|
754
760
|
];
|
|
755
761
|
TOTAL_STAGES = PIPELINE_STAGES.length;
|
|
756
762
|
PLATFORM_CHAR_LIMITS = {
|
|
@@ -4682,7 +4688,7 @@ var init_session = __esm({
|
|
|
4682
4688
|
import { spawn } from "child_process";
|
|
4683
4689
|
import { existsSync as existsSync4 } from "fs";
|
|
4684
4690
|
import { Socket } from "net";
|
|
4685
|
-
import { dirname as dirname3, join as
|
|
4691
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
4686
4692
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4687
4693
|
function isZodSchema(value) {
|
|
4688
4694
|
return value != null && typeof value === "object" && "toJSONSchema" in value && typeof value.toJSONSchema === "function";
|
|
@@ -4703,7 +4709,7 @@ function getNodeExecPath() {
|
|
|
4703
4709
|
function getBundledCliPath() {
|
|
4704
4710
|
const sdkUrl = import.meta.resolve("@github/copilot/sdk");
|
|
4705
4711
|
const sdkPath = fileURLToPath3(sdkUrl);
|
|
4706
|
-
return
|
|
4712
|
+
return join6(dirname3(dirname3(sdkPath)), "index.js");
|
|
4707
4713
|
}
|
|
4708
4714
|
var import_node2, MIN_PROTOCOL_VERSION, CopilotClient;
|
|
4709
4715
|
var init_client = __esm({
|
|
@@ -5868,7 +5874,7 @@ var init_dist = __esm({
|
|
|
5868
5874
|
|
|
5869
5875
|
// src/L1-infra/ai/copilot.ts
|
|
5870
5876
|
import { existsSync as existsSync5 } from "fs";
|
|
5871
|
-
import { join as
|
|
5877
|
+
import { join as join7, dirname as dirname4 } from "path";
|
|
5872
5878
|
import { createRequire as createRequire2 } from "module";
|
|
5873
5879
|
function resolveCopilotCliPath() {
|
|
5874
5880
|
const platform = process.platform;
|
|
@@ -5879,14 +5885,14 @@ function resolveCopilotCliPath() {
|
|
|
5879
5885
|
const require_ = createRequire2(import.meta.url);
|
|
5880
5886
|
const searchPaths = require_.resolve.paths(platformPkg) ?? [];
|
|
5881
5887
|
for (const base of searchPaths) {
|
|
5882
|
-
const candidate =
|
|
5888
|
+
const candidate = join7(base, platformPkg, binaryName);
|
|
5883
5889
|
if (existsSync5(candidate)) return candidate;
|
|
5884
5890
|
}
|
|
5885
5891
|
} catch {
|
|
5886
5892
|
}
|
|
5887
5893
|
let dir = dirname4(import.meta.dirname ?? __dirname);
|
|
5888
5894
|
for (let i = 0; i < 10; i++) {
|
|
5889
|
-
const candidate =
|
|
5895
|
+
const candidate = join7(dir, "node_modules", platformPkg, binaryName);
|
|
5890
5896
|
if (existsSync5(candidate)) return candidate;
|
|
5891
5897
|
const parent = dirname4(dir);
|
|
5892
5898
|
if (parent === dir) break;
|
|
@@ -6795,7 +6801,7 @@ var init_githubClient = __esm({
|
|
|
6795
6801
|
const response = await this.octokit.rest.issues.listForRepo({
|
|
6796
6802
|
owner: this.owner,
|
|
6797
6803
|
repo: this.repo,
|
|
6798
|
-
state: "
|
|
6804
|
+
state: options.state ?? "all",
|
|
6799
6805
|
labels: options.labels && options.labels.length > 0 ? normalizeLabels(options.labels).join(",") : void 0,
|
|
6800
6806
|
sort: void 0,
|
|
6801
6807
|
direction: void 0,
|
|
@@ -8708,7 +8714,25 @@ async function rescheduleIdeaPosts(options) {
|
|
|
8708
8714
|
fullBookedMap.delete(ms);
|
|
8709
8715
|
}
|
|
8710
8716
|
}
|
|
8711
|
-
|
|
8717
|
+
const { getIdea: getIdea2 } = await Promise.resolve().then(() => (init_ideaService(), ideaService_exports));
|
|
8718
|
+
const ideaPublishByMap = /* @__PURE__ */ new Map();
|
|
8719
|
+
for (const item of ideaPosts) {
|
|
8720
|
+
const ideaId = item.metadata.ideaIds?.[0];
|
|
8721
|
+
if (ideaId && !ideaPublishByMap.has(ideaId)) {
|
|
8722
|
+
try {
|
|
8723
|
+
const idea = await getIdea2(parseInt(ideaId, 10));
|
|
8724
|
+
if (idea?.publishBy) ideaPublishByMap.set(ideaId, idea.publishBy);
|
|
8725
|
+
} catch {
|
|
8726
|
+
}
|
|
8727
|
+
}
|
|
8728
|
+
}
|
|
8729
|
+
ideaPosts.sort((a, b) => {
|
|
8730
|
+
const aId = a.metadata.ideaIds?.[0];
|
|
8731
|
+
const bId = b.metadata.ideaIds?.[0];
|
|
8732
|
+
const aDate = aId ? ideaPublishByMap.get(aId) ?? "9999" : "9999";
|
|
8733
|
+
const bDate = bId ? ideaPublishByMap.get(bId) ?? "9999" : "9999";
|
|
8734
|
+
return aDate.localeCompare(bDate);
|
|
8735
|
+
});
|
|
8712
8736
|
const lateClient = new LateApiClient();
|
|
8713
8737
|
const result = { rescheduled: 0, unchanged: 0, failed: 0, details: [] };
|
|
8714
8738
|
const nowMs = Date.now();
|
|
@@ -8758,7 +8782,18 @@ async function rescheduleIdeaPosts(options) {
|
|
|
8758
8782
|
continue;
|
|
8759
8783
|
}
|
|
8760
8784
|
if (!dryRun) {
|
|
8761
|
-
|
|
8785
|
+
try {
|
|
8786
|
+
await lateClient.schedulePost(latePostId, newSlotDatetime);
|
|
8787
|
+
} catch (scheduleErr) {
|
|
8788
|
+
const errMsg = scheduleErr instanceof Error ? scheduleErr.message : String(scheduleErr);
|
|
8789
|
+
if (errMsg.includes("Published posts can only have their recycling config updated")) {
|
|
8790
|
+
logger_default.info(`Skipping ${label}: post already published on platform`);
|
|
8791
|
+
result.details.push({ itemId: item.id, platform, latePostId, oldSlot, newSlot: null, error: "Already published \u2014 skipped" });
|
|
8792
|
+
result.unchanged++;
|
|
8793
|
+
continue;
|
|
8794
|
+
}
|
|
8795
|
+
throw scheduleErr;
|
|
8796
|
+
}
|
|
8762
8797
|
await updatePublishedItemSchedule2(item.id, newSlotDatetime);
|
|
8763
8798
|
}
|
|
8764
8799
|
ctx.bookedMap.set(newSlotMs, {
|
|
@@ -9323,6 +9358,17 @@ import { default as default3 } from "fluent-ffmpeg";
|
|
|
9323
9358
|
// src/L1-infra/process/process.ts
|
|
9324
9359
|
import { execFile as nodeExecFile, execSync as nodeExecSync, spawnSync as nodeSpawnSync } from "child_process";
|
|
9325
9360
|
import { createRequire } from "module";
|
|
9361
|
+
function execCommand(cmd, args, opts) {
|
|
9362
|
+
return new Promise((resolve3, reject) => {
|
|
9363
|
+
nodeExecFile(cmd, args, { ...opts, encoding: "utf-8" }, (error, stdout, stderr) => {
|
|
9364
|
+
if (error) {
|
|
9365
|
+
reject(Object.assign(error, { stdout: String(stdout ?? ""), stderr: String(stderr ?? "") }));
|
|
9366
|
+
} else {
|
|
9367
|
+
resolve3({ stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
|
|
9368
|
+
}
|
|
9369
|
+
});
|
|
9370
|
+
});
|
|
9371
|
+
}
|
|
9326
9372
|
function execFileRaw(cmd, args, opts, callback) {
|
|
9327
9373
|
nodeExecFile(cmd, args, { ...opts, encoding: "utf-8" }, (error, stdout, stderr) => {
|
|
9328
9374
|
callback(error, String(stdout ?? ""), String(stderr ?? ""));
|
|
@@ -10617,6 +10663,376 @@ function transcodeToMp42(...args) {
|
|
|
10617
10663
|
return transcodeToMp4(...args);
|
|
10618
10664
|
}
|
|
10619
10665
|
|
|
10666
|
+
// src/L2-clients/ffmpeg/videoConcat.ts
|
|
10667
|
+
init_fileSystem();
|
|
10668
|
+
init_paths();
|
|
10669
|
+
init_configLogger();
|
|
10670
|
+
async function concatVideos(segments, output, opts = {}) {
|
|
10671
|
+
if (segments.length === 0) {
|
|
10672
|
+
throw new Error("concatVideos: no segments provided");
|
|
10673
|
+
}
|
|
10674
|
+
if (segments.length === 1) {
|
|
10675
|
+
await ensureDirectory(dirname(output));
|
|
10676
|
+
await execCommand(getFFmpegPath(), [
|
|
10677
|
+
"-y",
|
|
10678
|
+
"-i",
|
|
10679
|
+
segments[0],
|
|
10680
|
+
"-c",
|
|
10681
|
+
"copy",
|
|
10682
|
+
output
|
|
10683
|
+
], { maxBuffer: 50 * 1024 * 1024 });
|
|
10684
|
+
return output;
|
|
10685
|
+
}
|
|
10686
|
+
const fadeDuration = opts.fadeDuration ?? 0;
|
|
10687
|
+
if (fadeDuration > 0) {
|
|
10688
|
+
return concatWithXfade(segments, output, fadeDuration);
|
|
10689
|
+
}
|
|
10690
|
+
return concatWithDemuxer(segments, output);
|
|
10691
|
+
}
|
|
10692
|
+
async function concatWithDemuxer(segments, output) {
|
|
10693
|
+
await ensureDirectory(dirname(output));
|
|
10694
|
+
const listContent = segments.map((s) => `file '${s.replace(/'/g, "'\\''")}'`).join("\n");
|
|
10695
|
+
const listPath = output + ".concat-list.txt";
|
|
10696
|
+
await writeTextFile(listPath, listContent);
|
|
10697
|
+
logger_default.info(`Concat (demuxer): ${segments.length} segments \u2192 ${output}`);
|
|
10698
|
+
await execCommand(getFFmpegPath(), [
|
|
10699
|
+
"-y",
|
|
10700
|
+
"-f",
|
|
10701
|
+
"concat",
|
|
10702
|
+
"-safe",
|
|
10703
|
+
"0",
|
|
10704
|
+
"-i",
|
|
10705
|
+
listPath,
|
|
10706
|
+
"-c",
|
|
10707
|
+
"copy",
|
|
10708
|
+
"-movflags",
|
|
10709
|
+
"+faststart",
|
|
10710
|
+
output
|
|
10711
|
+
], { maxBuffer: 50 * 1024 * 1024 });
|
|
10712
|
+
return output;
|
|
10713
|
+
}
|
|
10714
|
+
async function concatWithXfade(segments, output, fadeDuration) {
|
|
10715
|
+
await ensureDirectory(dirname(output));
|
|
10716
|
+
logger_default.info(`Concat (xfade ${fadeDuration}s): ${segments.length} segments \u2192 ${output}`);
|
|
10717
|
+
const durations = await Promise.all(segments.map((s) => getVideoDuration2(s)));
|
|
10718
|
+
const inputs = segments.flatMap((s) => ["-i", s]);
|
|
10719
|
+
const filterParts = [];
|
|
10720
|
+
for (let i = 0; i < segments.length; i++) {
|
|
10721
|
+
filterParts.push(`[${i}:v]fps=30,settb=AVTB,setpts=PTS-STARTPTS[vin${i}]`);
|
|
10722
|
+
filterParts.push(`[${i}:a]aresample=async=1,asetpts=PTS-STARTPTS[ain${i}]`);
|
|
10723
|
+
}
|
|
10724
|
+
let prevLabel = "[vin0]";
|
|
10725
|
+
let prevAudioLabel = "[ain0]";
|
|
10726
|
+
let cumulativeOffset = 0;
|
|
10727
|
+
for (let i = 1; i < segments.length; i++) {
|
|
10728
|
+
const offset = cumulativeOffset + durations[i - 1] - fadeDuration;
|
|
10729
|
+
const outLabel = i < segments.length - 1 ? `[v${i}]` : "[vout]";
|
|
10730
|
+
const outAudioLabel = i < segments.length - 1 ? `[a${i}]` : "[aout]";
|
|
10731
|
+
filterParts.push(
|
|
10732
|
+
`${prevLabel}[vin${i}]xfade=transition=fade:duration=${fadeDuration}:offset=${offset.toFixed(3)}${outLabel}`
|
|
10733
|
+
);
|
|
10734
|
+
filterParts.push(
|
|
10735
|
+
`${prevAudioLabel}[ain${i}]acrossfade=d=${fadeDuration}${outAudioLabel}`
|
|
10736
|
+
);
|
|
10737
|
+
prevLabel = outLabel;
|
|
10738
|
+
prevAudioLabel = outAudioLabel;
|
|
10739
|
+
cumulativeOffset = offset;
|
|
10740
|
+
}
|
|
10741
|
+
const filterComplex = filterParts.join(";");
|
|
10742
|
+
await execCommand(getFFmpegPath(), [
|
|
10743
|
+
"-y",
|
|
10744
|
+
...inputs,
|
|
10745
|
+
"-filter_complex",
|
|
10746
|
+
filterComplex,
|
|
10747
|
+
"-map",
|
|
10748
|
+
"[vout]",
|
|
10749
|
+
"-map",
|
|
10750
|
+
"[aout]",
|
|
10751
|
+
"-c:v",
|
|
10752
|
+
"libx264",
|
|
10753
|
+
"-pix_fmt",
|
|
10754
|
+
"yuv420p",
|
|
10755
|
+
"-preset",
|
|
10756
|
+
"ultrafast",
|
|
10757
|
+
"-crf",
|
|
10758
|
+
"23",
|
|
10759
|
+
"-c:a",
|
|
10760
|
+
"aac",
|
|
10761
|
+
"-b:a",
|
|
10762
|
+
"128k",
|
|
10763
|
+
"-movflags",
|
|
10764
|
+
"+faststart",
|
|
10765
|
+
output
|
|
10766
|
+
], { maxBuffer: 50 * 1024 * 1024 });
|
|
10767
|
+
return output;
|
|
10768
|
+
}
|
|
10769
|
+
async function normalizeForConcat(videoPath, referenceVideo, output) {
|
|
10770
|
+
await ensureDirectory(dirname(output));
|
|
10771
|
+
const refProps = await getVideoProperties(referenceVideo);
|
|
10772
|
+
logger_default.info(`Normalizing ${videoPath} to match ${referenceVideo} (${refProps.width}x${refProps.height} ${refProps.fps}fps)`);
|
|
10773
|
+
await execCommand(getFFmpegPath(), [
|
|
10774
|
+
"-y",
|
|
10775
|
+
"-i",
|
|
10776
|
+
videoPath,
|
|
10777
|
+
"-vf",
|
|
10778
|
+
`scale=${refProps.width}:${refProps.height}:force_original_aspect_ratio=decrease,pad=${refProps.width}:${refProps.height}:(ow-iw)/2:(oh-ih)/2,fps=${refProps.fps}`,
|
|
10779
|
+
"-c:v",
|
|
10780
|
+
"libx264",
|
|
10781
|
+
"-pix_fmt",
|
|
10782
|
+
"yuv420p",
|
|
10783
|
+
"-preset",
|
|
10784
|
+
"ultrafast",
|
|
10785
|
+
"-crf",
|
|
10786
|
+
"23",
|
|
10787
|
+
"-c:a",
|
|
10788
|
+
"aac",
|
|
10789
|
+
"-b:a",
|
|
10790
|
+
"128k",
|
|
10791
|
+
"-ar",
|
|
10792
|
+
"48000",
|
|
10793
|
+
"-ac",
|
|
10794
|
+
"2",
|
|
10795
|
+
"-movflags",
|
|
10796
|
+
"+faststart",
|
|
10797
|
+
output
|
|
10798
|
+
], { maxBuffer: 50 * 1024 * 1024 });
|
|
10799
|
+
return output;
|
|
10800
|
+
}
|
|
10801
|
+
async function getVideoDuration2(videoPath) {
|
|
10802
|
+
const { stdout } = await execCommand(getFFprobePath(), [
|
|
10803
|
+
"-v",
|
|
10804
|
+
"error",
|
|
10805
|
+
"-show_entries",
|
|
10806
|
+
"format=duration",
|
|
10807
|
+
"-of",
|
|
10808
|
+
"csv=p=0",
|
|
10809
|
+
videoPath
|
|
10810
|
+
], { timeout: 1e4 });
|
|
10811
|
+
const duration = parseFloat(stdout.trim());
|
|
10812
|
+
if (!isFinite(duration) || duration <= 0) {
|
|
10813
|
+
throw new Error(`Failed to get duration for ${videoPath}: ${stdout.trim()}`);
|
|
10814
|
+
}
|
|
10815
|
+
return duration;
|
|
10816
|
+
}
|
|
10817
|
+
async function getVideoProperties(videoPath) {
|
|
10818
|
+
const { stdout } = await execCommand(getFFprobePath(), [
|
|
10819
|
+
"-v",
|
|
10820
|
+
"error",
|
|
10821
|
+
"-select_streams",
|
|
10822
|
+
"v:0",
|
|
10823
|
+
"-show_entries",
|
|
10824
|
+
"stream=width,height,r_frame_rate",
|
|
10825
|
+
"-of",
|
|
10826
|
+
"json",
|
|
10827
|
+
videoPath
|
|
10828
|
+
], { timeout: 1e4 });
|
|
10829
|
+
const data = JSON.parse(stdout);
|
|
10830
|
+
const stream = data.streams?.[0];
|
|
10831
|
+
if (!stream) throw new Error(`No video stream found in ${videoPath}`);
|
|
10832
|
+
const fpsRaw = stream.r_frame_rate ?? "30/1";
|
|
10833
|
+
const fpsParts = fpsRaw.split("/");
|
|
10834
|
+
const fps = fpsParts.length === 2 ? Math.round(parseInt(fpsParts[0]) / parseInt(fpsParts[1])) : 30;
|
|
10835
|
+
return {
|
|
10836
|
+
width: stream.width,
|
|
10837
|
+
height: stream.height,
|
|
10838
|
+
fps: isFinite(fps) && fps > 0 ? fps : 30
|
|
10839
|
+
};
|
|
10840
|
+
}
|
|
10841
|
+
|
|
10842
|
+
// src/L3-services/introOutro/introOutroService.ts
|
|
10843
|
+
init_fileSystem();
|
|
10844
|
+
init_paths();
|
|
10845
|
+
init_environment();
|
|
10846
|
+
|
|
10847
|
+
// src/L1-infra/config/brand.ts
|
|
10848
|
+
init_fileSystem();
|
|
10849
|
+
init_environment();
|
|
10850
|
+
init_configLogger();
|
|
10851
|
+
var defaultBrand = {
|
|
10852
|
+
name: "Creator",
|
|
10853
|
+
handle: "@creator",
|
|
10854
|
+
tagline: "",
|
|
10855
|
+
voice: {
|
|
10856
|
+
tone: "professional, friendly",
|
|
10857
|
+
personality: "A knowledgeable content creator.",
|
|
10858
|
+
style: "Clear and concise."
|
|
10859
|
+
},
|
|
10860
|
+
advocacy: {
|
|
10861
|
+
primary: [],
|
|
10862
|
+
interests: [],
|
|
10863
|
+
avoids: []
|
|
10864
|
+
},
|
|
10865
|
+
customVocabulary: [],
|
|
10866
|
+
hashtags: {
|
|
10867
|
+
always: [],
|
|
10868
|
+
preferred: [],
|
|
10869
|
+
platforms: {}
|
|
10870
|
+
},
|
|
10871
|
+
contentGuidelines: {
|
|
10872
|
+
shortsFocus: "Highlight key moments and insights.",
|
|
10873
|
+
blogFocus: "Educational and informative content.",
|
|
10874
|
+
socialFocus: "Engaging and authentic posts."
|
|
10875
|
+
}
|
|
10876
|
+
};
|
|
10877
|
+
var cachedBrand = null;
|
|
10878
|
+
function validateBrandConfig(brand) {
|
|
10879
|
+
const requiredStrings = ["name", "handle", "tagline"];
|
|
10880
|
+
for (const field of requiredStrings) {
|
|
10881
|
+
if (!brand[field]) {
|
|
10882
|
+
logger_default.warn(`brand.json: missing or empty field "${field}"`);
|
|
10883
|
+
}
|
|
10884
|
+
}
|
|
10885
|
+
const requiredObjects = [
|
|
10886
|
+
{ key: "voice", subKeys: ["tone", "personality", "style"] },
|
|
10887
|
+
{ key: "advocacy", subKeys: ["primary", "interests"] },
|
|
10888
|
+
{ key: "hashtags", subKeys: ["always", "preferred"] },
|
|
10889
|
+
{ key: "contentGuidelines", subKeys: ["shortsFocus", "blogFocus", "socialFocus"] }
|
|
10890
|
+
];
|
|
10891
|
+
for (const { key, subKeys } of requiredObjects) {
|
|
10892
|
+
if (!brand[key]) {
|
|
10893
|
+
logger_default.warn(`brand.json: missing section "${key}"`);
|
|
10894
|
+
} else {
|
|
10895
|
+
const section = brand[key];
|
|
10896
|
+
for (const sub of subKeys) {
|
|
10897
|
+
if (!section[sub] || Array.isArray(section[sub]) && section[sub].length === 0) {
|
|
10898
|
+
logger_default.warn(`brand.json: missing or empty field "${key}.${sub}"`);
|
|
10899
|
+
}
|
|
10900
|
+
}
|
|
10901
|
+
}
|
|
10902
|
+
}
|
|
10903
|
+
if (!brand.customVocabulary || brand.customVocabulary.length === 0) {
|
|
10904
|
+
logger_default.warn('brand.json: "customVocabulary" is empty \u2014 Whisper prompt will be blank');
|
|
10905
|
+
}
|
|
10906
|
+
}
|
|
10907
|
+
function getBrandConfig() {
|
|
10908
|
+
if (cachedBrand) return cachedBrand;
|
|
10909
|
+
const config2 = getConfig();
|
|
10910
|
+
const brandPath = config2.BRAND_PATH;
|
|
10911
|
+
if (!fileExistsSync(brandPath)) {
|
|
10912
|
+
logger_default.warn("brand.json not found \u2014 using defaults");
|
|
10913
|
+
cachedBrand = { ...defaultBrand };
|
|
10914
|
+
return cachedBrand;
|
|
10915
|
+
}
|
|
10916
|
+
const raw = readTextFileSync(brandPath);
|
|
10917
|
+
cachedBrand = JSON.parse(raw);
|
|
10918
|
+
validateBrandConfig(cachedBrand);
|
|
10919
|
+
logger_default.info(`Brand config loaded: ${cachedBrand.name}`);
|
|
10920
|
+
return cachedBrand;
|
|
10921
|
+
}
|
|
10922
|
+
function getWhisperPrompt() {
|
|
10923
|
+
const brand = getBrandConfig();
|
|
10924
|
+
return brand.customVocabulary.join(", ");
|
|
10925
|
+
}
|
|
10926
|
+
function getIntroOutroConfig() {
|
|
10927
|
+
const brand = getBrandConfig();
|
|
10928
|
+
return brand.introOutro ?? { enabled: false, fadeDuration: 0 };
|
|
10929
|
+
}
|
|
10930
|
+
|
|
10931
|
+
// src/L3-services/introOutro/introOutroService.ts
|
|
10932
|
+
init_configLogger();
|
|
10933
|
+
|
|
10934
|
+
// src/L0-pure/introOutro/introOutroResolver.ts
|
|
10935
|
+
function resolveIntroOutroToggle(config2, videoType, platform) {
|
|
10936
|
+
const globalDefault = { intro: config2.enabled, outro: config2.enabled };
|
|
10937
|
+
const videoTypeRule = config2.rules?.[videoType];
|
|
10938
|
+
const baseToggle = videoTypeRule ? { intro: videoTypeRule.intro, outro: videoTypeRule.outro } : globalDefault;
|
|
10939
|
+
if (!platform || !config2.platformOverrides?.[platform]?.[videoType]) {
|
|
10940
|
+
return baseToggle;
|
|
10941
|
+
}
|
|
10942
|
+
const platformRule = config2.platformOverrides[platform][videoType];
|
|
10943
|
+
return {
|
|
10944
|
+
intro: platformRule.intro ?? baseToggle.intro,
|
|
10945
|
+
outro: platformRule.outro ?? baseToggle.outro
|
|
10946
|
+
};
|
|
10947
|
+
}
|
|
10948
|
+
function resolveIntroPath(config2, platform, aspectRatio) {
|
|
10949
|
+
if (!config2.intro) return null;
|
|
10950
|
+
if (aspectRatio && config2.intro.aspectRatios?.[aspectRatio]) {
|
|
10951
|
+
return config2.intro.aspectRatios[aspectRatio];
|
|
10952
|
+
}
|
|
10953
|
+
if (platform && config2.intro.platforms?.[platform]) {
|
|
10954
|
+
return config2.intro.platforms[platform];
|
|
10955
|
+
}
|
|
10956
|
+
return config2.intro.default ?? null;
|
|
10957
|
+
}
|
|
10958
|
+
function resolveOutroPath(config2, platform, aspectRatio) {
|
|
10959
|
+
if (!config2.outro) return null;
|
|
10960
|
+
if (aspectRatio && config2.outro.aspectRatios?.[aspectRatio]) {
|
|
10961
|
+
return config2.outro.aspectRatios[aspectRatio];
|
|
10962
|
+
}
|
|
10963
|
+
if (platform && config2.outro.platforms?.[platform]) {
|
|
10964
|
+
return config2.outro.platforms[platform];
|
|
10965
|
+
}
|
|
10966
|
+
return config2.outro.default ?? null;
|
|
10967
|
+
}
|
|
10968
|
+
|
|
10969
|
+
// src/L3-services/introOutro/introOutroService.ts
|
|
10970
|
+
async function applyIntroOutro(videoPath, videoType, outputPath, platform, aspectRatio) {
|
|
10971
|
+
const envConfig = getConfig();
|
|
10972
|
+
if (envConfig.SKIP_INTRO_OUTRO) {
|
|
10973
|
+
logger_default.debug("Intro/outro skipped via SKIP_INTRO_OUTRO");
|
|
10974
|
+
return videoPath;
|
|
10975
|
+
}
|
|
10976
|
+
const config2 = getIntroOutroConfig();
|
|
10977
|
+
if (!config2.enabled) {
|
|
10978
|
+
logger_default.debug("Intro/outro disabled in brand config");
|
|
10979
|
+
return videoPath;
|
|
10980
|
+
}
|
|
10981
|
+
const toggle = resolveIntroOutroToggle(config2, videoType, platform);
|
|
10982
|
+
if (!toggle.intro && !toggle.outro) {
|
|
10983
|
+
logger_default.debug(`Intro/outro both disabled for ${videoType}${platform ? ` / ${platform}` : ""}`);
|
|
10984
|
+
return videoPath;
|
|
10985
|
+
}
|
|
10986
|
+
const brandPath = envConfig.BRAND_PATH;
|
|
10987
|
+
const brandDir = dirname(brandPath);
|
|
10988
|
+
const introRelative = toggle.intro ? resolveIntroPath(config2, platform, aspectRatio) : null;
|
|
10989
|
+
const outroRelative = toggle.outro ? resolveOutroPath(config2, platform, aspectRatio) : null;
|
|
10990
|
+
const introPath = introRelative ? resolve(brandDir, introRelative) : null;
|
|
10991
|
+
const outroPath = outroRelative ? resolve(brandDir, outroRelative) : null;
|
|
10992
|
+
const introExists = introPath ? await fileExists(introPath) : false;
|
|
10993
|
+
const outroExists = outroPath ? await fileExists(outroPath) : false;
|
|
10994
|
+
if (introPath && !introExists) {
|
|
10995
|
+
logger_default.warn(`Intro video not found: ${introPath} \u2014 skipping intro`);
|
|
10996
|
+
}
|
|
10997
|
+
if (outroPath && !outroExists) {
|
|
10998
|
+
logger_default.warn(`Outro video not found: ${outroPath} \u2014 skipping outro`);
|
|
10999
|
+
}
|
|
11000
|
+
const validIntro = introPath && introExists ? introPath : null;
|
|
11001
|
+
const validOutro = outroPath && outroExists ? outroPath : null;
|
|
11002
|
+
if (!validIntro && !validOutro) {
|
|
11003
|
+
logger_default.debug("No valid intro/outro files found \u2014 skipping");
|
|
11004
|
+
return videoPath;
|
|
11005
|
+
}
|
|
11006
|
+
const videoDir = dirname(outputPath);
|
|
11007
|
+
const segments = [];
|
|
11008
|
+
const normalizedIntroPath = validIntro ? join(videoDir, ".intro-normalized.mp4") : null;
|
|
11009
|
+
const normalizedOutroPath = validOutro ? join(videoDir, ".outro-normalized.mp4") : null;
|
|
11010
|
+
try {
|
|
11011
|
+
if (validIntro && normalizedIntroPath) {
|
|
11012
|
+
await normalizeForConcat(validIntro, videoPath, normalizedIntroPath);
|
|
11013
|
+
segments.push(normalizedIntroPath);
|
|
11014
|
+
}
|
|
11015
|
+
segments.push(videoPath);
|
|
11016
|
+
if (validOutro && normalizedOutroPath) {
|
|
11017
|
+
await normalizeForConcat(validOutro, videoPath, normalizedOutroPath);
|
|
11018
|
+
segments.push(normalizedOutroPath);
|
|
11019
|
+
}
|
|
11020
|
+
logger_default.info(`Applying intro/outro (${validIntro ? "intro" : ""}${validIntro && validOutro ? "+" : ""}${validOutro ? "outro" : ""}) for ${videoType}${platform ? ` / ${platform}` : ""}: ${outputPath}`);
|
|
11021
|
+
await concatVideos(segments, outputPath, { fadeDuration: config2.fadeDuration });
|
|
11022
|
+
return outputPath;
|
|
11023
|
+
} finally {
|
|
11024
|
+
if (normalizedIntroPath) await removeFile(normalizedIntroPath).catch(() => {
|
|
11025
|
+
});
|
|
11026
|
+
if (normalizedOutroPath) await removeFile(normalizedOutroPath).catch(() => {
|
|
11027
|
+
});
|
|
11028
|
+
}
|
|
11029
|
+
}
|
|
11030
|
+
|
|
11031
|
+
// src/L4-agents/videoServiceBridge.ts
|
|
11032
|
+
function applyIntroOutro2(videoPath, videoType, outputPath, platform, aspectRatio) {
|
|
11033
|
+
return applyIntroOutro(videoPath, videoType, outputPath, platform, aspectRatio);
|
|
11034
|
+
}
|
|
11035
|
+
|
|
10620
11036
|
// src/L0-pure/captions/captionGenerator.ts
|
|
10621
11037
|
function pad(n, width) {
|
|
10622
11038
|
return String(n).padStart(width, "0");
|
|
@@ -11284,88 +11700,6 @@ init_ai();
|
|
|
11284
11700
|
init_fileSystem();
|
|
11285
11701
|
init_environment();
|
|
11286
11702
|
init_configLogger();
|
|
11287
|
-
|
|
11288
|
-
// src/L1-infra/config/brand.ts
|
|
11289
|
-
init_fileSystem();
|
|
11290
|
-
init_environment();
|
|
11291
|
-
init_configLogger();
|
|
11292
|
-
var defaultBrand = {
|
|
11293
|
-
name: "Creator",
|
|
11294
|
-
handle: "@creator",
|
|
11295
|
-
tagline: "",
|
|
11296
|
-
voice: {
|
|
11297
|
-
tone: "professional, friendly",
|
|
11298
|
-
personality: "A knowledgeable content creator.",
|
|
11299
|
-
style: "Clear and concise."
|
|
11300
|
-
},
|
|
11301
|
-
advocacy: {
|
|
11302
|
-
primary: [],
|
|
11303
|
-
interests: [],
|
|
11304
|
-
avoids: []
|
|
11305
|
-
},
|
|
11306
|
-
customVocabulary: [],
|
|
11307
|
-
hashtags: {
|
|
11308
|
-
always: [],
|
|
11309
|
-
preferred: [],
|
|
11310
|
-
platforms: {}
|
|
11311
|
-
},
|
|
11312
|
-
contentGuidelines: {
|
|
11313
|
-
shortsFocus: "Highlight key moments and insights.",
|
|
11314
|
-
blogFocus: "Educational and informative content.",
|
|
11315
|
-
socialFocus: "Engaging and authentic posts."
|
|
11316
|
-
}
|
|
11317
|
-
};
|
|
11318
|
-
var cachedBrand = null;
|
|
11319
|
-
function validateBrandConfig(brand) {
|
|
11320
|
-
const requiredStrings = ["name", "handle", "tagline"];
|
|
11321
|
-
for (const field of requiredStrings) {
|
|
11322
|
-
if (!brand[field]) {
|
|
11323
|
-
logger_default.warn(`brand.json: missing or empty field "${field}"`);
|
|
11324
|
-
}
|
|
11325
|
-
}
|
|
11326
|
-
const requiredObjects = [
|
|
11327
|
-
{ key: "voice", subKeys: ["tone", "personality", "style"] },
|
|
11328
|
-
{ key: "advocacy", subKeys: ["primary", "interests"] },
|
|
11329
|
-
{ key: "hashtags", subKeys: ["always", "preferred"] },
|
|
11330
|
-
{ key: "contentGuidelines", subKeys: ["shortsFocus", "blogFocus", "socialFocus"] }
|
|
11331
|
-
];
|
|
11332
|
-
for (const { key, subKeys } of requiredObjects) {
|
|
11333
|
-
if (!brand[key]) {
|
|
11334
|
-
logger_default.warn(`brand.json: missing section "${key}"`);
|
|
11335
|
-
} else {
|
|
11336
|
-
const section = brand[key];
|
|
11337
|
-
for (const sub of subKeys) {
|
|
11338
|
-
if (!section[sub] || Array.isArray(section[sub]) && section[sub].length === 0) {
|
|
11339
|
-
logger_default.warn(`brand.json: missing or empty field "${key}.${sub}"`);
|
|
11340
|
-
}
|
|
11341
|
-
}
|
|
11342
|
-
}
|
|
11343
|
-
}
|
|
11344
|
-
if (!brand.customVocabulary || brand.customVocabulary.length === 0) {
|
|
11345
|
-
logger_default.warn('brand.json: "customVocabulary" is empty \u2014 Whisper prompt will be blank');
|
|
11346
|
-
}
|
|
11347
|
-
}
|
|
11348
|
-
function getBrandConfig() {
|
|
11349
|
-
if (cachedBrand) return cachedBrand;
|
|
11350
|
-
const config2 = getConfig();
|
|
11351
|
-
const brandPath = config2.BRAND_PATH;
|
|
11352
|
-
if (!fileExistsSync(brandPath)) {
|
|
11353
|
-
logger_default.warn("brand.json not found \u2014 using defaults");
|
|
11354
|
-
cachedBrand = { ...defaultBrand };
|
|
11355
|
-
return cachedBrand;
|
|
11356
|
-
}
|
|
11357
|
-
const raw = readTextFileSync(brandPath);
|
|
11358
|
-
cachedBrand = JSON.parse(raw);
|
|
11359
|
-
validateBrandConfig(cachedBrand);
|
|
11360
|
-
logger_default.info(`Brand config loaded: ${cachedBrand.name}`);
|
|
11361
|
-
return cachedBrand;
|
|
11362
|
-
}
|
|
11363
|
-
function getWhisperPrompt() {
|
|
11364
|
-
const brand = getBrandConfig();
|
|
11365
|
-
return brand.customVocabulary.join(", ");
|
|
11366
|
-
}
|
|
11367
|
-
|
|
11368
|
-
// src/L2-clients/whisper/whisperClient.ts
|
|
11369
11703
|
var MAX_FILE_SIZE_MB = 25;
|
|
11370
11704
|
var WARN_FILE_SIZE_MB = 20;
|
|
11371
11705
|
var MAX_RETRIES = 3;
|
|
@@ -12089,6 +12423,10 @@ var ShortVideoAsset = class extends VideoAsset {
|
|
|
12089
12423
|
get videoPath() {
|
|
12090
12424
|
return join(this.videoDir, "media.mp4");
|
|
12091
12425
|
}
|
|
12426
|
+
/** Path to the short with intro/outro applied */
|
|
12427
|
+
get introOutroVideoPath() {
|
|
12428
|
+
return join(this.videoDir, "media-intro-outro.mp4");
|
|
12429
|
+
}
|
|
12092
12430
|
/** Directory containing social posts for this short */
|
|
12093
12431
|
get postsDir() {
|
|
12094
12432
|
return join(this.videoDir, "posts");
|
|
@@ -12155,6 +12493,57 @@ var ShortVideoAsset = class extends VideoAsset {
|
|
|
12155
12493
|
await extractCompositeClip2(parentVideo, this.clip.segments, this.videoPath);
|
|
12156
12494
|
return this.videoPath;
|
|
12157
12495
|
}
|
|
12496
|
+
/**
|
|
12497
|
+
* Apply intro/outro to the short clip.
|
|
12498
|
+
* Uses brand config rules for 'shorts' video type.
|
|
12499
|
+
*
|
|
12500
|
+
* @returns Path to the intro/outro'd video, or the original path if skipped
|
|
12501
|
+
*/
|
|
12502
|
+
async getIntroOutroVideo() {
|
|
12503
|
+
if (await fileExists(this.introOutroVideoPath)) {
|
|
12504
|
+
return this.introOutroVideoPath;
|
|
12505
|
+
}
|
|
12506
|
+
const candidates = [this.clip.captionedPath, this.clip.outputPath];
|
|
12507
|
+
let clipPath;
|
|
12508
|
+
for (const candidate of candidates) {
|
|
12509
|
+
if (candidate && await fileExists(candidate)) {
|
|
12510
|
+
clipPath = candidate;
|
|
12511
|
+
break;
|
|
12512
|
+
}
|
|
12513
|
+
}
|
|
12514
|
+
if (!clipPath) {
|
|
12515
|
+
clipPath = await this.getResult();
|
|
12516
|
+
}
|
|
12517
|
+
return applyIntroOutro2(clipPath, "shorts", this.introOutroVideoPath);
|
|
12518
|
+
}
|
|
12519
|
+
/**
|
|
12520
|
+
* Apply intro/outro to all platform variants of this short.
|
|
12521
|
+
* Resolves the correct intro/outro file per aspect ratio, auto-cropping
|
|
12522
|
+
* from the default file when no ratio-specific file is configured.
|
|
12523
|
+
*
|
|
12524
|
+
* @returns Map of platform to intro/outro'd variant path
|
|
12525
|
+
*/
|
|
12526
|
+
async getIntroOutroVariants() {
|
|
12527
|
+
const results = /* @__PURE__ */ new Map();
|
|
12528
|
+
if (!this.clip.variants || this.clip.variants.length === 0) return results;
|
|
12529
|
+
for (const variant of this.clip.variants) {
|
|
12530
|
+
const outputPath = join(this.videoDir, `media-${variant.platform}-intro-outro.mp4`);
|
|
12531
|
+
if (await fileExists(outputPath)) {
|
|
12532
|
+
results.set(variant.platform, outputPath);
|
|
12533
|
+
continue;
|
|
12534
|
+
}
|
|
12535
|
+
if (!await fileExists(variant.path)) continue;
|
|
12536
|
+
const result = await applyIntroOutro2(
|
|
12537
|
+
variant.path,
|
|
12538
|
+
"shorts",
|
|
12539
|
+
outputPath,
|
|
12540
|
+
variant.platform,
|
|
12541
|
+
variant.aspectRatio
|
|
12542
|
+
);
|
|
12543
|
+
results.set(variant.platform, result);
|
|
12544
|
+
}
|
|
12545
|
+
return results;
|
|
12546
|
+
}
|
|
12158
12547
|
// ── Transcript ───────────────────────────────────────────────────────────────
|
|
12159
12548
|
/**
|
|
12160
12549
|
* Get transcript filtered to this short's time range.
|
|
@@ -12224,6 +12613,10 @@ var MediumClipAsset = class extends VideoAsset {
|
|
|
12224
12613
|
get videoPath() {
|
|
12225
12614
|
return join(this.videoDir, "media.mp4");
|
|
12226
12615
|
}
|
|
12616
|
+
/** Path to the clip with intro/outro applied */
|
|
12617
|
+
get introOutroVideoPath() {
|
|
12618
|
+
return join(this.videoDir, "media-intro-outro.mp4");
|
|
12619
|
+
}
|
|
12227
12620
|
/**
|
|
12228
12621
|
* Directory containing social media posts for this clip.
|
|
12229
12622
|
*/
|
|
@@ -12271,6 +12664,29 @@ var MediumClipAsset = class extends VideoAsset {
|
|
|
12271
12664
|
await extractCompositeClip2(parentVideo, this.clip.segments, this.videoPath);
|
|
12272
12665
|
return this.videoPath;
|
|
12273
12666
|
}
|
|
12667
|
+
/**
|
|
12668
|
+
* Apply intro/outro to the medium clip.
|
|
12669
|
+
* Uses brand config rules for 'medium-clips' video type.
|
|
12670
|
+
*
|
|
12671
|
+
* @returns Path to the intro/outro'd video, or the original path if skipped
|
|
12672
|
+
*/
|
|
12673
|
+
async getIntroOutroVideo() {
|
|
12674
|
+
if (await fileExists(this.introOutroVideoPath)) {
|
|
12675
|
+
return this.introOutroVideoPath;
|
|
12676
|
+
}
|
|
12677
|
+
const candidates = [this.clip.captionedPath, this.clip.outputPath];
|
|
12678
|
+
let clipPath;
|
|
12679
|
+
for (const candidate of candidates) {
|
|
12680
|
+
if (candidate && await fileExists(candidate)) {
|
|
12681
|
+
clipPath = candidate;
|
|
12682
|
+
break;
|
|
12683
|
+
}
|
|
12684
|
+
}
|
|
12685
|
+
if (!clipPath) {
|
|
12686
|
+
clipPath = await this.getResult();
|
|
12687
|
+
}
|
|
12688
|
+
return applyIntroOutro2(clipPath, "medium-clips", this.introOutroVideoPath);
|
|
12689
|
+
}
|
|
12274
12690
|
};
|
|
12275
12691
|
|
|
12276
12692
|
// src/L5-assets/MainVideoAsset.ts
|
|
@@ -12513,7 +12929,7 @@ var SilenceRemovalAgent = class extends BaseAgent {
|
|
|
12513
12929
|
return this.removals;
|
|
12514
12930
|
}
|
|
12515
12931
|
};
|
|
12516
|
-
async function
|
|
12932
|
+
async function getVideoDuration3(videoPath) {
|
|
12517
12933
|
try {
|
|
12518
12934
|
const metadata = await ffprobe2(videoPath);
|
|
12519
12935
|
return metadata.format.duration ?? 0;
|
|
@@ -12588,7 +13004,7 @@ async function removeDeadSilence(video, transcript, model) {
|
|
|
12588
13004
|
logger_default.info("[SilenceRemoval] All removals exceeded 20% cap \u2014 skipping edit");
|
|
12589
13005
|
return noEdit;
|
|
12590
13006
|
}
|
|
12591
|
-
const videoDuration = await
|
|
13007
|
+
const videoDuration = await getVideoDuration3(video.repoPath);
|
|
12592
13008
|
const sortedRemovals = [...removals].sort((a, b) => a.start - b.start);
|
|
12593
13009
|
const keepSegments = [];
|
|
12594
13010
|
let cursor = 0;
|
|
@@ -15394,6 +15810,10 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
|
|
|
15394
15810
|
get producedVideoPath() {
|
|
15395
15811
|
return join(this.videoDir, `${this.slug}-produced.mp4`);
|
|
15396
15812
|
}
|
|
15813
|
+
/** Path to the video with intro/outro applied: videoDir/{slug}-intro-outro.mp4 */
|
|
15814
|
+
get introOutroVideoPath() {
|
|
15815
|
+
return join(this.videoDir, `${this.slug}-intro-outro.mp4`);
|
|
15816
|
+
}
|
|
15397
15817
|
/** Path to a produced video for a specific aspect ratio: videoDir/{slug}-produced-{ar}.mp4 */
|
|
15398
15818
|
producedVideoPathFor(aspectRatio) {
|
|
15399
15819
|
const arSuffix = aspectRatio.replace(":", "x");
|
|
@@ -15671,6 +16091,21 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
|
|
|
15671
16091
|
logger_default.info(`Captions burned into video: ${this.captionedVideoPath}`);
|
|
15672
16092
|
return this.captionedVideoPath;
|
|
15673
16093
|
}
|
|
16094
|
+
/**
|
|
16095
|
+
* Get the video with intro/outro applied.
|
|
16096
|
+
* Concatenates intro (if configured) + captioned video + outro (if configured).
|
|
16097
|
+
*
|
|
16098
|
+
* @param opts - Options controlling generation
|
|
16099
|
+
* @returns Path to the intro/outro video, or captioned video if skipped
|
|
16100
|
+
*/
|
|
16101
|
+
async getIntroOutroVideo(opts) {
|
|
16102
|
+
if (!opts?.force && await fileExists(this.introOutroVideoPath)) {
|
|
16103
|
+
return this.introOutroVideoPath;
|
|
16104
|
+
}
|
|
16105
|
+
const captionedPath = await this.getCaptionedVideo(opts);
|
|
16106
|
+
const result = await applyIntroOutro2(captionedPath, "main", this.introOutroVideoPath);
|
|
16107
|
+
return result;
|
|
16108
|
+
}
|
|
15674
16109
|
/**
|
|
15675
16110
|
* Get the fully produced video.
|
|
15676
16111
|
* If not already generated, runs the ProducerAgent.
|
|
@@ -17841,16 +18276,53 @@ async function processVideo(videoPath, ideas) {
|
|
|
17841
18276
|
} else {
|
|
17842
18277
|
skipStage("caption-burn" /* CaptionBurn */, "SKIP_CAPTIONS");
|
|
17843
18278
|
}
|
|
18279
|
+
let introOutroVideoPath;
|
|
18280
|
+
if (!cfg.SKIP_INTRO_OUTRO) {
|
|
18281
|
+
introOutroVideoPath = await trackStage("intro-outro" /* IntroOutro */, () => asset.getIntroOutroVideo());
|
|
18282
|
+
} else {
|
|
18283
|
+
skipStage("intro-outro" /* IntroOutro */, "SKIP_INTRO_OUTRO");
|
|
18284
|
+
}
|
|
17844
18285
|
let shorts = [];
|
|
17845
18286
|
if (!cfg.SKIP_SHORTS) {
|
|
17846
|
-
const shortAssets = await trackStage("shorts" /* Shorts */, () =>
|
|
18287
|
+
const shortAssets = await trackStage("shorts" /* Shorts */, async () => {
|
|
18288
|
+
const assets = await asset.getShorts();
|
|
18289
|
+
if (!cfg.SKIP_INTRO_OUTRO) {
|
|
18290
|
+
for (const shortAsset of assets) {
|
|
18291
|
+
const introOutroPath = await shortAsset.getIntroOutroVideo();
|
|
18292
|
+
if (introOutroPath !== shortAsset.clip.outputPath) {
|
|
18293
|
+
shortAsset.clip.outputPath = introOutroPath;
|
|
18294
|
+
shortAsset.clip.captionedPath = introOutroPath;
|
|
18295
|
+
}
|
|
18296
|
+
const variantResults = await shortAsset.getIntroOutroVariants();
|
|
18297
|
+
if (shortAsset.clip.variants) {
|
|
18298
|
+
for (const variant of shortAsset.clip.variants) {
|
|
18299
|
+
const updated = variantResults.get(variant.platform);
|
|
18300
|
+
if (updated) variant.path = updated;
|
|
18301
|
+
}
|
|
18302
|
+
}
|
|
18303
|
+
}
|
|
18304
|
+
}
|
|
18305
|
+
return assets;
|
|
18306
|
+
}) ?? [];
|
|
17847
18307
|
shorts = shortAssets.map((s) => s.clip);
|
|
17848
18308
|
} else {
|
|
17849
18309
|
skipStage("shorts" /* Shorts */, "SKIP_SHORTS");
|
|
17850
18310
|
}
|
|
17851
18311
|
let mediumClips = [];
|
|
17852
18312
|
if (!cfg.SKIP_MEDIUM_CLIPS) {
|
|
17853
|
-
const mediumAssets = await trackStage("medium-clips" /* MediumClips */, () =>
|
|
18313
|
+
const mediumAssets = await trackStage("medium-clips" /* MediumClips */, async () => {
|
|
18314
|
+
const assets = await asset.getMediumClips();
|
|
18315
|
+
if (!cfg.SKIP_INTRO_OUTRO) {
|
|
18316
|
+
for (const clipAsset of assets) {
|
|
18317
|
+
const introOutroPath = await clipAsset.getIntroOutroVideo();
|
|
18318
|
+
if (introOutroPath !== clipAsset.clip.outputPath) {
|
|
18319
|
+
clipAsset.clip.outputPath = introOutroPath;
|
|
18320
|
+
clipAsset.clip.captionedPath = introOutroPath;
|
|
18321
|
+
}
|
|
18322
|
+
}
|
|
18323
|
+
}
|
|
18324
|
+
return assets;
|
|
18325
|
+
}) ?? [];
|
|
17854
18326
|
mediumClips = mediumAssets.map((m) => m.clip);
|
|
17855
18327
|
} else {
|
|
17856
18328
|
skipStage("medium-clips" /* MediumClips */, "SKIP_MEDIUM_CLIPS");
|
|
@@ -17887,7 +18359,7 @@ async function processVideo(videoPath, ideas) {
|
|
|
17887
18359
|
skipStage("medium-clip-posts" /* MediumClipPosts */, "SKIP_SOCIAL");
|
|
17888
18360
|
}
|
|
17889
18361
|
if (!cfg.SKIP_SOCIAL_PUBLISH && socialPosts.length > 0) {
|
|
17890
|
-
await trackStage("queue-build" /* QueueBuild */, () => asset.buildQueue(shorts, mediumClips, socialPosts, captionedVideoPath));
|
|
18362
|
+
await trackStage("queue-build" /* QueueBuild */, () => asset.buildQueue(shorts, mediumClips, socialPosts, introOutroVideoPath ?? captionedVideoPath));
|
|
17891
18363
|
} else if (cfg.SKIP_SOCIAL_PUBLISH) {
|
|
17892
18364
|
skipStage("queue-build" /* QueueBuild */, "SKIP_SOCIAL_PUBLISH");
|
|
17893
18365
|
} else {
|
|
@@ -17923,6 +18395,7 @@ async function processVideo(videoPath, ideas) {
|
|
|
17923
18395
|
enhancedVideoPath,
|
|
17924
18396
|
captions: captions ? [captions.srt, captions.vtt, captions.ass] : void 0,
|
|
17925
18397
|
captionedVideoPath,
|
|
18398
|
+
introOutroVideoPath,
|
|
17926
18399
|
summary,
|
|
17927
18400
|
chapters,
|
|
17928
18401
|
shorts,
|
|
@@ -19191,6 +19664,593 @@ async function runConfigure(subcommand, args = []) {
|
|
|
19191
19664
|
}
|
|
19192
19665
|
}
|
|
19193
19666
|
|
|
19667
|
+
// src/L7-app/commands/introOutro.ts
|
|
19668
|
+
init_fileSystem();
|
|
19669
|
+
init_environment();
|
|
19670
|
+
init_paths();
|
|
19671
|
+
init_configLogger();
|
|
19672
|
+
var PLATFORMS = ["tiktok", "youtube", "instagram", "linkedin", "x"];
|
|
19673
|
+
var VIDEO_TYPES = ["main", "shorts", "medium-clips"];
|
|
19674
|
+
var ASPECT_RATIOS = ["16:9", "9:16", "1:1", "4:5"];
|
|
19675
|
+
async function loadBrand() {
|
|
19676
|
+
const config2 = getConfig();
|
|
19677
|
+
const brandPath = config2.BRAND_PATH;
|
|
19678
|
+
const brand = await readJsonFile(brandPath, {});
|
|
19679
|
+
return { brand, brandPath };
|
|
19680
|
+
}
|
|
19681
|
+
async function saveBrand(brandPath, brand) {
|
|
19682
|
+
await writeJsonFile(brandPath, brand);
|
|
19683
|
+
logger_default.info(`brand.json updated: ${brandPath}`);
|
|
19684
|
+
}
|
|
19685
|
+
function getIntroOutro(brand) {
|
|
19686
|
+
return brand.introOutro ?? {
|
|
19687
|
+
enabled: false,
|
|
19688
|
+
fadeDuration: 0,
|
|
19689
|
+
intro: { default: "", platforms: {} },
|
|
19690
|
+
outro: { default: "", platforms: {} },
|
|
19691
|
+
rules: {
|
|
19692
|
+
main: { intro: true, outro: true },
|
|
19693
|
+
shorts: { intro: false, outro: true },
|
|
19694
|
+
"medium-clips": { intro: true, outro: true }
|
|
19695
|
+
},
|
|
19696
|
+
platformOverrides: {}
|
|
19697
|
+
};
|
|
19698
|
+
}
|
|
19699
|
+
function showConfig(cfg) {
|
|
19700
|
+
console.log();
|
|
19701
|
+
console.log("\u250C\u2500 Intro/Outro Configuration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
19702
|
+
console.log(`\u2502 Enabled: ${cfg.enabled ? "\u2705 Yes" : "\u274C No"}`);
|
|
19703
|
+
console.log(`\u2502 Fade Duration: ${cfg.fadeDuration}s`);
|
|
19704
|
+
console.log("\u251C\u2500 Files \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
19705
|
+
console.log(`\u2502 Intro default: ${cfg.intro?.default || "(not set)"}`);
|
|
19706
|
+
if (cfg.intro?.platforms && Object.keys(cfg.intro.platforms).length > 0) {
|
|
19707
|
+
for (const [p, path] of Object.entries(cfg.intro.platforms)) {
|
|
19708
|
+
console.log(`\u2502 ${p.padEnd(12)} ${path}`);
|
|
19709
|
+
}
|
|
19710
|
+
}
|
|
19711
|
+
if (cfg.intro?.aspectRatios && Object.keys(cfg.intro.aspectRatios).length > 0) {
|
|
19712
|
+
for (const [ratio, path] of Object.entries(cfg.intro.aspectRatios)) {
|
|
19713
|
+
console.log(`\u2502 ${ratio.padEnd(12)} ${path}`);
|
|
19714
|
+
}
|
|
19715
|
+
}
|
|
19716
|
+
console.log(`\u2502 Outro default: ${cfg.outro?.default || "(not set)"}`);
|
|
19717
|
+
if (cfg.outro?.platforms && Object.keys(cfg.outro.platforms).length > 0) {
|
|
19718
|
+
for (const [p, path] of Object.entries(cfg.outro.platforms)) {
|
|
19719
|
+
console.log(`\u2502 ${p.padEnd(12)} ${path}`);
|
|
19720
|
+
}
|
|
19721
|
+
}
|
|
19722
|
+
if (cfg.outro?.aspectRatios && Object.keys(cfg.outro.aspectRatios).length > 0) {
|
|
19723
|
+
for (const [ratio, path] of Object.entries(cfg.outro.aspectRatios)) {
|
|
19724
|
+
console.log(`\u2502 ${ratio.padEnd(12)} ${path}`);
|
|
19725
|
+
}
|
|
19726
|
+
}
|
|
19727
|
+
console.log("\u251C\u2500 Rules \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
19728
|
+
for (const vt of VIDEO_TYPES) {
|
|
19729
|
+
const rule = cfg.rules?.[vt];
|
|
19730
|
+
if (rule) {
|
|
19731
|
+
console.log(`\u2502 ${vt.padEnd(14)} intro: ${rule.intro ? "\u2705" : "\u274C"} outro: ${rule.outro ? "\u2705" : "\u274C"}`);
|
|
19732
|
+
} else {
|
|
19733
|
+
console.log(`\u2502 ${vt.padEnd(14)} (uses global default)`);
|
|
19734
|
+
}
|
|
19735
|
+
}
|
|
19736
|
+
if (cfg.platformOverrides && Object.keys(cfg.platformOverrides).length > 0) {
|
|
19737
|
+
console.log("\u251C\u2500 Platform Overrides \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
19738
|
+
for (const [platform, videoTypes] of Object.entries(cfg.platformOverrides)) {
|
|
19739
|
+
for (const [vt, toggle] of Object.entries(videoTypes)) {
|
|
19740
|
+
const parts = [];
|
|
19741
|
+
if (toggle.intro !== void 0) parts.push(`intro: ${toggle.intro ? "\u2705" : "\u274C"}`);
|
|
19742
|
+
if (toggle.outro !== void 0) parts.push(`outro: ${toggle.outro ? "\u2705" : "\u274C"}`);
|
|
19743
|
+
console.log(`\u2502 ${platform}/${vt}: ${parts.join(" ")}`);
|
|
19744
|
+
}
|
|
19745
|
+
}
|
|
19746
|
+
}
|
|
19747
|
+
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
19748
|
+
console.log();
|
|
19749
|
+
}
|
|
19750
|
+
async function runWizard(brand, brandPath) {
|
|
19751
|
+
const rl2 = createPromptInterface();
|
|
19752
|
+
const cfg = getIntroOutro(brand);
|
|
19753
|
+
const brandDir = dirname(brandPath);
|
|
19754
|
+
try {
|
|
19755
|
+
console.log();
|
|
19756
|
+
console.log("\u{1F3AC} Intro/Outro Setup Wizard");
|
|
19757
|
+
console.log("\u2500".repeat(50));
|
|
19758
|
+
const enableAnswer = await rl2.question(`Enable intro/outro? (${cfg.enabled ? "Y/n" : "y/N"}): `);
|
|
19759
|
+
if (enableAnswer.trim()) {
|
|
19760
|
+
cfg.enabled = enableAnswer.trim().toLowerCase().startsWith("y");
|
|
19761
|
+
}
|
|
19762
|
+
const introDefault = cfg.intro?.default || "./assets/intro.mp4";
|
|
19763
|
+
const introAnswer = await rl2.question(`Intro video path [${introDefault}]: `);
|
|
19764
|
+
const introPath = introAnswer.trim() || introDefault;
|
|
19765
|
+
cfg.intro = { ...cfg.intro, default: introPath };
|
|
19766
|
+
const resolvedIntro = resolve(brandDir, introPath);
|
|
19767
|
+
if (!await fileExists(resolvedIntro)) {
|
|
19768
|
+
console.log(` \u26A0\uFE0F File not found: ${resolvedIntro}`);
|
|
19769
|
+
console.log(` (You can add it later \u2014 the path is saved)`);
|
|
19770
|
+
} else {
|
|
19771
|
+
console.log(` \u2705 Found: ${resolvedIntro}`);
|
|
19772
|
+
}
|
|
19773
|
+
const outroDefault = cfg.outro?.default || "./assets/outro.mp4";
|
|
19774
|
+
const outroAnswer = await rl2.question(`Outro video path [${outroDefault}]: `);
|
|
19775
|
+
const outroPath = outroAnswer.trim() || outroDefault;
|
|
19776
|
+
cfg.outro = { ...cfg.outro, default: outroPath };
|
|
19777
|
+
const resolvedOutro = resolve(brandDir, outroPath);
|
|
19778
|
+
if (!await fileExists(resolvedOutro)) {
|
|
19779
|
+
console.log(` \u26A0\uFE0F File not found: ${resolvedOutro}`);
|
|
19780
|
+
console.log(` (You can add it later \u2014 the path is saved)`);
|
|
19781
|
+
} else {
|
|
19782
|
+
console.log(` \u2705 Found: ${resolvedOutro}`);
|
|
19783
|
+
}
|
|
19784
|
+
const fadeDefault = cfg.fadeDuration ?? 0.5;
|
|
19785
|
+
const fadeAnswer = await rl2.question(`Crossfade duration in seconds [${fadeDefault}]: `);
|
|
19786
|
+
cfg.fadeDuration = fadeAnswer.trim() ? parseFloat(fadeAnswer.trim()) : fadeDefault;
|
|
19787
|
+
if (!isFinite(cfg.fadeDuration) || cfg.fadeDuration < 0) {
|
|
19788
|
+
console.log(" \u26A0\uFE0F Invalid fade duration, using 0.5s");
|
|
19789
|
+
cfg.fadeDuration = 0.5;
|
|
19790
|
+
}
|
|
19791
|
+
console.log();
|
|
19792
|
+
console.log("Configure which video types get intro/outro:");
|
|
19793
|
+
for (const vt of VIDEO_TYPES) {
|
|
19794
|
+
const current = cfg.rules?.[vt] ?? { intro: true, outro: true };
|
|
19795
|
+
const introAns = await rl2.question(` ${vt} \u2014 include intro? (${current.intro ? "Y/n" : "y/N"}): `);
|
|
19796
|
+
const outroAns = await rl2.question(` ${vt} \u2014 include outro? (${current.outro ? "Y/n" : "y/N"}): `);
|
|
19797
|
+
const introVal = introAns.trim() ? introAns.trim().toLowerCase().startsWith("y") : current.intro;
|
|
19798
|
+
const outroVal = outroAns.trim() ? outroAns.trim().toLowerCase().startsWith("y") : current.outro;
|
|
19799
|
+
if (!cfg.rules) cfg.rules = {};
|
|
19800
|
+
cfg.rules[vt] = { intro: introVal, outro: outroVal };
|
|
19801
|
+
}
|
|
19802
|
+
const platformAnswer = await rl2.question("\nSet platform-specific intro/outro files? (y/N): ");
|
|
19803
|
+
if (platformAnswer.trim().toLowerCase().startsWith("y")) {
|
|
19804
|
+
for (const platform of PLATFORMS) {
|
|
19805
|
+
const pIntro = await rl2.question(` ${platform} intro path (empty = use default): `);
|
|
19806
|
+
if (pIntro.trim()) {
|
|
19807
|
+
if (!cfg.intro.platforms) cfg.intro.platforms = {};
|
|
19808
|
+
cfg.intro.platforms[platform] = pIntro.trim();
|
|
19809
|
+
}
|
|
19810
|
+
const pOutro = await rl2.question(` ${platform} outro path (empty = use default): `);
|
|
19811
|
+
if (pOutro.trim()) {
|
|
19812
|
+
if (!cfg.outro.platforms) cfg.outro.platforms = {};
|
|
19813
|
+
cfg.outro.platforms[platform] = pOutro.trim();
|
|
19814
|
+
}
|
|
19815
|
+
}
|
|
19816
|
+
}
|
|
19817
|
+
const ratioAnswer = await rl2.question("\nSet aspect-ratio-specific intro/outro files? (y/N): ");
|
|
19818
|
+
if (ratioAnswer.trim().toLowerCase().startsWith("y")) {
|
|
19819
|
+
const nonDefaultRatios = ASPECT_RATIOS.filter((r) => r !== "16:9");
|
|
19820
|
+
for (const ratio of nonDefaultRatios) {
|
|
19821
|
+
const rIntro = await rl2.question(` ${ratio} intro path (empty = use default): `);
|
|
19822
|
+
if (rIntro.trim()) {
|
|
19823
|
+
if (!cfg.intro.aspectRatios) cfg.intro.aspectRatios = {};
|
|
19824
|
+
cfg.intro.aspectRatios[ratio] = rIntro.trim();
|
|
19825
|
+
}
|
|
19826
|
+
const rOutro = await rl2.question(` ${ratio} outro path (empty = use default): `);
|
|
19827
|
+
if (rOutro.trim()) {
|
|
19828
|
+
if (!cfg.outro.aspectRatios) cfg.outro.aspectRatios = {};
|
|
19829
|
+
cfg.outro.aspectRatios[ratio] = rOutro.trim();
|
|
19830
|
+
}
|
|
19831
|
+
}
|
|
19832
|
+
}
|
|
19833
|
+
brand.introOutro = cfg;
|
|
19834
|
+
await saveBrand(brandPath, brand);
|
|
19835
|
+
console.log();
|
|
19836
|
+
console.log("\u2705 Intro/outro configuration saved!");
|
|
19837
|
+
showConfig(cfg);
|
|
19838
|
+
} finally {
|
|
19839
|
+
rl2.close();
|
|
19840
|
+
}
|
|
19841
|
+
}
|
|
19842
|
+
async function handleEnable(brand, brandPath) {
|
|
19843
|
+
const cfg = getIntroOutro(brand);
|
|
19844
|
+
cfg.enabled = true;
|
|
19845
|
+
brand.introOutro = cfg;
|
|
19846
|
+
await saveBrand(brandPath, brand);
|
|
19847
|
+
console.log("\u2705 Intro/outro enabled");
|
|
19848
|
+
}
|
|
19849
|
+
async function handleDisable(brand, brandPath) {
|
|
19850
|
+
const cfg = getIntroOutro(brand);
|
|
19851
|
+
cfg.enabled = false;
|
|
19852
|
+
brand.introOutro = cfg;
|
|
19853
|
+
await saveBrand(brandPath, brand);
|
|
19854
|
+
console.log("\u274C Intro/outro disabled");
|
|
19855
|
+
}
|
|
19856
|
+
async function handleSetIntro(brand, brandPath, args) {
|
|
19857
|
+
if (args.length < 1) {
|
|
19858
|
+
console.error("Usage: vidpipe intro-outro set-intro <path> [--platform <name>]");
|
|
19859
|
+
process.exitCode = 1;
|
|
19860
|
+
return;
|
|
19861
|
+
}
|
|
19862
|
+
const cfg = getIntroOutro(brand);
|
|
19863
|
+
const platformIdx = args.indexOf("--platform");
|
|
19864
|
+
if (platformIdx >= 0 && args[platformIdx + 1]) {
|
|
19865
|
+
const platform = args[platformIdx + 1];
|
|
19866
|
+
const filePath = args.filter((_, i) => i !== platformIdx && i !== platformIdx + 1)[0];
|
|
19867
|
+
if (!cfg.intro) cfg.intro = { platforms: {} };
|
|
19868
|
+
if (!cfg.intro.platforms) cfg.intro.platforms = {};
|
|
19869
|
+
cfg.intro.platforms[platform] = filePath;
|
|
19870
|
+
console.log(`\u2705 Intro for ${platform}: ${filePath}`);
|
|
19871
|
+
} else {
|
|
19872
|
+
if (!cfg.intro) cfg.intro = {};
|
|
19873
|
+
cfg.intro.default = args[0];
|
|
19874
|
+
console.log(`\u2705 Default intro: ${args[0]}`);
|
|
19875
|
+
}
|
|
19876
|
+
brand.introOutro = cfg;
|
|
19877
|
+
await saveBrand(brandPath, brand);
|
|
19878
|
+
}
|
|
19879
|
+
async function handleSetOutro(brand, brandPath, args) {
|
|
19880
|
+
if (args.length < 1) {
|
|
19881
|
+
console.error("Usage: vidpipe intro-outro set-outro <path> [--platform <name>]");
|
|
19882
|
+
process.exitCode = 1;
|
|
19883
|
+
return;
|
|
19884
|
+
}
|
|
19885
|
+
const cfg = getIntroOutro(brand);
|
|
19886
|
+
const platformIdx = args.indexOf("--platform");
|
|
19887
|
+
if (platformIdx >= 0 && args[platformIdx + 1]) {
|
|
19888
|
+
const platform = args[platformIdx + 1];
|
|
19889
|
+
const filePath = args.filter((_, i) => i !== platformIdx && i !== platformIdx + 1)[0];
|
|
19890
|
+
if (!cfg.outro) cfg.outro = { platforms: {} };
|
|
19891
|
+
if (!cfg.outro.platforms) cfg.outro.platforms = {};
|
|
19892
|
+
cfg.outro.platforms[platform] = filePath;
|
|
19893
|
+
console.log(`\u2705 Outro for ${platform}: ${filePath}`);
|
|
19894
|
+
} else {
|
|
19895
|
+
if (!cfg.outro) cfg.outro = {};
|
|
19896
|
+
cfg.outro.default = args[0];
|
|
19897
|
+
console.log(`\u2705 Default outro: ${args[0]}`);
|
|
19898
|
+
}
|
|
19899
|
+
brand.introOutro = cfg;
|
|
19900
|
+
await saveBrand(brandPath, brand);
|
|
19901
|
+
}
|
|
19902
|
+
async function handleSetIntroRatio(brand, brandPath, args) {
|
|
19903
|
+
if (args.length < 2) {
|
|
19904
|
+
console.error("Usage: vidpipe intro-outro set-intro-ratio <ratio> <path>");
|
|
19905
|
+
console.error(` ratio: ${ASPECT_RATIOS.join(", ")}`);
|
|
19906
|
+
process.exitCode = 1;
|
|
19907
|
+
return;
|
|
19908
|
+
}
|
|
19909
|
+
const ratio = args[0];
|
|
19910
|
+
if (!ASPECT_RATIOS.includes(ratio)) {
|
|
19911
|
+
console.error(`Unknown aspect ratio: ${ratio}. Must be one of: ${ASPECT_RATIOS.join(", ")}`);
|
|
19912
|
+
process.exitCode = 1;
|
|
19913
|
+
return;
|
|
19914
|
+
}
|
|
19915
|
+
const cfg = getIntroOutro(brand);
|
|
19916
|
+
if (!cfg.intro) cfg.intro = {};
|
|
19917
|
+
if (!cfg.intro.aspectRatios) cfg.intro.aspectRatios = {};
|
|
19918
|
+
cfg.intro.aspectRatios[ratio] = args[1];
|
|
19919
|
+
brand.introOutro = cfg;
|
|
19920
|
+
await saveBrand(brandPath, brand);
|
|
19921
|
+
console.log(`\u2705 Intro for ${ratio}: ${args[1]}`);
|
|
19922
|
+
}
|
|
19923
|
+
async function handleSetOutroRatio(brand, brandPath, args) {
|
|
19924
|
+
if (args.length < 2) {
|
|
19925
|
+
console.error("Usage: vidpipe intro-outro set-outro-ratio <ratio> <path>");
|
|
19926
|
+
console.error(` ratio: ${ASPECT_RATIOS.join(", ")}`);
|
|
19927
|
+
process.exitCode = 1;
|
|
19928
|
+
return;
|
|
19929
|
+
}
|
|
19930
|
+
const ratio = args[0];
|
|
19931
|
+
if (!ASPECT_RATIOS.includes(ratio)) {
|
|
19932
|
+
console.error(`Unknown aspect ratio: ${ratio}. Must be one of: ${ASPECT_RATIOS.join(", ")}`);
|
|
19933
|
+
process.exitCode = 1;
|
|
19934
|
+
return;
|
|
19935
|
+
}
|
|
19936
|
+
const cfg = getIntroOutro(brand);
|
|
19937
|
+
if (!cfg.outro) cfg.outro = {};
|
|
19938
|
+
if (!cfg.outro.aspectRatios) cfg.outro.aspectRatios = {};
|
|
19939
|
+
cfg.outro.aspectRatios[ratio] = args[1];
|
|
19940
|
+
brand.introOutro = cfg;
|
|
19941
|
+
await saveBrand(brandPath, brand);
|
|
19942
|
+
console.log(`\u2705 Outro for ${ratio}: ${args[1]}`);
|
|
19943
|
+
}
|
|
19944
|
+
async function handleSetFade(brand, brandPath, args) {
|
|
19945
|
+
if (args.length < 1) {
|
|
19946
|
+
console.error("Usage: vidpipe intro-outro set-fade <seconds>");
|
|
19947
|
+
process.exitCode = 1;
|
|
19948
|
+
return;
|
|
19949
|
+
}
|
|
19950
|
+
const duration = parseFloat(args[0]);
|
|
19951
|
+
if (!isFinite(duration) || duration < 0) {
|
|
19952
|
+
console.error("Fade duration must be a non-negative number");
|
|
19953
|
+
process.exitCode = 1;
|
|
19954
|
+
return;
|
|
19955
|
+
}
|
|
19956
|
+
const cfg = getIntroOutro(brand);
|
|
19957
|
+
cfg.fadeDuration = duration;
|
|
19958
|
+
brand.introOutro = cfg;
|
|
19959
|
+
await saveBrand(brandPath, brand);
|
|
19960
|
+
console.log(`\u2705 Fade duration: ${duration}s${duration === 0 ? " (hard cut)" : ""}`);
|
|
19961
|
+
}
|
|
19962
|
+
async function handleSetRule(brand, brandPath, args) {
|
|
19963
|
+
if (args.length < 3) {
|
|
19964
|
+
console.error("Usage: vidpipe intro-outro set-rule <video-type> <intro|outro|both> <on|off>");
|
|
19965
|
+
console.error(" video-type: main, shorts, medium-clips");
|
|
19966
|
+
console.error(" Example: vidpipe intro-outro set-rule shorts intro off");
|
|
19967
|
+
process.exitCode = 1;
|
|
19968
|
+
return;
|
|
19969
|
+
}
|
|
19970
|
+
const videoType = args[0];
|
|
19971
|
+
if (!VIDEO_TYPES.includes(videoType)) {
|
|
19972
|
+
console.error(`Unknown video type: ${args[0]}. Must be one of: ${VIDEO_TYPES.join(", ")}`);
|
|
19973
|
+
process.exitCode = 1;
|
|
19974
|
+
return;
|
|
19975
|
+
}
|
|
19976
|
+
const target = args[1];
|
|
19977
|
+
const rawValue = args[2].toLowerCase();
|
|
19978
|
+
if (!["on", "off", "true", "false"].includes(rawValue)) {
|
|
19979
|
+
console.error(`Invalid value: ${args[2]}. Must be one of: on, off, true, false`);
|
|
19980
|
+
process.exitCode = 1;
|
|
19981
|
+
return;
|
|
19982
|
+
}
|
|
19983
|
+
const value = rawValue === "on" || rawValue === "true";
|
|
19984
|
+
const cfg = getIntroOutro(brand);
|
|
19985
|
+
if (!cfg.rules) cfg.rules = {};
|
|
19986
|
+
const current = cfg.rules[videoType] ?? { intro: true, outro: true };
|
|
19987
|
+
if (target === "intro") current.intro = value;
|
|
19988
|
+
else if (target === "outro") current.outro = value;
|
|
19989
|
+
else if (target === "both") {
|
|
19990
|
+
current.intro = value;
|
|
19991
|
+
current.outro = value;
|
|
19992
|
+
} else {
|
|
19993
|
+
console.error(`Unknown target: ${target}. Must be intro, outro, or both`);
|
|
19994
|
+
process.exitCode = 1;
|
|
19995
|
+
return;
|
|
19996
|
+
}
|
|
19997
|
+
cfg.rules[videoType] = current;
|
|
19998
|
+
brand.introOutro = cfg;
|
|
19999
|
+
await saveBrand(brandPath, brand);
|
|
20000
|
+
console.log(`\u2705 ${videoType}: intro=${current.intro ? "on" : "off"}, outro=${current.outro ? "on" : "off"}`);
|
|
20001
|
+
}
|
|
20002
|
+
function printHelp() {
|
|
20003
|
+
console.log(`
|
|
20004
|
+
Usage: vidpipe intro-outro [subcommand] [args...]
|
|
20005
|
+
|
|
20006
|
+
Manage video intro and outro configuration in brand.json.
|
|
20007
|
+
|
|
20008
|
+
Subcommands:
|
|
20009
|
+
(none) Interactive setup wizard
|
|
20010
|
+
show Display current intro/outro configuration
|
|
20011
|
+
enable Enable intro/outro processing
|
|
20012
|
+
disable Disable intro/outro processing
|
|
20013
|
+
set-intro <path> [--platform <name>] Set intro video file path
|
|
20014
|
+
set-outro <path> [--platform <name>] Set outro video file path
|
|
20015
|
+
set-intro-ratio <ratio> <path> Set aspect-ratio-specific intro file
|
|
20016
|
+
set-outro-ratio <ratio> <path> Set aspect-ratio-specific outro file
|
|
20017
|
+
set-fade <seconds> Set crossfade duration (0 = hard cut)
|
|
20018
|
+
set-rule <type> <intro|outro|both> <on|off> Configure per-video-type rules
|
|
20019
|
+
types: main, shorts, medium-clips
|
|
20020
|
+
ratios: ${ASPECT_RATIOS.join(", ")}
|
|
20021
|
+
|
|
20022
|
+
Examples:
|
|
20023
|
+
vidpipe intro-outro # Interactive wizard
|
|
20024
|
+
vidpipe intro-outro show # Show current config
|
|
20025
|
+
vidpipe intro-outro enable # Turn on intro/outro
|
|
20026
|
+
vidpipe intro-outro set-intro ./assets/intro-yt.mp4 --platform youtube
|
|
20027
|
+
vidpipe intro-outro set-intro-ratio 9:16 ./assets/intro-portrait.mp4
|
|
20028
|
+
vidpipe intro-outro set-outro-ratio 1:1 ./assets/outro-square.mp4
|
|
20029
|
+
vidpipe intro-outro set-fade 1.0 # 1-second crossfade
|
|
20030
|
+
vidpipe intro-outro set-rule shorts intro off
|
|
20031
|
+
`);
|
|
20032
|
+
}
|
|
20033
|
+
async function runIntroOutro(subcommand, args = []) {
|
|
20034
|
+
const { brand, brandPath } = await loadBrand();
|
|
20035
|
+
switch (subcommand) {
|
|
20036
|
+
case void 0:
|
|
20037
|
+
await runWizard(brand, brandPath);
|
|
20038
|
+
break;
|
|
20039
|
+
case "show":
|
|
20040
|
+
showConfig(getIntroOutro(brand));
|
|
20041
|
+
break;
|
|
20042
|
+
case "enable":
|
|
20043
|
+
await handleEnable(brand, brandPath);
|
|
20044
|
+
break;
|
|
20045
|
+
case "disable":
|
|
20046
|
+
await handleDisable(brand, brandPath);
|
|
20047
|
+
break;
|
|
20048
|
+
case "set-intro":
|
|
20049
|
+
await handleSetIntro(brand, brandPath, args);
|
|
20050
|
+
break;
|
|
20051
|
+
case "set-outro":
|
|
20052
|
+
await handleSetOutro(brand, brandPath, args);
|
|
20053
|
+
break;
|
|
20054
|
+
case "set-intro-ratio":
|
|
20055
|
+
await handleSetIntroRatio(brand, brandPath, args);
|
|
20056
|
+
break;
|
|
20057
|
+
case "set-outro-ratio":
|
|
20058
|
+
await handleSetOutroRatio(brand, brandPath, args);
|
|
20059
|
+
break;
|
|
20060
|
+
case "set-fade":
|
|
20061
|
+
await handleSetFade(brand, brandPath, args);
|
|
20062
|
+
break;
|
|
20063
|
+
case "set-rule":
|
|
20064
|
+
await handleSetRule(brand, brandPath, args);
|
|
20065
|
+
break;
|
|
20066
|
+
case "help":
|
|
20067
|
+
case "--help":
|
|
20068
|
+
printHelp();
|
|
20069
|
+
break;
|
|
20070
|
+
default:
|
|
20071
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
20072
|
+
printHelp();
|
|
20073
|
+
process.exitCode = 1;
|
|
20074
|
+
}
|
|
20075
|
+
}
|
|
20076
|
+
|
|
20077
|
+
// src/L7-app/commands/ideaUpdate.ts
|
|
20078
|
+
init_environment();
|
|
20079
|
+
init_ideaService();
|
|
20080
|
+
init_types();
|
|
20081
|
+
var VALID_PLATFORMS2 = new Set(Object.values(Platform));
|
|
20082
|
+
var VALID_STATUSES = /* @__PURE__ */ new Set(["draft", "ready", "recorded", "published"]);
|
|
20083
|
+
var VALID_URGENCIES = /* @__PURE__ */ new Map([
|
|
20084
|
+
["hot", 3],
|
|
20085
|
+
// 3 days from now
|
|
20086
|
+
["urgent", 7],
|
|
20087
|
+
// 7 days (= hot-trend)
|
|
20088
|
+
["soon", 14],
|
|
20089
|
+
// 14 days (= timely)
|
|
20090
|
+
["flexible", 60]
|
|
20091
|
+
// 60 days (= evergreen)
|
|
20092
|
+
]);
|
|
20093
|
+
function parsePlatforms2(raw) {
|
|
20094
|
+
if (!raw) return void 0;
|
|
20095
|
+
return raw.split(",").map((p) => p.trim().toLowerCase()).filter((p) => VALID_PLATFORMS2.has(p));
|
|
20096
|
+
}
|
|
20097
|
+
function parseCommaSeparated2(raw) {
|
|
20098
|
+
if (!raw) return void 0;
|
|
20099
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
20100
|
+
}
|
|
20101
|
+
function resolveUrgency(urgency) {
|
|
20102
|
+
const days = VALID_URGENCIES.get(urgency.toLowerCase());
|
|
20103
|
+
if (days === void 0) {
|
|
20104
|
+
throw new Error(`Invalid urgency: ${urgency}. Must be one of: ${[...VALID_URGENCIES.keys()].join(", ")}`);
|
|
20105
|
+
}
|
|
20106
|
+
const date = /* @__PURE__ */ new Date();
|
|
20107
|
+
date.setDate(date.getDate() + days);
|
|
20108
|
+
return date.toISOString().split("T")[0];
|
|
20109
|
+
}
|
|
20110
|
+
async function runIdeaUpdate(issueNumber, options) {
|
|
20111
|
+
initConfig();
|
|
20112
|
+
const num = parseInt(issueNumber, 10);
|
|
20113
|
+
if (!isFinite(num) || num <= 0) {
|
|
20114
|
+
console.error(`Invalid issue number: ${issueNumber}`);
|
|
20115
|
+
process.exitCode = 1;
|
|
20116
|
+
return;
|
|
20117
|
+
}
|
|
20118
|
+
if (options.status && !VALID_STATUSES.has(options.status)) {
|
|
20119
|
+
console.error(`Invalid status: ${options.status}. Must be one of: ${[...VALID_STATUSES].join(", ")}`);
|
|
20120
|
+
process.exitCode = 1;
|
|
20121
|
+
return;
|
|
20122
|
+
}
|
|
20123
|
+
let publishBy = options.publishBy;
|
|
20124
|
+
if (options.urgency) {
|
|
20125
|
+
try {
|
|
20126
|
+
publishBy = resolveUrgency(options.urgency);
|
|
20127
|
+
} catch (err) {
|
|
20128
|
+
console.error(err.message);
|
|
20129
|
+
process.exitCode = 1;
|
|
20130
|
+
return;
|
|
20131
|
+
}
|
|
20132
|
+
}
|
|
20133
|
+
const updates = {};
|
|
20134
|
+
if (options.topic !== void 0) updates.topic = options.topic;
|
|
20135
|
+
if (options.hook !== void 0) updates.hook = options.hook;
|
|
20136
|
+
if (options.audience !== void 0) updates.audience = options.audience;
|
|
20137
|
+
if (options.keyTakeaway !== void 0) updates.keyTakeaway = options.keyTakeaway;
|
|
20138
|
+
if (options.trendContext !== void 0) updates.trendContext = options.trendContext;
|
|
20139
|
+
if (options.status !== void 0) updates.status = options.status;
|
|
20140
|
+
const platforms = parsePlatforms2(options.platforms);
|
|
20141
|
+
if (platforms) updates.platforms = platforms;
|
|
20142
|
+
const talkingPoints = parseCommaSeparated2(options.talkingPoints);
|
|
20143
|
+
if (talkingPoints) updates.talkingPoints = talkingPoints;
|
|
20144
|
+
const tags = parseCommaSeparated2(options.tags);
|
|
20145
|
+
if (tags) updates.tags = tags;
|
|
20146
|
+
if (publishBy) updates.publishBy = publishBy;
|
|
20147
|
+
if (Object.keys(updates).length === 0) {
|
|
20148
|
+
console.error("No updates specified. Use --topic, --status, --urgency, etc.");
|
|
20149
|
+
process.exitCode = 1;
|
|
20150
|
+
return;
|
|
20151
|
+
}
|
|
20152
|
+
try {
|
|
20153
|
+
const idea = await updateIdea(num, updates);
|
|
20154
|
+
console.log(`\u2705 Idea #${idea.issueNumber} updated: ${idea.topic}`);
|
|
20155
|
+
console.log(` Status: ${idea.status}`);
|
|
20156
|
+
console.log(` Publish by: ${idea.publishBy}`);
|
|
20157
|
+
console.log(` URL: ${idea.issueUrl}`);
|
|
20158
|
+
} catch (err) {
|
|
20159
|
+
console.error(`Failed to update idea #${num}: ${err.message}`);
|
|
20160
|
+
process.exitCode = 1;
|
|
20161
|
+
}
|
|
20162
|
+
}
|
|
20163
|
+
async function runIdeaGet(issueNumber) {
|
|
20164
|
+
initConfig();
|
|
20165
|
+
const num = parseInt(issueNumber, 10);
|
|
20166
|
+
if (!isFinite(num) || num <= 0) {
|
|
20167
|
+
console.error(`Invalid issue number: ${issueNumber}`);
|
|
20168
|
+
process.exitCode = 1;
|
|
20169
|
+
return;
|
|
20170
|
+
}
|
|
20171
|
+
try {
|
|
20172
|
+
const idea = await getIdea(num);
|
|
20173
|
+
if (!idea) {
|
|
20174
|
+
console.error(`Idea #${num} not found`);
|
|
20175
|
+
process.exitCode = 1;
|
|
20176
|
+
return;
|
|
20177
|
+
}
|
|
20178
|
+
console.log(`
|
|
20179
|
+
\u{1F4CB} Idea #${idea.issueNumber}: ${idea.topic}`);
|
|
20180
|
+
console.log(` Status: ${idea.status}`);
|
|
20181
|
+
console.log(` Hook: ${idea.hook}`);
|
|
20182
|
+
console.log(` Audience: ${idea.audience}`);
|
|
20183
|
+
console.log(` Key Takeaway: ${idea.keyTakeaway}`);
|
|
20184
|
+
console.log(` Platforms: ${idea.platforms.join(", ")}`);
|
|
20185
|
+
console.log(` Tags: ${idea.tags.join(", ") || "(none)"}`);
|
|
20186
|
+
console.log(` Publish by: ${idea.publishBy}`);
|
|
20187
|
+
if (idea.trendContext) console.log(` Trend: ${idea.trendContext}`);
|
|
20188
|
+
if (idea.talkingPoints.length > 0) {
|
|
20189
|
+
console.log(` Talking Points:`);
|
|
20190
|
+
for (const pt of idea.talkingPoints) {
|
|
20191
|
+
console.log(` \u2022 ${pt}`);
|
|
20192
|
+
}
|
|
20193
|
+
}
|
|
20194
|
+
console.log(` URL: ${idea.issueUrl}`);
|
|
20195
|
+
console.log();
|
|
20196
|
+
} catch (err) {
|
|
20197
|
+
console.error(`Failed to get idea #${num}: ${err.message}`);
|
|
20198
|
+
process.exitCode = 1;
|
|
20199
|
+
}
|
|
20200
|
+
}
|
|
20201
|
+
function printIdeaTable(ideas) {
|
|
20202
|
+
console.log(`
|
|
20203
|
+
${"#".padEnd(6)} ${"Topic".padEnd(40)} ${"Status".padEnd(12)} ${"Publish By".padEnd(12)} ${"Platforms"}`);
|
|
20204
|
+
console.log("\u2500".repeat(100));
|
|
20205
|
+
for (const idea of ideas) {
|
|
20206
|
+
console.log(
|
|
20207
|
+
`${String(idea.issueNumber).padEnd(6)} ${idea.topic.substring(0, 38).padEnd(40)} ${idea.status.padEnd(12)} ${idea.publishBy.padEnd(12)} ${idea.platforms.join(", ")}`
|
|
20208
|
+
);
|
|
20209
|
+
}
|
|
20210
|
+
console.log(`
|
|
20211
|
+
${ideas.length} idea(s) found`);
|
|
20212
|
+
}
|
|
20213
|
+
async function runIdeaSearch(query, options) {
|
|
20214
|
+
initConfig();
|
|
20215
|
+
try {
|
|
20216
|
+
let ideas;
|
|
20217
|
+
if (query) {
|
|
20218
|
+
ideas = await searchIdeas(query);
|
|
20219
|
+
} else {
|
|
20220
|
+
ideas = await listIdeas({
|
|
20221
|
+
status: options.status,
|
|
20222
|
+
platform: options.platform,
|
|
20223
|
+
tag: options.tag,
|
|
20224
|
+
priority: options.priority
|
|
20225
|
+
});
|
|
20226
|
+
}
|
|
20227
|
+
if (query && options.status) {
|
|
20228
|
+
ideas = ideas.filter((i) => i.status === options.status);
|
|
20229
|
+
}
|
|
20230
|
+
if (ideas.length === 0) {
|
|
20231
|
+
console.log("No ideas found.");
|
|
20232
|
+
return;
|
|
20233
|
+
}
|
|
20234
|
+
if (options.format === "json") {
|
|
20235
|
+
console.log(JSON.stringify(ideas.map((i) => ({
|
|
20236
|
+
issueNumber: i.issueNumber,
|
|
20237
|
+
topic: i.topic,
|
|
20238
|
+
hook: i.hook,
|
|
20239
|
+
status: i.status,
|
|
20240
|
+
platforms: i.platforms,
|
|
20241
|
+
publishBy: i.publishBy,
|
|
20242
|
+
tags: i.tags,
|
|
20243
|
+
issueUrl: i.issueUrl
|
|
20244
|
+
})), null, 2));
|
|
20245
|
+
return;
|
|
20246
|
+
}
|
|
20247
|
+
printIdeaTable(ideas);
|
|
20248
|
+
} catch (err) {
|
|
20249
|
+
console.error(`Failed to search ideas: ${err.message}`);
|
|
20250
|
+
process.exitCode = 1;
|
|
20251
|
+
}
|
|
20252
|
+
}
|
|
20253
|
+
|
|
19194
20254
|
// src/L1-infra/http/http.ts
|
|
19195
20255
|
import { default as default8 } from "express";
|
|
19196
20256
|
import { Router } from "express";
|
|
@@ -19840,11 +20900,36 @@ program.command("ideate").description("Generate AI-powered content ideas using t
|
|
|
19840
20900
|
await runIdeate(opts);
|
|
19841
20901
|
process.exit(0);
|
|
19842
20902
|
});
|
|
20903
|
+
program.command("idea").description("Manage ideas \u2014 view, update, and search existing ideas").argument("<subcommand>", "Subcommand: get, update, search").argument("[arg]", "Issue number (get/update) or search query (search)").option("--topic <topic>", "Update the idea topic/title").option("--hook <hook>", "Update the attention-grabbing hook").option("--audience <audience>", "Update the target audience").option("--platforms <platforms>", "Target platforms (comma-separated)").option("--key-takeaway <takeaway>", "Update the core message").option("--talking-points <points>", "Update talking points (comma-separated)").option("--tags <tags>", "Filter/update tags (comma-separated)").option("--status <status>", "Filter/update status (draft|ready|recorded|published)").option("--publish-by <date>", "Update publish deadline (ISO 8601 date)").option("--urgency <level>", "Set urgency: hot (3d), urgent (7d), soon (14d), flexible (60d)").option("--trend-context <context>", "Update trend context").option("--priority <level>", "Filter by priority: hot-trend, timely, evergreen").option("--platform <platform>", "Filter by platform").option("--tag <tag>", "Filter by tag").option("--format <format>", "Output format: table (default) or json").action(async (subcommand, arg, opts) => {
|
|
20904
|
+
initConfig();
|
|
20905
|
+
if (subcommand === "update") {
|
|
20906
|
+
if (!arg) {
|
|
20907
|
+
console.error("Usage: vidpipe idea update <issue-number> [options]");
|
|
20908
|
+
process.exitCode = 1;
|
|
20909
|
+
} else await runIdeaUpdate(arg, opts);
|
|
20910
|
+
} else if (subcommand === "get") {
|
|
20911
|
+
if (!arg) {
|
|
20912
|
+
console.error("Usage: vidpipe idea get <issue-number>");
|
|
20913
|
+
process.exitCode = 1;
|
|
20914
|
+
} else await runIdeaGet(arg);
|
|
20915
|
+
} else if (subcommand === "search") {
|
|
20916
|
+
await runIdeaSearch(arg, opts);
|
|
20917
|
+
} else {
|
|
20918
|
+
console.error(`Unknown subcommand: ${subcommand}. Use 'get', 'update', or 'search'`);
|
|
20919
|
+
process.exitCode = 1;
|
|
20920
|
+
}
|
|
20921
|
+
process.exit(process.exitCode ?? 0);
|
|
20922
|
+
});
|
|
19843
20923
|
program.command("configure [subcommand]").description("Manage global configuration \u2014 API keys, defaults, and preferences").argument("[args...]", "Arguments for the subcommand (e.g., key and value for set)").action(async (subcommand, args) => {
|
|
19844
20924
|
await runConfigure(subcommand, args);
|
|
19845
20925
|
process.exit(process.exitCode ?? 0);
|
|
19846
20926
|
});
|
|
19847
|
-
|
|
20927
|
+
program.command("intro-outro [subcommand]").description("Manage video intro and outro assets \u2014 paths, rules, and platform overrides").argument("[args...]", "Arguments for the subcommand").action(async (subcommand, args) => {
|
|
20928
|
+
initConfig();
|
|
20929
|
+
await runIntroOutro(subcommand, args);
|
|
20930
|
+
process.exit(process.exitCode ?? 0);
|
|
20931
|
+
});
|
|
20932
|
+
var defaultCmd = program.command("process", { isDefault: true }).argument("[video-path]", "Path to a video file to process (implies --once)").option("--watch-dir <path>", "Folder to watch for new recordings (default: env WATCH_FOLDER)").option("--output-dir <path>", "Output directory for processed videos (default: ./recordings)").option("--openai-key <key>", "OpenAI API key (default: env OPENAI_API_KEY)").option("--exa-key <key>", "Exa AI API key for web search (default: env EXA_API_KEY)").option("--youtube-key <key>", "YouTube API key (default: env YOUTUBE_API_KEY)").option("--perplexity-key <key>", "Perplexity API key (default: env PERPLEXITY_API_KEY)").option("--once", "Process a single video and exit (no watching)").option("--brand <path>", "Path to brand.json config (default: ./brand.json)").option("--no-silence-removal", "Skip silence removal stage").option("--no-shorts", "Skip shorts generation").option("--no-medium-clips", "Skip medium clip generation").option("--no-social", "Skip social media post generation").option("--no-captions", "Skip caption generation/burning").option("--no-visual-enhancement", "Skip visual enhancement (AI image overlays)").option("--no-intro-outro", "Skip intro/outro concatenation").option("--no-social-publish", "Skip social media publishing/queue-build stage").option("--late-api-key <key>", "Late API key (default: env LATE_API_KEY)").option("--late-profile-id <id>", "Late profile ID (default: env LATE_PROFILE_ID)").option("--ideas <ids>", "Comma-separated idea IDs to link to this video").option("-v, --verbose", "Verbose logging").option("--progress", "Emit structured JSON progress events to stderr").option("--doctor", "Check all prerequisites and exit").action(async (videoPath) => {
|
|
19848
20933
|
const opts = defaultCmd.opts();
|
|
19849
20934
|
if (opts.doctor) {
|
|
19850
20935
|
await runDoctor();
|
|
@@ -19866,6 +20951,7 @@ var defaultCmd = program.command("process", { isDefault: true }).argument("[vide
|
|
|
19866
20951
|
social: opts.social,
|
|
19867
20952
|
captions: opts.captions,
|
|
19868
20953
|
visualEnhancement: opts.visualEnhancement,
|
|
20954
|
+
introOutro: opts.introOutro,
|
|
19869
20955
|
socialPublish: opts.socialPublish,
|
|
19870
20956
|
lateApiKey: opts.lateApiKey,
|
|
19871
20957
|
lateProfileId: opts.lateProfileId
|