mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
Add realtime dictation providers (#1329)
* feat: add realtime dictation providers * fix: cancel realtime dictation startup * fix: refresh local dictation availability * fix: preserve dictation on disconnect * fix: normalize OpenAI language hints
This commit is contained in:
@@ -1,21 +1,39 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type {
|
||||
TranscriptionMode,
|
||||
TranscriptionProvider,
|
||||
TranscriptionProviderInfo,
|
||||
VoiceMode
|
||||
import {
|
||||
BROWSER_LOCAL_TRANSCRIPTION_PROVIDER,
|
||||
type TranscriptionMode,
|
||||
type TranscriptionProvider,
|
||||
type TranscriptionProviderInfo,
|
||||
type VoiceMode
|
||||
} from '@hapi/protocol/voice'
|
||||
|
||||
const VOICE_MODE_KEY = 'hapi-voice-mode'
|
||||
const TRANSCRIPTION_PROVIDER_KEY = 'hapi-transcription-provider'
|
||||
const TRANSCRIPTION_MODE_KEY = 'hapi-transcription-mode'
|
||||
const CHANGE_EVENT = 'hapi-voice-input-change'
|
||||
export const VOICE_LANGUAGE_CHANGE_EVENT = 'hapi-voice-language-change'
|
||||
|
||||
function notifyChange(): void {
|
||||
window.dispatchEvent(new Event(CHANGE_EVENT))
|
||||
}
|
||||
|
||||
async function browserLocalTranscriptionSupported(): Promise<boolean> {
|
||||
const constructor = (globalThis as typeof globalThis & {
|
||||
SpeechRecognition?: {
|
||||
prototype: object
|
||||
available?: (options: { langs: string[]; processLocally: true }) => Promise<string>
|
||||
}
|
||||
}).SpeechRecognition
|
||||
if (!constructor || typeof constructor.available !== 'function' || !('processLocally' in constructor.prototype)) return false
|
||||
const language = localStorage.getItem('hapi-voice-lang') || navigator.language
|
||||
try {
|
||||
return await constructor.available({ langs: [language], processLocally: true }) === 'available'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function readVoiceMode(): VoiceMode {
|
||||
return localStorage.getItem(VOICE_MODE_KEY) === 'dictation' ? 'dictation' : 'assistant'
|
||||
}
|
||||
@@ -33,7 +51,8 @@ function resolveMode(
|
||||
stored: string | null
|
||||
): TranscriptionMode {
|
||||
const modes = providers.find((candidate) => candidate.id === provider)?.modes ?? ['standard']
|
||||
return stored === 'realtime' && modes.includes('realtime') ? 'realtime' : 'standard'
|
||||
if ((stored === 'standard' || stored === 'realtime') && modes.includes(stored)) return stored
|
||||
return modes[0] ?? 'standard'
|
||||
}
|
||||
|
||||
export function useVoiceInputPreferences(api: ApiClient | null) {
|
||||
@@ -45,16 +64,28 @@ export function useVoiceInputPreferences(api: ApiClient | null) {
|
||||
useEffect(() => {
|
||||
if (!api) return
|
||||
let cancelled = false
|
||||
api.fetchTranscriptionProviders().then(({ providers: available }) => {
|
||||
if (cancelled) return
|
||||
setProviders(available)
|
||||
const selectedProvider = resolveProvider(available, localStorage.getItem(TRANSCRIPTION_PROVIDER_KEY))
|
||||
setProviderState(selectedProvider)
|
||||
setTranscriptionModeState(resolveMode(available, selectedProvider, localStorage.getItem(TRANSCRIPTION_MODE_KEY)))
|
||||
}).catch(() => {
|
||||
if (!cancelled) setProviders([])
|
||||
})
|
||||
return () => { cancelled = true }
|
||||
let request = 0
|
||||
const refreshProviders = () => {
|
||||
const current = ++request
|
||||
Promise.all([api.fetchTranscriptionProviders(), browserLocalTranscriptionSupported()]).then(([{ providers: configured }, browserLocal]) => {
|
||||
if (cancelled || current !== request) return
|
||||
const available = browserLocal
|
||||
? [...configured, BROWSER_LOCAL_TRANSCRIPTION_PROVIDER]
|
||||
: configured
|
||||
setProviders(available)
|
||||
const selectedProvider = resolveProvider(available, localStorage.getItem(TRANSCRIPTION_PROVIDER_KEY))
|
||||
setProviderState(selectedProvider)
|
||||
setTranscriptionModeState(resolveMode(available, selectedProvider, localStorage.getItem(TRANSCRIPTION_MODE_KEY)))
|
||||
}).catch(() => {
|
||||
if (!cancelled && current === request) setProviders([])
|
||||
})
|
||||
}
|
||||
refreshProviders()
|
||||
window.addEventListener(VOICE_LANGUAGE_CHANGE_EVENT, refreshProviders)
|
||||
return () => {
|
||||
cancelled = true
|
||||
window.removeEventListener(VOICE_LANGUAGE_CHANGE_EVENT, refreshProviders)
|
||||
}
|
||||
}, [api])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user