Files
ALwrity/frontend/src/components/VideoStudio/modules/EditVideo/hooks/useEditVideo.ts

310 lines
9.4 KiB
TypeScript

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<File | null>(null);
const [videoPreview, setVideoPreview] = useState<string | null>(null);
const [videoDuration, setVideoDuration] = useState<number>(10);
// Edit operation
const [operation, setOperation] = useState<EditOperation>('trim');
// Trim settings
const [startTime, setStartTime] = useState<number>(0);
const [endTime, setEndTime] = useState<number>(10);
const [maxDuration, setMaxDuration] = useState<number | null>(null);
const [trimMode, setTrimMode] = useState<TrimMode>('beginning');
// Speed settings
const [speedFactor, setSpeedFactor] = useState<number>(1.0);
// Stabilization settings
const [smoothing, setSmoothing] = useState<number>(10);
// Text overlay settings
const [overlayText, setOverlayText] = useState<string>('');
const [textPosition, setTextPosition] = useState<string>('center');
const [fontSize, setFontSize] = useState<number>(48);
const [fontColor, setFontColor] = useState<string>('white');
const [backgroundColor, setBackgroundColor] = useState<string>('black@0.5');
const [textStartTime, setTextStartTime] = useState<number>(0);
const [textEndTime, setTextEndTime] = useState<number | null>(null);
// Volume settings
const [volumeFactor, setVolumeFactor] = useState<number>(1.0);
// Normalize settings
const [targetLevel, setTargetLevel] = useState<number>(-14);
// Denoise settings
const [denoiseStrength, setDenoiseStrength] = useState<number>(0.5);
// State
const [editing, setEditing] = useState<boolean>(false);
const [progress, setProgress] = useState<number>(0);
const [error, setError] = useState<string | null>(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<void> => {
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,
};
};