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 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: "shorts" /* Shorts */, name: "Shorts", stageNumber: 7 },
746
- { stage: "medium-clips" /* MediumClips */, name: "Medium Clips", stageNumber: 8 },
747
- { stage: "chapters" /* Chapters */, name: "Chapters", stageNumber: 9 },
748
- { stage: "summary" /* Summary */, name: "Summary", stageNumber: 10 },
749
- { stage: "social-media" /* SocialMedia */, name: "Social Media", stageNumber: 11 },
750
- { stage: "short-posts" /* ShortPosts */, name: "Short Posts", stageNumber: 12 },
751
- { stage: "medium-clip-posts" /* MediumClipPosts */, name: "Medium Clip Posts", stageNumber: 13 },
752
- { stage: "queue-build" /* QueueBuild */, name: "Queue Build", stageNumber: 14 },
753
- { stage: "blog" /* Blog */, name: "Blog", stageNumber: 15 }
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 join5 } from "path";
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 join5(dirname3(dirname3(sdkPath)), "index.js");
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 join6, dirname as dirname4 } from "path";
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 = join6(base, platformPkg, binaryName);
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 = join6(dir, "node_modules", platformPkg, binaryName);
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: "open",
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
- ideaPosts.sort((a, b) => a.metadata.createdAt.localeCompare(b.metadata.createdAt));
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
- await lateClient.schedulePost(latePostId, newSlotDatetime);
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 getVideoDuration2(videoPath) {
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 getVideoDuration2(video.repoPath);
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 */, () => asset.getShorts()) ?? [];
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 */, () => asset.getMediumClips()) ?? [];
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
- 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-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) => {
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