@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.cjs CHANGED
@@ -252,8 +252,8 @@ function formatValidationError(error) {
252
252
  }
253
253
  const issues = error.issues;
254
254
  return issues.map((issue) => {
255
- const path4 = issue.path.length ? issue.path.join(".") : "root";
256
- return `${path4}: ${issue.message}`;
255
+ const path5 = issue.path.length ? issue.path.join(".") : "root";
256
+ return `${path5}: ${issue.message}`;
257
257
  }).join("; ");
258
258
  }
259
259
  function parseDemoDefinition(input) {
@@ -383,7 +383,7 @@ function createEmptyStep(id) {
383
383
  };
384
384
  }
385
385
  var SECRET_PATTERN = /\$\{(ENV|SECRET):([A-Za-z0-9_]+)\}/g;
386
- function resolveSecrets(value, resolver, path4 = "root") {
386
+ function resolveSecrets(value, resolver, path5 = "root") {
387
387
  if (typeof value === "string") {
388
388
  if (!SECRET_PATTERN.test(value)) {
389
389
  return value;
@@ -392,20 +392,20 @@ function resolveSecrets(value, resolver, path4 = "root") {
392
392
  return value.replace(SECRET_PATTERN, (_match, _type, key) => {
393
393
  const resolved = resolver(key);
394
394
  if (resolved === void 0) {
395
- throw new Error(`Missing secret for ${key} at ${path4}`);
395
+ throw new Error(`Missing secret for ${key} at ${path5}`);
396
396
  }
397
397
  return resolved;
398
398
  });
399
399
  }
400
400
  if (Array.isArray(value)) {
401
401
  return value.map(
402
- (entry, index) => resolveSecrets(entry, resolver, `${path4}[${index}]`)
402
+ (entry, index) => resolveSecrets(entry, resolver, `${path5}[${index}]`)
403
403
  );
404
404
  }
405
405
  if (value && typeof value === "object") {
406
406
  const result = {};
407
407
  for (const [key, entry] of Object.entries(value)) {
408
- result[key] = resolveSecrets(entry, resolver, `${path4}.${key}`);
408
+ result[key] = resolveSecrets(entry, resolver, `${path5}.${key}`);
409
409
  }
410
410
  return result;
411
411
  }
@@ -484,10 +484,10 @@ function createTypeAction(selector, text) {
484
484
  text
485
485
  };
486
486
  }
487
- function createNavigateAction(path4) {
487
+ function createNavigateAction(path5) {
488
488
  return {
489
489
  action: "navigate",
490
- path: path4
490
+ path: path5
491
491
  };
492
492
  }
493
493
  function createWaitAction(duration) {
@@ -560,14 +560,323 @@ function createDragAction(selector, deltaX, deltaY, steps = 20) {
560
560
 
561
561
  // ../playwright/src/demo-runner.ts
562
562
  var import_test = require("@playwright/test");
563
- var fs3 = __toESM(require("fs/promises"), 1);
564
- var path3 = __toESM(require("path"), 1);
563
+ var fs4 = __toESM(require("fs/promises"), 1);
564
+ var path4 = __toESM(require("path"), 1);
565
565
 
566
566
  // ../generation/src/voice-synthesis.ts
567
567
  var import_child_process = require("child_process");
568
568
  var import_elevenlabs = require("elevenlabs");
569
+ var fs2 = __toESM(require("fs/promises"), 1);
570
+ var path2 = __toESM(require("path"), 1);
571
+
572
+ // ../generation/src/voice-cache.ts
573
+ var crypto = __toESM(require("crypto"), 1);
569
574
  var fs = __toESM(require("fs/promises"), 1);
570
575
  var path = __toESM(require("path"), 1);
576
+ var CACHE_VERSION = 1;
577
+ var INDEX_FILE = "index.json";
578
+ var AUDIO_DIR = "audio";
579
+ var DEFAULT_VOICE_SETTINGS = {
580
+ stability: 0.5,
581
+ similarityBoost: 0.75,
582
+ style: 0,
583
+ useSpeakerBoost: true
584
+ };
585
+ function normalizeVoiceSettings(settings) {
586
+ return {
587
+ stability: settings?.stability ?? DEFAULT_VOICE_SETTINGS.stability,
588
+ similarityBoost: settings?.similarityBoost ?? DEFAULT_VOICE_SETTINGS.similarityBoost,
589
+ style: settings?.style ?? DEFAULT_VOICE_SETTINGS.style,
590
+ useSpeakerBoost: settings?.useSpeakerBoost ?? DEFAULT_VOICE_SETTINGS.useSpeakerBoost
591
+ };
592
+ }
593
+ function generateCacheKey(voiceId, modelId, text, voiceSettings) {
594
+ const normalizedText = text.trim();
595
+ const keyData = JSON.stringify({
596
+ voiceId,
597
+ modelId,
598
+ text: normalizedText,
599
+ voiceSettings: {
600
+ stability: voiceSettings.stability,
601
+ similarityBoost: voiceSettings.similarityBoost,
602
+ style: voiceSettings.style,
603
+ useSpeakerBoost: voiceSettings.useSpeakerBoost
604
+ }
605
+ });
606
+ return crypto.createHash("sha256").update(keyData).digest("hex");
607
+ }
608
+ var VoiceCache = class {
609
+ constructor(config) {
610
+ __publicField(this, "config");
611
+ __publicField(this, "index", null);
612
+ __publicField(this, "stats", { hits: 0, misses: 0 });
613
+ __publicField(this, "indexDirty", false);
614
+ this.config = {
615
+ ...config,
616
+ enabled: config.enabled ?? true
617
+ };
618
+ }
619
+ /**
620
+ * Gets the path to the cache index file
621
+ */
622
+ get indexPath() {
623
+ return path.join(this.config.cacheDir, INDEX_FILE);
624
+ }
625
+ /**
626
+ * Gets the path to the audio cache directory
627
+ */
628
+ get audioDir() {
629
+ return path.join(this.config.cacheDir, AUDIO_DIR);
630
+ }
631
+ /**
632
+ * Whether caching is enabled
633
+ */
634
+ get enabled() {
635
+ return this.config.enabled ?? true;
636
+ }
637
+ /**
638
+ * Initializes the cache directory and loads the index
639
+ */
640
+ async initialize() {
641
+ if (!this.enabled) {
642
+ return;
643
+ }
644
+ await fs.mkdir(this.config.cacheDir, { recursive: true });
645
+ await fs.mkdir(this.audioDir, { recursive: true });
646
+ await this.loadIndex();
647
+ }
648
+ /**
649
+ * Loads the cache index from disk
650
+ */
651
+ async loadIndex() {
652
+ try {
653
+ const content = await fs.readFile(this.indexPath, "utf-8");
654
+ const parsed = JSON.parse(content);
655
+ if (parsed.version !== CACHE_VERSION) {
656
+ console.warn(
657
+ `[voice-cache] Index version mismatch (${parsed.version} vs ${CACHE_VERSION}), rebuilding cache`
658
+ );
659
+ this.index = { version: CACHE_VERSION, entries: {} };
660
+ this.indexDirty = true;
661
+ return;
662
+ }
663
+ this.index = parsed;
664
+ } catch (error) {
665
+ if (error.code === "ENOENT") {
666
+ this.index = { version: CACHE_VERSION, entries: {} };
667
+ this.indexDirty = true;
668
+ } else {
669
+ console.warn(`[voice-cache] Failed to load index, creating new one:`, error);
670
+ this.index = { version: CACHE_VERSION, entries: {} };
671
+ this.indexDirty = true;
672
+ }
673
+ }
674
+ }
675
+ /**
676
+ * Saves the cache index to disk
677
+ */
678
+ async saveIndex() {
679
+ if (!this.enabled || !this.index || !this.indexDirty) {
680
+ return;
681
+ }
682
+ await fs.writeFile(this.indexPath, JSON.stringify(this.index, null, 2));
683
+ this.indexDirty = false;
684
+ }
685
+ /**
686
+ * Looks up a cached audio file by its synthesis parameters.
687
+ * Returns the cache entry if found, null otherwise.
688
+ */
689
+ async get(voiceId, modelId, text, voiceSettings) {
690
+ if (!this.enabled || !this.index) {
691
+ this.stats.misses++;
692
+ return null;
693
+ }
694
+ const key = generateCacheKey(voiceId, modelId, text, voiceSettings);
695
+ const entry = this.index.entries[key];
696
+ if (!entry) {
697
+ this.stats.misses++;
698
+ return null;
699
+ }
700
+ const audioPath = path.join(this.audioDir, entry.audioFileName);
701
+ try {
702
+ await fs.access(audioPath);
703
+ } catch {
704
+ delete this.index.entries[key];
705
+ this.indexDirty = true;
706
+ this.stats.misses++;
707
+ return null;
708
+ }
709
+ entry.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
710
+ this.indexDirty = true;
711
+ this.stats.hits++;
712
+ return entry;
713
+ }
714
+ /**
715
+ * Gets the full path to a cached audio file
716
+ */
717
+ getAudioPath(entry) {
718
+ return path.join(this.audioDir, entry.audioFileName);
719
+ }
720
+ /**
721
+ * Stores a synthesized audio file in the cache.
722
+ * The audio data should be provided as a Buffer.
723
+ */
724
+ async put(voiceId, modelId, text, voiceSettings, audioData, durationMs) {
725
+ if (!this.index) {
726
+ await this.initialize();
727
+ }
728
+ const key = generateCacheKey(voiceId, modelId, text, voiceSettings);
729
+ const audioFileName = `${key}.mp3`;
730
+ const audioPath = path.join(this.audioDir, audioFileName);
731
+ await fs.writeFile(audioPath, audioData);
732
+ const now = (/* @__PURE__ */ new Date()).toISOString();
733
+ const entry = {
734
+ key,
735
+ voiceId,
736
+ modelId,
737
+ text: text.trim(),
738
+ voiceSettings,
739
+ audioFileName,
740
+ durationMs,
741
+ fileSizeBytes: audioData.length,
742
+ createdAt: now,
743
+ lastUsedAt: now
744
+ };
745
+ this.index.entries[key] = entry;
746
+ this.indexDirty = true;
747
+ return entry;
748
+ }
749
+ /**
750
+ * Removes a specific entry from the cache
751
+ */
752
+ async remove(key) {
753
+ if (!this.enabled || !this.index) {
754
+ return false;
755
+ }
756
+ const entry = this.index.entries[key];
757
+ if (!entry) {
758
+ return false;
759
+ }
760
+ const audioPath = path.join(this.audioDir, entry.audioFileName);
761
+ try {
762
+ await fs.unlink(audioPath);
763
+ } catch {
764
+ }
765
+ delete this.index.entries[key];
766
+ this.indexDirty = true;
767
+ return true;
768
+ }
769
+ /**
770
+ * Clears all entries from the cache
771
+ */
772
+ async clear() {
773
+ if (!this.index) {
774
+ await this.initialize();
775
+ }
776
+ const count = Object.keys(this.index.entries).length;
777
+ try {
778
+ const files = await fs.readdir(this.audioDir);
779
+ await Promise.all(
780
+ files.map((file) => fs.unlink(path.join(this.audioDir, file)).catch(() => {
781
+ }))
782
+ );
783
+ } catch {
784
+ }
785
+ this.index.entries = {};
786
+ this.indexDirty = true;
787
+ await this.saveIndex();
788
+ return count;
789
+ }
790
+ /**
791
+ * Lists all entries in the cache
792
+ */
793
+ async list() {
794
+ if (!this.index) {
795
+ await this.initialize();
796
+ }
797
+ return Object.values(this.index.entries).sort(
798
+ (a, b) => new Date(b.lastUsedAt).getTime() - new Date(a.lastUsedAt).getTime()
799
+ );
800
+ }
801
+ /**
802
+ * Gets statistics about the cache
803
+ */
804
+ async getStats() {
805
+ if (!this.index) {
806
+ await this.initialize();
807
+ }
808
+ const entries = Object.values(this.index.entries);
809
+ const totalSizeBytes = entries.reduce((sum, e) => sum + e.fileSizeBytes, 0);
810
+ let oldestEntry = null;
811
+ let newestEntry = null;
812
+ if (entries.length > 0) {
813
+ const sorted = [...entries].sort(
814
+ (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
815
+ );
816
+ oldestEntry = sorted[0].createdAt;
817
+ newestEntry = sorted[sorted.length - 1].createdAt;
818
+ }
819
+ return {
820
+ totalEntries: entries.length,
821
+ totalSizeBytes,
822
+ oldestEntry,
823
+ newestEntry,
824
+ hitCount: this.stats.hits,
825
+ missCount: this.stats.misses
826
+ };
827
+ }
828
+ /**
829
+ * Prunes entries older than the specified age (in days)
830
+ */
831
+ async pruneOlderThan(days) {
832
+ if (!this.index) {
833
+ await this.initialize();
834
+ }
835
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
836
+ let removed = 0;
837
+ for (const [key, entry] of Object.entries(this.index.entries)) {
838
+ const lastUsed = new Date(entry.lastUsedAt).getTime();
839
+ if (lastUsed < cutoff) {
840
+ await this.remove(key);
841
+ removed++;
842
+ }
843
+ }
844
+ await this.saveIndex();
845
+ return removed;
846
+ }
847
+ /**
848
+ * Validates the cache by checking that all indexed files exist
849
+ * and removing orphaned entries
850
+ */
851
+ async validate() {
852
+ if (!this.index) {
853
+ await this.initialize();
854
+ }
855
+ let valid = 0;
856
+ let removed = 0;
857
+ for (const [key, entry] of Object.entries(this.index.entries)) {
858
+ const audioPath = path.join(this.audioDir, entry.audioFileName);
859
+ try {
860
+ await fs.access(audioPath);
861
+ valid++;
862
+ } catch {
863
+ delete this.index.entries[key];
864
+ this.indexDirty = true;
865
+ removed++;
866
+ }
867
+ }
868
+ if (removed > 0) {
869
+ await this.saveIndex();
870
+ }
871
+ return { valid, removed };
872
+ }
873
+ };
874
+ function getDefaultCacheDir(projectRoot) {
875
+ const root = projectRoot || process.cwd();
876
+ return path.join(root, ".voice-cache");
877
+ }
878
+
879
+ // ../generation/src/voice-synthesis.ts
571
880
  var DEFAULT_MAX_OUTPUT_BYTES = 512 * 1024;
572
881
  var DEFAULT_MAX_CONCURRENCY = 2;
573
882
  var DEFAULT_RETRY_OPTIONS = {
@@ -732,10 +1041,34 @@ var VoiceSynthesizer = class {
732
1041
  constructor(config) {
733
1042
  __publicField(this, "client");
734
1043
  __publicField(this, "config");
1044
+ __publicField(this, "cache", null);
1045
+ __publicField(this, "cacheInitialized", false);
735
1046
  this.config = config;
736
1047
  this.client = new import_elevenlabs.ElevenLabsClient({
737
1048
  apiKey: config.apiKey
738
1049
  });
1050
+ if (config.cache !== false) {
1051
+ const cacheConfig = config.cache ?? {
1052
+ cacheDir: getDefaultCacheDir(),
1053
+ enabled: true
1054
+ };
1055
+ this.cache = new VoiceCache(cacheConfig);
1056
+ }
1057
+ }
1058
+ /**
1059
+ * Ensures the cache is initialized before use
1060
+ */
1061
+ async ensureCacheInitialized() {
1062
+ if (this.cache && !this.cacheInitialized) {
1063
+ await this.cache.initialize();
1064
+ this.cacheInitialized = true;
1065
+ }
1066
+ }
1067
+ /**
1068
+ * Gets the voice cache instance (if enabled)
1069
+ */
1070
+ getCache() {
1071
+ return this.cache;
739
1072
  }
740
1073
  /**
741
1074
  * List available voices (useful for finding voice IDs)
@@ -752,14 +1085,31 @@ var VoiceSynthesizer = class {
752
1085
  * Synthesize a single text segment to audio
753
1086
  */
754
1087
  async synthesizeSegment(text, outputPath, options) {
1088
+ const modelId = this.config.modelId || "eleven_multilingual_v2";
1089
+ const voiceSettings = normalizeVoiceSettings(options);
1090
+ if (this.cache && !options?.skipCache) {
1091
+ await this.ensureCacheInitialized();
1092
+ const cachedEntry = await this.cache.get(
1093
+ this.config.voiceId,
1094
+ modelId,
1095
+ text,
1096
+ voiceSettings
1097
+ );
1098
+ if (cachedEntry) {
1099
+ const cachedPath = this.cache.getAudioPath(cachedEntry);
1100
+ await fs2.copyFile(cachedPath, outputPath);
1101
+ console.log(`[voice] Cache hit for "${text.slice(0, 40)}..." (saved API call)`);
1102
+ return { durationMs: cachedEntry.durationMs, fromCache: true };
1103
+ }
1104
+ }
755
1105
  const audio = await this.client.textToSpeech.convert(this.config.voiceId, {
756
1106
  text,
757
- model_id: this.config.modelId || "eleven_multilingual_v2",
1107
+ model_id: modelId,
758
1108
  voice_settings: {
759
- stability: options?.stability ?? 0.5,
760
- similarity_boost: options?.similarityBoost ?? 0.75,
761
- style: options?.style ?? 0,
762
- use_speaker_boost: options?.useSpeakerBoost ?? true
1109
+ stability: voiceSettings.stability,
1110
+ similarity_boost: voiceSettings.similarityBoost,
1111
+ style: voiceSettings.style,
1112
+ use_speaker_boost: voiceSettings.useSpeakerBoost
763
1113
  }
764
1114
  });
765
1115
  const chunks = [];
@@ -767,15 +1117,29 @@ var VoiceSynthesizer = class {
767
1117
  chunks.push(Buffer.from(chunk));
768
1118
  }
769
1119
  const audioBuffer = Buffer.concat(chunks);
770
- await fs.writeFile(outputPath, audioBuffer);
1120
+ await fs2.writeFile(outputPath, audioBuffer);
771
1121
  const probedDurationMs = await probeMediaDurationMs(outputPath);
1122
+ let durationMs;
772
1123
  if (probedDurationMs !== null) {
773
- return { durationMs: probedDurationMs };
1124
+ durationMs = probedDurationMs;
1125
+ } else {
1126
+ const fileSizeBytes = audioBuffer.length;
1127
+ durationMs = Math.round(fileSizeBytes * 8 / 128);
1128
+ console.warn(`[voice] ffprobe unavailable, using estimated duration for ${outputPath}`);
1129
+ }
1130
+ if (this.cache) {
1131
+ await this.ensureCacheInitialized();
1132
+ await this.cache.put(
1133
+ this.config.voiceId,
1134
+ modelId,
1135
+ text,
1136
+ voiceSettings,
1137
+ audioBuffer,
1138
+ durationMs
1139
+ );
1140
+ await this.cache.saveIndex();
774
1141
  }
775
- const fileSizeBytes = audioBuffer.length;
776
- const estimatedDurationMs = Math.round(fileSizeBytes * 8 / 128);
777
- console.warn(`[voice] ffprobe unavailable, using estimated duration for ${outputPath}`);
778
- return { durationMs: estimatedDurationMs };
1142
+ return { durationMs, fromCache: false };
779
1143
  }
780
1144
  /**
781
1145
  * Generate a sound effect from text description
@@ -793,16 +1157,16 @@ var VoiceSynthesizer = class {
793
1157
  for await (const chunk of audio) {
794
1158
  chunks.push(Buffer.from(chunk));
795
1159
  }
796
- await fs.writeFile(outputPath, Buffer.concat(chunks));
1160
+ await fs2.writeFile(outputPath, Buffer.concat(chunks));
797
1161
  }
798
1162
  /**
799
1163
  * Synthesize all segments from a script JSON file
800
1164
  */
801
1165
  async synthesizeScript(scriptPath, outputDir, options) {
802
- const scriptContent = await fs.readFile(scriptPath, "utf-8");
1166
+ const scriptContent = await fs2.readFile(scriptPath, "utf-8");
803
1167
  const script = JSON.parse(scriptContent);
804
- const audioDir = path.join(outputDir, "audio", script.demoName);
805
- await fs.mkdir(audioDir, { recursive: true });
1168
+ const audioDir = path2.join(outputDir, "audio", script.demoName);
1169
+ await fs2.mkdir(audioDir, { recursive: true });
806
1170
  const synthesizedSegments = new Array(script.segments.length);
807
1171
  const maxConcurrency = Math.max(
808
1172
  1,
@@ -820,7 +1184,7 @@ var VoiceSynthesizer = class {
820
1184
  const segment = script.segments[index];
821
1185
  const safeStepId = sanitizeFileSegment(segment.stepId, `step-${index + 1}`);
822
1186
  const audioFileName = `${String(index + 1).padStart(2, "0")}_${safeStepId}.mp3`;
823
- const audioPath = path.join(audioDir, audioFileName);
1187
+ const audioPath = path2.join(audioDir, audioFileName);
824
1188
  progressCount += 1;
825
1189
  options?.onProgress?.(progressCount, script.segments.length, segment.stepId);
826
1190
  try {
@@ -834,7 +1198,8 @@ var VoiceSynthesizer = class {
834
1198
  durationMs: result.durationMs,
835
1199
  text: segment.text
836
1200
  };
837
- console.log(`[voice] Synthesized: ${segment.stepId} (${result.durationMs.toFixed(0)}ms)`);
1201
+ const cacheStatus = result.fromCache ? "[cached]" : "[new]";
1202
+ console.log(`[voice] ${cacheStatus} ${segment.stepId} (${result.durationMs.toFixed(0)}ms)`);
838
1203
  } catch (error) {
839
1204
  console.error(`[voice] Failed to synthesize ${segment.stepId}:`, error);
840
1205
  throw error;
@@ -844,7 +1209,7 @@ var VoiceSynthesizer = class {
844
1209
  await Promise.all(workers);
845
1210
  let soundEffectsPath;
846
1211
  if (options?.generateClickSounds) {
847
- soundEffectsPath = path.join(audioDir, "click.mp3");
1212
+ soundEffectsPath = path2.join(audioDir, "click.mp3");
848
1213
  await this.generateClickSound(soundEffectsPath);
849
1214
  }
850
1215
  return {
@@ -884,7 +1249,7 @@ var VoiceSynthesizer = class {
884
1249
  }
885
1250
  };
886
1251
  async function generateTimingManifest(scriptPath, synthesisResult, outputPath) {
887
- const scriptContent = await fs.readFile(scriptPath, "utf-8");
1252
+ const scriptContent = await fs2.readFile(scriptPath, "utf-8");
888
1253
  const script = JSON.parse(scriptContent);
889
1254
  const manifest = {
890
1255
  demoName: script.demoName,
@@ -901,7 +1266,7 @@ async function generateTimingManifest(scriptPath, synthesisResult, outputPath) {
901
1266
  soundEffects: synthesisResult.soundEffectsPath ? { clickSound: synthesisResult.soundEffectsPath } : void 0,
902
1267
  backgroundMusic: synthesisResult.backgroundMusicPath
903
1268
  };
904
- await fs.writeFile(outputPath, JSON.stringify(manifest, null, 2));
1269
+ await fs2.writeFile(outputPath, JSON.stringify(manifest, null, 2));
905
1270
  return manifest;
906
1271
  }
907
1272
  function createVoiceSynthesizer(config) {
@@ -909,8 +1274,8 @@ function createVoiceSynthesizer(config) {
909
1274
  }
910
1275
 
911
1276
  // ../generation/src/script-generator.ts
912
- var fs2 = __toESM(require("fs/promises"), 1);
913
- var path2 = __toESM(require("path"), 1);
1277
+ var fs3 = __toESM(require("fs/promises"), 1);
1278
+ var path3 = __toESM(require("path"), 1);
914
1279
  var ScriptGenerator = class {
915
1280
  constructor(demoName, title, startTimeMs) {
916
1281
  __publicField(this, "segments", []);
@@ -1009,8 +1374,8 @@ var ScriptGenerator = class {
1009
1374
  */
1010
1375
  async exportJSON(outputPath) {
1011
1376
  const output = this.getOutput();
1012
- await fs2.mkdir(path2.dirname(outputPath), { recursive: true });
1013
- await fs2.writeFile(outputPath, JSON.stringify(output, null, 2));
1377
+ await fs3.mkdir(path3.dirname(outputPath), { recursive: true });
1378
+ await fs3.writeFile(outputPath, JSON.stringify(output, null, 2));
1014
1379
  }
1015
1380
  /**
1016
1381
  * Exports script as SRT subtitle format.
@@ -1029,8 +1394,8 @@ var ScriptGenerator = class {
1029
1394
 
1030
1395
  `;
1031
1396
  });
1032
- await fs2.mkdir(path2.dirname(outputPath), { recursive: true });
1033
- await fs2.writeFile(outputPath, srt.trim());
1397
+ await fs3.mkdir(path3.dirname(outputPath), { recursive: true });
1398
+ await fs3.writeFile(outputPath, srt.trim());
1034
1399
  }
1035
1400
  /**
1036
1401
  * Exports script in a format suitable for AI voice generation services.
@@ -1052,8 +1417,8 @@ var ScriptGenerator = class {
1052
1417
  syncPointMs: segment.startTimeMs
1053
1418
  }))
1054
1419
  };
1055
- await fs2.mkdir(path2.dirname(outputPath), { recursive: true });
1056
- await fs2.writeFile(outputPath, JSON.stringify(voiceScript, null, 2));
1420
+ await fs3.mkdir(path3.dirname(outputPath), { recursive: true });
1421
+ await fs3.writeFile(outputPath, JSON.stringify(voiceScript, null, 2));
1057
1422
  }
1058
1423
  /**
1059
1424
  * Exports script as human-readable markdown for voice actors.
@@ -1100,8 +1465,8 @@ var ScriptGenerator = class {
1100
1465
 
1101
1466
  `;
1102
1467
  });
1103
- await fs2.mkdir(path2.dirname(outputPath), { recursive: true });
1104
- await fs2.writeFile(outputPath, markdown);
1468
+ await fs3.mkdir(path3.dirname(outputPath), { recursive: true });
1469
+ await fs3.writeFile(outputPath, markdown);
1105
1470
  }
1106
1471
  };
1107
1472
  function formatSRTTime(ms) {
@@ -1540,13 +1905,13 @@ async function withActionTimeout(timeoutMs, label, fn) {
1540
1905
  }
1541
1906
  async function captureDiagnostics(params) {
1542
1907
  const { context, stepId, stepIndex, actionIndex, action, error, runId } = params;
1543
- const diagnosticsDir = path3.join(context.outputDir, "diagnostics");
1908
+ const diagnosticsDir = path4.join(context.outputDir, "diagnostics");
1544
1909
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1545
1910
  const stepLabel = sanitizeDiagnosticsSegment(stepId ?? "unknown-step", "unknown-step");
1546
1911
  const actionLabel = actionIndex !== void 0 ? `action-${actionIndex + 1}` : "action";
1547
1912
  const baseName = `${timestamp}-${runId}-${stepLabel}-${actionLabel}`;
1548
1913
  try {
1549
- await fs3.mkdir(diagnosticsDir, { recursive: true });
1914
+ await fs4.mkdir(diagnosticsDir, { recursive: true });
1550
1915
  } catch {
1551
1916
  }
1552
1917
  const metadata = {
@@ -1560,7 +1925,7 @@ async function captureDiagnostics(params) {
1560
1925
  capturedAt: (/* @__PURE__ */ new Date()).toISOString()
1561
1926
  };
1562
1927
  try {
1563
- const screenshotPath = path3.join(diagnosticsDir, `${baseName}.png`);
1928
+ const screenshotPath = path4.join(diagnosticsDir, `${baseName}.png`);
1564
1929
  await context.page.screenshot({ path: screenshotPath, fullPage: true });
1565
1930
  } catch {
1566
1931
  }
@@ -1569,28 +1934,28 @@ async function captureDiagnostics(params) {
1569
1934
  const maxChars = 2e4;
1570
1935
  const snippet = domContent.length > maxChars ? `${domContent.slice(0, maxChars)}
1571
1936
  <!-- truncated -->` : domContent;
1572
- const domPath = path3.join(diagnosticsDir, `${baseName}.dom.html`);
1573
- await fs3.writeFile(domPath, snippet, "utf-8");
1937
+ const domPath = path4.join(diagnosticsDir, `${baseName}.dom.html`);
1938
+ await fs4.writeFile(domPath, snippet, "utf-8");
1574
1939
  } catch {
1575
1940
  }
1576
1941
  try {
1577
- const metaPath = path3.join(diagnosticsDir, `${baseName}.json`);
1578
- await fs3.writeFile(metaPath, JSON.stringify(metadata, null, 2), "utf-8");
1942
+ const metaPath = path4.join(diagnosticsDir, `${baseName}.json`);
1943
+ await fs4.writeFile(metaPath, JSON.stringify(metadata, null, 2), "utf-8");
1579
1944
  } catch {
1580
1945
  }
1581
1946
  }
1582
1947
  async function loadDemoDefinition(filePath, options) {
1583
- const content = await fs3.readFile(filePath, "utf-8");
1948
+ const content = await fs4.readFile(filePath, "utf-8");
1584
1949
  const definition = parseFromYAML(content, options);
1585
1950
  validateDemoDefinition(definition);
1586
1951
  return definition;
1587
1952
  }
1588
1953
  function resolveFilePath(filePath, baseDir) {
1589
- if (path3.isAbsolute(filePath)) {
1954
+ if (path4.isAbsolute(filePath)) {
1590
1955
  return filePath;
1591
1956
  }
1592
1957
  const root = baseDir ?? process.cwd();
1593
- return path3.resolve(root, filePath);
1958
+ return path4.resolve(root, filePath);
1594
1959
  }
1595
1960
  function resolveNavigatePath(actionPath, context) {
1596
1961
  const resolvedPath = resolvePath(actionPath, { baseURL: context.baseURL });
@@ -2032,8 +2397,8 @@ async function runDemo(definition, context, options = {}) {
2032
2397
  }
2033
2398
  if (scriptGenerator) {
2034
2399
  scriptGenerator.finishAllSteps();
2035
- const scriptOutputDir = options.scriptOutputDir ?? path3.join(context.outputDir, "scripts");
2036
- const scriptBasePath = path3.join(scriptOutputDir, definition.name);
2400
+ const scriptOutputDir = options.scriptOutputDir ?? path4.join(context.outputDir, "scripts");
2401
+ const scriptBasePath = path4.join(scriptOutputDir, definition.name);
2037
2402
  await scriptGenerator.exportJSON(`${scriptBasePath}.json`);
2038
2403
  await scriptGenerator.exportSRT(`${scriptBasePath}.srt`);
2039
2404
  await scriptGenerator.exportMarkdown(`${scriptBasePath}.md`);
@@ -2089,8 +2454,8 @@ async function runDemoFromFile(definitionPath, context, options) {
2089
2454
  return runDemo(definition, context, options);
2090
2455
  }
2091
2456
  async function discoverDemos(demoDir) {
2092
- const files = await fs3.readdir(demoDir);
2093
- return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => path3.join(demoDir, f));
2457
+ const files = await fs4.readdir(demoDir);
2458
+ return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => path4.join(demoDir, f));
2094
2459
  }
2095
2460
  // Annotate the CommonJS export names for ESM import in node:
2096
2461
  0 && (module.exports = {