sinProject-Inc/talk

View on GitHub
src/lib/speech/web_speech_recognition.ts

Summary

Maintainability
A
25 mins
Test Coverage
import type { LocaleCode } from '$lib/locale/locale_code'
import type { SpeechElement } from './speech_element'
import { TextContent } from './text_content'

// NOTE: TypeScriptでSpeechRecognitionの型をきちんと書く https://qiita.com/akkadaska/items/9c1781052038db444182

interface ISpeechRecognitionEvent {
    isTrusted?: boolean
    results: {
        isFinal: boolean
        [key: number]:
            | undefined
            | {
                    transcript: string
              }
    }[]
}
interface ISpeechRecognition extends EventTarget {
    lang: string
    continuous: boolean
    onend: (() => void) | undefined
    interimResults: boolean
    onresult: (event: ISpeechRecognitionEvent) => void

    abort(): void
    start(): void
    stop(): void
}

interface ISpeechRecognitionConstructor {
    new (): ISpeechRecognition
}

interface IWindow extends Window {
    // eslint-disable-next-line @typescript-eslint/naming-convention
    SpeechRecognition: ISpeechRecognitionConstructor
    webkitSpeechRecognition: ISpeechRecognitionConstructor
}

declare const window: IWindow

export class WebSpeechRecognition {
    private readonly _recognition: ISpeechRecognition
    private readonly _is_android: boolean
    private _final_transcript = ''

    public constructor(
        private readonly _locale_code: LocaleCode,
        private readonly _speech_element: SpeechElement,
        private readonly _on_end_callback?: () => void
    ) {
        if (!('webkitSpeechRecognition' in window)) {
            this._speech_element.text_content = new TextContent('Speech Recognition Not Available')
            throw new Error('Speech Recognition Not Available')
        }

        const speech_recognition = window.SpeechRecognition || window.webkitSpeechRecognition

        this._recognition = new speech_recognition()

        this._recognition.lang = this._locale_code.code
        this._recognition.onend = this._on_end_callback

        this._is_android = window.navigator.userAgent.toLowerCase().includes('android')
        this._recognition.interimResults = !this._is_android
    }

    private _set_on_result(): void {
        this._final_transcript = ''

        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        this._recognition.onresult = (event: any): void => {
            let interim_transcript = ''

            for (let i = event.resultIndex; i < event.results.length; i++) {
                const transcript = event.results[i][0].transcript

                if (event.results[i].isFinal) {
                    this._final_transcript += transcript
                } else {
                    interim_transcript = transcript
                }

                this._speech_element.text_content = new TextContent(
                    this._final_transcript + interim_transcript
                )
            }
        }
    }

    private _start(continuous: boolean): void {
        this._set_on_result()

        this._recognition.continuous = continuous

        this._speech_element.show_hint()
        this._recognition.start()
    }

    public start_not_continuous(): void {
        this._start(false)
    }

    public start_continuous(): void {
        this._start(true)
    }

    public stop(): void {
        this._recognition.stop()
    }
}