Spaces:
Running
Running
| const SPOKEN_TEXT_MAX_LENGTH = 2400; | |
| export function spokenReplyText(value) { | |
| return String(value || '') | |
| .replace(/!\[[^\]]*]\([^)]+\)/g, '') | |
| .replace(/```[\s\S]*?```/g, ' 代码块 ') | |
| .replace(/`([^`]+)`/g, '$1') | |
| .replace(/\[([^\]]+)]\([^)]+\)/g, '$1') | |
| .replace(/[#>*_~]/g, '') | |
| .replace(/\s+/g, ' ') | |
| .trim() | |
| .slice(0, SPOKEN_TEXT_MAX_LENGTH); | |
| } | |
| const MESSAGE_SPEECH_FIRST_SEGMENT_CHARS = 180; | |
| const MESSAGE_SPEECH_SEGMENT_CHARS = 420; | |
| export function splitSpeechSegments(value) { | |
| const text = String(value || '').replace(/\s+/g, ' ').trim(); | |
| if (!text) { | |
| return []; | |
| } | |
| const sentencePattern = /[^.!?;,\u3002\uff01\uff1f\uff1b\uff0c]+[.!?;,\u3002\uff01\uff1f\uff1b\uff0c]?/g; | |
| const sentences = text.match(sentencePattern) || [text]; | |
| const segments = []; | |
| let current = ''; | |
| const pushCurrent = () => { | |
| const segment = current.trim(); | |
| if (segment) { | |
| segments.push(segment); | |
| } | |
| current = ''; | |
| }; | |
| const pushLongText = (chunk, limit) => { | |
| let rest = chunk.trim(); | |
| while (rest.length > limit) { | |
| const slice = rest.slice(0, limit).trim(); | |
| if (slice) { | |
| segments.push(slice); | |
| } | |
| rest = rest.slice(limit).trim(); | |
| } | |
| current = rest; | |
| }; | |
| for (const sentence of sentences) { | |
| const chunk = sentence.trim(); | |
| if (!chunk) { | |
| continue; | |
| } | |
| const limit = segments.length ? MESSAGE_SPEECH_SEGMENT_CHARS : MESSAGE_SPEECH_FIRST_SEGMENT_CHARS; | |
| const next = current ? `${current} ${chunk}` : chunk; | |
| if (next.length <= limit) { | |
| current = next; | |
| continue; | |
| } | |
| if (current) { | |
| pushCurrent(); | |
| } | |
| if (chunk.length > limit) { | |
| pushLongText(chunk, limit); | |
| } else { | |
| current = chunk; | |
| } | |
| } | |
| pushCurrent(); | |
| return segments; | |
| } | |
| export function voiceDialogStatusLabel(state) { | |
| const labels = { | |
| idle: '准备对话', | |
| listening: '正在听', | |
| transcribing: '正在转写', | |
| sending: '正在发送', | |
| waiting: '等待回复', | |
| speaking: '正在朗读', | |
| summarizing: '正在整理任务', | |
| handoff: '确认交给 Codex', | |
| error: '对话出错' | |
| }; | |
| return labels[state] || labels.idle; | |
| } | |
| export function downsampleAudio(input, inputRate, outputRate) { | |
| if (outputRate === inputRate) { | |
| return input; | |
| } | |
| const ratio = inputRate / outputRate; | |
| const outputLength = Math.max(1, Math.floor(input.length / ratio)); | |
| const output = new Float32Array(outputLength); | |
| for (let index = 0; index < outputLength; index += 1) { | |
| const sourceIndex = index * ratio; | |
| const before = Math.floor(sourceIndex); | |
| const after = Math.min(before + 1, input.length - 1); | |
| const weight = sourceIndex - before; | |
| output[index] = input[before] * (1 - weight) + input[after] * weight; | |
| } | |
| return output; | |
| } | |
| export function floatToPcm16Base64(input) { | |
| const bytes = new Uint8Array(input.length * 2); | |
| const view = new DataView(bytes.buffer); | |
| for (let index = 0; index < input.length; index += 1) { | |
| const sample = Math.max(-1, Math.min(1, input[index])); | |
| view.setInt16(index * 2, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true); | |
| } | |
| let binary = ''; | |
| const chunkSize = 0x8000; | |
| for (let index = 0; index < bytes.length; index += chunkSize) { | |
| binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize)); | |
| } | |
| return btoa(binary); | |
| } | |
| export function pcm16Base64ToFloat(base64) { | |
| const binary = atob(base64); | |
| const length = Math.floor(binary.length / 2); | |
| const output = new Float32Array(length); | |
| for (let index = 0; index < length; index += 1) { | |
| const lo = binary.charCodeAt(index * 2); | |
| const hi = binary.charCodeAt(index * 2 + 1); | |
| const value = (hi << 8) | lo; | |
| const signed = value >= 0x8000 ? value - 0x10000 : value; | |
| output[index] = Math.max(-1, Math.min(1, signed / 0x8000)); | |
| } | |
| return output; | |
| } | |
| export function audioLevel(samples) { | |
| if (!samples?.length) { | |
| return 0; | |
| } | |
| let total = 0; | |
| for (let index = 0; index < samples.length; index += 1) { | |
| total += samples[index] * samples[index]; | |
| } | |
| return Math.sqrt(total / samples.length); | |
| } | |