上傳檔案到「modules/tts」

This commit is contained in:
X
2026-01-20 02:33:21 +00:00
parent 215ef201ee
commit 3197e245bb
3 changed files with 3754 additions and 3734 deletions

View File

@@ -40,7 +40,10 @@ export function speedToV3SpeechRate(speed) {
return Math.round((normalizeSpeed(speed) - 1) * 100); return Math.round((normalizeSpeed(speed) - 1) * 100);
} }
export function inferResourceIdBySpeaker(value) { export function inferResourceIdBySpeaker(value, explicitResourceId = null) {
if (explicitResourceId) {
return explicitResourceId;
}
const v = (value || '').trim(); const v = (value || '').trim();
const lower = v.toLowerCase(); const lower = v.toLowerCase();
if (lower.startsWith('icl_') || lower.startsWith('s_')) { if (lower.startsWith('icl_') || lower.startsWith('s_')) {
@@ -110,7 +113,7 @@ export async function speakSegmentAuth(messageId, segment, segmentIndex, batchId
} = ctx; } = ctx;
const speaker = segment.resolvedSpeaker; const speaker = segment.resolvedSpeaker;
const resourceId = inferResourceIdBySpeaker(speaker); const resourceId = segment.resolvedResourceId || inferResourceIdBySpeaker(speaker);
const params = buildSynthesizeParams({ text: segment.text, speaker, resourceId }, config); const params = buildSynthesizeParams({ text: segment.text, speaker, resourceId }, config);
const emotion = normalizeEmotion(segment.emotion); const emotion = normalizeEmotion(segment.emotion);
const contextTexts = resolveContextTexts(segment.context, resourceId); const contextTexts = resolveContextTexts(segment.context, resourceId);
@@ -171,7 +174,7 @@ export async function speakSegmentAuth(messageId, segment, segmentIndex, batchId
async function playWithStreaming(messageId, segment, segmentIndex, batchId, params, headers, ctx) { async function playWithStreaming(messageId, segment, segmentIndex, batchId, params, headers, ctx) {
const { player, storeLocalCache, buildCacheKey, updateState } = ctx; const { player, storeLocalCache, buildCacheKey, updateState } = ctx;
const speaker = segment.resolvedSpeaker; const speaker = segment.resolvedSpeaker;
const resourceId = inferResourceIdBySpeaker(speaker); const resourceId = params.resourceId;
const controller = new AbortController(); const controller = new AbortController();
const chunks = []; const chunks = [];
@@ -250,7 +253,7 @@ async function playWithStreaming(messageId, segment, segmentIndex, batchId, para
async function playWithoutStreaming(messageId, segment, segmentIndex, batchId, params, headers, ctx) { async function playWithoutStreaming(messageId, segment, segmentIndex, batchId, params, headers, ctx) {
const { player, storeLocalCache, buildCacheKey, updateState } = ctx; const { player, storeLocalCache, buildCacheKey, updateState } = ctx;
const speaker = segment.resolvedSpeaker; const speaker = segment.resolvedSpeaker;
const resourceId = inferResourceIdBySpeaker(speaker); const resourceId = params.resourceId;
const result = await synthesizeV3(params, headers); const result = await synthesizeV3(params, headers);
updateState({ audioBlob: result.audioBlob, usage: result.usage, status: 'queued' }); updateState({ audioBlob: result.audioBlob, usage: result.usage, status: 'queued' });

View File

@@ -1412,6 +1412,13 @@ select.input { cursor: pointer; }
<label class="form-label">名称</label> <label class="form-label">名称</label>
<input type="text" id="newVoiceName" class="input" placeholder="显示名称"> <input type="text" id="newVoiceName" class="input" placeholder="显示名称">
</div> </div>
<div class="form-group">
<label class="form-label">复刻版本</label>
<select id="newVoiceResourceId" class="input">
<option value="seed-icl-2.0">复刻 2.0</option>
<option value="seed-icl-1.0">复刻 1.0</option>
</select>
</div>
<button class="btn btn-primary" id="addMySpeakerBtn" style="margin-top: 18px;"><i class="fa-solid fa-plus"></i></button> <button class="btn btn-primary" id="addMySpeakerBtn" style="margin-top: 18px;"><i class="fa-solid fa-plus"></i></button>
</div> </div>
</div> </div>
@@ -2155,6 +2162,7 @@ function normalizeMySpeakers(list) {
name: String(item?.name || '').trim(), name: String(item?.name || '').trim(),
value: String(item?.value || '').trim(), value: String(item?.value || '').trim(),
source: item?.source || getVoiceSource(item?.value || ''), source: item?.source || getVoiceSource(item?.value || ''),
resourceId: item?.resourceId || null,
})).filter(item => item.value); })).filter(item => item.value);
} }
@@ -2265,11 +2273,14 @@ function doTestVoice(speaker, source, textElId, statusElId) {
setTestStatus(statusElId, 'playing', '正在合成...'); setTestStatus(statusElId, 'playing', '正在合成...');
const speakerItem = mySpeakers.find(s => s.value === speaker);
const resolvedResourceId = speakerItem?.resourceId;
post('xb-tts:test-speak', { post('xb-tts:test-speak', {
text, text,
speaker, speaker,
source, source,
resourceId: source === 'auth' ? inferResourceIdBySpeaker(speaker) : '', resourceId: source === 'auth' ? (resolvedResourceId || inferResourceIdBySpeaker(speaker)) : '',
}); });
} }
@@ -2437,10 +2448,11 @@ document.addEventListener('DOMContentLoaded', () => {
$('addMySpeakerBtn').addEventListener('click', () => { $('addMySpeakerBtn').addEventListener('click', () => {
const id = $('newVoiceId').value.trim(); const id = $('newVoiceId').value.trim();
const name = $('newVoiceName').value.trim(); const name = $('newVoiceName').value.trim();
const resourceId = $('newVoiceResourceId').value;
if (!id) { post('xb-tts:toast', { type: 'error', message: '请输入音色ID' }); return; } if (!id) { post('xb-tts:toast', { type: 'error', message: '请输入音色ID' }); return; }
if (!isInMyList(id)) { if (!isInMyList(id)) {
mySpeakers.push({ name: name || id, value: id, source: 'auth' }); mySpeakers.push({ name: name || id, value: id, source: 'auth', resourceId });
} }
selectedVoiceValue = id; selectedVoiceValue = id;
$('newVoiceId').value = ''; $('newVoiceId').value = '';

View File

@@ -254,7 +254,8 @@ function resolveSpeakerWithSource(speakerName, mySpeakers, defaultSpeaker) {
const defaultItem = list.find(s => s.value === defaultSpeaker); const defaultItem = list.find(s => s.value === defaultSpeaker);
return { return {
value: defaultSpeaker, value: defaultSpeaker,
source: defaultItem?.source || getVoiceSource(defaultSpeaker) source: defaultItem?.source || getVoiceSource(defaultSpeaker),
resourceId: defaultItem?.resourceId || null
}; };
} }
@@ -264,7 +265,8 @@ function resolveSpeakerWithSource(speakerName, mySpeakers, defaultSpeaker) {
if (byName?.value) { if (byName?.value) {
return { return {
value: byName.value, value: byName.value,
source: byName.source || getVoiceSource(byName.value) source: byName.source || getVoiceSource(byName.value),
resourceId: byName.resourceId || null
}; };
} }
@@ -274,12 +276,13 @@ function resolveSpeakerWithSource(speakerName, mySpeakers, defaultSpeaker) {
if (byValue?.value) { if (byValue?.value) {
return { return {
value: byValue.value, value: byValue.value,
source: byValue.source || getVoiceSource(byValue.value) source: byValue.source || getVoiceSource(byValue.value),
resourceId: byValue.resourceId || null
}; };
} }
if (FREE_VOICE_KEYS.has(speakerName)) { if (FREE_VOICE_KEYS.has(speakerName)) {
return { value: speakerName, source: 'free' }; return { value: speakerName, source: 'free', resourceId: null };
} }
// ★ 回退到默认,这是问题发生的地方 // ★ 回退到默认,这是问题发生的地方
@@ -288,7 +291,8 @@ function resolveSpeakerWithSource(speakerName, mySpeakers, defaultSpeaker) {
const defaultItem = list.find(s => s.value === defaultSpeaker); const defaultItem = list.find(s => s.value === defaultSpeaker);
return { return {
value: defaultSpeaker, value: defaultSpeaker,
source: defaultItem?.source || getVoiceSource(defaultSpeaker) source: defaultItem?.source || getVoiceSource(defaultSpeaker),
resourceId: defaultItem?.resourceId || null
}; };
} }
@@ -623,7 +627,8 @@ async function speakMessage(messageId, { mode = 'manual' } = {}) {
return { return {
...seg, ...seg,
resolvedSpeaker: resolved.value, resolvedSpeaker: resolved.value,
resolvedSource: resolved.source resolvedSource: resolved.source,
resolvedResourceId: resolved.resourceId
}; };
}); });
@@ -1325,7 +1330,7 @@ export async function initTts() {
return; return;
} }
const resourceId = options.resourceId || inferResourceIdBySpeaker(resolved.value); const resourceId = options.resourceId || resolved.resourceId || inferResourceIdBySpeaker(resolved.value);
const result = await synthesizeV3({ const result = await synthesizeV3({
appId: config.volc.appId, appId: config.volc.appId,
accessKey: config.volc.accessKey, accessKey: config.volc.accessKey,