@stellartech/voice-widget-directus 1.0.3 → 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 +53 -22
- 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
|
@@ -530,6 +530,9 @@ function useVoicingApi(api) {
|
|
|
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,
|
|
@@ -538,7 +541,7 @@ function useVoicingApi(api) {
|
|
|
538
541
|
audio_files_collection: "AudioFiles",
|
|
539
542
|
// Include lesson context for callback tracking
|
|
540
543
|
lesson_id: lessonId || null,
|
|
541
|
-
collection
|
|
544
|
+
collection,
|
|
542
545
|
voice_config: {
|
|
543
546
|
provider,
|
|
544
547
|
voice_id: voiceId,
|
|
@@ -588,7 +591,10 @@ function useVoicingApi(api) {
|
|
|
588
591
|
if (!effectiveFlowId) {
|
|
589
592
|
throw new Error("Voice flow ID is not configured. Set it in Widget Config or in the Flow ID field.");
|
|
590
593
|
}
|
|
591
|
-
|
|
594
|
+
if (!request.collection) {
|
|
595
|
+
throw new Error("collection is required but was not provided");
|
|
596
|
+
}
|
|
597
|
+
const collection = request.collection;
|
|
592
598
|
let texts = [];
|
|
593
599
|
let lessonTitle = `Lesson ${request.lessonId}`;
|
|
594
600
|
try {
|
|
@@ -1037,12 +1043,15 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1037
1043
|
voices.value[voiceIndex].example_status = "processing";
|
|
1038
1044
|
}
|
|
1039
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
|
+
}
|
|
1040
1049
|
generateVoiceSample(
|
|
1041
1050
|
voice.voice,
|
|
1042
1051
|
selectedModel.value,
|
|
1043
1052
|
effectiveFlowId,
|
|
1044
1053
|
props.primaryKey,
|
|
1045
|
-
props.collection
|
|
1054
|
+
props.collection
|
|
1046
1055
|
).then(() => {
|
|
1047
1056
|
console.log("[Voice Widget] Sample generation request sent for", voice.voice);
|
|
1048
1057
|
}).catch((error) => {
|
|
@@ -1183,6 +1192,9 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1183
1192
|
};
|
|
1184
1193
|
await createPendingVoiceVariant(props.primaryKey, voiceConfig);
|
|
1185
1194
|
startVoiceoverPolling();
|
|
1195
|
+
if (!props.collection) {
|
|
1196
|
+
throw new Error("collection is required but was not provided");
|
|
1197
|
+
}
|
|
1186
1198
|
const result = await generateFullVoiceover({
|
|
1187
1199
|
provider: selectedModel.value,
|
|
1188
1200
|
voiceId: providerVoiceId,
|
|
@@ -1190,7 +1202,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1190
1202
|
preprocessing: preprocessingEnabled.value,
|
|
1191
1203
|
flowId: flowId.value,
|
|
1192
1204
|
lessonId: props.primaryKey,
|
|
1193
|
-
collection: props.collection
|
|
1205
|
+
collection: props.collection,
|
|
1194
1206
|
toneId: selectedToneId.value || void 0,
|
|
1195
1207
|
styleId: selectedStyleId.value || void 0
|
|
1196
1208
|
});
|
|
@@ -1272,11 +1284,12 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1272
1284
|
generatedAudioUrl.value = isUuid ? getAudioUrl(audioFileId) : await resolveAudioFileUrl(audioFileId);
|
|
1273
1285
|
const config = variant.voice_config;
|
|
1274
1286
|
if (config) {
|
|
1287
|
+
isRestoringFromVariant.value = true;
|
|
1275
1288
|
if (config.provider) {
|
|
1276
1289
|
selectedModel.value = config.provider;
|
|
1277
1290
|
}
|
|
1278
1291
|
if (config.voice_id) {
|
|
1279
|
-
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);
|
|
1280
1293
|
if (matchingVoice) {
|
|
1281
1294
|
selectedVoiceId.value = matchingVoice.id;
|
|
1282
1295
|
}
|
|
@@ -1290,19 +1303,27 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1290
1303
|
if (typeof config.preprocessing === "boolean") {
|
|
1291
1304
|
preprocessingEnabled.value = config.preprocessing;
|
|
1292
1305
|
}
|
|
1306
|
+
isRestoringFromVariant.value = false;
|
|
1293
1307
|
}
|
|
1294
1308
|
}
|
|
1295
|
-
function regenerateVoiceover() {
|
|
1309
|
+
async function regenerateVoiceover() {
|
|
1310
|
+
const variant = selectedVariant.value;
|
|
1311
|
+
if (variant) {
|
|
1312
|
+
await loadVariantAtIndex(currentVariantIndex.value);
|
|
1313
|
+
}
|
|
1296
1314
|
generateVoiceover();
|
|
1297
1315
|
}
|
|
1298
1316
|
async function confirmVoiceover() {
|
|
1299
1317
|
const variant = selectedVariant.value;
|
|
1300
1318
|
if (!variant) return;
|
|
1301
1319
|
try {
|
|
1320
|
+
if (!props.collection) {
|
|
1321
|
+
throw new Error("collection is required but was not provided");
|
|
1322
|
+
}
|
|
1302
1323
|
await linkAudioToLesson(
|
|
1303
1324
|
props.primaryKey,
|
|
1304
1325
|
variant.audio_file_id,
|
|
1305
|
-
props.collection
|
|
1326
|
+
props.collection
|
|
1306
1327
|
);
|
|
1307
1328
|
const value = {
|
|
1308
1329
|
audio_file_id: variant.audio_file_id,
|
|
@@ -1392,20 +1413,29 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1392
1413
|
console.log("[Voice Widget] Checking variants for lesson:", props.primaryKey);
|
|
1393
1414
|
const variants = await fetchVoiceVariants(props.primaryKey);
|
|
1394
1415
|
console.log("[Voice Widget] All variants for lesson:", variants.length);
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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
|
+
}
|
|
1404
1434
|
}
|
|
1435
|
+
currentVariantIndex.value = 0;
|
|
1436
|
+
currentMode.value = "result";
|
|
1437
|
+
console.log("[Voice Widget] AUTO-SHOWING RESULT PAGE with", completedVariants.length, "variants");
|
|
1405
1438
|
}
|
|
1406
|
-
currentVariantIndex.value = 0;
|
|
1407
|
-
currentMode.value = "result";
|
|
1408
|
-
console.log("[Voice Widget] AUTO-SHOWING RESULT PAGE with", completedVariants.length, "variants");
|
|
1409
1439
|
}
|
|
1410
1440
|
}
|
|
1411
1441
|
initFromValue();
|
|
@@ -1454,8 +1484,9 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1454
1484
|
}
|
|
1455
1485
|
}
|
|
1456
1486
|
watch(() => props.value, initFromValue, { deep: true });
|
|
1487
|
+
const isRestoringFromVariant = ref(false);
|
|
1457
1488
|
watch(selectedModel, () => {
|
|
1458
|
-
if (currentVoices.value.length > 0) {
|
|
1489
|
+
if (!isRestoringFromVariant.value && currentVoices.value.length > 0) {
|
|
1459
1490
|
selectedVoiceId.value = currentVoices.value[0].id;
|
|
1460
1491
|
}
|
|
1461
1492
|
});
|
|
@@ -1934,10 +1965,10 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1934
1965
|
}
|
|
1935
1966
|
});
|
|
1936
1967
|
|
|
1937
|
-
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";
|
|
1938
1969
|
n(css,{});
|
|
1939
1970
|
|
|
1940
|
-
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"]]);
|
|
1941
1972
|
|
|
1942
1973
|
var index = defineInterface({
|
|
1943
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": {
|