Files
opencode-mobile/src/lib/speech.ts

81 lines
2.0 KiB
TypeScript

import { useState, useCallback, useRef } from "react"
import { ExpoSpeechRecognitionModule, useSpeechRecognitionEvent } from "expo-speech-recognition"
interface SpeechState {
listening: boolean
transcript: string
error: string | null
}
interface SpeechActions {
start: () => Promise<void>
stop: () => void
cancel: () => void
}
export function useSpeech(onResult: (text: string) => void): SpeechState & SpeechActions {
const [listening, setListening] = useState(false)
const [transcript, setTranscript] = useState("")
const [error, setError] = useState<string | null>(null)
const pending = useRef("")
useSpeechRecognitionEvent("start", () => {
setListening(true)
setError(null)
setTranscript("")
pending.current = ""
})
useSpeechRecognitionEvent("end", () => {
setListening(false)
// Deliver final transcript
if (pending.current.trim()) {
onResult(pending.current.trim())
}
setTranscript("")
pending.current = ""
})
useSpeechRecognitionEvent("result", (event) => {
const text = event.results[0]?.transcript || ""
pending.current = text
setTranscript(text)
})
useSpeechRecognitionEvent("error", (event) => {
// "no-speech" is not really an error — user just didn't say anything
if (event.error === "no-speech") {
setListening(false)
return
}
setError(event.message || event.error)
setListening(false)
})
const start = useCallback(async () => {
const result = await ExpoSpeechRecognitionModule.requestPermissionsAsync()
if (!result.granted) {
setError("Microphone permission denied")
return
}
ExpoSpeechRecognitionModule.start({
lang: "en-US",
interimResults: true,
continuous: true,
})
}, [])
const stop = useCallback(() => {
ExpoSpeechRecognitionModule.stop()
}, [])
const cancel = useCallback(() => {
pending.current = ""
ExpoSpeechRecognitionModule.abort()
setListening(false)
setTranscript("")
}, [])
return { listening, transcript, error, start, stop, cancel }
}