chat whisper

This commit is contained in:
2026-02-18 03:17:21 +00:00
parent 826904d74f
commit fee9682fca
2 changed files with 218 additions and 7 deletions
@@ -9,6 +9,7 @@ import {
FileType2,
Maximize2,
Minimize2,
Square,
Play,
Pause,
Volume2,
@@ -28,6 +29,7 @@ import { cardStyle } from '@/components/Card';
import { useFiles } from 'widgets/FileBrowser';
import { getHeaders } from 'hooks/useClient';
import { config } from 'config';
import { toast } from 'sonner';
import { getIcon } from 'material-file-icons';
type FileViewerProps = {
@@ -922,6 +924,10 @@ export const FileViewer = ({ open, onOpenChange, filePath, fileName, root }: Fil
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [expanded, setExpanded] = useState(false);
const [ttsLoading, setTtsLoading] = useState(false);
const [ttsPlaying, setTtsPlaying] = useState(false);
const ttsAudioRef = useRef<HTMLAudioElement | null>(null);
const ttsUrlRef = useRef<string | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const files = useFiles(root);
const fileType = getFileType(fileName);
@@ -931,6 +937,16 @@ export const FileViewer = ({ open, onOpenChange, filePath, fileName, root }: Fil
setContent(null);
setError(null);
setExpanded(false);
if (ttsAudioRef.current) {
ttsAudioRef.current.pause();
ttsAudioRef.current = null;
}
if (ttsUrlRef.current) {
URL.revokeObjectURL(ttsUrlRef.current);
ttsUrlRef.current = null;
}
setTtsLoading(false);
setTtsPlaying(false);
return;
}
@@ -957,6 +973,54 @@ export const FileViewer = ({ open, onOpenChange, filePath, fileName, root }: Fil
a.click();
};
const handleReadAloud = async () => {
if (ttsPlaying) {
if (ttsAudioRef.current) {
ttsAudioRef.current.pause();
ttsAudioRef.current = null;
}
if (ttsUrlRef.current) {
URL.revokeObjectURL(ttsUrlRef.current);
ttsUrlRef.current = null;
}
setTtsPlaying(false);
return;
}
setTtsLoading(true);
try {
const res = await fetch('http://alpha:9051/v1/audio/speech', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'kokoro', input: content, voice: 'af_heart', response_format: 'mp3' }),
});
if (!res.ok) throw new Error('TTS request failed');
const blob = await res.blob();
const url = URL.createObjectURL(blob);
ttsUrlRef.current = url;
const audio = new Audio(url);
ttsAudioRef.current = audio;
audio.addEventListener('ended', () => {
setTtsPlaying(false);
ttsAudioRef.current = null;
URL.revokeObjectURL(url);
ttsUrlRef.current = null;
});
await audio.play();
setTtsPlaying(true);
} catch {
toast.error('Failed to generate speech audio');
if (ttsUrlRef.current) {
URL.revokeObjectURL(ttsUrlRef.current);
ttsUrlRef.current = null;
}
ttsAudioRef.current = null;
setTtsPlaying(false);
} finally {
setTtsLoading(false);
}
};
const headerIcon =
fileType === 'audio' ? (
<Music className="h-4 w-4 text-duck-teal shrink-0" />
@@ -1019,6 +1083,22 @@ export const FileViewer = ({ open, onOpenChange, filePath, fileName, root }: Fil
>
{fileType === 'code' ? getLang(fileName) : fileType}
</span>
{(fileType === 'markdown' || fileType === 'code' || fileType === 'text') && content && (
<button
onClick={handleReadAloud}
disabled={ttsLoading}
className="p-1.5 rounded-md text-duck-dark/40 hover:text-duck-dark hover:bg-duck-dark/5 disabled:opacity-40 transition-colors cursor-pointer"
title="Read Aloud"
>
{ttsLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : ttsPlaying ? (
<Square className="h-4 w-4" />
) : (
<Volume2 className="h-4 w-4" />
)}
</button>
)}
<button
onClick={handleDownload}
className={`p-1.5 rounded-md transition-colors cursor-pointer ${