/** * Timeline AI Overlay * Визуализация AI анализа на таймлайне */ import { AnimatePresence, motion } from "framer-motion" import { Activity, AlertTriangle, Camera, Eye, type LucideIcon, Sparkles, TrendingUp, Users, Volume2, Zap, } from "lucide-react" import { useEffect, useRef, useState } from "react" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import { cn } from "@/lib/utils" import { useTimelineAIAnalysis } from "../../hooks/use-timeline-ai-analysis" interface TimelineAIOverlayProps { timelineWidth: number timelineDuration: number pixelsPerSecond: number className?: string } interface AnalysisSegment { id: string startTime: number endTime: number type: "scene" | "keyMoment" | "quality" | "emotion" | "audio" confidence: number label: string color: string icon: LucideIcon description?: string intensity?: number } export function TimelineAIOverlay({ timelineWidth, timelineDuration, pixelsPerSecond, className, }: TimelineAIOverlayProps) { const { state: aiState } = useTimelineAIAnalysis() const [segments, setSegments] = useState([]) const [hoveredSegment, setHoveredSegment] = useState(null) const canvasRef = useRef(null) // Конвертируем данные анализа в сегменты для отображения useEffect(() => { const newSegments: AnalysisSegment[] = [] // Сегменты сцен if (aiState.sceneAnalysis?.scenes) { aiState.sceneAnalysis.scenes.forEach((scene: any) => { newSegments.push({ id: `scene-${scene.id}`, startTime: scene.startTime, endTime: scene.startTime + scene.duration, type: "scene", confidence: scene.confidence || 0.8, label: getSceneLabel(scene.type), color: getSceneColor(scene.type), icon: getSceneIcon(scene.type), description: `${scene.type} сцена (${scene.duration.toFixed(1)}с)`, intensity: scene.intensity || 0.5, }) }) } // Сегменты ключевых моментов if (aiState.keyMoments.length > 0) { aiState.keyMoments.forEach((moment) => { newSegments.push({ id: `moment-${moment.id}`, startTime: moment.timestamp - 0.5, endTime: moment.timestamp + 0.5, type: "keyMoment", confidence: moment.score, label: "Ключевой момент", color: getMomentColor(moment.type), icon: Sparkles, description: moment.description, intensity: moment.score, }) }) } // Сегменты качества (используем qualityMetrics если есть) if (aiState.currentAnalysis?.qualityMetrics) { // Создаем один сегмент для общего качества const quality = aiState.currentAnalysis.qualityMetrics.overall newSegments.push({ id: "quality-overall", startTime: 0, endTime: timelineDuration, type: "quality", confidence: quality / 100, label: quality < 60 ? "Низкое качество" : "Хорошее качество", color: quality < 60 ? "#ef4444" : "#10b981", icon: quality < 60 ? AlertTriangle : TrendingUp, description: `Общее качество: ${quality}/100`, intensity: quality / 100, }) } setSegments(newSegments) }, [aiState, timelineDuration]) // Рисуем визуализацию интенсивности на canvas useEffect(() => { const canvas = canvasRef.current if (!canvas) return const ctx = canvas.getContext("2d") if (!ctx) return // Очищаем canvas ctx.clearRect(0, 0, canvas.width, canvas.height) // Рисуем градиентную визуализацию для каждого сегмента segments.forEach((segment) => { const x = segment.startTime * pixelsPerSecond const width = (segment.endTime - segment.startTime) * pixelsPerSecond const intensity = segment.intensity || 0.5 // Создаем градиент const gradient = ctx.createLinearGradient(x, 0, x, canvas.height) gradient.addColorStop(0, `${segment.color}00`) gradient.addColorStop( 0.5, `${segment.color}${Math.floor(intensity * 40) .toString(16) .padStart(2, "0")}`, ) gradient.addColorStop(1, `${segment.color}00`) ctx.fillStyle = gradient ctx.fillRect(x, 0, width, canvas.height) }) }, [segments, pixelsPerSecond]) return (
{/* Canvas для фоновой визуализации */} {/* Индикатор анализа */} {aiState.isAnalyzing && (
Анализ {Math.round(aiState.analysisProgress)}% )} {/* Интерактивные маркеры сегментов */} {segments.map((segment) => { const Icon = segment.icon const x = segment.startTime * pixelsPerSecond const isKeyMoment = segment.type === "keyMoment" return ( setHoveredSegment(segment.id)} onMouseLeave={() => setHoveredSegment(null)} >
{segment.label}
{segment.description && (
{segment.description}
)}
{segment.startTime.toFixed(1)}с - {segment.endTime.toFixed(1)}с
Уверенность: {Math.round(segment.confidence * 100)}%
) })}
{/* Легенда (появляется при наведении) */} {segments.length > 0 && (
Сцены
Ключевые моменты
Качество
)}
) } // Вспомогательные функции для определения иконок и цветов function getSceneIcon(type: string): LucideIcon { switch (type) { case "action": return Zap case "dialogue": return Users case "landscape": return Eye case "closeup": return Camera case "audio_peak": return Volume2 default: return Activity } } function getSceneLabel(type: string): string { switch (type) { case "action": return "Экшн" case "dialogue": return "Диалог" case "landscape": return "Пейзаж" case "closeup": return "Крупный план" case "establishing": return "Общий план" default: return "Сцена" } } function getSceneColor(type: string): string { switch (type) { case "action": return "#ef4444" case "dialogue": return "#3b82f6" case "landscape": return "#10b981" case "closeup": return "#f59e0b" case "establishing": return "#8b5cf6" default: return "#6b7280" } } function getMomentColor(type: string): string { switch (type) { case "climax": return "#ef4444" case "emotional_peak": return "#f59e0b" case "action_peak": return "#eab308" case "visual_highlight": return "#3b82f6" case "audio_peak": return "#8b5cf6" default: return "#6b7280" } }