Files
simstudioai--sim/apps/sim/app/(interfaces)/chat/hooks/use-audio-streaming.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

171 lines
4.5 KiB
TypeScript

'use client'
import { type RefObject, useCallback, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
const logger = createLogger('UseAudioStreaming')
declare global {
interface Window {
webkitAudioContext?: typeof AudioContext
}
}
interface AudioStreamingOptions {
voiceId: string
modelId?: string
chatId: string
onAudioStart?: () => void
onAudioEnd?: () => void
onError?: (error: Error) => void
}
interface AudioQueueItem {
text: string
options: AudioStreamingOptions
}
export function useAudioStreaming(sharedAudioContextRef?: RefObject<AudioContext | null>) {
const [isPlayingAudio, setIsPlayingAudio] = useState(false)
const localAudioContextRef = useRef<AudioContext | null>(null)
const audioContextRef = sharedAudioContextRef || localAudioContextRef
const currentSourceRef = useRef<AudioBufferSourceNode | null>(null)
const abortControllerRef = useRef<AbortController | null>(null)
const audioQueueRef = useRef<AudioQueueItem[]>([])
const isProcessingQueueRef = useRef(false)
const getAudioContext = useCallback(() => {
if (!audioContextRef.current) {
const AudioContextConstructor = window.AudioContext || window.webkitAudioContext
if (!AudioContextConstructor) {
throw new Error('AudioContext is not supported in this browser')
}
audioContextRef.current = new AudioContextConstructor()
}
return audioContextRef.current
}, [])
const stopAudio = useCallback(() => {
abortControllerRef.current?.abort()
if (currentSourceRef.current) {
try {
currentSourceRef.current.stop()
} catch (e) {
// Already stopped
}
currentSourceRef.current = null
}
audioQueueRef.current = []
isProcessingQueueRef.current = false
setIsPlayingAudio(false)
}, [])
const processAudioQueue = useCallback(async () => {
if (isProcessingQueueRef.current || audioQueueRef.current.length === 0) {
return
}
isProcessingQueueRef.current = true
const item = audioQueueRef.current.shift()
if (!item) {
isProcessingQueueRef.current = false
return
}
const { text, options } = item
const {
voiceId,
modelId = 'eleven_flash_v2_5',
chatId,
onAudioStart,
onAudioEnd,
onError,
} = options
try {
const audioContext = getAudioContext()
if (audioContext.state === 'suspended') {
await audioContext.resume()
}
// boundary-raw-fetch: TTS proxy returns raw audio bytes consumed via response.arrayBuffer() and decoded by AudioContext.decodeAudioData
const response = await fetch('/api/proxy/tts/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
text,
voiceId,
modelId,
chatId,
}),
signal: abortControllerRef.current?.signal,
})
if (!response.ok) {
const errorText = await response.text().catch(() => '')
throw new Error(errorText || `TTS request failed: ${response.statusText}`)
}
const arrayBuffer = await response.arrayBuffer()
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
const source = audioContext.createBufferSource()
source.buffer = audioBuffer
source.connect(audioContext.destination)
source.onended = () => {
currentSourceRef.current = null
onAudioEnd?.()
isProcessingQueueRef.current = false
if (audioQueueRef.current.length === 0) {
setIsPlayingAudio(false)
}
setTimeout(() => processAudioQueue(), 0)
}
currentSourceRef.current = source
source.start(0)
setIsPlayingAudio(true)
onAudioStart?.()
} catch (error) {
if (error instanceof Error && error.name !== 'AbortError') {
logger.error('Audio streaming error:', error)
onError?.(error)
}
isProcessingQueueRef.current = false
setTimeout(() => processAudioQueue(), 0)
}
}, [getAudioContext])
const streamTextToAudio = useCallback(
async (text: string, options: AudioStreamingOptions) => {
if (!text.trim()) {
return
}
if (!abortControllerRef.current || abortControllerRef.current.signal.aborted) {
abortControllerRef.current = new AbortController()
}
audioQueueRef.current.push({ text, options })
processAudioQueue()
},
[processAudioQueue]
)
return {
isPlayingAudio,
streamTextToAudio,
stopAudio,
}
}