chore: import upstream snapshot with attribution
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
'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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user