feat: add streaming for stt
This commit is contained in:
@@ -1,2 +1,4 @@
|
||||
export { useNonStreamTTS } from './useNonStreamTTS';
|
||||
export { useStreamTTS } from './useStreamTTS';
|
||||
export { useNonStreamSTT } from './use-non-stream-stt';
|
||||
export { useNonStreamTTS } from './use-non-stream-tts';
|
||||
export { useStreamSTT } from './use-stream-stt';
|
||||
export { useStreamTTS } from './use-stream-tts';
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { speechToText } from '../../apis';
|
||||
import { extractErrorMessage } from '../../config';
|
||||
|
||||
interface UseNonStreamSTTParams {
|
||||
onSuccess?: (result: { text: string }) => void;
|
||||
onError?: (error: any) => void;
|
||||
}
|
||||
|
||||
interface STTParams {
|
||||
model: string;
|
||||
language?: string;
|
||||
file: File;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const useNonStreamSTT = (params?: UseNonStreamSTTParams) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<any>(null);
|
||||
const cancelTokenRef = useRef<any>(null);
|
||||
|
||||
const generate = useCallback(
|
||||
async (sttParams: STTParams, cancelToken?: any) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
cancelTokenRef.current = cancelToken;
|
||||
|
||||
const result: any = await speechToText(
|
||||
{
|
||||
data: sttParams
|
||||
},
|
||||
{
|
||||
cancelToken
|
||||
}
|
||||
);
|
||||
|
||||
if (
|
||||
(result?.status_code && result?.status_code !== 200) ||
|
||||
result?.error
|
||||
) {
|
||||
const errorMessage = extractErrorMessage(result);
|
||||
setError({
|
||||
error: true,
|
||||
errorMessage
|
||||
});
|
||||
params?.onError?.(errorMessage);
|
||||
return null;
|
||||
}
|
||||
|
||||
params?.onSuccess?.(result);
|
||||
return result;
|
||||
} catch (err: any) {
|
||||
const res = err?.response?.data;
|
||||
if (res?.error || (res?.status_code && res?.status_code !== 200)) {
|
||||
const errorMessage = extractErrorMessage(res);
|
||||
setError({
|
||||
error: true,
|
||||
errorMessage
|
||||
});
|
||||
params?.onError?.(errorMessage);
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[params]
|
||||
);
|
||||
|
||||
return {
|
||||
generate,
|
||||
loading,
|
||||
error
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
fetchChunkedDataPostFormData,
|
||||
readStreamData
|
||||
} from '@/utils/fetch-chunk-data';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { AUDIO_SPEECH_TO_TEXT_API } from '../../apis';
|
||||
import { extractErrorMessage } from '../../config';
|
||||
|
||||
interface UseStreamSTTParams {
|
||||
onChunk?: (text: string) => void;
|
||||
onComplete?: (fullText: string) => void;
|
||||
onError?: (error: any) => void;
|
||||
}
|
||||
|
||||
interface STTParams {
|
||||
model: string;
|
||||
language?: string;
|
||||
file: File;
|
||||
stream?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const useStreamSTT = (params?: UseStreamSTTParams) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<any>(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const controllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const generate = useCallback(
|
||||
async (sttParams: STTParams) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setProgress(0);
|
||||
|
||||
// Abort previous request if exists
|
||||
controllerRef.current?.abort();
|
||||
controllerRef.current = new AbortController();
|
||||
const signal = controllerRef.current.signal;
|
||||
|
||||
// Add stream parameter
|
||||
const streamParams = {
|
||||
...sttParams,
|
||||
stream: true
|
||||
};
|
||||
|
||||
const result = await fetchChunkedDataPostFormData({
|
||||
url: AUDIO_SPEECH_TO_TEXT_API,
|
||||
data: streamParams,
|
||||
signal
|
||||
});
|
||||
|
||||
if ('error' in result) {
|
||||
const errorMessage = extractErrorMessage(result.data);
|
||||
setError({
|
||||
error: true,
|
||||
errorMessage
|
||||
});
|
||||
params?.onError?.(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const { reader, decoder } = result;
|
||||
|
||||
if (!reader || !decoder) {
|
||||
throw new Error('Failed to get reader from response');
|
||||
}
|
||||
|
||||
// Collect all text chunks
|
||||
let fullText = '';
|
||||
|
||||
await readStreamData(
|
||||
reader,
|
||||
decoder,
|
||||
(chunks: any[]) => {
|
||||
chunks.forEach((chunk) => {
|
||||
if (chunk.error) {
|
||||
const errorMessage = extractErrorMessage(chunk.error);
|
||||
setError({
|
||||
error: true,
|
||||
errorMessage
|
||||
});
|
||||
params?.onError?.(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// STT stream response format: { text: "..." }
|
||||
if (chunk.text) {
|
||||
fullText += chunk.text;
|
||||
params?.onChunk?.(fullText);
|
||||
setProgress((prev) => prev + 1);
|
||||
}
|
||||
});
|
||||
},
|
||||
100 // throttle delay
|
||||
);
|
||||
|
||||
// Stream completed
|
||||
params?.onComplete?.(fullText);
|
||||
} catch (err: any) {
|
||||
if (err.name === 'AbortError') {
|
||||
console.log('Stream aborted');
|
||||
return;
|
||||
}
|
||||
|
||||
const errorMessage = err?.message || 'Stream processing failed';
|
||||
setError({
|
||||
error: true,
|
||||
errorMessage
|
||||
});
|
||||
params?.onError?.(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[params]
|
||||
);
|
||||
|
||||
const abort = useCallback(() => {
|
||||
controllerRef.current?.abort();
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
generate,
|
||||
abort,
|
||||
loading,
|
||||
error,
|
||||
progress
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user