import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Box, Paper, Typography, TextField, Button, MenuItem, Select, FormControl, InputLabel, LinearProgress, Alert, Table, TableHead, TableRow, TableCell, TableBody, TableContainer, Checkbox, Tooltip, CircularProgress, Accordion, AccordionSummary, AccordionDetails, Autocomplete, Chip, } from '@mui/material'; import { Tags as TagsIcon, CloudDownload as CloudDownloadIcon, Save as SaveIcon, FolderOpen as FolderOpenIcon, Upload as UploadIcon, ChevronDown as ExpandMoreIcon, RotateCcw as ResetIcon, } from 'lucide-react'; import api from '../api'; const POLL_INTERVAL_MS = 800; export default function BulkAnnotatePanel({ onCommitted }) { const [folderPath, setFolderPath] = useState(''); const [tier, setTier] = useState('basic'); const [status, setStatus] = useState(null); const [results, setResults] = useState([]); const [selected, setSelected] = useState({}); const [copyFiles, setCopyFiles] = useState(true); const [message, setMessage] = useState(''); const [error, setError] = useState(''); const [committing, setCommitting] = useState(false); const [isDocker, setIsDocker] = useState(false); const [uploading, setUploading] = useState(false); const [labels, setLabels] = useState({ genre: [], mood: [], instruments: [] }); const [labelsOverridden, setLabelsOverridden] = useState(false); const [labelsLoading, setLabelsLoading] = useState(false); const [labelsSaving, setLabelsSaving] = useState(false); const [labelsMessage, setLabelsMessage] = useState(''); const [labelsError, setLabelsError] = useState(''); const pollRef = useRef(null); const folderInputRef = useRef(null); useEffect(() => { api.get('/api/environment') .then(({ data }) => setIsDocker(!!data?.docker)) .catch(() => {}); }, []); const loadLabels = useCallback(async () => { setLabelsLoading(true); try { const { data } = await api.get('/api/annotator-labels'); setLabels({ genre: data?.labels?.genre || [], mood: data?.labels?.mood || [], instruments: data?.labels?.instruments || [], }); setLabelsOverridden(!!data?.overridden); setLabelsError(''); } catch (exc) { setLabelsError(exc.response?.data?.error || exc.message); } finally { setLabelsLoading(false); } }, []); useEffect(() => { loadLabels(); }, [loadLabels]); const updateLabelCategory = (category, value) => { const cleaned = Array.from(new Set( (value || []).map((s) => String(s).trim()).filter(Boolean) )); setLabels((prev) => ({ ...prev, [category]: cleaned })); setLabelsMessage(''); }; const saveLabels = async () => { setLabelsSaving(true); setLabelsMessage(''); setLabelsError(''); try { const { data } = await api.put('/api/annotator-labels', labels); setLabels(data?.labels || labels); setLabelsOverridden(!!data?.overridden); setLabelsMessage('Annotator labels saved.'); } catch (exc) { setLabelsError(exc.response?.data?.error || exc.message); } finally { setLabelsSaving(false); } }; const resetLabels = async () => { setLabelsSaving(true); setLabelsMessage(''); setLabelsError(''); try { const { data } = await api.delete('/api/annotator-labels'); setLabels(data?.labels || { genre: [], mood: [], instruments: [] }); setLabelsOverridden(false); setLabelsMessage('Reverted to built-in default labels.'); } catch (exc) { setLabelsError(exc.response?.data?.error || exc.message); } finally { setLabelsSaving(false); } }; const stopPolling = useCallback(() => { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } }, []); const fetchStatus = useCallback(async () => { let data; try { const resp = await api.get('/api/bulk-annotate/status'); data = resp.data; } catch (exc) { return; } setStatus(data); const annotationState = data.state; const downloadState = data.clap_download?.state; if (annotationState === 'done') { try { const resp = await api.get('/api/bulk-annotate/results'); setResults(resp.data.results || []); const sel = {}; (resp.data.results || []).forEach((r, i) => { sel[i] = !r.error; }); setSelected(sel); } catch { return; } } else if (annotationState === 'error') { setError(data.error || 'Annotation failed.'); } const annotationInactive = annotationState !== 'running'; const downloadInactive = downloadState !== 'running'; if (annotationInactive && downloadInactive) { stopPolling(); } }, [stopPolling]); const startPolling = useCallback(() => { stopPolling(); pollRef.current = setInterval(fetchStatus, POLL_INTERVAL_MS); }, [fetchStatus, stopPolling]); useEffect(() => { fetchStatus(); return () => stopPolling(); }, [fetchStatus, stopPolling]); const startAnnotation = async () => { setError(''); setMessage(''); setResults([]); try { await api.post('/api/bulk-annotate', { folder_path: folderPath, tier }); startPolling(); } catch (exc) { setError(exc.response?.data?.error || exc.message); } }; const pickFolder = async () => { setError(''); try { const { data } = await api.post('/api/pick-folder', { start_dir: folderPath || undefined }); if (data?.path) setFolderPath(data.path); } catch (exc) { setError(exc.response?.data?.error || exc.message); } }; const openFolderUpload = () => { setError(''); if (folderInputRef.current) { folderInputRef.current.value = ''; folderInputRef.current.click(); } }; const handleFolderSelected = async (event) => { const fileList = Array.from(event.target.files || []); if (fileList.length === 0) return; setError(''); setUploading(true); try { const form = new FormData(); fileList.forEach((file) => { form.append('files', file); form.append('rel_paths', file.webkitRelativePath || file.name); }); const { data } = await api.post('/api/upload-folder', form, { headers: { 'Content-Type': 'multipart/form-data' }, }); if (data?.path) setFolderPath(data.path); } catch (exc) { setError(exc.response?.data?.error || exc.message); } finally { setUploading(false); } }; const downloadClap = async () => { setError(''); try { await api.post('/api/bulk-annotate/download-clap', {}); startPolling(); } catch (exc) { setError(exc.response?.data?.error || exc.message); } }; const updatePrompt = (idx, value) => { setResults(prev => prev.map((r, i) => (i === idx ? { ...r, prompt: value } : r))); }; const toggleSelected = (idx) => { setSelected(prev => ({ ...prev, [idx]: !prev[idx] })); }; const toggleAll = () => { const allSelected = results.every((_, i) => selected[i]); const next = {}; results.forEach((_, i) => { next[i] = !allSelected; }); setSelected(next); }; const commit = async () => { setError(''); setMessage(''); setCommitting(true); try { const entries = results .filter((_, i) => selected[i]) .map(r => ({ file_name: r.file_name, prompt: r.prompt, path: r.path })); const { data } = await api.post('/api/bulk-annotate/commit', { entries, copy_files: copyFiles }); setMessage(data.message || 'Committed.'); if (onCommitted) onCommitted(); } catch (exc) { setError(exc.response?.data?.error || exc.message); } finally { setCommitting(false); } }; const isRunning = status?.state === 'running'; const clapDownload = status?.clap_download; const clapAvailable = !!status?.clap_available; const clapDownloading = clapDownload?.state === 'running'; const richBlocked = tier === 'rich' && !clapAvailable; const progressPct = status?.total ? Math.round((status.current / status.total) * 100) : 0; return ( Bulk Auto-Annotation Point at a folder of audio files and auto-generate prompts. Basic uses librosa (tempo + key). Rich adds CLAP tagging (genre, mood, instruments). }> Annotator Labels {labelsOverridden && ( )} These replace the built-in label sets used by Rich (CLAP) auto-annotation. Type a label and press Enter to add it; click the × on a chip to remove it. {labelsError && {labelsError}} {labelsMessage && {labelsMessage}} {['genre', 'mood', 'instruments'].map((category) => ( updateLabelCategory(category, value)} disabled={labelsLoading || labelsSaving} renderTags={(value, getTagProps) => value.map((option, index) => ( )) } renderInput={(params) => ( )} /> ))} setFolderPath(e.target.value)} placeholder="Click Browse to choose a folder…" sx={{ flexGrow: 1, minWidth: 260 }} disabled={isRunning} InputProps={{ readOnly: true }} /> {isDocker ? ( <> ) : ( )} Tier {tier === 'rich' && !clapAvailable && ( )} {isRunning && ( {status?.current}/{status?.total} — {status?.current_file} )} {clapDownload?.state === 'running' && ( {clapDownload.message || 'Downloading CLAP checkpoint…'} )} {clapDownload?.state === 'error' && ( CLAP download failed: {clapDownload.error} )} {error && {error}} {message && {message}} {results.length > 0 && ( <> selected[i])} indeterminate={ results.some((_, i) => selected[i]) && !results.every((_, i) => selected[i]) } onChange={toggleAll} /> File Prompt (editable) {results.map((row, idx) => ( toggleSelected(idx)} disabled={!!row.error} /> {row.file_name} {row.error && ( {row.error} )} updatePrompt(idx, e.target.value)} disabled={!!row.error} /> ))}
)}
); }