vidpipe 1.3.10 → 1.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -152,6 +152,7 @@ vidpipe doctor # Check all prerequisites
152
152
  | `--no-git` | Skip git commit/push |
153
153
  | `--late-api-key <key>` | Override Late API key |
154
154
  | `-v, --verbose` | Debug-level logging |
155
+ | `--progress` | Emit structured JSON progress events to stderr |
155
156
  | `--doctor` | Check that all prerequisites are installed |
156
157
 
157
158
  ### Ideate Options
@@ -357,6 +358,28 @@ graph LR
357
358
 
358
359
  Each stage can be independently skipped with `--no-*` flags. A stage failure does not abort the pipeline — subsequent stages proceed with whatever data is available.
359
360
 
361
+ ### Progress Events
362
+
363
+ Pass `--progress` to emit structured JSONL progress events to stderr while normal logs continue on stdout:
364
+
365
+ ```bash
366
+ vidpipe process video.mp4 --progress 2>progress.jsonl
367
+ ```
368
+
369
+ Each line is a self-contained JSON object:
370
+
371
+ ```jsonl
372
+ {"event":"pipeline:start","videoPath":"video.mp4","totalStages":16,"timestamp":"..."}
373
+ {"event":"stage:start","stage":"ingestion","stageNumber":1,"totalStages":16,"name":"Ingestion","timestamp":"..."}
374
+ {"event":"stage:complete","stage":"ingestion","stageNumber":1,"totalStages":16,"name":"Ingestion","duration":423,"success":true,"timestamp":"..."}
375
+ {"event":"stage:skip","stage":"shorts","stageNumber":7,"totalStages":16,"name":"Shorts","reason":"SKIP_SHORTS","timestamp":"..."}
376
+ {"event":"pipeline:complete","totalDuration":45000,"stagesCompleted":14,"stagesFailed":0,"stagesSkipped":2,"timestamp":"..."}
377
+ ```
378
+
379
+ Event types: `pipeline:start`, `stage:start`, `stage:complete`, `stage:error`, `stage:skip`, `pipeline:complete`.
380
+
381
+ Integrating tools can read stderr line-by-line to display a live progress UI (e.g., "Stage 3/16: Silence Removal").
382
+
360
383
  ---
361
384
 
362
385
  ## 🤖 LLM Providers
package/dist/cli.js CHANGED
@@ -634,6 +634,11 @@ var init_configLogger = __esm({
634
634
  });
635
635
 
636
636
  // src/L0-pure/types/index.ts
637
+ function getStageInfo(stage) {
638
+ const info = PIPELINE_STAGES.find((s) => s.stage === stage);
639
+ if (!info) throw new Error(`Unknown pipeline stage: ${stage}`);
640
+ return info;
641
+ }
637
642
  function toLatePlatform(platform) {
638
643
  return platform === "x" /* X */ ? "twitter" : platform;
639
644
  }
@@ -658,7 +663,7 @@ function normalizePlatformString(raw) {
658
663
  function isSupportedVideoExtension(ext) {
659
664
  return SUPPORTED_VIDEO_EXTENSIONS.includes(ext.toLowerCase());
660
665
  }
661
- var Platform, PLATFORM_CHAR_LIMITS, SUPPORTED_VIDEO_EXTENSIONS;
666
+ var Platform, PIPELINE_STAGES, TOTAL_STAGES, PLATFORM_CHAR_LIMITS, SUPPORTED_VIDEO_EXTENSIONS;
662
667
  var init_types = __esm({
663
668
  "src/L0-pure/types/index.ts"() {
664
669
  "use strict";
@@ -670,6 +675,25 @@ var init_types = __esm({
670
675
  Platform2["X"] = "x";
671
676
  return Platform2;
672
677
  })(Platform || {});
678
+ PIPELINE_STAGES = [
679
+ { stage: "ingestion" /* Ingestion */, name: "Ingestion", stageNumber: 1 },
680
+ { stage: "transcription" /* Transcription */, name: "Transcription", stageNumber: 2 },
681
+ { stage: "silence-removal" /* SilenceRemoval */, name: "Silence Removal", stageNumber: 3 },
682
+ { stage: "visual-enhancement" /* VisualEnhancement */, name: "Visual Enhancement", stageNumber: 4 },
683
+ { stage: "captions" /* Captions */, name: "Captions", stageNumber: 5 },
684
+ { stage: "caption-burn" /* CaptionBurn */, name: "Caption Burn", stageNumber: 6 },
685
+ { stage: "shorts" /* Shorts */, name: "Shorts", stageNumber: 7 },
686
+ { stage: "medium-clips" /* MediumClips */, name: "Medium Clips", stageNumber: 8 },
687
+ { stage: "chapters" /* Chapters */, name: "Chapters", stageNumber: 9 },
688
+ { stage: "summary" /* Summary */, name: "Summary", stageNumber: 10 },
689
+ { stage: "social-media" /* SocialMedia */, name: "Social Media", stageNumber: 11 },
690
+ { stage: "short-posts" /* ShortPosts */, name: "Short Posts", stageNumber: 12 },
691
+ { stage: "medium-clip-posts" /* MediumClipPosts */, name: "Medium Clip Posts", stageNumber: 13 },
692
+ { stage: "queue-build" /* QueueBuild */, name: "Queue Build", stageNumber: 14 },
693
+ { stage: "blog" /* Blog */, name: "Blog", stageNumber: 15 },
694
+ { stage: "git-push" /* GitPush */, name: "Git Push", stageNumber: 16 }
695
+ ];
696
+ TOTAL_STAGES = PIPELINE_STAGES.length;
673
697
  PLATFORM_CHAR_LIMITS = {
674
698
  tiktok: 2200,
675
699
  youtube: 5e3,
@@ -2570,6 +2594,32 @@ init_fileSystem();
2570
2594
  init_configLogger();
2571
2595
  init_environment();
2572
2596
 
2597
+ // src/L1-infra/progress/progressEmitter.ts
2598
+ var ProgressEmitter = class {
2599
+ enabled = false;
2600
+ /** Turn on progress event output to stderr. */
2601
+ enable() {
2602
+ this.enabled = true;
2603
+ }
2604
+ /** Turn off progress event output. */
2605
+ disable() {
2606
+ this.enabled = false;
2607
+ }
2608
+ /** Whether the emitter is currently active. */
2609
+ isEnabled() {
2610
+ return this.enabled;
2611
+ }
2612
+ /**
2613
+ * Write a progress event as a single JSON line to stderr.
2614
+ * No-op when the emitter is disabled.
2615
+ */
2616
+ emit(event) {
2617
+ if (!this.enabled) return;
2618
+ process.stderr.write(JSON.stringify(event) + "\n");
2619
+ }
2620
+ };
2621
+ var progressEmitter = new ProgressEmitter();
2622
+
2573
2623
  // src/L5-assets/Asset.ts
2574
2624
  init_fileSystem();
2575
2625
  var Asset = class {
@@ -12498,17 +12548,54 @@ function createScheduleAgent(...args) {
12498
12548
  init_types();
12499
12549
  async function runStage(stageName, fn, stageResults) {
12500
12550
  const start = Date.now();
12551
+ if (progressEmitter.isEnabled()) {
12552
+ const info = getStageInfo(stageName);
12553
+ progressEmitter.emit({
12554
+ event: "stage:start",
12555
+ stage: stageName,
12556
+ stageNumber: info.stageNumber,
12557
+ totalStages: TOTAL_STAGES,
12558
+ name: info.name,
12559
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
12560
+ });
12561
+ }
12501
12562
  try {
12502
12563
  const result = await fn();
12503
12564
  const duration = Date.now() - start;
12504
12565
  stageResults.push({ stage: stageName, success: true, duration });
12505
12566
  logger_default.info(`Stage ${stageName} completed in ${duration}ms`);
12567
+ if (progressEmitter.isEnabled()) {
12568
+ const info = getStageInfo(stageName);
12569
+ progressEmitter.emit({
12570
+ event: "stage:complete",
12571
+ stage: stageName,
12572
+ stageNumber: info.stageNumber,
12573
+ totalStages: TOTAL_STAGES,
12574
+ name: info.name,
12575
+ duration,
12576
+ success: true,
12577
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
12578
+ });
12579
+ }
12506
12580
  return result;
12507
12581
  } catch (err) {
12508
12582
  const duration = Date.now() - start;
12509
12583
  const message = err instanceof Error ? err.message : String(err);
12510
12584
  stageResults.push({ stage: stageName, success: false, error: message, duration });
12511
12585
  logger_default.error(`Stage ${stageName} failed after ${duration}ms: ${message}`);
12586
+ if (progressEmitter.isEnabled()) {
12587
+ const info = getStageInfo(stageName);
12588
+ progressEmitter.emit({
12589
+ event: "stage:error",
12590
+ stage: stageName,
12591
+ stageNumber: info.stageNumber,
12592
+ totalStages: TOTAL_STAGES,
12593
+ name: info.name,
12594
+ duration,
12595
+ error: message,
12596
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
12597
+ });
12598
+ }
12512
12599
  return void 0;
12513
12600
  }
12514
12601
  }
@@ -12516,16 +12603,50 @@ async function processVideo(videoPath, ideas) {
12516
12603
  const pipelineStart = Date.now();
12517
12604
  const stageResults = [];
12518
12605
  const cfg = getConfig();
12606
+ let stagesSkipped = 0;
12519
12607
  costTracker3.reset();
12520
12608
  function trackStage(stage, fn) {
12521
12609
  costTracker3.setStage(stage);
12522
12610
  return runStage(stage, fn, stageResults);
12523
12611
  }
12612
+ function skipStage(stage, reason) {
12613
+ stagesSkipped++;
12614
+ if (progressEmitter.isEnabled()) {
12615
+ const info = getStageInfo(stage);
12616
+ progressEmitter.emit({
12617
+ event: "stage:skip",
12618
+ stage,
12619
+ stageNumber: info.stageNumber,
12620
+ totalStages: TOTAL_STAGES,
12621
+ name: info.name,
12622
+ reason,
12623
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
12624
+ });
12625
+ }
12626
+ }
12627
+ if (progressEmitter.isEnabled()) {
12628
+ progressEmitter.emit({
12629
+ event: "pipeline:start",
12630
+ videoPath,
12631
+ totalStages: TOTAL_STAGES,
12632
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
12633
+ });
12634
+ }
12524
12635
  logger_default.info(`Pipeline starting for: ${videoPath}`);
12525
12636
  const asset = await trackStage("ingestion" /* Ingestion */, () => MainVideoAsset.ingest(videoPath));
12526
12637
  if (!asset) {
12527
12638
  const totalDuration = Date.now() - pipelineStart;
12528
12639
  logger_default.error("Ingestion failed \u2014 cannot proceed without video metadata");
12640
+ if (progressEmitter.isEnabled()) {
12641
+ progressEmitter.emit({
12642
+ event: "pipeline:complete",
12643
+ totalDuration,
12644
+ stagesCompleted: 0,
12645
+ stagesFailed: 1,
12646
+ stagesSkipped: 0,
12647
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
12648
+ });
12649
+ }
12529
12650
  return {
12530
12651
  video: { originalPath: videoPath, repoPath: "", videoDir: "", slug: "", filename: "", duration: 0, size: 0, createdAt: /* @__PURE__ */ new Date() },
12531
12652
  transcript: void 0,
@@ -12552,28 +12673,40 @@ async function processVideo(videoPath, ideas) {
12552
12673
  let editedVideoPath;
12553
12674
  if (!cfg.SKIP_SILENCE_REMOVAL) {
12554
12675
  editedVideoPath = await trackStage("silence-removal" /* SilenceRemoval */, () => asset.getEditedVideo());
12676
+ } else {
12677
+ skipStage("silence-removal" /* SilenceRemoval */, "SKIP_SILENCE_REMOVAL");
12555
12678
  }
12556
12679
  let enhancedVideoPath;
12557
12680
  if (!cfg.SKIP_VISUAL_ENHANCEMENT) {
12558
12681
  enhancedVideoPath = await trackStage("visual-enhancement" /* VisualEnhancement */, () => asset.getEnhancedVideo());
12682
+ } else {
12683
+ skipStage("visual-enhancement" /* VisualEnhancement */, "SKIP_VISUAL_ENHANCEMENT");
12559
12684
  }
12560
12685
  let captions;
12561
12686
  if (!cfg.SKIP_CAPTIONS) {
12562
12687
  captions = await trackStage("captions" /* Captions */, () => asset.getCaptions());
12688
+ } else {
12689
+ skipStage("captions" /* Captions */, "SKIP_CAPTIONS");
12563
12690
  }
12564
12691
  let captionedVideoPath;
12565
12692
  if (!cfg.SKIP_CAPTIONS) {
12566
12693
  captionedVideoPath = await trackStage("caption-burn" /* CaptionBurn */, () => asset.getCaptionedVideo());
12694
+ } else {
12695
+ skipStage("caption-burn" /* CaptionBurn */, "SKIP_CAPTIONS");
12567
12696
  }
12568
12697
  let shorts = [];
12569
12698
  if (!cfg.SKIP_SHORTS) {
12570
12699
  const shortAssets = await trackStage("shorts" /* Shorts */, () => asset.getShorts()) ?? [];
12571
12700
  shorts = shortAssets.map((s) => s.clip);
12701
+ } else {
12702
+ skipStage("shorts" /* Shorts */, "SKIP_SHORTS");
12572
12703
  }
12573
12704
  let mediumClips = [];
12574
12705
  if (!cfg.SKIP_MEDIUM_CLIPS) {
12575
12706
  const mediumAssets = await trackStage("medium-clips" /* MediumClips */, () => asset.getMediumClips()) ?? [];
12576
12707
  mediumClips = mediumAssets.map((m) => m.clip);
12708
+ } else {
12709
+ skipStage("medium-clips" /* MediumClips */, "SKIP_MEDIUM_CLIPS");
12577
12710
  }
12578
12711
  const chapters = await trackStage("chapters" /* Chapters */, () => asset.getChapters());
12579
12712
  const summary = await trackStage("summary" /* Summary */, () => asset.getSummary());
@@ -12588,6 +12721,8 @@ async function processVideo(videoPath, ideas) {
12588
12721
  socialPosts.push(...posts);
12589
12722
  }
12590
12723
  });
12724
+ } else {
12725
+ skipStage("short-posts" /* ShortPosts */, "NO_SHORTS");
12591
12726
  }
12592
12727
  if (mediumClips.length > 0) {
12593
12728
  await trackStage("medium-clip-posts" /* MediumClipPosts */, async () => {
@@ -12596,14 +12731,26 @@ async function processVideo(videoPath, ideas) {
12596
12731
  socialPosts.push(...posts);
12597
12732
  }
12598
12733
  });
12734
+ } else {
12735
+ skipStage("medium-clip-posts" /* MediumClipPosts */, "NO_MEDIUM_CLIPS");
12599
12736
  }
12737
+ } else {
12738
+ skipStage("social-media" /* SocialMedia */, "SKIP_SOCIAL");
12739
+ skipStage("short-posts" /* ShortPosts */, "SKIP_SOCIAL");
12740
+ skipStage("medium-clip-posts" /* MediumClipPosts */, "SKIP_SOCIAL");
12600
12741
  }
12601
12742
  if (!cfg.SKIP_SOCIAL_PUBLISH && socialPosts.length > 0) {
12602
12743
  await trackStage("queue-build" /* QueueBuild */, () => asset.buildQueue(shorts, mediumClips, socialPosts, captionedVideoPath));
12744
+ } else if (cfg.SKIP_SOCIAL_PUBLISH) {
12745
+ skipStage("queue-build" /* QueueBuild */, "SKIP_SOCIAL_PUBLISH");
12746
+ } else {
12747
+ skipStage("queue-build" /* QueueBuild */, "NO_SOCIAL_POSTS");
12603
12748
  }
12604
12749
  const blogPost = await trackStage("blog" /* Blog */, () => asset.getBlog());
12605
12750
  if (!cfg.SKIP_GIT) {
12606
12751
  await trackStage("git-push" /* GitPush */, () => asset.commitAndPushChanges());
12752
+ } else {
12753
+ skipStage("git-push" /* GitPush */, "SKIP_GIT");
12607
12754
  }
12608
12755
  const totalDuration = Date.now() - pipelineStart;
12609
12756
  const report = costTracker3.getReport();
@@ -12614,6 +12761,18 @@ async function processVideo(videoPath, ideas) {
12614
12761
  await writeTextFile(costPath, costMd);
12615
12762
  logger_default.info(`Cost report saved: ${costPath}`);
12616
12763
  }
12764
+ const stagesCompleted = stageResults.filter((r) => r.success).length;
12765
+ const stagesFailed = stageResults.filter((r) => !r.success).length;
12766
+ if (progressEmitter.isEnabled()) {
12767
+ progressEmitter.emit({
12768
+ event: "pipeline:complete",
12769
+ totalDuration,
12770
+ stagesCompleted,
12771
+ stagesFailed,
12772
+ stagesSkipped,
12773
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
12774
+ });
12775
+ }
12617
12776
  logger_default.info(`Pipeline completed in ${totalDuration}ms`);
12618
12777
  return {
12619
12778
  video,
@@ -14520,7 +14679,7 @@ program.command("configure [subcommand]").description("Manage global configurati
14520
14679
  await runConfigure(subcommand, args);
14521
14680
  process.exit(process.exitCode ?? 0);
14522
14681
  });
14523
- 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-git", "Skip git commit/push stage").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("--doctor", "Check all prerequisites and exit").action(async (videoPath) => {
14682
+ 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-git", "Skip git commit/push stage").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) => {
14524
14683
  const opts = defaultCmd.opts();
14525
14684
  if (opts.doctor) {
14526
14685
  await runDoctor();
@@ -14550,6 +14709,7 @@ var defaultCmd = program.command("process", { isDefault: true }).argument("[vide
14550
14709
  logger_default.info(BANNER);
14551
14710
  initConfig(cliOptions);
14552
14711
  if (opts.verbose) setVerbose();
14712
+ if (opts.progress) progressEmitter.enable();
14553
14713
  validateRequiredKeys();
14554
14714
  const config2 = getConfig();
14555
14715
  logger_default.info(`Watch folder: ${config2.WATCH_FOLDER}`);