'use client'; import { useState, useRef, useEffect, Suspense } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; function UniqueizerContent() { const router = useRouter(); const searchParams = useSearchParams(); const fileInputRef = useRef(null); const fileUploadRef = useRef(null); const [selectedFile, setSelectedFile] = useState(null); const [preview, setPreview] = useState(null); const [uploading, setUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [status, setStatus] = useState(''); const [error, setError] = useState(''); const [videoKey, setVideoKey] = useState(null); // generationUuid получаем из URL (передаётся из дашборда) const generationUuidFromUrl = searchParams.get('generationUuid'); const [generationUuid, setGenerationUuid] = useState(generationUuidFromUrl); const [scenarioStarted, setScenarioStarted] = useState(false); // Проверяем generationUuid из URL useEffect(() => { const uuidFromUrl = searchParams.get('generationUuid'); if (uuidFromUrl) { setGenerationUuid(uuidFromUrl); setScenarioStarted(true); setStatus('Сценарий запущен'); } else { setError('generationUuid не указан. Вернитесь на дашборд и запустите сценарий.'); } }, [searchParams]); // Drag and drop useEffect(() => { const el = fileUploadRef.current; if (!el) return; ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { el.addEventListener(eventName, preventDefaults, false); }); ['dragenter', 'dragover'].forEach(eventName => { el.addEventListener(eventName, () => el.classList.add('dragover'), false); }); ['dragleave', 'drop'].forEach(eventName => { el.addEventListener(eventName, () => el.classList.remove('dragover'), false); }); el.addEventListener('drop', handleDrop, false); }, []); function preventDefaults(e: Event) { e.preventDefault(); e.stopPropagation(); } function handleDrop(e: DragEvent) { const dt = e.dataTransfer; if (dt?.files.length) { handleFileSelect(dt.files[0]); } } function handleFileChange(e: React.ChangeEvent) { if (e.target.files?.length) { handleFileSelect(e.target.files[0]); } } function handleFileSelect(file: File) { const allowedTypes = ['video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/webm']; if (!allowedTypes.includes(file.type)) { setError('Недопустимый тип файла. Разрешены: MP4, MOV, AVI, WebM'); return; } const maxSize = 500 * 1024 * 1024; if (file.size > maxSize) { setError('Файл слишком большой. Максимум: 500MB'); return; } setSelectedFile(file); setPreview(URL.createObjectURL(file)); setError(''); setVideoKey(null); } function removeFile() { setSelectedFile(null); setPreview(null); setVideoKey(null); if (fileInputRef.current) fileInputRef.current.value = ''; } function formatFileSize(bytes: number): string { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; } async function handleUploadVideo() { try { const s3Key = await uploadVideoMultipart(); setVideoKey(s3Key); // Сохраняем ключ после загрузки } catch (err: unknown) { // Ошибка уже обработана в uploadVideoMultipart } } async function uploadVideoMultipart() { if (!selectedFile) throw new Error('Файл не выбран'); if (!generationUuid) throw new Error('generationUuid не получен'); setUploading(true); setUploadProgress(0); setError(''); setStatus('Инициализация загрузки...'); try { const csrfToken = await getCsrfToken(); // 1. Инициализация multipart upload const initResponse = await fetch('/api/upload/video/init', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrfToken, }, credentials: 'same-origin', body: JSON.stringify({ filename: selectedFile.name, contentType: selectedFile.type, fileSize: selectedFile.size, generationUuid, }), }); if (!initResponse.ok) { const errorData = await initResponse.json(); throw new Error(errorData.message || 'Ошибка инициализации загрузки'); } const initData = await initResponse.json(); const { fileId, uploadId, parts, s3Key } = initData.data; setStatus(`Загрузка видео... 0%`); // 2. Параллельная загрузка частей напрямую в S3 const uploadedParts = new Array(parts.length); const PART_SIZE = 10 * 1024 * 1024; // 10MB (увеличено для эффективности) const MAX_CONCURRENT = 6; // До 6 частей параллельно // Функция загрузки одной части с retry и прогрессом const uploadPartWithRetry = async (partIndex: number, maxRetries = 3): Promise<{ PartNumber: number; ETag: string } | undefined> => { const part = parts[partIndex]; const start = partIndex * PART_SIZE; const end = Math.min(start + PART_SIZE, selectedFile.size); const chunk = selectedFile.slice(start, end); for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const startTime = Date.now(); console.log(`[Upload] Часть ${part.partNumber}: начало загрузки, размер: ${(chunk.size / 1024 / 1024).toFixed(2)} MB`); // Используем XMLHttpRequest для лучшей производительности с бинарными данными const etag = await new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', part.presignedUrl, true); // Отслеживаем прогресс загрузки xhr.upload.onprogress = function(e) { if (e.lengthComputable) { const percent = (e.loaded / e.total * 100).toFixed(1); const speed = (e.loaded / ((Date.now() - startTime) / 1000) / 1024 / 1024).toFixed(2); console.log(`[Upload] Часть ${part.partNumber}: ${percent}% (${speed} MB/s)`); } }; // Устанавливаем таймаут 120 секунд xhr.timeout = 120000; xhr.onload = function() { const duration = ((Date.now() - startTime) / 1000).toFixed(2); const speed = (chunk.size / 1024 / 1024 / parseFloat(duration)).toFixed(2); console.log(`[Upload] Часть ${part.partNumber}: завершено за ${duration}s (${speed} MB/s)`); if (xhr.status === 200) { const etag = xhr.getResponseHeader('ETag'); if (etag) { resolve(etag.replace(/"/g, '')); // MinIO возвращает ETag в кавычках } else { reject(new Error('Нет ETag')); } } else { reject(new Error(`HTTP ${xhr.status}: ${xhr.responseText}`)); } }; xhr.onerror = function() { console.error(`[Upload] Часть ${part.partNumber}: ошибка сети`); reject(new Error('Network error')); }; xhr.ontimeout = function() { console.error(`[Upload] Часть ${part.partNumber}: таймаут`); reject(new Error('Timeout')); }; xhr.send(chunk); }); return { PartNumber: part.partNumber, ETag: etag, }; } catch (err) { if (attempt === maxRetries) { throw new Error(`Часть ${part.partNumber}: ${err instanceof Error ? err.message : 'ошибка'}`); } // Ждём перед retry await new Promise(r => setTimeout(r, 100 * attempt)); } } }; // Загружаем части пачками по MAX_CONCURRENT for (let i = 0; i < parts.length; i += MAX_CONCURRENT) { const batch = []; const batchEnd = Math.min(i + MAX_CONCURRENT, parts.length); for (let j = i; j < batchEnd; j++) { batch.push(uploadPartWithRetry(j)); } const results = await Promise.all(batch); // Сохраняем результаты for (let k = 0; k < results.length; k++) { uploadedParts[i + k] = results[k]; } // Обновляем прогресс const progress = Math.round(((i + batch.length) / parts.length) * 100); setUploadProgress(progress); setStatus(`Загрузка видео... ${progress}%`); } // 3. Завершение multipart upload setStatus('Завершение загрузки...'); const completeResponse = await fetch('/api/upload/video/complete', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrfToken, }, credentials: 'same-origin', body: JSON.stringify({ fileId, uploadId, parts: uploadedParts.filter(p => p !== undefined), }), }); if (!completeResponse.ok) { const errorData = await completeResponse.json(); throw new Error(errorData.message || 'Ошибка завершения загрузки'); } setUploadProgress(100); setStatus('Видео загружено'); setUploading(false); return s3Key; } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Ошибка загрузки'); setUploading(false); throw err; } } async function handleUniqueize() { setError(''); try { if (!generationUuid) throw new Error('generationUuid не получен'); let s3Key = videoKey; // Если видео ещё не загружено, загружаем if (!s3Key) { setUploading(true); setStatus('Загрузка видео...'); s3Key = await uploadVideoMultipart(); setVideoKey(s3Key); setUploading(false); } setStatus('Видео отправлено на обработку'); const csrfToken = await getCsrfToken(); // Вызываем step/1 с url видео const stepPayload = { input: { url: s3Key, }, }; const stepResponse = await fetch('/api/scenario/uniqueizer/step/1', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrfToken, }, credentials: 'same-origin', body: JSON.stringify(stepPayload), }); if (!stepResponse.ok) { const data = await stepResponse.json(); throw new Error(data.message || 'Ошибка выполнения шага'); } // Переход на страницу результата setTimeout(() => { router.push(`/result?generationUuid=${generationUuid}`); }, 1000); } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Ошибка обработки'); setUploading(false); } } async function getCsrfToken(): Promise { const response = await fetch('/api/auth/csrf', { credentials: 'same-origin' }); const data = await response.json(); return data.csrfToken || ''; } function handleClose() { router.push('/dashboard'); } return (

Уникализация видео

{error &&
{error}
} {status && (
{status}
)} {/* Статус сценария */} {!scenarioStarted && (
Запуск сценария...
)} {scenarioStarted && !error && (
✓ Сценарий запущен (ID: {generationUuid?.slice(0, 8)}...)
)} {/* Загрузка видео */}

1. Загрузите видео

{!selectedFile ? (
fileInputRef.current?.click()} >
🎬
Нажмите для загрузки или перетащите файл
MP4, MOV, AVI, WebM до 500MB
) : (
)} {selectedFile && !videoKey && (
{uploading && uploadProgress > 0 && (
{uploadProgress}%
)}
)}
{/* Кнопка уникализации */} {(videoKey || selectedFile) && (

2. Уникализируйте

После нажатия видео будет отправлено на обработку

)} {/* Кнопка закрытия */}
); } const styles: Record = { page: { minHeight: '100vh', background: '#f5f5f5', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '40px 20px', }, container: { maxWidth: '700px', width: '100%', }, emptyState: { textAlign: 'center', padding: '60px 20px', color: '#666', fontSize: '16px', }, card: { background: 'white', padding: '40px', borderRadius: '8px', boxShadow: '0 2px 10px rgba(0,0,0,0.1)', }, title: { fontSize: '24px', fontWeight: 600, marginBottom: '24px', color: '#333', textAlign: 'center', }, infoBox: { display: 'flex', alignItems: 'center', gap: '10px', padding: '12px', background: '#e8f5e9', color: '#2e7d32', borderRadius: '6px', marginBottom: '20px', fontSize: '14px', }, section: { marginBottom: '32px', padding: '20px', background: '#f8f9fa', borderRadius: '8px', }, sectionTitle: { fontSize: '16px', fontWeight: 600, color: '#333', marginBottom: '8px', }, sectionDesc: { fontSize: '14px', color: '#666', marginBottom: '16px', }, fileUpload: { border: '2px dashed #ddd', borderRadius: '8px', padding: '40px 20px', textAlign: 'center', cursor: 'pointer', transition: 'all 0.2s', }, fileUploadIcon: { fontSize: '48px', marginBottom: '16px', }, fileUploadText: { fontSize: '16px', color: '#666', marginBottom: '8px', }, fileUploadHint: { fontSize: '13px', color: '#999', }, filePreview: { position: 'relative', marginBottom: '16px', }, previewVideo: { width: '100%', maxHeight: '400px', borderRadius: '8px', background: '#000', }, filePreviewInfo: { padding: '12px', background: '#fff', borderRadius: '6px', marginTop: '8px', fontSize: '14px', color: '#666', }, filePreviewName: { fontWeight: 500, color: '#333', marginBottom: '4px', }, fileRemove: { position: 'absolute', top: '8px', right: '8px', background: 'rgba(220, 38, 38, 0.9)', color: 'white', border: 'none', borderRadius: '50%', width: '32px', height: '32px', fontSize: '20px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', }, btn: { width: '100%', padding: '14px 24px', borderRadius: '6px', fontSize: '15px', fontWeight: 500, cursor: 'pointer', border: 'none', transition: 'all 0.2s', }, btnPrimary: { background: '#007bff', color: 'white', }, btnDisabled: { background: '#ccc', cursor: 'not-allowed', }, btnSecondary: { width: '100%', padding: '12px 24px', background: '#f5f5f5', color: '#333', border: '1px solid #ddd', borderRadius: '6px', fontSize: '14px', fontWeight: 500, cursor: 'pointer', }, error: { background: '#fee', color: '#c00', padding: '12px', borderRadius: '6px', marginBottom: '20px', fontSize: '14px', }, status: { display: 'flex', alignItems: 'center', gap: '10px', padding: '12px', background: '#e7f3ff', color: '#0066cc', borderRadius: '6px', marginBottom: '20px', fontSize: '14px', }, spinner: { display: 'inline-block', width: '16px', height: '16px', border: '2px solid rgba(0,123,255,0.3)', borderTopColor: '#007bff', borderRadius: '50%', animation: 'spin 1s linear infinite', }, progressContainer: { position: 'relative', height: '24px', background: '#e0e0e0', borderRadius: '12px', marginBottom: '12px', overflow: 'hidden', }, progressBar: { height: '100%', background: 'linear-gradient(90deg, #007bff, #0056b3)', borderRadius: '12px', transition: 'width 0.3s ease', }, progressText: { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', fontSize: '13px', fontWeight: '600', color: '#333', }, }; export default function UniqueizerPage() { return (
Загрузка...
}>
); }