feat: add streaming for stt

This commit is contained in:
jialin
2026-03-17 16:35:55 +08:00
committed by jialin
parent 39bcc158bb
commit 49a60c25af
12 changed files with 311 additions and 42 deletions
+4 -2
View File
@@ -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
};
};