feat: Integrate frontend with backend API for transcription and export

This commit is contained in:
Claude 2025-11-23 07:57:52 +00:00
parent e7fcd4bc08
commit 4fadc2c507
No known key found for this signature in database

View File

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useRef } from 'react';
import { import {
FileAudio, FileAudio,
Upload, Upload,
@ -25,6 +25,8 @@ interface FileItem {
status: 'pending' | 'processing' | 'completed' | 'error'; status: 'pending' | 'processing' | 'completed' | 'error';
progress?: number; progress?: number;
transcription?: TranscriptionSegment[]; transcription?: TranscriptionSegment[];
file?: File;
fullText?: string;
} }
interface TranscriptionSegment { interface TranscriptionSegment {
@ -33,6 +35,9 @@ interface TranscriptionSegment {
text: string; text: string;
} }
// Get API URL from environment variable
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';
export default function App() { export default function App() {
const [fileQueue, setFileQueue] = useState<FileItem[]>([]); const [fileQueue, setFileQueue] = useState<FileItem[]>([]);
const [selectedFileId, setSelectedFileId] = useState<string | null>(null); const [selectedFileId, setSelectedFileId] = useState<string | null>(null);
@ -40,6 +45,7 @@ export default function App() {
const [windowSize, setWindowSize] = useState({ width: 1100, height: 700 }); const [windowSize, setWindowSize] = useState({ width: 1100, height: 700 });
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [exportFormat, setExportFormat] = useState('txt'); const [exportFormat, setExportFormat] = useState('txt');
const fileInputRef = useRef<HTMLInputElement>(null);
// Theme colors // Theme colors
const theme = { const theme = {
@ -56,18 +62,35 @@ export default function App() {
}; };
const handleAddFiles = () => { const handleAddFiles = () => {
// Simulated file addition for now fileInputRef.current?.click();
// TODO: Implement real file picker
const newFile: FileItem = {
id: Date.now().toString(),
name: `recording_${fileQueue.length + 1}.mp3`,
status: 'pending',
}; };
setFileQueue([...fileQueue, newFile]);
if (!selectedFileId) { const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSelectedFileId(newFile.id); const files = e.currentTarget.files;
if (!files) return;
const newFiles: FileItem[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const newFileItem: FileItem = {
id: `${Date.now()}-${i}`,
name: file.name,
status: 'pending',
file: file,
};
newFiles.push(newFileItem);
}
setFileQueue([...fileQueue, ...newFiles]);
if (!selectedFileId && newFiles.length > 0) {
setSelectedFileId(newFiles[0].id);
}
toast.success(`${newFiles.length} file(s) added to queue`);
// Reset input
if (fileInputRef.current) {
fileInputRef.current.value = '';
} }
toast.success('File added to queue');
}; };
const handleRemoveFile = (id: string) => { const handleRemoveFile = (id: string) => {
@ -82,7 +105,7 @@ export default function App() {
if (!selectedFileId) return; if (!selectedFileId) return;
const fileIndex = fileQueue.findIndex(f => f.id === selectedFileId); const fileIndex = fileQueue.findIndex(f => f.id === selectedFileId);
if (fileIndex === -1) return; if (fileIndex === -1 || !fileQueue[fileIndex].file) return;
// Update status to processing // Update status to processing
const updatedQueue = [...fileQueue]; const updatedQueue = [...fileQueue];
@ -91,33 +114,45 @@ export default function App() {
setFileQueue(updatedQueue); setFileQueue(updatedQueue);
try { try {
// TODO: Call real Whisper API const file = fileQueue[fileIndex].file!;
// Simulate progress for now const formData = new FormData();
let progress = 0; formData.append('file', file);
const interval = setInterval(() => { formData.append('language', 'fa'); // Farsi by default
progress += 10;
const updated = [...fileQueue];
updated[fileIndex].progress = progress;
if (progress >= 100) { // Show loading toast
clearInterval(interval); const loadingToastId = toast.loading('Loading Whisper model (first time only)...');
updated[fileIndex].status = 'completed';
updated[fileIndex].transcription = [ const response = await fetch(`${API_URL}/transcribe`, {
{ start: '00:00:00.000', end: '00:00:05.500', text: 'سلام دنیا، این یک تست است' }, method: 'POST',
{ start: '00:00:05.500', end: '00:00:10.200', text: 'خوش آمدید به برنامه تجزیه صوت' }, body: formData,
{ start: '00:00:10.200', end: '00:00:15.800', text: 'این برنامه با استفاده از مدل ویسپر کار می‌کند' }, });
{ start: '00:00:15.800', end: '00:00:22.300', text: 'شما می‌توانید فایل‌های صوتی و تصویری خود را به متن تبدیل کنید' },
{ start: '00:00:22.300', end: '00:00:28.100', text: 'این ابزار برای تحقیقات علمی و سخنرانی‌ها مفید است' }, if (!response.ok) {
]; throw new Error(`API error: ${response.statusText}`);
toast.success('Transcription completed!');
} }
const result = await response.json();
// Dismiss loading toast
toast.dismiss(loadingToastId);
if (result.status === 'success') {
const updated = [...fileQueue];
updated[fileIndex].status = 'completed';
updated[fileIndex].progress = 100;
updated[fileIndex].transcription = result.segments;
updated[fileIndex].fullText = result.text;
setFileQueue(updated); setFileQueue(updated);
}, 300); toast.success('Transcription completed!');
} else {
throw new Error(result.error || 'Unknown error');
}
} catch (error) { } catch (error) {
const updated = [...fileQueue]; const updated = [...fileQueue];
updated[fileIndex].status = 'error'; updated[fileIndex].status = 'error';
setFileQueue(updated); setFileQueue(updated);
toast.error('Failed to transcribe file'); const errorMsg = error instanceof Error ? error.message : 'Failed to transcribe file';
toast.error(errorMsg);
} }
}; };
@ -126,13 +161,53 @@ export default function App() {
toast.success('Copied to clipboard'); toast.success('Copied to clipboard');
}; };
const handleExport = () => { const handleExport = async () => {
const selectedFile = fileQueue.find(f => f.id === selectedFileId); const selectedFile = fileQueue.find(f => f.id === selectedFileId);
if (selectedFile?.transcription) { if (!selectedFile?.transcription || !selectedFile.fullText) {
// TODO: Implement real export
toast.success(`Exporting as ${exportFormat.toUpperCase()}...`);
} else {
toast.error('No transcription to export'); toast.error('No transcription to export');
return;
}
try {
const toastId = toast.loading('Preparing export...');
const response = await fetch(`${API_URL}/export`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
transcription: selectedFile.fullText,
segments: selectedFile.transcription,
format: exportFormat,
}),
});
if (!response.ok) {
throw new Error(`Export failed: ${response.statusText}`);
}
const result = await response.json();
toast.dismiss(toastId);
if (result.status === 'success') {
// Create a blob and download
const blob = new Blob([result.content], { type: result.mime_type });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${selectedFile.name.split('.')[0]}.${exportFormat === 'json' ? 'json' : exportFormat}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success(`Exported as ${exportFormat.toUpperCase()}`);
} else {
throw new Error(result.error || 'Export failed');
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Failed to export';
toast.error(errorMsg);
} }
}; };
@ -182,6 +257,16 @@ export default function App() {
<div className="min-h-screen flex items-center justify-center p-8" style={{ backgroundColor: theme.bg }}> <div className="min-h-screen flex items-center justify-center p-8" style={{ backgroundColor: theme.bg }}>
<Toaster theme={isDark ? 'dark' : 'light'} position="top-right" /> <Toaster theme={isDark ? 'dark' : 'light'} position="top-right" />
{/* Hidden file input */}
<input
ref={fileInputRef}
type="file"
multiple
accept="audio/*,video/*"
onChange={handleFileInputChange}
style={{ display: 'none' }}
/>
<Resizable <Resizable
size={windowSize} size={windowSize}
onResizeStop={(_e, _direction, _ref, d) => { onResizeStop={(_e, _direction, _ref, d) => {
@ -352,12 +437,12 @@ export default function App() {
</div> </div>
<Select <Select
value={exportFormat} value={exportFormat}
onChange={(e) => setExportFormat(e.target.value as 'txt' | 'docx' | 'pdf' | 'srt')} onChange={(e) => setExportFormat(e.target.value as 'txt' | 'srt' | 'vtt' | 'json')}
> >
<option value="txt">TXT</option> <option value="txt">TXT</option>
<option value="docx">DOCX</option>
<option value="pdf">PDF</option>
<option value="srt">SRT</option> <option value="srt">SRT</option>
<option value="vtt">VTT</option>
<option value="json">JSON</option>
</Select> </Select>
<Button <Button
onClick={handleExport} onClick={handleExport}