All checks were successful
Deploy / deploy (push) Successful in 1m47s
Безопасность: - Rate-limit на /api/voice/chat (20/мин per cookie/IP, env VOICE_RATE_LIMIT). Защищает от случайных циклов и утечки PIN. - Усечение user prompt'а до 4000 символов в /api/voice/chat. - Tool-loop защита от циклов: если LLM дважды просит тот же tool с теми же args — прерываем (раньше мог уйти в бесконечный цикл при tool error'ах). Чистка кода: - lib/debug.ts — vlog/vwarn/verror гейтят браузерные логи за NEXT_PUBLIC_VOICE_DEBUG=1 (или localStorage 'voice-debug=1'). Серверные console.log оставлены — полезны в Docker logs. - lib/audio-wav.ts — вынесена дублированная floatToWav из VoiceController. - Удалены orphan компоненты FocusCard.tsx и CountdownCard.tsx (не подключены, отвергнуты по UX-фидбеку). Resilience: - WakeWordDetector: drop-on-busy в onChunk — на медленных устройствах (Android, бюджетный CPU) backlog inference больше не копится. - voice-history fallback на /tmp/voice-history если /data не примонтирован (локальная разработка / нестандартная конфигурация).
36 lines
1.4 KiB
TypeScript
36 lines
1.4 KiB
TypeScript
/**
|
|
* Float32 [-1, 1] PCM → WAV blob (mono, 16-bit, заданная частота).
|
|
* Используется браузером для отправки записанной фразы в STT.
|
|
*/
|
|
|
|
export function floatToWav(audio: Float32Array, sampleRate = 16000): Blob {
|
|
const numSamples = audio.length
|
|
const buffer = new ArrayBuffer(44 + numSamples * 2)
|
|
const view = new DataView(buffer)
|
|
|
|
writeStr(view, 0, 'RIFF')
|
|
view.setUint32(4, 36 + numSamples * 2, true)
|
|
writeStr(view, 8, 'WAVE')
|
|
writeStr(view, 12, 'fmt ')
|
|
view.setUint32(16, 16, true) // fmt chunk size
|
|
view.setUint16(20, 1, true) // PCM
|
|
view.setUint16(22, 1, true) // channels
|
|
view.setUint32(24, sampleRate, true)
|
|
view.setUint32(28, sampleRate * 2, true) // byte rate
|
|
view.setUint16(32, 2, true) // block align
|
|
view.setUint16(34, 16, true) // bits per sample
|
|
writeStr(view, 36, 'data')
|
|
view.setUint32(40, numSamples * 2, true)
|
|
|
|
let offset = 44
|
|
for (let i = 0; i < numSamples; i++, offset += 2) {
|
|
const s = Math.max(-1, Math.min(1, audio[i]))
|
|
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true)
|
|
}
|
|
return new Blob([buffer], { type: 'audio/wav' })
|
|
}
|
|
|
|
function writeStr(view: DataView, offset: number, s: string): void {
|
|
for (let i = 0; i < s.length; i++) view.setUint8(offset + i, s.charCodeAt(i))
|
|
}
|