asi se fue xd

This commit is contained in:
2026-02-18 12:13:22 -06:00
parent d27da30494
commit d0fdd04132
17 changed files with 612 additions and 735 deletions

View File

@@ -0,0 +1,177 @@
/**
* Singleton Whisper WebSocket Service
* One shared connection used by all voice components (FloatingVoice, useVoiceCapture, etc.)
*/
import { ref } from 'vue'
import { endpoints } from '../config/endpoints'
export type WhisperStatus = 'offline' | 'loading' | 'ready'
type TranscriptionCallback = (msg: {
success?: boolean
text?: string
error?: string
partial?: boolean
model?: string
device?: string
}) => void
// ====== Singleton state ======
const status = ref<WhisperStatus>('loading')
let socket: WebSocket | null = null
let reconnectTimer: number | null = null
const listeners = new Set<TranscriptionCallback>()
// ====== Connection management ======
function connect() {
if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) return
console.log('[WhisperSocket] Connecting to', endpoints.whisper)
socket = new WebSocket(endpoints.whisper)
const timeout = setTimeout(() => {
if (socket && socket.readyState !== WebSocket.OPEN) {
console.error('[WhisperSocket] Connection timeout (10s)')
socket.close()
status.value = 'loading'
}
}, 10000)
socket.onopen = () => {
clearTimeout(timeout)
console.log('[WhisperSocket] Connected')
status.value = 'ready'
}
socket.onmessage = (event) => {
try {
const msg = JSON.parse(event.data)
if (msg.type === 'ready') {
console.log('[WhisperSocket] Server ready:', msg.model, msg.device)
status.value = 'ready'
} else if (msg.type === 'transcription') {
// Broadcast to all listeners
for (const cb of listeners) {
cb(msg)
}
}
} catch (e) {
console.error('[WhisperSocket] Message parse error:', e)
}
}
socket.onclose = () => {
console.log('[WhisperSocket] Closed, will reconnect...')
socket = null
status.value = 'loading'
scheduleReconnect()
}
socket.onerror = (e) => {
console.error('[WhisperSocket] Error:', e)
status.value = 'loading'
}
}
function scheduleReconnect() {
if (reconnectTimer) return
reconnectTimer = window.setTimeout(() => {
reconnectTimer = null
checkStatusAndConnect()
}, 2000)
}
async function checkStatusAndConnect() {
try {
const res = await fetch('/api/whisper/status')
const data = await res.json()
if (data.running) {
connect()
} else {
status.value = 'loading'
scheduleReconnect()
}
} catch {
status.value = 'loading'
scheduleReconnect()
}
}
// ====== Public API ======
/** Initialize the singleton connection (call once at app startup) */
export function initWhisperSocket() {
checkStatusAndConnect()
}
/** Send audio for transcription */
export function sendAudio(base64: string, language: string, partial: boolean) {
if (socket?.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
type: 'transcribe',
audio: base64,
language,
partial
}))
} else {
console.warn('[WhisperSocket] Not connected, dropping audio')
}
}
/** Subscribe to transcription results. Returns unsubscribe function. */
export function onTranscription(callback: TranscriptionCallback): () => void {
listeners.add(callback)
return () => listeners.delete(callback)
}
/** Get reactive status */
export function getWhisperStatus() {
return status
}
/** Check if socket is connected */
export function isConnected(): boolean {
return socket?.readyState === WebSocket.OPEN
}
/** Force reconnect (e.g. when user toggles Whisper) */
export async function reconnect() {
if (status.value === 'loading' && socket?.readyState === WebSocket.CONNECTING) return
status.value = 'loading'
if (socket) {
socket.close()
socket = null
}
try {
const res = await fetch('/api/whisper/toggle', { method: 'POST' })
const data = await res.json()
if (data.running) {
connect()
} else {
// Poll until ready
const poll = async () => {
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 2000))
try {
const s = await fetch('/api/whisper/status')
const d = await s.json()
if (d.running) {
connect()
return
}
} catch { /* retry */ }
}
status.value = 'offline'
}
poll()
}
} catch {
status.value = 'loading'
scheduleReconnect()
}
}