@stellartech/voice-widget-directus 1.0.2 → 1.0.4
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 +46 -0
- package/dist/index.js +321 -111
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @stellartech/voice-widget-directus
|
|
2
|
+
|
|
3
|
+
Voice generation widget with model/voice selection and audio preview for Directus.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @stellartech/voice-widget-directus
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Multiple TTS provider support (Gemini, ElevenLabs)
|
|
14
|
+
- Voice selection with audio sample previews
|
|
15
|
+
- Tone and style customization
|
|
16
|
+
- Full voiceover generation with progress tracking
|
|
17
|
+
- Async generation with background processing
|
|
18
|
+
- Audio playback and variant management
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Directus ^11.0.0
|
|
23
|
+
- Vue ^3.4.0
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
The widget requires the following collections in your Directus instance:
|
|
28
|
+
|
|
29
|
+
- `Voices` - Available voice options with samples
|
|
30
|
+
- `VoiceTones` - Tone presets for voice generation
|
|
31
|
+
- `VoiceStyles` - Style presets for voice generation
|
|
32
|
+
- `VoiceVariants` - Generated voice variants storage
|
|
33
|
+
- `AudioFiles` - Audio file records
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
After installation, the interface will be available in your Directus instance as a custom interface type. Configure it on a JSON field to enable voice generation for your content.
|
|
38
|
+
|
|
39
|
+
### Widget Options
|
|
40
|
+
|
|
41
|
+
- **Voices Collection**: Collection containing voice definitions
|
|
42
|
+
- **Flow ID**: Directus Flow ID for voice generation proxy
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -455,7 +455,7 @@ const VOICE_MODELS = [
|
|
|
455
455
|
{ id: "elevenlabs", name: "ElevenLabs" }
|
|
456
456
|
];
|
|
457
457
|
const SAMPLE_TEXT = "Hello! This is a sample of my voice. I hope you enjoy listening to how I sound.";
|
|
458
|
-
const DEFAULT_FLOW_ID = "
|
|
458
|
+
const DEFAULT_FLOW_ID = "7fa08903-ed7d-4632-81fc-f422d873b8f8";
|
|
459
459
|
function useVoicingApi(api) {
|
|
460
460
|
async function fetchVoices(collection = "Voices") {
|
|
461
461
|
try {
|
|
@@ -525,17 +525,29 @@ function useVoicingApi(api) {
|
|
|
525
525
|
return [];
|
|
526
526
|
}
|
|
527
527
|
}
|
|
528
|
-
async function generateVoiceSample(voiceId, provider, flowId = DEFAULT_FLOW_ID) {
|
|
528
|
+
async function generateVoiceSample(voiceId, provider, flowId = DEFAULT_FLOW_ID, lessonId, collection) {
|
|
529
529
|
const effectiveFlowId = flowId && flowId.trim() ? flowId.trim() : DEFAULT_FLOW_ID;
|
|
530
530
|
if (!effectiveFlowId) {
|
|
531
531
|
throw new Error("Voice flow ID is not configured. Set it in Widget Config or in the Flow ID field.");
|
|
532
532
|
}
|
|
533
|
+
if (!collection) {
|
|
534
|
+
throw new Error("collection is required but was not provided");
|
|
535
|
+
}
|
|
533
536
|
const voicingPayload = {
|
|
534
537
|
texts: [SAMPLE_TEXT],
|
|
535
538
|
provider,
|
|
536
539
|
preprocessing: false,
|
|
537
540
|
title: `Voice Sample - ${voiceId}`,
|
|
538
|
-
audio_files_collection: "AudioFiles"
|
|
541
|
+
audio_files_collection: "AudioFiles",
|
|
542
|
+
// Include lesson context for callback tracking
|
|
543
|
+
lesson_id: lessonId || null,
|
|
544
|
+
collection,
|
|
545
|
+
voice_config: {
|
|
546
|
+
provider,
|
|
547
|
+
voice_id: voiceId,
|
|
548
|
+
style: "neutral",
|
|
549
|
+
is_sample: true
|
|
550
|
+
}
|
|
539
551
|
};
|
|
540
552
|
if (provider === "gemini") {
|
|
541
553
|
voicingPayload.speakers = [{ voice: voiceId, name: "Sample", style: "neutral" }];
|
|
@@ -566,7 +578,8 @@ function useVoicingApi(api) {
|
|
|
566
578
|
const url = await resolveAudioFileUrl(audioFileId);
|
|
567
579
|
return { url, audioFileId };
|
|
568
580
|
}
|
|
569
|
-
|
|
581
|
+
console.log("[Voice Widget] Sample processing async, callback will update Voices.example");
|
|
582
|
+
return { url: "", audioFileId: "" };
|
|
570
583
|
} catch (e) {
|
|
571
584
|
console.error("Failed to generate voice sample:", e);
|
|
572
585
|
throw new Error(e.response?.data?.detail || e.message || "Failed to generate sample");
|
|
@@ -578,7 +591,10 @@ function useVoicingApi(api) {
|
|
|
578
591
|
if (!effectiveFlowId) {
|
|
579
592
|
throw new Error("Voice flow ID is not configured. Set it in Widget Config or in the Flow ID field.");
|
|
580
593
|
}
|
|
581
|
-
|
|
594
|
+
if (!request.collection) {
|
|
595
|
+
throw new Error("collection is required but was not provided");
|
|
596
|
+
}
|
|
597
|
+
const collection = request.collection;
|
|
582
598
|
let texts = [];
|
|
583
599
|
let lessonTitle = `Lesson ${request.lessonId}`;
|
|
584
600
|
try {
|
|
@@ -672,17 +688,30 @@ function useVoicingApi(api) {
|
|
|
672
688
|
return `/assets/${audioFilesRecordId}`;
|
|
673
689
|
}
|
|
674
690
|
async function fetchVoiceVariants(lessonId) {
|
|
691
|
+
const lessonIdStr = String(lessonId);
|
|
692
|
+
console.log("[Voice Widget] ========== fetchVoiceVariants START ==========");
|
|
693
|
+
console.log("[Voice Widget] Lesson ID:", lessonIdStr);
|
|
694
|
+
console.log("[Voice Widget] API object exists:", !!api);
|
|
675
695
|
try {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
696
|
+
console.log('[Voice Widget] Calling api.get("/items/VoiceVariants")...');
|
|
697
|
+
const response = await api.get("/items/VoiceVariants");
|
|
698
|
+
console.log("[Voice Widget] Response status:", response?.status);
|
|
699
|
+
console.log("[Voice Widget] Response data type:", typeof response?.data);
|
|
700
|
+
console.log("[Voice Widget] Response.data.data type:", typeof response?.data?.data);
|
|
701
|
+
const allData = response.data?.data || response.data || [];
|
|
702
|
+
console.log("[Voice Widget] Extracted data length:", allData?.length);
|
|
703
|
+
if (allData.length > 0) {
|
|
704
|
+
console.log("[Voice Widget] First item:", JSON.stringify(allData[0]));
|
|
705
|
+
}
|
|
706
|
+
const filtered = allData.filter((v) => String(v.lesson_id) === lessonIdStr);
|
|
707
|
+
console.log("[Voice Widget] Filtered count:", filtered.length);
|
|
708
|
+
console.log("[Voice Widget] ========== fetchVoiceVariants END ==========");
|
|
709
|
+
return filtered;
|
|
684
710
|
} catch (error) {
|
|
685
|
-
console.error("
|
|
711
|
+
console.error("[Voice Widget] ========== fetchVoiceVariants ERROR ==========");
|
|
712
|
+
console.error("[Voice Widget] Error:", error);
|
|
713
|
+
console.error("[Voice Widget] Error message:", error?.message);
|
|
714
|
+
console.error("[Voice Widget] Error response:", error?.response?.status, error?.response?.data);
|
|
686
715
|
return [];
|
|
687
716
|
}
|
|
688
717
|
}
|
|
@@ -699,13 +728,13 @@ function useVoicingApi(api) {
|
|
|
699
728
|
console.warn("[Voice Widget] Could not get file UUID from AudioFiles record. fileField:", fileField);
|
|
700
729
|
return;
|
|
701
730
|
}
|
|
702
|
-
console.log(`[Voice Widget] Patching ${collection}/${voiceId} with
|
|
731
|
+
console.log(`[Voice Widget] Patching ${collection}/${voiceId} with example=${fileUuid}`);
|
|
703
732
|
await api.patch(`/items/${collection}/${voiceId}`, {
|
|
704
|
-
|
|
733
|
+
example: fileUuid
|
|
705
734
|
});
|
|
706
|
-
console.log(`[Voice Widget] Updated voice ${voiceId}
|
|
735
|
+
console.log(`[Voice Widget] Updated voice ${voiceId} example to ${fileUuid}`);
|
|
707
736
|
} catch (error) {
|
|
708
|
-
console.error("[Voice Widget] Failed to update voice
|
|
737
|
+
console.error("[Voice Widget] Failed to update voice example:", error);
|
|
709
738
|
}
|
|
710
739
|
}
|
|
711
740
|
async function deleteVoiceVariant(variantId) {
|
|
@@ -768,6 +797,59 @@ function useVoicingApi(api) {
|
|
|
768
797
|
return null;
|
|
769
798
|
}
|
|
770
799
|
}
|
|
800
|
+
async function setVoiceExampleStatus(voiceId, status, collection = "Voices") {
|
|
801
|
+
try {
|
|
802
|
+
await api.patch(`/items/${collection}/${voiceId}`, {
|
|
803
|
+
example_status: status
|
|
804
|
+
});
|
|
805
|
+
console.log(`[Voice Widget] Set voice ${voiceId} example_status to ${status}`);
|
|
806
|
+
} catch (error) {
|
|
807
|
+
console.error("[Voice Widget] Failed to set example_status:", error);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async function createPendingVoiceVariant(lessonId, voiceConfig) {
|
|
811
|
+
try {
|
|
812
|
+
const response = await api.post("/items/VoiceVariants", {
|
|
813
|
+
lesson_id: lessonId,
|
|
814
|
+
voice_config: voiceConfig,
|
|
815
|
+
status: "processing"
|
|
816
|
+
});
|
|
817
|
+
const variantId = response.data.data?.id;
|
|
818
|
+
console.log(`[Voice Widget] Created pending VoiceVariant: ${variantId}`);
|
|
819
|
+
return variantId;
|
|
820
|
+
} catch (error) {
|
|
821
|
+
console.error("[Voice Widget] Failed to create pending VoiceVariant:", error);
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
async function fetchPendingVariants(lessonId) {
|
|
826
|
+
const lessonIdStr = String(lessonId);
|
|
827
|
+
console.log("[Voice Widget] ========== fetchPendingVariants START ==========");
|
|
828
|
+
try {
|
|
829
|
+
const response = await api.get("/items/VoiceVariants", {
|
|
830
|
+
params: {
|
|
831
|
+
fields: "id,lesson_id,voice_config,status,date_created",
|
|
832
|
+
limit: -1
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
const allData = response.data?.data || [];
|
|
836
|
+
console.log("[Voice Widget] fetchPendingVariants: got", allData.length, "total variants");
|
|
837
|
+
console.log("[Voice Widget] All statuses:", allData.map((v) => v.status));
|
|
838
|
+
const pending = allData.filter((v) => {
|
|
839
|
+
const matches = String(v.lesson_id) === lessonIdStr && v.status === "processing";
|
|
840
|
+
if (matches) {
|
|
841
|
+
console.log("[Voice Widget] Found PROCESSING variant:", v.id, "status:", v.status);
|
|
842
|
+
}
|
|
843
|
+
return matches;
|
|
844
|
+
});
|
|
845
|
+
console.log("[Voice Widget] fetchPendingVariants: returning", pending.length, "processing variants");
|
|
846
|
+
console.log("[Voice Widget] ========== fetchPendingVariants END ==========");
|
|
847
|
+
return pending;
|
|
848
|
+
} catch (error) {
|
|
849
|
+
console.error("[Voice Widget] Failed to fetch pending variants:", error);
|
|
850
|
+
return [];
|
|
851
|
+
}
|
|
852
|
+
}
|
|
771
853
|
return {
|
|
772
854
|
fetchVoices,
|
|
773
855
|
fetchFlowId,
|
|
@@ -782,7 +864,10 @@ function useVoicingApi(api) {
|
|
|
782
864
|
deleteVoiceVariant,
|
|
783
865
|
createVoiceVariant,
|
|
784
866
|
linkAudioToLesson,
|
|
785
|
-
pollVoicingJob
|
|
867
|
+
pollVoicingJob,
|
|
868
|
+
setVoiceExampleStatus,
|
|
869
|
+
createPendingVoiceVariant,
|
|
870
|
+
fetchPendingVariants
|
|
786
871
|
};
|
|
787
872
|
}
|
|
788
873
|
|
|
@@ -877,14 +962,17 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
877
962
|
deleteVoiceVariant,
|
|
878
963
|
createVoiceVariant,
|
|
879
964
|
linkAudioToLesson,
|
|
880
|
-
pollVoicingJob
|
|
965
|
+
pollVoicingJob,
|
|
966
|
+
setVoiceExampleStatus,
|
|
967
|
+
createPendingVoiceVariant,
|
|
968
|
+
fetchPendingVariants
|
|
881
969
|
} = useVoicingApi(api);
|
|
882
970
|
const currentMode = ref("selection");
|
|
883
971
|
const loading = ref(true);
|
|
884
972
|
const initError = ref(null);
|
|
885
973
|
const headerExpanded = ref(!props.defaultCollapsed);
|
|
886
974
|
const savingUrl = ref(false);
|
|
887
|
-
const flowId = ref("
|
|
975
|
+
const flowId = ref("7fa08903-ed7d-4632-81fc-f422d873b8f8");
|
|
888
976
|
const selectedModel = ref("gemini");
|
|
889
977
|
const selectedVoiceId = ref(null);
|
|
890
978
|
const voices = ref([]);
|
|
@@ -905,9 +993,11 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
905
993
|
const hasExistingVoices = ref(false);
|
|
906
994
|
const allVariants = ref([]);
|
|
907
995
|
const currentVariantIndex = ref(0);
|
|
908
|
-
|
|
996
|
+
ref(null);
|
|
909
997
|
const isPollingProgress = ref(false);
|
|
910
998
|
const progressStatus = ref("");
|
|
999
|
+
let samplePollingInterval = null;
|
|
1000
|
+
let voiceoverPollingInterval = null;
|
|
911
1001
|
const selectedVariant = computed(() => allVariants.value[currentVariantIndex.value]);
|
|
912
1002
|
const currentVoices = computed(() => {
|
|
913
1003
|
return voices.value.filter((v) => v.provider === selectedModel.value);
|
|
@@ -947,24 +1037,125 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
947
1037
|
}
|
|
948
1038
|
generatingSampleFor.value = voiceId;
|
|
949
1039
|
try {
|
|
950
|
-
|
|
951
|
-
const { url, audioFileId } = await generateVoiceSample(
|
|
952
|
-
voice.voice_id,
|
|
953
|
-
selectedModel.value,
|
|
954
|
-
effectiveFlowId
|
|
955
|
-
);
|
|
956
|
-
voiceSamples.value[voiceId] = url;
|
|
957
|
-
await updateVoiceSampleFile(voiceId, audioFileId, props.voicesCollection);
|
|
1040
|
+
await setVoiceExampleStatus(voiceId, "processing", props.voicesCollection);
|
|
958
1041
|
const voiceIndex = voices.value.findIndex((v) => v.id === voiceId);
|
|
959
1042
|
if (voiceIndex !== -1) {
|
|
960
|
-
voices.value[voiceIndex].
|
|
1043
|
+
voices.value[voiceIndex].example_status = "processing";
|
|
961
1044
|
}
|
|
1045
|
+
const effectiveFlowId = flowId.value && flowId.value.trim() ? flowId.value.trim() : void 0;
|
|
1046
|
+
if (!props.collection) {
|
|
1047
|
+
throw new Error("collection is required but was not provided");
|
|
1048
|
+
}
|
|
1049
|
+
generateVoiceSample(
|
|
1050
|
+
voice.voice,
|
|
1051
|
+
selectedModel.value,
|
|
1052
|
+
effectiveFlowId,
|
|
1053
|
+
props.primaryKey,
|
|
1054
|
+
props.collection
|
|
1055
|
+
).then(() => {
|
|
1056
|
+
console.log("[Voice Widget] Sample generation request sent for", voice.voice);
|
|
1057
|
+
}).catch((error) => {
|
|
1058
|
+
console.error("[Voice Widget] Failed to start sample generation:", error?.message ?? error);
|
|
1059
|
+
setVoiceExampleStatus(voiceId, null, props.voicesCollection);
|
|
1060
|
+
});
|
|
1061
|
+
setTimeout(() => {
|
|
1062
|
+
generatingSampleFor.value = null;
|
|
1063
|
+
}, 1500);
|
|
1064
|
+
startSamplePolling();
|
|
962
1065
|
} catch (error) {
|
|
963
1066
|
console.error("[Voice Widget] Failed to generate voice:", error?.message ?? error);
|
|
964
|
-
} finally {
|
|
965
1067
|
generatingSampleFor.value = null;
|
|
966
1068
|
}
|
|
967
1069
|
}
|
|
1070
|
+
function startSamplePolling() {
|
|
1071
|
+
if (samplePollingInterval) return;
|
|
1072
|
+
console.log("[Voice Widget] Starting sample completion polling");
|
|
1073
|
+
samplePollingInterval = setInterval(async () => {
|
|
1074
|
+
try {
|
|
1075
|
+
const freshVoices = await fetchVoices(props.voicesCollection);
|
|
1076
|
+
let hasProcessing = false;
|
|
1077
|
+
for (const freshVoice of freshVoices) {
|
|
1078
|
+
const localVoice = voices.value.find((v) => v.id === freshVoice.id);
|
|
1079
|
+
if (localVoice) {
|
|
1080
|
+
if (localVoice.example_status === "processing" && freshVoice.example_status !== "processing") {
|
|
1081
|
+
console.log("[Voice Widget] Sample completed for", freshVoice.voice);
|
|
1082
|
+
localVoice.example_status = freshVoice.example_status;
|
|
1083
|
+
localVoice.example = freshVoice.example;
|
|
1084
|
+
}
|
|
1085
|
+
if (freshVoice.example_status === "processing") {
|
|
1086
|
+
hasProcessing = true;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (!hasProcessing) {
|
|
1091
|
+
console.log("[Voice Widget] All samples completed, stopping polling");
|
|
1092
|
+
stopSamplePolling();
|
|
1093
|
+
}
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
console.error("[Voice Widget] Sample polling error:", error);
|
|
1096
|
+
}
|
|
1097
|
+
}, 3e3);
|
|
1098
|
+
}
|
|
1099
|
+
function stopSamplePolling() {
|
|
1100
|
+
if (samplePollingInterval) {
|
|
1101
|
+
clearInterval(samplePollingInterval);
|
|
1102
|
+
samplePollingInterval = null;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
let voiceoverPollCount = 0;
|
|
1106
|
+
let voiceoverLessonId = null;
|
|
1107
|
+
function startVoiceoverPolling() {
|
|
1108
|
+
stopVoiceoverPolling();
|
|
1109
|
+
voiceoverPollCount = 0;
|
|
1110
|
+
voiceoverLessonId = props.primaryKey ? String(props.primaryKey) : null;
|
|
1111
|
+
if (!voiceoverLessonId) {
|
|
1112
|
+
console.error("[Voice Widget] Cannot start polling - no primaryKey!");
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
console.log("[Voice Widget] Starting voiceover completion polling for lesson:", voiceoverLessonId);
|
|
1116
|
+
const checkCompletion = async () => {
|
|
1117
|
+
if (!voiceoverLessonId) {
|
|
1118
|
+
console.error("[Voice Widget] Poll aborted - no lesson ID");
|
|
1119
|
+
stopVoiceoverPolling();
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
voiceoverPollCount++;
|
|
1123
|
+
console.log(`[Voice Widget] Poll #${voiceoverPollCount} for lesson ${voiceoverLessonId}...`);
|
|
1124
|
+
try {
|
|
1125
|
+
const allVariantsNow = await fetchVoiceVariants(voiceoverLessonId);
|
|
1126
|
+
console.log(`[Voice Widget] Poll #${voiceoverPollCount} raw result:`, JSON.stringify(allVariantsNow));
|
|
1127
|
+
const completedWithAudio = allVariantsNow.filter((v) => v.audio_file_id);
|
|
1128
|
+
console.log(`[Voice Widget] Poll #${voiceoverPollCount}: total=${allVariantsNow.length}, withAudio=${completedWithAudio.length}`);
|
|
1129
|
+
if (completedWithAudio.length > 0) {
|
|
1130
|
+
console.log("[Voice Widget] COMPLETED! Transitioning to result...");
|
|
1131
|
+
stopVoiceoverPolling();
|
|
1132
|
+
allVariants.value = completedWithAudio;
|
|
1133
|
+
for (const v of allVariants.value) {
|
|
1134
|
+
if (v.audio_file_id) {
|
|
1135
|
+
const isUuid = v.audio_file_id?.includes("-");
|
|
1136
|
+
v.audioUrl = isUuid ? getAudioUrl(v.audio_file_id) : await resolveAudioFileUrl(v.audio_file_id);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
currentVariantIndex.value = 0;
|
|
1140
|
+
hasExistingVoices.value = true;
|
|
1141
|
+
currentMode.value = "result";
|
|
1142
|
+
console.log("[Voice Widget] Mode set to result, allVariants:", allVariants.value.length);
|
|
1143
|
+
} else {
|
|
1144
|
+
console.log(`[Voice Widget] Poll #${voiceoverPollCount} - waiting for audio_file_id...`);
|
|
1145
|
+
}
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
console.error("[Voice Widget] Voiceover polling error:", error);
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
voiceoverPollingInterval = setInterval(checkCompletion, 2e3);
|
|
1151
|
+
setTimeout(checkCompletion, 500);
|
|
1152
|
+
}
|
|
1153
|
+
function stopVoiceoverPolling() {
|
|
1154
|
+
if (voiceoverPollingInterval) {
|
|
1155
|
+
clearInterval(voiceoverPollingInterval);
|
|
1156
|
+
voiceoverPollingInterval = null;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
968
1159
|
function getSelectedTonePrompt() {
|
|
969
1160
|
if (selectedToneId.value === "other") {
|
|
970
1161
|
return customTone.value;
|
|
@@ -991,7 +1182,19 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
991
1182
|
const stylePrompt = getSelectedStylePrompt();
|
|
992
1183
|
const combinedStyle = `${tonePrompt}. ${stylePrompt}`.trim();
|
|
993
1184
|
const selectedVoice = voices.value.find((v) => v.id === selectedVoiceId.value);
|
|
994
|
-
const providerVoiceId = selectedVoice?.
|
|
1185
|
+
const providerVoiceId = selectedVoice?.voice || selectedVoiceId.value;
|
|
1186
|
+
const voiceConfig = {
|
|
1187
|
+
provider: selectedModel.value,
|
|
1188
|
+
voice_id: providerVoiceId,
|
|
1189
|
+
style: combinedStyle,
|
|
1190
|
+
tone_id: selectedToneId.value,
|
|
1191
|
+
style_id: selectedStyleId.value
|
|
1192
|
+
};
|
|
1193
|
+
await createPendingVoiceVariant(props.primaryKey, voiceConfig);
|
|
1194
|
+
startVoiceoverPolling();
|
|
1195
|
+
if (!props.collection) {
|
|
1196
|
+
throw new Error("collection is required but was not provided");
|
|
1197
|
+
}
|
|
995
1198
|
const result = await generateFullVoiceover({
|
|
996
1199
|
provider: selectedModel.value,
|
|
997
1200
|
voiceId: providerVoiceId,
|
|
@@ -999,21 +1202,12 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
999
1202
|
preprocessing: preprocessingEnabled.value,
|
|
1000
1203
|
flowId: flowId.value,
|
|
1001
1204
|
lessonId: props.primaryKey,
|
|
1002
|
-
collection: props.collection
|
|
1205
|
+
collection: props.collection,
|
|
1003
1206
|
toneId: selectedToneId.value || void 0,
|
|
1004
1207
|
styleId: selectedStyleId.value || void 0
|
|
1005
1208
|
});
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
voice_id: providerVoiceId,
|
|
1009
|
-
style: combinedStyle,
|
|
1010
|
-
tone_id: selectedToneId.value,
|
|
1011
|
-
style_id: selectedStyleId.value
|
|
1012
|
-
};
|
|
1013
|
-
if (result.job_id) {
|
|
1014
|
-
processingMessage.value = "Processing in background...";
|
|
1015
|
-
await startProgressPolling(result.job_id, voiceConfig);
|
|
1016
|
-
} else if (result.audio_file_id) {
|
|
1209
|
+
if (result.audio_file_id) {
|
|
1210
|
+
stopVoiceoverPolling();
|
|
1017
1211
|
generatedAudioId.value = result.audio_file_id;
|
|
1018
1212
|
generatedAudioUrl.value = getAudioUrl(result.audio_file_id);
|
|
1019
1213
|
progressPercent.value = 100;
|
|
@@ -1024,37 +1218,15 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1024
1218
|
);
|
|
1025
1219
|
await refreshVariants();
|
|
1026
1220
|
currentMode.value = "result";
|
|
1027
|
-
} else
|
|
1028
|
-
processingMessage.value = "
|
|
1029
|
-
|
|
1221
|
+
} else {
|
|
1222
|
+
processingMessage.value = "Generating voiceover...";
|
|
1223
|
+
console.log("[Voice Widget] Async generation started, polling for completion...");
|
|
1030
1224
|
}
|
|
1031
1225
|
} catch (error) {
|
|
1032
1226
|
errorMessage.value = error.message || "Failed to generate voiceover";
|
|
1033
1227
|
progressPercent.value = 0;
|
|
1034
1228
|
}
|
|
1035
1229
|
}
|
|
1036
|
-
async function pollForCompletion(callbackData, voiceConfig) {
|
|
1037
|
-
const maxAttempts = 60;
|
|
1038
|
-
const pollInterval = 5e3;
|
|
1039
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
1040
|
-
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
1041
|
-
progressPercent.value = Math.min(90, 30 + i / maxAttempts * 60);
|
|
1042
|
-
try {
|
|
1043
|
-
const variants = await fetchVoiceVariants(props.primaryKey);
|
|
1044
|
-
const latest = variants.find((v) => v.callback_data === callbackData);
|
|
1045
|
-
if (latest && latest.audio_file_id) {
|
|
1046
|
-
generatedAudioId.value = latest.audio_file_id;
|
|
1047
|
-
generatedAudioUrl.value = await resolveAudioFileUrl(latest.audio_file_id);
|
|
1048
|
-
progressPercent.value = 100;
|
|
1049
|
-
currentMode.value = "result";
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
} catch (error) {
|
|
1053
|
-
console.error("Poll error:", error);
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
errorMessage.value = "Generation timed out. Please try again.";
|
|
1057
|
-
}
|
|
1058
1230
|
function retryGeneration() {
|
|
1059
1231
|
errorMessage.value = null;
|
|
1060
1232
|
generateVoiceover();
|
|
@@ -1086,37 +1258,6 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1086
1258
|
console.error("Failed to delete variant:", error);
|
|
1087
1259
|
}
|
|
1088
1260
|
}
|
|
1089
|
-
async function startProgressPolling(jobId, voiceConfig) {
|
|
1090
|
-
currentJobId.value = jobId;
|
|
1091
|
-
isPollingProgress.value = true;
|
|
1092
|
-
while (isPollingProgress.value) {
|
|
1093
|
-
const job = await pollVoicingJob(jobId);
|
|
1094
|
-
if (job) {
|
|
1095
|
-
progressPercent.value = job.progress;
|
|
1096
|
-
processingMessage.value = job.message;
|
|
1097
|
-
progressStatus.value = job.status;
|
|
1098
|
-
if (job.status === "completed") {
|
|
1099
|
-
isPollingProgress.value = false;
|
|
1100
|
-
if (job.audio_file_id) {
|
|
1101
|
-
await createVoiceVariant(
|
|
1102
|
-
props.primaryKey,
|
|
1103
|
-
job.audio_file_id,
|
|
1104
|
-
voiceConfig
|
|
1105
|
-
);
|
|
1106
|
-
}
|
|
1107
|
-
await refreshVariants();
|
|
1108
|
-
currentMode.value = "result";
|
|
1109
|
-
return;
|
|
1110
|
-
}
|
|
1111
|
-
if (job.status.startsWith("failed")) {
|
|
1112
|
-
isPollingProgress.value = false;
|
|
1113
|
-
errorMessage.value = job.message || "Generation failed";
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
1261
|
async function refreshVariants() {
|
|
1121
1262
|
const variants = await fetchVoiceVariants(props.primaryKey);
|
|
1122
1263
|
allVariants.value = variants;
|
|
@@ -1143,11 +1284,12 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1143
1284
|
generatedAudioUrl.value = isUuid ? getAudioUrl(audioFileId) : await resolveAudioFileUrl(audioFileId);
|
|
1144
1285
|
const config = variant.voice_config;
|
|
1145
1286
|
if (config) {
|
|
1287
|
+
isRestoringFromVariant.value = true;
|
|
1146
1288
|
if (config.provider) {
|
|
1147
1289
|
selectedModel.value = config.provider;
|
|
1148
1290
|
}
|
|
1149
1291
|
if (config.voice_id) {
|
|
1150
|
-
const matchingVoice = voices.value.find((v) => v.name === config.voice_id);
|
|
1292
|
+
const matchingVoice = voices.value.find((v) => v.voice === config.voice_id || v.name === config.voice_id);
|
|
1151
1293
|
if (matchingVoice) {
|
|
1152
1294
|
selectedVoiceId.value = matchingVoice.id;
|
|
1153
1295
|
}
|
|
@@ -1161,19 +1303,27 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1161
1303
|
if (typeof config.preprocessing === "boolean") {
|
|
1162
1304
|
preprocessingEnabled.value = config.preprocessing;
|
|
1163
1305
|
}
|
|
1306
|
+
isRestoringFromVariant.value = false;
|
|
1164
1307
|
}
|
|
1165
1308
|
}
|
|
1166
|
-
function regenerateVoiceover() {
|
|
1309
|
+
async function regenerateVoiceover() {
|
|
1310
|
+
const variant = selectedVariant.value;
|
|
1311
|
+
if (variant) {
|
|
1312
|
+
await loadVariantAtIndex(currentVariantIndex.value);
|
|
1313
|
+
}
|
|
1167
1314
|
generateVoiceover();
|
|
1168
1315
|
}
|
|
1169
1316
|
async function confirmVoiceover() {
|
|
1170
1317
|
const variant = selectedVariant.value;
|
|
1171
1318
|
if (!variant) return;
|
|
1172
1319
|
try {
|
|
1320
|
+
if (!props.collection) {
|
|
1321
|
+
throw new Error("collection is required but was not provided");
|
|
1322
|
+
}
|
|
1173
1323
|
await linkAudioToLesson(
|
|
1174
1324
|
props.primaryKey,
|
|
1175
1325
|
variant.audio_file_id,
|
|
1176
|
-
props.collection
|
|
1326
|
+
props.collection
|
|
1177
1327
|
);
|
|
1178
1328
|
const value = {
|
|
1179
1329
|
audio_file_id: variant.audio_file_id,
|
|
@@ -1210,40 +1360,92 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1210
1360
|
}
|
|
1211
1361
|
}
|
|
1212
1362
|
async function initialize() {
|
|
1363
|
+
console.log("[Voice Widget] ===== INITIALIZE START =====");
|
|
1364
|
+
console.log("[Voice Widget] Props:", {
|
|
1365
|
+
collection: props.collection,
|
|
1366
|
+
primaryKey: props.primaryKey,
|
|
1367
|
+
voicesCollection: props.voicesCollection,
|
|
1368
|
+
tonesCollection: props.tonesCollection,
|
|
1369
|
+
stylesCollection: props.stylesCollection,
|
|
1370
|
+
flowId: props.flowId
|
|
1371
|
+
});
|
|
1213
1372
|
loading.value = true;
|
|
1214
1373
|
initError.value = null;
|
|
1215
1374
|
try {
|
|
1216
1375
|
if (!props.flowId) {
|
|
1217
1376
|
flowId.value = await fetchFlowId();
|
|
1377
|
+
console.log("[Voice Widget] Fetched flowId:", flowId.value);
|
|
1218
1378
|
} else {
|
|
1219
1379
|
flowId.value = props.flowId;
|
|
1380
|
+
console.log("[Voice Widget] Using props flowId:", flowId.value);
|
|
1220
1381
|
}
|
|
1382
|
+
console.log("[Voice Widget] Fetching voices, tones, styles...");
|
|
1221
1383
|
const [loadedVoices, loadedTones, loadedStyles] = await Promise.all([
|
|
1222
1384
|
fetchVoices(props.voicesCollection),
|
|
1223
1385
|
fetchTones(props.tonesCollection),
|
|
1224
1386
|
fetchStyles(props.stylesCollection)
|
|
1225
1387
|
]);
|
|
1388
|
+
console.log("[Voice Widget] Loaded voices:", loadedVoices.length, loadedVoices);
|
|
1389
|
+
console.log("[Voice Widget] Loaded tones:", loadedTones.length, loadedTones);
|
|
1390
|
+
console.log("[Voice Widget] Loaded styles:", loadedStyles.length, loadedStyles);
|
|
1226
1391
|
voices.value = loadedVoices;
|
|
1227
1392
|
tones.value = loadedTones;
|
|
1228
1393
|
styles.value = loadedStyles;
|
|
1394
|
+
console.log("[Voice Widget] currentVoices (filtered by model):", currentVoices.value.length);
|
|
1229
1395
|
if (currentVoices.value.length > 0 && !selectedVoiceId.value) {
|
|
1230
1396
|
selectedVoiceId.value = currentVoices.value[0].id;
|
|
1397
|
+
console.log("[Voice Widget] Auto-selected voice:", selectedVoiceId.value);
|
|
1231
1398
|
}
|
|
1232
1399
|
if (tones.value.length > 0 && !selectedToneId.value) {
|
|
1233
1400
|
selectedToneId.value = tones.value[0].id;
|
|
1401
|
+
console.log("[Voice Widget] Auto-selected tone:", selectedToneId.value);
|
|
1234
1402
|
}
|
|
1235
1403
|
if (styles.value.length > 0 && !selectedStyleId.value) {
|
|
1236
1404
|
selectedStyleId.value = styles.value[0].id;
|
|
1405
|
+
console.log("[Voice Widget] Auto-selected style:", selectedStyleId.value);
|
|
1406
|
+
}
|
|
1407
|
+
const processingVoices = voices.value.filter((v) => v.example_status === "processing");
|
|
1408
|
+
if (processingVoices.length > 0) {
|
|
1409
|
+
console.log("[Voice Widget] Found", processingVoices.length, "voices with processing samples");
|
|
1410
|
+
startSamplePolling();
|
|
1237
1411
|
}
|
|
1238
1412
|
if (props.primaryKey) {
|
|
1413
|
+
console.log("[Voice Widget] Checking variants for lesson:", props.primaryKey);
|
|
1239
1414
|
const variants = await fetchVoiceVariants(props.primaryKey);
|
|
1240
|
-
|
|
1415
|
+
console.log("[Voice Widget] All variants for lesson:", variants.length);
|
|
1416
|
+
const processingVariants = variants.filter((v) => v.status === "processing" && !v.audio_file_id);
|
|
1417
|
+
if (processingVariants.length > 0) {
|
|
1418
|
+
console.log("[Voice Widget] Found", processingVariants.length, "processing voiceovers, showing progress");
|
|
1419
|
+
currentMode.value = "progress";
|
|
1420
|
+
processingMessage.value = "Generating voiceover...";
|
|
1421
|
+
progressPercent.value = 50;
|
|
1422
|
+
startVoiceoverPolling();
|
|
1423
|
+
} else {
|
|
1424
|
+
const completedVariants = variants.filter((v) => v.audio_file_id);
|
|
1425
|
+
hasExistingVoices.value = completedVariants.length > 0;
|
|
1426
|
+
console.log("[Voice Widget] Completed variants with audio:", completedVariants.length);
|
|
1427
|
+
if (completedVariants.length > 0) {
|
|
1428
|
+
allVariants.value = completedVariants;
|
|
1429
|
+
for (const v of allVariants.value) {
|
|
1430
|
+
if (v.audio_file_id) {
|
|
1431
|
+
const isUuid = v.audio_file_id?.includes("-");
|
|
1432
|
+
v.audioUrl = isUuid ? getAudioUrl(v.audio_file_id) : await resolveAudioFileUrl(v.audio_file_id);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
currentVariantIndex.value = 0;
|
|
1436
|
+
currentMode.value = "result";
|
|
1437
|
+
console.log("[Voice Widget] AUTO-SHOWING RESULT PAGE with", completedVariants.length, "variants");
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1241
1440
|
}
|
|
1242
1441
|
initFromValue();
|
|
1442
|
+
console.log("[Voice Widget] ===== INITIALIZE SUCCESS =====");
|
|
1243
1443
|
} catch (error) {
|
|
1444
|
+
console.error("[Voice Widget] ===== INITIALIZE ERROR =====", error);
|
|
1244
1445
|
initError.value = error.message || "Failed to load configuration";
|
|
1245
1446
|
} finally {
|
|
1246
1447
|
loading.value = false;
|
|
1448
|
+
console.log("[Voice Widget] loading set to false, currentMode:", currentMode.value);
|
|
1247
1449
|
}
|
|
1248
1450
|
}
|
|
1249
1451
|
async function initFromValue() {
|
|
@@ -1282,12 +1484,20 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1282
1484
|
}
|
|
1283
1485
|
}
|
|
1284
1486
|
watch(() => props.value, initFromValue, { deep: true });
|
|
1487
|
+
const isRestoringFromVariant = ref(false);
|
|
1285
1488
|
watch(selectedModel, () => {
|
|
1286
|
-
if (currentVoices.value.length > 0) {
|
|
1489
|
+
if (!isRestoringFromVariant.value && currentVoices.value.length > 0) {
|
|
1287
1490
|
selectedVoiceId.value = currentVoices.value[0].id;
|
|
1288
1491
|
}
|
|
1289
1492
|
});
|
|
1290
|
-
onMounted(
|
|
1493
|
+
onMounted(() => {
|
|
1494
|
+
console.log("[Voice Widget] onMounted called");
|
|
1495
|
+
initialize();
|
|
1496
|
+
});
|
|
1497
|
+
onUnmounted(() => {
|
|
1498
|
+
stopSamplePolling();
|
|
1499
|
+
stopVoiceoverPolling();
|
|
1500
|
+
});
|
|
1291
1501
|
return (_ctx, _cache) => {
|
|
1292
1502
|
const _component_v_icon = resolveComponent("v-icon");
|
|
1293
1503
|
return openBlock(), createElementBlock("div", _hoisted_1, [
|
|
@@ -1495,8 +1705,8 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1495
1705
|
key: voice.id,
|
|
1496
1706
|
voice,
|
|
1497
1707
|
"is-selected": selectedVoiceId.value === voice.id,
|
|
1498
|
-
loading: generatingSampleFor.value === voice.id,
|
|
1499
|
-
"sample-url": voiceSamples.value[voice.id] || (voice.
|
|
1708
|
+
loading: generatingSampleFor.value === voice.id || voice.example_status === "processing",
|
|
1709
|
+
"sample-url": voiceSamples.value[voice.id] || (voice.example ? `/assets/${voice.example}` : voice.sample_url),
|
|
1500
1710
|
provider: selectedModel.value,
|
|
1501
1711
|
onSelect: selectVoice,
|
|
1502
1712
|
onGenerateVoice: generateVoice
|
|
@@ -1755,10 +1965,10 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1755
1965
|
}
|
|
1756
1966
|
});
|
|
1757
1967
|
|
|
1758
|
-
var css = "\n.voice-widget[data-v-
|
|
1968
|
+
var css = "\n.voice-widget[data-v-5371543d] {\n font-family: var(--theme--fonts--sans--font-family);\n padding: 16px;\n border: 1px solid var(--theme--form--field--input--border-color);\n border-radius: var(--theme--border-radius);\n background: var(--theme--background);\n}\n.widget__header[data-v-5371543d] {\n margin-bottom: 20px;\n}\n.widget__header-row[data-v-5371543d] {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n gap: 16px;\n}\n.widget__header-text[data-v-5371543d] {\n flex: 1;\n}\n.widget__title[data-v-5371543d] {\n display: flex;\n align-items: center;\n gap: 8px;\n margin: 0 0 4px 0;\n font-size: 18px;\n font-weight: 600;\n color: var(--theme--foreground);\n}\n.widget__collapse-btn[data-v-5371543d] {\n background: none;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: var(--theme--foreground-subdued);\n border-radius: 4px;\n}\n.widget__collapse-btn[data-v-5371543d]:hover {\n background: var(--theme--background-accent);\n}\n.widget__subtitle[data-v-5371543d] {\n margin: 0;\n font-size: 14px;\n color: var(--theme--foreground-subdued);\n}\n.widget__header-controls[data-v-5371543d] {\n display: flex;\n gap: 16px;\n align-items: center;\n}\n.widget__url-input[data-v-5371543d] {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.widget__url-label[data-v-5371543d] {\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n white-space: nowrap;\n}\n.widget__url-field[data-v-5371543d] {\n padding: 6px 10px;\n border: 1px solid var(--theme--form--field--input--border-color);\n border-radius: var(--theme--border-radius);\n font-size: 12px;\n width: 280px;\n background: var(--theme--form--field--input--background);\n color: var(--theme--foreground);\n}\n.widget__section[data-v-5371543d] {\n margin-bottom: 20px;\n}\n.widget__section-label[data-v-5371543d] {\n display: block;\n margin-bottom: 8px;\n font-size: 14px;\n font-weight: 500;\n color: var(--theme--foreground);\n}\n.widget__model-buttons[data-v-5371543d] {\n display: flex;\n gap: 8px;\n}\n.widget__model-btn[data-v-5371543d] {\n padding: 8px 16px;\n border: 1px solid var(--theme--form--field--input--border-color);\n border-radius: var(--theme--border-radius);\n background: var(--theme--background);\n color: var(--theme--foreground);\n cursor: pointer;\n font-size: 14px;\n transition: all 0.15s ease;\n}\n.widget__model-btn[data-v-5371543d]:hover {\n border-color: var(--theme--primary);\n}\n.widget__model-btn--active[data-v-5371543d] {\n background: var(--theme--primary);\n border-color: var(--theme--primary);\n color: var(--theme--primary-foreground, #fff);\n}\n.widget__voice-list[data-v-5371543d] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.widget__section--toggle[data-v-5371543d] {\n padding: 12px;\n background: var(--theme--background-subdued);\n border-radius: var(--theme--border-radius);\n}\n.widget__toggle[data-v-5371543d] {\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n}\n.widget__toggle input[data-v-5371543d] {\n width: 16px;\n height: 16px;\n cursor: pointer;\n}\n.widget__toggle-label[data-v-5371543d] {\n font-size: 14px;\n font-weight: 500;\n color: var(--theme--foreground);\n}\n.widget__toggle-note[data-v-5371543d] {\n margin: 4px 0 0 24px;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n}\n.widget__footer[data-v-5371543d] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding-top: 16px;\n border-top: 1px solid var(--theme--border-color-subdued);\n margin-top: 20px;\n}\n.widget__footer-left[data-v-5371543d],\n.widget__footer-right[data-v-5371543d] {\n display: flex;\n gap: 8px;\n}\n.widget__variant-nav[data-v-5371543d] {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 12px;\n margin-bottom: 16px;\n padding: 8px 0;\n}\n.widget__variant-counter[data-v-5371543d] {\n font-size: 14px;\n font-weight: 500;\n color: var(--theme--foreground-subdued);\n min-width: 60px;\n text-align: center;\n}\n.widget__btn--icon[data-v-5371543d] {\n padding: 6px;\n min-width: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.widget__result-date[data-v-5371543d] {\n margin-top: 8px;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n}\n.widget__btn[data-v-5371543d] {\n padding: 8px 16px;\n border: none;\n border-radius: var(--theme--border-radius);\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n.widget__btn[data-v-5371543d]:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.widget__btn--primary[data-v-5371543d] {\n background: var(--theme--primary);\n color: var(--theme--primary-foreground, #fff);\n}\n.widget__btn--primary[data-v-5371543d]:hover:not(:disabled) {\n background: var(--theme--primary-accent);\n}\n.widget__btn--secondary[data-v-5371543d] {\n background: var(--theme--background-accent);\n color: var(--theme--foreground);\n border: 1px solid var(--theme--form--field--input--border-color);\n}\n.widget__btn--secondary[data-v-5371543d]:hover:not(:disabled) {\n background: var(--theme--background-normal);\n}\n\n/* Processing State */\n.widget__processing[data-v-5371543d] {\n padding: 40px 20px;\n text-align: center;\n}\n.widget__progress[data-v-5371543d] {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 12px;\n margin-bottom: 16px;\n font-size: 16px;\n color: var(--theme--foreground);\n}\n.widget__progress-bar[data-v-5371543d] {\n height: 8px;\n background: var(--theme--background-accent);\n border-radius: 4px;\n overflow: hidden;\n max-width: 400px;\n margin: 0 auto;\n}\n.widget__progress-fill[data-v-5371543d] {\n height: 100%;\n background: var(--theme--primary);\n transition: width 0.3s ease;\n}\n\n/* Error State */\n.widget__error[data-v-5371543d] {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n padding: 20px;\n color: var(--theme--danger);\n text-align: center;\n}\n.widget__error-actions[data-v-5371543d] {\n display: flex;\n gap: 8px;\n margin-top: 8px;\n}\n.widget__retry[data-v-5371543d] {\n padding: 6px 12px;\n background: var(--theme--danger);\n color: #fff;\n border: none;\n border-radius: var(--theme--border-radius);\n cursor: pointer;\n}\n\n/* Loading State */\n.widget__loading[data-v-5371543d] {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 12px;\n padding: 40px 20px;\n color: var(--theme--foreground-subdued);\n}\n\n/* Result State */\n.widget__result[data-v-5371543d] {\n padding: 20px;\n background: var(--theme--background-subdued);\n border-radius: var(--theme--border-radius);\n margin-bottom: 20px;\n}\n.widget__result-info[data-v-5371543d] {\n margin-top: 16px;\n padding-top: 16px;\n border-top: 1px solid var(--theme--border-color-subdued);\n}\n.widget__result-info p[data-v-5371543d] {\n margin: 4px 0;\n font-size: 14px;\n color: var(--theme--foreground-subdued);\n}\n.widget__result-info strong[data-v-5371543d] {\n color: var(--theme--foreground);\n}\n\n/* Variants List View */\n.widget__variants-list[data-v-5371543d] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n max-height: 400px;\n overflow-y: auto;\n margin-bottom: 16px;\n}\n.widget__variant-item[data-v-5371543d] {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px;\n border: 1px solid var(--theme--border-color-subdued);\n border-radius: var(--theme--border-radius);\n cursor: pointer;\n transition: all 0.15s ease;\n}\n.widget__variant-item[data-v-5371543d]:hover {\n border-color: var(--theme--primary);\n}\n.widget__variant-item--selected[data-v-5371543d] {\n border-color: var(--theme--primary);\n background: var(--theme--primary-background);\n}\n.widget__variant-radio[data-v-5371543d] {\n flex-shrink: 0;\n}\n.widget__variant-radio input[data-v-5371543d] {\n width: 16px;\n height: 16px;\n cursor: pointer;\n}\n.widget__variant-player[data-v-5371543d] {\n flex: 1;\n min-width: 200px;\n}\n.widget__variant-meta[data-v-5371543d] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 120px;\n}\n.widget__variant-voice[data-v-5371543d] {\n font-size: 13px;\n font-weight: 500;\n color: var(--theme--foreground);\n}\n.widget__variant-date[data-v-5371543d] {\n font-size: 11px;\n color: var(--theme--foreground-subdued);\n}\n.widget__btn--danger[data-v-5371543d] {\n color: var(--theme--danger);\n background: transparent;\n border: none;\n}\n.widget__btn--danger[data-v-5371543d]:hover {\n background: var(--theme--danger-background);\n}\n.widget__selected-info[data-v-5371543d] {\n padding: 12px;\n background: var(--theme--background-subdued);\n border-radius: var(--theme--border-radius);\n margin-bottom: 16px;\n}\n.widget__selected-info p[data-v-5371543d] {\n margin: 4px 0;\n font-size: 14px;\n color: var(--theme--foreground-subdued);\n}\n.widget__selected-info strong[data-v-5371543d] {\n color: var(--theme--foreground);\n}\n.widget__progress-status[data-v-5371543d] {\n text-align: center;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n margin-top: 8px;\n}\n\n/* Animations */\n.spinning[data-v-5371543d] {\n animation: spin-5371543d 1s linear infinite;\n}\n@keyframes spin-5371543d {\nfrom { transform: rotate(0deg);\n}\nto { transform: rotate(360deg);\n}\n}\n";
|
|
1759
1969
|
n(css,{});
|
|
1760
1970
|
|
|
1761
|
-
var InterfaceComponent = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-
|
|
1971
|
+
var InterfaceComponent = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-5371543d"], ["__file", "interface.vue"]]);
|
|
1762
1972
|
|
|
1763
1973
|
var index = defineInterface({
|
|
1764
1974
|
id: "voice-widget",
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stellartech/voice-widget-directus",
|
|
3
3
|
"description": "Voice generation widget with model/voice selection and audio preview for Directus",
|
|
4
4
|
"icon": "mic",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.4",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"readme": "README.md",
|
|
8
8
|
"repository": {
|