@t3lnet/sceneforge 1.0.7 → 1.0.9
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 +27 -0
- package/dist/index.cjs +419 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +137 -0
- package/dist/index.d.ts +137 -0
- package/dist/index.js +419 -54
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
256
|
-
return `${
|
|
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,
|
|
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 ${
|
|
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, `${
|
|
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, `${
|
|
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(
|
|
487
|
+
function createNavigateAction(path5) {
|
|
488
488
|
return {
|
|
489
489
|
action: "navigate",
|
|
490
|
-
path:
|
|
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
|
|
564
|
-
var
|
|
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:
|
|
1107
|
+
model_id: modelId,
|
|
758
1108
|
voice_settings: {
|
|
759
|
-
stability:
|
|
760
|
-
similarity_boost:
|
|
761
|
-
style:
|
|
762
|
-
use_speaker_boost:
|
|
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
|
|
1120
|
+
await fs2.writeFile(outputPath, audioBuffer);
|
|
771
1121
|
const probedDurationMs = await probeMediaDurationMs(outputPath);
|
|
1122
|
+
let durationMs;
|
|
772
1123
|
if (probedDurationMs !== null) {
|
|
773
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1166
|
+
const scriptContent = await fs2.readFile(scriptPath, "utf-8");
|
|
803
1167
|
const script = JSON.parse(scriptContent);
|
|
804
|
-
const audioDir =
|
|
805
|
-
await
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
913
|
-
var
|
|
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
|
|
1013
|
-
await
|
|
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
|
|
1033
|
-
await
|
|
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
|
|
1056
|
-
await
|
|
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
|
|
1104
|
-
await
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
1573
|
-
await
|
|
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 =
|
|
1578
|
-
await
|
|
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
|
|
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 (
|
|
1954
|
+
if (path4.isAbsolute(filePath)) {
|
|
1590
1955
|
return filePath;
|
|
1591
1956
|
}
|
|
1592
1957
|
const root = baseDir ?? process.cwd();
|
|
1593
|
-
return
|
|
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 ??
|
|
2036
|
-
const scriptBasePath =
|
|
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
|
|
2093
|
-
return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((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 = {
|