From 34348881b3fdd3801acf1bb09ab6d9e74276127f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Wed, 2 Jul 2025 17:18:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=88=86=E9=95=9C=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E5=AF=B9=E5=BA=94=E7=9A=84=E9=9F=B3=E9=A2=91=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/constants.ts | 2 +- api/script_project.ts | 51 +- components/common/EmptyStateAnimation.tsx | 4 +- components/pages/create-to-video2.tsx | 2 +- components/pages/create-video-workflow.tsx | 81 --- components/pages/history-page.tsx | 331 --------- components/pages/home-page2.tsx | 6 +- components/pages/script-to-video.tsx | 7 - components/pages/script-work-flow.tsx | 197 ------ components/pages/video-to-video.tsx | 783 --------------------- components/pages/work-flow.tsx | 2 - components/ui/audio-visualizer.tsx | 365 ++++++++++ components/ui/media-properties-modal.tsx | 514 ++++++++++++++ components/ui/video-tab-content.tsx | 22 + next.config.js | 8 + package-lock.json | 23 + package.json | 2 + 17 files changed, 989 insertions(+), 1411 deletions(-) delete mode 100644 components/pages/create-video-workflow.tsx delete mode 100644 components/pages/history-page.tsx delete mode 100644 components/pages/script-to-video.tsx delete mode 100644 components/pages/script-work-flow.tsx delete mode 100644 components/pages/video-to-video.tsx create mode 100644 components/ui/audio-visualizer.tsx create mode 100644 components/ui/media-properties-modal.tsx diff --git a/api/constants.ts b/api/constants.ts index 08eac7f..06ee960 100644 --- a/api/constants.ts +++ b/api/constants.ts @@ -1 +1 @@ -export const BASE_URL = "https://movieflow.api.huiying.video" +export const BASE_URL = "/api/proxy" diff --git a/api/script_project.ts b/api/script_project.ts index 615e99e..138456a 100644 --- a/api/script_project.ts +++ b/api/script_project.ts @@ -18,7 +18,7 @@ export interface CreateScriptProjectRequest { resolution?: number; } -// 创建剧本项目响应数据类型 +// 剧本项目数据类型 export interface ScriptProject { id: number; title: string; @@ -32,11 +32,60 @@ export interface ScriptProject { status: number; cate_tags: string[]; creator_name: string; + updated_at?: string; + created_at?: string; mode: number; resolution: number; } +// 获取剧本项目列表请求数据类型 +export interface GetScriptProjectListRequest { + page: number; // 页码 从1开始 + per_page: number; // 每页条数 默认10条 + sort_by: string; // 排序字段 默认update_time + sort_order: string; // 排序顺序 默认desc + project_type: number; // 项目类型 默认1 +} +// 获取剧本项目列表响应数据类型 +export interface ScriptProjectList { + total: number; + items: ScriptProject[]; +} + +// 修改剧本项目请求数据类型 +export interface UpdateScriptProjectRequest { + id: number; + title?: string; + script_author?: string; + characters?: Array<{ + name?: string; + desc?: string; + }>; + summary?: string; + status?: number; +} + +// 删除剧本项目请求数据类型 +export interface DeleteScriptProjectRequest { + id: number; +} + // 创建剧本项目 export const createScriptProject = async (data: CreateScriptProjectRequest): Promise> => { return post>('/script_project/create', data); +}; + +// 获取剧本项目列表 +export const getScriptProjectList = async (data: GetScriptProjectListRequest): Promise> => { + return post>('/script_project/page', data); +}; + +// 修改剧本项目 +export const updateScriptProject = async (data: UpdateScriptProjectRequest): Promise> => { + return post>('/script_project/update', data); +}; + +// 删除剧本项目 +export const deleteScriptProject = async (data: DeleteScriptProjectRequest): Promise> => { + return post>('/script_project/delete', data); }; \ No newline at end of file diff --git a/components/common/EmptyStateAnimation.tsx b/components/common/EmptyStateAnimation.tsx index b86b181..c621da7 100644 --- a/components/common/EmptyStateAnimation.tsx +++ b/components/common/EmptyStateAnimation.tsx @@ -567,7 +567,7 @@ const ImageQueue = ({ shouldStart, onComplete }: { shouldStart: boolean; onCompl }); tl.to(images, { - x: -100, + x: 0, opacity: 1, rotation: 0, duration: 1, @@ -797,7 +797,7 @@ const ImageQueue = ({ shouldStart, onComplete }: { shouldStart: boolean; onCompl onComplete={handleStageTextComplete} /> )} -
+
{imageUrls.map((url, index) => (
{ // 创建剧集数据 const episodeData: CreateScriptEpisodeRequest = { - title: "episode 1", + title: "episode default", script_id: projectId, status: 1, summary: script diff --git a/components/pages/create-video-workflow.tsx b/components/pages/create-video-workflow.tsx deleted file mode 100644 index 88ae5d7..0000000 --- a/components/pages/create-video-workflow.tsx +++ /dev/null @@ -1,81 +0,0 @@ -"use client"; - -import { useState } from 'react'; -import { Card, CardContent } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { Progress } from '@/components/ui/progress'; -import { Separator } from '@/components/ui/separator'; -import { - ArrowLeft, - ArrowRight, - FileText, - Users, - Film, - Music, - Video, - CheckCircle, -} from 'lucide-react'; -import { InputScriptStep } from '@/components/workflow/input-script-step'; -import { GenerateChaptersStep } from '@/components/workflow/generate-chapters-step'; -import { GenerateShotsStep } from '@/components/workflow/generate-shots-step'; -import { AddMusicStep } from '@/components/workflow/add-music-step'; -import { FinalCompositionStep } from '@/components/workflow/final-composition-step'; - -const steps = [ - { id: 1, name: 'Input Script', icon: FileText, description: 'Enter your script and settings' }, - { id: 2, name: 'Generate Chapters', icon: Users, description: 'AI splits script and assigns actors' }, - { id: 3, name: 'Generate Shots', icon: Film, description: 'Create storyboard and scenes' }, - { id: 4, name: 'Add Music', icon: Music, description: 'Background music and audio' }, - { id: 5, name: 'Final Video', icon: Video, description: 'Compose and export video' }, -]; - -export function CreateVideoWorkflow() { - const [currentStep, setCurrentStep] = useState(1); - const [completedSteps, setCompletedSteps] = useState([]); - - const handleNext = () => { - if (currentStep < steps.length) { - setCompletedSteps([...completedSteps, currentStep]); - setCurrentStep(currentStep + 1); - } - }; - - const handlePrevious = () => { - if (currentStep > 1) { - setCurrentStep(currentStep - 1); - } - }; - - const handleStepClick = (stepId: number) => { - if (stepId <= currentStep || completedSteps.includes(stepId)) { - setCurrentStep(stepId); - } - }; - - const renderStepContent = () => { - switch (currentStep) { - case 1: - return ; - case 2: - return ; - case 3: - return ; - case 4: - return ; - case 5: - return ; - default: - return null; - } - }; - - return ( -
- {/* Step Content */} -
- {renderStepContent()} -
-
- ); -} \ 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 21f0e00..0000000 --- a/components/pages/history-page.tsx +++ /dev/null @@ -1,331 +0,0 @@ -"use client"; - -import { useState } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Badge } from '@/components/ui/badge'; -import { Progress } from '@/components/ui/progress'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { - Search, - Filter, - MoreHorizontal, - Play, - Download, - Trash2, - RefreshCw, - Clock, - CheckCircle, - XCircle, - Eye, -} from 'lucide-react'; - -const mockTasks = [ - { - id: 1, - title: 'Tech Product Demo', - status: 'completed', - progress: 100, - duration: '2:45', - createdAt: '2024-01-15T10:30:00Z', - completedAt: '2024-01-15T11:15:00Z', - chapters: 4, - actors: ['Sarah Chen', 'Dr. Marcus Webb'], - thumbnail: 'https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=300', - }, - { - id: 2, - title: 'Marketing Campaign Video', - status: 'processing', - progress: 65, - duration: '1:30', - createdAt: '2024-01-14T15:20:00Z', - completedAt: null, - chapters: 3, - actors: ['Alex Rivera'], - thumbnail: 'https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=300', - }, - { - id: 3, - title: 'Educational Content Series', - status: 'completed', - progress: 100, - duration: '5:20', - createdAt: '2024-01-12T09:00:00Z', - completedAt: '2024-01-12T10:30:00Z', - chapters: 6, - actors: ['Dr. Marcus Webb', 'Sarah Chen'], - thumbnail: 'https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=300', - }, - { - id: 4, - title: 'Company Introduction', - status: 'failed', - progress: 0, - duration: '0:00', - createdAt: '2024-01-10T14:45:00Z', - completedAt: null, - chapters: 2, - actors: ['Sarah Chen'], - thumbnail: null, - }, - { - id: 5, - title: 'Quarterly Report Presentation', - status: 'processing', - progress: 25, - duration: '3:15', - createdAt: '2024-01-09T11:00:00Z', - completedAt: null, - chapters: 5, - actors: ['Dr. Marcus Webb', 'Alex Rivera'], - thumbnail: 'https://images.pexels.com/photos/3184291/pexels-photo-3184291.jpeg?auto=compress&cs=tinysrgb&w=300', - }, -]; - -const statusConfig = { - completed: { - icon: CheckCircle, - color: 'text-green-600', - bgColor: 'bg-green-100 dark:bg-green-900/20', - label: 'Completed', - }, - processing: { - icon: RefreshCw, - color: 'text-blue-600', - bgColor: 'bg-blue-100 dark:bg-blue-900/20', - label: 'Processing', - }, - failed: { - icon: XCircle, - color: 'text-red-600', - bgColor: 'bg-red-100 dark:bg-red-900/20', - label: 'Failed', - }, -}; - -export function HistoryPage() { - const [searchQuery, setSearchQuery] = useState(''); - const [statusFilter, setStatusFilter] = useState('all'); - - const filteredTasks = mockTasks.filter(task => { - const matchesSearch = task.title.toLowerCase().includes(searchQuery.toLowerCase()) || - task.actors.some(actor => actor.toLowerCase().includes(searchQuery.toLowerCase())); - const matchesStatus = statusFilter === 'all' || task.status === statusFilter; - return matchesSearch && matchesStatus; - }); - - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleString(); - }; - - const getTimeSince = (dateString: string) => { - const now = new Date(); - const date = new Date(dateString); - const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60)); - - if (diffInHours < 1) return 'Less than an hour ago'; - if (diffInHours < 24) return `${diffInHours} hours ago`; - const diffInDays = Math.floor(diffInHours / 24); - return `${diffInDays} days ago`; - }; - - return ( -
- {/* Header */} -
-
-

Task History

-

- Track the progress and manage your video generation tasks -

-
-
- - {/* Filters */} - - -
-
-
- - setSearchQuery(e.target.value)} - className="pl-10 w-64" - /> -
- - - - - - setStatusFilter('all')}> - All Status - - setStatusFilter('completed')}> - Completed - - setStatusFilter('processing')}> - Processing - - setStatusFilter('failed')}> - Failed - - - -
- - {filteredTasks.length} tasks - -
-
-
- - {/* Tasks List */} -
- {filteredTasks.map((task) => { - const StatusIcon = statusConfig[task.status as keyof typeof statusConfig].icon; - const statusProps = statusConfig[task.status as keyof typeof statusConfig]; - - return ( - - -
- {/* Thumbnail */} -
- {task.thumbnail ? ( - {task.title} - ) : ( -
- 🎬 -
- )} - {task.status === 'processing' && ( -
- -
- )} -
- - {/* Task Info */} -
-
-

{task.title}

- - - - - - {task.status === 'completed' && ( - <> - - - Preview - - - - Download - - - )} - {task.status === 'failed' && ( - - - Retry - - )} - - - View Details - - - - Delete - - - -
- -
- - - {statusProps.label} - -
- - {task.duration} -
- - {task.chapters} chapters - - - {task.actors.join(', ')} - -
- - {task.status === 'processing' && ( -
-
- Processing... - {task.progress}% -
- -
- )} - -
- Started {getTimeSince(task.createdAt)} - {task.completedAt && ( - Completed {formatDate(task.completedAt)} - )} -
-
-
-
-
- ); - })} -
- - {filteredTasks.length === 0 && ( - - -
-
- -
-
-

No tasks found

-

- {searchQuery || statusFilter !== 'all' - ? 'No tasks match your current filters. Try adjusting your search.' - : 'You haven\'t created any videos yet. Start by creating your first AI video!' - } -

-
-
-
-
- )} -
- ); -} \ No newline at end of file diff --git a/components/pages/home-page2.tsx b/components/pages/home-page2.tsx index 4980183..941cd33 100644 --- a/components/pages/home-page2.tsx +++ b/components/pages/home-page2.tsx @@ -6,16 +6,11 @@ import "./style/home-page2.css"; import { useRouter } from "next/navigation"; import { VideoScreenLayout } from '@/components/video-screen-layout'; import { VideoGridLayout } from '@/components/video-grid-layout'; -import LiquidGlass from '@/plugins/liquid-glass'; import { motion } from "framer-motion"; import { createScriptProject, CreateScriptProjectRequest } from '@/api/script_project'; -import { - createScriptEpisode, - CreateScriptEpisodeRequest -} from '@/api/script_episode'; import { ProjectTypeEnum, ModeEnum, @@ -54,6 +49,7 @@ export function HomePage2() { // 构建项目数据并调用API const projectData: CreateScriptProjectRequest = { + title: "script default", // 默认剧本名称 project_type: projectType, mode: ModeEnum.MANUAL, resolution: ResolutionEnum.FULL_HD_1080P diff --git a/components/pages/script-to-video.tsx b/components/pages/script-to-video.tsx deleted file mode 100644 index 8a8f28b..0000000 --- a/components/pages/script-to-video.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export function ScriptToVideo() { - return ( -
-

Script To Video

-
- ); -} \ No newline at end of file diff --git a/components/pages/script-work-flow.tsx b/components/pages/script-work-flow.tsx deleted file mode 100644 index c08247c..0000000 --- a/components/pages/script-work-flow.tsx +++ /dev/null @@ -1,197 +0,0 @@ -"use client"; - -import { useState, useEffect } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { ArrowLeft } from 'lucide-react'; -import { useRouter } from 'next/navigation'; -import { FilmstripStepper } from '@/components/filmstrip-stepper'; -import { AISuggestionBar } from '@/components/ai-suggestion-bar'; -import ScriptOverview from '@/components/pages/script-overview'; -import StoryboardView from '@/components/pages/storyboard-view'; - -// 定义工作流程阶段 -const WORKFLOW_STAGES = [ - { - id: 'overview', - title: 'Script Overview', - subtitle: 'Script Overview', - description: 'Extract script structure and key elements' - }, - { - id: 'storyboard', - title: 'Storyboard', - subtitle: 'Storyboard', - description: 'Visualize scene design and transitions' - }, - { - id: 'character', - title: 'Character Design', - subtitle: 'Character Design', - description: 'Customize character appearance and personality' - }, - { - id: 'post', - title: 'Post Production', - subtitle: 'Post Production', - description: 'Sound effects, music and special effects' - }, - { - id: 'output', - title: 'Final Output', - subtitle: 'Final Output', - description: 'Preview and export works' - } -]; - -export default function ScriptWorkFlow() { - const router = useRouter(); - const [currentStep, setCurrentStep] = useState('overview'); - const [loading, setLoading] = useState(true); - - // 根据当前步骤获取智能预设词条 - const getSmartSuggestions = (stepId: string): string[] => { - const suggestions = { - overview: [ - "Analyze core themes and emotions", - "Extract character relationship map", - "Generate scene and plot outline", - "Identify key turning points", - "Optimize story structure and pacing" - ], - storyboard: [ - "Design opening shot sequence", - "Plan transitions and visual effects", - "Generate key scene storyboards", - "Optimize shot language and rhythm", - "Add camera movement notes" - ], - character: [ - "Design protagonist appearance", - "Generate supporting character references", - "Create character relationship map", - "Add costume and prop designs", - "Optimize character actions" - ], - post: [ - "Plan sound and music style", - "Design visual effects solution", - "Add subtitles and graphics", - "Optimize color and lighting", - "Plan post-production workflow" - ], - output: [ - "Generate preview version", - "Optimize output parameters", - "Add opening and ending design", - "Export different formats", - "Create release plan" - ] - }; - return suggestions[stepId as keyof typeof suggestions] || []; - }; - - useEffect(() => { - // 模拟加载效果 - const timer = setTimeout(() => { - setLoading(false); - }, 1500); - return () => clearTimeout(timer); - }, []); - - // 处理步骤切换 - const handleStepChange = async (stepId: string) => { - setLoading(true); - setCurrentStep(stepId); - // 模拟加载效果 - await new Promise(resolve => setTimeout(resolve, 800)); - setLoading(false); - }; - - // 处理 AI 建议点击 - const handleSuggestionClick = (suggestion: string) => { - console.log('选择了建议:', suggestion); - // TODO: 处理建议点击逻辑 - }; - - // 处理输入提交 - const handleSubmit = (text: string) => { - console.log('提交了文本:', text); - // TODO: 处理文本提交逻辑 - }; - - return ( -
- {/* Navigation Tabs */} -
- {/* Glass Effect Background */} -
- - {/* Bottom Border */} -
- - {/* Navigation Content */} -
-
-
- {WORKFLOW_STAGES.map(stage => ( - handleStepChange(stage.id)} - className={` - flex-shrink-0 px-6 py-2 rounded-lg transition-all duration-300 - ${currentStep === stage.id - ? 'bg-white/10 text-white shadow-[0_0_15px_rgba(255,255,255,0.15)]' - : 'hover:bg-white/5 text-white/60 hover:text-white/80' - } - `} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > - {stage.title} - - ))} -
-
-
-
- - {/* Main Content Area */} -
- - - {loading ? ( -
- {[1, 2, 3].map(i => ( -
- ))} -
- ) : ( - <> - {currentStep === 'overview' && } - {currentStep === 'storyboard' && } - - )} - - -
- - {/* AI Suggestion Bar */} - stage.id === currentStep)?.title} stage?`} - /> -
- ); -} \ 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 1769356..0000000 --- a/components/pages/video-to-video.tsx +++ /dev/null @@ -1,783 +0,0 @@ -"use client"; - -import { useState, useEffect, useRef } from 'react'; -import { Card } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { ArrowLeft, ChevronDown, ChevronUp, Video, ListOrdered, Play, Loader2 } from 'lucide-react'; -import { useRouter } from 'next/navigation'; -import './style/video-to-video.css'; - -// 添加自定义滚动条样式 -const scrollbarStyles = ` - .custom-scrollbar::-webkit-scrollbar { - width: 4px; - } - .custom-scrollbar::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.05); - border-radius: 2px; - } - .custom-scrollbar::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); - border-radius: 2px; - } - .custom-scrollbar::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.2); - } -`; - -interface SceneVideo { - id: number; - video_url: string; - script: any; -} - -export function VideoToVideo() { - const router = useRouter(); - const [isUploading, setIsUploading] = useState(false); - const [isExpanded, setIsExpanded] = useState(false); - const [videoUrl, setVideoUrl] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [loadingText, setLoadingText] = useState('Generating...'); - const [generateObj, setGenerateObj] = useState({ - scripts: null, - frame_urls: null, - video_info: null, - scene_videos: null, - cut_video_url: null, - audio_video_url: null, - final_video_url: null - }); - const [showAllFrames, setShowAllFrames] = useState(false); - const containerRef = useRef(null); - const [showScrollNav, setShowScrollNav] = useState(false); - const [selectedVideoIndex, setSelectedVideoIndex] = useState(null); - const videosContainerRef = useRef(null); - const scriptsContainerRef = useRef(null); - - // 监听内容变化,自动滚动到底部 - useEffect(() => { - if (containerRef.current && (generateObj.scripts || generateObj.frame_urls || generateObj.video_info)) { - setTimeout(() => { - containerRef.current?.scrollTo({ - top: containerRef.current.scrollHeight, - behavior: 'smooth' - }); - }, 100); // 给一个小延迟,确保内容已经渲染 - } - }, [generateObj.scripts, generateObj.frame_urls, generateObj.video_info, generateObj.scene_videos, generateObj.cut_video_url, generateObj.audio_video_url, generateObj.final_video_url]); - - // 计算每行可以显示的图片数量(基于图片高度100px和容器宽度) - const imagesPerRow = Math.floor(1080 / (100 * 16/9 + 8)); // 假设图片宽高比16:9,间距8px - // 计算三行可以显示的最大图片数量 - const maxVisibleImages = imagesPerRow * 3; - - const handleUploadVideo = () => { - console.log('upload video'); - // 打开文件选择器 - const input = document.createElement('input'); - input.type = 'file'; - input.accept = 'video/*'; - input.onchange = (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (file) { - setVideoUrl(URL.createObjectURL(file)); - } - } - input.click(); - } - - const generateSences = async () => { - try { - generateObj.scene_videos = []; - const videoUrls = [ - 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4', - 'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4', - 'https://cdn.qikongjian.com/videos/1750384661_d8e30b79-828e-48cd-9025-ab62a996717c_text_to_video_0.mp4', - 'https://cdn.qikongjian.com/videos/1750320040_4b47996e-7c70-490e-8433-80c7df990fdd_text_to_video_0.mp4', - 'https://cdn.qikongjian.com/videos/1750303377_8c3c4ca6-c4ea-4376-8583-de3afa5681d8_text_to_video_0.mp4', - 'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4' - ]; - setGenerateObj({...generateObj, scene_videos: []}); - - // 使用 Promise.all 和 Array.map 来处理异步操作 - const promises = generateObj.scripts.map((element: any, index: number) => { - return new Promise((resolveVideo) => { - setTimeout(() => { - generateObj.scene_videos.push({ - id: index, - video_url: videoUrls[index], - script: element - }); - setGenerateObj({...generateObj}); - setLoadingText(`生成第 ${index + 1} 个分镜视频...`); - resolveVideo(); - }, index * 3000); // 每个视频间隔3秒 - }); - }); - - // 等待所有视频生成完成 - await Promise.all(promises); - } catch (error) { - console.error('生成分镜视频失败:', error); - throw error; - } - } - - const handleCreateVideo = async () => { - try { - // 清空所有数据 - setGenerateObj({ - scripts: null, - frame_urls: null, - video_info: null, - scene_videos: null, - cut_video_url: null, - audio_video_url: null, - final_video_url: null - }); - console.log('create video'); - setIsLoading(true); - setIsExpanded(true); - - // 提取帧 - await new Promise(resolve => setTimeout(resolve, 3000)); - setLoadingText('提取帧...'); - - // 生成帧 - await new Promise(resolve => setTimeout(resolve, 3000)); - generateObj.frame_urls = [ - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000000.jpg/1750507510_tmphfb431oc_000000.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000001.jpg/1750507511_tmphfb431oc_000001.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000002.jpg/1750507511_tmphfb431oc_000002.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000003.jpg/1750507511_tmphfb431oc_000003.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000004.jpg/1750507512_tmphfb431oc_000004.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000005.jpg/1750507512_tmphfb431oc_000005.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000006.jpg/1750507513_tmphfb431oc_000006.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000007.jpg/1750507514_tmphfb431oc_000007.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000008.jpg/1750507515_tmphfb431oc_000008.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000009.jpg/1750507515_tmphfb431oc_000009.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000010.jpg/1750507516_tmphfb431oc_000010.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000011.jpg/1750507516_tmphfb431oc_000011.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000012.jpg/1750507516_tmphfb431oc_000012.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000013.jpg/1750507517_tmphfb431oc_000013.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000014.jpg/1750507517_tmphfb431oc_000014.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000015.jpg/1750507517_tmphfb431oc_000015.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000016.jpg/1750507517_tmphfb431oc_000016.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000017.jpg/1750507517_tmphfb431oc_000017.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000018.jpg/1750507518_tmphfb431oc_000018.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000019.jpg/1750507520_tmphfb431oc_000019.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000020.jpg/1750507521_tmphfb431oc_000020.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000021.jpg/1750507523_tmphfb431oc_000021.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000022.jpg/1750507523_tmphfb431oc_000022.jpg", - "https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000023.jpg/1750507524_tmphfb431oc_000023.jpg", - ]; - setGenerateObj({...generateObj}); - setLoadingText('分析视频...'); - - // 生成视频信息 - await new Promise(resolve => setTimeout(resolve, 6000)); - generateObj.video_info = { - roles: [ - { - name: '雪 (YUKI)', - core_identity: '一位接近二十岁或二十出头的年轻女性,东亚裔,拥有深色长发和刘海,五官柔和且富有表现力。', - avatar: 'https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000000.jpg/1750507510_tmphfb431oc_000000.jpg', - },{ - name: '春 (HARU)', - core_identity: '一位接近二十岁或二十出头的年轻男性,东亚裔,拥有深色、发型整洁的中长发和深思的气质。', - avatar: 'https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000000.jpg/1750507510_tmphfb431oc_000000.jpg', - } - ], - sence: '叙事在两个不同的时间段展开。现在时空设定在一个安静的乡下小镇,冬季时被大雪覆盖。记忆则设定在春夏两季,一个日本高中的校园内外,天气温暖而晴朗。', - style: '电影感,照片般逼真,带有柔和、梦幻般的质感。其美学风格让人联想到日本的浪漫剧情片。现在时空的场景使用冷色、蓝色调的调色板,而记忆序列则沐浴在温暖的黄金时刻光晕中。大量使用浅景深、微妙的镜头光晕,以及平滑、富有情感的节奏。8K分辨率。' - }; - setGenerateObj({...generateObj}); - setLoadingText('提取分镜脚本...'); - - // 生成分镜脚本 - await new Promise(resolve => setTimeout(resolve, 6000)); - generateObj.scripts = [ - { - shot: '面部特写,拉远至广角镜头。', - frame: '序列以雪(YUKI)面部的特写开场,她闭着眼睛躺在一片纯净的雪地里。柔和的雪花轻轻飘落在她的黑发和苍白的皮肤上。摄像机缓慢拉远,形成一幅令人惊叹的广角画面,揭示出在黄昏时分,她是在一片广阔、寂静、白雪覆盖的景观中的一个渺小、孤独的身影。', - atmosphere: '忧郁、宁静、寂静且寒冷。' - }, { - shot: '切至室内中景,随后是一个透过窗户的主观视角镜头。', - frame: '我们切到雪(YUKI)在一个舒适、光线温暖的卧室里醒来。她穿着一件舒适的毛衣,从床上下来,走向一扇窗户。她的呼吸在冰冷的玻璃上凝成雾气。她的主观视角镜头揭示了外面的雪景,远处有一座红色的房子,以及一封信被放入邮箱的记忆,从而触发了一段闪回。', - atmosphere: '怀旧、温暖、内省。' - }, { - shot: '跟踪镜头,随后是一系列切出镜头和特写。', - frame: '记忆开始。一个跟踪镜头跟随着一群学生,包括雪(YUKI),他们在飘落的樱花花瓣构成的华盖下走路上学。场景切到一个阳光普照的教室。雪(YUKI)穿着校服,坐在她的课桌前,害羞地瞥了一眼坐在前几排的春(HARU)。他仿佛感觉到她的凝视,巧妙地转过头来。', - atmosphere: '充满青春气息、怀旧、温暖,带有一种萌芽的、未言明的浪漫感。' - }, { - shot: '静态中景,切到另一个中景,营造出共享空间的感觉。', - frame: '记忆转移到学校图书馆,充满了金色的光束。一个中景镜头显示春(HARU)靠在一个书架上,全神贯注地读一本书。然后摄像机切到雪(YUKI),她坐在附近的一张桌子旁,专注于画架上的一幅小画,当她感觉到他的存在时,嘴唇上泛起一丝淡淡的、秘密的微笑。', - atmosphere: '平和、亲密、书卷气、温暖。' - }, { - shot: '雪(YUKI)的中景,过渡到通过相机镜头的特写主观视角。', - frame: '在一个阳光明媚的运动日,雪(YUKI)站在运动场边缘,拿着一台老式相机。她举起相机,镜头推进到她透过取景器看的眼睛的特写。我们切到她的主观视角:除了站在运动场上、表情专注而坚定的春(HARU)之外,整个世界都是失焦的。', - atmosphere: '充满活力、专注,一种投入的观察和遥远的钦佩感。' - }, - ]; - setGenerateObj({...generateObj}); // 使用展开运算符创建新对象,确保触发更新 - setLoadingText('生成分镜视频...'); - - // 生成分镜视频 - await new Promise(resolve => setTimeout(resolve, 2000)); - await generateSences(); - setLoadingText('分镜剪辑...'); - - // 生成剪辑后的视频 - await new Promise(resolve => setTimeout(resolve, 6000)); - generateObj.cut_video_url = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4'; - setGenerateObj({...generateObj}); - setLoadingText('口型同步...'); - - // 口型同步后生成视频 - await new Promise(resolve => setTimeout(resolve, 6000)); - generateObj.audio_video_url = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4'; - setGenerateObj({...generateObj}); - setLoadingText('一致化处理...'); - - // 最终完成 - await new Promise(resolve => setTimeout(resolve, 6000)); - generateObj.final_video_url = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4'; - setGenerateObj({...generateObj}); - setLoadingText('完成'); - setIsLoading(false); - } catch (error) { - console.error('视频生成过程出错:', error); - setLoadingText('生成失败,请重试'); - setIsLoading(false); - } - } - - // 处理视频选中 - const handleVideoSelect = (index: number) => { - setSelectedVideoIndex(index); - // 滚动脚本到对应位置 - const scriptElement = document.getElementById(`script-${index}`); - scriptElement?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); - }; - - // 处理脚本选中 - const handleScriptSelect = (index: number) => { - setSelectedVideoIndex(index); - // 滚动视频到对应位置 - const videoElement = document.getElementById(`video-${index}`); - videoElement?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); - }; - - return ( -
- {/* 展示创作详细过程 */} - {generateObj && ( -
- {generateObj.frame_urls && ( -
-
- 提取帧 -
-
-
- {generateObj.frame_urls.map((frame: string, index: number) => ( -
- {`frame -
- Frame {index + 1} -
-
- ))} -
- - {generateObj.frame_urls.length > maxVisibleImages && ( -
setShowAllFrames(!showAllFrames)} - > -
- {showAllFrames ? ( - <> - - 收起 - - ) : ( - <> - - 展开全部 ({generateObj.frame_urls.length} 帧) - - )} -
-
- )} -
-
- )} - - {/* 视频信息 */} - {generateObj.video_info && ( -
-
- 视频信息 -
- {/* 展示:角色档案卡(头像、姓名、核心身份);场景;风格 */} -
- {/* 角色档案卡 */} -
- 角色档案: -
- {generateObj.video_info.roles.map((role: any, index: number) => ( -
-
-
- {role.name} -
-
-
-
{role.name}
-
{role.core_identity}
-
-
- ))} -
-
- - {/* 场景和风格 */} -
-
- 场景 -

{generateObj.video_info.sence}

-
-
- 风格 -

{generateObj.video_info.style}

-
-
-
-
- )} - - {/* 分镜脚本 */} - {generateObj.scripts && ( -
-
- 分镜脚本 -
-
-
- {generateObj.scripts.map((script: any, index: number) => ( -
- {/* 序号 */} -
- Scene {index + 1} -
- #{String(index + 1).padStart(2, '0')} -
-
- - {/* 滚动内容区域 */} -
-
- 镜头 -

{script.shot}

-
- -
- 场景 -

{script.frame}

-
- -
- 氛围 -

{script.atmosphere}

-
-
-
- ))} -
-
-
- )} - - {/* 分镜视频 */} - {generateObj.scene_videos && ( -
-
- 分镜视频 -
-
- {/* 视频展示区 */} -
- {generateObj.scripts.map((script: any, index: number) => { - const video = generateObj.scene_videos.find((v: any) => v.id === index); - const isSelected = selectedVideoIndex === index; - - return ( -
handleVideoSelect(index)} - > - {video ? ( - <> -
- ); - })} -
- - {/* 脚本展示区 */} -
- {generateObj.scripts.map((script: any, index: number) => { - const isSelected = selectedVideoIndex === index; - - return ( -
handleScriptSelect(index)} - > -
Scene {index + 1}
-
{script.frame}
-
- ); - })} -
-
-
- )} - - {/* 剪辑后的视频 */} - {generateObj.cut_video_url && ( -
-
- 剪辑后的视频 -
-
-
-
-
-
-
-
- )} - - {/* 口型同步后的视频 */} - {generateObj.audio_video_url && ( -
-
- 口型同步后的视频 -
-
-
-
-
-
-
-
- )} - - {/* 最终视频 */} - {generateObj.final_video_url && ( -
-
- 最终视频 -
-
-
-
-
-
-
-
- )} - - -
- )} - - {/* 回滚条 */} -
setShowScrollNav(true)} - onMouseLeave={() => setShowScrollNav(false)} - > - {/* 悬浮按钮 */} - - - {/* 展开的回滚导航 */} -
-
- {generateObj && ( - <> - {/* 进度条背景 */} -
- - {/* 动态进度条 */} -
- - {/* 步骤按钮 */} -
- {generateObj.frame_urls && ( - - )} - - {generateObj.video_info && ( - - )} - - {generateObj.scripts && ( - - )} - - {generateObj.scene_videos && ( - - )} - - {generateObj.cut_video_url && ( - - )} - - {generateObj.audio_video_url && ( - - )} - - {generateObj.final_video_url && ( - - )} -
- - )} -
-
-
- - {/* 工具栏 */} -
-
- {isExpanded ? ( -
setIsExpanded(false)}> - {/* 图标 展开按钮 */} - - Click to create -
- ) : ( -
setIsExpanded(true)}> - {/* 图标 折叠按钮 */} - -
- )} - -
-
-
-
-
- {/* 图标 添加视频 */} -
-
- Add Video -
-
- {videoUrl && ( -
-
-
-
- )} -
-
- -
-
-
Stop
-
Create
-
-
-
-
-
- - {/* Loading动画 */} - {isLoading && ( -
-
- {/* 外圈动画 */} -
- {/* 中圈动画 */} -
- {/* 内圈动画 */} -
-
-
-
- {/* Loading文字 */} -
- {loadingText} - - . - . - . - -
-
-
- )} -
- ); -} \ No newline at end of file diff --git a/components/pages/work-flow.tsx b/components/pages/work-flow.tsx index d17e7dd..b544833 100644 --- a/components/pages/work-flow.tsx +++ b/components/pages/work-flow.tsx @@ -201,8 +201,6 @@ export default function WorkFlow() { taskProgress: 0, mode: 'auto', // 全自动模式、人工干预模式 resolution: '1080p', // 1080p、2160p - taskCreatedAt: new Date().toISOString(), - taskUpdatedAt: new Date().toISOString(), }; return data; } diff --git a/components/ui/audio-visualizer.tsx b/components/ui/audio-visualizer.tsx new file mode 100644 index 0000000..3d5dd26 --- /dev/null +++ b/components/ui/audio-visualizer.tsx @@ -0,0 +1,365 @@ +'use client'; + +import React, { useRef, useEffect, useState } from 'react'; +import { motion } from 'framer-motion'; +import { Play, Pause, Volume2, VolumeX, AlertCircle } from 'lucide-react'; +import { cn } from '@/public/lib/utils'; +import WaveSurfer from 'wavesurfer.js'; + +interface AudioVisualizerProps { + audioUrl?: string; + title?: string; + volume?: number; + isActive?: boolean; + className?: string; + onVolumeChange?: (volume: number) => void; +} + +// 模拟波形数据生成器 +const generateMockWaveform = (width = 300, height = 50) => { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d')!; + + // 绘制模拟波形 + ctx.fillStyle = '#6b7280'; + const barWidth = 2; + const gap = 1; + const numBars = Math.floor(width / (barWidth + gap)); + + for (let i = 0; i < numBars; i++) { + const x = i * (barWidth + gap); + const barHeight = Math.random() * height * 0.8 + height * 0.1; + const y = (height - barHeight) / 2; + ctx.fillRect(x, y, barWidth, barHeight); + } + + return canvas.toDataURL(); +}; + +export function AudioVisualizer({ + audioUrl = '/audio/demo.mp3', + title = 'Background Music', + volume = 75, + isActive = false, + className, + onVolumeChange +}: AudioVisualizerProps) { + const waveformRef = useRef(null); + const wavesurfer = useRef(null); + const [isPlaying, setIsPlaying] = useState(false); + const [isMuted, setIsMuted] = useState(false); + const [duration, setDuration] = useState(120); // 默认2分钟 + const [currentTime, setCurrentTime] = useState(0); + const [isLoading, setIsLoading] = useState(true); + const [hasError, setHasError] = useState(false); + const [mockWaveformUrl, setMockWaveformUrl] = useState(''); + + // 生成模拟波形 + useEffect(() => { + if (waveformRef.current) { + const width = waveformRef.current.clientWidth || 300; + const mockUrl = generateMockWaveform(width, 50); + setMockWaveformUrl(mockUrl); + } + }, [isActive]); + + // 初始化 Wavesurfer + useEffect(() => { + if (!waveformRef.current) return; + + // 创建 wavesurfer 实例 + wavesurfer.current = WaveSurfer.create({ + container: waveformRef.current, + waveColor: isActive ? '#3b82f6' : '#6b7280', + progressColor: isActive ? '#1d4ed8' : '#374151', + cursorColor: '#ffffff', + barWidth: 2, + barRadius: 3, + responsive: true, + height: 50, + normalize: true, + backend: 'WebAudio', + mediaControls: false, + }); + + // 尝试加载音频,如果失败则使用模拟数据 + setIsLoading(true); + setHasError(false); + + wavesurfer.current.load(audioUrl).catch(() => { + console.warn('音频文件加载失败,使用模拟数据'); + setHasError(true); + setIsLoading(false); + + // 如果加载失败,创建空的音频上下文用于演示 + if (wavesurfer.current) { + wavesurfer.current.empty(); + // 创建模拟的峰值数据 + const peaks = Array.from({ length: 1000 }, () => Math.random() * 2 - 1); + wavesurfer.current.loadDecodedBuffer({ + getChannelData: () => new Float32Array(peaks), + length: peaks.length, + sampleRate: 44100, + numberOfChannels: 1, + duration: 120 + } as any); + } + }); + + // 事件监听 + wavesurfer.current.on('ready', () => { + setDuration(wavesurfer.current?.getDuration() || 120); + setIsLoading(false); + if (wavesurfer.current) { + wavesurfer.current.setVolume(volume / 100); + } + }); + + wavesurfer.current.on('audioprocess', () => { + setCurrentTime(wavesurfer.current?.getCurrentTime() || 0); + }); + + wavesurfer.current.on('seek', () => { + setCurrentTime(wavesurfer.current?.getCurrentTime() || 0); + }); + + wavesurfer.current.on('play', () => { + setIsPlaying(true); + }); + + wavesurfer.current.on('pause', () => { + setIsPlaying(false); + }); + + wavesurfer.current.on('finish', () => { + setIsPlaying(false); + setCurrentTime(0); + }); + + wavesurfer.current.on('error', (error) => { + console.warn('Wavesurfer error:', error); + setHasError(true); + setIsLoading(false); + }); + + return () => { + if (wavesurfer.current) { + wavesurfer.current.destroy(); + } + }; + }, [audioUrl]); + + // 更新波形颜色当 isActive 改变时 + useEffect(() => { + if (wavesurfer.current && !hasError) { + wavesurfer.current.setOptions({ + waveColor: isActive ? '#3b82f6' : '#6b7280', + progressColor: isActive ? '#1d4ed8' : '#374151', + }); + } + }, [isActive, hasError]); + + // 更新音量 + useEffect(() => { + if (wavesurfer.current && !hasError) { + wavesurfer.current.setVolume(isMuted ? 0 : volume / 100); + } + }, [volume, isMuted, hasError]); + + // 模拟播放进度(当使用模拟数据时) + useEffect(() => { + let interval: NodeJS.Timeout; + if (isPlaying && hasError) { + interval = setInterval(() => { + setCurrentTime(prev => { + const next = prev + 1; + if (next >= duration) { + setIsPlaying(false); + return 0; + } + return next; + }); + }, 1000); + } + return () => clearInterval(interval); + }, [isPlaying, hasError, duration]); + + const togglePlayPause = () => { + if (hasError) { + // 模拟播放/暂停 + setIsPlaying(!isPlaying); + } else if (wavesurfer.current) { + wavesurfer.current.playPause(); + } + }; + + const toggleMute = () => { + setIsMuted(!isMuted); + }; + + const formatTime = (time: number) => { + const minutes = Math.floor(time / 60); + const seconds = Math.floor(time % 60); + return `${minutes}:${seconds.toString().padStart(2, '0')}`; + }; + + const progressPercentage = duration > 0 ? (currentTime / duration) * 100 : 0; + + return ( + +
+ {/* 标题和音量 */} +
+
+
+ {hasError ? ( + + ) : ( + + )} +
+
+
+ {title} + {hasError && (Demo)} +
+
Audio track
+
+
+
{volume}%
+
+ + {/* 波形可视化 */} +
+ {hasError ? ( + // 显示模拟波形图片 +
+ {mockWaveformUrl && ( + Audio waveform + )} + {/* 进度条覆盖层 */} +
+ {/* 播放游标 */} +
+
+ ) : ( +
+ )} + + {isLoading && !hasError && ( +
+
+
+ )} +
+ + {/* 控制栏 */} +
+ {/* 播放/暂停按钮 */} + + {isPlaying ? ( + + ) : ( + + )} + + + {/* 静音按钮 */} + + {isMuted ? ( + + ) : ( + + )} + + + {/* 时间显示 */} +
+ + {formatTime(currentTime)} / {formatTime(duration)} + +
+ + {/* 音量控制 */} +
+ + { + const newVolume = parseInt(e.target.value); + onVolumeChange?.(newVolume); + }} + className="w-16 h-1 bg-white/20 rounded-lg appearance-none cursor-pointer" + style={{ + background: `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${volume}%, rgba(255,255,255,0.2) ${volume}%, rgba(255,255,255,0.2) 100%)` + }} + /> + {volume}% +
+
+ + {/* 播放状态指示器 */} + {isPlaying && ( + + )} + + {/* 错误提示 */} + {hasError && ( +
+ 演示模式 - 使用模拟音频数据 +
+ )} +
+ + ); +} \ No newline at end of file diff --git a/components/ui/media-properties-modal.tsx b/components/ui/media-properties-modal.tsx new file mode 100644 index 0000000..ebe5943 --- /dev/null +++ b/components/ui/media-properties-modal.tsx @@ -0,0 +1,514 @@ +'use client'; + +import React, { useState, useRef, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { X, Play, Pause, Volume2, VolumeX, Upload, Library, Wand2, ZoomIn, RotateCw, Info, ChevronDown } from 'lucide-react'; +import { cn } from '@/public/lib/utils'; +import { AudioVisualizer } from './audio-visualizer'; + +interface MediaPropertiesModalProps { + isOpen: boolean; + onClose: () => void; + taskSketch: any[]; + currentSketchIndex: number; + onSketchSelect: (index: number) => void; +} + +export function MediaPropertiesModal({ + isOpen, + onClose, + taskSketch = [], + currentSketchIndex = 0, + onSketchSelect +}: MediaPropertiesModalProps) { + const [activeTab, setActiveTab] = useState<'media' | 'audio'>('media'); + const [selectedSketchIndex, setSelectedSketchIndex] = useState(currentSketchIndex); + const [isPlaying, setIsPlaying] = useState(false); + const [isMuted, setIsMuted] = useState(false); + const [progress, setProgress] = useState(0); + const [trimAutomatically, setTrimAutomatically] = useState(false); + const [audioVolume, setAudioVolume] = useState(75); + const videoPlayerRef = useRef(null); + const thumbnailsRef = useRef(null); + + // 当弹窗打开时,同步当前选中的分镜 + useEffect(() => { + if (isOpen) { + setSelectedSketchIndex(currentSketchIndex); + } + }, [isOpen, currentSketchIndex]); + + // 确保 taskSketch 是数组 + const sketches = Array.isArray(taskSketch) ? taskSketch : []; + + // 模拟媒体属性数据 + const currentSketch = sketches[selectedSketchIndex]; + const mediaProperties = { + duration: '00m : 10s : 500ms / 00m : 17s : 320ms', + trim: { start: '0.0s', end: '10.5s' }, + centerPoint: { x: 0.5, y: 0.5 }, + zoom: 100, + rotation: 0, + transition: 'Auto', + script: 'This part of the script is 21.00 seconds long.' + }; + + const audioProperties = { + sfxName: 'Background Music', + sfxVolume: audioVolume + }; + + // 自动滚动到选中项 + useEffect(() => { + if (thumbnailsRef.current && isOpen) { + const thumbnailContainer = thumbnailsRef.current; + const thumbnailWidth = thumbnailContainer.children[0]?.clientWidth ?? 0; + const thumbnailGap = 16; + const thumbnailScrollPosition = (thumbnailWidth + thumbnailGap) * selectedSketchIndex; + + thumbnailContainer.scrollTo({ + left: thumbnailScrollPosition - thumbnailContainer.clientWidth / 2 + thumbnailWidth / 2, + behavior: 'smooth' + }); + } + }, [selectedSketchIndex, isOpen]); + + // 视频播放控制 + useEffect(() => { + if (videoPlayerRef.current) { + if (isPlaying) { + videoPlayerRef.current.play().catch(() => { + setIsPlaying(false); + }); + } else { + videoPlayerRef.current.pause(); + } + } + }, [isPlaying, selectedSketchIndex]); + + // 更新进度条 + const handleTimeUpdate = () => { + if (videoPlayerRef.current) { + const progress = (videoPlayerRef.current.currentTime / videoPlayerRef.current.duration) * 100; + setProgress(progress); + } + }; + + const handleSketchSelect = (index: number) => { + setSelectedSketchIndex(index); + setProgress(0); + }; + + if (sketches.length === 0) { + return null; + } + + return ( + + {isOpen && ( + <> + {/* 背景遮罩 */} + + + {/* 弹窗内容 */} +
+ + {/* 标题栏 */} +
+
+ +

Media Properties

+
+
+ +
+ {/* 上部:分镜视频列表 */} +
+
+ {sketches.map((sketch, index) => ( + handleSketchSelect(index)} + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + + ))} +
+
+ + {/* 下部:主要内容区域 */} +
+ {/* 左侧 2/3:编辑选项 */} +
+ {/* Media/Audio & SFX 切换按钮 */} +
+ setActiveTab('media')} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + Media + + setActiveTab('audio')} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + Audio & SFX + +
+ + {/* 内容区域 */} +
+ {activeTab === 'media' ? ( + <> + {/* Duration - 只读 */} +
+ + {mediaProperties.duration} +
+ + {/* Trim - 可编辑 */} +
+ +
+ from + + to + +
+
+ + {/* Center point - 可编辑 */} +
+ +
+ + X + + Y +
+
+ + {/* Zoom & Rotation - 可编辑 */} +
+ +
+
+ + + % +
+
+ + + ° +
+
+
+ + {/* Transition - 可编辑 */} +
+
+ + +
+ +
+ + {/* Script - 只读 */} +
+ +

+ {mediaProperties.script} +

+
+ + ) : ( + <> + {/* SFX name - 只读 */} +
+ + {audioProperties.sfxName} +
+ + {/* SFX volume - 可编辑 */} +
+
+ + {audioProperties.sfxVolume}% +
+ setAudioVolume(parseInt(e.target.value))} + className="w-full h-2 bg-white/20 rounded-lg appearance-none cursor-pointer slider" + style={{ + background: `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${audioProperties.sfxVolume}%, rgba(255,255,255,0.2) ${audioProperties.sfxVolume}%, rgba(255,255,255,0.2) 100%)` + }} + /> +
+ + {/* Replace audio */} +
+ +
+ + + Replace audio + + + + Stock SFX + + + + Generate SFX + +
+
+ + )} +
+
+ + {/* 右侧 1/3:预览区域 */} +
+
+ {/* 视频预览 */} +
+
+ + {/* 音频预览 */} + +
+
+
+
+ + {/* 底部操作栏 */} +
+
+ { + // TODO: 实现重置逻辑 + console.log('Reset clicked'); + }} + > + Reset + + { + // TODO: 实现应用逻辑 + console.log('Apply clicked'); + onSketchSelect(selectedSketchIndex); + onClose(); + }} + > + Apply + +
+
+
+
+ + )} +
+ ); +} \ No newline at end of file diff --git a/components/ui/video-tab-content.tsx b/components/ui/video-tab-content.tsx index b709957..50dc87f 100644 --- a/components/ui/video-tab-content.tsx +++ b/components/ui/video-tab-content.tsx @@ -6,6 +6,7 @@ import { Trash2, RefreshCw, Play, Pause, Volume2, VolumeX, Upload, Library, Wand import { GlassIconButton } from './glass-icon-button'; import { cn } from '@/public/lib/utils'; import { ReplaceVideoModal } from './replace-video-modal'; +import { MediaPropertiesModal } from './media-properties-modal'; interface VideoTabContentProps { taskSketch: any[]; @@ -28,6 +29,7 @@ export function VideoTabContent({ const [progress, setProgress] = React.useState(0); const [isReplaceModalOpen, setIsReplaceModalOpen] = React.useState(false); const [activeReplaceMethod, setActiveReplaceMethod] = React.useState<'upload' | 'library' | 'generate'>('upload'); + const [isMediaPropertiesModalOpen, setIsMediaPropertiesModalOpen] = React.useState(false); // 监听外部播放状态变化 useEffect(() => { @@ -305,6 +307,17 @@ export function VideoTabContent({ onChange={(e) => console.log('Volume:', e.target.value)} />
+ + {/* 更多设置 点击打开 Media properties 弹窗 */} + setIsMediaPropertiesModalOpen(true)} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > +
Media properties
+
{/* 右列:视频预览和操作 */} @@ -416,6 +429,15 @@ export function VideoTabContent({ }} /> + {/* Media Properties 弹窗 */} + setIsMediaPropertiesModalOpen(false)} + taskSketch={taskSketch} + currentSketchIndex={currentSketchIndex} + onSketchSelect={onSketchSelect} + /> +
); diff --git a/next.config.js b/next.config.js index b6f0d8b..18ae2c3 100644 --- a/next.config.js +++ b/next.config.js @@ -4,6 +4,14 @@ const nextConfig = { ignoreDuringBuilds: true, }, images: { unoptimized: true }, + async rewrites() { + return [ + { + source: '/api/proxy/:path*', + destination: 'https://77.smartvideo.py.qikongjian.com/:path*', + }, + ]; + }, }; module.exports = nextConfig; diff --git a/package-lock.json b/package-lock.json index 8728540..c01b6a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "@types/react-beautiful-dnd": "^13.1.8", "@types/react-dom": "18.2.7", "@types/three": "^0.177.0", + "@types/wavesurfer.js": "^6.0.12", "antd": "^5.26.2", "autoprefixer": "10.4.15", "axios": "^1.10.0", @@ -88,6 +89,7 @@ "three": "^0.177.0", "typescript": "5.2.2", "vaul": "^0.9.9", + "wavesurfer.js": "^7.9.9", "zod": "^3.23.8" }, "devDependencies": { @@ -6526,6 +6528,12 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/debounce": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@types/debounce/-/debounce-1.2.4.tgz", + "integrity": "sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==", + "license": "MIT" + }, "node_modules/@types/gsap": { "version": "1.20.2", "resolved": "https://registry.npmmirror.com/@types/gsap/-/gsap-1.20.2.tgz", @@ -6645,6 +6653,15 @@ "meshoptimizer": "~0.18.1" } }, + "node_modules/@types/wavesurfer.js": { + "version": "6.0.12", + "resolved": "https://registry.npmmirror.com/@types/wavesurfer.js/-/wavesurfer.js-6.0.12.tgz", + "integrity": "sha512-oM9hYlPIVms4uwwoaGs9d0qp7Xk7IjSGkdwgmhUymVUIIilRfjtSQvoOgv4dpKiW0UozWRSyXfQqTobi0qWyCw==", + "license": "MIT", + "dependencies": { + "@types/debounce": "*" + } + }, "node_modules/@types/webxr": { "version": "0.5.22", "resolved": "https://registry.npmmirror.com/@types/webxr/-/webxr-0.5.22.tgz", @@ -12878,6 +12895,12 @@ "node": ">=10.13.0" } }, + "node_modules/wavesurfer.js": { + "version": "7.9.9", + "resolved": "https://registry.npmmirror.com/wavesurfer.js/-/wavesurfer.js-7.9.9.tgz", + "integrity": "sha512-8O/zu+RC7yjikxiuhsXzRZ8vvjV+Qq4PUKZBQsLLcq6fqbrSF3Vh99l7fT8zeEjKjDBNH2Qxsxq5mRJIuBmM3Q==", + "license": "BSD-3-Clause" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 4e83f67..fa31829 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@types/react-beautiful-dnd": "^13.1.8", "@types/react-dom": "18.2.7", "@types/three": "^0.177.0", + "@types/wavesurfer.js": "^6.0.12", "antd": "^5.26.2", "autoprefixer": "10.4.15", "axios": "^1.10.0", @@ -89,6 +90,7 @@ "three": "^0.177.0", "typescript": "5.2.2", "vaul": "^0.9.9", + "wavesurfer.js": "^7.9.9", "zod": "^3.23.8" }, "devDependencies": {