@t3lnet/sceneforge 1.0.7 → 1.0.8

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/index.js CHANGED
@@ -180,8 +180,8 @@ function formatValidationError(error) {
180
180
  }
181
181
  const issues = error.issues;
182
182
  return issues.map((issue) => {
183
- const path4 = issue.path.length ? issue.path.join(".") : "root";
184
- return `${path4}: ${issue.message}`;
183
+ const path5 = issue.path.length ? issue.path.join(".") : "root";
184
+ return `${path5}: ${issue.message}`;
185
185
  }).join("; ");
186
186
  }
187
187
  function parseDemoDefinition(input) {
@@ -311,7 +311,7 @@ function createEmptyStep(id) {
311
311
  };
312
312
  }
313
313
  var SECRET_PATTERN = /\$\{(ENV|SECRET):([A-Za-z0-9_]+)\}/g;
314
- function resolveSecrets(value, resolver, path4 = "root") {
314
+ function resolveSecrets(value, resolver, path5 = "root") {
315
315
  if (typeof value === "string") {
316
316
  if (!SECRET_PATTERN.test(value)) {
317
317
  return value;
@@ -320,20 +320,20 @@ function resolveSecrets(value, resolver, path4 = "root") {
320
320
  return value.replace(SECRET_PATTERN, (_match, _type, key) => {
321
321
  const resolved = resolver(key);
322
322
  if (resolved === void 0) {
323
- throw new Error(`Missing secret for ${key} at ${path4}`);
323
+ throw new Error(`Missing secret for ${key} at ${path5}`);
324
324
  }
325
325
  return resolved;
326
326
  });
327
327
  }
328
328
  if (Array.isArray(value)) {
329
329
  return value.map(
330
- (entry, index) => resolveSecrets(entry, resolver, `${path4}[${index}]`)
330
+ (entry, index) => resolveSecrets(entry, resolver, `${path5}[${index}]`)
331
331
  );
332
332
  }
333
333
  if (value && typeof value === "object") {
334
334
  const result = {};
335
335
  for (const [key, entry] of Object.entries(value)) {
336
- result[key] = resolveSecrets(entry, resolver, `${path4}.${key}`);
336
+ result[key] = resolveSecrets(entry, resolver, `${path5}.${key}`);
337
337
  }
338
338
  return result;
339
339
  }
@@ -412,10 +412,10 @@ function createTypeAction(selector, text) {
412
412
  text
413
413
  };
414
414
  }
415
- function createNavigateAction(path4) {
415
+ function createNavigateAction(path5) {
416
416
  return {
417
417
  action: "navigate",
418
- path: path4
418
+ path: path5
419
419
  };
420
420
  }
421
421
  function createWaitAction(duration) {
@@ -488,14 +488,323 @@ function createDragAction(selector, deltaX, deltaY, steps = 20) {
488
488
 
489
489
  // ../playwright/src/demo-runner.ts
490
490
  import { expect } from "@playwright/test";
491
- import * as fs3 from "fs/promises";
492
- import * as path3 from "path";
491
+ import * as fs4 from "fs/promises";
492
+ import * as path4 from "path";
493
493
 
494
494
  // ../generation/src/voice-synthesis.ts
495
495
  import { spawn } from "child_process";
496
496
  import { ElevenLabsClient } from "elevenlabs";
497
+ import * as fs2 from "fs/promises";
498
+ import * as path2 from "path";
499
+
500
+ // ../generation/src/voice-cache.ts
501
+ import * as crypto from "crypto";
497
502
  import * as fs from "fs/promises";
498
503
  import * as path from "path";
504
+ var CACHE_VERSION = 1;
505
+ var INDEX_FILE = "index.json";
506
+ var AUDIO_DIR = "audio";
507
+ var DEFAULT_VOICE_SETTINGS = {
508
+ stability: 0.5,
509
+ similarityBoost: 0.75,
510
+ style: 0,
511
+ useSpeakerBoost: true
512
+ };
513
+ function normalizeVoiceSettings(settings) {
514
+ return {
515
+ stability: settings?.stability ?? DEFAULT_VOICE_SETTINGS.stability,
516
+ similarityBoost: settings?.similarityBoost ?? DEFAULT_VOICE_SETTINGS.similarityBoost,
517
+ style: settings?.style ?? DEFAULT_VOICE_SETTINGS.style,
518
+ useSpeakerBoost: settings?.useSpeakerBoost ?? DEFAULT_VOICE_SETTINGS.useSpeakerBoost
519
+ };
520
+ }
521
+ function generateCacheKey(voiceId, modelId, text, voiceSettings) {
522
+ const normalizedText = text.trim();
523
+ const keyData = JSON.stringify({
524
+ voiceId,
525
+ modelId,
526
+ text: normalizedText,
527
+ voiceSettings: {
528
+ stability: voiceSettings.stability,
529
+ similarityBoost: voiceSettings.similarityBoost,
530
+ style: voiceSettings.style,
531
+ useSpeakerBoost: voiceSettings.useSpeakerBoost
532
+ }
533
+ });
534
+ return crypto.createHash("sha256").update(keyData).digest("hex");
535
+ }
536
+ var VoiceCache = class {
537
+ constructor(config) {
538
+ __publicField(this, "config");
539
+ __publicField(this, "index", null);
540
+ __publicField(this, "stats", { hits: 0, misses: 0 });
541
+ __publicField(this, "indexDirty", false);
542
+ this.config = {
543
+ ...config,
544
+ enabled: config.enabled ?? true
545
+ };
546
+ }
547
+ /**
548
+ * Gets the path to the cache index file
549
+ */
550
+ get indexPath() {
551
+ return path.join(this.config.cacheDir, INDEX_FILE);
552
+ }
553
+ /**
554
+ * Gets the path to the audio cache directory
555
+ */
556
+ get audioDir() {
557
+ return path.join(this.config.cacheDir, AUDIO_DIR);
558
+ }
559
+ /**
560
+ * Whether caching is enabled
561
+ */
562
+ get enabled() {
563
+ return this.config.enabled ?? true;
564
+ }
565
+ /**
566
+ * Initializes the cache directory and loads the index
567
+ */
568
+ async initialize() {
569
+ if (!this.enabled) {
570
+ return;
571
+ }
572
+ await fs.mkdir(this.config.cacheDir, { recursive: true });
573
+ await fs.mkdir(this.audioDir, { recursive: true });
574
+ await this.loadIndex();
575
+ }
576
+ /**
577
+ * Loads the cache index from disk
578
+ */
579
+ async loadIndex() {
580
+ try {
581
+ const content = await fs.readFile(this.indexPath, "utf-8");
582
+ const parsed = JSON.parse(content);
583
+ if (parsed.version !== CACHE_VERSION) {
584
+ console.warn(
585
+ `[voice-cache] Index version mismatch (${parsed.version} vs ${CACHE_VERSION}), rebuilding cache`
586
+ );
587
+ this.index = { version: CACHE_VERSION, entries: {} };
588
+ this.indexDirty = true;
589
+ return;
590
+ }
591
+ this.index = parsed;
592
+ } catch (error) {
593
+ if (error.code === "ENOENT") {
594
+ this.index = { version: CACHE_VERSION, entries: {} };
595
+ this.indexDirty = true;
596
+ } else {
597
+ console.warn(`[voice-cache] Failed to load index, creating new one:`, error);
598
+ this.index = { version: CACHE_VERSION, entries: {} };
599
+ this.indexDirty = true;
600
+ }
601
+ }
602
+ }
603
+ /**
604
+ * Saves the cache index to disk
605
+ */
606
+ async saveIndex() {
607
+ if (!this.enabled || !this.index || !this.indexDirty) {
608
+ return;
609
+ }
610
+ await fs.writeFile(this.indexPath, JSON.stringify(this.index, null, 2));
611
+ this.indexDirty = false;
612
+ }
613
+ /**
614
+ * Looks up a cached audio file by its synthesis parameters.
615
+ * Returns the cache entry if found, null otherwise.
616
+ */
617
+ async get(voiceId, modelId, text, voiceSettings) {
618
+ if (!this.enabled || !this.index) {
619
+ this.stats.misses++;
620
+ return null;
621
+ }
622
+ const key = generateCacheKey(voiceId, modelId, text, voiceSettings);
623
+ const entry = this.index.entries[key];
624
+ if (!entry) {
625
+ this.stats.misses++;
626
+ return null;
627
+ }
628
+ const audioPath = path.join(this.audioDir, entry.audioFileName);
629
+ try {
630
+ await fs.access(audioPath);
631
+ } catch {
632
+ delete this.index.entries[key];
633
+ this.indexDirty = true;
634
+ this.stats.misses++;
635
+ return null;
636
+ }
637
+ entry.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
638
+ this.indexDirty = true;
639
+ this.stats.hits++;
640
+ return entry;
641
+ }
642
+ /**
643
+ * Gets the full path to a cached audio file
644
+ */
645
+ getAudioPath(entry) {
646
+ return path.join(this.audioDir, entry.audioFileName);
647
+ }
648
+ /**
649
+ * Stores a synthesized audio file in the cache.
650
+ * The audio data should be provided as a Buffer.
651
+ */
652
+ async put(voiceId, modelId, text, voiceSettings, audioData, durationMs) {
653
+ if (!this.index) {
654
+ await this.initialize();
655
+ }
656
+ const key = generateCacheKey(voiceId, modelId, text, voiceSettings);
657
+ const audioFileName = `${key}.mp3`;
658
+ const audioPath = path.join(this.audioDir, audioFileName);
659
+ await fs.writeFile(audioPath, audioData);
660
+ const now = (/* @__PURE__ */ new Date()).toISOString();
661
+ const entry = {
662
+ key,
663
+ voiceId,
664
+ modelId,
665
+ text: text.trim(),
666
+ voiceSettings,
667
+ audioFileName,
668
+ durationMs,
669
+ fileSizeBytes: audioData.length,
670
+ createdAt: now,
671
+ lastUsedAt: now
672
+ };
673
+ this.index.entries[key] = entry;
674
+ this.indexDirty = true;
675
+ return entry;
676
+ }
677
+ /**
678
+ * Removes a specific entry from the cache
679
+ */
680
+ async remove(key) {
681
+ if (!this.enabled || !this.index) {
682
+ return false;
683
+ }
684
+ const entry = this.index.entries[key];
685
+ if (!entry) {
686
+ return false;
687
+ }
688
+ const audioPath = path.join(this.audioDir, entry.audioFileName);
689
+ try {
690
+ await fs.unlink(audioPath);
691
+ } catch {
692
+ }
693
+ delete this.index.entries[key];
694
+ this.indexDirty = true;
695
+ return true;
696
+ }
697
+ /**
698
+ * Clears all entries from the cache
699
+ */
700
+ async clear() {
701
+ if (!this.index) {
702
+ await this.initialize();
703
+ }
704
+ const count = Object.keys(this.index.entries).length;
705
+ try {
706
+ const files = await fs.readdir(this.audioDir);
707
+ await Promise.all(
708
+ files.map((file) => fs.unlink(path.join(this.audioDir, file)).catch(() => {
709
+ }))
710
+ );
711
+ } catch {
712
+ }
713
+ this.index.entries = {};
714
+ this.indexDirty = true;
715
+ await this.saveIndex();
716
+ return count;
717
+ }
718
+ /**
719
+ * Lists all entries in the cache
720
+ */
721
+ async list() {
722
+ if (!this.index) {
723
+ await this.initialize();
724
+ }
725
+ return Object.values(this.index.entries).sort(
726
+ (a, b) => new Date(b.lastUsedAt).getTime() - new Date(a.lastUsedAt).getTime()
727
+ );
728
+ }
729
+ /**
730
+ * Gets statistics about the cache
731
+ */
732
+ async getStats() {
733
+ if (!this.index) {
734
+ await this.initialize();
735
+ }
736
+ const entries = Object.values(this.index.entries);
737
+ const totalSizeBytes = entries.reduce((sum, e) => sum + e.fileSizeBytes, 0);
738
+ let oldestEntry = null;
739
+ let newestEntry = null;
740
+ if (entries.length > 0) {
741
+ const sorted = [...entries].sort(
742
+ (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
743
+ );
744
+ oldestEntry = sorted[0].createdAt;
745
+ newestEntry = sorted[sorted.length - 1].createdAt;
746
+ }
747
+ return {
748
+ totalEntries: entries.length,
749
+ totalSizeBytes,
750
+ oldestEntry,
751
+ newestEntry,
752
+ hitCount: this.stats.hits,
753
+ missCount: this.stats.misses
754
+ };
755
+ }
756
+ /**
757
+ * Prunes entries older than the specified age (in days)
758
+ */
759
+ async pruneOlderThan(days) {
760
+ if (!this.index) {
761
+ await this.initialize();
762
+ }
763
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
764
+ let removed = 0;
765
+ for (const [key, entry] of Object.entries(this.index.entries)) {
766
+ const lastUsed = new Date(entry.lastUsedAt).getTime();
767
+ if (lastUsed < cutoff) {
768
+ await this.remove(key);
769
+ removed++;
770
+ }
771
+ }
772
+ await this.saveIndex();
773
+ return removed;
774
+ }
775
+ /**
776
+ * Validates the cache by checking that all indexed files exist
777
+ * and removing orphaned entries
778
+ */
779
+ async validate() {
780
+ if (!this.index) {
781
+ await this.initialize();
782
+ }
783
+ let valid = 0;
784
+ let removed = 0;
785
+ for (const [key, entry] of Object.entries(this.index.entries)) {
786
+ const audioPath = path.join(this.audioDir, entry.audioFileName);
787
+ try {
788
+ await fs.access(audioPath);
789
+ valid++;
790
+ } catch {
791
+ delete this.index.entries[key];
792
+ this.indexDirty = true;
793
+ removed++;
794
+ }
795
+ }
796
+ if (removed > 0) {
797
+ await this.saveIndex();
798
+ }
799
+ return { valid, removed };
800
+ }
801
+ };
802
+ function getDefaultCacheDir(projectRoot) {
803
+ const root = projectRoot || process.cwd();
804
+ return path.join(root, ".voice-cache");
805
+ }
806
+
807
+ // ../generation/src/voice-synthesis.ts
499
808
  var DEFAULT_MAX_OUTPUT_BYTES = 512 * 1024;
500
809
  var DEFAULT_MAX_CONCURRENCY = 2;
501
810
  var DEFAULT_RETRY_OPTIONS = {
@@ -660,10 +969,34 @@ var VoiceSynthesizer = class {
660
969
  constructor(config) {
661
970
  __publicField(this, "client");
662
971
  __publicField(this, "config");
972
+ __publicField(this, "cache", null);
973
+ __publicField(this, "cacheInitialized", false);
663
974
  this.config = config;
664
975
  this.client = new ElevenLabsClient({
665
976
  apiKey: config.apiKey
666
977
  });
978
+ if (config.cache !== false) {
979
+ const cacheConfig = config.cache ?? {
980
+ cacheDir: getDefaultCacheDir(),
981
+ enabled: true
982
+ };
983
+ this.cache = new VoiceCache(cacheConfig);
984
+ }
985
+ }
986
+ /**
987
+ * Ensures the cache is initialized before use
988
+ */
989
+ async ensureCacheInitialized() {
990
+ if (this.cache && !this.cacheInitialized) {
991
+ await this.cache.initialize();
992
+ this.cacheInitialized = true;
993
+ }
994
+ }
995
+ /**
996
+ * Gets the voice cache instance (if enabled)
997
+ */
998
+ getCache() {
999
+ return this.cache;
667
1000
  }
668
1001
  /**
669
1002
  * List available voices (useful for finding voice IDs)
@@ -680,14 +1013,31 @@ var VoiceSynthesizer = class {
680
1013
  * Synthesize a single text segment to audio
681
1014
  */
682
1015
  async synthesizeSegment(text, outputPath, options) {
1016
+ const modelId = this.config.modelId || "eleven_multilingual_v2";
1017
+ const voiceSettings = normalizeVoiceSettings(options);
1018
+ if (this.cache && !options?.skipCache) {
1019
+ await this.ensureCacheInitialized();
1020
+ const cachedEntry = await this.cache.get(
1021
+ this.config.voiceId,
1022
+ modelId,
1023
+ text,
1024
+ voiceSettings
1025
+ );
1026
+ if (cachedEntry) {
1027
+ const cachedPath = this.cache.getAudioPath(cachedEntry);
1028
+ await fs2.copyFile(cachedPath, outputPath);
1029
+ console.log(`[voice] Cache hit for "${text.slice(0, 40)}..." (saved API call)`);
1030
+ return { durationMs: cachedEntry.durationMs, fromCache: true };
1031
+ }
1032
+ }
683
1033
  const audio = await this.client.textToSpeech.convert(this.config.voiceId, {
684
1034
  text,
685
- model_id: this.config.modelId || "eleven_multilingual_v2",
1035
+ model_id: modelId,
686
1036
  voice_settings: {
687
- stability: options?.stability ?? 0.5,
688
- similarity_boost: options?.similarityBoost ?? 0.75,
689
- style: options?.style ?? 0,
690
- use_speaker_boost: options?.useSpeakerBoost ?? true
1037
+ stability: voiceSettings.stability,
1038
+ similarity_boost: voiceSettings.similarityBoost,
1039
+ style: voiceSettings.style,
1040
+ use_speaker_boost: voiceSettings.useSpeakerBoost
691
1041
  }
692
1042
  });
693
1043
  const chunks = [];
@@ -695,15 +1045,29 @@ var VoiceSynthesizer = class {
695
1045
  chunks.push(Buffer.from(chunk));
696
1046
  }
697
1047
  const audioBuffer = Buffer.concat(chunks);
698
- await fs.writeFile(outputPath, audioBuffer);
1048
+ await fs2.writeFile(outputPath, audioBuffer);
699
1049
  const probedDurationMs = await probeMediaDurationMs(outputPath);
1050
+ let durationMs;
700
1051
  if (probedDurationMs !== null) {
701
- return { durationMs: probedDurationMs };
1052
+ durationMs = probedDurationMs;
1053
+ } else {
1054
+ const fileSizeBytes = audioBuffer.length;
1055
+ durationMs = Math.round(fileSizeBytes * 8 / 128);
1056
+ console.warn(`[voice] ffprobe unavailable, using estimated duration for ${outputPath}`);
1057
+ }
1058
+ if (this.cache) {
1059
+ await this.ensureCacheInitialized();
1060
+ await this.cache.put(
1061
+ this.config.voiceId,
1062
+ modelId,
1063
+ text,
1064
+ voiceSettings,
1065
+ audioBuffer,
1066
+ durationMs
1067
+ );
1068
+ await this.cache.saveIndex();
702
1069
  }
703
- const fileSizeBytes = audioBuffer.length;
704
- const estimatedDurationMs = Math.round(fileSizeBytes * 8 / 128);
705
- console.warn(`[voice] ffprobe unavailable, using estimated duration for ${outputPath}`);
706
- return { durationMs: estimatedDurationMs };
1070
+ return { durationMs, fromCache: false };
707
1071
  }
708
1072
  /**
709
1073
  * Generate a sound effect from text description
@@ -721,16 +1085,16 @@ var VoiceSynthesizer = class {
721
1085
  for await (const chunk of audio) {
722
1086
  chunks.push(Buffer.from(chunk));
723
1087
  }
724
- await fs.writeFile(outputPath, Buffer.concat(chunks));
1088
+ await fs2.writeFile(outputPath, Buffer.concat(chunks));
725
1089
  }
726
1090
  /**
727
1091
  * Synthesize all segments from a script JSON file
728
1092
  */
729
1093
  async synthesizeScript(scriptPath, outputDir, options) {
730
- const scriptContent = await fs.readFile(scriptPath, "utf-8");
1094
+ const scriptContent = await fs2.readFile(scriptPath, "utf-8");
731
1095
  const script = JSON.parse(scriptContent);
732
- const audioDir = path.join(outputDir, "audio", script.demoName);
733
- await fs.mkdir(audioDir, { recursive: true });
1096
+ const audioDir = path2.join(outputDir, "audio", script.demoName);
1097
+ await fs2.mkdir(audioDir, { recursive: true });
734
1098
  const synthesizedSegments = new Array(script.segments.length);
735
1099
  const maxConcurrency = Math.max(
736
1100
  1,
@@ -748,7 +1112,7 @@ var VoiceSynthesizer = class {
748
1112
  const segment = script.segments[index];
749
1113
  const safeStepId = sanitizeFileSegment(segment.stepId, `step-${index + 1}`);
750
1114
  const audioFileName = `${String(index + 1).padStart(2, "0")}_${safeStepId}.mp3`;
751
- const audioPath = path.join(audioDir, audioFileName);
1115
+ const audioPath = path2.join(audioDir, audioFileName);
752
1116
  progressCount += 1;
753
1117
  options?.onProgress?.(progressCount, script.segments.length, segment.stepId);
754
1118
  try {
@@ -762,7 +1126,8 @@ var VoiceSynthesizer = class {
762
1126
  durationMs: result.durationMs,
763
1127
  text: segment.text
764
1128
  };
765
- console.log(`[voice] Synthesized: ${segment.stepId} (${result.durationMs.toFixed(0)}ms)`);
1129
+ const cacheStatus = result.fromCache ? "[cached]" : "[new]";
1130
+ console.log(`[voice] ${cacheStatus} ${segment.stepId} (${result.durationMs.toFixed(0)}ms)`);
766
1131
  } catch (error) {
767
1132
  console.error(`[voice] Failed to synthesize ${segment.stepId}:`, error);
768
1133
  throw error;
@@ -772,7 +1137,7 @@ var VoiceSynthesizer = class {
772
1137
  await Promise.all(workers);
773
1138
  let soundEffectsPath;
774
1139
  if (options?.generateClickSounds) {
775
- soundEffectsPath = path.join(audioDir, "click.mp3");
1140
+ soundEffectsPath = path2.join(audioDir, "click.mp3");
776
1141
  await this.generateClickSound(soundEffectsPath);
777
1142
  }
778
1143
  return {
@@ -812,7 +1177,7 @@ var VoiceSynthesizer = class {
812
1177
  }
813
1178
  };
814
1179
  async function generateTimingManifest(scriptPath, synthesisResult, outputPath) {
815
- const scriptContent = await fs.readFile(scriptPath, "utf-8");
1180
+ const scriptContent = await fs2.readFile(scriptPath, "utf-8");
816
1181
  const script = JSON.parse(scriptContent);
817
1182
  const manifest = {
818
1183
  demoName: script.demoName,
@@ -829,7 +1194,7 @@ async function generateTimingManifest(scriptPath, synthesisResult, outputPath) {
829
1194
  soundEffects: synthesisResult.soundEffectsPath ? { clickSound: synthesisResult.soundEffectsPath } : void 0,
830
1195
  backgroundMusic: synthesisResult.backgroundMusicPath
831
1196
  };
832
- await fs.writeFile(outputPath, JSON.stringify(manifest, null, 2));
1197
+ await fs2.writeFile(outputPath, JSON.stringify(manifest, null, 2));
833
1198
  return manifest;
834
1199
  }
835
1200
  function createVoiceSynthesizer(config) {
@@ -837,8 +1202,8 @@ function createVoiceSynthesizer(config) {
837
1202
  }
838
1203
 
839
1204
  // ../generation/src/script-generator.ts
840
- import * as fs2 from "fs/promises";
841
- import * as path2 from "path";
1205
+ import * as fs3 from "fs/promises";
1206
+ import * as path3 from "path";
842
1207
  var ScriptGenerator = class {
843
1208
  constructor(demoName, title, startTimeMs) {
844
1209
  __publicField(this, "segments", []);
@@ -937,8 +1302,8 @@ var ScriptGenerator = class {
937
1302
  */
938
1303
  async exportJSON(outputPath) {
939
1304
  const output = this.getOutput();
940
- await fs2.mkdir(path2.dirname(outputPath), { recursive: true });
941
- await fs2.writeFile(outputPath, JSON.stringify(output, null, 2));
1305
+ await fs3.mkdir(path3.dirname(outputPath), { recursive: true });
1306
+ await fs3.writeFile(outputPath, JSON.stringify(output, null, 2));
942
1307
  }
943
1308
  /**
944
1309
  * Exports script as SRT subtitle format.
@@ -957,8 +1322,8 @@ var ScriptGenerator = class {
957
1322
 
958
1323
  `;
959
1324
  });
960
- await fs2.mkdir(path2.dirname(outputPath), { recursive: true });
961
- await fs2.writeFile(outputPath, srt.trim());
1325
+ await fs3.mkdir(path3.dirname(outputPath), { recursive: true });
1326
+ await fs3.writeFile(outputPath, srt.trim());
962
1327
  }
963
1328
  /**
964
1329
  * Exports script in a format suitable for AI voice generation services.
@@ -980,8 +1345,8 @@ var ScriptGenerator = class {
980
1345
  syncPointMs: segment.startTimeMs
981
1346
  }))
982
1347
  };
983
- await fs2.mkdir(path2.dirname(outputPath), { recursive: true });
984
- await fs2.writeFile(outputPath, JSON.stringify(voiceScript, null, 2));
1348
+ await fs3.mkdir(path3.dirname(outputPath), { recursive: true });
1349
+ await fs3.writeFile(outputPath, JSON.stringify(voiceScript, null, 2));
985
1350
  }
986
1351
  /**
987
1352
  * Exports script as human-readable markdown for voice actors.
@@ -1028,8 +1393,8 @@ var ScriptGenerator = class {
1028
1393
 
1029
1394
  `;
1030
1395
  });
1031
- await fs2.mkdir(path2.dirname(outputPath), { recursive: true });
1032
- await fs2.writeFile(outputPath, markdown);
1396
+ await fs3.mkdir(path3.dirname(outputPath), { recursive: true });
1397
+ await fs3.writeFile(outputPath, markdown);
1033
1398
  }
1034
1399
  };
1035
1400
  function formatSRTTime(ms) {
@@ -1468,13 +1833,13 @@ async function withActionTimeout(timeoutMs, label, fn) {
1468
1833
  }
1469
1834
  async function captureDiagnostics(params) {
1470
1835
  const { context, stepId, stepIndex, actionIndex, action, error, runId } = params;
1471
- const diagnosticsDir = path3.join(context.outputDir, "diagnostics");
1836
+ const diagnosticsDir = path4.join(context.outputDir, "diagnostics");
1472
1837
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1473
1838
  const stepLabel = sanitizeDiagnosticsSegment(stepId ?? "unknown-step", "unknown-step");
1474
1839
  const actionLabel = actionIndex !== void 0 ? `action-${actionIndex + 1}` : "action";
1475
1840
  const baseName = `${timestamp}-${runId}-${stepLabel}-${actionLabel}`;
1476
1841
  try {
1477
- await fs3.mkdir(diagnosticsDir, { recursive: true });
1842
+ await fs4.mkdir(diagnosticsDir, { recursive: true });
1478
1843
  } catch {
1479
1844
  }
1480
1845
  const metadata = {
@@ -1488,7 +1853,7 @@ async function captureDiagnostics(params) {
1488
1853
  capturedAt: (/* @__PURE__ */ new Date()).toISOString()
1489
1854
  };
1490
1855
  try {
1491
- const screenshotPath = path3.join(diagnosticsDir, `${baseName}.png`);
1856
+ const screenshotPath = path4.join(diagnosticsDir, `${baseName}.png`);
1492
1857
  await context.page.screenshot({ path: screenshotPath, fullPage: true });
1493
1858
  } catch {
1494
1859
  }
@@ -1497,28 +1862,28 @@ async function captureDiagnostics(params) {
1497
1862
  const maxChars = 2e4;
1498
1863
  const snippet = domContent.length > maxChars ? `${domContent.slice(0, maxChars)}
1499
1864
  <!-- truncated -->` : domContent;
1500
- const domPath = path3.join(diagnosticsDir, `${baseName}.dom.html`);
1501
- await fs3.writeFile(domPath, snippet, "utf-8");
1865
+ const domPath = path4.join(diagnosticsDir, `${baseName}.dom.html`);
1866
+ await fs4.writeFile(domPath, snippet, "utf-8");
1502
1867
  } catch {
1503
1868
  }
1504
1869
  try {
1505
- const metaPath = path3.join(diagnosticsDir, `${baseName}.json`);
1506
- await fs3.writeFile(metaPath, JSON.stringify(metadata, null, 2), "utf-8");
1870
+ const metaPath = path4.join(diagnosticsDir, `${baseName}.json`);
1871
+ await fs4.writeFile(metaPath, JSON.stringify(metadata, null, 2), "utf-8");
1507
1872
  } catch {
1508
1873
  }
1509
1874
  }
1510
1875
  async function loadDemoDefinition(filePath, options) {
1511
- const content = await fs3.readFile(filePath, "utf-8");
1876
+ const content = await fs4.readFile(filePath, "utf-8");
1512
1877
  const definition = parseFromYAML(content, options);
1513
1878
  validateDemoDefinition(definition);
1514
1879
  return definition;
1515
1880
  }
1516
1881
  function resolveFilePath(filePath, baseDir) {
1517
- if (path3.isAbsolute(filePath)) {
1882
+ if (path4.isAbsolute(filePath)) {
1518
1883
  return filePath;
1519
1884
  }
1520
1885
  const root = baseDir ?? process.cwd();
1521
- return path3.resolve(root, filePath);
1886
+ return path4.resolve(root, filePath);
1522
1887
  }
1523
1888
  function resolveNavigatePath(actionPath, context) {
1524
1889
  const resolvedPath = resolvePath(actionPath, { baseURL: context.baseURL });
@@ -1960,8 +2325,8 @@ async function runDemo(definition, context, options = {}) {
1960
2325
  }
1961
2326
  if (scriptGenerator) {
1962
2327
  scriptGenerator.finishAllSteps();
1963
- const scriptOutputDir = options.scriptOutputDir ?? path3.join(context.outputDir, "scripts");
1964
- const scriptBasePath = path3.join(scriptOutputDir, definition.name);
2328
+ const scriptOutputDir = options.scriptOutputDir ?? path4.join(context.outputDir, "scripts");
2329
+ const scriptBasePath = path4.join(scriptOutputDir, definition.name);
1965
2330
  await scriptGenerator.exportJSON(`${scriptBasePath}.json`);
1966
2331
  await scriptGenerator.exportSRT(`${scriptBasePath}.srt`);
1967
2332
  await scriptGenerator.exportMarkdown(`${scriptBasePath}.md`);
@@ -2017,8 +2382,8 @@ async function runDemoFromFile(definitionPath, context, options) {
2017
2382
  return runDemo(definition, context, options);
2018
2383
  }
2019
2384
  async function discoverDemos(demoDir) {
2020
- const files = await fs3.readdir(demoDir);
2021
- return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => path3.join(demoDir, f));
2385
+ const files = await fs4.readdir(demoDir);
2386
+ return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => path4.join(demoDir, f));
2022
2387
  }
2023
2388
  export {
2024
2389
  DEMO_SCHEMA_VERSION,