From 0269ec3038bc916575f266e9f6ef8c2037f6b8a6 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Tue, 1 Jul 2025 17:00:02 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E7=94=A8=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/constants.ts | 1 + api/enums.ts | 65 + {lib => api}/request.ts | 4 +- api/script_episode.ts | 47 + api/script_project.ts | 48 + app/create/page.tsx | 2 +- app/page.tsx | 2 +- components/pages/create-to-video.tsx | 1334 +++++------------ components/pages/create-to-video2.tsx | 482 ------ components/pages/home-page.tsx | 163 -- .../pages/{home-page2.tsx => homepage.tsx} | 119 +- components/pages/style/create-to-video.css | 73 +- components/pages/style/create-to-video2.css | 132 -- .../style/{home-page2.css => home-page.css} | 0 components/parallax.tsx | 8 +- components/vanta-halo-background.tsx | 56 +- components/video-grid-layout.tsx | 2 +- components/video-screen-layout.tsx | 2 +- 18 files changed, 757 insertions(+), 1783 deletions(-) create mode 100644 api/constants.ts create mode 100644 api/enums.ts rename {lib => api}/request.ts (98%) create mode 100644 api/script_episode.ts create mode 100644 api/script_project.ts delete mode 100644 components/pages/create-to-video2.tsx delete mode 100644 components/pages/home-page.tsx rename components/pages/{home-page2.tsx => homepage.tsx} (55%) delete mode 100644 components/pages/style/create-to-video2.css rename components/pages/style/{home-page2.css => home-page.css} (100%) diff --git a/api/constants.ts b/api/constants.ts new file mode 100644 index 0000000..08eac7f --- /dev/null +++ b/api/constants.ts @@ -0,0 +1 @@ +export const BASE_URL = "https://movieflow.api.huiying.video" diff --git a/api/enums.ts b/api/enums.ts new file mode 100644 index 0000000..e7a082d --- /dev/null +++ b/api/enums.ts @@ -0,0 +1,65 @@ +// 主项目(产品)类型枚举 +export enum ProjectTypeEnum { + SCRIPT_TO_VIDEO = 1, // "剧本转视频" + VIDEO_TO_VIDEO = 2, // "视频复刻视频" +} + +// 模式枚举 +export enum ModeEnum { + MANUAL = 1, // "手动" + AUTOMATIC = 2, // "自动" +} + +// 分辨率枚举 +export enum ResolutionEnum { + HD_720P = 1, // "720p" + FULL_HD_1080P = 2, // "1080p" + UHD_2K = 3, // "2k" + UHD_4K = 4, // "4k" +} + +// 项目类型映射 +export const ProjectTypeMap = { + [ProjectTypeEnum.SCRIPT_TO_VIDEO]: { + value: "script_to_video", + label: "剧本转视频", + tab: "script" + }, + [ProjectTypeEnum.VIDEO_TO_VIDEO]: { + value: "video_to_video", + label: "视频复刻视频", + tab: "clone" + } +} as const; + +// 模式映射 +export const ModeMap = { + [ModeEnum.MANUAL]: { + value: "manual", + label: "手动" + }, + [ModeEnum.AUTOMATIC]: { + value: "automatic", + label: "自动" + } +} as const; + +// 分辨率映射 +export const ResolutionMap = { + [ResolutionEnum.HD_720P]: { + value: "720p", + label: "720P" + }, + [ResolutionEnum.FULL_HD_1080P]: { + value: "1080p", + label: "1080P" + }, + [ResolutionEnum.UHD_2K]: { + value: "2k", + label: "2K" + }, + [ResolutionEnum.UHD_4K]: { + value: "4k", + label: "4K" + } +} as const; \ No newline at end of file diff --git a/lib/request.ts b/api/request.ts similarity index 98% rename from lib/request.ts rename to api/request.ts index a3edcd3..8fd0317 100644 --- a/lib/request.ts +++ b/api/request.ts @@ -1,8 +1,8 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, AxiosHeaders } from 'axios'; - +import { BASE_URL } from './constants' // 创建 axios 实例 const request: AxiosInstance = axios.create({ - baseURL: 'https://77.smartvideo.py.qikongjian.com', // 设置基础URL + baseURL: BASE_URL, // 设置基础URL timeout: 10000, // 请求超时时间 headers: { 'Content-Type': 'application/json', diff --git a/api/script_episode.ts b/api/script_episode.ts new file mode 100644 index 0000000..058f6c5 --- /dev/null +++ b/api/script_episode.ts @@ -0,0 +1,47 @@ +import { post } from './request'; + +// 创建剧集的数据类型 +export interface CreateScriptEpisodeRequest { + title?: string; + script_id?: number; + characters?: { + characters?: Array<{ + name?: string; + desc?: string; + }>; + }; + summary?: string; + atmosphere?: string; + scene?: string; + status?: number; + task_description?: string; + creator_name?: string; + cate_tags?: { + tags?: string[]; + }; + episode_sort?: number; +} + +// 创建剧集响应数据类型 +export interface ScriptEpisode { + id: number; + title: string; + script_id: number; + status: number; + episode_sort: number; + creator_name: string; + created_at: string; +} + +// 创建剧集响应格式 +export interface EpisodeApiResponse { + code: number; + msg: string; + data: T; + successfull: boolean; +} + +// 创建剧集 +export const createScriptEpisode = async (data: CreateScriptEpisodeRequest): Promise> => { + return post>('/script_episode/create', data); +}; \ No newline at end of file diff --git a/api/script_project.ts b/api/script_project.ts new file mode 100644 index 0000000..db005b6 --- /dev/null +++ b/api/script_project.ts @@ -0,0 +1,48 @@ +import { post } from './request'; + +// 创建剧本项目的数据类型 +export interface CreateScriptProjectRequest { + title?: string; + script_author?: string; + characters?: Array<{ + name?: string; + desc?: string; + }>; + summary?: string; + project_type?: number; + status?: number; + cate_tags?: string[]; + creator_name?: string; + mode?: number; + resolution?: number; +} + +// API响应类型 +export interface ApiResponse { + code: number; + message: string; + data: T; +} + +// 创建剧本项目响应数据类型 +export interface ScriptProject { + id: number; + title: string; + script_author: string; + characters: Array<{ + name: string; + desc: string; + }>; + summary: string; + project_type: number; + status: number; + cate_tags: string[]; + creator_name: string; + mode: number; + resolution: number; +} + +// 创建剧本项目 +export const createScriptProject = async (data: CreateScriptProjectRequest): Promise> => { + return post>('/script_project/create', data); +}; \ No newline at end of file diff --git a/app/create/page.tsx b/app/create/page.tsx index b392b28..148813c 100644 --- a/app/create/page.tsx +++ b/app/create/page.tsx @@ -1,5 +1,5 @@ // import { redirect } from 'next/navigation'; -import { CreateToVideo2 } from '@/components/pages/create-to-video2'; +import { CreateToVideo2 } from '@/components/pages/create-to-video'; export default function CreatePage() { // redirect('/create/video-to-video'); diff --git a/app/page.tsx b/app/page.tsx index 180ca67..6cde325 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ import { DashboardLayout } from '@/components/layout/dashboard-layout'; // import { HomePage } from '@/components/pages/home-page'; -import { HomePage2 } from '@/components/pages/home-page2'; +import { HomePage2 } from '@/components/pages/homepage'; import OAuthCallbackHandler from '@/components/ui/oauth-callback-handler'; export default function Home() { diff --git a/components/pages/create-to-video.tsx b/components/pages/create-to-video.tsx index 80275b5..050dbf4 100644 --- a/components/pages/create-to-video.tsx +++ b/components/pages/create-to-video.tsx @@ -3,7 +3,7 @@ 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, Pause, MoreHorizontal, Edit2, Check, X, RefreshCw } from 'lucide-react'; +import { ArrowLeft, ChevronDown, ChevronUp, Video, ListOrdered, Play, Loader2, Pause, MoreHorizontal, Edit2, Check, X, RefreshCw, Lightbulb, Package, Crown, ArrowUp } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, SheetClose } from "@/components/ui/sheet"; import { Input } from "@/components/ui/input"; @@ -11,6 +11,18 @@ import { Textarea } from "@/components/ui/textarea"; import './style/create-to-video.css'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { Skeleton } from "@/components/ui/skeleton"; +import LiquidGlass from '@/plugins/liquid-glass/index' +import { Dropdown } from 'antd'; +import type { MenuProps } from 'antd'; +import Image from 'next/image'; +import dynamic from 'next/dynamic'; +import { ProjectTypeEnum, ModeEnum, ResolutionEnum } from "@/api/enums"; +import { createScriptProject, CreateScriptProjectRequest } from "@/api/script_project"; +import { createScriptEpisode, CreateScriptEpisodeRequest } from "@/api/script_episode"; + +const JoyrideNoSSR = dynamic(() => import('react-joyride'), { + ssr: false, +}); // 添加自定义滚动条样式 const scrollbarStyles = ` @@ -36,71 +48,21 @@ interface SceneVideo { script: any; } -export function CreateToVideo() { +const ideaText = 'a cute capybara with an orange on its head, staring into the distance and walking forward'; + +export function CreateToVideo2() { const router = useRouter(); - const [isUploading, setIsUploading] = useState(false); + const [isClient, setIsClient] = useState(false); const [isExpanded, setIsExpanded] = useState(false); const [videoUrl, setVideoUrl] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [loadingText, setLoadingText] = useState('Generating...'); - const [isPaused, setIsPaused] = useState(false); - const [showMoreSettings, setShowMoreSettings] = useState(false); - 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); - const [activeTab, setActiveTab] = useState('clone'); - const [editingField, setEditingField] = useState<{ - type: 'shot' | 'frame' | 'atmosphere' | null; - value: string; - }>({ type: null, value: '' }); - const [alternativeVideos, setAlternativeVideos] = useState<{ [key: number]: string[] }>({ - 0: [ - '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', - ], - 1: [ - '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', - ] - }); - const [currentVideoIndex, setCurrentVideoIndex] = useState<{ [key: number]: number }>({}); - const [volume, setVolume] = useState<{ [key: number]: number }>({}); - const [transition, setTransition] = useState<{ [key: number]: string }>({}); - const [buttonPosition, setButtonPosition] = useState({ x: window.innerWidth - 100, y: window.innerHeight / 2 }); - const [isDragging, setIsDragging] = useState(false); - const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); - const buttonRef = useRef(null); - - // 计算每行可以显示的图片数量(基于图片高度100px和容器宽度) - const imagesPerRow = Math.floor(1080 / (100 * 16/9 + 8)); // 假设图片宽高比16:9,间距8px - // 计算三行可以显示的最大图片数量 - const maxVisibleImages = imagesPerRow * 3; - - // 生成分镜视频后默认选中第一个 - useEffect(() => { - if (generateObj.scene_videos?.length > 0 && selectedVideoIndex === null) { - setSelectedVideoIndex(0); - } - }, [generateObj.scene_videos]); - - // 处理暂停/继续 - const handlePauseResume = () => { - setIsPaused(!isPaused); - // TODO: 实现具体的暂停/继续逻辑 - - }; + const [activeTab, setActiveTab] = useState('script'); + const [isFocus, setIsFocus] = useState(false); + const [selectedMode, setSelectedMode] = useState(ModeEnum.AUTOMATIC); + const [selectedResolution, setSelectedResolution] = useState(ResolutionEnum.HD_720P); + const [inputText, setInputText] = useState(''); + const editorRef = useRef(null); + const [runTour, setRunTour] = useState(true); const handleUploadVideo = () => { console.log('upload video'); @@ -117,324 +79,223 @@ export function CreateToVideo() { 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', + if (videoUrl || inputText) { + try { + // 确定项目类型 + const projectType = activeTab === 'script' ? ProjectTypeEnum.SCRIPT_TO_VIDEO : ProjectTypeEnum.VIDEO_TO_VIDEO; + + // 构建项目数据并调用API + const projectData: CreateScriptProjectRequest = { + project_type: projectType, + mode: selectedMode, + resolution: selectedResolution + }; + + const projectResponse = await createScriptProject(projectData); + + if (projectResponse.code === 0 && projectResponse.data.id) { + const projectId = projectResponse.data.id; + + // 创建剧集数据 + const episodeData: CreateScriptEpisodeRequest = { + }; + + // 调用创建剧集API + const episodeResponse = await createScriptEpisode(episodeData); + + if (episodeResponse.code === 0) { + // 成功创建后跳转到work-flow页面 + router.push('/create/work-flow'); + } else { + alert(`创建剧集失败: ${episodeResponse.msg}`); } - ], - 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); + } else { + alert(`创建项目失败: ${projectResponse.message}`); + } + } catch (error) { + alert("创建项目时发生错误,请稍后重试"); + } } } - // 处理视频选中 - const handleVideoSelect = (index: number) => { - setSelectedVideoIndex(index); - // 滚动脚本到对应位置 - const scriptElement = document.getElementById(`script-${index}`); - scriptElement?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); + // 下拉菜单项配置 + const modeItems: MenuProps['items'] = [ + { + type: 'group', + label: ( +
Mode
+ ), + children: [ + { + key: ModeEnum.AUTOMATIC, + label: ( +
+
+ Auto + +
+ Automatically selects the best model for optimal efficiency +
+ ), + }, + { + key: ModeEnum.MANUAL, + label: ( +
+
+ Manual + +
+ Offers reliable, consistent performance every time +
+ ), + }, + ], + }, + ]; + + // 分辨率选项配置 + const resolutionItems: MenuProps['items'] = [ + { + type: 'group', + label: ( +
Resolution
+ ), + children: [ + { + key: ResolutionEnum.HD_720P, + label: ( +
+ 720P +
+ ), + }, + { + key: ResolutionEnum.FULL_HD_1080P, + label: ( +
+ 1080P + +
+ ), + }, + { + key: ResolutionEnum.UHD_2K, + label: ( +
+ 2K + +
+ ), + }, + { + key: ResolutionEnum.UHD_4K, + label: ( +
+ 4K + +
+ ), + }, + ], + }, + ]; + + // 处理模式选择 + const handleModeSelect: MenuProps['onClick'] = ({ key }) => { + setSelectedMode(Number(key) as ModeEnum); }; - // 处理脚本选中 - const handleScriptSelect = (index: number) => { - setSelectedVideoIndex(index); - // 滚动视频到对应位置 - const videoElement = document.getElementById(`video-${index}`); - videoElement?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); + // 处理分辨率选择 + const handleResolutionSelect: MenuProps['onClick'] = ({ key }) => { + setSelectedResolution(Number(key) as ResolutionEnum); }; - // 处理脚本编辑 - const handleEditScript = (type: 'shot' | 'frame' | 'atmosphere', value: string) => { - setEditingField({ type, value }); - }; + const handleStartCreating = () => { + setActiveTab('script'); + setInputText(ideaText); + } - // 处理脚本点击 - const handleScriptClick = (type: 'shot' | 'frame' | 'atmosphere', value: string) => { - if (editingField.type !== type) { - handleEditScript(type, value); - } - }; - - // 自动保存脚本编辑 - useEffect(() => { - if (editingField.type && editingField.value && selectedVideoIndex !== null) { - const timeoutId = setTimeout(() => { - const updatedScripts = [...generateObj.scripts]; - const fieldType = editingField.type as keyof typeof updatedScripts[number]; - updatedScripts[selectedVideoIndex] = { - ...updatedScripts[selectedVideoIndex], - [fieldType]: editingField.value - }; - setGenerateObj({ - ...generateObj, - scripts: updatedScripts - }); - }, 500); // 500ms 防抖 - - return () => clearTimeout(timeoutId); - } - }, [editingField.value, editingField.type, selectedVideoIndex, generateObj]); - - // 刷新分镜脚本 - const handleRefreshScript = () => { - if (selectedVideoIndex === null) return; - // TODO: 实现刷新脚本的逻辑 - console.log('Refresh script for scene', selectedVideoIndex + 1); - }; - - // 处理删除分镜视频 - const handleDeleteScene = (index: number) => { - if (generateObj.scene_videos) { - const newSceneVideos = generateObj.scene_videos.filter((_: SceneVideo, i: number) => i !== index); - setGenerateObj({ - ...generateObj, - scene_videos: newSceneVideos - }); - setSelectedVideoIndex(null); - } - }; - - // 处理重新生成分镜视频 - const handleRegenerateScene = async (index: number) => { - // TODO: 实现重新生成逻辑 - console.log('Regenerate scene', index); - }; - - // 处理切换其他生成的视频 - const handleSwitchVideo = (sceneIndex: number, videoIndex: number) => { - if (alternativeVideos[sceneIndex]?.[videoIndex]) { - const newSceneVideos = [...generateObj.scene_videos]; - newSceneVideos[sceneIndex] = { - ...newSceneVideos[sceneIndex], - video_url: alternativeVideos[sceneIndex][videoIndex] - }; - setGenerateObj({ - ...generateObj, - scene_videos: newSceneVideos - }); - setCurrentVideoIndex({ - ...currentVideoIndex, - [sceneIndex]: videoIndex - }); - } - }; - - // 处理音量调节 - const handleVolumeChange = (index: number, value: number) => { - setVolume({ - ...volume, - [index]: value - }); - // TODO: 实现音量调节逻辑 - }; - - // 处理转场设置 - const handleTransitionChange = (index: number, value: string) => { - setTransition({ - ...transition, - [index]: value - }); - // TODO: 实现转场效果逻辑 - }; - - // 处理拖动开始 - const handleDragStart = (e: React.MouseEvent) => { - setIsDragging(true); - setDragStart({ - x: e.clientX - buttonPosition.x, - y: e.clientY - buttonPosition.y - }); - }; - - // 处理拖动 - const handleDrag = (e: MouseEvent) => { - if (isDragging && buttonRef.current) { - const newX = Math.min( - Math.max(0, e.clientX - dragStart.x), - window.innerWidth - buttonRef.current.offsetWidth - ); - const newY = Math.min( - Math.max(0, e.clientY - dragStart.y), - window.innerHeight - buttonRef.current.offsetHeight - ); + // 处理编辑器聚焦 + const handleEditorFocus = () => { + setIsFocus(true); + if (editorRef.current && inputText) { + // 创建范围对象 + const range = document.createRange(); + const selection = window.getSelection(); - setButtonPosition({ x: newX, y: newY }); + // 获取编辑器内的文本节点 + const textNode = Array.from(editorRef.current.childNodes).find( + node => node.nodeType === Node.TEXT_NODE + ) || editorRef.current.appendChild(document.createTextNode(inputText)); + + // 设置范围到文本末尾 + range.setStart(textNode, inputText.length); + range.setEnd(textNode, inputText.length); + + // 应用选择 + selection?.removeAllRanges(); + selection?.addRange(range); } }; - // 处理拖动结束 - const handleDragEnd = () => { - setIsDragging(false); + // 处理编辑器内容变化 + const handleEditorChange = (e: React.FormEvent) => { + const newText = e.currentTarget.textContent || ''; + setInputText(newText); }; - // 添加拖动事件监听 + // 引导步骤 + const steps: Step[] = [ + { + target: '.video-storyboard-tools', + content: 'Welcome to AI Video Creation Tool! This is the main creation area.', + placement: 'top', + }, + { + target: '.storyboard-tools-tab', + content: 'Choose between Script mode to create videos from text, or Clone mode to recreate existing videos.', + placement: 'bottom', + }, + { + target: '.video-prompt-editor', + content: 'Describe your video content here. Our AI will generate videos based on your description.', + placement: 'top', + }, + { + target: '.tool-operation-button', + content: 'Select different creation modes and video resolutions to customize your output.', + placement: 'top', + }, + { + target: '.tool-submit-button', + content: 'Click here to start creating your video once you are ready!', + placement: 'left', + }, + ]; + + // 处理引导结束 + const handleJoyrideCallback = (data: any) => { + const { status } = data; + if (status === 'finished' || status === 'skipped') { + setRunTour(false); + // 可以在这里存储用户已完成引导的状态 + localStorage.setItem('hasCompletedTour', 'true'); + } + }; + + // 检查是否需要显示引导 useEffect(() => { - if (isDragging) { - window.addEventListener('mousemove', handleDrag); - window.addEventListener('mouseup', handleDragEnd); + const hasCompletedTour = localStorage.getItem('hasCompletedTour'); + if (hasCompletedTour) { + setRunTour(false); } + }, []); - return () => { - window.removeEventListener('mousemove', handleDrag); - window.removeEventListener('mouseup', handleDragEnd); - }; - }, [isDragging, dragStart]); + useEffect(() => { + setIsClient(true); + }, []); return (
- {/* 展示创作详细过程 */} - {generateObj && ( -
- - {/* 第一行 - 占2/3高度 */} - - - {/* 第一列 - 角色档案卡 */} - -
-
-

角色档案

-
-
- {generateObj.video_info ? ( -
- {generateObj.video_info.roles.map((role: any, index: number) => ( -
-
-
- {role.name} -
-
-
-
{role.name}
-
{role.core_identity}
-
-
- ))} -
- ) : ( -
- {[1, 2].map((i) => ( -
-
- -
- - - -
-
-
- ))} -
- )} -
-
-
- - - {/* 第二列 - 视频预览和加载状态 */} - -
-
-

视频预览

-
-
-
- {isLoading ? ( -
-
-
-
+ {isClient && ( + + )} +
+
+
+ empty_video +
+ Generated videos will appear here. + handleStartCreating()}>Start creating!
-
- {loadingText} - - . - . - . - -
- ) : generateObj.final_video_url ? ( -
-
-
- - - - {/* 第三列 - 概要信息 */} - -
-
-

概要信息

-
-
- {generateObj.video_info ? ( -
-
- 场景 -

{generateObj.video_info.sence}

-
-
- 风格 -

{generateObj.video_info.style}

-
-
- ) : ( -
-
- - -
-
- - -
-
- )} -
-
-
- - - - - {/* 第二行 - 占1/3高度 */} - - - {/* 第一列 - 分镜脚本 */} - -
-
-
-

分镜脚本

- {selectedVideoIndex !== null && ( - - Scene {selectedVideoIndex + 1} - - )} -
- -
-
- {generateObj.scripts && selectedVideoIndex !== null ? ( -
-
-
- 镜头 -
- {editingField.type === 'shot' ? ( -