Skip to content

Commit f3f05a0

Browse files
committed
1
1 parent 70c9270 commit f3f05a0

14 files changed

Lines changed: 105 additions & 83 deletions

File tree

src/public/pages/scripts/features/speechRecognition.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ export async function openSpeechRecognitionSession({
143143
ws.binaryType = 'arraybuffer'
144144

145145
let finalSettled = false
146+
let closedIntentionally = false
146147
/** @type {(value: any) => void} */
147148
let resolveFinal
148149
/** @type {(reason?: any) => void} */
@@ -181,7 +182,9 @@ export async function openSpeechRecognitionSession({
181182
})
182183

183184
ws.addEventListener('close', () => {
184-
if (!finalSettled) rejectFinal(new Error('SpeechRecognition websocket closed unexpectedly'))
185+
if (finalSettled) return
186+
if (closedIntentionally) resolveFinal({ text: '' })
187+
else rejectFinal(new Error('SpeechRecognition websocket closed unexpectedly'))
185188
})
186189

187190
ws.send(JSON.stringify({ type: 'start', language, hotwords }))
@@ -210,6 +213,8 @@ export async function openSpeechRecognitionSession({
210213
* @returns {void}
211214
*/
212215
close: () => {
216+
closedIntentionally = true
217+
if (!finalSettled) resolveFinal({ text: '' })
213218
try { ws.close() } catch { /* ignore */ }
214219
},
215220
}

src/public/parts/serviceGenerators/SpeechRecognition/fallback/main.mjs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@ const configTemplate = {
4242
*/
4343
async function GetSource(config, { username, SaveConfig }) {
4444
const unnamedSources = []
45-
let sources
46-
try {
47-
sources = await Promise.all(config.sources.map(source => loadSpeechRecognitionSourceFromNameOrConfigData(username, source, unnamedSources, {
48-
SaveConfig
49-
})))
50-
} catch (e) {
45+
const settled = await Promise.allSettled(config.sources.map(source => loadSpeechRecognitionSourceFromNameOrConfigData(username, source, unnamedSources, {
46+
SaveConfig
47+
})))
48+
const failed = settled.find(entry => entry.status === 'rejected')
49+
if (failed) {
5150
await Promise.all(unnamedSources.map(source => source?.Unload?.()))
52-
throw e
51+
throw failed.reason
5352
}
53+
const sources = settled.map(entry => /** @type {PromiseFulfilledResult<SpeechRecognitionSource_t>} */entry.value)
5454
return {
5555
type: 'speech-recognition',
5656
info: buildSourceInfo(product_info, { name: config.name, provider: config.provider || 'unknown' }),

src/public/parts/serviceGenerators/SpeechRecognition/openai-whisper/main.mjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ async function GetSource(config) {
5454
const wav = pcmToWav(pcm)
5555
const form = new FormData()
5656
form.append('model', model)
57-
form.append('language', normalizeLang(options.language, 'auto'))
57+
const rawLang = String(options.language || '').trim().toLowerCase()
58+
if (rawLang && rawLang !== 'auto') {
59+
const language = normalizeLang(options.language, 'short')
60+
if (/^[a-z]{2}$/.test(language)) form.append('language', language)
61+
}
5862
if (options.hotwords?.length)
5963
form.append('prompt', options.hotwords.join(', '))
6064
form.append('file', new Blob([wav], { type: 'audio/wav' }), 'audio.wav')

src/public/parts/serviceGenerators/SpeechRecognition/shared/recognizeHelpers.mjs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,10 @@ export function openWs(url, opts = {}) {
127127
* @returns {string} 文本
128128
*/
129129
export function extractRtasrText(resultData) {
130-
let out = ''
131-
for (const rt of resultData?.cn?.st?.rt || [])
132-
for (const ws of rt.ws || [])
133-
for (const cw of ws.cw || [])
134-
out += cw.w || ''
135-
return out
130+
let text = ''
131+
for (const recognitionResult of resultData?.cn?.st?.rt || [])
132+
for (const wordSegment of recognitionResult.ws || [])
133+
for (const candidateWord of wordSegment.cw || [])
134+
text += candidateWord.w || ''
135+
return text
136136
}

src/public/parts/serviceGenerators/SpeechRecognition/xfyun-lfasr-fast/main.mjs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,16 @@ function parseJsonField(value) {
5858
function parseFastResult(result) {
5959
const parsed = parseJsonField(result)
6060
if (!parsed) return typeof result === 'string' ? result : ''
61-
let out = ''
61+
let text = ''
6262
for (const item of parsed.lattice || []) {
6363
const best = parseJsonField(item.json_1best)
6464
if (!best) continue
65-
for (const rt of best.st?.rt || [])
66-
for (const ws of rt.ws || [])
67-
for (const cw of ws.cw || [])
68-
out += cw.w || ''
65+
for (const recognitionResult of best.st?.rt || [])
66+
for (const wordSegment of recognitionResult.ws || [])
67+
for (const candidateWord of wordSegment.cw || [])
68+
text += candidateWord.w || ''
6969
}
70-
return out
70+
return text
7171
}
7272

7373
/**

src/public/parts/serviceGenerators/SpeechRecognition/xfyun-rtasr-std/main.mjs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,18 @@ const configTemplate = {
4242
*/
4343
function sleep(ms, signal) {
4444
return new Promise((resolve, reject) => {
45-
const timer = setTimeout(resolve, ms)
46-
signal?.addEventListener('abort', () => {
45+
/**
46+
* @returns {void}
47+
*/
48+
const onAbort = () => {
4749
clearTimeout(timer)
4850
reject(signal.reason instanceof Error ? signal.reason : new Error('aborted'))
49-
}, { once: true })
51+
}
52+
const timer = setTimeout(() => {
53+
signal?.removeEventListener('abort', onAbort)
54+
resolve()
55+
}, ms)
56+
signal?.addEventListener('abort', onAbort)
5057
})
5158
}
5259

src/public/parts/serviceGenerators/SpeechRecognition/xfyun-rtasr/main.mjs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,18 @@ const configTemplate = {
4343
*/
4444
function sleep(ms, signal) {
4545
return new Promise((resolve, reject) => {
46-
const timer = setTimeout(resolve, ms)
47-
signal?.addEventListener('abort', () => {
46+
/**
47+
* @returns {void}
48+
*/
49+
const onAbort = () => {
4850
clearTimeout(timer)
4951
reject(signal.reason instanceof Error ? signal.reason : new Error('aborted'))
50-
}, { once: true })
52+
}
53+
const timer = setTimeout(() => {
54+
signal?.removeEventListener('abort', onAbort)
55+
resolve()
56+
}, ms)
57+
signal?.addEventListener('abort', onAbort)
5158
})
5259
}
5360

src/public/parts/serviceGenerators/SpeechRecognition/xfyun-spark/main.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ function decodePayload(msg) {
7777
function applySegment(segments, decoded, lastText) {
7878
const { piece, pgs, rg, sn } = decoded
7979
if (pgs === 'rpl' && Array.isArray(rg) && rg.length >= 2) {
80-
for (let i = rg[0]; i <= rg[1]; i++) segments.delete(i)
80+
for (let segmentIndex = rg[0]; segmentIndex <= rg[1]; segmentIndex++) segments.delete(segmentIndex)
8181
segments.set(sn, piece)
8282
return buildSegmentText(segments)
8383
}

src/public/parts/shells/chat/public/hub/composerFiles.mjs

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ async function setVoiceBtnIcon(recording) {
3232
}
3333

3434
let isRecording = false
35-
/** @type {Awaited<ReturnType<typeof startVoiceRecording>> | null} */
36-
let voiceSession = null
35+
/** @type {Promise<Awaited<ReturnType<typeof startVoiceRecording>>> | null} */
36+
let voiceSessionPromise = null
3737

3838
/**
3939
* @returns {HTMLElement|null} 附件预览容器
@@ -151,24 +151,34 @@ export async function toggleVoiceRecording() {
151151
if (!voiceButton) return
152152

153153
if (isRecording) {
154-
const session = voiceSession
155-
voiceSession = null
156-
await setVoiceBtnIcon(false)
154+
const sessionPromise = voiceSessionPromise
155+
voiceSessionPromise = null
157156
isRecording = false
158-
const file = await session?.stop()
159-
if (!file) return
160-
const [added] = await addFilesFromEvent({ target: { files: [file] } })
161-
if (added) await maybeRecognizeVoiceAttachment(added.file, added.element, file)
157+
await setVoiceBtnIcon(false)
158+
try {
159+
const session = await sessionPromise
160+
const file = await session?.stop()
161+
if (!file) return
162+
const [added] = await addFilesFromEvent({ target: { files: [file] } })
163+
if (added) await maybeRecognizeVoiceAttachment(added.file, added.element, file)
164+
}
165+
catch { /* 启动失败或 stop 失败时忽略 */ }
162166
return
163167
}
164168

169+
isRecording = true
170+
const starting = startVoiceRecording()
171+
voiceSessionPromise = starting
165172
try {
166-
isRecording = true
167-
voiceSession = await startVoiceRecording()
173+
await starting
174+
if (voiceSessionPromise !== starting) return
168175
await setVoiceBtnIcon(true)
169176
}
170177
catch {
178+
if (voiceSessionPromise !== starting) return
179+
voiceSessionPromise = null
171180
isRecording = false
181+
await setVoiceBtnIcon(false)
172182
showToastI18n('error', 'chat.voiceRecording.errorAccessingMicrophone')
173183
}
174184
}
@@ -178,11 +188,14 @@ export async function toggleVoiceRecording() {
178188
* @returns {Promise<void>}
179189
*/
180190
export async function stopVoiceIfRecording() {
181-
if (isRecording && voiceSession) {
182-
const session = voiceSession
183-
voiceSession = null
184-
isRecording = false
185-
await setVoiceBtnIcon(false)
186-
await session.stop().catch(() => null)
191+
if (!isRecording) return
192+
const sessionPromise = voiceSessionPromise
193+
voiceSessionPromise = null
194+
isRecording = false
195+
await setVoiceBtnIcon(false)
196+
try {
197+
const session = await sessionPromise
198+
await session?.stop()
187199
}
200+
catch { /* ignore */ }
188201
}

src/public/parts/shells/chat/public/hub/messages/render/file.mjs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ let revealScheduled = false
132132
* 合并调度 `revealMessageAudioSpeechRecognitionItems(#messages)`:同一微任务队列内只执行一次。
133133
* @returns {void}
134134
*/
135-
function scheduleRevealMessageAudioSpeechRecognitionItems() {
135+
export function scheduleRevealMessageAudioSpeechRecognitionItems() {
136136
if (revealScheduled) return
137137
revealScheduled = true
138138
queueMicrotask(() => {
@@ -220,9 +220,7 @@ export async function renderMessageFileIdsHtml(message) {
220220
rows.push(await renderSingleFileAttachmentHtml(groupId, id, meta, mime, alt))
221221
}
222222
if (!rows.length) return ''
223-
const html = `<div class="message-files flex flex-col gap-1 mt-1">${rows.join('')}</div>`
224-
scheduleRevealMessageAudioSpeechRecognitionItems()
225-
return html
223+
return `<div class="message-files flex flex-col gap-1 mt-1">${rows.join('')}</div>`
226224
}
227225

228226
/**

0 commit comments

Comments
 (0)