import { useState, useMemo, useEffect, useCallback } from 'react'; import { aiApiClient } from '../../../../../api/client'; export type EditOperation = 'trim' | 'speed' | 'stabilize' | 'text' | 'volume' | 'normalize' | 'denoise'; export type TrimMode = 'beginning' | 'middle' | 'end'; export const useEditVideo = () => { const [videoFile, setVideoFile] = useState(null); const [videoPreview, setVideoPreview] = useState(null); const [videoDuration, setVideoDuration] = useState(10); // Edit operation const [operation, setOperation] = useState('trim'); // Trim settings const [startTime, setStartTime] = useState(0); const [endTime, setEndTime] = useState(10); const [maxDuration, setMaxDuration] = useState(null); const [trimMode, setTrimMode] = useState('beginning'); // Speed settings const [speedFactor, setSpeedFactor] = useState(1.0); // Stabilization settings const [smoothing, setSmoothing] = useState(10); // Text overlay settings const [overlayText, setOverlayText] = useState(''); const [textPosition, setTextPosition] = useState('center'); const [fontSize, setFontSize] = useState(48); const [fontColor, setFontColor] = useState('white'); const [backgroundColor, setBackgroundColor] = useState('black@0.5'); const [textStartTime, setTextStartTime] = useState(0); const [textEndTime, setTextEndTime] = useState(null); // Volume settings const [volumeFactor, setVolumeFactor] = useState(1.0); // Normalize settings const [targetLevel, setTargetLevel] = useState(-14); // Denoise settings const [denoiseStrength, setDenoiseStrength] = useState(0.5); // State const [editing, setEditing] = useState(false); const [progress, setProgress] = useState(0); const [error, setError] = useState(null); const [result, setResult] = useState<{ video_url: string; cost: number; edit_type: string } | null>(null); // Update preview when file changes useEffect(() => { if (videoFile) { const url = URL.createObjectURL(videoFile); setVideoPreview(url); const video = document.createElement('video'); video.preload = 'metadata'; video.onloadedmetadata = () => { setVideoDuration(video.duration); setEndTime(video.duration); URL.revokeObjectURL(video.src); }; video.src = url; return () => { URL.revokeObjectURL(url); }; } else { setVideoPreview(null); setVideoDuration(10); setEndTime(10); } }, [videoFile]); const canEdit = useMemo(() => { if (!videoFile) return false; switch (operation) { case 'trim': return startTime < endTime && endTime <= videoDuration; case 'speed': return speedFactor > 0 && speedFactor <= 4.0; case 'stabilize': return smoothing >= 1 && smoothing <= 100; case 'text': return overlayText.trim().length > 0; case 'volume': return volumeFactor >= 0 && volumeFactor <= 5.0; case 'normalize': return targetLevel >= -50 && targetLevel <= 0; case 'denoise': return denoiseStrength >= 0 && denoiseStrength <= 1; default: return false; } }, [videoFile, operation, startTime, endTime, videoDuration, speedFactor, smoothing, overlayText, volumeFactor, targetLevel, denoiseStrength]); const costHint = useMemo(() => { if (!videoFile) return 'Upload a video to see cost'; return 'Free (FFmpeg processing)'; }, [videoFile]); const operationDescription = useMemo(() => { switch (operation) { case 'trim': return `Trim video from ${startTime.toFixed(1)}s to ${endTime.toFixed(1)}s (${(endTime - startTime).toFixed(1)}s output)`; case 'speed': const resultDuration = videoDuration / speedFactor; return `${speedFactor}x speed -> ${resultDuration.toFixed(1)}s output`; case 'stabilize': return `Stabilize with smoothing: ${smoothing} (higher = smoother)`; case 'text': return `Add "${overlayText.substring(0, 20)}${overlayText.length > 20 ? '...' : ''}" at ${textPosition}`; case 'volume': return `${volumeFactor === 0 ? 'Mute' : volumeFactor < 1 ? 'Reduce' : volumeFactor === 1 ? 'Keep' : 'Boost'} volume to ${Math.round(volumeFactor * 100)}%`; case 'normalize': return `Normalize audio to ${targetLevel} LUFS`; case 'denoise': return `Reduce noise at ${Math.round(denoiseStrength * 100)}% strength`; default: return ''; } }, [operation, startTime, endTime, speedFactor, videoDuration, smoothing, overlayText, textPosition, volumeFactor, targetLevel, denoiseStrength]); const processVideo = useCallback(async (): Promise => { if (!videoFile || !canEdit) return; setEditing(true); setProgress(0); setError(null); setResult(null); const progressInterval = setInterval(() => { setProgress((prev) => { if (prev >= 90) return prev; return prev + Math.random() * 10; }); }, 1000); try { const formData = new FormData(); formData.append('file', videoFile); let endpoint = '/api/video-studio/edit/'; switch (operation) { case 'trim': endpoint += 'trim'; formData.append('start_time', startTime.toString()); formData.append('end_time', endTime.toString()); if (maxDuration !== null) { formData.append('max_duration', maxDuration.toString()); } formData.append('trim_mode', trimMode); break; case 'speed': endpoint += 'speed'; formData.append('speed_factor', speedFactor.toString()); break; case 'stabilize': endpoint += 'stabilize'; formData.append('smoothing', smoothing.toString()); break; case 'text': endpoint += 'text'; formData.append('text', overlayText); formData.append('position', textPosition); formData.append('font_size', fontSize.toString()); formData.append('font_color', fontColor); formData.append('background_color', backgroundColor); formData.append('start_time', textStartTime.toString()); if (textEndTime !== null) { formData.append('end_time', textEndTime.toString()); } break; case 'volume': endpoint += 'volume'; formData.append('volume_factor', volumeFactor.toString()); break; case 'normalize': endpoint += 'normalize'; formData.append('target_level', targetLevel.toString()); break; case 'denoise': endpoint += 'denoise'; formData.append('strength', denoiseStrength.toString()); break; } const response = await aiApiClient.post(endpoint, formData, { headers: { 'Content-Type': 'multipart/form-data', }, }); clearInterval(progressInterval); setProgress(100); setResult({ video_url: response.data.video_url, cost: response.data.cost || 0, edit_type: response.data.edit_type, }); } catch (err: any) { clearInterval(progressInterval); setError(err.response?.data?.detail || err.message || 'Video editing failed'); } finally { setEditing(false); } }, [videoFile, canEdit, operation, startTime, endTime, maxDuration, trimMode, speedFactor, smoothing, overlayText, textPosition, fontSize, fontColor, backgroundColor, textStartTime, textEndTime, volumeFactor, targetLevel, denoiseStrength]); const reset = useCallback(() => { setVideoFile(null); setVideoPreview(null); setVideoDuration(10); setOperation('trim'); setStartTime(0); setEndTime(10); setMaxDuration(null); setTrimMode('beginning'); setSpeedFactor(1.0); setSmoothing(10); setOverlayText(''); setTextPosition('center'); setFontSize(48); setFontColor('white'); setBackgroundColor('black@0.5'); setTextStartTime(0); setTextEndTime(null); setVolumeFactor(1.0); setTargetLevel(-14); setDenoiseStrength(0.5); setEditing(false); setProgress(0); setError(null); setResult(null); }, []); return { // Video state videoFile, videoPreview, videoDuration, setVideoFile, // Operation operation, setOperation, // Trim settings startTime, endTime, maxDuration, trimMode, setStartTime, setEndTime, setMaxDuration, setTrimMode, // Speed settings speedFactor, setSpeedFactor, // Stabilization settings smoothing, setSmoothing, // Text overlay settings overlayText, textPosition, fontSize, fontColor, backgroundColor, textStartTime, textEndTime, setOverlayText, setTextPosition, setFontSize, setFontColor, setBackgroundColor, setTextStartTime, setTextEndTime, // Volume settings volumeFactor, setVolumeFactor, // Normalize settings targetLevel, setTargetLevel, // Denoise settings denoiseStrength, setDenoiseStrength, // State editing, progress, error, result, canEdit, costHint, operationDescription, // Actions processVideo, reset, }; };