chat whisper
This commit is contained in:
@@ -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 ${
|
||||
|
||||
Reference in New Issue
Block a user