@stellartech/voice-widget-directus 1.0.0 → 1.0.2

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.
Files changed (2) hide show
  1. package/dist/index.js +444 -307
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -461,14 +461,13 @@ function useVoicingApi(api) {
461
461
  try {
462
462
  const response = await api.get(`/items/${collection}`, {
463
463
  params: {
464
- filter: { status: { _eq: "published" } },
465
464
  sort: ["sort", "name"],
466
- fields: ["id", "name", "voice_id", "provider", "description", "sample_url", "sample_file", "sort"]
465
+ fields: ["*"]
467
466
  }
468
467
  });
469
468
  return response.data.data || [];
470
469
  } catch (error) {
471
- console.error("Failed to fetch voices:", error);
470
+ console.error("Failed to fetch voices:", error?.response?.data || error?.message || error);
472
471
  return [];
473
472
  }
474
473
  }
@@ -486,7 +485,6 @@ function useVoicingApi(api) {
486
485
  try {
487
486
  const response = await api.get(`/items/${collection}`, {
488
487
  params: {
489
- filter: { status: { _eq: "published" } },
490
488
  sort: ["sort", "name"],
491
489
  fields: ["id", "name", "prompt", "sort", "is_custom"]
492
490
  }
@@ -502,19 +500,13 @@ function useVoicingApi(api) {
502
500
  return tones;
503
501
  } catch (error) {
504
502
  console.error("Failed to fetch tones:", error);
505
- return [
506
- { id: "teacher", name: "Teacher", prompt: "Speak in a clear, educational, and patient manner", sort: 1, is_custom: false },
507
- { id: "storyteller", name: "Storyteller", prompt: "Speak in an engaging, narrative style with varied pacing", sort: 2, is_custom: false },
508
- { id: "podcaster", name: "Podcaster", prompt: "Speak in a conversational, informal yet professional tone", sort: 3, is_custom: false },
509
- { id: "other", name: "Other (Custom)", prompt: "", sort: 9999, is_custom: true }
510
- ];
503
+ return [];
511
504
  }
512
505
  }
513
506
  async function fetchStyles(collection = "VoiceStyles") {
514
507
  try {
515
508
  const response = await api.get(`/items/${collection}`, {
516
509
  params: {
517
- filter: { status: { _eq: "published" } },
518
510
  sort: ["sort", "name"],
519
511
  fields: ["id", "name", "prompt", "sort", "is_custom"]
520
512
  }
@@ -530,12 +522,7 @@ function useVoicingApi(api) {
530
522
  return styles;
531
523
  } catch (error) {
532
524
  console.error("Failed to fetch styles:", error);
533
- return [
534
- { id: "happy", name: "Happy", prompt: "enthusiastic and cheerful", sort: 1, is_custom: false },
535
- { id: "chill", name: "Chill", prompt: "relaxed and calm", sort: 2, is_custom: false },
536
- { id: "excited", name: "Excited", prompt: "energetic and animated", sort: 3, is_custom: false },
537
- { id: "other", name: "Other (Custom)", prompt: "", sort: 9999, is_custom: true }
538
- ];
525
+ return [];
539
526
  }
540
527
  }
541
528
  async function generateVoiceSample(voiceId, provider, flowId = DEFAULT_FLOW_ID) {
@@ -547,7 +534,8 @@ function useVoicingApi(api) {
547
534
  texts: [SAMPLE_TEXT],
548
535
  provider,
549
536
  preprocessing: false,
550
- title: `Voice Sample - ${voiceId}`
537
+ title: `Voice Sample - ${voiceId}`,
538
+ audio_files_collection: "AudioFiles"
551
539
  };
552
540
  if (provider === "gemini") {
553
541
  voicingPayload.speakers = [{ voice: voiceId, name: "Sample", style: "neutral" }];
@@ -557,14 +545,28 @@ function useVoicingApi(api) {
557
545
  voicingPayload.voice_id = voiceId;
558
546
  }
559
547
  try {
548
+ console.log("[Voice Widget] Calling flow:", effectiveFlowId);
560
549
  const response = await api.post(`/flows/trigger/${effectiveFlowId}`, voicingPayload);
550
+ console.log("[Voice Widget] Full response:", JSON.stringify(response.data, null, 2));
561
551
  const flowResponse = response.data;
562
- const audioFileId = flowResponse?.data?.audio_file_id || flowResponse?.audio_file_id || null;
552
+ let audioFileId = null;
553
+ if (flowResponse?.data?.audio_file_id) {
554
+ audioFileId = flowResponse.data.audio_file_id;
555
+ } else if (flowResponse?.audio_file_id) {
556
+ audioFileId = flowResponse.audio_file_id;
557
+ } else if (typeof flowResponse === "string") {
558
+ try {
559
+ const parsed = JSON.parse(flowResponse);
560
+ audioFileId = parsed?.data?.audio_file_id || parsed?.audio_file_id;
561
+ } catch {
562
+ }
563
+ }
564
+ console.log("[Voice Widget] Extracted audioFileId:", audioFileId);
563
565
  if (audioFileId) {
564
566
  const url = await resolveAudioFileUrl(audioFileId);
565
567
  return { url, audioFileId };
566
568
  }
567
- throw new Error("No audio file returned from voice generation");
569
+ throw new Error("No audio file returned from voice generation. Response: " + JSON.stringify(flowResponse));
568
570
  } catch (e) {
569
571
  console.error("Failed to generate voice sample:", e);
570
572
  throw new Error(e.response?.data?.detail || e.message || "Failed to generate sample");
@@ -576,10 +578,11 @@ function useVoicingApi(api) {
576
578
  if (!effectiveFlowId) {
577
579
  throw new Error("Voice flow ID is not configured. Set it in Widget Config or in the Flow ID field.");
578
580
  }
581
+ const collection = request.collection || "SM_Lessons";
579
582
  let texts = [];
580
583
  let lessonTitle = `Lesson ${request.lessonId}`;
581
584
  try {
582
- const lessonResponse = await api.get(`/items/SM_Lessons/${request.lessonId}`, {
585
+ const lessonResponse = await api.get(`/items/${collection}/${request.lessonId}`, {
583
586
  params: { fields: ["text", "title"] }
584
587
  });
585
588
  const textContent = lessonResponse.data.data?.text || "";
@@ -596,11 +599,24 @@ function useVoicingApi(api) {
596
599
  console.error("Failed to fetch lesson text content:", e);
597
600
  throw new Error(e.message || "Failed to fetch lesson text content");
598
601
  }
602
+ const voiceConfig = {
603
+ provider: request.provider,
604
+ voice_id: request.voiceId,
605
+ style: request.style,
606
+ preprocessing: request.preprocessing,
607
+ tone_id: request.toneId,
608
+ style_id: request.styleId
609
+ };
599
610
  const voicingPayload = {
600
611
  texts,
601
612
  provider: request.provider,
602
613
  preprocessing: request.preprocessing,
603
- title: `Voiceover - ${lessonTitle}`
614
+ title: `Voiceover - ${lessonTitle}`,
615
+ audio_files_collection: "AudioFiles",
616
+ // Pass these so the flow can include them in callback_data
617
+ lesson_id: request.lessonId,
618
+ collection,
619
+ voice_config: voiceConfig
604
620
  };
605
621
  if (request.provider === "gemini") {
606
622
  voicingPayload.speakers = [{
@@ -617,43 +633,20 @@ function useVoicingApi(api) {
617
633
  const response = await api.post(`/flows/trigger/${effectiveFlowId}`, voicingPayload);
618
634
  const flowResponse = response.data;
619
635
  const data = flowResponse?.data || flowResponse;
620
- let fileUuid = null;
621
- if (data?.audio_file_id) {
636
+ let jobId = null;
637
+ if (data?.callback_data) {
622
638
  try {
623
- const audioFilesResponse = await api.get(`/items/AudioFiles/${data.audio_file_id}`, {
624
- params: { fields: ["file"] }
625
- });
626
- fileUuid = audioFilesResponse.data.data?.file || null;
627
- } catch (e) {
628
- console.warn("[Voice Widget] Could not resolve file UUID:", e);
629
- }
630
- if (fileUuid) {
631
- try {
632
- await api.post("/items/VoiceVariants", {
633
- lesson_id: request.lessonId,
634
- audio_file_id: fileUuid,
635
- voice_config: {
636
- provider: request.provider,
637
- voice_id: request.voiceId,
638
- style: request.style,
639
- preprocessing: request.preprocessing,
640
- tone_id: request.toneId,
641
- style_id: request.styleId
642
- },
643
- callback_data: data.callback_data || null,
644
- status: "published"
645
- });
646
- console.log("[Voice Widget] Saved voice variant with file UUID:", fileUuid);
647
- } catch (saveError) {
648
- console.warn("Failed to save voice variant:", saveError);
649
- }
639
+ const callbackData = JSON.parse(data.callback_data);
640
+ jobId = callbackData.job_id || null;
641
+ } catch {
650
642
  }
651
643
  }
652
644
  return {
653
- audio_file_id: fileUuid || data?.audio_file_id || null,
654
- status: data?.status || null,
645
+ audio_file_id: data?.audio_file_id || null,
646
+ status: data?.status || "processing",
655
647
  callback_data: data?.callback_data || null,
656
- generation_time: data?.generation_time || null
648
+ generation_time: data?.generation_time || null,
649
+ job_id: jobId
657
650
  };
658
651
  } catch (e) {
659
652
  console.error("Failed to generate voiceover:", e);
@@ -668,7 +661,8 @@ function useVoicingApi(api) {
668
661
  const response = await api.get(`/items/AudioFiles/${audioFilesRecordId}`, {
669
662
  params: { fields: ["file"] }
670
663
  });
671
- const fileId = response.data.data?.file;
664
+ const fileField = response.data.data?.file;
665
+ const fileId = typeof fileField === "string" ? fileField : fileField?.id;
672
666
  if (fileId) {
673
667
  return `/assets/${fileId}`;
674
668
  }
@@ -694,14 +688,18 @@ function useVoicingApi(api) {
694
688
  }
695
689
  async function updateVoiceSampleFile(voiceId, audioFilesRecordId, collection = "Voices") {
696
690
  try {
691
+ console.log(`[Voice Widget] updateVoiceSampleFile called with voiceId=${voiceId}, audioFilesRecordId=${audioFilesRecordId}, collection=${collection}`);
697
692
  const response = await api.get(`/items/AudioFiles/${audioFilesRecordId}`, {
698
693
  params: { fields: ["file"] }
699
694
  });
700
- const fileUuid = response.data.data?.file;
695
+ console.log("[Voice Widget] AudioFiles response:", response.data);
696
+ const fileField = response.data.data?.file;
697
+ const fileUuid = typeof fileField === "string" ? fileField : fileField?.id;
701
698
  if (!fileUuid) {
702
- console.warn("[Voice Widget] Could not get file UUID from AudioFiles record");
699
+ console.warn("[Voice Widget] Could not get file UUID from AudioFiles record. fileField:", fileField);
703
700
  return;
704
701
  }
702
+ console.log(`[Voice Widget] Patching ${collection}/${voiceId} with sample_file=${fileUuid}`);
705
703
  await api.patch(`/items/${collection}/${voiceId}`, {
706
704
  sample_file: fileUuid
707
705
  });
@@ -710,6 +708,66 @@ function useVoicingApi(api) {
710
708
  console.error("[Voice Widget] Failed to update voice sample_file:", error);
711
709
  }
712
710
  }
711
+ async function deleteVoiceVariant(variantId) {
712
+ try {
713
+ await api.delete(`/items/VoiceVariants/${variantId}`);
714
+ console.log(`[Voice Widget] Deleted voice variant: ${variantId}`);
715
+ } catch (error) {
716
+ console.error("[Voice Widget] Failed to delete voice variant:", error);
717
+ throw error;
718
+ }
719
+ }
720
+ async function createVoiceVariant(lessonId, audioFileId, voiceConfig) {
721
+ try {
722
+ const response = await api.post("/items/VoiceVariants", {
723
+ lesson_id: lessonId,
724
+ audio_file_id: audioFileId,
725
+ voice_config: voiceConfig,
726
+ status: "draft"
727
+ });
728
+ const variantId = response.data.data?.id;
729
+ console.log(`[Voice Widget] Created VoiceVariant: ${variantId}`);
730
+ return variantId;
731
+ } catch (error) {
732
+ console.error("[Voice Widget] Failed to create VoiceVariant:", error);
733
+ throw error;
734
+ }
735
+ }
736
+ async function linkAudioToLesson(lessonId, audioFileId, collection) {
737
+ try {
738
+ await api.patch(`/items/${collection}/${lessonId}`, {
739
+ audio: audioFileId
740
+ });
741
+ console.log(`[Voice Widget] Linked audio ${audioFileId} to lesson ${lessonId} in ${collection}`);
742
+ } catch (error) {
743
+ console.error("[Voice Widget] Failed to link audio to lesson:", error);
744
+ throw error;
745
+ }
746
+ }
747
+ async function pollVoicingJob(jobId) {
748
+ try {
749
+ const response = await api.get("/items/VoicingJobs", {
750
+ params: {
751
+ filter: { job_id: { _eq: jobId } },
752
+ fields: ["status", "progress", "message", "audio_file_id"],
753
+ limit: 1
754
+ }
755
+ });
756
+ const job = response.data.data?.[0];
757
+ if (job) {
758
+ return {
759
+ status: job.status,
760
+ progress: job.progress,
761
+ message: job.message,
762
+ audio_file_id: job.audio_file_id
763
+ };
764
+ }
765
+ return null;
766
+ } catch (error) {
767
+ console.error("[Voice Widget] Failed to poll voicing job:", error);
768
+ return null;
769
+ }
770
+ }
713
771
  return {
714
772
  fetchVoices,
715
773
  fetchFlowId,
@@ -720,7 +778,11 @@ function useVoicingApi(api) {
720
778
  getAudioUrl,
721
779
  resolveAudioFileUrl,
722
780
  fetchVoiceVariants,
723
- updateVoiceSampleFile
781
+ updateVoiceSampleFile,
782
+ deleteVoiceVariant,
783
+ createVoiceVariant,
784
+ linkAudioToLesson,
785
+ pollVoicingJob
724
786
  };
725
787
  }
726
788
 
@@ -733,49 +795,56 @@ const _hoisted_3 = { class: "widget__progress" };
733
795
  const _hoisted_4 = { class: "widget__progress-bar" };
734
796
  const _hoisted_5 = {
735
797
  key: 0,
798
+ class: "widget__progress-status"
799
+ };
800
+ const _hoisted_6 = {
801
+ key: 1,
736
802
  class: "widget__error"
737
803
  };
738
- const _hoisted_6 = { class: "widget__loading" };
739
- const _hoisted_7 = { class: "widget__error" };
740
- const _hoisted_8 = { class: "widget__header" };
741
- const _hoisted_9 = { class: "widget__header-row" };
742
- const _hoisted_10 = { class: "widget__header-text" };
743
- const _hoisted_11 = { class: "widget__title" };
744
- const _hoisted_12 = ["title"];
745
- const _hoisted_13 = { class: "widget__header-controls" };
746
- const _hoisted_14 = { class: "widget__url-input" };
747
- const _hoisted_15 = ["disabled"];
748
- const _hoisted_16 = { class: "widget__section" };
749
- const _hoisted_17 = { class: "widget__model-buttons" };
750
- const _hoisted_18 = ["onClick"];
751
- const _hoisted_19 = { class: "widget__section" };
752
- const _hoisted_20 = { class: "widget__voice-list" };
753
- const _hoisted_21 = { class: "widget__section" };
804
+ const _hoisted_7 = { class: "widget__loading" };
805
+ const _hoisted_8 = { class: "widget__error" };
806
+ const _hoisted_9 = { class: "widget__header" };
807
+ const _hoisted_10 = { class: "widget__header-row" };
808
+ const _hoisted_11 = { class: "widget__header-text" };
809
+ const _hoisted_12 = { class: "widget__title" };
810
+ const _hoisted_13 = ["title"];
811
+ const _hoisted_14 = { class: "widget__header-controls" };
812
+ const _hoisted_15 = { class: "widget__url-input" };
813
+ const _hoisted_16 = ["disabled"];
814
+ const _hoisted_17 = { class: "widget__section" };
815
+ const _hoisted_18 = { class: "widget__model-buttons" };
816
+ const _hoisted_19 = ["onClick"];
817
+ const _hoisted_20 = { class: "widget__section" };
818
+ const _hoisted_21 = { class: "widget__voice-list" };
754
819
  const _hoisted_22 = { class: "widget__section" };
755
- const _hoisted_23 = { class: "widget__section widget__section--toggle" };
756
- const _hoisted_24 = { class: "widget__toggle" };
757
- const _hoisted_25 = { class: "widget__footer" };
758
- const _hoisted_26 = { class: "widget__footer-right" };
759
- const _hoisted_27 = ["disabled"];
820
+ const _hoisted_23 = { class: "widget__section" };
821
+ const _hoisted_24 = { class: "widget__section widget__section--toggle" };
822
+ const _hoisted_25 = { class: "widget__toggle" };
823
+ const _hoisted_26 = { class: "widget__footer" };
824
+ const _hoisted_27 = { class: "widget__footer-right" };
760
825
  const _hoisted_28 = ["disabled"];
761
- const _hoisted_29 = { class: "widget__header" };
762
- const _hoisted_30 = { class: "widget__header-row" };
763
- const _hoisted_31 = { class: "widget__header-text" };
764
- const _hoisted_32 = { class: "widget__title" };
765
- const _hoisted_33 = { class: "widget__subtitle" };
766
- const _hoisted_34 = {
767
- key: 0,
768
- class: "widget__variant-nav"
769
- };
770
- const _hoisted_35 = ["disabled"];
771
- const _hoisted_36 = { class: "widget__variant-counter" };
772
- const _hoisted_37 = ["disabled"];
773
- const _hoisted_38 = { class: "widget__result" };
774
- const _hoisted_39 = { class: "widget__result-info" };
775
- const _hoisted_40 = {
826
+ const _hoisted_29 = ["disabled"];
827
+ const _hoisted_30 = { class: "widget__header" };
828
+ const _hoisted_31 = { class: "widget__header-row" };
829
+ const _hoisted_32 = { class: "widget__header-text" };
830
+ const _hoisted_33 = { class: "widget__title" };
831
+ const _hoisted_34 = { class: "widget__subtitle" };
832
+ const _hoisted_35 = { class: "widget__variants-list" };
833
+ const _hoisted_36 = ["onClick"];
834
+ const _hoisted_37 = { class: "widget__variant-radio" };
835
+ const _hoisted_38 = ["checked", "onChange"];
836
+ const _hoisted_39 = { class: "widget__variant-player" };
837
+ const _hoisted_40 = { class: "widget__variant-meta" };
838
+ const _hoisted_41 = { class: "widget__variant-voice" };
839
+ const _hoisted_42 = { class: "widget__variant-date" };
840
+ const _hoisted_43 = ["onClick"];
841
+ const _hoisted_44 = {
776
842
  key: 0,
777
- class: "widget__result-date"
843
+ class: "widget__selected-info"
778
844
  };
845
+ const _hoisted_45 = { class: "widget__footer" };
846
+ const _hoisted_46 = { class: "widget__footer-right" };
847
+ const _hoisted_47 = ["disabled"];
779
848
  var _sfc_main = /* @__PURE__ */ defineComponent({
780
849
  __name: "interface",
781
850
  props: {
@@ -804,7 +873,11 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
804
873
  getAudioUrl,
805
874
  resolveAudioFileUrl,
806
875
  fetchVoiceVariants,
807
- updateVoiceSampleFile
876
+ updateVoiceSampleFile,
877
+ deleteVoiceVariant,
878
+ createVoiceVariant,
879
+ linkAudioToLesson,
880
+ pollVoicingJob
808
881
  } = useVoicingApi(api);
809
882
  const currentMode = ref("selection");
810
883
  const loading = ref(true);
@@ -832,13 +905,17 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
832
905
  const hasExistingVoices = ref(false);
833
906
  const allVariants = ref([]);
834
907
  const currentVariantIndex = ref(0);
908
+ const currentJobId = ref(null);
909
+ const isPollingProgress = ref(false);
910
+ const progressStatus = ref("");
911
+ const selectedVariant = computed(() => allVariants.value[currentVariantIndex.value]);
835
912
  const currentVoices = computed(() => {
836
913
  return voices.value.filter((v) => v.provider === selectedModel.value);
837
914
  });
838
915
  const canGenerate = computed(() => {
839
916
  return selectedVoiceId.value !== null && selectedToneId.value !== null && selectedStyleId.value !== null;
840
917
  });
841
- const currentVariantDate = computed(() => {
918
+ computed(() => {
842
919
  const variant = allVariants.value[currentVariantIndex.value];
843
920
  if (!variant?.date_created) return null;
844
921
  return new Date(variant.date_created).toLocaleString();
@@ -906,15 +983,15 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
906
983
  if (!canGenerate.value) return;
907
984
  currentMode.value = "progress";
908
985
  errorMessage.value = null;
909
- processingMessage.value = "Generating voiceover...";
910
- progressPercent.value = 10;
986
+ processingMessage.value = "Starting generation...";
987
+ progressPercent.value = 5;
988
+ progressStatus.value = "";
911
989
  try {
912
990
  const tonePrompt = getSelectedTonePrompt();
913
991
  const stylePrompt = getSelectedStylePrompt();
914
992
  const combinedStyle = `${tonePrompt}. ${stylePrompt}`.trim();
915
993
  const selectedVoice = voices.value.find((v) => v.id === selectedVoiceId.value);
916
994
  const providerVoiceId = selectedVoice?.voice_id || selectedVoiceId.value;
917
- progressPercent.value = 30;
918
995
  const result = await generateFullVoiceover({
919
996
  provider: selectedModel.value,
920
997
  voiceId: providerVoiceId,
@@ -922,25 +999,41 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
922
999
  preprocessing: preprocessingEnabled.value,
923
1000
  flowId: flowId.value,
924
1001
  lessonId: props.primaryKey,
1002
+ collection: props.collection || "SM_Lessons",
925
1003
  toneId: selectedToneId.value || void 0,
926
1004
  styleId: selectedStyleId.value || void 0
927
1005
  });
928
- progressPercent.value = 90;
929
- if (result.audio_file_id) {
1006
+ const voiceConfig = {
1007
+ provider: selectedModel.value,
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) {
930
1017
  generatedAudioId.value = result.audio_file_id;
931
1018
  generatedAudioUrl.value = getAudioUrl(result.audio_file_id);
932
1019
  progressPercent.value = 100;
1020
+ await createVoiceVariant(
1021
+ props.primaryKey,
1022
+ result.audio_file_id,
1023
+ voiceConfig
1024
+ );
1025
+ await refreshVariants();
933
1026
  currentMode.value = "result";
934
1027
  } else if (result.status === "processing" && result.callback_data) {
935
1028
  processingMessage.value = "Processing in background...";
936
- await pollForCompletion(result.callback_data);
1029
+ await pollForCompletion(result.callback_data, voiceConfig);
937
1030
  }
938
1031
  } catch (error) {
939
1032
  errorMessage.value = error.message || "Failed to generate voiceover";
940
1033
  progressPercent.value = 0;
941
1034
  }
942
1035
  }
943
- async function pollForCompletion(callbackData) {
1036
+ async function pollForCompletion(callbackData, voiceConfig) {
944
1037
  const maxAttempts = 60;
945
1038
  const pollInterval = 5e3;
946
1039
  for (let i = 0; i < maxAttempts; i++) {
@@ -970,19 +1063,77 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
970
1063
  currentMode.value = "selection";
971
1064
  errorMessage.value = null;
972
1065
  progressPercent.value = 0;
1066
+ progressStatus.value = "";
1067
+ isPollingProgress.value = false;
973
1068
  }
974
- async function prevVariant() {
975
- if (currentVariantIndex.value > 0) {
976
- currentVariantIndex.value--;
977
- await loadVariantAtIndex(currentVariantIndex.value);
1069
+ function selectVariant(index) {
1070
+ currentVariantIndex.value = index;
1071
+ loadVariantAtIndex(index);
1072
+ }
1073
+ async function deleteVariant(variantId) {
1074
+ if (!confirm("Delete this voiceover?")) return;
1075
+ try {
1076
+ await deleteVoiceVariant(variantId);
1077
+ allVariants.value = allVariants.value.filter((v) => v.id !== variantId);
1078
+ if (currentVariantIndex.value >= allVariants.value.length) {
1079
+ currentVariantIndex.value = Math.max(0, allVariants.value.length - 1);
1080
+ }
1081
+ if (allVariants.value.length === 0) {
1082
+ hasExistingVoices.value = false;
1083
+ goBackToSelection();
1084
+ }
1085
+ } catch (error) {
1086
+ console.error("Failed to delete variant:", error);
978
1087
  }
979
1088
  }
980
- async function nextVariant() {
981
- if (currentVariantIndex.value < allVariants.value.length - 1) {
982
- currentVariantIndex.value++;
983
- await loadVariantAtIndex(currentVariantIndex.value);
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));
984
1118
  }
985
1119
  }
1120
+ async function refreshVariants() {
1121
+ const variants = await fetchVoiceVariants(props.primaryKey);
1122
+ allVariants.value = variants;
1123
+ for (const v of allVariants.value) {
1124
+ const isUuid = v.audio_file_id?.includes("-");
1125
+ v.audioUrl = isUuid ? getAudioUrl(v.audio_file_id) : await resolveAudioFileUrl(v.audio_file_id);
1126
+ }
1127
+ currentVariantIndex.value = 0;
1128
+ hasExistingVoices.value = variants.length > 0;
1129
+ }
1130
+ function formatVariantDate(dateStr) {
1131
+ if (!dateStr) return "";
1132
+ return new Date(dateStr).toLocaleString();
1133
+ }
1134
+ function getVoiceNameFromConfig(config) {
1135
+ return config?.voice_id || "Unknown";
1136
+ }
986
1137
  async function loadVariantAtIndex(index) {
987
1138
  const variant = allVariants.value[index];
988
1139
  if (!variant) return;
@@ -1015,25 +1166,41 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1015
1166
  function regenerateVoiceover() {
1016
1167
  generateVoiceover();
1017
1168
  }
1018
- function confirmVoiceover() {
1019
- const value = {
1020
- audio_file_id: generatedAudioId.value,
1021
- model: selectedModel.value,
1022
- voice_id: selectedVoiceId.value,
1023
- tone_id: selectedToneId.value,
1024
- style_id: selectedStyleId.value,
1025
- custom_tone: customTone.value,
1026
- custom_style: customStyle.value,
1027
- preprocessing: preprocessingEnabled.value,
1028
- confirmed_at: (/* @__PURE__ */ new Date()).toISOString()
1029
- };
1030
- emit("input", value);
1169
+ async function confirmVoiceover() {
1170
+ const variant = selectedVariant.value;
1171
+ if (!variant) return;
1172
+ try {
1173
+ await linkAudioToLesson(
1174
+ props.primaryKey,
1175
+ variant.audio_file_id,
1176
+ props.collection || "SM_Lessons"
1177
+ );
1178
+ const value = {
1179
+ audio_file_id: variant.audio_file_id,
1180
+ model: variant.voice_config?.provider || selectedModel.value,
1181
+ voice_id: variant.voice_config?.voice_id || selectedVoiceId.value,
1182
+ tone_id: variant.voice_config?.tone_id || selectedToneId.value,
1183
+ style_id: variant.voice_config?.style_id || selectedStyleId.value,
1184
+ custom_tone: customTone.value,
1185
+ custom_style: customStyle.value,
1186
+ preprocessing: variant.voice_config?.preprocessing ?? preprocessingEnabled.value,
1187
+ confirmed_at: (/* @__PURE__ */ new Date()).toISOString()
1188
+ };
1189
+ emit("input", value);
1190
+ goBackToSelection();
1191
+ } catch (error) {
1192
+ console.error("Failed to link audio:", error);
1193
+ }
1031
1194
  }
1032
1195
  async function viewGeneratedVoices() {
1033
1196
  try {
1034
1197
  const variants = await fetchVoiceVariants(props.primaryKey);
1035
1198
  if (variants.length > 0) {
1036
1199
  allVariants.value = variants;
1200
+ for (const v of allVariants.value) {
1201
+ const isUuid = v.audio_file_id?.includes("-");
1202
+ v.audioUrl = isUuid ? getAudioUrl(v.audio_file_id) : await resolveAudioFileUrl(v.audio_file_id);
1203
+ }
1037
1204
  currentVariantIndex.value = 0;
1038
1205
  await loadVariantAtIndex(0);
1039
1206
  currentMode.value = "result";
@@ -1042,27 +1209,6 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1042
1209
  console.error("Failed to fetch voice variants:", error);
1043
1210
  }
1044
1211
  }
1045
- function getModelName(modelId) {
1046
- const model = VOICE_MODELS.find((m) => m.id === modelId);
1047
- return model?.name || modelId;
1048
- }
1049
- function getVoiceName(voiceId) {
1050
- if (!voiceId) return "Not selected";
1051
- const voice = voices.value.find((v) => v.id === voiceId);
1052
- return voice?.name || voiceId;
1053
- }
1054
- function getToneName(toneId) {
1055
- if (!toneId) return "Not selected";
1056
- if (toneId === "other") return customTone.value || "Custom";
1057
- const tone = tones.value.find((t) => t.id === toneId);
1058
- return tone?.name || toneId;
1059
- }
1060
- function getStyleName(styleId) {
1061
- if (!styleId) return "Not selected";
1062
- if (styleId === "other") return customStyle.value || "Custom";
1063
- const style = styles.value.find((s) => s.id === styleId);
1064
- return style?.name || styleId;
1065
- }
1066
1212
  async function initialize() {
1067
1213
  loading.value = true;
1068
1214
  initError.value = null;
@@ -1172,7 +1318,14 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1172
1318
  /* STYLE */
1173
1319
  )
1174
1320
  ]),
1175
- errorMessage.value ? (openBlock(), createElementBlock("div", _hoisted_5, [
1321
+ progressStatus.value && !errorMessage.value ? (openBlock(), createElementBlock(
1322
+ "p",
1323
+ _hoisted_5,
1324
+ " Status: " + toDisplayString(progressStatus.value),
1325
+ 1
1326
+ /* TEXT */
1327
+ )) : createCommentVNode("v-if", true),
1328
+ errorMessage.value ? (openBlock(), createElementBlock("div", _hoisted_6, [
1176
1329
  createVNode(_component_v_icon, { name: "error" }),
1177
1330
  createElementVNode(
1178
1331
  "span",
@@ -1197,7 +1350,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1197
1350
  { key: 1 },
1198
1351
  [
1199
1352
  createCommentVNode(" Loading State "),
1200
- createElementVNode("div", _hoisted_6, [
1353
+ createElementVNode("div", _hoisted_7, [
1201
1354
  createVNode(_component_v_icon, {
1202
1355
  name: "refresh",
1203
1356
  class: "spinning"
@@ -1218,7 +1371,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1218
1371
  { key: 2 },
1219
1372
  [
1220
1373
  createCommentVNode(" Error State "),
1221
- createElementVNode("div", _hoisted_7, [
1374
+ createElementVNode("div", _hoisted_8, [
1222
1375
  createVNode(_component_v_icon, { name: "error" }),
1223
1376
  createElementVNode(
1224
1377
  "span",
@@ -1240,10 +1393,10 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1240
1393
  { key: 3 },
1241
1394
  [
1242
1395
  createCommentVNode(" Page 1: Selection Mode "),
1243
- createElementVNode("div", _hoisted_8, [
1244
- createElementVNode("div", _hoisted_9, [
1245
- createElementVNode("div", _hoisted_10, [
1246
- createElementVNode("h2", _hoisted_11, [
1396
+ createElementVNode("div", _hoisted_9, [
1397
+ createElementVNode("div", _hoisted_10, [
1398
+ createElementVNode("div", _hoisted_11, [
1399
+ createElementVNode("h2", _hoisted_12, [
1247
1400
  createVNode(_component_v_icon, { name: "mic" }),
1248
1401
  _cache[4] || (_cache[4] = createTextVNode(
1249
1402
  " Voice Generation ",
@@ -1260,7 +1413,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1260
1413
  name: headerExpanded.value ? "expand_less" : "expand_more",
1261
1414
  small: ""
1262
1415
  }, null, 8, ["name"])
1263
- ], 8, _hoisted_12)
1416
+ ], 8, _hoisted_13)
1264
1417
  ]),
1265
1418
  _cache[5] || (_cache[5] = createElementVNode(
1266
1419
  "p",
@@ -1272,9 +1425,9 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1272
1425
  ]),
1273
1426
  withDirectives(createElementVNode(
1274
1427
  "div",
1275
- _hoisted_13,
1428
+ _hoisted_14,
1276
1429
  [
1277
- createElementVNode("div", _hoisted_14, [
1430
+ createElementVNode("div", _hoisted_15, [
1278
1431
  _cache[6] || (_cache[6] = createElementVNode(
1279
1432
  "label",
1280
1433
  { class: "widget__url-label" },
@@ -1287,7 +1440,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1287
1440
  class: "widget__url-field",
1288
1441
  "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => flowId.value = $event),
1289
1442
  disabled: savingUrl.value
1290
- }, null, 8, _hoisted_15), [
1443
+ }, null, 8, _hoisted_16), [
1291
1444
  [vModelText, flowId.value]
1292
1445
  ])
1293
1446
  ])
@@ -1300,7 +1453,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1300
1453
  ])
1301
1454
  ]),
1302
1455
  createCommentVNode(" Model Selection "),
1303
- createElementVNode("div", _hoisted_16, [
1456
+ createElementVNode("div", _hoisted_17, [
1304
1457
  _cache[7] || (_cache[7] = createElementVNode(
1305
1458
  "label",
1306
1459
  { class: "widget__section-label" },
@@ -1308,7 +1461,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1308
1461
  -1
1309
1462
  /* CACHED */
1310
1463
  )),
1311
- createElementVNode("div", _hoisted_17, [
1464
+ createElementVNode("div", _hoisted_18, [
1312
1465
  (openBlock(true), createElementBlock(
1313
1466
  Fragment,
1314
1467
  null,
@@ -1317,7 +1470,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1317
1470
  key: model.id,
1318
1471
  class: normalizeClass(["widget__model-btn", { "widget__model-btn--active": selectedModel.value === model.id }]),
1319
1472
  onClick: ($event) => selectModel(model.id)
1320
- }, toDisplayString(model.name), 11, _hoisted_18);
1473
+ }, toDisplayString(model.name), 11, _hoisted_19);
1321
1474
  }),
1322
1475
  128
1323
1476
  /* KEYED_FRAGMENT */
@@ -1325,7 +1478,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1325
1478
  ])
1326
1479
  ]),
1327
1480
  createCommentVNode(" Voice Selection "),
1328
- createElementVNode("div", _hoisted_19, [
1481
+ createElementVNode("div", _hoisted_20, [
1329
1482
  _cache[8] || (_cache[8] = createElementVNode(
1330
1483
  "label",
1331
1484
  { class: "widget__section-label" },
@@ -1333,7 +1486,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1333
1486
  -1
1334
1487
  /* CACHED */
1335
1488
  )),
1336
- createElementVNode("div", _hoisted_20, [
1489
+ createElementVNode("div", _hoisted_21, [
1337
1490
  (openBlock(true), createElementBlock(
1338
1491
  Fragment,
1339
1492
  null,
@@ -1355,7 +1508,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1355
1508
  ])
1356
1509
  ]),
1357
1510
  createCommentVNode(" Tone Selection "),
1358
- createElementVNode("div", _hoisted_21, [
1511
+ createElementVNode("div", _hoisted_22, [
1359
1512
  _cache[9] || (_cache[9] = createElementVNode(
1360
1513
  "label",
1361
1514
  { class: "widget__section-label" },
@@ -1373,7 +1526,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1373
1526
  }, null, 8, ["items", "selected-id", "custom-value"])
1374
1527
  ]),
1375
1528
  createCommentVNode(" Style Selection "),
1376
- createElementVNode("div", _hoisted_22, [
1529
+ createElementVNode("div", _hoisted_23, [
1377
1530
  _cache[10] || (_cache[10] = createElementVNode(
1378
1531
  "label",
1379
1532
  { class: "widget__section-label" },
@@ -1391,8 +1544,8 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1391
1544
  }, null, 8, ["items", "selected-id", "custom-value"])
1392
1545
  ]),
1393
1546
  createCommentVNode(" Preprocessing Toggle "),
1394
- createElementVNode("div", _hoisted_23, [
1395
- createElementVNode("label", _hoisted_24, [
1547
+ createElementVNode("div", _hoisted_24, [
1548
+ createElementVNode("label", _hoisted_25, [
1396
1549
  withDirectives(createElementVNode(
1397
1550
  "input",
1398
1551
  {
@@ -1422,7 +1575,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1422
1575
  ))
1423
1576
  ]),
1424
1577
  createCommentVNode(" Footer Actions "),
1425
- createElementVNode("div", _hoisted_25, [
1578
+ createElementVNode("div", _hoisted_26, [
1426
1579
  _cache[13] || (_cache[13] = createElementVNode(
1427
1580
  "div",
1428
1581
  { class: "widget__footer-left" },
@@ -1430,17 +1583,17 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1430
1583
  -1
1431
1584
  /* CACHED */
1432
1585
  )),
1433
- createElementVNode("div", _hoisted_26, [
1586
+ createElementVNode("div", _hoisted_27, [
1434
1587
  createElementVNode("button", {
1435
1588
  class: "widget__btn widget__btn--secondary",
1436
1589
  onClick: viewGeneratedVoices,
1437
1590
  disabled: !hasExistingVoices.value
1438
- }, " View Generated Voices ", 8, _hoisted_27),
1591
+ }, " View Generated Voices ", 8, _hoisted_28),
1439
1592
  createElementVNode("button", {
1440
1593
  class: "widget__btn widget__btn--primary",
1441
1594
  onClick: generateVoiceover,
1442
1595
  disabled: !canGenerate.value
1443
- }, " Generate Voiceover ", 8, _hoisted_28)
1596
+ }, " Generate Voiceover ", 8, _hoisted_29)
1444
1597
  ])
1445
1598
  ])
1446
1599
  ],
@@ -1450,163 +1603,147 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1450
1603
  Fragment,
1451
1604
  { key: 4 },
1452
1605
  [
1453
- createCommentVNode(" Page 3: Result Mode "),
1454
- createElementVNode("div", _hoisted_29, [
1455
- createElementVNode("div", _hoisted_30, [
1456
- createElementVNode("div", _hoisted_31, [
1457
- createElementVNode("h2", _hoisted_32, [
1606
+ createCommentVNode(" Page 3: Result Mode - List View "),
1607
+ createElementVNode("div", _hoisted_30, [
1608
+ createElementVNode("div", _hoisted_31, [
1609
+ createElementVNode("div", _hoisted_32, [
1610
+ createElementVNode("h2", _hoisted_33, [
1458
1611
  createVNode(_component_v_icon, { name: "check_circle" }),
1459
1612
  _cache[14] || (_cache[14] = createTextVNode(
1460
- " Voiceover Generated ",
1613
+ " Generated Voiceovers ",
1461
1614
  -1
1462
1615
  /* CACHED */
1463
1616
  ))
1464
1617
  ]),
1465
- createElementVNode("p", _hoisted_33, [
1466
- allVariants.value.length > 1 ? (openBlock(), createElementBlock(
1467
- Fragment,
1468
- { key: 0 },
1469
- [
1470
- createTextVNode(
1471
- " Viewing " + toDisplayString(currentVariantIndex.value + 1) + " of " + toDisplayString(allVariants.value.length) + " generated voiceovers ",
1472
- 1
1473
- /* TEXT */
1474
- )
1475
- ],
1476
- 64
1477
- /* STABLE_FRAGMENT */
1478
- )) : (openBlock(), createElementBlock(
1479
- Fragment,
1480
- { key: 1 },
1481
- [
1482
- createTextVNode(" Listen to your generated voiceover and confirm or regenerate ")
1483
- ],
1484
- 64
1485
- /* STABLE_FRAGMENT */
1486
- ))
1487
- ])
1488
- ])
1489
- ])
1490
- ]),
1491
- createCommentVNode(" Navigation for multiple variants "),
1492
- allVariants.value.length > 1 ? (openBlock(), createElementBlock("div", _hoisted_34, [
1493
- createElementVNode("button", {
1494
- class: "widget__btn widget__btn--icon",
1495
- onClick: prevVariant,
1496
- disabled: currentVariantIndex.value === 0
1497
- }, [
1498
- createVNode(_component_v_icon, { name: "chevron_left" })
1499
- ], 8, _hoisted_35),
1500
- createElementVNode(
1501
- "span",
1502
- _hoisted_36,
1503
- toDisplayString(currentVariantIndex.value + 1) + " / " + toDisplayString(allVariants.value.length),
1504
- 1
1505
- /* TEXT */
1506
- ),
1507
- createElementVNode("button", {
1508
- class: "widget__btn widget__btn--icon",
1509
- onClick: nextVariant,
1510
- disabled: currentVariantIndex.value === allVariants.value.length - 1
1511
- }, [
1512
- createVNode(_component_v_icon, { name: "chevron_right" })
1513
- ], 8, _hoisted_37)
1514
- ])) : createCommentVNode("v-if", true),
1515
- createElementVNode("div", _hoisted_38, [
1516
- createVNode(AudioPlayer, {
1517
- src: generatedAudioUrl.value,
1518
- loading: false,
1519
- size: "large"
1520
- }, null, 8, ["src"]),
1521
- createElementVNode("div", _hoisted_39, [
1522
- createElementVNode("p", null, [
1523
- _cache[15] || (_cache[15] = createElementVNode(
1524
- "strong",
1525
- null,
1526
- "Model:",
1527
- -1
1528
- /* CACHED */
1529
- )),
1530
- createTextVNode(
1531
- " " + toDisplayString(getModelName(selectedModel.value)),
1532
- 1
1533
- /* TEXT */
1534
- )
1535
- ]),
1536
- createElementVNode("p", null, [
1537
- _cache[16] || (_cache[16] = createElementVNode(
1538
- "strong",
1539
- null,
1540
- "Voice:",
1541
- -1
1542
- /* CACHED */
1543
- )),
1544
- createTextVNode(
1545
- " " + toDisplayString(getVoiceName(selectedVoiceId.value)),
1546
- 1
1547
- /* TEXT */
1548
- )
1549
- ]),
1550
- createElementVNode("p", null, [
1551
- _cache[17] || (_cache[17] = createElementVNode(
1552
- "strong",
1553
- null,
1554
- "Tone:",
1555
- -1
1556
- /* CACHED */
1557
- )),
1558
- createTextVNode(
1559
- " " + toDisplayString(getToneName(selectedToneId.value)),
1560
- 1
1561
- /* TEXT */
1562
- )
1563
- ]),
1564
- createElementVNode("p", null, [
1565
- _cache[18] || (_cache[18] = createElementVNode(
1566
- "strong",
1567
- null,
1568
- "Style:",
1569
- -1
1570
- /* CACHED */
1571
- )),
1572
- createTextVNode(
1573
- " " + toDisplayString(getStyleName(selectedStyleId.value)),
1574
- 1
1575
- /* TEXT */
1576
- )
1577
- ]),
1578
- currentVariantDate.value ? (openBlock(), createElementBlock("p", _hoisted_40, [
1579
- _cache[19] || (_cache[19] = createElementVNode(
1580
- "strong",
1581
- null,
1582
- "Generated:",
1583
- -1
1584
- /* CACHED */
1585
- )),
1586
- createTextVNode(
1587
- " " + toDisplayString(currentVariantDate.value),
1618
+ createElementVNode(
1619
+ "p",
1620
+ _hoisted_34,
1621
+ toDisplayString(allVariants.value.length) + " voiceover(s) available. Select one to preview and confirm. ",
1588
1622
  1
1589
1623
  /* TEXT */
1590
1624
  )
1591
- ])) : createCommentVNode("v-if", true)
1625
+ ])
1592
1626
  ])
1593
1627
  ]),
1594
- createElementVNode("div", { class: "widget__footer" }, [
1628
+ createCommentVNode(" Variants List "),
1629
+ createElementVNode("div", _hoisted_35, [
1630
+ (openBlock(true), createElementBlock(
1631
+ Fragment,
1632
+ null,
1633
+ renderList(allVariants.value, (variant, index) => {
1634
+ return openBlock(), createElementBlock("div", {
1635
+ key: variant.id,
1636
+ class: normalizeClass(["widget__variant-item", { "widget__variant-item--selected": currentVariantIndex.value === index }]),
1637
+ onClick: ($event) => selectVariant(index)
1638
+ }, [
1639
+ createElementVNode("div", _hoisted_37, [
1640
+ createElementVNode("input", {
1641
+ type: "radio",
1642
+ checked: currentVariantIndex.value === index,
1643
+ onChange: ($event) => selectVariant(index)
1644
+ }, null, 40, _hoisted_38)
1645
+ ]),
1646
+ createElementVNode("div", _hoisted_39, [
1647
+ createVNode(AudioPlayer, {
1648
+ src: variant.audioUrl,
1649
+ loading: false,
1650
+ size: "small"
1651
+ }, null, 8, ["src"])
1652
+ ]),
1653
+ createElementVNode("div", _hoisted_40, [
1654
+ createElementVNode(
1655
+ "span",
1656
+ _hoisted_41,
1657
+ toDisplayString(variant.voice_config?.provider) + " - " + toDisplayString(getVoiceNameFromConfig(variant.voice_config)),
1658
+ 1
1659
+ /* TEXT */
1660
+ ),
1661
+ createElementVNode(
1662
+ "span",
1663
+ _hoisted_42,
1664
+ toDisplayString(formatVariantDate(variant.date_created)),
1665
+ 1
1666
+ /* TEXT */
1667
+ )
1668
+ ]),
1669
+ createElementVNode("button", {
1670
+ class: "widget__btn widget__btn--icon widget__btn--danger",
1671
+ onClick: withModifiers(($event) => deleteVariant(variant.id), ["stop"]),
1672
+ title: "Delete this voiceover"
1673
+ }, [
1674
+ createVNode(_component_v_icon, {
1675
+ name: "delete",
1676
+ small: ""
1677
+ })
1678
+ ], 8, _hoisted_43)
1679
+ ], 10, _hoisted_36);
1680
+ }),
1681
+ 128
1682
+ /* KEYED_FRAGMENT */
1683
+ ))
1684
+ ]),
1685
+ createCommentVNode(" Selected Variant Details "),
1686
+ selectedVariant.value ? (openBlock(), createElementBlock("div", _hoisted_44, [
1687
+ createElementVNode("p", null, [
1688
+ _cache[15] || (_cache[15] = createElementVNode(
1689
+ "strong",
1690
+ null,
1691
+ "Model:",
1692
+ -1
1693
+ /* CACHED */
1694
+ )),
1695
+ createTextVNode(
1696
+ " " + toDisplayString(selectedVariant.value.voice_config?.provider),
1697
+ 1
1698
+ /* TEXT */
1699
+ )
1700
+ ]),
1701
+ createElementVNode("p", null, [
1702
+ _cache[16] || (_cache[16] = createElementVNode(
1703
+ "strong",
1704
+ null,
1705
+ "Voice:",
1706
+ -1
1707
+ /* CACHED */
1708
+ )),
1709
+ createTextVNode(
1710
+ " " + toDisplayString(getVoiceNameFromConfig(selectedVariant.value.voice_config)),
1711
+ 1
1712
+ /* TEXT */
1713
+ )
1714
+ ]),
1715
+ createElementVNode("p", null, [
1716
+ _cache[17] || (_cache[17] = createElementVNode(
1717
+ "strong",
1718
+ null,
1719
+ "Style:",
1720
+ -1
1721
+ /* CACHED */
1722
+ )),
1723
+ createTextVNode(
1724
+ " " + toDisplayString(selectedVariant.value.voice_config?.style),
1725
+ 1
1726
+ /* TEXT */
1727
+ )
1728
+ ])
1729
+ ])) : createCommentVNode("v-if", true),
1730
+ createElementVNode("div", _hoisted_45, [
1595
1731
  createElementVNode("div", { class: "widget__footer-left" }, [
1596
1732
  createElementVNode("button", {
1597
1733
  class: "widget__btn widget__btn--secondary",
1598
1734
  onClick: goBackToSelection
1599
- }, " Return ")
1735
+ }, " Back ")
1600
1736
  ]),
1601
- createElementVNode("div", { class: "widget__footer-right" }, [
1737
+ createElementVNode("div", _hoisted_46, [
1602
1738
  createElementVNode("button", {
1603
1739
  class: "widget__btn widget__btn--secondary",
1604
1740
  onClick: regenerateVoiceover
1605
1741
  }, " Regenerate "),
1606
1742
  createElementVNode("button", {
1607
1743
  class: "widget__btn widget__btn--primary",
1608
- onClick: confirmVoiceover
1609
- }, " Confirm ")
1744
+ onClick: confirmVoiceover,
1745
+ disabled: !selectedVariant.value
1746
+ }, " Confirm ", 8, _hoisted_47)
1610
1747
  ])
1611
1748
  ])
1612
1749
  ],
@@ -1618,10 +1755,10 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1618
1755
  }
1619
1756
  });
1620
1757
 
1621
- var css = "\n.voice-widget[data-v-f98cb13e] {\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-f98cb13e] {\n margin-bottom: 20px;\n}\n.widget__header-row[data-v-f98cb13e] {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n gap: 16px;\n}\n.widget__header-text[data-v-f98cb13e] {\n flex: 1;\n}\n.widget__title[data-v-f98cb13e] {\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-f98cb13e] {\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-f98cb13e]:hover {\n background: var(--theme--background-accent);\n}\n.widget__subtitle[data-v-f98cb13e] {\n margin: 0;\n font-size: 14px;\n color: var(--theme--foreground-subdued);\n}\n.widget__header-controls[data-v-f98cb13e] {\n display: flex;\n gap: 16px;\n align-items: center;\n}\n.widget__url-input[data-v-f98cb13e] {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.widget__url-label[data-v-f98cb13e] {\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n white-space: nowrap;\n}\n.widget__url-field[data-v-f98cb13e] {\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-f98cb13e] {\n margin-bottom: 20px;\n}\n.widget__section-label[data-v-f98cb13e] {\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-f98cb13e] {\n display: flex;\n gap: 8px;\n}\n.widget__model-btn[data-v-f98cb13e] {\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-f98cb13e]:hover {\n border-color: var(--theme--primary);\n}\n.widget__model-btn--active[data-v-f98cb13e] {\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-f98cb13e] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.widget__section--toggle[data-v-f98cb13e] {\n padding: 12px;\n background: var(--theme--background-subdued);\n border-radius: var(--theme--border-radius);\n}\n.widget__toggle[data-v-f98cb13e] {\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n}\n.widget__toggle input[data-v-f98cb13e] {\n width: 16px;\n height: 16px;\n cursor: pointer;\n}\n.widget__toggle-label[data-v-f98cb13e] {\n font-size: 14px;\n font-weight: 500;\n color: var(--theme--foreground);\n}\n.widget__toggle-note[data-v-f98cb13e] {\n margin: 4px 0 0 24px;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n}\n.widget__footer[data-v-f98cb13e] {\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-f98cb13e],\n.widget__footer-right[data-v-f98cb13e] {\n display: flex;\n gap: 8px;\n}\n.widget__variant-nav[data-v-f98cb13e] {\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-f98cb13e] {\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-f98cb13e] {\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-f98cb13e] {\n margin-top: 8px;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n}\n.widget__btn[data-v-f98cb13e] {\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-f98cb13e]:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.widget__btn--primary[data-v-f98cb13e] {\n background: var(--theme--primary);\n color: var(--theme--primary-foreground, #fff);\n}\n.widget__btn--primary[data-v-f98cb13e]:hover:not(:disabled) {\n background: var(--theme--primary-accent);\n}\n.widget__btn--secondary[data-v-f98cb13e] {\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-f98cb13e]:hover:not(:disabled) {\n background: var(--theme--background-normal);\n}\n\n/* Processing State */\n.widget__processing[data-v-f98cb13e] {\n padding: 40px 20px;\n text-align: center;\n}\n.widget__progress[data-v-f98cb13e] {\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-f98cb13e] {\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-f98cb13e] {\n height: 100%;\n background: var(--theme--primary);\n transition: width 0.3s ease;\n}\n\n/* Error State */\n.widget__error[data-v-f98cb13e] {\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-f98cb13e] {\n display: flex;\n gap: 8px;\n margin-top: 8px;\n}\n.widget__retry[data-v-f98cb13e] {\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-f98cb13e] {\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-f98cb13e] {\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-f98cb13e] {\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-f98cb13e] {\n margin: 4px 0;\n font-size: 14px;\n color: var(--theme--foreground-subdued);\n}\n.widget__result-info strong[data-v-f98cb13e] {\n color: var(--theme--foreground);\n}\n\n/* Animations */\n.spinning[data-v-f98cb13e] {\n animation: spin-f98cb13e 1s linear infinite;\n}\n@keyframes spin-f98cb13e {\nfrom { transform: rotate(0deg);\n}\nto { transform: rotate(360deg);\n}\n}\n";
1758
+ var css = "\n.voice-widget[data-v-10fc7a16] {\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-10fc7a16] {\n margin-bottom: 20px;\n}\n.widget__header-row[data-v-10fc7a16] {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n gap: 16px;\n}\n.widget__header-text[data-v-10fc7a16] {\n flex: 1;\n}\n.widget__title[data-v-10fc7a16] {\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-10fc7a16] {\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-10fc7a16]:hover {\n background: var(--theme--background-accent);\n}\n.widget__subtitle[data-v-10fc7a16] {\n margin: 0;\n font-size: 14px;\n color: var(--theme--foreground-subdued);\n}\n.widget__header-controls[data-v-10fc7a16] {\n display: flex;\n gap: 16px;\n align-items: center;\n}\n.widget__url-input[data-v-10fc7a16] {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.widget__url-label[data-v-10fc7a16] {\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n white-space: nowrap;\n}\n.widget__url-field[data-v-10fc7a16] {\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-10fc7a16] {\n margin-bottom: 20px;\n}\n.widget__section-label[data-v-10fc7a16] {\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-10fc7a16] {\n display: flex;\n gap: 8px;\n}\n.widget__model-btn[data-v-10fc7a16] {\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-10fc7a16]:hover {\n border-color: var(--theme--primary);\n}\n.widget__model-btn--active[data-v-10fc7a16] {\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-10fc7a16] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.widget__section--toggle[data-v-10fc7a16] {\n padding: 12px;\n background: var(--theme--background-subdued);\n border-radius: var(--theme--border-radius);\n}\n.widget__toggle[data-v-10fc7a16] {\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n}\n.widget__toggle input[data-v-10fc7a16] {\n width: 16px;\n height: 16px;\n cursor: pointer;\n}\n.widget__toggle-label[data-v-10fc7a16] {\n font-size: 14px;\n font-weight: 500;\n color: var(--theme--foreground);\n}\n.widget__toggle-note[data-v-10fc7a16] {\n margin: 4px 0 0 24px;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n}\n.widget__footer[data-v-10fc7a16] {\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-10fc7a16],\n.widget__footer-right[data-v-10fc7a16] {\n display: flex;\n gap: 8px;\n}\n.widget__variant-nav[data-v-10fc7a16] {\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-10fc7a16] {\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-10fc7a16] {\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-10fc7a16] {\n margin-top: 8px;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n}\n.widget__btn[data-v-10fc7a16] {\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-10fc7a16]:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.widget__btn--primary[data-v-10fc7a16] {\n background: var(--theme--primary);\n color: var(--theme--primary-foreground, #fff);\n}\n.widget__btn--primary[data-v-10fc7a16]:hover:not(:disabled) {\n background: var(--theme--primary-accent);\n}\n.widget__btn--secondary[data-v-10fc7a16] {\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-10fc7a16]:hover:not(:disabled) {\n background: var(--theme--background-normal);\n}\n\n/* Processing State */\n.widget__processing[data-v-10fc7a16] {\n padding: 40px 20px;\n text-align: center;\n}\n.widget__progress[data-v-10fc7a16] {\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-10fc7a16] {\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-10fc7a16] {\n height: 100%;\n background: var(--theme--primary);\n transition: width 0.3s ease;\n}\n\n/* Error State */\n.widget__error[data-v-10fc7a16] {\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-10fc7a16] {\n display: flex;\n gap: 8px;\n margin-top: 8px;\n}\n.widget__retry[data-v-10fc7a16] {\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-10fc7a16] {\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-10fc7a16] {\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-10fc7a16] {\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-10fc7a16] {\n margin: 4px 0;\n font-size: 14px;\n color: var(--theme--foreground-subdued);\n}\n.widget__result-info strong[data-v-10fc7a16] {\n color: var(--theme--foreground);\n}\n\n/* Variants List View */\n.widget__variants-list[data-v-10fc7a16] {\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-10fc7a16] {\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-10fc7a16]:hover {\n border-color: var(--theme--primary);\n}\n.widget__variant-item--selected[data-v-10fc7a16] {\n border-color: var(--theme--primary);\n background: var(--theme--primary-background);\n}\n.widget__variant-radio[data-v-10fc7a16] {\n flex-shrink: 0;\n}\n.widget__variant-radio input[data-v-10fc7a16] {\n width: 16px;\n height: 16px;\n cursor: pointer;\n}\n.widget__variant-player[data-v-10fc7a16] {\n flex: 1;\n min-width: 200px;\n}\n.widget__variant-meta[data-v-10fc7a16] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 120px;\n}\n.widget__variant-voice[data-v-10fc7a16] {\n font-size: 13px;\n font-weight: 500;\n color: var(--theme--foreground);\n}\n.widget__variant-date[data-v-10fc7a16] {\n font-size: 11px;\n color: var(--theme--foreground-subdued);\n}\n.widget__btn--danger[data-v-10fc7a16] {\n color: var(--theme--danger);\n background: transparent;\n border: none;\n}\n.widget__btn--danger[data-v-10fc7a16]:hover {\n background: var(--theme--danger-background);\n}\n.widget__selected-info[data-v-10fc7a16] {\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-10fc7a16] {\n margin: 4px 0;\n font-size: 14px;\n color: var(--theme--foreground-subdued);\n}\n.widget__selected-info strong[data-v-10fc7a16] {\n color: var(--theme--foreground);\n}\n.widget__progress-status[data-v-10fc7a16] {\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-10fc7a16] {\n animation: spin-10fc7a16 1s linear infinite;\n}\n@keyframes spin-10fc7a16 {\nfrom { transform: rotate(0deg);\n}\nto { transform: rotate(360deg);\n}\n}\n";
1622
1759
  n(css,{});
1623
1760
 
1624
- var InterfaceComponent = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-f98cb13e"], ["__file", "interface.vue"]]);
1761
+ var InterfaceComponent = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-10fc7a16"], ["__file", "interface.vue"]]);
1625
1762
 
1626
1763
  var index = defineInterface({
1627
1764
  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.0",
5
+ "version": "1.0.2",
6
6
  "license": "MIT",
7
7
  "readme": "README.md",
8
8
  "repository": {