diff --git a/app/create/script-to-video/page.tsx b/app/create/script-to-video/page.tsx deleted file mode 100644 index 2e38915..0000000 --- a/app/create/script-to-video/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { ScriptToVideo } from '@/components/pages/script-to-video'; - -export default function ScriptToVideoPage() { - return ; -} \ No newline at end of file diff --git a/app/create/video-to-video/page.tsx b/app/create/video-to-video/page.tsx deleted file mode 100644 index 9a8685b..0000000 --- a/app/create/video-to-video/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { VideoToVideo } from '@/components/pages/video-to-video'; - -export default function VideoToVideoPage() { - return ; -} \ No newline at end of file diff --git a/app/history/page.tsx b/app/history/page.tsx index 300dd4d..cf84544 100644 --- a/app/history/page.tsx +++ b/app/history/page.tsx @@ -1,10 +1,11 @@ import { DashboardLayout } from '@/components/layout/dashboard-layout'; -import { HistoryPage } from '@/components/pages/history-page'; export default function History() { return ( - + + History + ); } \ No newline at end of file diff --git a/components/common/EmptyStateAnimation.tsx b/components/common/EmptyStateAnimation.tsx deleted file mode 100644 index 86254fd..0000000 --- a/components/common/EmptyStateAnimation.tsx +++ /dev/null @@ -1,328 +0,0 @@ -"use client"; - -import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { ArrowUp } from 'lucide-react'; -import gsap from 'gsap'; -import { ImageWave } from '@/components/ui/ImageWave'; - -interface AnimationStageProps { - shouldStart: boolean; - onComplete: () => void; -} - -// 动画1:模拟输入和点击 -const InputAnimation: React.FC = ({ shouldStart, onComplete }) => { - const containerRef = useRef(null); - const inputRef = useRef(null); - const cursorRef = useRef(null); - const buttonRef = useRef(null); - const mouseRef = useRef(null); - const [displayText, setDisplayText] = useState(''); - - const demoText = "a cute capybara with an orange on its head"; - - useEffect(() => { - if (!shouldStart || !containerRef.current) return; - - // 重置状态 - setDisplayText(''); - - const tl = gsap.timeline({ - onComplete: () => { - setTimeout(onComplete, 500); - } - }); - - // 1. 显示输入框和鼠标 - tl.fromTo([inputRef.current, mouseRef.current], { - scale: 0.9, - opacity: 0 - }, { - scale: 1, - opacity: 1, - duration: 0.3, - ease: "back.out(1.7)", - stagger: 0.1 - }); - - // 2. 鼠标移动到输入框中心 - tl.to(mouseRef.current, { - x: 20, - y: 0, - duration: 0.3 - }); - - // 3. 输入框聚焦效果 - tl.to(inputRef.current, { - scale: 1.02, - boxShadow: '0 0 0 2px rgba(59, 130, 246, 0.5)', - duration: 0.2 - }); - - // 4. 打字动画 - const typingDuration = demoText.length * 0.05; - tl.to({}, { - duration: typingDuration, - onUpdate: function() { - const progress = this.progress(); - const targetChar = Math.floor(progress * demoText.length); - setDisplayText(demoText.slice(0, targetChar)); - } - }); - - // 6. 鼠标移动到按钮位置(调整移动时间和缓动函数) - tl.to(mouseRef.current, { - x: 650, - y: 0, - duration: 0.8, - ease: "power2.inOut" - }); - - // 7. 等待一小段时间 - tl.to({}, { duration: 0.2 }); - - // 8. 点击效果 - tl.to(mouseRef.current, { - scale: 0.8, - duration: 0.1, - yoyo: true, - repeat: 1 - }).to(buttonRef.current, { - scale: 0.95, - duration: 0.1, - yoyo: true, - repeat: 1 - }, "<"); - - // 9. 等待一小段时间 - tl.to({}, { duration: 0.3 }); - - // 10. 整体淡出 - tl.to(containerRef.current, { - opacity: 0, - y: -20, - duration: 0.3 - }); - - }, [shouldStart, onComplete]); - - return ( - - - - - {displayText} - - - - - - - Action - - - - - - - - - - ); -}; - -// 动画2:ImageWave 动画展示 -const WaveAnimation: React.FC = ({ shouldStart, onComplete }) => { - const containerRef = useRef(null); - const [showWave, setShowWave] = useState(false); - const [autoAnimate, setAutoAnimate] = useState(false); - - const imageUrls = [ - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', - ]; - - useEffect(() => { - if (!shouldStart) { - setShowWave(false); - setAutoAnimate(false); - return; - } - - // 显示 ImageWave - setShowWave(true); - - // 延迟开始自动动画 - const startTimeout = setTimeout(() => { - setAutoAnimate(true); - }, 300); - - return () => { - clearTimeout(startTimeout); - }; - }, [shouldStart]); - - const handleAnimationComplete = () => { - // 动画完成后淡出并触发完成回调 - gsap.to(containerRef.current, { - opacity: 0, - scale: 0.9, - duration: 0.3, - onComplete: () => { - setAutoAnimate(false); - setShowWave(false); - onComplete(); - } - }); - }; - - return ( - - - - ); -}; - -// 动画3:图片墙打破,显示视频 -const FinalAnimation: React.FC = ({ shouldStart, onComplete }) => { - const containerRef = useRef(null); - const videoRef = useRef(null); - const [showVideo, setShowVideo] = useState(false); - - const videoUrl = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4'; - - useEffect(() => { - if (!shouldStart || !containerRef.current) return; - - const tl = gsap.timeline({ - onComplete: () => { - setTimeout(() => { - // 淡出视频 - gsap.to(containerRef.current, { - opacity: 0, - scale: 0.9, - duration: 0.3, - onComplete - }); - }, 3000); - } - }); - - // 显示容器 - tl.fromTo(containerRef.current, - { opacity: 0, scale: 0.9 }, - { opacity: 1, scale: 1, duration: 0.3 } - ); - - // 显示视频 - setShowVideo(true); - - }, [shouldStart, onComplete]); - - return ( - - {showVideo && ( - - )} - - ); -}; - -// 主组件 -export const EmptyStateAnimation = ({ className }: { className: string }) => { - const [currentStage, setCurrentStage] = useState<'input' | 'wave' | 'final'>('input'); - const [animationCycle, setAnimationCycle] = useState(0); - const [isReady, setIsReady] = useState(true); - - const handleStageComplete = useCallback(() => { - // 先将当前阶段标记为不可执行 - setIsReady(false); - - // 延迟切换到下一个阶段 - setTimeout(() => { - switch (currentStage) { - case 'input': - setCurrentStage('wave'); - break; - case 'wave': - setCurrentStage('final'); - break; - case 'final': - setAnimationCycle(prev => prev + 1); - setCurrentStage('input'); - break; - } - - // 给下一个阶段一些准备时间 - setTimeout(() => { - setIsReady(true); - }, 100); - }, 300); - }, [currentStage]); - - return ( - - - - - - ); -}; \ No newline at end of file diff --git a/components/pages/history-page.tsx b/components/pages/history-page.tsx deleted file mode 100644 index c590883..0000000 --- a/components/pages/history-page.tsx +++ /dev/null @@ -1,224 +0,0 @@ -"use client"; - -import { useState, useEffect } from 'react'; -import { Card } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { Input } from '@/components/ui/input'; -import { - Search, - Filter, - Video, - Calendar, - Eye, - Edit, - Trash2, - MoreHorizontal, - Play -} from 'lucide-react'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger -} from '@/components/ui/dropdown-menu'; - -interface HistoryItem { - id: number; - title: string; - type: 'script-to-video' | 'video-to-video'; - status: 'completed' | 'processing' | 'failed'; - createdAt: string; - duration?: string; - thumbnail?: string; -} - -export function HistoryPage() { - const [searchTerm, setSearchTerm] = useState(''); - const [historyItems, setHistoryItems] = useState([]); - const [filteredItems, setFilteredItems] = useState([]); - - // Mock data - replace with actual API call - useEffect(() => { - const mockData: HistoryItem[] = [ - { - id: 1, - title: "Sample Script Video", - type: "script-to-video", - status: "completed", - createdAt: "2024-01-15", - duration: "2:30", - thumbnail: "/assets/empty_video.png" - }, - { - id: 2, - title: "Video Enhancement Project", - type: "video-to-video", - status: "processing", - createdAt: "2024-01-14", - duration: "1:45" - }, - { - id: 3, - title: "Marketing Video", - type: "script-to-video", - status: "completed", - createdAt: "2024-01-13", - duration: "3:15", - thumbnail: "/assets/empty_video.png" - } - ]; - setHistoryItems(mockData); - setFilteredItems(mockData); - }, []); - - // Filter items based on search term - useEffect(() => { - const filtered = historyItems.filter(item => - item.title.toLowerCase().includes(searchTerm.toLowerCase()) - ); - setFilteredItems(filtered); - }, [searchTerm, historyItems]); - - const getStatusBadgeVariant = (status: string) => { - switch (status) { - case 'completed': - return 'default'; - case 'processing': - return 'secondary'; - case 'failed': - return 'destructive'; - default: - return 'default'; - } - }; - - const getTypeIcon = (type: string) => { - return type === 'script-to-video' ? '📝' : '🎥'; - }; - - return ( - - {/* Header */} - - - Project History - View and manage your video projects - - - {/* Search and Filter */} - - - - setSearchTerm(e.target.value)} - className="pl-10 bg-white/5 border-white/20 text-white placeholder:text-white/40" - /> - - - - Filter - - - - - {/* Project Grid */} - {filteredItems.length === 0 ? ( - - - - {searchTerm ? 'No projects found' : 'No projects yet'} - - - {searchTerm - ? 'Try adjusting your search terms' - : 'Create your first video project to see it here' - } - - - ) : ( - - {filteredItems.map((item) => ( - - {/* Thumbnail */} - - {item.thumbnail ? ( - - ) : ( - - - - )} - - {/* Play overlay */} - - - - - - - {/* Type indicator */} - - {getTypeIcon(item.type)} - - - {/* Duration */} - {item.duration && ( - - {item.duration} - - )} - - - {/* Content */} - - - - {item.title} - - - - - - - - - - - View - - - - Edit - - - - Delete - - - - - - - - - {item.createdAt} - - - {item.status} - - - - - ))} - - )} - - ); -} \ No newline at end of file diff --git a/components/pages/home-page2.tsx b/components/pages/home-page2.tsx index 77b8644..9a55abf 100644 --- a/components/pages/home-page2.tsx +++ b/components/pages/home-page2.tsx @@ -88,8 +88,8 @@ export function HomePage2() { const projectData: CreateScriptProjectRequest = { title: "script default", // 默认剧本名称 project_type: projectType, - mode: ModeEnum.MANUAL, - resolution: ResolutionEnum.FULL_HD_1080P + mode: ModeEnum.MANUAL === 'manual' ? 1 : 2, // 1 表示手动模式,2 表示自动模式 + resolution: 1080 // 1080p 分辨率 }; const projectResponse = await createScriptProject(projectData); @@ -121,7 +121,7 @@ export function HomePage2() { {/* 工具栏-列表形式切换 */} { + onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleToolChange(activeTool === "stretch" ? "right" : "left"); }} diff --git a/components/pages/script-to-video.tsx b/components/pages/script-to-video.tsx deleted file mode 100644 index bf8f914..0000000 --- a/components/pages/script-to-video.tsx +++ /dev/null @@ -1,168 +0,0 @@ -"use client"; - -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { Card } from '@/components/ui/card'; -import { Textarea } from '@/components/ui/textarea'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { ArrowLeft, Loader2 } from 'lucide-react'; -import { useRouter } from 'next/navigation'; -import { ModeEnum, ResolutionEnum } from "@/app/model/enums"; -import { createScriptEpisode, CreateScriptEpisodeRequest, updateScriptEpisode, UpdateScriptEpisodeRequest } from "@/api/script_episode"; -import { convertScriptToScene } from "@/api/video_flow"; - -export function ScriptToVideo() { - const router = useRouter(); - const [script, setScript] = useState(''); - const [selectedMode, setSelectedMode] = useState(ModeEnum.AUTOMATIC); - const [selectedResolution, setSelectedResolution] = useState(ResolutionEnum.HD_720P); - const [isCreating, setIsCreating] = useState(false); - - const handleBack = () => { - router.push('/create'); - }; - - const handleCreate = async () => { - if (!script.trim()) { - alert('请输入剧本内容'); - return; - } - - try { - setIsCreating(true); - - // Create episode - const episodeData: CreateScriptEpisodeRequest = { - title: "Script Episode", - script_id: 0, // This should come from a project - status: 1, - summary: script - }; - - const episodeResponse = await createScriptEpisode(episodeData); - if (episodeResponse.code !== 0) { - alert(`创建剧集失败: ${episodeResponse.message}`); - return; - } - - const episodeId = episodeResponse.data.id; - - // Convert script to scenes - const convertResponse = await convertScriptToScene(script, episodeId, 0); - - if (convertResponse.code === 0) { - // Update episode with generated data - const updateEpisodeData: UpdateScriptEpisodeRequest = { - id: episodeId, - atmosphere: convertResponse.data.atmosphere, - summary: convertResponse.data.summary, - scene: convertResponse.data.scene, - characters: convertResponse.data.characters, - }; - - await updateScriptEpisode(updateEpisodeData); - - // Navigate to workflow - router.push(`/create/work-flow?episodeId=${episodeId}`); - } else { - alert(`转换失败: ${convertResponse.message}`); - } - } catch (error) { - console.error('创建过程出错:', error); - alert("创建项目时发生错误,请稍后重试"); - } finally { - setIsCreating(false); - } - }; - - return ( - - - {/* Header */} - - - - - Script to Video - - - {/* Main Content */} - - - {/* Script Input */} - - - Script Content - - setScript(e.target.value)} - placeholder="Enter your script here..." - className="min-h-[200px] bg-white/5 border-white/20 text-white placeholder:text-white/50" - rows={10} - /> - - - {/* Settings */} - - - - Mode - - setSelectedMode(Number(value) as ModeEnum)}> - - - - - Automatic - Manual - - - - - - - Resolution - - setSelectedResolution(Number(value) as ResolutionEnum)}> - - - - - 720P HD - 1080P Full HD - 2K UHD - 4K UHD - - - - - - {/* Create Button */} - - - {isCreating ? ( - <> - - Creating... - > - ) : ( - 'Create Video' - )} - - - - - - - ); -} \ No newline at end of file diff --git a/components/pages/video-to-video.tsx b/components/pages/video-to-video.tsx deleted file mode 100644 index ee9a341..0000000 --- a/components/pages/video-to-video.tsx +++ /dev/null @@ -1,233 +0,0 @@ -"use client"; - -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { Card } from '@/components/ui/card'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { ArrowLeft, Upload, Loader2 } from 'lucide-react'; -import { useRouter } from 'next/navigation'; -import { ModeEnum, ResolutionEnum } from "@/app/model/enums"; -import { createScriptEpisode, CreateScriptEpisodeRequest, updateScriptEpisode, UpdateScriptEpisodeRequest } from "@/api/script_episode"; -import { convertVideoToScene } from "@/api/video_flow"; -import { getUploadToken, uploadToQiniu } from "@/api/common"; - -export function VideoToVideo() { - const router = useRouter(); - const [videoUrl, setVideoUrl] = useState(''); - const [selectedMode, setSelectedMode] = useState(ModeEnum.AUTOMATIC); - const [selectedResolution, setSelectedResolution] = useState(ResolutionEnum.HD_720P); - const [isUploading, setIsUploading] = useState(false); - const [isCreating, setIsCreating] = useState(false); - - const handleBack = () => { - router.push('/create'); - }; - - const handleUploadVideo = async () => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = 'video/*'; - input.onchange = async (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (file) { - try { - setIsUploading(true); - - // Get upload token - const { token } = await getUploadToken(); - - // Upload to Qiniu - const uploadedVideoUrl = await uploadToQiniu(file, token); - - setVideoUrl(uploadedVideoUrl); - } catch (error) { - console.error('Upload error:', error); - alert('Upload failed, please try again'); - } finally { - setIsUploading(false); - } - } - }; - input.click(); - }; - - const handleCreate = async () => { - if (!videoUrl) { - alert('请先上传视频'); - return; - } - - try { - setIsCreating(true); - - // Create episode - const episodeData: CreateScriptEpisodeRequest = { - title: "Video Episode", - script_id: 0, // This should come from a project - status: 1, - summary: "Video conversion" - }; - - const episodeResponse = await createScriptEpisode(episodeData); - if (episodeResponse.code !== 0) { - alert(`创建剧集失败: ${episodeResponse.message}`); - return; - } - - const episodeId = episodeResponse.data.id; - - // Convert video to scenes - const convertResponse = await convertVideoToScene(videoUrl, episodeId, 0); - - if (convertResponse.code === 0) { - // Update episode with generated data - const updateEpisodeData: UpdateScriptEpisodeRequest = { - id: episodeId, - atmosphere: convertResponse.data.atmosphere, - summary: convertResponse.data.summary, - scene: convertResponse.data.scene, - characters: convertResponse.data.characters, - }; - - await updateScriptEpisode(updateEpisodeData); - - // Navigate to workflow - router.push(`/create/work-flow?episodeId=${episodeId}`); - } else { - alert(`转换失败: ${convertResponse.message}`); - } - } catch (error) { - console.error('创建过程出错:', error); - alert("创建项目时发生错误,请稍后重试"); - } finally { - setIsCreating(false); - } - }; - - return ( - - - {/* Header */} - - - - - Video to Video - - - {/* Main Content */} - - - {/* Video Upload */} - - - Upload Video - - - {videoUrl ? ( - - - - Replace Video - - - ) : ( - - - - Click to upload a video file - - {isUploading ? ( - <> - - Uploading... - > - ) : ( - <> - - Choose Video - > - )} - - - - )} - - - - {/* Settings */} - - - - Mode - - setSelectedMode(Number(value) as ModeEnum)}> - - - - - Automatic - Manual - - - - - - - Resolution - - setSelectedResolution(Number(value) as ResolutionEnum)}> - - - - - 720P HD - 1080P Full HD - 2K UHD - 4K UHD - - - - - - {/* Create Button */} - - - {isCreating ? ( - <> - - Creating... - > - ) : ( - 'Create Video' - )} - - - - - - - ); -} \ No newline at end of file diff --git a/components/pages/work-flow.tsx b/components/pages/work-flow.tsx index 8bcf00b..10293ba 100644 --- a/components/pages/work-flow.tsx +++ b/components/pages/work-flow.tsx @@ -8,7 +8,7 @@ import { ErrorBoundary } from "@/components/ui/error-boundary"; import { TaskInfo } from "./work-flow/task-info"; import { MediaViewer } from "./work-flow/media-viewer"; import { ThumbnailGrid } from "./work-flow/thumbnail-grid"; -import { useWorkflowData } from "./work-flow/use-workflow-data.ts"; +import { useWorkflowData } from "./work-flow/use-workflow-data"; import { usePlaybackControls } from "./work-flow/use-playback-controls"; import { AlertCircle, RefreshCw } from "lucide-react"; import { motion } from "framer-motion"; diff --git a/components/pages/work-flow/use-api-data.ts b/components/pages/work-flow/use-api-data.ts index 0d48c5c..f726e56 100644 --- a/components/pages/work-flow/use-api-data.ts +++ b/components/pages/work-flow/use-api-data.ts @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, StreamData } from '@/api/video_flow'; import { useSearchParams } from 'next/navigation'; +import { ApiResponse } from '@/api/common'; // 步骤映射 const STEP_MAP = { @@ -56,8 +57,8 @@ export const useApiData = () => { const [streamInterval, setStreamInterval] = useState(null); // 处理流式数据 - const handleStreamData = useCallback((streamData: StreamData) => { - const { category, message, data, status } = streamData; + const handleStreamData = useCallback((streamData: ApiResponse) => { + const { category, message, data, status } = streamData.data; // 更新加载文本 setCurrentLoadingText(message); @@ -114,7 +115,7 @@ export const useApiData = () => { }); // 如果状态为 completed,停止获取流式数据 - if (status === 'completed' || streamData.all_completed) { + if (status === 'completed' || streamData.data.all_completed) { setNeedStreamData(false); } }, []); @@ -124,7 +125,7 @@ export const useApiData = () => { if (!episodeId || !needStreamData) return; try { - const streamData = await getRunningStreamData({ episodeId }); + const streamData = await getRunningStreamData({ project_id: episodeId }); handleStreamData(streamData); } catch (error) { console.error('获取流式数据失败:', error); diff --git a/components/pages/work-flow/use-workflow-data.ts b/components/pages/work-flow/use-workflow-data.ts deleted file mode 100644 index 4b566f1..0000000 --- a/components/pages/work-flow/use-workflow-data.ts +++ /dev/null @@ -1,406 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { useSearchParams } from 'next/navigation'; -import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData } from '@/api/video_flow'; - -// 步骤映射 -const STEP_MAP = { - 'initializing': '0', - 'sketch': '1', - 'character': '2', - 'video': '3', - 'music': '4', - 'final_video': '6' -} as const; -// 执行loading文字映射 -const LOADING_TEXT_MAP = { - initializing: 'initializing...', - sketch: (count: number, total: number) => `Generating sketch ${count + 1 > total ? total : count + 1}/${total}...`, - sketchComplete: 'Sketch generation complete', - character: 'Drawing characters...', - newCharacter: (count: number, total: number) => `Drawing character ${count + 1 > total ? total : count + 1}/${total}...`, - video: (count: number, total: number) => `Generating video ${count + 1 > total ? total : count + 1}/${total}...`, - videoComplete: 'Video generation complete', - audio: 'Generating background audio...', - postProduction: (step: string) => `Post-production: ${step}...`, - final: 'Generating final product...', - complete: 'Task completed' -} as const; - -type ApiStep = keyof typeof STEP_MAP; - -// 添加 TaskObject 接口 -interface TaskObject { - taskStatus: string; - title: string; - currentLoadingText: string; - sketchCount?: number; - totalSketchCount?: number; - isGeneratingSketch?: boolean; - isGeneratingVideo?: boolean; - roles?: any[]; - music?: any[]; - final?: any; -} - -export function useWorkflowData() { - const searchParams = useSearchParams(); - const episodeId = searchParams.get('episodeId'); - - // 更新 taskObject 的类型 - const [taskObject, setTaskObject] = useState(null); - const [taskSketch, setTaskSketch] = useState([]); - const [taskVideos, setTaskVideos] = useState([]); - const [sketchCount, setSketchCount] = useState(0); - const [isLoading, setIsLoading] = useState(true); - const [currentStep, setCurrentStep] = useState('0'); - const [currentSketchIndex, setCurrentSketchIndex] = useState(0); - const [isGeneratingSketch, setIsGeneratingSketch] = useState(false); - const [isGeneratingVideo, setIsGeneratingVideo] = useState(false); - const [currentLoadingText, setCurrentLoadingText] = useState('正在加载项目数据...'); - const [totalSketchCount, setTotalSketchCount] = useState(0); - const [roles, setRoles] = useState([]); - const [music, setMusic] = useState([]); - const [final, setFinal] = useState(null); - const [dataLoadError, setDataLoadError] = useState(null); - const [needStreamData, setNeedStreamData] = useState(false); - - // 获取流式数据 - const fetchStreamData = async () => { - if (!episodeId || !needStreamData) return; - - try { - const response = await getRunningStreamData({ project_id: episodeId }); - if (!response.successful) { - throw new Error(response.message); - } - - let loadingText: any = LOADING_TEXT_MAP.initializing; - let finalStep = '1', sketchCount = 0; - const all_task_data = response.data; - // all_task_data 下标0 和 下标1 换位置 - const temp = all_task_data[0]; - all_task_data[0] = all_task_data[1]; - all_task_data[1] = temp; - - console.log('all_task_data', all_task_data); - for (const task of all_task_data) { - - // 如果有已完成的数据,同步到状态 - if (task.task_name === 'generate_sketch' && task.task_result) { - if (task.task_result.data.length >= 0 && taskSketch.length !== task.task_result.data.length) { - // 正在生成草图中 替换 sketch 数据 - const sketchList = []; - for (const sketch of task.task_result.data) { - sketchList.push({ - url: sketch.image_path, - script: sketch.sketch_name - }); - } - setTaskSketch(sketchList); - setSketchCount(sketchList.length); - setIsGeneratingSketch(true); - setCurrentSketchIndex(sketchList.length - 1); - loadingText = LOADING_TEXT_MAP.sketch(sketchList.length, task.task_result.total_count); - } - if (task.task_status === 'COMPLETED') { - // 草图生成完成 - setIsGeneratingSketch(false); - sketchCount = task.task_result.total_count; - console.log('----------草图生成完成', sketchCount); - loadingText = LOADING_TEXT_MAP.sketchComplete; - finalStep = '2'; - } - setTotalSketchCount(task.task_result.total_count); - } - if (task.task_name === 'generate_character' && task.task_result) { - if (task.task_result.data.length >= 0 && roles.length !== task.task_result.data.length) { - // 正在生成角色中 替换角色数据 - const characterList = []; - for (const character of task.task_result.data) { - characterList.push({ - name: character.character_name, - url: character.image_path, - sound: null, - soundDescription: '', - roleDescription: '' - }); - } - setRoles(characterList); - loadingText = LOADING_TEXT_MAP.newCharacter(characterList.length, task.task_result.total_count); - } - if (task.task_status === 'COMPLETED') { - console.log('----------角色生成完成,有几个分镜', sketchCount); - // 角色生成完成 - finalStep = '3'; - - loadingText = LOADING_TEXT_MAP.video(0, sketchCount); - } - } - if (task.task_name === 'generate_videos' && task.task_result) { - if (task.task_result.data.length >= 0 && taskVideos.length !== task.task_result.data.length) { - // 正在生成视频中 替换视频数据 - const videoList = []; - for (const video of task.task_result.data) { - // 每一项 video 有多个视频 先默认取第一个 - videoList.push({ - url: video[0].qiniuVideoUrl, - script: video[0].operation.metadata.video.prompt, - audio: null, - }); - } - setTaskVideos(videoList); - setIsGeneratingVideo(true); - setCurrentSketchIndex(videoList.length - 1); - loadingText = LOADING_TEXT_MAP.video(videoList.length, task.task_result.total_count); - } - if (task.task_status === 'COMPLETED') { - console.log('----------视频生成完成'); - // 视频生成完成 - setIsGeneratingVideo(false); - finalStep = '4'; - - // 暂时没有音频生成 直接跳过 - finalStep = '5'; - loadingText = LOADING_TEXT_MAP.postProduction('post-production...'); - } - } - if (task.task_name === 'generate_final_video') { - if (task.task_result && task.task_result.video) { - setFinal({ - url: task.task_result.video, - }) - finalStep = '6'; - loadingText = LOADING_TEXT_MAP.complete; - - // 停止轮询 - setNeedStreamData(false); - } - } - } - - console.log('----------finalStep', finalStep); - // 设置步骤 - setCurrentStep(finalStep); - setTaskObject(prev => { - if (!prev) return null; - return { - ...prev, - taskStatus: finalStep - }; - }); - setCurrentLoadingText(loadingText); - - } catch (error) { - console.error('获取数据失败:', error); - } - }; - - // 轮询获取流式数据 - useEffect(() => { - let interval: NodeJS.Timeout; - - if (needStreamData) { - interval = setInterval(fetchStreamData, 10000); - fetchStreamData(); // 立即执行一次 - } - - return () => { - if (interval) { - clearInterval(interval); - } - }; - }, [needStreamData]); - - // 初始化数据 - const initializeWorkflow = async () => { - if (!episodeId) { - setDataLoadError('缺少必要的参数'); - setIsLoading(false); - return; - } - - try { - setIsLoading(true); - setCurrentLoadingText('正在加载项目数据...'); - - // 获取剧集详情 - const response = await detailScriptEpisodeNew({ project_id: episodeId }); - if (!response.successful) { - throw new Error(response.message); - } - - const { name, status, data } = response.data; - setIsLoading(false); - - // 设置初始数据 - setTaskObject({ - taskStatus: '0', - title: name || 'generating...', - currentLoadingText: status === 'COMPLETED' ? LOADING_TEXT_MAP.complete : LOADING_TEXT_MAP.initializing - }); - - // 设置标题 - if (!name) { - // 如果没有标题,轮询获取 - const titleResponse = await getScriptTitle({ project_id: episodeId }); - console.log('titleResponse', titleResponse); - if (titleResponse.successful) { - setTaskObject((prev: TaskObject | null) => ({ - ...(prev || {}), - title: titleResponse.data.name - } as TaskObject)); - } - } - - let loadingText: any = LOADING_TEXT_MAP.initializing; - if (status === 'COMPLETED') { - loadingText = LOADING_TEXT_MAP.complete; - } - - // 如果有已完成的数据,同步到状态 - let finalStep = '1'; - if (data) { - if (data.sketch && data.sketch.data && data.sketch.data.length > 0) { - const sketchList = []; - for (const sketch of data.sketch.data) { - sketchList.push({ - url: sketch.image_path, - script: sketch.sketch_name, - }); - } - setTaskSketch(sketchList); - setSketchCount(sketchList.length); - setTotalSketchCount(data.sketch.total_count); - // 设置为最后一个草图 - if (data.sketch.total_count > data.sketch.data.length) { - setIsGeneratingSketch(true); - setCurrentSketchIndex(data.sketch.data.length - 1); - loadingText = LOADING_TEXT_MAP.sketch(data.sketch.data.length, data.sketch.total_count); - } else { - finalStep = '2'; - if (!data.character || !data.character.data || !data.character.data.length) { - loadingText = LOADING_TEXT_MAP.newCharacter(0, data.character.total_count); - } - } - } - if (data.character && data.character.data && data.character.data.length > 0) { - const characterList = []; - for (const character of data.character.data) { - characterList.push({ - name: character.character_name, - url: character.image_path, - sound: null, - soundDescription: '', - roleDescription: '' - }); - } - setRoles(characterList); - if (data.character.total_count > data.character.data.length) { - loadingText = LOADING_TEXT_MAP.newCharacter(data.character.data.length, data.character.total_count); - } else { - finalStep = '3'; - if (!data.video || !data.video.data || !data.video.data.length) { - loadingText = LOADING_TEXT_MAP.video(0, data.video.total_count || data.sketch.total_count); - } - } - } - if (data.video && data.video.data && data.video.data.length > 0) { - const videoList = []; - for (const video of data.video.data) { - // 每一项 video 有多个视频 先默认取第一个 - videoList.push({ - url: video[0].qiniuVideoUrl, - script: video[0].operation.metadata.video.prompt, - audio: null, - }); - } - setTaskVideos(videoList); - // 如果在视频步骤,设置为最后一个视频 - if (data.video.total_count > data.video.data.length) { - setIsGeneratingVideo(true); - setCurrentSketchIndex(data.video.data.length - 1); - loadingText = LOADING_TEXT_MAP.video(data.video.data.length, data.video.total_count); - } else { - finalStep = '4'; - loadingText = LOADING_TEXT_MAP.audio; - - // 暂时没有音频生成 直接跳过 - finalStep = '5'; - loadingText = LOADING_TEXT_MAP.postProduction('post-production...'); - } - } - if (data.final_video && data.final_video.video) { - setFinal({ - url: data.final_video.video - }); - finalStep = '6'; - loadingText = LOADING_TEXT_MAP.complete; - } - } - - // 设置步骤 - setCurrentStep(finalStep); - setTaskObject(prev => { - if (!prev) return null; - return { - ...prev, - taskStatus: finalStep - }; - }); - console.log('---------loadingText', loadingText); - setCurrentLoadingText(loadingText); - - // 设置是否需要获取流式数据 - setNeedStreamData(status !== 'COMPLETED' && finalStep !== '6'); - - } catch (error) { - console.error('初始化失败:', error); - setDataLoadError('加载失败,请重试'); - setIsLoading(false); - } - }; - - // 重试加载数据 - const retryLoadData = () => { - setDataLoadError(null); - // 重置所有状态 - setTaskSketch([]); - setTaskVideos([]); - setSketchCount(0); - setTotalSketchCount(0); - setRoles([]); - setMusic([]); - setFinal(null); - setCurrentSketchIndex(0); - setCurrentStep('0'); - // 重新初始化 - initializeWorkflow(); - }; - - // 初始化 - useEffect(() => { - initializeWorkflow(); - }, [episodeId]); - - return { - taskObject, - taskSketch, - taskVideos, - sketchCount, - isLoading, - currentStep, - currentSketchIndex, - isGeneratingSketch, - isGeneratingVideo, - currentLoadingText, - totalSketchCount, - roles, - music, - final, - dataLoadError, - setCurrentSketchIndex, - retryLoadData, - }; -} \ No newline at end of file diff --git a/components/pages/work-flow/use-workflow-data.tsx b/components/pages/work-flow/use-workflow-data.tsx index 5610a08..4b566f1 100644 --- a/components/pages/work-flow/use-workflow-data.tsx +++ b/components/pages/work-flow/use-workflow-data.tsx @@ -1,15 +1,56 @@ 'use client'; -import { useState, useEffect, useRef, useCallback } from 'react'; -import { getRandomMockData, STEP_MESSAGES, MOCK_DELAY_TIME, MOCK_DATA } from '@/components/work-flow/constants'; +import { useState, useEffect } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData } from '@/api/video_flow'; -// 当前选择的mock数据 -let selectedMockData: any = null; +// 步骤映射 +const STEP_MAP = { + 'initializing': '0', + 'sketch': '1', + 'character': '2', + 'video': '3', + 'music': '4', + 'final_video': '6' +} as const; +// 执行loading文字映射 +const LOADING_TEXT_MAP = { + initializing: 'initializing...', + sketch: (count: number, total: number) => `Generating sketch ${count + 1 > total ? total : count + 1}/${total}...`, + sketchComplete: 'Sketch generation complete', + character: 'Drawing characters...', + newCharacter: (count: number, total: number) => `Drawing character ${count + 1 > total ? total : count + 1}/${total}...`, + video: (count: number, total: number) => `Generating video ${count + 1 > total ? total : count + 1}/${total}...`, + videoComplete: 'Video generation complete', + audio: 'Generating background audio...', + postProduction: (step: string) => `Post-production: ${step}...`, + final: 'Generating final product...', + complete: 'Task completed' +} as const; + +type ApiStep = keyof typeof STEP_MAP; + +// 添加 TaskObject 接口 +interface TaskObject { + taskStatus: string; + title: string; + currentLoadingText: string; + sketchCount?: number; + totalSketchCount?: number; + isGeneratingSketch?: boolean; + isGeneratingVideo?: boolean; + roles?: any[]; + music?: any[]; + final?: any; +} export function useWorkflowData() { - const [taskObject, setTaskObject] = useState(null); + const searchParams = useSearchParams(); + const episodeId = searchParams.get('episodeId'); + + // 更新 taskObject 的类型 + const [taskObject, setTaskObject] = useState(null); const [taskSketch, setTaskSketch] = useState([]); - const [taskRoles, setTaskRoles] = useState([]); const [taskVideos, setTaskVideos] = useState([]); const [sketchCount, setSketchCount] = useState(0); const [isLoading, setIsLoading] = useState(true); @@ -18,321 +59,347 @@ export function useWorkflowData() { const [isGeneratingSketch, setIsGeneratingSketch] = useState(false); const [isGeneratingVideo, setIsGeneratingVideo] = useState(false); const [currentLoadingText, setCurrentLoadingText] = useState('正在加载项目数据...'); + const [totalSketchCount, setTotalSketchCount] = useState(0); + const [roles, setRoles] = useState([]); + const [music, setMusic] = useState([]); + const [final, setFinal] = useState(null); const [dataLoadError, setDataLoadError] = useState(null); - const [isLoadingData, setIsLoadingData] = useState(false); + const [needStreamData, setNeedStreamData] = useState(false); + + // 获取流式数据 + const fetchStreamData = async () => { + if (!episodeId || !needStreamData) return; - // 异步加载数据 - 改进错误处理和fallback机制 - const loadMockData = async () => { try { - setIsLoadingData(true); - setDataLoadError(null); - setCurrentLoadingText('正在从服务器获取项目数据...'); - - // 尝试从接口获取数据 - selectedMockData = await getRandomMockData(); - - console.log('成功从接口获取数据:', selectedMockData); - setCurrentLoadingText('项目数据加载完成'); - + const response = await getRunningStreamData({ project_id: episodeId }); + if (!response.successful) { + throw new Error(response.message); + } + + let loadingText: any = LOADING_TEXT_MAP.initializing; + let finalStep = '1', sketchCount = 0; + const all_task_data = response.data; + // all_task_data 下标0 和 下标1 换位置 + const temp = all_task_data[0]; + all_task_data[0] = all_task_data[1]; + all_task_data[1] = temp; + + console.log('all_task_data', all_task_data); + for (const task of all_task_data) { + + // 如果有已完成的数据,同步到状态 + if (task.task_name === 'generate_sketch' && task.task_result) { + if (task.task_result.data.length >= 0 && taskSketch.length !== task.task_result.data.length) { + // 正在生成草图中 替换 sketch 数据 + const sketchList = []; + for (const sketch of task.task_result.data) { + sketchList.push({ + url: sketch.image_path, + script: sketch.sketch_name + }); + } + setTaskSketch(sketchList); + setSketchCount(sketchList.length); + setIsGeneratingSketch(true); + setCurrentSketchIndex(sketchList.length - 1); + loadingText = LOADING_TEXT_MAP.sketch(sketchList.length, task.task_result.total_count); + } + if (task.task_status === 'COMPLETED') { + // 草图生成完成 + setIsGeneratingSketch(false); + sketchCount = task.task_result.total_count; + console.log('----------草图生成完成', sketchCount); + loadingText = LOADING_TEXT_MAP.sketchComplete; + finalStep = '2'; + } + setTotalSketchCount(task.task_result.total_count); + } + if (task.task_name === 'generate_character' && task.task_result) { + if (task.task_result.data.length >= 0 && roles.length !== task.task_result.data.length) { + // 正在生成角色中 替换角色数据 + const characterList = []; + for (const character of task.task_result.data) { + characterList.push({ + name: character.character_name, + url: character.image_path, + sound: null, + soundDescription: '', + roleDescription: '' + }); + } + setRoles(characterList); + loadingText = LOADING_TEXT_MAP.newCharacter(characterList.length, task.task_result.total_count); + } + if (task.task_status === 'COMPLETED') { + console.log('----------角色生成完成,有几个分镜', sketchCount); + // 角色生成完成 + finalStep = '3'; + + loadingText = LOADING_TEXT_MAP.video(0, sketchCount); + } + } + if (task.task_name === 'generate_videos' && task.task_result) { + if (task.task_result.data.length >= 0 && taskVideos.length !== task.task_result.data.length) { + // 正在生成视频中 替换视频数据 + const videoList = []; + for (const video of task.task_result.data) { + // 每一项 video 有多个视频 先默认取第一个 + videoList.push({ + url: video[0].qiniuVideoUrl, + script: video[0].operation.metadata.video.prompt, + audio: null, + }); + } + setTaskVideos(videoList); + setIsGeneratingVideo(true); + setCurrentSketchIndex(videoList.length - 1); + loadingText = LOADING_TEXT_MAP.video(videoList.length, task.task_result.total_count); + } + if (task.task_status === 'COMPLETED') { + console.log('----------视频生成完成'); + // 视频生成完成 + setIsGeneratingVideo(false); + finalStep = '4'; + + // 暂时没有音频生成 直接跳过 + finalStep = '5'; + loadingText = LOADING_TEXT_MAP.postProduction('post-production...'); + } + } + if (task.task_name === 'generate_final_video') { + if (task.task_result && task.task_result.video) { + setFinal({ + url: task.task_result.video, + }) + finalStep = '6'; + loadingText = LOADING_TEXT_MAP.complete; + + // 停止轮询 + setNeedStreamData(false); + } + } + } + + console.log('----------finalStep', finalStep); + // 设置步骤 + setCurrentStep(finalStep); + setTaskObject(prev => { + if (!prev) return null; + return { + ...prev, + taskStatus: finalStep + }; + }); + setCurrentLoadingText(loadingText); + } catch (error) { - // 报错 - } finally { - setIsLoadingData(false); + console.error('获取数据失败:', error); + } + }; + + // 轮询获取流式数据 + useEffect(() => { + let interval: NodeJS.Timeout; + + if (needStreamData) { + interval = setInterval(fetchStreamData, 10000); + fetchStreamData(); // 立即执行一次 + } + + return () => { + if (interval) { + clearInterval(interval); + } + }; + }, [needStreamData]); + + // 初始化数据 + const initializeWorkflow = async () => { + if (!episodeId) { + setDataLoadError('缺少必要的参数'); + setIsLoading(false); + return; + } + + try { + setIsLoading(true); + setCurrentLoadingText('正在加载项目数据...'); + + // 获取剧集详情 + const response = await detailScriptEpisodeNew({ project_id: episodeId }); + if (!response.successful) { + throw new Error(response.message); + } + + const { name, status, data } = response.data; + setIsLoading(false); + + // 设置初始数据 + setTaskObject({ + taskStatus: '0', + title: name || 'generating...', + currentLoadingText: status === 'COMPLETED' ? LOADING_TEXT_MAP.complete : LOADING_TEXT_MAP.initializing + }); + + // 设置标题 + if (!name) { + // 如果没有标题,轮询获取 + const titleResponse = await getScriptTitle({ project_id: episodeId }); + console.log('titleResponse', titleResponse); + if (titleResponse.successful) { + setTaskObject((prev: TaskObject | null) => ({ + ...(prev || {}), + title: titleResponse.data.name + } as TaskObject)); + } + } + + let loadingText: any = LOADING_TEXT_MAP.initializing; + if (status === 'COMPLETED') { + loadingText = LOADING_TEXT_MAP.complete; + } + + // 如果有已完成的数据,同步到状态 + let finalStep = '1'; + if (data) { + if (data.sketch && data.sketch.data && data.sketch.data.length > 0) { + const sketchList = []; + for (const sketch of data.sketch.data) { + sketchList.push({ + url: sketch.image_path, + script: sketch.sketch_name, + }); + } + setTaskSketch(sketchList); + setSketchCount(sketchList.length); + setTotalSketchCount(data.sketch.total_count); + // 设置为最后一个草图 + if (data.sketch.total_count > data.sketch.data.length) { + setIsGeneratingSketch(true); + setCurrentSketchIndex(data.sketch.data.length - 1); + loadingText = LOADING_TEXT_MAP.sketch(data.sketch.data.length, data.sketch.total_count); + } else { + finalStep = '2'; + if (!data.character || !data.character.data || !data.character.data.length) { + loadingText = LOADING_TEXT_MAP.newCharacter(0, data.character.total_count); + } + } + } + if (data.character && data.character.data && data.character.data.length > 0) { + const characterList = []; + for (const character of data.character.data) { + characterList.push({ + name: character.character_name, + url: character.image_path, + sound: null, + soundDescription: '', + roleDescription: '' + }); + } + setRoles(characterList); + if (data.character.total_count > data.character.data.length) { + loadingText = LOADING_TEXT_MAP.newCharacter(data.character.data.length, data.character.total_count); + } else { + finalStep = '3'; + if (!data.video || !data.video.data || !data.video.data.length) { + loadingText = LOADING_TEXT_MAP.video(0, data.video.total_count || data.sketch.total_count); + } + } + } + if (data.video && data.video.data && data.video.data.length > 0) { + const videoList = []; + for (const video of data.video.data) { + // 每一项 video 有多个视频 先默认取第一个 + videoList.push({ + url: video[0].qiniuVideoUrl, + script: video[0].operation.metadata.video.prompt, + audio: null, + }); + } + setTaskVideos(videoList); + // 如果在视频步骤,设置为最后一个视频 + if (data.video.total_count > data.video.data.length) { + setIsGeneratingVideo(true); + setCurrentSketchIndex(data.video.data.length - 1); + loadingText = LOADING_TEXT_MAP.video(data.video.data.length, data.video.total_count); + } else { + finalStep = '4'; + loadingText = LOADING_TEXT_MAP.audio; + + // 暂时没有音频生成 直接跳过 + finalStep = '5'; + loadingText = LOADING_TEXT_MAP.postProduction('post-production...'); + } + } + if (data.final_video && data.final_video.video) { + setFinal({ + url: data.final_video.video + }); + finalStep = '6'; + loadingText = LOADING_TEXT_MAP.complete; + } + } + + // 设置步骤 + setCurrentStep(finalStep); + setTaskObject(prev => { + if (!prev) return null; + return { + ...prev, + taskStatus: finalStep + }; + }); + console.log('---------loadingText', loadingText); + setCurrentLoadingText(loadingText); + + // 设置是否需要获取流式数据 + setNeedStreamData(status !== 'COMPLETED' && finalStep !== '6'); + + } catch (error) { + console.error('初始化失败:', error); + setDataLoadError('加载失败,请重试'); + setIsLoading(false); } }; // 重试加载数据 - const retryLoadData = async () => { - console.log('用户点击重试,重新加载数据...'); - selectedMockData = null; // 重置数据 + const retryLoadData = () => { setDataLoadError(null); - setIsLoading(true); - setCurrentStep('0'); - - // 重新初始化整个流程 - await initializeWorkflow(); - }; - - // 模拟接口请求 获取任务详情 - const getTaskDetail = async (taskId: string) => { - // 确保已经加载了数据 - if (!selectedMockData) { - console.warn('selectedMockData为空,重新加载数据'); - await loadMockData(); - } - - // 确保数据结构正确 - if (!selectedMockData || !selectedMockData.detail) { - throw new Error('数据结构不正确'); - } - - const data = { - projectId: selectedMockData.detail.projectId, - projectName: selectedMockData.detail.projectName, - taskId: taskId, - taskName: selectedMockData.detail.taskName, - taskDescription: selectedMockData.detail.taskDescription, - taskStatus: selectedMockData.detail.taskStatus, - taskProgress: 0, - mode: selectedMockData.detail.mode, - resolution: selectedMockData.detail.resolution.toString(), - }; - return data; - }; - - // 模拟接口请求 每次获取一个分镜草图 轮询获取 - const getTaskSketch = async (taskId: string) => { - if (isGeneratingSketch || taskSketch.length > 0) return; - - setIsGeneratingSketch(true); + // 重置所有状态 setTaskSketch([]); - - const sketchData = selectedMockData.sketch; - const totalSketches = sketchData.length; - - // 模拟分批获取分镜草图 - for (let i = 0; i < totalSketches; i++) { - await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.sketch)); // 10s - - const newSketch = { - id: `sketch-${i}`, - url: sketchData[i].url, - script: sketchData[i].script, - bg_rgb: sketchData[i].bg_rgb, - status: 'done' - }; - - setTaskSketch(prev => { - if (prev.find(sketch => sketch.id === newSketch.id)) { - return prev; - } - return [...prev, newSketch]; - }); - setCurrentSketchIndex(i); - setSketchCount(i + 1); - } - - // 等待最后一个动画完成再设置生成状态为false - await new Promise(resolve => setTimeout(resolve, 1500)); - setIsGeneratingSketch(false); - }; - - // 模拟接口请求 每次获取一个角色 轮询获取 - const getTaskRole = async (taskId: string) => { - setTaskRoles([]); - const roleData = selectedMockData.roles; - const totalRoles = roleData.length; - - for (let i = 0; i < totalRoles; i++) { - // 先更新loading文字显示当前正在生成的角色 - setCurrentLoadingText(STEP_MESSAGES.newCharacter(i, totalRoles)); - - await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.character)); // 2s 一个角色 - - // 添加角色到列表 - setTaskRoles(prev => [...prev, roleData[i]]); - - // 更新loading文字显示已完成的角色数量 - setCurrentLoadingText(STEP_MESSAGES.newCharacter(i + 1, totalRoles)); - - // 如果不是最后一个角色,稍微延迟一下让用户看到更新 - if (i < totalRoles - 1) { - await new Promise(resolve => setTimeout(resolve, 500)); - } - } - }; - - // 模拟接口请求 获取背景音 - const getTaskBackgroundAudio = async (taskId: string) => { - await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.audio)); // 10s - }; - - // 模拟接口请求 获取最终成品 - const getTaskFinalProduct = async (taskId: string) => { - await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.final)); // 50s - }; - - // 模拟接口请求 每次获取一个分镜视频 轮询获取 - const getTaskVideo = async (taskId: string) => { - setIsGeneratingVideo(true); setTaskVideos([]); - - const videoData = selectedMockData.video; - const totalVideos = videoData.length; - - // 模拟分批获取分镜视频 - for (let i = 0; i < totalVideos; i++) { - await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.video)); // 60s - - const newVideo = { - id: `video-${i}`, - url: videoData[i].url, - script: videoData[i].script, - status: 'done' - }; - - setTaskVideos(prev => { - if (prev.find(video => video.id === newVideo.id)) { - return prev; - } - return [...prev, newVideo]; - }); - setCurrentSketchIndex(i); - } - - // 等待最后一个动画完成再设置生成状态为false - await new Promise(resolve => setTimeout(resolve, 1500)); - setIsGeneratingVideo(false); + setSketchCount(0); + setTotalSketchCount(0); + setRoles([]); + setMusic([]); + setFinal(null); + setCurrentSketchIndex(0); + setCurrentStep('0'); + // 重新初始化 + initializeWorkflow(); }; - // 更新加载文字 - useEffect(() => { - if (isLoading) { - // 在初始加载阶段,保持当前loading文字不变 - return; - } - - const totalSketches = selectedMockData?.sketch?.length || 0; - const totalVideos = selectedMockData?.video?.length || 0; - const totalCharacters = selectedMockData?.roles?.length || 0; - - if (currentStep === '1') { - if (isGeneratingSketch) { - setCurrentLoadingText(STEP_MESSAGES.sketch(sketchCount, totalSketches)); - } else { - setCurrentLoadingText(STEP_MESSAGES.sketchComplete); - } - } else if (currentStep === '2') { - // 在角色生成阶段,loading文字已经在 getTaskRole 函数中直接管理 - // 这里不需要额外设置,避免覆盖 - if (taskRoles.length === totalCharacters) { - setCurrentLoadingText(STEP_MESSAGES.newCharacter(totalCharacters, totalCharacters)); - } - } else if (currentStep === '3') { - if (isGeneratingVideo) { - setCurrentLoadingText(STEP_MESSAGES.video(taskVideos.length, totalVideos)); - } else { - setCurrentLoadingText(STEP_MESSAGES.videoComplete); - } - } else if (currentStep === '4') { - setCurrentLoadingText(STEP_MESSAGES.audio); - } else if (currentStep === '5') { - setCurrentLoadingText(STEP_MESSAGES.final); - } else { - setCurrentLoadingText(STEP_MESSAGES.complete); - } - }, [isLoading, currentStep, isGeneratingSketch, sketchCount, isGeneratingVideo, taskVideos.length, taskSketch.length, taskRoles.length]); - - // 工作流初始化函数 - const initializeWorkflow = async () => { - try { - setIsLoading(true); - setCurrentLoadingText('正在初始化工作流...'); - - const taskId = (typeof window !== 'undefined' ? localStorage.getItem("taskId") : null) || "taskId-123"; - - // 首先加载数据 - await loadMockData(); - - // 然后获取任务详情 - setCurrentLoadingText('正在加载任务详情...'); - const data = await getTaskDetail(taskId); - setTaskObject(data); - - // 数据加载完成,进入工作流 - setIsLoading(false); - setCurrentStep('1'); - - // 只在任务详情加载完成后获取分镜草图 - await getTaskSketch(taskId); - - // 修改 taskObject 下的 taskStatus 为 '2' - setTaskObject((prev: any) => ({ - ...prev, - taskStatus: '2' - })); - setCurrentStep('2'); - - // 获取分镜草图后,开始绘制角色 - await getTaskRole(taskId); - - // 修改 taskObject 下的 taskStatus 为 '3' - setTaskObject((prev: any) => ({ - ...prev, - taskStatus: '3' - })); - setCurrentStep('3'); - - // 获取绘制角色后,开始获取分镜视频 - await getTaskVideo(taskId); - - // 修改 taskObject 下的 taskStatus 为 '4' - setTaskObject((prev: any) => ({ - ...prev, - taskStatus: '4' - })); - setCurrentStep('4'); - - // 获取分镜视频后,开始获取背景音 - await getTaskBackgroundAudio(taskId); - // 后期制作:抽卡中 对口型中 配音中 一致性处理中 - setCurrentLoadingText(STEP_MESSAGES.postProduction('Selecting optimal frames')); - await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.postProduction)); - setCurrentLoadingText(STEP_MESSAGES.postProduction('Aligning lip sync')); - await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.postProduction)); - setCurrentLoadingText(STEP_MESSAGES.postProduction('Adding background audio')); - await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.postProduction)); - setCurrentLoadingText(STEP_MESSAGES.postProduction('Consistency processing')); - await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.postProduction)); - - // 修改 taskObject 下的 taskStatus 为 '5' - setTaskObject((prev: any) => ({ - ...prev, - taskStatus: '5' - })); - setCurrentStep('5'); - - // 获取背景音后,开始获取最终成品 - await getTaskFinalProduct(taskId); - await new Promise(resolve => setTimeout(resolve, 2000)); - - // 修改 taskObject 下的 taskStatus 为 '6' - setTaskObject((prev: any) => ({ - ...prev, - taskStatus: '6' - })); - setCurrentStep('6'); - - } catch (error) { - console.error('工作流初始化失败:', error); - setDataLoadError('工作流初始化失败,请刷新页面重试'); - setIsLoading(false); - } - }; - - // 初始化数据 + // 初始化 useEffect(() => { initializeWorkflow(); - }, []); + }, [episodeId]); return { - // 状态数据 taskObject, taskSketch, taskVideos, sketchCount, - isLoading: isLoading || isLoadingData, // 合并loading状态 + isLoading, currentStep, currentSketchIndex, isGeneratingSketch, isGeneratingVideo, currentLoadingText, - totalSketchCount: selectedMockData?.sketch?.length || 0, - roles: selectedMockData?.roles || [], - music: selectedMockData?.music || {}, - final: selectedMockData?.final || {}, + totalSketchCount, + roles, + music, + final, dataLoadError, - // 操作方法 setCurrentSketchIndex, retryLoadData, }; diff --git a/components/ui/liquid-glass-button.tsx b/components/ui/liquid-glass-button.tsx index ef15663..447f293 100644 --- a/components/ui/liquid-glass-button.tsx +++ b/components/ui/liquid-glass-button.tsx @@ -91,46 +91,64 @@ const liquidbuttonVariants = cva( } ) -function LiquidButton({ - className, - variant, - size, - asChild = false, - children, - ...props -}: React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean - }) { - const Comp = asChild ? Slot : "button" - - return ( - <> - - - - - - {children} - - - - > - ) +type PolymorphicRef = React.ComponentPropsWithRef["ref"] +type AsProp = { + as?: C } +type PropsToOmit = keyof (AsProp & P) +type PolymorphicComponentProp< + C extends React.ElementType, + Props = {} +> = React.PropsWithChildren> & + Omit, PropsToOmit> + +type PolymorphicComponentPropWithRef< + C extends React.ElementType, + Props = {} +> = PolymorphicComponentProp & { ref?: PolymorphicRef } + +interface LiquidButtonProps extends VariantProps { + asChild?: boolean +} + +const LiquidButton = React.forwardRef( + ( + { className, variant, size, asChild = false, children, ...props }: PolymorphicComponentPropWithRef, + ref?: PolymorphicRef + ) => { + const Comp = asChild ? Slot : "button" + + return ( + <> + + + + + + {children} + + + + > + ) + } +); + +(LiquidButton as any).displayName = "LiquidButton" function GlassFilter() { diff --git a/plugins/liquid-glass/index.tsx b/plugins/liquid-glass/index.tsx deleted file mode 100644 index a0e6b72..0000000 --- a/plugins/liquid-glass/index.tsx +++ /dev/null @@ -1,612 +0,0 @@ -import { type CSSProperties, forwardRef, useCallback, useEffect, useId, useRef, useState } from "react" -import { ShaderDisplacementGenerator, fragmentShaders } from "./shader-utils" -import { displacementMap, polarDisplacementMap, prominentDisplacementMap } from "./utils" - -// Generate shader-based displacement map using shaderUtils -const generateShaderDisplacementMap = (width: number, height: number): string => { - const generator = new ShaderDisplacementGenerator({ - width, - height, - fragment: fragmentShaders.liquidGlass, - }) - - const dataUrl = generator.updateShader() - generator.destroy() - - return dataUrl -} - -const getMap = (mode: "standard" | "polar" | "prominent" | "shader", shaderMapUrl?: string) => { - switch (mode) { - case "standard": - return displacementMap - case "polar": - return polarDisplacementMap - case "prominent": - return prominentDisplacementMap - case "shader": - return shaderMapUrl || displacementMap - default: - throw new Error(`Invalid mode: ${mode}`) - } -} - -/* ---------- SVG filter (edge-only displacement) ---------- */ -const GlassFilter: React.FC<{ id: string; displacementScale: number; aberrationIntensity: number; width: number; height: number; mode: "standard" | "polar" | "prominent" | "shader"; shaderMapUrl?: string }> = ({ - id, - displacementScale, - aberrationIntensity, - width, - height, - mode, - shaderMapUrl, -}) => ( - - - - - - - - - - - {/* Create edge mask using the displacement map itself */} - - - - - - {/* Original undisplaced image for center */} - - - {/* Red channel displacement with slight offset */} - - - - {/* Green channel displacement */} - - - - {/* Blue channel displacement with slight offset */} - - - - {/* Combine all channels with screen blend mode for chromatic aberration */} - - - - {/* Add slight blur to soften the aberration effect */} - - - {/* Apply edge mask to aberration effect */} - - - {/* Create inverted mask for center */} - - - - - - {/* Combine edge aberration with clean center */} - - - - -) - -/* ---------- container ---------- */ -const GlassContainer = forwardRef< - HTMLDivElement, - React.PropsWithChildren<{ - className?: string - style?: React.CSSProperties - displacementScale?: number - blurAmount?: number - saturation?: number - aberrationIntensity?: number - mouseOffset?: { x: number; y: number } - onMouseLeave?: () => void - onMouseEnter?: () => void - onMouseDown?: () => void - onMouseUp?: () => void - active?: boolean - overLight?: boolean - cornerRadius?: number - padding?: string - glassSize?: { width: number; height: number } - onClick?: () => void - mode?: "standard" | "polar" | "prominent" | "shader" - }> ->( - ( - { - children, - className = "", - style, - displacementScale = 25, - blurAmount = 12, - saturation = 180, - aberrationIntensity = 2, - onMouseEnter, - onMouseLeave, - onMouseDown, - onMouseUp, - active = false, - overLight = false, - cornerRadius = 999, - padding = "24px 32px", - glassSize = { width: 270, height: 69 }, - onClick, - mode = "standard", - }, - ref, - ) => { - const filterId = useId() - const [shaderMapUrl, setShaderMapUrl] = useState("") - - const isFirefox = navigator.userAgent.toLowerCase().includes("firefox") - - // Generate shader displacement map when in shader mode - useEffect(() => { - if (mode === "shader") { - const url = generateShaderDisplacementMap(glassSize.width, glassSize.height) - setShaderMapUrl(url) - } - }, [mode, glassSize.width, glassSize.height]) - - const backdropStyle = { - filter: isFirefox ? null : `url(#${filterId})`, - backdropFilter: `blur(${(overLight ? 12 : 4) + blurAmount * 32}px) saturate(${saturation}%)`, - } - - return ( - - - - - {/* backdrop layer that gets wiggly */} - - - {/* user content stays sharp */} - - {children} - - - - ) - }, -) - -GlassContainer.displayName = "GlassContainer" - -interface LiquidGlassProps { - children: React.ReactNode - displacementScale?: number - blurAmount?: number - saturation?: number - aberrationIntensity?: number - elasticity?: number - cornerRadius?: number - globalMousePos?: { x: number; y: number } - mouseOffset?: { x: number; y: number } - mouseContainer?: React.RefObject | null - className?: string - padding?: string - style?: React.CSSProperties - overLight?: boolean - mode?: "standard" | "polar" | "prominent" | "shader" - onClick?: () => void -} - -export default function LiquidGlass({ - children, - displacementScale = 70, - blurAmount = 0.0625, - saturation = 140, - aberrationIntensity = 2, - elasticity = 0.15, - cornerRadius = 999, - globalMousePos: externalGlobalMousePos, - mouseOffset: externalMouseOffset, - mouseContainer = null, - className = "", - padding = "24px 32px", - overLight = false, - style = {}, - mode = "standard", - onClick, -}: LiquidGlassProps) { - const glassRef = useRef(null) - const [isHovered, setIsHovered] = useState(false) - const [isActive, setIsActive] = useState(false) - const [glassSize, setGlassSize] = useState({ width: 270, height: 69 }) - const [internalGlobalMousePos, setInternalGlobalMousePos] = useState({ x: 0, y: 0 }) - const [internalMouseOffset, setInternalMouseOffset] = useState({ x: 0, y: 0 }) - - // Use external mouse position if provided, otherwise use internal - const globalMousePos = externalGlobalMousePos || internalGlobalMousePos - const mouseOffset = externalMouseOffset || internalMouseOffset - - // Internal mouse tracking - const handleMouseMove = useCallback( - (e: MouseEvent) => { - const container = mouseContainer?.current || glassRef.current - if (!container) { - return - } - - const rect = container.getBoundingClientRect() - const centerX = rect.left + rect.width / 2 - const centerY = rect.top + rect.height / 2 - - setInternalMouseOffset({ - x: ((e.clientX - centerX) / rect.width) * 100, - y: ((e.clientY - centerY) / rect.height) * 100, - }) - - setInternalGlobalMousePos({ - x: e.clientX, - y: e.clientY, - }) - }, - [mouseContainer], - ) - - // Set up mouse tracking if no external mouse position is provided - useEffect(() => { - if (externalGlobalMousePos && externalMouseOffset) { - // External mouse tracking is provided, don't set up internal tracking - return - } - - const container = mouseContainer?.current || glassRef.current - if (!container) { - return - } - - container.addEventListener("mousemove", handleMouseMove) - - return () => { - container.removeEventListener("mousemove", handleMouseMove) - } - }, [handleMouseMove, mouseContainer, externalGlobalMousePos, externalMouseOffset]) - - // Calculate directional scaling based on mouse position - const calculateDirectionalScale = useCallback(() => { - if (!globalMousePos.x || !globalMousePos.y || !glassRef.current) { - return "scale(1)" - } - - const rect = glassRef.current.getBoundingClientRect() - const pillCenterX = rect.left + rect.width / 2 - const pillCenterY = rect.top + rect.height / 2 - const pillWidth = glassSize.width - const pillHeight = glassSize.height - - const deltaX = globalMousePos.x - pillCenterX - const deltaY = globalMousePos.y - pillCenterY - - // Calculate distance from mouse to pill edges (not center) - const edgeDistanceX = Math.max(0, Math.abs(deltaX) - pillWidth / 2) - const edgeDistanceY = Math.max(0, Math.abs(deltaY) - pillHeight / 2) - const edgeDistance = Math.sqrt(edgeDistanceX * edgeDistanceX + edgeDistanceY * edgeDistanceY) - - // Activation zone: 200px from edges - const activationZone = 200 - - // If outside activation zone, no effect - if (edgeDistance > activationZone) { - return "scale(1)" - } - - // Calculate fade-in factor (1 at edge, 0 at activation zone boundary) - const fadeInFactor = 1 - edgeDistance / activationZone - - // Normalize the deltas for direction - const centerDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY) - if (centerDistance === 0) { - return "scale(1)" - } - - const normalizedX = deltaX / centerDistance - const normalizedY = deltaY / centerDistance - - // Calculate stretch factors with fade-in - const stretchIntensity = Math.min(centerDistance / 300, 1) * elasticity * fadeInFactor - - // X-axis scaling: stretch horizontally when moving left/right, compress when moving up/down - const scaleX = 1 + Math.abs(normalizedX) * stretchIntensity * 0.3 - Math.abs(normalizedY) * stretchIntensity * 0.15 - - // Y-axis scaling: stretch vertically when moving up/down, compress when moving left/right - const scaleY = 1 + Math.abs(normalizedY) * stretchIntensity * 0.3 - Math.abs(normalizedX) * stretchIntensity * 0.15 - - return `scaleX(${Math.max(0.8, scaleX)}) scaleY(${Math.max(0.8, scaleY)})` - }, [globalMousePos, elasticity, glassSize]) - - // Helper function to calculate fade-in factor based on distance from element edges - const calculateFadeInFactor = useCallback(() => { - if (!globalMousePos.x || !globalMousePos.y || !glassRef.current) { - return 0 - } - - const rect = glassRef.current.getBoundingClientRect() - const pillCenterX = rect.left + rect.width / 2 - const pillCenterY = rect.top + rect.height / 2 - const pillWidth = glassSize.width - const pillHeight = glassSize.height - - const edgeDistanceX = Math.max(0, Math.abs(globalMousePos.x - pillCenterX) - pillWidth / 2) - const edgeDistanceY = Math.max(0, Math.abs(globalMousePos.y - pillCenterY) - pillHeight / 2) - const edgeDistance = Math.sqrt(edgeDistanceX * edgeDistanceX + edgeDistanceY * edgeDistanceY) - - const activationZone = 200 - return edgeDistance > activationZone ? 0 : 1 - edgeDistance / activationZone - }, [globalMousePos, glassSize]) - - // Helper function to calculate elastic translation - const calculateElasticTranslation = useCallback(() => { - if (!glassRef.current) { - return { x: 0, y: 0 } - } - - const fadeInFactor = calculateFadeInFactor() - const rect = glassRef.current.getBoundingClientRect() - const pillCenterX = rect.left + rect.width / 2 - const pillCenterY = rect.top + rect.height / 2 - - return { - x: (globalMousePos.x - pillCenterX) * elasticity * 0.1 * fadeInFactor, - y: (globalMousePos.y - pillCenterY) * elasticity * 0.1 * fadeInFactor, - } - }, [globalMousePos, elasticity, calculateFadeInFactor]) - - // Update glass size whenever component mounts or window resizes - useEffect(() => { - const updateGlassSize = () => { - if (glassRef.current) { - const rect = glassRef.current.getBoundingClientRect() - setGlassSize({ width: rect.width, height: rect.height }) - } - } - - updateGlassSize() - window.addEventListener("resize", updateGlassSize) - return () => window.removeEventListener("resize", updateGlassSize) - }, []) - - const transformStyle = `translate(calc(-50% + ${calculateElasticTranslation().x}px), calc(-50% + ${calculateElasticTranslation().y}px)) ${isActive && Boolean(onClick) ? "scale(0.96)" : calculateDirectionalScale()}` - - const baseStyle = { - ...style, - transform: transformStyle, - transition: "all ease-out 0.2s", - } - - const positionStyles = { - position: baseStyle.position || "relative", - top: baseStyle.top || "50%", - left: baseStyle.left || "50%", - } - - return ( - <> - {/* Over light effect */} - - - - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - onMouseDown={() => setIsActive(true)} - onMouseUp={() => setIsActive(false)} - active={isActive} - overLight={overLight} - onClick={onClick} - mode={mode} - > - {children} - - - {/* Border layer 1 - extracted from glass container */} - - - {/* Border layer 2 - duplicate with mix-blend-overlay */} - - - {/* Hover effects */} - {Boolean(onClick) && ( - <> - - - - > - )} - > - ) -} diff --git a/plugins/liquid-glass/shader-utils.ts b/plugins/liquid-glass/shader-utils.ts deleted file mode 100644 index e0957ff..0000000 --- a/plugins/liquid-glass/shader-utils.ts +++ /dev/null @@ -1,134 +0,0 @@ -// Adapted from https://github.com/shuding/liquid-glass - -export interface Vec2 { - x: number - y: number -} - -export interface ShaderOptions { - width: number - height: number - fragment: (uv: Vec2, mouse?: Vec2) => Vec2 - mousePosition?: Vec2 -} - -function smoothStep(a: number, b: number, t: number): number { - t = Math.max(0, Math.min(1, (t - a) / (b - a))) - return t * t * (3 - 2 * t) -} - -function length(x: number, y: number): number { - return Math.sqrt(x * x + y * y) -} - -function roundedRectSDF(x: number, y: number, width: number, height: number, radius: number): number { - const qx = Math.abs(x) - width + radius - const qy = Math.abs(y) - height + radius - return Math.min(Math.max(qx, qy), 0) + length(Math.max(qx, 0), Math.max(qy, 0)) - radius -} - -function texture(x: number, y: number): Vec2 { - return { x, y } -} - -// Shader fragment functions for different effects -export const fragmentShaders = { - liquidGlass: (uv: Vec2): Vec2 => { - const ix = uv.x - 0.5 - const iy = uv.y - 0.5 - const distanceToEdge = roundedRectSDF(ix, iy, 0.3, 0.2, 0.6) - const displacement = smoothStep(0.8, 0, distanceToEdge - 0.15) - const scaled = smoothStep(0, 1, displacement) - return texture(ix * scaled + 0.5, iy * scaled + 0.5) - }, -} - -export type FragmentShaderType = keyof typeof fragmentShaders - -export class ShaderDisplacementGenerator { - private canvas: HTMLCanvasElement - private context: CanvasRenderingContext2D - private canvasDPI = 1 - - constructor(private options: ShaderOptions) { - this.canvas = document.createElement("canvas") - this.canvas.width = options.width * this.canvasDPI - this.canvas.height = options.height * this.canvasDPI - this.canvas.style.display = "none" - - const context = this.canvas.getContext("2d") - if (!context) { - throw new Error("Could not get 2D context") - } - this.context = context - } - - updateShader(mousePosition?: Vec2): string { - const w = this.options.width * this.canvasDPI - const h = this.options.height * this.canvasDPI - - let maxScale = 0 - const rawValues: number[] = [] - - // Calculate displacement values - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - const uv: Vec2 = { x: x / w, y: y / h } - - const pos = this.options.fragment(uv, mousePosition) - const dx = pos.x * w - x - const dy = pos.y * h - y - - maxScale = Math.max(maxScale, Math.abs(dx), Math.abs(dy)) - rawValues.push(dx, dy) - } - } - - // Improved normalization to prevent artifacts while maintaining intensity - if (maxScale > 0) { - maxScale = Math.max(maxScale, 1) // Ensure minimum scale to prevent over-normalization - } else { - maxScale = 1 - } - - // Create ImageData and fill it - const imageData = this.context.createImageData(w, h) - const data = imageData.data - - // Convert to image data with smoother normalization - let rawIndex = 0 - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - const dx = rawValues[rawIndex++] - const dy = rawValues[rawIndex++] - - // Smooth the displacement values at edges to prevent hard transitions - const edgeDistance = Math.min(x, y, w - x - 1, h - y - 1) - const edgeFactor = Math.min(1, edgeDistance / 2) // Smooth within 2 pixels of edge - - const smoothedDx = dx * edgeFactor - const smoothedDy = dy * edgeFactor - - const r = smoothedDx / maxScale + 0.5 - const g = smoothedDy / maxScale + 0.5 - - const pixelIndex = (y * w + x) * 4 - data[pixelIndex] = Math.max(0, Math.min(255, r * 255)) // Red channel (X displacement) - data[pixelIndex + 1] = Math.max(0, Math.min(255, g * 255)) // Green channel (Y displacement) - data[pixelIndex + 2] = Math.max(0, Math.min(255, g * 255)) // Blue channel (Y displacement for SVG filter compatibility) - data[pixelIndex + 3] = 255 // Alpha channel - } - } - - this.context.putImageData(imageData, 0, 0) - return this.canvas.toDataURL() - } - - destroy(): void { - this.canvas.remove() - } - - getScale(): number { - return this.canvasDPI - } -} diff --git a/plugins/liquid-glass/utils.ts b/plugins/liquid-glass/utils.ts deleted file mode 100644 index ce6b8ff..0000000 --- a/plugins/liquid-glass/utils.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const displacementMap = - "data:image/jpeg;base64,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cUAUAkEkziomgGgkoxZGgxZFQFQlYnQHRdPfj/10KCSCKESCNiVkViPSLpD0h6I0Q0I0A2IoBWBIJIBKBIJoJIJ2R2J0JWBUJ0JUB0XTv479dFZDYiglYigkhEgjZFQjRFQjRFQjQigFYigHYigmgEgmglYlYnQlQlYlQHQlQnQ9P/kf1yVkNiNCNkNiVENiNiViNEViNkVCVgKCViViViSCViSCVgdCViVCViVCdgVCVCdD1D+U/XBWQ2I0I2Q2JUQ2I0JWQ0I2JUQ2JUI2JUI2J0JWJWJWA2R0BWJ0I2JUJ2BUJUJ0P//EABkQAQEBAQEBAAAAAAAAAAAAAAECABEDEP/aAAgBAQABAgB1atWrVq1atWrVq1atWrVq1atWrVq1atWrVq+OrVq1atWrVq1atWrVq1atWrVq1atWrVq1atXxVppppppdWrVq1atWrVq1NNNNNNNNNNNPVWmmmmms6tWrVq1atWpppppppppppppp6q0000uc51atWrVq1ammmmmmmmmmmmmt1Vpppc5znVq1atWrVqaaaaaaaaaaaaaeqtNLnOc51atWrVq1ammmmmmmmmmmmmnqrS5znOc6tWrVq16222mmmmmmlVppp6tKuc5znOrVq1a9TbbbbTTTTTSq000qtLnOc5zq1atWrW0222200000qqqtKqrnOc5zq1atTbbbbbbbbTTTSqqqqqq5znOc6tTTTbbbbbbbbTTTSqqqqrlVznOctNNNtttttttttNNNNKqqqrqznKqrTTTTbbbbbbbbbTTTSqqqqrqznOc5aaaabbbbbbbbbaaaaVVVVVdWc5znVq1NNttttttttttNNKqqqqudWc5znVq16tbbbbbbbbbbTTSqqqq5XVnOc6tWrVrb1tttttttttNNKqqqqrWrK5VWmmm2230bbbbbbaaaXOc5zlVa1KuVVppptttt9G22222mmlzlVznK6tWVVWmmmm2222222222mlznOc5znLWppVVWmmm22222229bTWrOc5znOcq1qaaVpWmm222222229erVqznOc5znKtatStK0rTbTTbbbberXr1as5znOc5aVpppppWlabaabbbb1ta9WrVnOc5znU0rTTTTTTTTTbTTbbbTWvVq1as5znOdTTStNNNNNNNNNtNNtttN6tWvVq1ZznOrU00rTTTTTTTTTTTTTbTWvVq1atWrOc6tTTTStNNNNNNNNNNtNNtNa9WrVq1Z1Z1NNNNNK1q1NNNNNNNNNNNtNatWrVq1atWrU00000rWrVq1atWrVq1alaaa1atWrVq1NNNammmmla1atWrVq1aterVq16tWrVnVqa1NK1qaaaVX/xAAWEAADAAAAAAAAAAAAAAAAAAAhgJD/2gAIAQEAAz8AaExf/8QAGhEBAQEBAQEBAAAAAAAAAAAAAQISEQADEP/aAAgBAgEBAgDx48ePHjx48ePHjx48ePHjx48ePHjx48ePHj86IiIiIiInjx48ePHjx48IiIiIj0oooooooooRERER73ve60UUUUUUVrWiiiiiihERERER73ve97ooooorRWiiiiihKERERER73ve973RRRRWtFFFFFFCIiIiIiPe973ve60UUVrRRRRRRQiIlCIiI973ve973pRRWiiiiiiiiiiiiiiihEe973ve973RRWtFFFFFFFFFFFFFFFFFFa13ve973WitaKKKKKKKKKKKKKKKKKK1rWtd1rutFa1oooooooooooosssooorWta1rWta1rRRRRRRRRRRZZZZZZZZZWta1rWta1rRRRRRRRRZZZZZZZZZZZZe9a1rWta1rWitaKLLLLLLLLLLLLLLLLL3rWta1rWtFbLLLLLLLLLLLLLLLLLLLL3vWta1rWita1ssssssss+hZZZZZZZZe961rWta0Vre97LLLLLLLLLLLPoWWWWWXrWta1oorWta3ssss+hZZZZ9Cyyyyyyyyiita1orWta1ve9llllllllllllllllFFa0VorWta1ve9llllllllllllllllllFFFaK1rWta1rWiyyyyyyyyyyyyiiiiiiitFFa1rWta1oosoosssssoooosoooorRRRWta1rWta0UUUUUWUUUUUUUUUUUVoooorWta1rWtaKKKKKKmiiiiiiiiiiiiiiitd73ve61oSiiipoqaKKKKKKKKKK0UUUVrve973vREREZoSihEooooorRRRRWtd73ve9EREREREoSiiiiitFllllla73ve9ERERERESiiiiiitH0PoWWWWVrXe96IiIiMoiJRRRRRRWjwlFFllllFFd6IiIiIlCUUUUUUUUUePHjx48ePCIiIiIiIiUUUUUUUUUUUePHjx48ePHjx48ePHjx48IiUUUUUUJRRRX//xAAWEQADAAAAAAAAAAAAAAAAAAABYJD/2gAIAQIBAz8AtEV7/8QAFxEBAQEBAAAAAAAAAAAAAAAAAAECEP/aAAgBAwEBAgCtNNNNNNNNNNNNNNNNNNNNNNNNNNNNNcrTTTTTTTTTTTTTTTTTTTTTTTTTTTTTXKrTTTTTTTU000000000000000000001FVpppppqampqaaaaaaaaaaaaaaaaaaaa5Vaaaaampqampqammmmmmmmmmmlaaaaaaiq0001NTU1NTU1NTTTTTTTTTTSqqtNNNcqtNNSyzU1LNTU1NTTTTTTTTTSqqq001ytNLLLLNTU1NTU1NTbbbTTTTTSqqq001ytNLLLLLNTU1NTU3NttttNNNNNKqq001KrSyyyyyzU1NTU3Nzc02220000qqqqrSqqyyyyyzU1NTU3Nzc3NttttNNNKqqqqqqssssss1NTU3Nzc3NzbbbbTTTSqqqqqqrLLLLLNTU1Nzc3Nzc22220000qqqqqqqqssss1NTU3Nzc3NzbbbbbTTSqqqqqqqqqqzU1NTc3Nzc3Nzbc22000qqqqqqqqqqqtTU3Nzc3Nzc3NtzbTTSqqqqrKqqqqqtNNzc23Nzc3Nzc3NTU1KqqqrKqqqqqtNNNNttzc3Nzc3NzU1NLLLLLKqqqqqqqq0022223Nzc3NzU1NSyyyyyyqqqqqqqrTTbbbbc3Nzc3NTU1LLLLLLKsqqqqqqrTTTTbbbc3Nzc1NTUsssssssqqqqqqrTTTTTbbbTc3NTU1NTUsssssqqqqqqqq0000222023NTU1NTUsssssqqqqqqqq000000003NTU1NTU1LLLLLNKrTSqqqqtNNNNNNtNNTU1NSzUssss00qq0qqqqrTTTTTTTTTU1NTUs1LLLNNNKrTTTSqqq00000000001NTU1LNTU0000qtNNNKqqqtNNNNNNNNTU1NTUs1NNNNNKss1NNNK00qtK0000001NNTU0s000000qq000001NKrStNNNNK1NNNNStNNNNNKqtNNNNNNNK0000000rU0000rTTTTTSq00000rTTTTTTTTTTTTTTTTStNNNNKr/xAAUEQEAAAAAAAAAAAAAAAAAAACg/9oACAEDAQM/AAAf/9k=" - -export const polarDisplacementMap = - "data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAZABkAAD/2wCEAAYEBAQFBAYFBQYJBgUGCQsIBgYICwwKCgsKCgwQDAwMDAwMEAwODxAPDgwTExQUExMcGxsbHB8fHx8fHx8fHx8BBwcHDQwNGBAQGBoVERUaHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fH//CABEIAQABAAMBEQACEQEDEQH/xAAxAAADAQEBAAAAAAAAAAAAAAABAgMABAcBAAMBAQEBAAAAAAAAAAAAAAIDBAEABQb/2gAMAwEAAhADEAAAAPG/tfu93bu3bs7d27t3bu2du7d27h3bs3du7d27t3bc3du7d27tvbu3du7d27T3E+2du05u7tm7O2cM7d2zt3Du2YOzbw7N3bcHZt7dm3tvbeO9u7dx3d3Ht3cS05pzd24dOds0Z2HdnDsGdswdg7hw7cHYNzbg3NvbcO9izbx3TvbtPae09pLTmnCObh3ZuHcO4eGcM4ZgzB2DhHYOEbg0QWbcxZtzFmLjvEuO6e07p4jmsWnCOERIiWHcO4NA8M4DwzBmLgjsXRHCNEEI0QQ4sxZjwlxLjvEtPa2keJuJt04bCREsJECw6A3BoHFHhmKIrmLwjQXRGgpCCHEIMcWE8x4S1i4lraR7W02wnIiJsJkTIFg3AWXoHgGqGAcXBTBXhXgXQUgBADAGIMceE8J4T4lrFraTaT6TYbabiZFjAeAissBBegNAcq8UcXBXATBXVpoKQAlqYBg4wzMx4WYx8T1i1yJtN+NsN9NxYwmVmQZlllllaA1V8oYoYoimAnAmrXVoS1MAawwAwcwSzCzCfMzXLWIn035j8b6xwYwMIMKjKzyiCyCuVfKGKAoIpgJgJq0JSEtTWprDQzAzRzBZvFnMfOZORuRvzHw6a1wYwMZbSphUeUQUQXqqxF4gCgCmAnLnykJaGpTUrFhqw0M0S0S3GZrM52E5HTTfm0xlNY4OYGMtrJZlMKSCiVOqrkWKAKACCE+XPVTJSGlGKDFq1YcvNEuFm4zeZmuwqEb6ymspja61wcymutpS0pPJMJIJ1FcqsRYTAJ4ueKkSpkpDSjFK1StVnBnAXCXYzeduuwqEyhMrrKY6nNoDnU5lNZLSlmQYQap1U4ihRYzBcxXLlS1MyVNiUYlWqVyg9ecBeDO5nc7dowqGyhMrzaY6vOoDnU50uZLihmQwIJUaqcRIzUEwXIVy5UtTI0zYhGKRyVckPXnrLxZ+O7naVGlQ2VJtebXH151AdRT2S9kNM7chgnJUaqMRIooJLXIVR5UiREkzaibEq9CuUKFZ6zQLPxn9RpUadWHXW111cfbn0W+inuh7IcZ26dgnJZ9WfESM0hIFRFUuTHUxNEmIm5COQtCQ9WoWaRZ+O/qOKjTqxlibXnWx9efVdFE0Oh7ocZnadgmNZ9WYUSMkrktcRTHkw1EWIkxE3To9CUJFCdSs0C9AvRtHbVrKsZUnW11sotj6roommiHtM8zu0zBMYl1ZxnOM1LipUBTHkwJETni2eTkI+daULSnUrakGox6Oq8qtZVjLG6+vsNFuoqqmqKHRQ8zzM7TNWUhLqzYk4ySuC1RFMMRAp4Mni2eT50fOlKBSnVKNIPTj09V5VayzWWJ99fbKb5RVVNUU0noaahpnCVokMS8suTnGSVxUnnFMMRAp+dk0XTyfNOidKZxUnVKNQPSNKdq8qvZZjbm6/UXym2U2VTVFVJ6XleZX6RolMScsuTmCKFwUqAo5+RzlNBk0HTRfMlMyUoWpGrU1QNUNKetQdXsu1tyffaLjVfKbKqsiqk1LS0NI7SOEhiPllyUwRQuCk84I5+RzlNzslg6aNEs6ZkqnFaNWo1rerKVdag6vO7XdB0X6joyq+U2TXZFVJanloMjzG4RmI+STJzBGdfOpPOE/N0/MU3O2WDpo0yzplSqda0axLVrasa1bWkrvZdrrnR0bT0ZV0DVdNdZ66zVPJSY36NwjPRckeSmCM6udKeYEc3Tcxzc7JOd8saZZVSpVMLEaxJsW9Y0r21JXey7X9DKOnaega+garpstPXSWp5KWjo0ThEeh5I8lKEJ1c6k8oT82Tcxy8zZOd8sKZJ1SpXMts+sSbVvWNa+tUV3t6HP6Do6dq6Br6Mr6EWWmsrLU8lTRUaJwhPQ8keRkXCdfMlHME/Lk3KcvM2TnojhTJKuVLJVsn1qWtU9mVs61RXob0Nf0sp6eq6Mr6Rs6EWWmsrLXSOow06J2gPQ8kWRkXzzK5kp5Qn5cl5Tk5XSc9EcKo5VyzslFswtS1yntGtfXqO9Lel1HSdPTtXSNnSNnQi281lZK3iraKjQv0B7z+SLIyL5plcyE8i5uTpeU5OV0fPTHCqONciWyLbPrkG5VLgrZt6jvS3pdR1HT07X05Z1Bb0ItvNbWOukVbQ06F+8895/JDkI180yuZCONc3JkvIyTmdFzUx89cUrJJ2yLdNrp2vW9wVs69bOmlvS6jpZV1bX1Db0qt6VW3mttHa8NbQ06B7ecY8/pwDGMOaVXIhHGqbk6TkZHyvi5qYueuKNsc7ZFvm1yGvTS8a29es+ml3S+jqOvq2vpXb1Ku6lXXnttHbSGtoKt57z5x7z+nAMIg5pU8k6OJM3IcnI2LkbFzUxc9cMbY53SLfLr0N6CXuGt2dFh9NL+p9PUyrqG3pXb/8QAGxAAAwEBAQEBAAAAAAAAAAAAAAECEQMwECD/2gAIAQEAAQIAMzMzMzM/W7u7u745mZmZnhu7u7u+GZmZmZ4bu7u7vhmZmZmeG7u7u7+l8zMzMzBjGMY/m7u7u6IQhCEISzMzMxjGMYxje7u7u6hCEIQhJLMzMxjGMYxjGN7u7upoQhCEIQlmZmY0xjGMYxje7vzU0IQhCEISzMzMaYxjGMYxtvd3dQhCEIQhCEszMaaYxjGMYxtvd1NNCEIQhCEISzMxppjGMYxjG293U000IQhCJEISzMxppjTVKiihjG93U000IkkkkkQklmZjTTVFFFFFDG2291NNNOSSSSSRCSSWY0001SoooooY223upppoRJJJJJIkklmNNNNUqVFFFFDbbe6mmnJJJJJJJIkklmNNNNUUUUWUMbbb3U005JJJJJJJJSSWY001SpUqLKKKKbbe6mmnJJJJJJJJKSSzGmmqVFFllllFNtvdTTlySSQQSSSSkksxrGqVK1ZZZZRTbb3U05ckkEEEEkkpJLMaxqlSsssssoptt7qacuSSCCCCSSUklmNY1Sssssssoptt7qacuSSCCCCCSUklmNY1StWdCyyyim23uppy5JIIIIIIJUpLMxpqlZZZZ0LLKbbe6mnLkggggggglSkszGqVK1Z0LOh0LKdNvdTly4IIIIIIIJSSWZjVK1a6HQ6HQ6Flum3upy5cuCDmcyCCCUklmY1StWdDodDodCy3Tb3U5cuHBBzOZBBBKlJZmNUrVrodDodCyy3Tb3U5cuCDmczmQQQSpSWYk1StdDodDodDoWWU291OXDgg5nM5nM5kEqUlmY1StdDodTodDoWW6be6nLhwczmczmczmQSpSWZjVK10Op1Oh0OhZbpt7qckOHzOZzOZzOZBClJZiTVKzodTqdDqdDoW6be6nLhwczmczmczmcyFKSzBq10XRdTqdTqdDo7dNvdRJD5vmczkczmf/8QAFhAAAwAAAAAAAAAAAAAAAAAAMXCQ/9oACAEBAAM/AK3FJf/EABsRAAMBAQEBAQAAAAAAAAAAAAABAhEDIBAw/9oACAECAQECAMzM9bu7u7u+szMzMzPw3d3d3fwzMzMzPD8bu7u7vlfczMzMzw/G7u7u75X3MzMzMGMYxj+bu7u7ohCEIXzMzMzMYxjGMYzd3d3U0IQhCEISzMzMaaYxjGMY3u7u6mmhCEIQhLMzMxppjGMYxjbe7u6mhCEIQhCSWZmY0xjGMYxjG93d1NCEIQhCEkszMxpjGMYxjGN7u7qaEIQhCEJJZmY00xjGUMYxjbe7qaaESIRIhCSWZmNNMZRRRRQxjbe7qaaESSSSSIQklmY00xlFFFFDG2293U000SSSSSSISSzMaaaooooooZTbb3U0005JJJJJJEkkszGmqVFFFFFFDbbe6mmmiSSSSSSRJJLMxpqiiiiiiim223upppySSSSSSSISSzGmmqKKKKKKKKbbe6mmnJJJJJJJJKSSzGmmqKKLLKKKdNtvdTTTkkkgkkkklJJZjTVKiiiyyiinTbb3U05cuSSCSCSSUkkljTVKiiiyyyyinTb3U05cuSCCCCSSUklmNNUqVFllllllOm3uppy5JIIIIIJJUpLMaapUqLLLLLLKbbe6mnLkkgggggklSksxpqlSsssssssp0291OXLkggggggklSksxpqlRZZZZ0LLdOm3upy5cEEEEEEEEqUkljTVKiyyzodDoW6dNvdTly4IIIOZBBBKlJJY01Ssss6HQ6HQt26bbepy5cOCCDmcyCCVKSSxqlStWWdDodDoW7dNtvU5cuCCDmczmQQSpSSWNUqVqzodDodDoW7dNtvU5cOHBzOZzOZzIIUqUljVKlas6HQ6HQ6Fu3Tpt6nLhwQczmczmcyCFKSSxplK1Z0Oh0Op0Ojt06bey5cOHBzOZzOZzIUKUkljGUWdDodDodTodHbp0200S4cPmczmczmczmQpSSTGMZZ0Oh0Op1Op0du3TbRJJD5vmczmcjmczmoUpJJjP/8QAFBEBAAAAAAAAAAAAAAAAAAAAoP/aAAgBAgEDPwAAH//EABsRAAMBAQEBAQAAAAAAAAAAAAABAhEDEDAg/9oACAEDAQECAPzmZmZnx3d3d3fjmZmZ8d3d3d+OZmZmfHd3d3fjmZmZmfDd3d3d9Qhe5mZmZ4xjGP3d3d3dEIQhCEZmZmZjGMYxjGbu7u6IQhCEIXmZhmMYxjGMYzd3d3UIQhCEIQlmZhjGMYxjGMfu7uoQhCEIQhLMzMGmMYxjGMZu7uppoQhCEIQklmZjTGMYxjGMbb3d1NCEIQhCEISzMxpjGMYxjGMb3d1NCEIkQhCEkszGmMYyihjGMbb3d1NCESSIkQhJLMxppjGUUUMYxtvd1NNNCJJESIQklmY0xjKKKKKGMbb3dTTTRJJJJJIhJLMxpjGUUUUUUMbb3dTTQiSSSSSRCSWZjTTGUUUUUUMbb3dTTRJJJJJJJIklmY0xjKKKKKKKG293U005JJJJJJJEkksaaaaoooooooobbb3U05JJJJJJJJEkksaaZRRRRRRRRQ223uppySSSSSSSSIQkNNMoooooooooptt7qackkkkkEEkiEksGmqKKLLKLKKKbbe6mnJJJBBBBJJKSSxpplFFFllllFFNtvdTTkkkggggkklJZjTTVFFFlllllFDbe6mnLkggggggkkSzGmUUUUWWWWWUUU291NOSSCCCCCCSRLMaaZRRRZZZZZRRTb3U5ckkEEEEEEkpLMaaaoossssssop0291OXJBBBBBBBBKSzGmMossssssssp0291OXJBBBzOZBBBKlZjTVFFllllllllOm3upy5cEEHM5kEEEqVmNNUUWWWWdCyyynTb1NOXLggg5nMggglSvGmUqLLOhZ0LLLKdNm6nLgggg5nMggglSsxpqlRZZ0Oh0OhZZTpt7qcuHBzOZzOZzOZBKleNNUUWWdDodDodCynQxmy5cEHM5n/xAAUEQEAAAAAAAAAAAAAAAAAAACg/9oACAEDAQM/AAAf/9k=" - -export const prominentDisplacementMap = - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAABVXElEQVR4nO19aZasPKxkuE5toffS+1/YR/8AS6GQZAxZd3qvffJQtjEe5AgNQGaN//N/caZxAAAODFyZsnLcnZIGz47UiVVeNeWpWDlmJbhILW8rv7oaYBz4SpWS+ZJKuwofHMeVH8DXoFMjVHpmXFdJpR1zzRipWFUiVYJaIlVCLpynQO0fHRE7uQ5Vg/JUUQl8TfyeoAXGzJyVI1aemVGdSg24WXtEPKYLdWJ0lQ4HHOdnIKdjzLP04eGsZ+4csbeDelukY3XyfVqO6Ts6ciWdGtyIQKOfajAjlXVneAL1HCCYpzGy1O9xn4fDI/RLe6r6YhxkqKECes0BaZBwoFgHXZV4pVBrRufKg4U1LzHckwwSYSrQBy2ANh1RSkXLNWxvU7qcEQPUSM2XOqYjGQTQRQOB3UQVVwT8WauIvzBtsQZpcFlT2tiI9Y3RS25gmlM844DtdOSANkhHNC35KKbALj9AGYFanCrguAe1KVJFBk4lB9Qu7ej71xy4u3DkzNCa3M0C9N3ozgSqYmIMqhzDL/EpRaDL1o9UA9SmYFRtHP2ZGFIpg5oL9JIDdCo36Jhw5LPwOeyYgtII5KLN8yBWiC/ELTGUBsdz6LMxDOsKuFum4Q40WJaj7mBNA2GCQm1WDkL5IKco9Euw1uIInd8r/nTK8jsu0KhGeYF+DHxZB7ccCGcZyjMVHtGaCfBxW/THgXhiB02sLBaOPryNdZjJIA7VLfTNQIX+O7TefrqrrGTbWSwo0WACYtC5YrSyO2OCXN4X8+gtByomLHBfgLvqWWSxRj+Ar7DT1KgOPRMHOoBys+yioMG9D1SiX+Y2K2+NwE0xkkHmKXm1e9Jn7j8C7dZfCogsKRGHC/CqaJDzCvodEdm1y6IAdO38dEwIS8s+j52vSMLD7aD/vGOGZxyIy8jBAFt/IBTLYCAM3ThCuX9ErX8kI4Ds/HRFXpG4PT30Q8oQK9s8+nSXl4OeFRUNyrzBInxGW+RO+a6oFQVnNQeWYQDitUIJL3L/ldZ/hH6cQTAecaBEZObAi/uhjSnQnqVl5YnVzo8gJg5U2C7rUKbBRQrQlfw7sC5TcyGDwFEyGpcgk4VBVqIwtA5njRLlQCXasoPOLQf1sOn6L9Df8U0WntGP8BzgBQe6Uw0TdsIAREpw0aAWNDTNPxsBu9C1PkUInoQGPFBccpCVXTti/iRDifgS3GuSzJhYG8TGC89Y/ZYoH0xw+5EyiI1r9U+d8BD3YUBsuX7m1aK/WvIm+hGeAzB8xx4H+lNra3ANV53q0K/ci45ZZwTUXema0dlFJMATULB2CN5B/D4fynoqKg3KVgTTkS6REUev/q03oYRVLopeme6u6qmeG4A2WKF/xJaz/sshGBH2mAMZtZkDfTCQDQKW6PcLrZ/eCEBufVZbnmlg6UiSgeQXZKg+R1Wpan5NhlyZaKAR6vwjTOBGodckRlH/aNTqDQdipVzuFWv0UzM91aFfxp31123QsPfvOJBwUxQTPRZhwC36Gc1rI1CGuR4q8Norvy5IRpz+EaW3h/X9T8sKQ4k145o4c4aFQP/qr3J4uP5G/dslOxzYDADCXFJxHRJYuw791ObLmv4YB6r6+4C47CQV6wcCtMiFEdBM7KFQ/+UtYCgQteZ3fvr5FEChBXrzGl9FplT/2jlx4x0HkJtVbX4K/Rj4Ps5zBzBwHPPcgWPM9z3P+tTGKq+WsVmu56O1uY4IxfNCm5gWz7XlCVv9TId0XmUcFoefKZaJOT3vnTasOv/rUj1KBeUMi8FLhtfk8HdH/YeehXU9B8Jse9xnlKOpf43+sXgXCL0dyJWdvl/cFMohQTYIRZHqf8AIlOq/EsWVGVXxL/l0k4wY5crBaFhmbtW/OuhLDoSjjbN322eBfu5uE/0AvjEu5cc6HojaHVe9VkL1NJKmv/R3PotwKhzR6n6ZwKnjr1VVRsCWczbgzIg1rNBttucKj4EpGzIIH6Sygx8xII4S601wwARJgC5ug8Y5ZfXf+f0POEDQFHxvoZ/mhtisnJUUv6/ayIESbQUHypZEpJIDmNh9hv5z0hH9PDEQpg9aiNEDMcPEcApRn760MxFF9sE62sIysX55MRCVaxoQfBXoMeNtElsKahm8KtDfcsAHrdAvZwXT79AP4Ju13XEAsWjgQ+/6Zw60UYGxgyrDEQGImQyIGUDndomDOwQQsgWkhLq+dr5+H77dMJ9cexSt9jvJEC/t2KBP7mqMaQGIG9d5AvcC9EqzeAxNEqALVshaHgYDZ/EbSGrvefhr4NZOos+DUQTErVOU0K+OEJyuBwCb6hwINJ8xM0jBsbpwcBVwpT30B5D9eKKej2N7nNiudFeKYJfw7Xygs1djYwXVc2al+K2+C3ylKMDleX6AfngMEFHyjgPCBMksbgq9QT95RDkM8BoUzk/IyFpo3tffHfR/3qBMndYfdw1i41EWk2vRZiIf7KxzJvk8n3AgNCgnRXN+h35u4zEA43vFAah7kx2erqYMEh6g/wxGzU/bCQNMZBX6R5wJQGvEVTwEQJyWsP48Vrb+L5kvJtAwQfAKKRJQcvjL6n8zALhwXIH+lgM+sQ7Z+zXSbY9+4HwOAAIlyKshjXgcAVsvOMC8WgTEmEBk9BvoC/8nhwF0ynoDfDhmIPKgBpRe9wu4R5FD2eKWFGHMLnA5UsvRk6ScQEJtGIIzsSUr2hwAMMpHLHJvO4Gv9/Yh+stogVbxjaj4ESNCDlg/5EBpDXJArAYBdCGKTEmDeyPwLuq9w3co3YI9YXrd4Mg9H037u95YOxoyBqG8U/+4g75mRkC2kq0yC2Dy/GL0w1wgCSJ/EwfwJBgAXYsUBkB9ISPGJYOI/jA3bES9DZoLU9DQ5oYO8TT7YNIgRMCd4ucl96PkmDi0GZ4X9X/VGMhyBiFz7/r/XvTzQq4g+DxR4vsXceCcwYNQGMERksw5GaAOiH1Ho+6XqPdKUswQr+oXxfaSRTInZ2ildyLcqPyi0KyZp6BkENwt7w0Y+nRWM00k0HEgQPY3oh/hXSAQcBGwCwR3uWzzlAP1jVHE52I0ROn/ZEcI05ohruucLYauyyOfrPeHX4qufiD8FbZ0iO/qk74HR8AE/cANLka4F/WGyDQZtQMRNCf0ufGgs2VGLIMfM9BRVe6jnzrRhTRtrJ8ZAzAa4r0gxGLxFtoTDmRH36GXvBSgNgUhQ0APs51nuees+4ubPA06h5wadb3v4l2HzRgxEasHSOznyfJur7hJyzlk7GKCe0SUG4BUm8IhHjK4ugojVcUyPPgR9GufVXHGAKwy470gKTK47ZIXd0JV2ceQN4fCbArAmXkKCNEwQCtC5LOh5BU03QFo6qV9UdxMjW8mNsFtaawvYoAmOXqS+kR0h666iHjDegC9KP7ZldbbMVVuol+oq/mqyJfMGIAxwdqUipkDfMljDiChX8JiVI2NcpRBDAMEHLAGtNLk7hRphD9S20D/BzmwiExYK4FWhLlGhPqNtcYZTsQM40aGPiEeKcNtMPE9UoMA99foj5fXcO+LTQxQFQsOxDbPOAB1yhcWAEimANEEwWkACQPmJW90v2kvkx1nSuizkx0d7s1BD1qsez72eC5HL7Y7cOE47pkSt2ud63UwbQQA10oNlMYfRrlJYyjQUVX+HvSPEANUcbAUlQPS5hEH4Mq+rAxuGFkAsCnAjM4RPH7bHtF+u7oQsd0d4hHhjtym6ionhTifmJnwbASRD4gGE76noZNFkiXwcdLAoW946rygeW1nAdaVmzUy560YgHqgGCAC+uqrD3lDAPqCA6zsCan1PSIUFqBwhED+21zLeZTbPmXKKBeRhU0ViZctkcba4d+RiiOdOuaZM9PwwfeOubGeBenUwUeGTun8jJBh0IMVxIjAjWbhF6FfWM1FigHkuSwCNCUOVnfiOQcU7vJAOpEhBwNoHCHAfaHgZYG4twBBhG/Y6SX0B7VXu586l9HDfI5QeS0f6iVai0yD/OzcR23sACGnQAzmojrnh1ddLH+ErgLu49k36E9w55mE9tXSvlvQI+IVMSyu3gx9xIH75wAo6oFoCuBbjsoC8NLuHSA6qxJcizsWy8yQ+kU6aj6o+m88VTGzQhIXxTrF5dcWYEThTORlCxCUgtRnqnyG/gLuy+L3JaIIerDsSkFHs1A8JtvhAApKrANiVG6uOEKAOzyy6R38gigrsWYJjtimCPVC71W+S0fMx4cAQXRNUTgfPNg5yeJZQSUNCS5Xzg80E6TRSAmREsAW+mVWNu+FYlIFNGu+wWpjiiz794hFa1zeHn3PATSPBawZoimYKwkznJvtRyzhj3CqFOhKyokkXAwqh/tpkqvqs8gwPeiIuUdW2esmtuHe+aCuytSsCI3zwwreMgX6M9w/R/8a7pVHZ42/bVWCciR1oq7OFOJHHEC4HMIBJGdpYQGmIwQ4+q/6JfhdIiLWJHfZg7A3sTLgvrMGXTpozhYPsIKAKwijARuBAv0SxdkMew64Op9rdwswaFHJAriUSuEk+P4S9JcKC2HaZ/oOiFnGACuBvuYAosonRSUBXx0MzCVl9oKX1TOg9PVbga7VPx+z6NHOoUjZCEgkMI/FOyZdHrRBs4dgE3KqdCc2LEAgSbQAliksBoLQHqB/Dfe+iPMLMYCiHDEGkCL7PyBMY9vzWXFgDsTOjJNhaQGC27N0fjpotkol6624PfkYhM6d3yXX5cbes/4gIdAxGwH3jjqXFVHC4muppChjxzsL0HmGn6N/pY/kVFekmq0YwBssQoIo6wy/AxdDdjjQ3hRCvGSupHN+2kRnWX+woM/8aPJ5b8QaqMQpX04t8PQIJwITKp/nktjcAlY3gRVsLjZDAppzdoTYAqhkIsqZFWv0X1LtaCASXsA966CKKnsxwGywCgksj0LZuzv+mgNUAzMFtq9onZ/ixr+JgPId7jF3vd6tCPqwu6gzOykYAcowms/jVc83/i0SiBskbmp5a8g4wMsqAcRrHyzDjPVX6C+BXrs6D90eUUz3MQBu3aHOzi6NQ+YAX3vp+5IDCISBbbnRrPN5NLfS/Y77tCWFtsvOT1QzMm5RRFLA0fOxmtoRslMidsO6iWXPDmR9YfmBwGoFNBr0z/YmTy6KNlmhXwR7awd4/l0MENifUH7rDmXdLxxAGQAkDuR33YY8HJhS0Jvfw5dJbm1FBPLIsyivHU24F1WXt6qMCupRhBUxsSMuuPea6AKp7ufM8EusWATNtPu+0SwzmjmvMRg3Ud7zbNAOUXq36BebrHkR9Z3bk8lwHr5hTkVETevwIOGeCFOGwqVNqDmA4qxYAJApcAtgU2XUCwNKCAriScoq9FhvNRh6oW5kN3SVSg4I9K+l4UI2pu6/EG9yFocnSilYBttTm6WYoyxGkQYuEKtGKPH9Fv0KdymWxKiKNmFIDMDKANGrQeX/gLyX0iw85kBTZA6YRXKDQ8SAz3e1hSK+ay9Z7okGHhgstrncszT6Kh0h75EMyM+Jzo+bgrNltAYB+o0dCEbeJskziZZzwEURJNCJZQn3l+g3nUKN651lmaeaEANkU7CKjAmmbUiwc3u05wCPyA6Pt4nQd8ZyGvI3IB60kQx62eBgvpsNbjey3Ik+qdMfoV+4QIZyyyStfzR24OxfngYcPFX2KS3LAsxAj8IR1O6jv0b8UztAZ3nyVhN+HLfwCwX0UKADNe4x7DzJ9CEHCqOEwh3ikYLuL9E2wilT/KL7MfxUoAFVZlXX7Z+K/i6VgW9tB4gJJrRwS9Rknu0AQz+yQsUlJQZZZQ+59SBpPEB/VEk+HEm1gHtftJnw/Ef547joTEH2f1DjHgnubitMpW1yICI+u0POtMLvCZsFU1oiCIb4BDrzIai6uEOi6vJGFoPupGOK8fCi2gHTEQRxd374WZghnkE/ycCgF2vQSHRKgATYuT37jpA1ztLT4fLoLN49xW8904/j4okpSMWOErULZOMI6Jk5dxxgoq62ivbMVj7oeJ3ijAFXYgCqLzaS4W6dVKIviqJ3DYJkCi53NN3wMWugoTDhPiCe/Z8ZErihgJ9dyXKu7sYn7KV0Faessu5QiC/gXhLDOqTGPPmzhn4cd9sUFJbh1h3qwgDoKX/P544DPjEEC1PsVhJKUA/k7QSPSKDPWirtcfZ6a2sT5hRTyYFsCibckbwgJOfHdpPVv/lFRUhg0OebQqU8aV2y/LU1wOfoFxzzVnbFfNWsCb8KsWkKHHyG+/Nvj/sbDszLiwfDPQfoypCKujFhmUTmx6H5wITo+oc9Jp5YJmsd25IxuTEIbz51u+IIlab+UVoAArQFA5h534UYCYD2lI9HvikUl6Lld+hPmH6A/o8Vv2XmN8LQ02DbFBTuEMk5jjBdl2lAwlOwDQ7YJEETax0hEsHgY5khrMMQD8/rfucNjtRi0OsGSDpcYk6Gg07J3R446C+ZHG4KPPbN6j+ZAmPHrTMZYB/dyKfoz6agqLS8FEnIbRGxz1xvvwoBZvzMFDeIOlYk3KNxh7hPUfzB+bnjAEBzW2+VLF6Oo80zH6yIWCNF2UgHR55Gl0Txzwy7QJeUKDMI8UesOYgVQf3fmoJZuRIza5Y99C9MQaikPdpigvQQ5yY13M+3nSjuKmZTgPBmW9b92AsDwHyAcyCAfskB2ZND5dAsngTXKX7+XJcs0e9ypx0VnVRnynTUGXaBUFqAigO2ZeYLLUxByYHuplCpXD5B/wrxDdxFyDtkUArJr0Mju9ebpiCSp3CHlnzwjq1zKWbvnwxIC6kpF/e5MzpHyifcu9tToX+AGkQmiOg1U6aGAE4D8nbMjcwcCAGAdTVI/XMR144Edyg/h2mEnDFawF18faFB6oHP1m0QJHyv+DNJALAFYEFc1++bgrPVwh3SQUJ70fdGj5IDbENunKBKBAzxQutLMSl+RT93xcwB9Ta5p7MqU/R8XGQT96dPAvP1EdDvb0CQKWjV/ywGjwh3iBcZk5YxUeAF+l+4PRnTDRm0E8p8l7d90HtEt6ZgQQnNpzAA6RFB5gCvgmfHKQuFxTcioAMfoso3gToNEvq9fe55vQdlOlwmvkJMlT8m9Me0AOT2aABgcogG4UgGwVlhlUMpUd4VFTkbDWr0V9r9kQsURmywHuxGKfbY8noZ7khYNxqIR5QJwz49qsj4NiSwEdym0+jCAd8bnnDamQvAS+dHVH4GvRqBCv3cVcB92omwf01ynGUXiJmAcOO/5YCp+Y4MbAFw7Ut2hA7MjSuTrHGJfpbSDwYA3lusl6vC5QD4ZbiT/foscNMjYm8kY51G71wgCOgbDiB1UqSR8vGYLUDn/AR8j3CtEgPpLGiUcp9SupZJi9TYFOT8jGkBDt8+4YAo/nC703BPFiBAPCO+48DUMgF5S/QXyH4Cd8H6I59HGm89CLOdGNDGN6YAfknnAtncxC+yEZ0D0j5xoF4zyVQ9dbPa/ScYATjomRV8yjrn7QnbiTRvS0xyU/MmpsOlBwmFTevj0tZHpEH5USbAw4PAuoj7QuyELQb6dSTtAKpvEf9O8T+H/pmJQfAGDVYe0a0puA0JytujYhY6t6dZdkan4N71PSi/NAKQehBhhGbzyBupfECUrcgZU8ET7jFhGhwhVv/z6vpjQM9MwLUjQdez7slGIHL7l6B/qhIecd/nKaF/pu9Lbcsi0dLg3InsEW2ZgqTCrf86LPZhK9hXJmDisHZCGKy1I1ShX/J8tG3W3kA1ecNkM+Ja3BGaWwB6acfV81T5Jw0yBzQS4CD48LHc+eGQAJMG+TnxmgO00hAMTEHZNiG2aaGfTl11sZgvCc1E2kSV9G9SUdDg0jpRNIj1701B4FeCvrUxK1Roz3qFnsnQ53zU9PKp0Q/igDUGXchDl/vRTD6s1PS9Sd5hGP2fs2LUHCicn0QG9oUCDXiGR5Wn+etKt2mwyAehVUV0Pk+ur+a5/EbYXKqpmQhVvenpNOBi7/ZYnyEaxrzdmcJfm16aSL1Ok5fDHY7XjgZrF6g9hcKVKiWOoZMNC6m8TfN/MDGK6P/4MfbWukAEYIU+bfoxaNAmGchcAwh27/S9uIv7TOhkW7TsG3+Hdkndip+DdOrGIxJTEK1L7N4798oY/gpoQsroj9AHQx8K6zXEOxdIWGQD8Yi6zXm2nNjKHZcEwMKMHHDF9MIFkjxDP+o+Bz8zQVgRVzfS8ZYGt3B/7/MsTgULYCf6ILilQecRga7aiYYj9L0/MTVJ94eTBnGS3eAM6WnBd/5cZ78i+qWrpPtX+xqn6sIHfaWhMZiZA+PwrVMX6L9Wa7AROMgIWOx7jEgDK7L8iQOZ3hf6RQh7bs+aCb7dkRJPoX/m03eC4fldGiw9oh1TENR/Hw3rfsa9vRDL0olMUK90Q/2XdkBqMLtFzNcaK01ba/ip06AagT4mNI0GyQU6vmr1vzACZ7ceGDDxJk+Og/ahpBdIzlEUA3ELYmUJd3GQAGrfXMKZBfTPs9d3goGXNLj1iILPszYF1pOEBDQpWYimRnamkh27UFh3HMBXvHAU/WQjoBPo98BTErvz34xAVP9XTyPQQB+E/efNC8VPedi9UbIJLvlBM4zWQHZgyMIl/1TxiwbpIP4c+mfyt0Hf0YCVt9WXHpE8yrVZhXHkARlfjiJpZdYTSe5Zf2cXqLQMSEf1muCZjgOjXAZN/lT2JfqvhTAKpxa3Cj5efPlSTY9oB0LGdmRMm2AZHp1S7WVFzhf6/g7uoviDBvkh6J+V+jao33jep0GuP0/Ki3GgNp0pEHeIdpQpV6OIVmjCFbdH3KHOBfKar3sXaEAtgG3w4K3lGaJYgz29YpU0oqBCJECNB4EYOfz9z7crGo9gBK4aGwhTl82Mg79igq7JNE7aC0H/igmsy1iAItIN6KvMZ15/HNfy7sxgSYNjVa8ekQTHxbDJHergntOY+pdRPtEJBNT69nDNl6K8NBfCHP8gQj9vahR9nj+L+hBJHrOfZAEQ6+yMN0nBAP7jq6P/k+JgfibA5LndDl+4IL6Be4Z1QG2jTdp6O5Uacz7dBuX8Dg3gjbt6vs7alKYgOP07Lz4Y/JLNDeqn4kDnAhWm4Kt1gQZC+2x8WPrq/3CRNPoFfVygHOT/FBYAV035MHj8d/Wtiv+L4G4fmogEAIfVjIshGg2XGzMzvgtWf6f4fwP0z/TtmGOtC8rf0kBeEJr1OTBgQAsrgimQG0GpcZGyuEkNB+BCQVyA/kvJIG0KtsC4GCcgW0u7EtY+5sKt0WF4c/SbBTDv/IQ+CKlwoF5A94fBMSBGcoHMFJgdAN0A3dX9UyVdecF0konIJ3OjbAMUzYpT1VkfxX8Yy6oKp6SiQWypDGGISzMmW2cKCg7KgDF16Efh/+Rjie/WBfq6Rsle0IDX6N4MmvmCx4Rv9hsZ/ZdkhuPRjce4tL66QJUvhJgJR7IDwQuKd0VBcyh2Y8TMKPK2Td1ZbWBXpWZZv5Rnw8QASBB82AA9ABXWj2hQeUQLU+DFtSO0VDaOdRSwZnAXNGjugYbGkQyDuCdHznTJvoTO6j+gbjhMr7XTq/8DOL7S12KS4kel+0Mb/noAEQ/Uw31awn3X59mHfkS5ZpoGdRBc+0WWQf9woKRBGRgcALlJB531F4HSA+AipfWr5wMCMRzEpXuDAXxFQC8tg3/gQ4DH4j24XYs1Y+QN/84XWF8MR7GgkzE9AJDHn8NfKYIVP+ZOcQYUFg8au1zInGoAbhkaZW5IZSxy5gb6nfkdwDoILvyikie3NKD2ggFW8P70IA2VkyzJpCNGIDhCfGQmkJrvoN+q/xQDLLaTpVHLIeazBIQGWSBiMexjZzX8/c978xugR2EfQG3yBOrNqiB+4/OQ4+SXSJvcM+Lwe9A/03ch+Jg/Zn40u9Q9HEAOG6CKH5ULJLPt0sD0QKCSUt0/M91xHQaAbo9y+zkJ12phXDpmuW+mtSPEstB3Is589RiYdf9pHBj34AbD9aCGwvY9geb78mG9O9BPDYBKegvos35ZE4Py8QsxiAAUg2BA5wYdDeANjlR/9cFUYWKQrbBd3hExC5rVvCnmEugtB77qs6EZqB/EgWRW7RqKVOh+4gCL5er64L9X5gCGRb2V52OZ0gtCpoSwcJlMPZ0Fy6AUTqrxytt6BGHtQ//cEfpCDKK2Xuv7WHlLg/rluTS3cF3/BkS5qqx3C/9HiqVen/dAkRqEZtHzwVjt66B5ysSLtZvWyDI5a6YjJDJk9DsHznZfszyZgAx68YVSEbF92IiSErxYFkUHfamMLYv6mBEhhwx3Euv1dejLaUESKu78okQDVDGAj97QQHZxlfLiTV6RAwXcc2XzGFhUvuRBRgAoXKAs8dGvSkylPA/xo0kYly/EdtIwahwIH/N5/kuYTg+DEb0gRnmuWWyQCOQp9EdqFjpfwj1PQ+q/9YKpfs78rkGoaIAqBqhpcKvsc8qQysgzvCKiv8E0lujvml2TSWbHpxRnm+dvksn1IldxhBAROKiPgxqXfs5BL8nJEwBvE9llJEGsD2N3axzhqArrp6C/jXtL3yrLigndK6JKA9TpngZn3QHMMKCw+1xTKdTC9bcMR6iNOlffJuWzCyQ9yxyCzgPVdGnQMknO/HL4SNFRFOpVk12gAwHHBR9SMaCcb4xyV4x7ymfayzY9gH6F8gIJJfR73FuxeRkOYDEXLk3loKz8on0a9ETKszcxXdqXj6L4Z95gnY8lDRYuEMcA9/vaEDhKcEogqaFOHRzx3CCMDoI10+CIOFZMU+ZImUX7IiX8CayfQv8HVH7K988BULg9K9doxy8q7Xz5iijbH4Q0eJ2IwhLVK4ofxIeEaUX20gXyD2IAUGk45C3pkigLq2FHkY651x0XSHBcAj3EwTN/dcdH6goI0blthGP1Fvo9vlfQf4r7WPz2cKozAmiYkHcotlT4rl2m5ZvSMnUXK+kMPoJwGRCMGtOl4s8ttcHsDfD+VY3x5jXLqbVo9VooGIG0EQEEyQUS6KPzecj5OQ5tbHk5Gj0OWzU7Rfz3BfTLIGoN/T3c24j6jTAsmCD50iCALjw37KBlxAYdDYZfTVMvCTEK4Sr0EYDrDRr1D/qsAgDrVjJxO3XKJRmkkvhgXw0DPU5xMnRvXiUOcN+dS3PMEfmZlz/6jWFAmmlcTtosV08g6TV8QG5cZQLu9/IK1PAyHO6Y0OcPaZ8y9eNkNDSI6chLTWvTuJMyQAAu+0IdB0ojEPKgShrd8zaZOM+tJNtx9jm3Rp4VOgRNwoRp48DCBeIjv+lQAj1nFktoyb+E/pa38zHuLVU/iwIX99V+jwn17f+YlCpSn3pdpYkwQSFr4tIRQkTwvgsU9obYledga3kG/bRA4HIwTNNLGJDNtSmUTRfITw13Zq5myRcy7vnRBrtdTdJWYZl9fa3yP8O9FdNzACSIP2WCSaP6osxlEGRMqt8HTFYhAfpwmBrckSiR1T/ooy4Q0wZhOJ/PT6Hf1zk5YPmI/kOkGX9NbNMFQqzPFmCh+zfA7zMsQHzn7fwk7tOOfKt4EET5ngnsGumgsy1ZbZ5Ctg91JyQdwaLi3mqqaDhzIBsBG8gUvw3hxTiln0ynduAfhpjHHAZYS/ORdlwgyYCKrOOFBh30O0UmOn4RAGiD6qzkH+CeiisXqFPw7u10TFi7Rp3/80T/Z0+RXaCRYNqFAQsXyCGOyJDIK2UgHqziWYpfDh7pN1VNsGPqEfOd1i4QmrOcaiNgxBjpgmYJ198G8bsqv8P9ggNNsXoVQpomO1CAu4Nv5Rq1BmE5C525qH/LGC57C2Aey8oFsr/xKjpBRIrz+YWp4gCGPqo/JugH6KvriQml+i8tgFQiXi711ax9j4CwTUDIFBFw1Sy0RJL54lSqic8B5PSdHXjDhKVB2LEBI/xxLF6lEbULqXzHd4V+txUIjRn67P3rKDKrX5cG7cv0hcJXtOZWHjESOPjBVsmB6bVen6TUwx3SSuXf24DG3a9V/iPcP9L91E/xHACZDPsO0i0TKJm8RqpP1LiaqSIhIAb1Dwe9X0ZksLwf4fX2GeQCiVtlQ+vNjV+f/Hth+V7QBDFwuUDFt/DsOy7zCsxMRnBhBNKXY45ZD34i5s0p80dxX2yQPgdAQ4ZO8UPRf8OEhhJrh6dIWQSMxXhk9d9aAGvJH3i9bZjc/7GZ/Db0n8lfOph24KDiOT12gY6hUD6IKnV4cAQYgBq8T1lcpfEspfqKAyXoOfVvg+IhGXaYUF5IzUbLkWYBokii7neNbpl0S9QulxA5WIkJ/UC2NJc/kszJuSZssKYbQSAa2FVnJShgOKCv1lnjVTAwoilf8mOQ6AoJvsb9Q9BzjbtAGfoLMhQ+0i0WNplwmxh/IrJ0HNkCICI73u70vYn+j+4NG5zfrv7PJI7QuY9sGQ4kL4ho4ICOmv7oER9Gj5lds1Cido3718p+jwa1C3Rb88YspC7zlLboECUyKONan48x090IMqwLVTxC4DY0jT+C/jMFuB/AmP8vYwD8v6SO4AJlGnCD9R4IKx67QxyQZAF+ruyf6P4zLV0gVB4/tKYlw0a+Y0KbxP0Q2Rms+RgzFxMoRBYy8Mawj/Rkln80DY8KxvT+2WKL4s+OzeJTphwKr+Zm2be4fwz6ctdmZeMCoTEFd/TYvwUkl++6QtXaRvRMWguAqezlRhAc+mP2lt0bv60E12R/UP2f6Rj0wMvuh4p3NGlg93+QnJ9jGoHr3N26FnxYpUF/S3xvKvuPFb9VNi5QWXlbM/T81dOGU7SVSP2fuRHrhQbZDjAfRkT/QGyA0CAMVO7WH0xThYVgABcNCgvAt0Er3LvK57MzOfRpoGsCcT71VJ/kfxHoOW24QGXlazJUU5KL9ItF1cVBhVCGQe+nJqyZD47maBBqCyDa6G4tvzNNzMf5TIOgFoBUvnEAIyC+HGIzJl4kF2n4o/mPQL+BeKn8PqgqtPw5v4g5FgKGRSf9jKWmYMLiSJkRM6LmETW98qqczx9MZASMEEMsw2zmEbCp/NtNbFKL+9II9PBdufW/QPHz+fA26MF/Nvmwj35oy+KuaKfPZiNVcudfwe5sGSJgJO1OflG4PYpoH/a0zN+QStn7W9MM9MM5cFANuz3hk/ajswYr3yc9k966339bLGti5agqz5RcIGr0KR+2yVDD/c5zEmXsEEfyfxDVf6LBlSF7Yp0UXSFJ408nC4IB0h/0zQF+hc68doF+Q6BpJSI3ZPQi3e3gkMpHoP8A8VL/nas8RZQfqXI0La+aJxgpFP+UoPqOltFyygjiETQ994z82Dj1N54t6I8l0WMHi0t8HvGaMMnQRAKb43pmpAemIuB9AixrxrpZVwlAvxBzpvJGEAqUy92DwIdHxqFLizY8YkMA9nxM8FLDQbBZD7EhIT9b/m3pNAL8pRl5QnzYEZcpAAfB6YbPbeBrzTBHLNJaUk/VfEb8nYK/rV+5QJ5KhyfVP+DD86QdkCw6X8ibVcdBuAeTxPoRK/EPJn1ddO7XcdBRNq46dp7PJzeFzpmsigi73LbpKhf1dGrpAmEX+qFe+LAxmdtUSCZaT3NR9K5lUvz5WQGSiSjGzUbm70yE8svDoSmb9y8cCI7+HbK7hwO55b2oegIUPlJ31et6ALULhFfWAM2iSbhSs/aSulSLZu0FWT1VZrPANkHcnvEvUIA8+SuxO2TfIONbpcyHo6rJu1aMRxU7KauVlYJ/xIHnpyoXqOtlDf3y1BLUC5bdJ3HEEzxrL4iKhvLubo9n8z79xYmj3ut1IGKC8YGPR+IDd5f1fR7xXSrcy5/2cG4b3LlAuMPpW4Nw2/AmLS1AB/2BFDBk9HdDlDV/YRoJkiNpt8gB1/opkMMsvrgvdDvNB3B/jfW7aTcuEDb08wL6eALqdyGyOFGkpPPDWnGEuhB5zMu9JV7N7e9J8gQgPgcQm1DcEYpbc6B+IvbhDO9r1vU7Z/sGjQu06PETYvzE7SAkt8RQG2B9ZtMLDgx3z+T3IH5gmn9RYr8I8U4o6IlYcIEWNKhi5ZdM+Fl9v9MgNttwgc6047D/lE3oU1b51blGf1eWoayXy0fM/83J3P3z646Wv+YfbxCxL+T1TIaZjb2nfJzAs/SL9P1mm5ULhI2wWFquu1r28Jgdd6bz5vsWkQOjOtVc8Q+kCsYAAvT9C8G5dQJ6tgMfppUi2zm12WCjZfXTiI8GuIU+flh27XAjZiPux9pZIj6UrtS/BP8zjWi05w+WqL7ns/FlOKCxAzsjPprn67P7bfpLvrsTq7RvGfAL0A9y2VN9WSwBPSQDfbiW+/sXU7gHetbMPL8MdzUFBQCg9aebQiKa96HwD6r5p40B3LhAll5YBuwZh3dpaTfHstj1kx8tbw33NycGPeh1UbYD3CzB/VgWf2ySP9js+VWJACWXnw7/66CPm57V+SnbLyzDsvN/PgnuUWn6RbG6DfoDU/pFjfc6+V6ffpAe+UUfpm27eaPI39X8Qyn75aPaqXyLz2JlvvbXeLO/+8KY9lygMr3zi34kNQ76rt8yNFvajVz5T3Ah3/m5boNSDRD/4ZfgHtVSxRGKZ96nn5Lp2362b4P+3JA/kPaGrj2c/a7+CbzvpPL+TAY9atwrSX42/R4h96MkF2jvsj+SRpGrzi562Pwuy1+28B9I4urkU/P8gXtvdq0YnxmEPy3qD1ygP5g+m3NxNcUM/6I8NpO+1xmh6qXf7s3+wfS/kQD/P13pbxDjn57D0gX6Penr+SWfvoL4/9OvSf/gVv4FFuCPTwB/fhv+TPp77mn+ufS/jAB876+awxkF1j/M+O+n9T2A9T2GvQHeXvjn0l9wF2h7lJc3H+hE8e2+xY2R/0k02H4qog0f3XBrns/8+dRv5QcW4E8/wrhJixvY5W2+8k75/wwOlBLertx6nLLf5helt8+sfs4F+u1EWj2SLB/aVw/8DzuTtL68QNb1+relrMJLpT648eYLILcP4H+7N1tN4ln6uXeBXnfy41LLb7GX73ihAP39yzP/VrpF9l1xxyPaPft5+pH3zeJu7lmAd4NtXvWjUtPXFZevNx5czK8H/89Lo8jnV54WxaLx4sIfTO/eL954tfmVC/SzCv4HpWbf3+uLnkfAurwiH14g+0cp0QO3/l3yVDmWxfWIP5YeQf+5X/TqG2FPL/kFdrMAZP+tpULTx3+O4t+Qinbgn3D61yl7+eW/P0sXJGW/tBIPAuVHaWcDXvhFdMnSAuyv5xM1/451S5Wc32I/b/D7L9yLBRjOgcNwn/jwL6VKnQ9UvxVgZ+WXkWKzx/+b8fN0i/5949Bbhm0X6Ndp+ubewkdJfusGMU+ZY2r6TIbuor8/lU6NnDiz6x+GkcYrg1DW9OlemOsWO9Dfswzbvwu03+ypsn8ouPZEAnr9baYj6HsuSsZug151/4JTZEgdMU/nku6X+tTs0T8y+hnhLKR8C/18s3vZrLcAnwB9cfYnTOcR8/yRdvrbZgb3+EuAGOoU/eVAf5rEpTkdPPPyOw6Uv6s3yJsKdPgpeXWiX0P/FveNF7ThAr0DenfqYyPAqXgWln/Glf3+kVjBlLCfxzlI3/9z3j+n+Osv5Y/FGwdyS23D11IlF5+lfTXz2ibcEeOJC/QU6119qvzx/8IStL5VVf8NpfiRZJ7nv/hQ7NbGktYv/7V4uCo7QjnTjbtMNaTL2hc2YdsgNBbgBda7U/1+fKQ87qB49DRwfd9xwNQ/ghH4J/wilmrAcNT3fCxtgjpCPQHUTdpOW+56R4nush1WUIM7F+iRau/qRzpT6qTlRCzdPwGgGrv7GZBd/ksIqj/kP8zFWf+1NGBkjojL7p/fFLo/1hTQ58XH/Gt35kgnRte0q0QP/SXP9lygVx7OSsE/tJ6Fr5+KRxkASAO+UCJgsgwjNJkzrG6S/nWpU8+WJ3xDdH//n9Ts+ota5a/JR8l/qCOakPWhQdiwBpUFeKv13yN+Q1od6vLNHwd6peYd6PNC5QDcIPi/WPw3U/3/b+TfY65dIDIg5X/jpMHK7AeJsC4mIvChHG/bGjx/Elyuc4dFC9CvqVXiLzonZSq0fnKKTovhns+8CyT/ZJfdoZF/auovSI5pOGo579rdWg5vc51tLsmmwDJiCqpp7dnMMlprGrR8QAP93gvacIEqY3rLjWfFoRLmgulgW8ULjewO0uG7wr8VflC9mYIj7eDfhvucxvxYMeSmq+PbSvVgZd/ZhCZT1HHVlCBb4gcp04NGOlLlrolYu0CPEZ9r9rT+/X8LHAXqO88nRAKlvOVfYlmG3ok4EF2gEYiBv8wIMKDFdQGCyz6osaE86P4S99H7z77QFhLihq04sLYGXRA9YsWCD1QVCPADiM81DeiR5MjZ7slAqftzGADQe2+JGwz9I3LALhygMOBO/n9JKgXvTo4pfkI/K/sxyO2JAbFD3HorWZcNzkyNIpptFzJ94hqFfnf4AHx3M35Q+YQDj14s0ZoK/rvuUBLTQYhH9Yqoh7/xNtGIzf6KZPirbl/WwSvxwYmRFH8oyrFRYUWxTNmnPy/lnXqK/kUAUPKhcIF+geLfAn33n1qa1GGvDHzD2fgvgDgIlgaDaRDh/tKR/TUpYjLU8hv/wbnPQbA0QCAAW4MwWGZCrOfEsdxCboEMcmKN9Q1TkCu/u+n+JOilZak2FtaA6/nOT3yDLYNe3J6DT1gNceCwoSf0TxqcsfKwgf42IyAoZDU/oe8t+UYQNyDQq/8Tdf9IA/kE4nzepKj++anOYzKUNamSLMAODe44sKnsc769cCN1+r5oR9+MMZXPgvY7oWIB4HeQxoyYB/78UwL5L5cSpBYWAI5yRIOQmaAZOvIoL/yfAOAdS0p7pAHDZ37Rhgv0CPRydh/3mwQgI7DjBeWPNANxIJgItgCYrwP9JSp/nQijKC1ARL/cAiri4D4UxhzCS9vKq9DInfe/MAsf+0WNC/QJ6KVYQbx7tfCFDT0oc28K2B0i0FsEzDeOxrQYAx4Ej2kK/A25P2cEWIyDgOhv/kz9PYZ/rI2QQW+GVhZAIoFiv+72rgNke+kyJn5MhlSzdIFSzSfKPlyeGzwRIlh5x0xutrAAiOGB0cC8I/dzEOE+QjDwRzigfs4IlUhYRyyOZArEF1o/ETNrMKwIarCfCLXOhD2nKBAD0UeSmfQ19y7QG2Uf8zd+Trak68UPBKVxKKxLuHuDqPgxY99gScztmabgbHPRYA469tzXX5qC9jB0kv/jsObirLQ2hnhX/7G3fBdI7wjhXhYutKXjXjNhJ4/KLEAbcE3tAj0APVoR1HFtlVFL+hZTNeJR3AgKAQDoUQB/7M0fXNA/aXBeI1HBbzYCqlPik9qrJkI/eEHRR+Lwt1D/je5/Z7rPlGEZzo0rUwe+S/Q/JYNbgOLu+x/CvRv3OKAjmBR/8IIMjr0REBr4Uf5jrjU2v3/SgLdn/AlHSJyf4LiDAB21vn2uhoR+MwvBAlBXI47iHEMo8iwskcCKU3RpnzaZgJtiSYbv+0cYC4rv457yXQRcUKJKtZpn6A9tZL/2c8wughfElJjNDgt/zWSbBKfsyneE2nDkp1ISGqbiN01/nY8K3oFNuM83giRcVjLQiHk+OwYg0GAW7K70WNAFe0zAkhhQMsy3QfeVfSzu436h8rNMsyLhVKpwROyVRoBr1AuSxpMJg3ynYEMPvx30+75EX2lfRSoIwSxJJkNGvzwKiDWD+pdbQBqLVymr465t4Ro9ZcJipMosxCB4gft4qsY950uSVNqraLChSfhNZtP916mEQAH3kUCfz7omohtBZkBYXR2/jQOl72EWICI43FGIuBc17zTIjWeNDYE4brGDO+kglzLWtwYBvCWhcusBWe8g3X0j7CnuOX+r8qFyLC6hOZdCbo2AuD3zbKf+gUAGpsHpAh28fNqqEA0zBxDH/iQxXEY4sm5mzQ1E/6dCvBqB3gLocPD2PEPUpZnsNVuvuP50/HGgNuhvmYC2mcxvywV6iftYyZnOJnQCLVNGV2cESs8HVCNt5ELHdhLlpfU5GjYOwK3TRymhX2LfC67xVqbe3km6XzhgcC8tQOH9jzi3bQvgIiRhBlgmrLtBqDt6xYQ56soFeob7Htac6byd8UKgwx/fmkdulTBMTxQ6vsk4LFwgg75lUMmU0e8cmL6T7+WLNOhvFJRwwI0AAr6zEUCqCaeQmiGcygzcR/8RhaEXdX4RN1m7Rim/YsIsFs8BnuJ+S+XnUxX0i8tZ+fUwyi5Q6fkA87dPHrpAJjGz2ozOA6T7R/gWAWtunVaXRiqVaoLUdg4ASvQvXCAnDAJ5rKgZ1JvFQum8VpZEdtztVpu7kSlT3JVe5hdMaJ4DyMTLU2vcz7xLqrMGFR/UKbQ2CUBHygetLwZhbswC/dJ5dO9ppqzpOUNfKAO9VSprDzPuYTKytDlanRlYTXOHxz43YUAVDV+DmnMFH1HpzcW4WS7GiPiaBnYq04AbsGtU2pfKXMNGBKDPAXZwjy2Vj7h59akK+irc3AOHtrOIiGZUXg3sW7909pELNOIQZ9UpfbtNdLWRt+XCZdW6qlND8qQaBJfZw1FM9y5Qbgn2fLIXlOYAomG5IlHNuKUBnVUVURoEE9HCICAUl88BHuGe87fQF52xhv4M72S4/CMRVwPCqb3T5ognzyer/B0XaOTNoGuLMKDcIb4mpyF/VYCCP/ZSgs5+4gK1NIhDhLHixDyblbS1jbeAVjTgsglwxPYlVUqDkPOga7F4G5SKBaCBfOGoKh9B/36smI6YyWjmzE770hQMqh/U4Gx0+T8VMWQnipCgSgO6dq8ZsSaq/0UAgFjswoCQAXWCeSqSkDO368pSqmlgdlWuyeFB03WIleGXhxnONsu3QT9X+dzJLfSpWIOAJzBhyDd5wnG4d4Tk/JQukEB/xHxnBETIbAQOdmHPadi12QgM70drBHmsnnHhFRWyRzolNCh9pELxxwBApsSztcV16sC/TSECXNMg9RYu4Qw838bKNNvaBbrBPeU/hT6iKPMxD201xoFZE/7VhUG8cn4uShw4DuDM/Odtbl0gRq/olOT+pLmXjlS50ugN+pFwP1Bjd+ECXZVf4Sx3nh0hJoOjn2fbLadJB9RRvKFBhfXWL4r5lWuUnwPcQH/Q3x76yoon0L/VLkUi0EuN2ofhiPfKM/M1KfFfywFE9I849IihsO8LK7z0NDTOOixWRJEDALEGCxfIP18FQ9CBPnEgb1Ocep1K4xmKGzQI9Wu/qDMINnOq//b15JVU+RqXm9CP9W0A0F0licGYXSCkm55SnEfJ4GsWyCaMahd1R4e+Dq37RducFzRiYXBGwGdFctBLTBuO8TUzjZXQIoqiHDfVvyv7KBMregdTxKra59l1KNz6RTEvBuG7BtnPQZ/30o658SgbjLlrSxGHWR0OdIh3VIUBJQ2uzxeOA2PaBEb/Acn5UkT9WypB366jkY9jfQJ0TA7ULtBX7wJRZlRk8I3IrheaTFyviuUIGzRIqRTPgOPjFKlHRwNUZ1HnT4PQvwox6O+SJG0YUEI/FhfQLzqM6ZguzbkSw/e1UvKCike/YzpCE99IxHAX6MujhUEZRr8NR9taM2ErRdlm58flMz+7LlDFkKDmG3fItinrps0FrlwgRGJY3TGHVrfyJgBY+UUxv3oOUEOfWj6CPkh2duQeTL2FYp5eWg4Iit1NoQL3durrqrQYAHRVdoFGHNGlMYe4cYSq+Wu+UxaUGVBMi/4uz46vxIF8JJqNNO41ybg1aw7Iwln916Yy1t7eGF28NX0NB6qM+fo5wNrbQVq/ZwT6M5Otp+zuqPKKgDIZ4hn6w22c3wKyysYFunA79f04gP+0mR19UuNqEWQ70kMxmupNqkQX9O4ENBNjoexBoEc81fk/jvvod2W1dZvs+xIYjlRHbPaITFpR5WzRwDcg0aDhSXgZLijdtBO2AfXZNfSlWQP99qxMrEmHZCI35A1Q0JEvDJ+vy+c5pvMzznhgdsgXeyVtnNqNZB8kjbgXcnQQI6HcmtH9zZIbTAM0zcAef6eV1ptSKXg1BWVxiiwEBvs0iJWeAflF8PrLArSrytDPZ19AX9pIPtLAOm+lPVx+4V+/HIXiB98JlUcB5AVl/+fKTEqoMCgGAH1BzOAfHgN3HMiaKIkuxwC1Ci/R/6WNCxcIsR8rNjuYUwO/mWdTkN4cEVY8pgGf6l1P9ov6l+F2oB8RvK6XTV0o/ivPu850ukvBDjD6h2to0f1iGexOqPFhxMY22WP4lSal8PVIk//E/THXYpTgZbIYRyMNQXaucfjOGNc+RZvMAdsg2YVmr7stUBpQSNqaAlMWg68MX7RQGlQ8q2kQ53RWLF+G+zXQlzaaryihc6B0wU9+KTre+UGyA34JKX6kh8GXuOx+aDwG52kOjfjl4NVroeWKopOZOTCmYq6dloTsDHqMIhQeSJ0gkCFsE+Yl9Z7E7aFXQq4tmpUOX4F49ojitVjXz3ne06B9GW4H+lbT14/U7F7xI25AGmWRBJC1F5QeBrtq/3IysC80gY1BR5/RiL2YL0StA/R5Jzgt5E8AvSrtM8W1coHS0wCkvF9FTBhxLzBxv7kdyG7IVL0jNXDxRL8xILi7+1nWz0kWNKC8fiNMQRzzxdlR1U89UUBfigL0igZ6IZo0gXmI80OZ4/A2INCCMBxq6I4QRwsojcBkwmUBpobj74iJFyTTl5xrXxJOaQEwVhBfmYLIASeDbIfp+6UycoAR0A367M2zk5NNgYxwxIcDCvfoKXm9bTPR4Drr8yi/EfYJ9BFBLM1GalDmk+JXt8pFUe2FIR5TvpQxeuTw1xjgt0EpIB7peE1MqDNHH3M7/Vem+UWgxgIM65YzJBlYZrisOhdojOslCAY9Ig0GgT6QATGT9jRl09ZUmFZ3qNzKMjiuAgPvYY8G4SyAgW8W9yPol3IZ3KCS3YoJqZKN/rk9nbh9YhGFZzry04D0QAARyfYZ9JhMAgC+HQRmwpjkiegv9qNcAguKOSCKHxWIY5GxvnaBsttTc28xc1ocZK1T0xv0x/zTmgKRU+kRIVIl1dvQ2h2d/bZ1Lvaghb7VGBf34b7OlzRo5smeD38JGPNfXl8NJkbZOJjiV+8/fsa0A5gGRN+JYCfHthZ0jdXwEioyqBjFHnImYTfAunoRqHOBXM4M/bQ1Y/gU8hZ43oBOvhDI8wk2IfXkEE/BsXcS2wAvaVC/C/QA+lM00vKB4ud8Q4OCWnFEv8s+l2m/koIYB7P/k41ATtfZr6D+Bz335Tuh7h5Fxca2+4KFyFBkW0mp9v7hIK5doOrTmgJQ5dwCTNy38hcH5qw5HwAL9Jfq/8YUbHpED2mQboOmpd5Dn/YptLyDO7dc0GCM1Ceq/KF5h/VE/5V5YgRGytsRZgRs0MgBfrvLKZrBwmnMw8z4wjkTob9A/77693rEIdL+aj4lwmoBfVH/g65CLA7qK7s6nUeEngbSGHIbVKGf9kNYketfKn7rkOR+naJdH3NEm+dyF1xwHAcr9KcoFp9BbUA2BJEGmQOXuLPVTiwIC0myHROIEgAwjrML1KF/iwaW58lVMxakhWe0E3N8QwyIIQGriUemYKYsW+9zYsAmnDkTvxBTrfCZzxO3rb5k7fasaVDOx4rJCFhSGhyhXu4C8adT/9d+D6dB4AC8xn0evguEOMMR/+aVjiLDXhBeuUDB3e+gLzTIkqcFCfR5yQdSSADHYucOSTPV8dkjQqi/fTJQfSHmFvpJKB8qfs93NNg0xPk3D+ntIFQ0OAiFTIYdF8j8n/BCxMxb8O0KjAOV7AgxB0iSg+Upah4O3GcukPW2gD5bHsoU6E9rYRcIJfTvomHPV6bgI48Ik0gAhjwIW0Jf9qbQUohIleLH6C84kDdD1MBc9UE1R+ULWVi8coFGHwYY7ud4w8QtXlDM88QLDpCISnQajgP64fX3XlBkVOh8R+CcSH97EByVsYcEvdvz2hQE2a5pMDPfssJ96ENklER2z4RlpRZtq8opxXX5PNlQWjwgRoAb37lA5vnIbdDLCrMjBIf+Jf16ljGRMDMNMEWRXZdsEBYukOI+9laPy/sYU6VeMeAcQH9HCFKZOWBt9k3BHFEmV9KAngTzUgW4SRY/qfiZOUv0h52TeXIy/DU15urYKVX/I+AewgFDvNCA2UVM8J04/x71rFFtwZiLNWiquzJiA0I/IhOkmXODr6XdKcHQpUPOU/jL+p6LhuOaD5Z/agroknNp4hGE+Nhug773eaCCE8JsoT/SoEP/A6NswQBHBTEeAFkAtwym+CMZ9GsxdhzkCIG+KMwZeObyBPrUbYQjMjk5JkNBv4Ae9ClOkcxHPnZyRtT/E44nOjsOGEjUL7rLg1z80hQAWx4R6M5EGwRn6Pv2UP0C7qGTHad/7i63kXpVVIwY2RShPsnREW9nJzpN9/uNnUmGMVoOuCM0e/PRiQCutxYpGzcSSOH8dOg3zSKgJ+EbqbJ5yVqmxL94c5dEFxwA1RsQz2vXHLAh4jeN8q39px5R+v8Amz6PkGEJd25cttlHv2+Y9LaTEiXEAtRFVv+YW8IcwLQAFvhaKIxAgIFY000SE+ucMWFGcLugRpSPgL5ygQa1tKGNFV6zFPIFRXZpFhyQ+qULNGgILClxDT13dtcjAnCk7wMESJVYR0GSkc8uEa/or2hQot+1V8VJTWPq76Uj5O6K5c1fjwbhyicOwBwhTGuA2gUKmW7OZYaRPcWS0R+4IZ49f1DkmWx2XIkX5E5YBd38UQ4gFHHnAq0ekCGcWpkC29bh0/Nd4NugK+jnmgbuoZ87fY9X6A87ynMImzLzWQqm+0sLENW/XaumIHLgkuxEfLjt80MEGJn8LJmIftHrrRGIed7EAgBohWxVrOwLDpTqnziAseIDRoRvjow3TQGCR/Tt6NnEOsmoKN4xQUw5V67IENEfFJvsTN6koZ6PH63yoIwpFfaClhw4pSxx8DEJMGw2PLMyDc0PKDpZ8btMKvQzvl1f5LxkeBojlNpEUNvhgLcUA5L9ouTzZHcIrPt3TAHmhcAY+HbhUkYVf0WS0RU33Z5cs41+a2mO8uZWHfJZmoIxKWGIB7tDqC0Am4JrA+DFggycDPSGe5KeSonlw0KLfOBiawTiMeNBZHhBi6xA9v4XHOCWKG8NkZA8v3hAVpoC66sxBWfLrQdhshP1xnDjR+gvTy3RbxfKNKBZrwqYzkfMDJkC48agYrgfOovX7nJAjPjkax4d+TPnv1o7fE15XSOLq7SKI7aMRe8zZ+JOBRrQlAvZRhQ+4wD8whv1X4UBgyZQgB6BFR0N4g9jCfofKn4gFN+hX3UbWj4EGvDkyz0SoNPRtX40Be7MMA2G82FMfX/pp2kWLnGbyhcmoCrK5KOEeaXiBIpGYA0SvM3GCPhYoxh3JU9ig4HvlgOAKw6Qb/OYA5iAfmgKAPWIvlmstQgafEuxIwZLWRS8NmuKnTVgGpQaS9MIgLZjESVPrHsozDHAcNxnCyARsFMCvedTThWF9MJiSwtgGcozEwTu4v/I6It0iYoQ1ml9rsFwGed7oN4PK/VK9xfuEOrirSmovhP8SPFLMZ5S9AsN4qlH6LfenA/NzrHhvrALas8+T3xMZs8UD8OxwJ29oBHgbs1glMAzAoxGv4j0gjTE+Zl5GBOQ+MBCExosiMBOyIb3rxyoikYhsIWJLhN4xAUlYhFLU5C+E7xW/HEz9OyLAGCNftmHxAemQZg8XyiwGyqF4PNMMoD8InN+/C4Qef9sDUDQF93vcLlNaeG10JJe8AypBpWS8YHJgCjGXoasTQRtjHj2Nk8F/IADzC4edOkOhVMoznamYPmNsB0vqCFGRjznd9BvG2b7eo3DbeIGB6LGeToETaxsDYgV7vlMMrBBkLs97AVl6Hv/R9zIRWIOx0Wp7iexSOAEBBpA4J5UhtBAYRATOy2ITrwvnBS/rfwxBzoXSEJhd6qKERFBr6aAvxNco3+nmIjxI+hfuD2BEkyDxRYOQp5BP+M+2gEniThCqC1AbQcQQX9HAM4PzpAYd+IiFYsRAwUTat0RZz0oD4Kawx0Oyn3nZ8EB4UOp+/WBMUIRoAvnjrspQP8grMV33A8hRol4zq/gjvps6faMOVyp1eqNnGvm9SPjnvig28CO0KD7/ZapoM8cuDECUf2PMiMrjXxgGrDwC8SXGi0LzSdNeRILjPmNO1SgHArrfQ6AQ4LSLICKiGdpLWYKvjP6RS5YkCEzgeRbN1igfxRnDeg+buUU8XBen/cyuT3BL6zswAFnQnnLfwwyBSi8/0vsh1fUHIgBjHrnSaR8LDIJ96Xul90vJBZ8Zm975SuHhyF+ESPfCEqqveQAOj4g4b4E/dxlJQzt/s3LcLvFxg4UDWTneEuYDLzHwopYGWhAw/GieBcxHHxyD1TsgDJBNqNyfhzoCfc3nk9OIsworuwIsSRN1KVYRhSR98+V5ZREDUPfgACZArUAUfGfvRUcOM9Uqp2vXYQErTuEgiRoY4A1GeRsYwdKiHfoX7u2UjPglZzBSNNgRlma7B+UFx0jIg6gh+r+7AKZnfEMk+EuZSMmchPJi3JBwv2IuB9xu8PW22xtzvGejJ1WjT4HWt8Gbd+KM3lOiCN5/w90fw/6EBmj/w8xN3BPeyD52unvam7Rj1Djp6j9mNzI3m2RTEALO4AY+CZtZJgWF+iUdeHqiBeUZlQXGoGLGAsncKgodnR/lw72/uG+Pky1wzEavKDnHLAls/flCmXUHACWlECYvzW4jwEWRVEqOf8O/TIfqx88KDW++uQLq044HSABmWiwtLOzcenx69F6PuLm6USK5CLiyUs4VEm11ET1liXh1BMTAEYOyG0fkx57Qa5oTJIbHLitQZXXLcO9O4T3MYDY0J9CP2IxRw7zVGEBaG46yXaHg/rnPbsY0llbcpbYqfUjbozAKo2QzxqKi0Gprzkwi6z4RyOWa6qd8zPVLVsAQPU9KsUPtgxvOVDkkTYIfXE2xm4MkIo/g37o5UqJOFZoP6HvE+sdoUDjnAaBtRfowaquNAJs/Q/CFj+D2yaA4N5FkeTGp2ALj5W6ZbHzMrHhwuAygRukOKIFMCWS3SFRH97+Ew5khYW2CDgrtmIA4QY3/gj9oy52wQA3WFkAmipyZZmGQzPYhLvwC+L6W4bdfQL9rRckXC3cdFrO6IolB8reutQ5PwgY6iwAoinIbo8IdtPzWXEguawQSiDQ2GMAFfp+DLDQNO/QT5sU0B83r7UAkw/OyWkK5MJ6u02RTEmxUjEo6N3uqqiRwIgj3aahxSAZklhXDJ00un+Bf/Hpxqw9Zu05SmcBLOMWQDhA9a85gE3dZBxAEQMM8DfCRIJRsrYBP4Z+pGLn/2TFL9A3oNNUR5yYrWiVaNuDWBE3AI2gQfhOmSIe6KcxYrHMdDulApcO1xIIcyVtenZIwVLnCJXBQHtTyECcKXHHAaTLebbZhociLS18I6wlw8LplzZ76L+uFPTzDg2tF0VuiB+85USS0FV1eZsGwZT1nKCfnR+qD/EAyBScSfgQZyoFXrjm19BPmWqYkNzTiZxn5ydbgHNK7Ah1FoDHCJahuYX/jAMg3FtjaJvSHaKfR88K45ehXz2cBvRepKtChmZSWgA7hu1vcOCKzzYSQX9wI6UBGvUvY71wgcqZL4AeEe/VoziviXT/weg/RxELgKggInnUsanIwBxQM/uIAyDcR5rVIQHcHZovw1UaJRcD+ksltM2HcVcpqA2K37Y8Oj+G+LDVwzOB2Otk+wTaXeJGyQdXnKDLQcV8YzGNWcyQiqURkLOcKXpbJp0eWwBceUO8OELHPFs8W6zcocCBVPmAA4i6v4oBQhHeg/4qxG9CP4FSKp0DVF9agEE4GDIlPmudUONdTJguQRB04AOS0wza74kfv0OyMaav3WpKP75U8CO22kZ/7U7gUp+wlRLQER0hsQCwVfcB8ZSOVj7jAKLuv40BqNjEABHuUiy9oGfohxY7C1D47inDbUIAMGkQ1sINlumgUTgqUI8o00DyubiZRl/s4toqvzVy5LC7QCY9CgNGRQPOFM+8KgvAR8TKZxxA8n8iB5ApMVda/YukvRigrdxB/6jQP7x90IIj9iCZeSrQ4MK/wv2lHbCWEe5HAsfV34+gn8eNxVEWn97qodSFkmdXB5F/CA3kUYB4Mri3AOXz4MdvRkArJQ7OYbFd0sQAJtwEoLbNni/El68UM6EZiP1IJjb2iRFJBtFG1hV8ZZ4A1IMXuBslDu6E34SRltJXmTrgjigVm3y+amiF9pwXVbnRPvODJHxcYUDQpgiXe6YKBhB7dmjKU5cXT8RKDthsYxtMtt/EAKWr07XZQf+QY66k9lnxo8+0AYBtf0Z/4rziycSEmLh+npOi2gSqDEN0aVRZqeyL63pyvIPnI065T9iMQB8GlJnaFIg7RNDUykccQKhE6RpVcXD6UvxvQf/VaUWJHBJgkmFQvWTONsECzLNj4n9QD2FuGSUj5Sd23Z2J9QJ97sEpdIv7PIt8ybJmNPU+t6HFQvePuC6+zzi7lTBAM40pKNyh/sboMw4gVDIb12Fx/W9SfzX6i5CXplFzgK9NmWABIjEwfM7OPZkbrQuLYnJd3LOc59kd4muLQIB7i+cKpozq1Kha1tcX6BfPG4J+WnIIiKswoPR/NtHf3hj9lRzAXHvxpfjfhP7UP+9oDou5MWck9pWFjIl/jgQGwhp1pZYyjDK4q/oj91Y6/TV42wajqd/qMKK/eDqb0Z9i36sl1dw7QlUozE5YZsjv4YD5XfELMX0E/CH6pU/xwrUyMWRACdNBf8RT1hsPx4stZ+KpgVSh+HP7SsEXdmBrwOLEiIVVt0ecQPT7cURQImZQG4E1DdwRitrdBh0ZjpED2ET8DgfgfeqtofAvknbQn4JXb7lRI3p3UIet6y9FRm2apLB0TPyr+jfOZJbyGmWInOxUpeBDBFxagNs05G8/gSYx3CXqFTVZo78yAuG1Ajjs1t4/KmXP7hDbotdPxORep05ygp6Hq38c9wX6/dq0OwzxgPsXHOD6mQm8lZpuNrKoUghIl3RpyYStHh6l/d5MBUpRILJA/7guPGblmBk3Anfe/y36S5+HbEbMJLYEDiAQu+OAzbz4cdx36Be4FHCX4ucc4FEqLT4yH+Yo4VSecFhPVazSwaLDW63fpVFml2kj6hXdn319WwXfOeEg8v558BP0lzdGIa5RZMI7DljxW0FcFUtKlBeKq1N0mEYJR+knFSWDiO8L09LhLNknp5cx8Trl9vuUeDpWThtR70jzqcKWWZiv94BwZpwp74eWjlCwMPF4tSRwI5uFoaam4wCkEjHmmcXqn+R9hn7rpGPF1Vk6tRMA8HzuA4CsztMl6kEJD/kSqd9LAYfPYf1mIHZdEICCyhPg0BZUo232woDV8+D+RaB1QFwGCbccQBpXmHYW03eCN0JhNvQZ/aNqXHpEjzmQJlAGACE14W9Atl1rFSVX43o/SUfVx8e9AlPTu1Yl99lhasMR1o8K/WBinN2MJgyo0PYI/aXPs3VjlOa25oAvhyb5Bd7UDfSjqlyjH9XZcOYVB2yqAmhT/zbPRWYw9PNa5KpRFf+STzdJTC2O6cPMyoNOrQV1DBx2OYARrj2o5wMhI/XHSEf4DPno8+QeRrGWYgmxUlaNOKXwneAd9I9U2Xr8He4D9nXqduGCAyVGA+hxXThmfZeB9ADt2SfMGRQ1+eQvSmpAos/DLXJcmNW/QbZ0gc4MRwvZCJhWxobuv7qVUeZxcVNI2rwOf6XN14+jv/V5MsQTvlvQJ4jzTBijD9R/7EG4pIuSHsI1seZ3fvr5BE18koEWWKj/O3HdGgFuhkb3gxV5ZRCQm1VtFnZAdTzNH1Wbr/PPj6EfIWmxQ/82B2RQHnet/nk2XO8XDT+la+c5cAAzQk2J0fEZxI8e+hdErBnXxJkHXyWK4pgOzEEg9sbDG3A6IsEUjm/Rf8RObIZF8ec48OU7/Qn6recEa7ARsP5CgS4p+4HOrbwHBazUfwBPFHGGvjQZcfTAmwjKonKbFS3Wb4dIs8pIyupfFwlHcxcblEbAu91BP7RSLs8hgS5nOCU+4sCs/xIA2eln6H/oArncOu9oWcTEumM37iWr/3iiaUZnM9LkEj+xRmSuf/fpepNKAEv1eVQ0yNpdRtw1Apvoj7hngxBG33eBYoMbDoheGDhgMQAatbpAP/pTDfprtycxoQ0A5jwDCCMlsvo3JoQMdcKrDrhq7vxqkS8jgI4M39hgC/EAsnHI41YAva5NpiCrf/d2CO7ZKUIyAkfUx6bFH4QBdKpEOeM4nwVN8p4D0gmAKwZ4h/7RnNoPfBPc9RKbZz8fnpIVB7SrkOGiQF8Gkg4yDSQk4PY74C5Tc2FwD4bmLTk6uTKuV2lQSobOdkbAmrUqOQ30IAxomNCGwq848IVH6BcZ9U5RkmFoUx49e8u3mVmrf8dPjAG0yBONQJV8mAknoUuJ9RLWi093eTkogAr6R5NnGrAv1BV5uNoIdOgvcc8dpqMGA4jFhgOQtW9z4OsZ+hPuFaZxbYJmXXhnK6hB6FzmNisXRUVJWaygjyYvNBhdO+5xhCHWKWC0JEMcLngXCTFlXh2bHRHZtcviCv0LUzAU7rmBXZgdHqRmjzjwZYU1+iFnIzSv8x3uqfE6AJCeCw7QxOSGVS4O6Bxorv5XYca9laN3PBImlGAfG5/uqplYR3plzjBu5GYIfZZLCtbAL++LtUfeePa3YUC+BJF1ueewlg0OfG2if3HLv7ztYy06p1+BHpkgp6R/bhNQ14e2Tgm5JUotvXOq1gn0NPBKued71nTgrrq47u4DENe/at5CXyBuxYiSwu1pnB8sjED3QIDn0+Oer7plgp4t3Z5tDrTvAt2jP6G5gG/VRvtnPlg2Xj5AmUgMmSoI66Eqp4iDjM9R5Tdp0Hb0KN1duIZ+bmbFGy8oje5GgM6KSbFKvxcUQbwIAJSWPRPUI0qAfsoBeg6wg34beqRT0qDBU6Ea44gLxZ9H58oho4ojVM1s0IcbqGFBmyQ+KVq+Rn9zuTr9ff+q++PaWy9IGkSgG7aYe/ePqBamIBu6xruTznWI5egLDnwxGs4/Ye97bpQhKZJqv7/7GfkgF+rEBGfCVWZCTPnOjwxnPWcyxAqtLMZanXyfSv/Hz46iWQl6ryEc6KdaQ3440BkBh1qeRon4sTqGC2liNQdGdUra0xL+H/kMAsbYr+iHAAAAAElFTkSuQmCC"
View and manage your video projects
- {searchTerm - ? 'Try adjusting your search terms' - : 'Create your first video project to see it here' - } -
Click to upload a video file