diff --git a/api/openai-stt.ts b/api/openai-stt.ts new file mode 100644 index 0000000..858236f --- /dev/null +++ b/api/openai-stt.ts @@ -0,0 +1,29 @@ +import OpenAI from 'openai'; + +import { OpenAISTTPayload } from '@/core'; + +import { createOpenaiAudioTranscriptions } from '../src/server/createOpenaiAudioTranscriptions'; + +export const config = { + runtime: 'edge', +}; + +export default async (req: Request) => { + if (req.method !== 'POST') return new Response('Method Not Allowed', { status: 405 }); + + const OPENAI_API_KEY = process.env.OPENAI_API_KEY; + const OPENAI_PROXY_URL = process.env.OPENAI_PROXY_URL; + + if (!OPENAI_API_KEY) return new Response('OPENAI_API_KEY is not set', { status: 500 }); + + const payload = (await req.json()) as OpenAISTTPayload; + + const openai = new OpenAI({ apiKey: OPENAI_API_KEY, baseURL: OPENAI_PROXY_URL }); + const res = await createOpenaiAudioTranscriptions({ openai, payload }); + + return new Response(JSON.stringify(res), { + headers: { + 'content-type': 'application/json;charset=UTF-8', + }, + }); +}; diff --git a/api/openai-tts.ts b/api/openai-tts.ts new file mode 100644 index 0000000..2c13101 --- /dev/null +++ b/api/openai-tts.ts @@ -0,0 +1,23 @@ +import OpenAI from 'openai'; + +import { OpenAITTSPayload } from '@/core'; + +import { createOpenaiAudioSpeech } from '../src/server/createOpenaiAudioSpeech'; + +export const config = { + runtime: 'edge', +}; + +export default async (req: Request) => { + if (req.method !== 'POST') return new Response('Method Not Allowed', { status: 405 }); + const OPENAI_API_KEY = process.env.OPENAI_API_KEY; + const OPENAI_PROXY_URL = process.env.OPENAI_PROXY_URL; + + if (!OPENAI_API_KEY) return new Response('OPENAI_API_KEY is not set', { status: 500 }); + + const payload = (await req.json()) as OpenAITTSPayload; + + const openai = new OpenAI({ apiKey: OPENAI_API_KEY, baseURL: OPENAI_PROXY_URL }); + + return createOpenaiAudioSpeech({ openai, payload }); +}; diff --git a/src/react/hooks/useAudioPlayer.ts b/src/react/hooks/useAudioPlayer.ts index 99c99ec..f0b7d0c 100644 --- a/src/react/hooks/useAudioPlayer.ts +++ b/src/react/hooks/useAudioPlayer.ts @@ -5,13 +5,13 @@ import { arrayBufferConvert } from '@/core/utils/arrayBufferConvert'; import { audioBufferToBlob } from '@/core/utils/audioBufferToBlob'; import { AudioProps } from '@/react/AudioPlayer'; -export interface AudioPlayerHook extends AudioProps { +export interface AudioPlayerReturn extends AudioProps { isLoading?: boolean; ref: RefObject; reset: () => void; } -export const useAudioPlayer = (src: string): AudioPlayerHook => { +export const useAudioPlayer = (src: string): AudioPlayerReturn => { const audioRef = useRef(new Audio()); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); diff --git a/src/react/hooks/useStreamAudioPlayer.ts b/src/react/hooks/useStreamAudioPlayer.ts index a899604..98e815e 100644 --- a/src/react/hooks/useStreamAudioPlayer.ts +++ b/src/react/hooks/useStreamAudioPlayer.ts @@ -3,14 +3,14 @@ import { RefObject, useCallback, useEffect, useRef, useState } from 'react'; import { audioBufferToBlob, audioBuffersToBlob } from '@/core/utils/audioBufferToBlob'; import { AudioProps } from '@/react/AudioPlayer'; -export interface StreamAudioPlayerHook extends AudioProps { +export interface StreamAudioPlayerReturn extends AudioProps { download: () => void; load: (audioBuffer: AudioBuffer) => void; ref: RefObject; reset: () => void; } -export const useStreamAudioPlayer = (): StreamAudioPlayerHook => { +export const useStreamAudioPlayer = (): StreamAudioPlayerReturn => { const audioRef = useRef(new Audio()); const [audioBuffers, setAudioBuffer] = useState([]); const [currentTime, setCurrentTime] = useState(0); diff --git a/src/react/index.ts b/src/react/index.ts index 096d074..1837447 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -1,23 +1,13 @@ export { default as AudioPlayer, type AudioPlayerProps } from './AudioPlayer'; export { default as AudioVisualizer, type AudioVisualizerProps } from './AudioVisualizer'; -export { type AudioPlayerHook, useAudioPlayer } from './hooks/useAudioPlayer'; +export { type AudioPlayerReturn, useAudioPlayer } from './hooks/useAudioPlayer'; export { useAudioVisualizer } from './hooks/useAudioVisualizer'; export { useBlobUrl } from './hooks/useBlobUrl'; export { useStreamAudioPlayer } from './hooks/useStreamAudioPlayer'; export { useAudioRecorder } from './useAudioRecorder'; export { type EdgeSpeechOptions, useEdgeSpeech } from './useEdgeSpeech'; export { type MicrosoftSpeechOptions, useMicrosoftSpeech } from './useMicrosoftSpeech'; -export { - type OpenAISTTConfig, - useOpenaiSTT, - useOpenaiSTTWithPSR, - useOpenaiSTTWithRecord, - useOpenaiSTTWithSR, -} from './useOpenaiSTT'; -export { type OpenAITTSConfig, useOpenaiTTS } from './useOpenaiTTS'; -export { usePersistedSpeechRecognition } from './useSpeechRecognition/usePersistedSpeechRecognition'; -export { - type SpeechRecognitionOptions, - useSpeechRecognition, -} from './useSpeechRecognition/useSpeechRecognition'; +export { type OpenAISTTOptions, useOpenAISTT } from './useOpenAISTT'; +export { type OpenAITTSOptions, useOpenAITTS } from './useOpenAITTS'; +export { type SpeechRecognitionOptions, useSpeechRecognition } from './useSpeechRecognition'; export { type SpeechSynthesOptions, useSpeechSynthes } from './useSpeechSynthes'; diff --git a/src/react/useOpenaiSTT/demos/OpenaiSTTWithSR.tsx b/src/react/useOpenAISTT/demos/AutoStop.tsx similarity index 91% rename from src/react/useOpenaiSTT/demos/OpenaiSTTWithSR.tsx rename to src/react/useOpenAISTT/demos/AutoStop.tsx index 148de84..6416e21 100644 --- a/src/react/useOpenaiSTT/demos/OpenaiSTTWithSR.tsx +++ b/src/react/useOpenAISTT/demos/AutoStop.tsx @@ -1,4 +1,4 @@ -import { useOpenaiSTTWithSR } from '@lobehub/tts/react'; +import { useOpenAISTT } from '@lobehub/tts/react'; import { Icon, StoryBook, useControls, useCreateStore } from '@lobehub/ui'; import { Button, Input } from 'antd'; import { Mic, StopCircle } from 'lucide-react'; @@ -29,10 +29,10 @@ export default () => { { store }, ); - const { text, start, stop, isLoading, isRecording, url, formattedTime } = useOpenaiSTTWithSR( - locale, - { api }, - ); + const { text, start, stop, isLoading, isRecording, url, formattedTime } = useOpenAISTT(locale, { + api, + autoStop: true, + }); return ( diff --git a/src/react/useOpenaiSTT/demos/OpenaiSTTWithPSR.tsx b/src/react/useOpenAISTT/demos/index.tsx similarity index 91% rename from src/react/useOpenaiSTT/demos/OpenaiSTTWithPSR.tsx rename to src/react/useOpenAISTT/demos/index.tsx index 2efa180..6416e21 100644 --- a/src/react/useOpenaiSTT/demos/OpenaiSTTWithPSR.tsx +++ b/src/react/useOpenAISTT/demos/index.tsx @@ -1,4 +1,4 @@ -import { useOpenaiSTTWithPSR } from '@lobehub/tts/react'; +import { useOpenAISTT } from '@lobehub/tts/react'; import { Icon, StoryBook, useControls, useCreateStore } from '@lobehub/ui'; import { Button, Input } from 'antd'; import { Mic, StopCircle } from 'lucide-react'; @@ -29,11 +29,10 @@ export default () => { { store }, ); - const { text, start, stop, isLoading, isRecording, url, formattedTime } = useOpenaiSTTWithPSR( - locale, - { api }, - ); - + const { text, start, stop, isLoading, isRecording, url, formattedTime } = useOpenAISTT(locale, { + api, + autoStop: true, + }); return ( diff --git a/src/react/useOpenAISTT/index.md b/src/react/useOpenAISTT/index.md new file mode 100644 index 0000000..53e56ec --- /dev/null +++ b/src/react/useOpenAISTT/index.md @@ -0,0 +1,15 @@ +--- +nav: Components +group: STT +title: useOpenAISTT +--- + +## hooks + +- ENV: `OPENAI_API_KEY` `OPENAI_PROXY_URL` + + + +## Auto Stop + + diff --git a/src/react/useOpenAISTT/index.ts b/src/react/useOpenAISTT/index.ts new file mode 100644 index 0000000..642f566 --- /dev/null +++ b/src/react/useOpenAISTT/index.ts @@ -0,0 +1,12 @@ +import { useOpenAISTTAutoStop } from './useOpenAISTTAutoStop'; +import { useOpenAISTTInteractive } from './useOpenAISTTInteractive'; +import { OpenAISTTRecorderOptions } from './useOpenAISTTRecorder'; + +export interface OpenAISTTOptions extends OpenAISTTRecorderOptions { + autoStop?: boolean; +} + +export const useOpenAISTT = (locale: string, { autoStop, ...rest }: OpenAISTTOptions = {}) => { + const selectedHook = autoStop ? useOpenAISTTAutoStop : useOpenAISTTInteractive; + return selectedHook(locale, rest); +}; diff --git a/src/react/useOpenaiSTT/useOpenaiSTTWithSR.ts b/src/react/useOpenAISTT/useOpenAISTTAutoStop.ts similarity index 72% rename from src/react/useOpenaiSTT/useOpenaiSTTWithSR.ts rename to src/react/useOpenAISTT/useOpenAISTTAutoStop.ts index 3bd3211..e3b45fb 100644 --- a/src/react/useOpenaiSTT/useOpenaiSTTWithSR.ts +++ b/src/react/useOpenAISTT/useOpenAISTTAutoStop.ts @@ -1,11 +1,11 @@ import { useCallback, useState } from 'react'; -import { useOpenaiSTT } from '@/react/useOpenaiSTT/useOpenaiSTT'; -import { useSpeechRecognition } from '@/react/useSpeechRecognition'; +import { useOpenAISTTCore } from '@/react/useOpenAISTT/useOpenAISTTCore'; +import { useSpeechRecognitionAutoStop } from '@/react/useSpeechRecognition/useSpeechRecognitionAutoStop'; -import { STTConfig } from './useOpenaiSTTWithRecord'; +import { OpenAISTTRecorderOptions } from './useOpenAISTTRecorder'; -export const useOpenaiSTTWithSR = ( +export const useOpenAISTTAutoStop = ( locale: string, { onBlobAvailable, @@ -16,8 +16,12 @@ export const useOpenaiSTTWithSR = ( onStart, onStop, options, + onRecognitionStop, + onRecognitionStart, + onRecognitionError, + onRecognitionFinish, ...restConfig - }: STTConfig = {}, + }: OpenAISTTRecorderOptions = {}, ) => { const [isGlobalLoading, setIsGlobalLoading] = useState(false); const [shouldFetch, setShouldFetch] = useState(false); @@ -30,11 +34,15 @@ export const useOpenaiSTTWithSR = ( isLoading: isRecording, time, formattedTime, - } = useSpeechRecognition(locale, { + } = useSpeechRecognitionAutoStop(locale, { onBlobAvailable: (blobData) => { setShouldFetch(true); onBlobAvailable?.(blobData); }, + onRecognitionError, + onRecognitionFinish, + onRecognitionStart, + onRecognitionStop, onTextChange: (data) => { setText(data); onTextChange?.(data); @@ -55,7 +63,7 @@ export const useOpenaiSTTWithSR = ( setIsGlobalLoading(false); }, [stop]); - const { isLoading } = useOpenaiSTT({ + const { isLoading } = useOpenAISTTCore({ onError: (err, ...rest) => { onError?.(err, ...rest); console.error(err); diff --git a/src/react/useOpenaiSTT/useOpenaiSTT.ts b/src/react/useOpenAISTT/useOpenAISTTCore.ts similarity index 79% rename from src/react/useOpenaiSTT/useOpenaiSTT.ts rename to src/react/useOpenAISTT/useOpenAISTTCore.ts index db197ab..c7a6820 100644 --- a/src/react/useOpenaiSTT/useOpenaiSTT.ts +++ b/src/react/useOpenAISTT/useOpenAISTTCore.ts @@ -2,14 +2,14 @@ import useSWR, { type SWRConfiguration } from 'swr'; import { OpenAISTTPayload, OpenaiSTT } from '@/core/OpenAISTT'; -export interface OpenAISTTConfig extends OpenAISTTPayload, SWRConfiguration { +export interface OpenAISTTCoreOptions extends OpenAISTTPayload, SWRConfiguration { api?: { key: string; url: string; }; shouldFetch?: boolean; } -export const useOpenaiSTT = (config: OpenAISTTConfig) => { +export const useOpenAISTTCore = (config: OpenAISTTCoreOptions) => { const key = new Date().getDate().toString(); const { shouldFetch, api, options, speech, ...swrConfig } = config; diff --git a/src/react/useOpenaiSTT/useOpenaiSTTWithPSR.ts b/src/react/useOpenAISTT/useOpenAISTTInteractive.ts similarity index 72% rename from src/react/useOpenaiSTT/useOpenaiSTTWithPSR.ts rename to src/react/useOpenAISTT/useOpenAISTTInteractive.ts index 73915aa..e33327c 100644 --- a/src/react/useOpenaiSTT/useOpenaiSTTWithPSR.ts +++ b/src/react/useOpenAISTT/useOpenAISTTInteractive.ts @@ -1,11 +1,11 @@ import { useCallback, useState } from 'react'; -import { useOpenaiSTT } from '@/react/useOpenaiSTT/useOpenaiSTT'; -import { usePersistedSpeechRecognition } from '@/react/useSpeechRecognition'; +import { useOpenAISTTCore } from '@/react/useOpenAISTT/useOpenAISTTCore'; +import { useSpeechRecognitionInteractive } from '@/react/useSpeechRecognition/useSpeechRecognitionInteractive'; -import { STTConfig } from './useOpenaiSTTWithRecord'; +import { OpenAISTTRecorderOptions } from './useOpenAISTTRecorder'; -export const useOpenaiSTTWithPSR = ( +export const useOpenAISTTInteractive = ( locale: string, { onBlobAvailable, @@ -16,8 +16,12 @@ export const useOpenaiSTTWithPSR = ( onStart, onStop, options, + onRecognitionStop, + onRecognitionStart, + onRecognitionError, + onRecognitionFinish, ...restConfig - }: STTConfig = {}, + }: OpenAISTTRecorderOptions = {}, ) => { const [isGlobalLoading, setIsGlobalLoading] = useState(false); const [shouldFetch, setShouldFetch] = useState(false); @@ -30,11 +34,15 @@ export const useOpenaiSTTWithPSR = ( isLoading: isRecording, time, formattedTime, - } = usePersistedSpeechRecognition(locale, { + } = useSpeechRecognitionInteractive(locale, { onBlobAvailable: (blobData) => { setShouldFetch(true); onBlobAvailable?.(blobData); }, + onRecognitionError, + onRecognitionFinish, + onRecognitionStart, + onRecognitionStop, onTextChange: (data) => { setText(data); onTextChange?.(data); @@ -55,7 +63,7 @@ export const useOpenaiSTTWithPSR = ( setIsGlobalLoading(false); }, [stop]); - const { isLoading } = useOpenaiSTT({ + const { isLoading } = useOpenAISTTCore({ onError: (err, ...rest) => { onError?.(err, ...rest); console.error(err); diff --git a/src/react/useOpenaiSTT/useOpenaiSTTWithRecord.ts b/src/react/useOpenAISTT/useOpenAISTTRecorder.ts similarity index 76% rename from src/react/useOpenaiSTT/useOpenaiSTTWithRecord.ts rename to src/react/useOpenAISTT/useOpenAISTTRecorder.ts index 295ab61..e896803 100644 --- a/src/react/useOpenaiSTT/useOpenaiSTTWithRecord.ts +++ b/src/react/useOpenAISTT/useOpenAISTTRecorder.ts @@ -2,21 +2,19 @@ import { useCallback, useState } from 'react'; import { SWRConfiguration } from 'swr'; import { useAudioRecorder } from '@/react/useAudioRecorder'; -import { useOpenaiSTT } from '@/react/useOpenaiSTT/useOpenaiSTT'; -import { SpeechRecognitionOptions } from '@/react/useSpeechRecognition/useSpeechRecognition'; +import { useOpenAISTTCore } from '@/react/useOpenAISTT/useOpenAISTTCore'; +import { SpeechRecognitionRecorderOptions } from '@/react/useSpeechRecognition/useSpeechRecognitionAutoStop'; -import { OpenAISTTConfig } from './useOpenaiSTT'; +import { OpenAISTTCoreOptions } from './useOpenAISTTCore'; -export interface STTConfig - extends SpeechRecognitionOptions, +export interface OpenAISTTRecorderOptions + extends SpeechRecognitionRecorderOptions, SWRConfiguration, - Partial { + Partial { onFinished?: SWRConfiguration['onSuccess']; - onStart?: () => void; - onStop?: () => void; } -export const useOpenaiSTTWithRecord = ({ +export const useOpenAISTTRecorder = ({ onBlobAvailable, onTextChange, onSuccess, @@ -26,7 +24,7 @@ export const useOpenaiSTTWithRecord = ({ onStop, options, ...restConfig -}: STTConfig = {}) => { +}: OpenAISTTRecorderOptions = {}) => { const [isGlobalLoading, setIsGlobalLoading] = useState(false); const [shouldFetch, setShouldFetch] = useState(false); const [text, setText] = useState(); @@ -51,7 +49,7 @@ export const useOpenaiSTTWithRecord = ({ setIsGlobalLoading(false); }, [stop]); - const { isLoading } = useOpenaiSTT({ + const { isLoading } = useOpenAISTTCore({ onError: (err, ...rest) => { onError?.(err, ...rest); console.error(err); diff --git a/src/react/useOpenaiTTS/demos/index.tsx b/src/react/useOpenAITTS/demos/index.tsx similarity index 94% rename from src/react/useOpenaiTTS/demos/index.tsx rename to src/react/useOpenAITTS/demos/index.tsx index 331da88..ff6f23e 100644 --- a/src/react/useOpenaiTTS/demos/index.tsx +++ b/src/react/useOpenAITTS/demos/index.tsx @@ -1,5 +1,5 @@ import { OpenAITTS } from '@lobehub/tts'; -import { AudioPlayer, useOpenaiTTS } from '@lobehub/tts/react'; +import { AudioPlayer, useOpenAITTS } from '@lobehub/tts/react'; import { Icon, StoryBook, useControls, useCreateStore } from '@lobehub/ui'; import { Button, Input } from 'antd'; import { Volume2 } from 'lucide-react'; @@ -35,7 +35,7 @@ export default () => { }, { store }, ); - const { setText, isGlobalLoading, audio, start, stop } = useOpenaiTTS(defaultText, { + const { setText, isGlobalLoading, audio, start, stop } = useOpenAITTS(defaultText, { api, options, }); diff --git a/src/react/useOpenaiTTS/index.md b/src/react/useOpenAITTS/index.md similarity index 87% rename from src/react/useOpenaiTTS/index.md rename to src/react/useOpenAITTS/index.md index 4528953..01ea339 100644 --- a/src/react/useOpenaiTTS/index.md +++ b/src/react/useOpenAITTS/index.md @@ -1,7 +1,7 @@ --- nav: Components group: TTS -title: useOpenaiTTS +title: useOpenAITTS --- ## hooks diff --git a/src/react/useOpenaiTTS/index.ts b/src/react/useOpenAITTS/index.ts similarity index 78% rename from src/react/useOpenaiTTS/index.ts rename to src/react/useOpenAITTS/index.ts index 3415732..0ea649b 100644 --- a/src/react/useOpenaiTTS/index.ts +++ b/src/react/useOpenAITTS/index.ts @@ -3,14 +3,14 @@ import { useState } from 'react'; import { OpenAITTS, type OpenAITTSPayload } from '@/core/OpenAITTS'; import { TTSConfig, useTTS } from '@/react/useTTS'; -export interface OpenAITTSConfig extends Pick, TTSConfig { +export interface OpenAITTSOptions extends Pick, TTSConfig { api?: { key?: string; proxy?: string; }; } -export const useOpenaiTTS = (defaultText: string, config: OpenAITTSConfig) => { +export const useOpenAITTS = (defaultText: string, config: OpenAITTSOptions) => { const [text, setText] = useState(defaultText); const { options, api, ...swrConfig } = config; const rest = useTTS( diff --git a/src/react/useOpenaiSTT/demos/index.tsx b/src/react/useOpenaiSTT/demos/index.tsx deleted file mode 100644 index ef1ac66..0000000 --- a/src/react/useOpenaiSTT/demos/index.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { useOpenaiSTTWithRecord } from '@lobehub/tts/react'; -import { Icon, StoryBook, useControls, useCreateStore } from '@lobehub/ui'; -import { Button, Input } from 'antd'; -import { Mic, StopCircle } from 'lucide-react'; -import { Flexbox } from 'react-layout-kit'; - -import { OPENAI_BASE_URL } from '@/core/const/api'; - -export default () => { - const store = useCreateStore(); - const api: any = useControls( - { - key: { - label: 'OPENAI_API_KEY', - value: '', - }, - proxy: { - label: 'OPENAI_PROXY_URL', - value: OPENAI_BASE_URL, - }, - }, - { store }, - ); - - const { text, start, stop, isLoading, isRecording, url, formattedTime } = useOpenaiSTTWithRecord({ - api, - }); - - return ( - - - {isRecording ? ( - - ) : isLoading ? ( - - ) : ( - - )} - - {url && - - ); -}; diff --git a/src/react/useOpenaiSTT/index.md b/src/react/useOpenaiSTT/index.md deleted file mode 100644 index e3e0222..0000000 --- a/src/react/useOpenaiSTT/index.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -nav: Components -group: STT -title: useOpenaiSTT ---- - -## hooks - -- ENV: `OPENAI_API_KEY` `OPENAI_PROXY_URL` - - - -## useOpenaiSTTWithSR - -use `OpenaiSTT` with `SpeechRecognition` - - - -## useOpenaiSTTWithPSR - -use `OpenaiSTT` with `PersistedSpeechRecognition` - - diff --git a/src/react/useOpenaiSTT/index.ts b/src/react/useOpenaiSTT/index.ts deleted file mode 100644 index d40bbd9..0000000 --- a/src/react/useOpenaiSTT/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { type OpenAISTTConfig, useOpenaiSTT } from './useOpenaiSTT'; -export { useOpenaiSTTWithPSR } from './useOpenaiSTTWithPSR'; -export { type STTConfig, useOpenaiSTTWithRecord } from './useOpenaiSTTWithRecord'; -export { useOpenaiSTTWithSR } from './useOpenaiSTTWithSR'; diff --git a/src/react/useSpeechRecognition/demos/PersistedSpeechRecognition.tsx b/src/react/useSpeechRecognition/demos/AutoStop.tsx similarity index 83% rename from src/react/useSpeechRecognition/demos/PersistedSpeechRecognition.tsx rename to src/react/useSpeechRecognition/demos/AutoStop.tsx index 57cf7c6..1fc5789 100644 --- a/src/react/useSpeechRecognition/demos/PersistedSpeechRecognition.tsx +++ b/src/react/useSpeechRecognition/demos/AutoStop.tsx @@ -1,4 +1,4 @@ -import { usePersistedSpeechRecognition } from '@lobehub/tts/react'; +import { useSpeechRecognition } from '@lobehub/tts/react'; import { Icon, StoryBook, useControls, useCreateStore } from '@lobehub/ui'; import { Button, Input } from 'antd'; import { Mic, StopCircle } from 'lucide-react'; @@ -13,8 +13,9 @@ export default () => { { store }, ); - const { text, start, stop, isLoading, formattedTime, url } = - usePersistedSpeechRecognition(locale); + const { text, start, stop, isLoading, formattedTime, url } = useSpeechRecognition(locale, { + autoStop: true, + }); return ( diff --git a/src/react/useSpeechRecognition/index.md b/src/react/useSpeechRecognition/index.md index 6f7e29e..c8bc7c2 100644 --- a/src/react/useSpeechRecognition/index.md +++ b/src/react/useSpeechRecognition/index.md @@ -8,6 +8,6 @@ title: useSpeechRecognition -## usePersistedSpeechRecognition +## Auto Stop - + diff --git a/src/react/useSpeechRecognition/index.ts b/src/react/useSpeechRecognition/index.ts index d18b19a..0c84413 100644 --- a/src/react/useSpeechRecognition/index.ts +++ b/src/react/useSpeechRecognition/index.ts @@ -1,2 +1,17 @@ -export { usePersistedSpeechRecognition } from './usePersistedSpeechRecognition'; -export { useSpeechRecognition } from './useSpeechRecognition'; +import { + SpeechRecognitionRecorderOptions, + useSpeechRecognitionAutoStop, +} from './useSpeechRecognitionAutoStop'; +import { useSpeechRecognitionInteractive } from './useSpeechRecognitionInteractive'; + +export interface SpeechRecognitionOptions extends SpeechRecognitionRecorderOptions { + autoStop?: boolean; +} + +export const useSpeechRecognition = ( + locale: string, + { autoStop, ...rest }: SpeechRecognitionOptions = {}, +) => { + const selectedHook = autoStop ? useSpeechRecognitionAutoStop : useSpeechRecognitionInteractive; + return selectedHook(locale, rest); +}; diff --git a/src/react/useSpeechRecognition/useSpeechRecognition.ts b/src/react/useSpeechRecognition/useSpeechRecognitionAutoStop.ts similarity index 52% rename from src/react/useSpeechRecognition/useSpeechRecognition.ts rename to src/react/useSpeechRecognition/useSpeechRecognitionAutoStop.ts index e626f3a..442a036 100644 --- a/src/react/useSpeechRecognition/useSpeechRecognition.ts +++ b/src/react/useSpeechRecognition/useSpeechRecognitionAutoStop.ts @@ -1,16 +1,24 @@ import { useCallback } from 'react'; import { useAudioRecorder } from '@/react/useAudioRecorder'; -import { useRecognition } from '@/react/useSpeechRecognition/useRecognition'; -export interface SpeechRecognitionOptions { +import { SpeechRecognitionCoreOptions, useSpeechRecognitionCore } from './useSpeechRecognitionCore'; + +export interface SpeechRecognitionRecorderOptions extends SpeechRecognitionCoreOptions { onBlobAvailable?: (blob: Blob) => void; - onTextChange?: (value: string) => void; + onStart?: () => void; + onStop?: () => void; } -export const useSpeechRecognition = ( +export const useSpeechRecognitionAutoStop = ( locale: string, - { onBlobAvailable, onTextChange }: SpeechRecognitionOptions = {}, + { + onStart, + onStop, + onBlobAvailable, + onRecognitionFinish, + ...rest + }: SpeechRecognitionRecorderOptions = {}, ) => { const { time, @@ -20,19 +28,22 @@ export const useSpeechRecognition = ( blob, url, } = useAudioRecorder(onBlobAvailable); - const { isLoading, start, stop, text } = useRecognition(locale, { - onRecognitionEnd: () => { + const { isLoading, start, stop, text } = useSpeechRecognitionCore(locale, { + onRecognitionFinish: (data) => { + onRecognitionFinish?.(data); stopRecord(); }, - onTextChange: onTextChange, + ...rest, }); const handleStart = useCallback(() => { + onStart?.(); start(); startRecord(); }, [start, startRecord]); const handleStop = useCallback(() => { + onStop?.(); stop(); stopRecord(); }, [stop, stopRecord]); diff --git a/src/react/useSpeechRecognition/useRecognition.ts b/src/react/useSpeechRecognition/useSpeechRecognitionCore.ts similarity index 57% rename from src/react/useSpeechRecognition/useRecognition.ts rename to src/react/useSpeechRecognition/useSpeechRecognitionCore.ts index de2dc49..3917a4f 100644 --- a/src/react/useSpeechRecognition/useRecognition.ts +++ b/src/react/useSpeechRecognition/useSpeechRecognitionCore.ts @@ -2,17 +2,28 @@ import { useCallback, useEffect, useState } from 'react'; import { SpeechRecognition } from '@/core/const/polyfill'; -export const useRecognition = ( +export interface SpeechRecognitionCoreOptions { + onRecognitionError?: (error: any) => void; + onRecognitionFinish?: (value: string) => void; + onRecognitionStart?: () => void; + onRecognitionStop?: () => void; + onTextChange?: (value: string) => void; +} + +export const useSpeechRecognitionCore = ( locale: string, - options?: { - onRecognitionEnd?: () => void; - onTextChange?: (value: string) => void; - }, + { + onTextChange, + onRecognitionStart, + onRecognitionFinish, + onRecognitionStop, + onRecognitionError, + }: SpeechRecognitionCoreOptions = {}, ) => { const [recognition, setRecognition] = useState(null); const [text, setText] = useState(''); const [isLoading, setIsLoading] = useState(false); - const [finalStop, setFinalStop] = useState(false); + const [isFinalStop, setFinalStop] = useState(false); useEffect(() => { if (recognition) return; @@ -32,49 +43,57 @@ export const useRecognition = ( speechRecognition.onresult = ({ results }: any) => { if (!results) return; const result = results[0]; - if (!finalStop && result?.[0]?.transcript) { + if (!isFinalStop && result?.[0]?.transcript) { const value = result[0].transcript; setText(value); - options?.onTextChange?.(value); + onTextChange?.(value); } if (result.isFinal) { speechRecognition.abort(); - setIsLoading(false); } }; setRecognition(speechRecognition); } catch (error) { console.error(error); + onRecognitionError?.(error); } - }, [options]); + }, [isFinalStop]); useEffect(() => { - if (!isLoading) { - options?.onRecognitionEnd?.(); + if (!isLoading && text) { + onRecognitionFinish?.(text); } - }, [isLoading, options]); + }, [text, isLoading]); useEffect(() => { if (recognition) recognition.lang = locale; - }, [locale, recognition]); + }, [recognition, locale]); const handleStart = useCallback(() => { setText(''); - options?.onTextChange?.(''); + onTextChange?.(''); try { recognition.start(); - } catch {} - }, [options, recognition]); + onRecognitionStart?.(); + } catch (error) { + console.error('handleStart', error); + onRecognitionError?.(error); + } + }, [recognition]); const handleStop = useCallback(() => { try { recognition.abort(); - } catch {} + onRecognitionStop?.(); + } catch (error) { + console.error(error); + onRecognitionError?.(error); + } setIsLoading(false); }, [recognition]); return { - isLoading, + isLoading: isLoading, start: handleStart, stop: handleStop, text, diff --git a/src/react/useSpeechRecognition/usePersistedSpeechRecognition.ts b/src/react/useSpeechRecognition/useSpeechRecognitionInteractive.ts similarity index 61% rename from src/react/useSpeechRecognition/usePersistedSpeechRecognition.ts rename to src/react/useSpeechRecognition/useSpeechRecognitionInteractive.ts index 048b98c..fba0553 100644 --- a/src/react/useSpeechRecognition/usePersistedSpeechRecognition.ts +++ b/src/react/useSpeechRecognition/useSpeechRecognitionInteractive.ts @@ -1,13 +1,20 @@ import { useCallback, useEffect, useState } from 'react'; import { useAudioRecorder } from '@/react/useAudioRecorder'; +import { SpeechRecognitionRecorderOptions } from '@/react/useSpeechRecognition/useSpeechRecognitionAutoStop'; -import { useRecognition } from './useRecognition'; -import { SpeechRecognitionOptions } from './useSpeechRecognition'; +import { useSpeechRecognitionCore } from './useSpeechRecognitionCore'; -export const usePersistedSpeechRecognition = ( +export const useSpeechRecognitionInteractive = ( locale: string, - { onBlobAvailable, onTextChange }: SpeechRecognitionOptions = {}, + { + onBlobAvailable, + onTextChange, + onRecognitionFinish, + onStop, + onStart, + ...rest + }: SpeechRecognitionRecorderOptions = {}, ) => { const [resultText, setResultText] = useState(); const [texts, setTexts] = useState([]); @@ -20,16 +27,18 @@ export const usePersistedSpeechRecognition = ( blob, url, } = useAudioRecorder(onBlobAvailable); - const { text, stop, start, isLoading } = useRecognition(locale, { - onRecognitionEnd: () => { + const { text, stop, start, isLoading } = useSpeechRecognitionCore(locale, { + onRecognitionFinish: (data) => { if (isGLobalLoading && !isLoading) { - if (text) setTexts([...texts, text]); + if (data) setTexts([...texts, data]); start(); } }, + ...rest, }); const handleStart = useCallback(() => { + onStart?.(); setTexts([]); setIsGlobalLoading(true); start(); @@ -37,16 +46,20 @@ export const usePersistedSpeechRecognition = ( }, [start, startRecord]); const handleStop = useCallback(() => { + onStop?.(); stop(); stopRecord(); setIsGlobalLoading(false); - }, [stop, stopRecord]); + if (resultText) { + onRecognitionFinish?.(resultText); + } + }, [stop, stopRecord, resultText]); useEffect(() => { const mergedText = [...texts, text].filter(Boolean).join(' '); setResultText(mergedText); onTextChange?.(mergedText); - }, [texts, text, onTextChange]); + }, [texts, text]); return { blob,