dsh-audiogen 0.4.18 → 0.4.20
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/lib/client.js +433 -364
- package/lib/client.js.map +1 -1
- package/lib/index.js +53 -6
- package/package.json +1 -1
- package/src/client/voices-view.tsx +26 -1
- package/src/protocol.ts +3 -1
- package/src/routes.ts +1 -0
- package/src/voice-manager.ts +53 -7
package/lib/index.js
CHANGED
|
@@ -1074,6 +1074,40 @@ const SHARED_VOICE_SORT_OPTIONS = [
|
|
|
1074
1074
|
"oldest",
|
|
1075
1075
|
"newest"
|
|
1076
1076
|
];
|
|
1077
|
+
/**
|
|
1078
|
+
* ISO 639-1 -> matching substrings across the two vendor vocabularies:
|
|
1079
|
+
* ElevenLabs uses ISO codes ("en"), MiniMax system ids use language labels
|
|
1080
|
+
* ("Chinese (Mandarin)_..." / "Japanese_..."). The aliases make one dropdown
|
|
1081
|
+
* value match both. "zh" also covers Cantonese (Chinese (Cantonese)).
|
|
1082
|
+
*/
|
|
1083
|
+
const LANGUAGE_ALIASES = {
|
|
1084
|
+
zh: [
|
|
1085
|
+
"zh",
|
|
1086
|
+
"chinese",
|
|
1087
|
+
"mandarin",
|
|
1088
|
+
"cantonese",
|
|
1089
|
+
"yue"
|
|
1090
|
+
],
|
|
1091
|
+
en: ["en", "english"],
|
|
1092
|
+
ja: ["ja", "japanese"],
|
|
1093
|
+
ko: ["ko", "korean"],
|
|
1094
|
+
es: ["es", "spanish"],
|
|
1095
|
+
fr: ["fr", "french"],
|
|
1096
|
+
de: ["de", "german"],
|
|
1097
|
+
ru: ["ru", "russian"],
|
|
1098
|
+
it: ["it", "italian"],
|
|
1099
|
+
pt: ["pt", "portuguese"],
|
|
1100
|
+
ar: ["ar", "arabic"],
|
|
1101
|
+
hi: ["hi", "hindi"]
|
|
1102
|
+
};
|
|
1103
|
+
function languageMatches(language, locale, needle) {
|
|
1104
|
+
const haystack = [language ?? "", locale ?? ""].join(" ").toLowerCase();
|
|
1105
|
+
const value = needle.trim().toLowerCase();
|
|
1106
|
+
if (value === "") return true;
|
|
1107
|
+
if (haystack.includes(value)) return true;
|
|
1108
|
+
for (const alias of LANGUAGE_ALIASES[value] ?? []) if (haystack.includes(alias)) return true;
|
|
1109
|
+
return false;
|
|
1110
|
+
}
|
|
1077
1111
|
const MINIMAX_LANGUAGE_PREFIXES = [
|
|
1078
1112
|
"Chinese (Mandarin)",
|
|
1079
1113
|
"Chinese (Cantonese)",
|
|
@@ -1238,7 +1272,10 @@ async function listElevenLabs(channel, options) {
|
|
|
1238
1272
|
} catch (error) {
|
|
1239
1273
|
failures.push(`共享音色库:${error instanceof Error ? error.message : String(error)}`);
|
|
1240
1274
|
}
|
|
1241
|
-
if (entries.length === 0 && failures.length > 0)
|
|
1275
|
+
if (entries.length === 0 && failures.length > 0) {
|
|
1276
|
+
const gatewayHint = gatewayVoiceLibraryHint(failures);
|
|
1277
|
+
throw new Error(`ElevenLabs 音色列表拉取失败:${failures.join(";").slice(0, 300)}${gatewayHint}`);
|
|
1278
|
+
}
|
|
1242
1279
|
const seen = /* @__PURE__ */ new Set();
|
|
1243
1280
|
const deduped = [];
|
|
1244
1281
|
for (const entry of entries) {
|
|
@@ -1246,13 +1283,23 @@ async function listElevenLabs(channel, options) {
|
|
|
1246
1283
|
seen.add(entry.voice_id);
|
|
1247
1284
|
deduped.push(entry);
|
|
1248
1285
|
}
|
|
1286
|
+
const note = failures.length === 0 ? void 0 : `部分端点失败(已忽略):${failures.join(";").slice(0, 300)}${gatewayVoiceLibraryHint(failures)}`;
|
|
1249
1287
|
return {
|
|
1250
1288
|
vendor: "elevenlabs",
|
|
1251
1289
|
voices: deduped,
|
|
1252
1290
|
truncated: false,
|
|
1253
|
-
...
|
|
1291
|
+
...note === void 0 ? {} : { note }
|
|
1254
1292
|
};
|
|
1255
1293
|
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Gateway (new-api 类) 通常不映射 ElevenLabs 音色库端点,其响应是
|
|
1296
|
+
* 404/Invalid URL。把这种失败翻译成可操作的建议,而不是一份原始 JSON。
|
|
1297
|
+
*/
|
|
1298
|
+
function gatewayVoiceLibraryHint(failures) {
|
|
1299
|
+
const raw = failures.join(" ").toLowerCase();
|
|
1300
|
+
if (!raw.includes("404") && !raw.includes("invalid url") && !raw.includes("not found")) return "";
|
|
1301
|
+
return "。该渠道网关未提供 ElevenLabs 音色库端点(/v1/voices、/v1/shared-voices):生成可用,但浏览/删除音色需要配置官方 API 地址 https://api.elevenlabs.io 的渠道";
|
|
1302
|
+
}
|
|
1256
1303
|
async function listVendorVoices(channel, options = {}) {
|
|
1257
1304
|
if (channel.apiUrl.trim() === "") throw new Error("渠道未配置 API 地址");
|
|
1258
1305
|
if (channel.apiKey.trim() === "") throw new Error("渠道未配置 API 密钥");
|
|
@@ -1324,7 +1371,7 @@ async function deleteVendorVoice(channel, voiceId) {
|
|
|
1324
1371
|
}
|
|
1325
1372
|
function cap(options) {
|
|
1326
1373
|
const limit = typeof options.limit === "number" && Number.isFinite(options.limit) ? Math.floor(options.limit) : 100;
|
|
1327
|
-
return Math.max(1, Math.min(
|
|
1374
|
+
return Math.max(1, Math.min(500, limit));
|
|
1328
1375
|
}
|
|
1329
1376
|
/** Which official shared-voice filters are set (for the MiniMax "not supported" note). */
|
|
1330
1377
|
function serverFilterNote(filters) {
|
|
@@ -1341,9 +1388,7 @@ function applyFilter(entries, options) {
|
|
|
1341
1388
|
const count = cap(options);
|
|
1342
1389
|
const matched = entries.filter((entry) => {
|
|
1343
1390
|
if (source !== "" && entry.source !== source) return false;
|
|
1344
|
-
if (language !== "")
|
|
1345
|
-
if (![entry.language ?? "", entry.locale ?? ""].join(" ").toLowerCase().includes(language)) return false;
|
|
1346
|
-
}
|
|
1391
|
+
if (language !== "" && !languageMatches(entry.language, entry.locale, language)) return false;
|
|
1347
1392
|
if (field(filters.accent) !== "" && field(entry.accent) !== field(filters.accent)) return false;
|
|
1348
1393
|
if (field(filters.gender) !== "" && field(entry.gender) !== field(filters.gender)) return false;
|
|
1349
1394
|
if (field(filters.age) !== "" && field(entry.age) !== field(filters.age)) return false;
|
|
@@ -1367,6 +1412,7 @@ function applyFilter(entries, options) {
|
|
|
1367
1412
|
}
|
|
1368
1413
|
return true;
|
|
1369
1414
|
});
|
|
1415
|
+
matched.sort((a, b) => a.deletable === b.deletable ? 0 : a.deletable ? -1 : 1);
|
|
1370
1416
|
return {
|
|
1371
1417
|
voices: matched.slice(0, count),
|
|
1372
1418
|
truncated: matched.length > count
|
|
@@ -2421,6 +2467,7 @@ function makeRoutes(deps) {
|
|
|
2421
2467
|
...str("language") === void 0 ? {} : { language: str("language") },
|
|
2422
2468
|
...str("keyword") === void 0 ? {} : { keyword: str("keyword") },
|
|
2423
2469
|
...str("source") === void 0 ? {} : { source: str("source") },
|
|
2470
|
+
...typeof body?.limit === "number" && Number.isFinite(body.limit) ? { limit: Math.floor(body.limit) } : {},
|
|
2424
2471
|
...Object.keys(filters).length === 0 ? {} : { serverFilters: filters }
|
|
2425
2472
|
});
|
|
2426
2473
|
writeJson(res, 200, {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-audiogen",
|
|
3
3
|
"description": "AI audio generation plugin for the dsh web GUI: multi-vendor TTS/music/sound-effect channels (OpenAI-compatible, ElevenLabs, MiniMax, Stability AI and custom), per-channel model/voice catalogs, Agent tool and a sidebar AI 音频 panel.",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.20",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -31,6 +31,23 @@ const SOURCE_LABELS: Record<VoiceEntry['source'], string> = {
|
|
|
31
31
|
shared: '社区共享',
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** 语言下拉:ISO 639-1 值;后端按两套厂商词汇匹配(ElevenLabs ISO / MiniMax 前缀)。 */
|
|
35
|
+
const LANGUAGE_OPTIONS: Array<{ value: string; label: string }> = [
|
|
36
|
+
{ value: '', label: '全部' },
|
|
37
|
+
{ value: 'zh', label: '中文(普通话 / 粤语)' },
|
|
38
|
+
{ value: 'en', label: '英语 English' },
|
|
39
|
+
{ value: 'ja', label: '日语 Japanese' },
|
|
40
|
+
{ value: 'ko', label: '韩语 Korean' },
|
|
41
|
+
{ value: 'es', label: '西班牙语 Spanish' },
|
|
42
|
+
{ value: 'fr', label: '法语 French' },
|
|
43
|
+
{ value: 'de', label: '德语 German' },
|
|
44
|
+
{ value: 'ru', label: '俄语 Russian' },
|
|
45
|
+
{ value: 'it', label: '意大利语 Italian' },
|
|
46
|
+
{ value: 'pt', label: '葡萄牙语 Portuguese' },
|
|
47
|
+
{ value: 'ar', label: '阿拉伯语 Arabic' },
|
|
48
|
+
{ value: 'hi', label: '印地语 Hindi' },
|
|
49
|
+
]
|
|
50
|
+
|
|
34
51
|
function useConfig(scope: AudiogenScope) {
|
|
35
52
|
const [value, setValue] = useState(scope.getSnapshot().value)
|
|
36
53
|
useEffect(() => scope.subscribe(() => { setValue(scope.getSnapshot().value) }), [scope])
|
|
@@ -76,6 +93,7 @@ export function VoicesView(props: {
|
|
|
76
93
|
const [freeUsersAllowed, setFreeUsersAllowed] = useState(false)
|
|
77
94
|
|
|
78
95
|
const [voices, setVoices] = useState<VoiceEntry[]>([])
|
|
96
|
+
const [truncated, setTruncated] = useState(false)
|
|
79
97
|
const [loading, setLoading] = useState(false)
|
|
80
98
|
const [error, setError] = useState<string | null>(null)
|
|
81
99
|
const [note, setNote] = useState<string | null>(null)
|
|
@@ -103,9 +121,11 @@ export function VoicesView(props: {
|
|
|
103
121
|
...(sort === '' ? {} : { sort }),
|
|
104
122
|
...(featured ? { featured: true } : {}),
|
|
105
123
|
...(freeUsersAllowed ? { free_users_allowed: true } : {}),
|
|
124
|
+
limit: 500,
|
|
106
125
|
})
|
|
107
126
|
if (response.ok) {
|
|
108
127
|
setVoices(response.voices ?? [])
|
|
128
|
+
setTruncated(response.truncated === true)
|
|
109
129
|
setNote(response.note ?? null)
|
|
110
130
|
} else {
|
|
111
131
|
setVoices([])
|
|
@@ -168,7 +188,11 @@ export function VoicesView(props: {
|
|
|
168
188
|
</label>
|
|
169
189
|
<label className={css.field}>
|
|
170
190
|
<span className={css.fieldLabel}>语言</span>
|
|
171
|
-
<
|
|
191
|
+
<select className={css.select} value={language} onChange={event => setLanguage(event.target.value)}>
|
|
192
|
+
{LANGUAGE_OPTIONS.map(option => (
|
|
193
|
+
<option key={option.value} value={option.value}>{option.label}</option>
|
|
194
|
+
))}
|
|
195
|
+
</select>
|
|
172
196
|
</label>
|
|
173
197
|
<label className={css.field}>
|
|
174
198
|
<span className={css.fieldLabel}>关键词</span>
|
|
@@ -218,6 +242,7 @@ export function VoicesView(props: {
|
|
|
218
242
|
{loading ? <span className={css.stateNote}>正在拉取音色列表…</span> : null}
|
|
219
243
|
{!loading && error !== null ? <span className={css.stateNote} data-error>⚠ {error}</span> : null}
|
|
220
244
|
{!loading && note !== null ? <span className={css.stateNote}>{note}</span> : null}
|
|
245
|
+
{!loading && truncated ? <span className={css.stateNote}>已截断:仅显示前 {voices.length} 个,可用筛选条件缩小范围</span> : null}
|
|
221
246
|
{!loading && error === null ? <span className={css.count}>{voices.length} 个音色</span> : null}
|
|
222
247
|
</div>
|
|
223
248
|
|
package/src/protocol.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
export const AUDIOGEN_SETTINGS_NAMESPACE = 'dsh-audiogen'
|
|
9
9
|
|
|
10
10
|
/** Published package version shared by the host updater and the client UI. */
|
|
11
|
-
export const PLUGIN_VERSION = '0.4.
|
|
11
|
+
export const PLUGIN_VERSION = '0.4.20'
|
|
12
12
|
|
|
13
13
|
/** Same-origin route family (loopback-only, mirroring dsh-imagegen). */
|
|
14
14
|
export const SETTINGS_API = {
|
|
@@ -70,6 +70,8 @@ export interface VoicesListRequest {
|
|
|
70
70
|
language?: string
|
|
71
71
|
keyword?: string
|
|
72
72
|
source?: string
|
|
73
|
+
/** Max returned entries (default 100, hard cap 500). */
|
|
74
|
+
limit?: number
|
|
73
75
|
/** Official /v1/shared-voices server-side filters (ElevenLabs only). */
|
|
74
76
|
search?: string
|
|
75
77
|
use_case?: string
|
package/src/routes.ts
CHANGED
|
@@ -426,6 +426,7 @@ export function makeRoutes(deps: AudiogenRoutesDeps): WebRoute[] {
|
|
|
426
426
|
...(str('language') === undefined ? {} : { language: str('language')! }),
|
|
427
427
|
...(str('keyword') === undefined ? {} : { keyword: str('keyword')! }),
|
|
428
428
|
...(str('source') === undefined ? {} : { source: str('source')! }),
|
|
429
|
+
...(typeof body?.limit === 'number' && Number.isFinite(body.limit) ? { limit: Math.floor(body.limit) } : {}),
|
|
429
430
|
...(Object.keys(filters).length === 0 ? {} : { serverFilters: filters }),
|
|
430
431
|
})
|
|
431
432
|
writeJson(res, 200, {
|
package/src/voice-manager.ts
CHANGED
|
@@ -70,6 +70,38 @@ export interface SharedVoiceFilters {
|
|
|
70
70
|
|
|
71
71
|
const SHARED_VOICE_SORT_OPTIONS = ['most_used', 'random', 'oldest', 'newest'] as const
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* ISO 639-1 -> matching substrings across the two vendor vocabularies:
|
|
75
|
+
* ElevenLabs uses ISO codes ("en"), MiniMax system ids use language labels
|
|
76
|
+
* ("Chinese (Mandarin)_..." / "Japanese_..."). The aliases make one dropdown
|
|
77
|
+
* value match both. "zh" also covers Cantonese (Chinese (Cantonese)).
|
|
78
|
+
*/
|
|
79
|
+
const LANGUAGE_ALIASES: Record<string, string[]> = {
|
|
80
|
+
zh: ['zh', 'chinese', 'mandarin', 'cantonese', 'yue'],
|
|
81
|
+
en: ['en', 'english'],
|
|
82
|
+
ja: ['ja', 'japanese'],
|
|
83
|
+
ko: ['ko', 'korean'],
|
|
84
|
+
es: ['es', 'spanish'],
|
|
85
|
+
fr: ['fr', 'french'],
|
|
86
|
+
de: ['de', 'german'],
|
|
87
|
+
ru: ['ru', 'russian'],
|
|
88
|
+
it: ['it', 'italian'],
|
|
89
|
+
pt: ['pt', 'portuguese'],
|
|
90
|
+
ar: ['ar', 'arabic'],
|
|
91
|
+
hi: ['hi', 'hindi'],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function languageMatches(language: string | undefined, locale: string | undefined, needle: string): boolean {
|
|
95
|
+
const haystack = [language ?? '', locale ?? ''].join(' ').toLowerCase()
|
|
96
|
+
const value = needle.trim().toLowerCase()
|
|
97
|
+
if (value === '') return true
|
|
98
|
+
if (haystack.includes(value)) return true
|
|
99
|
+
for (const alias of LANGUAGE_ALIASES[value] ?? []) {
|
|
100
|
+
if (haystack.includes(alias)) return true
|
|
101
|
+
}
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
|
|
73
105
|
export interface ListVoicesResult {
|
|
74
106
|
vendor: string
|
|
75
107
|
voices: VendorVoiceEntry[]
|
|
@@ -272,7 +304,10 @@ async function listElevenLabs(channel: AudioChannel, options: ListVoicesOptions)
|
|
|
272
304
|
}
|
|
273
305
|
|
|
274
306
|
if (entries.length === 0 && failures.length > 0) {
|
|
275
|
-
|
|
307
|
+
const gatewayHint = gatewayVoiceLibraryHint(failures)
|
|
308
|
+
throw new Error(
|
|
309
|
+
`ElevenLabs 音色列表拉取失败:${failures.join(';').slice(0, 300)}${gatewayHint}`,
|
|
310
|
+
)
|
|
276
311
|
}
|
|
277
312
|
// Owned voices come first and shadow shared ones with the same id.
|
|
278
313
|
const seen = new Set<string>()
|
|
@@ -282,14 +317,25 @@ async function listElevenLabs(channel: AudioChannel, options: ListVoicesOptions)
|
|
|
282
317
|
seen.add(entry.voice_id)
|
|
283
318
|
deduped.push(entry)
|
|
284
319
|
}
|
|
320
|
+
const note = failures.length === 0 ? undefined : `部分端点失败(已忽略):${failures.join(';').slice(0, 300)}${gatewayVoiceLibraryHint(failures)}`
|
|
285
321
|
return {
|
|
286
322
|
vendor: 'elevenlabs',
|
|
287
323
|
voices: deduped,
|
|
288
324
|
truncated: false,
|
|
289
|
-
...(
|
|
325
|
+
...(note === undefined ? {} : { note }),
|
|
290
326
|
}
|
|
291
327
|
}
|
|
292
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Gateway (new-api 类) 通常不映射 ElevenLabs 音色库端点,其响应是
|
|
331
|
+
* 404/Invalid URL。把这种失败翻译成可操作的建议,而不是一份原始 JSON。
|
|
332
|
+
*/
|
|
333
|
+
function gatewayVoiceLibraryHint(failures: string[]): string {
|
|
334
|
+
const raw = failures.join(' ').toLowerCase()
|
|
335
|
+
if (!raw.includes('404') && !raw.includes('invalid url') && !raw.includes('not found')) return ''
|
|
336
|
+
return '。该渠道网关未提供 ElevenLabs 音色库端点(/v1/voices、/v1/shared-voices):生成可用,但浏览/删除音色需要配置官方 API 地址 https://api.elevenlabs.io 的渠道'
|
|
337
|
+
}
|
|
338
|
+
|
|
293
339
|
// ------------------------------------------------------ public surface
|
|
294
340
|
|
|
295
341
|
export async function listVendorVoices(
|
|
@@ -387,7 +433,7 @@ function cap(options: ListVoicesOptions): number {
|
|
|
387
433
|
const limit = typeof options.limit === 'number' && Number.isFinite(options.limit)
|
|
388
434
|
? Math.floor(options.limit)
|
|
389
435
|
: 100
|
|
390
|
-
return Math.max(1, Math.min(
|
|
436
|
+
return Math.max(1, Math.min(500, limit))
|
|
391
437
|
}
|
|
392
438
|
|
|
393
439
|
/** Which official shared-voice filters are set (for the MiniMax "not supported" note). */
|
|
@@ -412,10 +458,7 @@ function applyFilter(
|
|
|
412
458
|
const count = cap(options)
|
|
413
459
|
const matched = entries.filter(entry => {
|
|
414
460
|
if (source !== '' && entry.source !== source) return false
|
|
415
|
-
if (language !== '')
|
|
416
|
-
const haystack = [entry.language ?? '', entry.locale ?? ''].join(' ').toLowerCase()
|
|
417
|
-
if (!haystack.includes(language)) return false
|
|
418
|
-
}
|
|
461
|
+
if (language !== '' && !languageMatches(entry.language, entry.locale, language)) return false
|
|
419
462
|
// Official shared-voice filters double as local fallback filters so they
|
|
420
463
|
// also apply to owned voices and providers without server-side support.
|
|
421
464
|
if (field(filters.accent) !== '' && field(entry.accent) !== field(filters.accent)) return false
|
|
@@ -438,5 +481,8 @@ function applyFilter(
|
|
|
438
481
|
}
|
|
439
482
|
return true
|
|
440
483
|
})
|
|
484
|
+
// Deletable (account-owned/custom) voices first so narrow default windows
|
|
485
|
+
// show the manageable ones; stable sort keeps provider order inside groups.
|
|
486
|
+
matched.sort((a, b) => (a.deletable === b.deletable ? 0 : a.deletable ? -1 : 1))
|
|
441
487
|
return { voices: matched.slice(0, count), truncated: matched.length > count }
|
|
442
488
|
}
|