import React, { useState, useEffect } from 'react'; import { Link2, Sparkles, Loader2, RefreshCw, CheckCircle, XCircle, Clock, Film, FolderOpen, Trash2 } from 'lucide-react'; import { downloadApi, processApi, jobsApi, bgmApi, fontsApi } from '../api/client'; import VideoStudio from '../components/VideoStudio'; // Pipeline step definitions const PIPELINE_STEPS = [ { id: 'download', label: '다운로드', statuses: ['downloading'] }, { id: 'trim', label: '트리밍', statuses: ['ready_for_trim'] }, { id: 'transcribe', label: '음성인식', statuses: ['extracting_audio', 'noise_reduction', 'transcribing'] }, { id: 'translate', label: '번역', statuses: ['translating'] }, { id: 'review', label: '편집', statuses: ['awaiting_review', 'awaiting_subtitle'] }, { id: 'render', label: '렌더링', statuses: ['processing'] }, ]; export default function HomePage() { // Core state const [url, setUrl] = useState(''); const [currentJob, setCurrentJob] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // Previous jobs const [previousJobs, setPreviousJobs] = useState([]); const [showPreviousJobs, setShowPreviousJobs] = useState(false); // Resources const [bgmList, setBgmList] = useState([]); const [fontList, setFontList] = useState([]); // Load BGM list useEffect(() => { const loadBgm = async () => { try { const res = await bgmApi.list(); setBgmList(res.data); // Auto-download if no BGM if (res.data.length === 0) { try { const downloadRes = await bgmApi.autoDownload(['upbeat', 'energetic'], 60); if (downloadRes.data.success) { const bgmRes = await bgmApi.list(); setBgmList(bgmRes.data); } } catch (e) { console.log('BGM auto-download failed:', e.message); } } } catch (err) { console.error('Failed to load BGM:', err); } }; loadBgm(); }, []); // Load fonts useEffect(() => { const loadFonts = async () => { try { const res = await fontsApi.list(); setFontList(res.data.fonts || []); } catch (err) { console.error('Failed to load fonts:', err); } }; loadFonts(); }, []); // Load previous jobs useEffect(() => { const loadPreviousJobs = async () => { try { const res = await jobsApi.list(20); setPreviousJobs(res.data || []); } catch (err) { console.error('Failed to load previous jobs:', err); } }; loadPreviousJobs(); }, []); // Reload previous jobs when current job changes const reloadPreviousJobs = async () => { try { const res = await jobsApi.list(20); setPreviousJobs(res.data || []); } catch (err) { console.error('Failed to reload previous jobs:', err); } }; // Poll job status useEffect(() => { const stopPollingStatuses = ['completed', 'failed', 'awaiting_subtitle', 'awaiting_review', 'ready_for_trim']; if (!currentJob || stopPollingStatuses.includes(currentJob.status)) { return; } const interval = setInterval(async () => { try { const res = await jobsApi.get(currentJob.job_id); setCurrentJob(res.data); } catch (err) { console.error('Poll failed:', err); } }, 1000); return () => clearInterval(interval); }, [currentJob]); // Submit URL const handleSubmit = async (e) => { e.preventDefault(); if (!url.trim()) return; setIsLoading(true); setError(null); try { const res = await downloadApi.start(url); setCurrentJob({ job_id: res.data.job_id, status: 'downloading', progress: 0, original_url: url, }); setUrl(''); } catch (err) { setError(err.response?.data?.detail || '다운로드 시작 실패'); } finally { setIsLoading(false); } }; // Reset job const handleReset = () => { setCurrentJob(null); setError(null); reloadPreviousJobs(); }; // Select previous job const handleSelectJob = async (job) => { try { const res = await jobsApi.get(job.job_id); setCurrentJob(res.data); setShowPreviousJobs(false); setError(null); } catch (err) { setError('작업을 불러오는데 실패했습니다'); } }; // Delete job const handleDeleteJob = async (e, jobId) => { e.stopPropagation(); if (!confirm('이 작업을 삭제하시겠습니까?')) return; try { await jobsApi.delete(jobId); reloadPreviousJobs(); if (currentJob?.job_id === jobId) { setCurrentJob(null); } } catch (err) { setError('작업 삭제 실패'); } }; // Start transcription const handleStartTranscription = async (translationMode) => { if (!currentJob) return; setError(null); try { await processApi.startTranscription(currentJob.job_id, { translationMode, useVocalSeparation: false, }); const res = await jobsApi.get(currentJob.job_id); setCurrentJob(res.data); } catch (err) { setError(err.response?.data?.detail || '음성 인식 시작 실패'); } }; // Trim video (with optional exclude regions for cutting middle sections) const handleTrim = async (startTime, endTime, excludeRegions = []) => { if (!currentJob) return; setError(null); try { await processApi.trim(currentJob.job_id, startTime, endTime, false, excludeRegions); const res = await jobsApi.get(currentJob.job_id); setCurrentJob(res.data); } catch (err) { setError(err.response?.data?.detail || '트리밍 실패'); } }; // Render final video const handleRender = async (options) => { if (!currentJob) return; setError(null); // Auto-select first BGM if none selected let bgmId = options.bgmId; if (!bgmId && bgmList.length > 0) { bgmId = bgmList[0].id; } try { await processApi.render(currentJob.job_id, { bgmId, bgmVolume: options.bgmVolume, subtitleStyle: options.subtitleStyle, keepOriginalAudio: false, introText: options.introText, introDuration: options.introDuration, introFontSize: options.introFontSize, introPosition: options.introPosition, }); const res = await jobsApi.get(currentJob.job_id); setCurrentJob(res.data); } catch (err) { setError(err.response?.data?.detail || '렌더링 시작 실패'); } }; // Get current step for pipeline indicator const getCurrentStep = () => { if (!currentJob) return -1; if (currentJob.status === 'completed') return PIPELINE_STEPS.length; if (currentJob.status === 'failed') return -1; for (let i = 0; i < PIPELINE_STEPS.length; i++) { if (PIPELINE_STEPS[i].statuses.includes(currentJob.status)) { return i; } } return -1; }; const currentStep = getCurrentStep(); return (
지원: 抖音(Douyin), 快手(Kuaishou), TikTok, YouTube, Bilibili