diff --git a/api/DTO/movie_start_dto.ts b/api/DTO/movie_start_dto.ts index 55a5ce5..898ec98 100644 --- a/api/DTO/movie_start_dto.ts +++ b/api/DTO/movie_start_dto.ts @@ -200,8 +200,6 @@ export interface CreateMovieProjectV3Request { mode: "auto" | "manual"; /** 分辨率:720p | 1080p | 4k */ resolution: "720p" | "1080p" | "4k"; - /** 类型 */ - genre: string; /** 语言 */ language: string; /**模板id */ @@ -210,6 +208,8 @@ export interface CreateMovieProjectV3Request { storyRole: { /**角色名 */ role_name: string; + /**角色描述 */ + role_description: string; /**照片URL */ photo_url: string; /**声音URL */ diff --git a/api/movie_start.ts b/api/movie_start.ts index c3b9e8c..f8e9c37 100644 --- a/api/movie_start.ts +++ b/api/movie_start.ts @@ -5,28 +5,26 @@ import { MovieStartDTO, StoryAnalysisTask, MovieStoryTaskDetail, + CreateMovieProjectV3Request, } from "./DTO/movie_start_dto"; import { get, post } from "./request"; import { StoryTemplateEntity, - ImageStoryEntity, } from "@/app/service/domain/Entities"; /** * 获取故事模板列表 */ -export const getTemplateStoryList = async () => { - return await get>("/template-story/list"); -}; - -/** - * 执行故事模板操作,生成电影项目 - */ -export const actionTemplateStory = async (template: StoryTemplateEntity) => { - return await post>( - "/template-story/action", - template - ); +export const getTemplateStoryList = async (page?: number, per_page?: number) => { + return await post>("/movie_template/story-template-list",{ + page, + per_page, + }); }; /** @@ -62,7 +60,7 @@ export const createMovieProjectV2 = async ( * @returns Promise> */ export const createMovieProjectV3 = async ( - request: CreateMovieProjectV2Request + request: CreateMovieProjectV3Request ) => { return post>( "/movie/create_movie_project_v3", diff --git a/app/create/page.tsx b/app/create/page.tsx index b392b28..e9327db 100644 --- a/app/create/page.tsx +++ b/app/create/page.tsx @@ -1,7 +1,7 @@ // import { redirect } from 'next/navigation'; -import { CreateToVideo2 } from '@/components/pages/create-to-video2'; +import CreateToVideo2 from '@/components/pages/create-to-video2'; export default function CreatePage() { // redirect('/create/video-to-video'); return ; -} \ No newline at end of file +} diff --git a/app/globals.css b/app/globals.css index 19716a9..69812df 100644 --- a/app/globals.css +++ b/app/globals.css @@ -174,4 +174,7 @@ body { float: left; height: 0; pointer-events: none; -} \ No newline at end of file +} +.ant-spin-nested-loading .ant-spin { + max-height: none !important; +} diff --git a/app/service/Interaction/templateStoryService.ts b/app/service/Interaction/templateStoryService.ts index 2b0c1e5..30d3413 100644 --- a/app/service/Interaction/templateStoryService.ts +++ b/app/service/Interaction/templateStoryService.ts @@ -1,4 +1,6 @@ +import { message } from "antd"; import { StoryTemplateEntity } from "../domain/Entities"; +import { useUploadFile } from "../domain/service"; /** 模板角色接口 */ interface TemplateRole { @@ -12,6 +14,8 @@ interface TemplateRole { import { TemplateStoryUseCase } from "../usecase/templateStoryUseCase"; import { getUploadToken, uploadToQiniu } from "@/api/common"; import { useState, useCallback, useMemo } from "react"; +import { createMovieProjectV3 } from "@/api/movie_start"; +import { CreateMovieProjectV3Request } from "@/api/DTO/movie_start_dto"; interface UseTemplateStoryService { /** 模板列表 */ @@ -25,33 +29,56 @@ interface UseTemplateStoryService { /** 加载状态 */ isLoading: boolean; /** 获取模板列表函数 */ + /** 获取模板列表函数 */ getTemplateStoryList: () => Promise; - /** action 生成电影函数 */ - actionStory: () => Promise; + /** + * action 生成电影函数 + * @param {string} user_id - 用户ID + * @param {"auto" | "manual"} mode - 生成模式 + * @param {"720p" | "1080p" | "4k"} resolution - 分辨率 + * @param {string} language - 语言 + * @returns {Promise} - 生成的电影ID + */ + actionStory: ( + user_id: string, + mode: "auto" | "manual", + resolution: "720p" | "1080p" | "4k" , + language: string + ) => Promise; /** 设置选中的模板 */ setSelectedTemplate: (template: StoryTemplateEntity | null) => void; /** 设置活跃角色索引 */ setActiveRoleIndex: (index: number) => void; - /** 设置当前活跃角色的图片URL */ - setActiveRoleImage: (imageUrl: string) => void; + /** 设置当前活跃角色的音频URL */ setActiveRoleAudio: (audioUrl: string) => void; /**清空数据 */ clearData: () => void; + /** 上传人物头像并分析 */ + AvatarAndAnalyzeFeatures: (imageUrl: string) => Promise; } export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { - const [templateStoryList, setTemplateStoryList] = useState([]); - const [selectedTemplate, setSelectedTemplate] = useState(null); + const [templateStoryList, setTemplateStoryList] = useState< + StoryTemplateEntity[] + >([]); + const [selectedTemplate, setSelectedTemplate] = + useState(null); const [activeRoleIndex, setActiveRoleIndex] = useState(0); const [isLoading, setIsLoading] = useState(false); + // 使用上传文件Hook + const { uploadFile } = useUploadFile(); /** 模板故事用例实例 */ const templateStoryUseCase = useMemo(() => new TemplateStoryUseCase(), []); /** 计算属性:当前活跃角色信息 */ const activeRole = useMemo(() => { - if (!selectedTemplate || activeRoleIndex < 0 || activeRoleIndex >= selectedTemplate.storyRole.length) { + if ( + !selectedTemplate || + activeRoleIndex < 0 || + activeRoleIndex >= selectedTemplate.storyRole.length + ) { return null; } return selectedTemplate.storyRole[activeRoleIndex]; @@ -64,117 +91,19 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { try { setIsLoading(true); - // const templates = await templateStoryUseCase.getTemplateStoryList(); - - const templates = await new Promise((resolve) => { - setTimeout(() => { - resolve([ - { - id: '1', - name: '奇幻冒险故事', - image_url: ['/assets/3dr_howlcastle.png', '/assets/3dr_spirited.jpg'], - generateText: '一个关于勇气与友谊的奇幻冒险故事,主角在神秘世界中寻找失落的宝藏,结识了各种奇特的生物和伙伴。', - storyRole: [ - { - role_name: '艾莉娅', - role_description: '一个勇敢的女孩,拥有强大的魔法力量,她的冒险旅程充满了危险和挑战。', - photo_url: '/assets/3dr_chihiro.png', - voice_url:"" - }, - { - role_name: '魔法师梅林', - role_description: '一个智慧的魔法师,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。', - photo_url: '/assets/3dr_mono.png', - voice_url:"" - }, - { - role_name: '守护者龙', - role_description: '一个强大的守护者,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。', - photo_url: '/assets/3dr_howlbg.jpg', - voice_url:"" - } - ] - }, - { - id: '2', - name: '科幻探索之旅', - image_url: ['/assets/3dr_monobg.jpg'], - generateText: '未来世界的太空探索故事,人类在浩瀚宇宙中寻找新的家园,面对未知的挑战和外星文明的接触。', - storyRole: [ - { - role_name: '船长凯特', - role_description: '一个勇敢的船长,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。', - photo_url: '/assets/3dr_chihiro.png', - voice_url:"" - }, - { - role_name: 'AI助手诺娃', - role_description: '一个强大的AI助手,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。', - photo_url: '/assets/3dr_mono.png', - voice_url: '' - } - ] - }, - { - id: '3', - name: '温馨家庭喜剧', - image_url: ['/assets/3dr_spirited.jpg'], - generateText: '一个充满欢笑和温情的家庭故事,讲述家庭成员之间的日常趣事,以及如何一起面对生活中的小挑战。', - storyRole: [ - { - role_name: '妈妈莉莉', - role_description: '一个温柔的妈妈,拥有强大的魔法力量,她的冒险旅程充满了危险和挑战。', - photo_url: '/assets/3dr_chihiro.png', - voice_url:"" - }, - { - role_name: '爸爸汤姆', - role_description: '一个勇敢的爸爸,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。', - photo_url: '/assets/3dr_mono.png', - voice_url:"" - }, - { - role_name: '孩子小杰', - role_description: '一个聪明的孩子,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。', - photo_url: '/assets/3dr_howlbg.jpg', - voice_url:"" - } - ] - } - ]); - }, 3000); - }); + const templates = await templateStoryUseCase.getTemplateStoryList(); setTemplateStoryList(templates); setSelectedTemplate(templates[0]); + setActiveRoleIndex(0); + console.log(selectedTemplate, activeRoleIndex) } catch (err) { - console.error('获取模板列表失败:', err); + console.error("获取模板列表失败:", err); } finally { setIsLoading(false); } }, [templateStoryUseCase]); - /** - * action 生成电影函数 - */ - const actionStory = useCallback(async (): Promise => { - if (!selectedTemplate) { - throw new Error('请先选择一个故事模板'); - } - - try { - setIsLoading(true); - console.log('selectedTemplate', selectedTemplate) - // const projectId = await templateStoryUseCase.actionStory(selectedTemplate); - return 'projectId'; - } catch (err) { - console.error('生成电影失败:', err); - throw err; - } finally { - setIsLoading(false); - } - }, [selectedTemplate, templateStoryUseCase]); - /** * 设置活跃角色索引 */ @@ -185,37 +114,120 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { /** * 设置当前活跃角色的图片URL */ - const setActiveRoleImage = useCallback((imageUrl: string): void => { - if (!selectedTemplate || activeRoleIndex < 0 || activeRoleIndex >= selectedTemplate.storyRole.length) { - return; - } + const setActiveRoleData = useCallback( + (imageUrl: string, desc: string): void => { + if ( + !selectedTemplate || + activeRoleIndex < 0 || + activeRoleIndex >= selectedTemplate.storyRole.length + ) { + console.log(selectedTemplate, activeRoleIndex); + return; + } + try { - const updatedTemplate = { - ...selectedTemplate, - storyRole: selectedTemplate.storyRole.map((role, index) => - index === activeRoleIndex ? { ...role, photo_url: imageUrl } : role - ), - }; - setSelectedTemplate(updatedTemplate); - }, [selectedTemplate, activeRoleIndex]); + const character_briefs = { + name: selectedTemplate.storyRole[activeRoleIndex].role_name, + image_url: imageUrl, + character_analysis: JSON.parse(desc).character_analysis, + }; + console.log("character_briefs", character_briefs); + const updatedTemplate = { + ...selectedTemplate, + storyRole: selectedTemplate.storyRole.map((role, index) => + index === activeRoleIndex + ? { + ...role, + photo_url: imageUrl, + role_description: JSON.stringify(character_briefs), + } + : role + ), + }; + + setSelectedTemplate(updatedTemplate); + } catch (error) { + message.error("Image analysis failed"); + console.log("error", error); + } + }, + [selectedTemplate, activeRoleIndex] + ); /** * 设置当前活跃角色的音频URL */ - const setActiveRoleAudio = useCallback((audioUrl: string): void => { - if (!selectedTemplate || activeRoleIndex < 0 || activeRoleIndex >= selectedTemplate.storyRole.length) { - return; - } + const setActiveRoleAudio = useCallback( + (audioUrl: string): void => { + if ( + !selectedTemplate || + activeRoleIndex < 0 || + activeRoleIndex >= selectedTemplate.storyRole.length + ) { + return; + } - const updatedTemplate = { - ...selectedTemplate, - storyRole: selectedTemplate.storyRole.map((role, index) => - index === activeRoleIndex ? { ...role, voice_url: audioUrl } : role - ), - }; - setSelectedTemplate(updatedTemplate); - }, [selectedTemplate, activeRoleIndex]); + const updatedTemplate = { + ...selectedTemplate, + storyRole: selectedTemplate.storyRole.map((role, index) => + index === activeRoleIndex ? { ...role, voice_url: audioUrl } : role + ), + }; + setSelectedTemplate(updatedTemplate); + }, + [selectedTemplate, activeRoleIndex] + ); + /** + * 上传人物头像并分析特征,替换旧的角色数据 + * @param {string} characterName - 角色名称 + */ + const AvatarAndAnalyzeFeatures = useCallback( + async (imageUrl: string): Promise => { + try { + setIsLoading(true); + + // 调用用例处理人物头像上传和特征分析 + const result = await templateStoryUseCase.AvatarAndAnalyzeFeatures( + imageUrl + ); + + setActiveRoleData(result.crop_url, result.whisk_caption); + console.log("人物头像和特征描述更新成功:", result); + } catch (error) { + console.error("人物头像上传和特征分析失败:", error); + throw error; + } finally { + setIsLoading(false); + } + }, + [templateStoryUseCase] + ); + const actionStory = useCallback( + async ( + user_id: string, + mode: "auto" | "manual" = "auto", + resolution: "720p" | "1080p" | "4k" = "720p", + language: string = "English" + ) => { + try { + const params: CreateMovieProjectV3Request = { + user_id, + mode, + resolution, + storyRole: selectedTemplate?.storyRole || [], + language, + template_id: selectedTemplate?.template_id || "", + }; + + const result = await createMovieProjectV3(params); + return result.data.project_id as string; + } catch (error) { + console.error("创建电影项目失败:", error); + } + }, + [selectedTemplate] + ); return { templateStoryList, selectedTemplate, @@ -226,12 +238,12 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { actionStory, setSelectedTemplate, setActiveRoleIndex: handleSetActiveRoleIndex, - setActiveRoleImage, setActiveRoleAudio, + AvatarAndAnalyzeFeatures, clearData: () => { setTemplateStoryList([]); setSelectedTemplate(null); setActiveRoleIndex(0); - } + }, }; }; diff --git a/app/service/domain/Entities.ts b/app/service/domain/Entities.ts index f8c5856..3a7dca7 100644 --- a/app/service/domain/Entities.ts +++ b/app/service/domain/Entities.ts @@ -140,6 +140,8 @@ export interface StoryTemplateEntity { image_url: string[]; /** 故事模板概览*/ generateText: string; + /** 故事模板ID */ + template_id: string; /**故事角色 */ storyRole: { /**角色名 */ diff --git a/app/service/usecase/templateStoryUseCase.ts b/app/service/usecase/templateStoryUseCase.ts index 77ce594..d6487ac 100644 --- a/app/service/usecase/templateStoryUseCase.ts +++ b/app/service/usecase/templateStoryUseCase.ts @@ -1,5 +1,6 @@ +import { generateCharacterBrief } from "@/api/video_flow"; import { StoryTemplateEntity } from "../domain/Entities"; -import { getTemplateStoryList, actionTemplateStory } from "@/api/movie_start"; +import { getTemplateStoryList } from "@/api/movie_start"; /** * 模板故事用例 @@ -21,35 +22,44 @@ export class TemplateStoryUseCase { try { const response = await getTemplateStoryList(); if (response.successful && response.data) { - this.templateStoryList = response.data; - return response.data; + this.templateStoryList = response.data.items; + return response.data.items; } - throw new Error(response.message || '获取故事模板列表失败'); + throw new Error(response.message || "获取故事模板列表失败"); } catch (error) { - console.error('获取故事模板列表失败:', error); + console.error("获取故事模板列表失败:", error); throw error; } } + + /** - * 执行故事模板相关操作 - * @param {StoryTemplateEntity} template - 选中的故事模板 - * @returns {Promise} - 项目id + * 上传人物头像并分析特征, + * @returns {Promise<{crop_url: string, whisk_caption: string}>} 返回新的头像URL和特征描述 */ - async actionStory(template: StoryTemplateEntity): Promise { + async AvatarAndAnalyzeFeatures( + imageUrl: string + ): Promise<{ crop_url: string; whisk_caption: string }> { + // 直接在这里处理上传和分析逻辑 try { - if (!template) { - throw new Error('故事模板不能为空'); + // 2. 调用AI分析接口获取人物特征描述 + const analysisResult = await generateCharacterBrief({ + image_url: imageUrl, + }); + + if (!analysisResult.successful || !analysisResult.data) { + throw new Error("人物特征分析失败"); } - const response = await actionTemplateStory(template); - if (response.successful && response.data) { - this.selectedTemplate = template; - return response.data.projectId; - } - throw new Error(response.message || '执行故事模板操作失败'); + // 3. 返回新的头像URL和特征描述,用于替换旧数据 + const result = { + crop_url: imageUrl, + whisk_caption: JSON.stringify(analysisResult.data.character_brief), + }; + + return result; } catch (error) { - console.error('执行故事模板操作失败:', error); throw error; } } diff --git a/components/ChatInputBox/AudioRecorder.tsx b/components/ChatInputBox/AudioRecorder.tsx index 36d52a0..43a2d3a 100644 --- a/components/ChatInputBox/AudioRecorder.tsx +++ b/components/ChatInputBox/AudioRecorder.tsx @@ -1,12 +1,24 @@ "use client"; import React, { useState, useRef, useEffect } from "react"; -import { Mic, MicOff, Upload, Play, Pause, Trash2, X } from "lucide-react"; +import { + Mic, + MicOff, + Upload, + Play, + Pause, + Trash2, + X, + Volume2, +} from "lucide-react"; import { Tooltip, Upload as AntdUpload, message } from "antd"; import { InboxOutlined } from "@ant-design/icons"; import WaveSurfer from "wavesurfer.js"; -import { getAudioDuration, useUploadFile } from "../../app/service/domain/service"; +import { + getAudioDuration, + useUploadFile, +} from "../../app/service/domain/service"; // 自定义样式 const audioRecorderStyles = ` @@ -92,16 +104,35 @@ export function AudioRecorder({ const [isPlaying, setIsPlaying] = useState(false); const [volume, setVolume] = useState(1); const [isMuted, setIsMuted] = useState(false); + const [recordingTime, setRecordingTime] = useState(0); // 录制时长(秒) + const [isUploadingRecording, setIsUploadingRecording] = useState(false); // 录制音频上传状态 const { uploadFile, isUploading } = useUploadFile(); const mediaRecorderRef = useRef(null); const chunksRef = useRef([]); + const recordingTimerRef = useRef(null); // 录制计时器引用 + const actualRecordingTimeRef = useRef(0); // 实际录制时长引用,用于准确获取时长 + + // 清理计时器 + useEffect(() => { + return () => { + if (recordingTimerRef.current) { + clearInterval(recordingTimerRef.current); + } + }; + }, []); // 开始录制 const startRecording = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - const mediaRecorder = new MediaRecorder(stream); + + // 检查支持的音频格式,优先使用 webm + const mimeType = MediaRecorder.isTypeSupported("audio/webm") + ? "audio/webm" + : "audio/mp4"; + + const mediaRecorder = new MediaRecorder(stream, { mimeType }); mediaRecorderRef.current = mediaRecorder; chunksRef.current = []; @@ -112,23 +143,70 @@ export function AudioRecorder({ }; mediaRecorder.onstop = async () => { - const audioBlob = new Blob(chunksRef.current, { type: "audio/wav" }); - const audioUrl = URL.createObjectURL(audioBlob); - const duration = await getAudioDuration(audioBlob); + const audioBlob = new Blob(chunksRef.current, { type: mimeType }); + + // 先停止计时器,然后获取最终时长 + if (recordingTimerRef.current) { + clearInterval(recordingTimerRef.current); + recordingTimerRef.current = null; + } + + // 使用引用值获取准确的录制时长 + const duration = actualRecordingTimeRef.current; + console.log("录制时长:", duration, "秒"); // 无论时长是否符合要求,都要释放麦克风权限 stream.getTracks().forEach((track) => track.stop()); - if(duration>20||duration<10){ - message.warning("Please keep your audio between 10 and 20 seconds to help us better learn your voice.") + if (duration > 20 || duration < 10) { + message.warning( + "Please keep your audio between 10 and 20 seconds to help us better learn your voice." + ); return; } - onAudioRecorded(audioBlob, audioUrl); + try { + // 将 Blob 转换为 File 对象,以便上传 + const audioFile = new File( + [audioBlob], + `recording_${Date.now()}.webm`, + { type: mimeType } + ); + + // 设置上传状态 + setIsUploadingRecording(true); + + // 使用 uploadFile 上传到服务器 + console.log("开始上传录制的音频文件..."); + const uploadedUrl = await uploadFile(audioFile); + console.log("音频上传成功,服务器URL:", uploadedUrl); + + // 使用服务器返回的真实URL地址 + onAudioRecorded(audioBlob, uploadedUrl); + } catch (error) { + console.error("音频上传失败:", error); + message.error("音频上传失败,请重试"); + return; + } finally { + setIsUploadingRecording(false); + } + + // 重置计时器状态 + setRecordingTime(0); + actualRecordingTimeRef.current = 0; // 重置引用值 + setIsRecording(false); }; mediaRecorder.start(); setIsRecording(true); + setRecordingTime(0); // 重置计时器 + actualRecordingTimeRef.current = 0; // 重置引用值 + + // 开始计时 + recordingTimerRef.current = setInterval(() => { + setRecordingTime((prev) => prev + 1); + actualRecordingTimeRef.current += 1; // 同时更新引用值 + }, 1000); } catch (error) { console.error("录制失败:", error); } @@ -139,6 +217,7 @@ export function AudioRecorder({ if (mediaRecorderRef.current && isRecording) { mediaRecorderRef.current.stop(); setIsRecording(false); + // 注意:计时器清理现在在 onstop 回调中处理 } }; @@ -184,7 +263,7 @@ export function AudioRecorder({
{mode === "upload" ? ( // 上传模式 -
+
{ + beforeUpload={async (file) => { console.log("beforeUpload 被调用:", file); // 移除 return false,让文件能够正常进入 customRequest const duration = await getAudioDuration(file); - if(duration>20||duration<10){ - message.warning("Please keep your audio between 10 and 20 seconds to help us better learn your voice.") + console.log("duration", duration); + if (duration > 20 || duration < 10) { + message.warning( + "Please keep your audio between 10 and 20 seconds to help us better learn your voice." + ); return false; } return true; @@ -258,7 +340,7 @@ export function AudioRecorder({
) : ( // 录制模式 -
+
{isRecording ? ( // 录制中状态
@@ -273,18 +355,33 @@ export function AudioRecorder({
Recording...
- {/* 录制状态指示器 */} + {/* 录制状态指示器和计时器 */}
-
+
Recording... + + {Math.floor(recordingTime / 60) + .toString() + .padStart(2, "0")} + :{(recordingTime % 60).toString().padStart(2, "0")} +
+ ) : isUploadingRecording ? ( + // 录制音频上传中状态 +
+
+ Uploading recorded audio... +
+
+
+
+
) : ( // 录制准备状态
-
🎙️
Click to start recording
@@ -314,8 +411,8 @@ export function AudioRecorder({ onClick={() => setMode("upload")} className={`transition-colors ${ mode === "upload" - ? "text-blue-300" - : "text-white/40 hover:text-white/60" + ? "text-[rgb(106,244,249)]" + : "text-white/40 hover:text-[rgb(106,244,249)]/60" }`} > @@ -327,8 +424,8 @@ export function AudioRecorder({ onClick={() => setMode("record")} className={`transition-colors ${ mode === "record" - ? "text-green-300" - : "text-white/40 hover:text-white/60" + ? "text-[rgb(199,59,255)]" + : "text-white/40 hover:text-[rgb(199,59,255)]/60" }`} > @@ -344,13 +441,99 @@ export function AudioRecorder({ return ( <> -
+
+ {/* 大旋转的播放圆盘 */} +
+
+ {/* 外层边框 - 唱片边缘 */} +
+ + {/* 唱片主体 - 纯黑色唱片面 */} +
+ {/* 唱片纹理 - 细密的同心圆 */} +
+ + {/* 中心孔洞 - 透明圆环,显示背景色 */} +
+ + {/* MovieFlow 字母 - 环形文本效果 */} +
+ + {/* 定义渐变 */} + + + + + + + + {/* 圆形路径 - 减小半径让文字更靠近中心 */} + + + {/* 环形文本 - 调整起始位置让文字更居中 */} + + + MovieFlow + + + +
+
+ + {/* 唱片光泽效果 */} +
+
+
+ {/* 头部 - 只显示操作按钮 */} -
+
@@ -358,15 +541,26 @@ export function AudioRecorder({ )}
- - {/* WaveSurfer 波形图区域 */} -
-
+ {/* 播放控制 */} +
+ + {/* WaveSurfer 波形图区域 */} +
- {/* 播放控制 */} -
- -
- {/* 音频设置 */} -
-
- - - {Math.round(volume * 100)}% - +
+
+ {/* 音量控制 */} +
+
+
+ {/* 可拖拽的音量滑块 */} + + {/* 视觉滑块指示器 */} +
+
+ {/* 音频图标 */} +
+ +
+
-
1x
@@ -443,7 +638,7 @@ interface WaveformPlayerProps { * @param {boolean} [isMuted=false] - 是否静音 * @returns {JSX.Element} 渲染的波形播放器组件 * ``` - */ + */ function WaveformPlayer({ audioUrl, isPlaying, @@ -463,23 +658,33 @@ function WaveformPlayer({ useEffect(() => { if (!containerRef.current || !audioUrl) return; + // 创建Canvas渐变 + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const gradient = ctx.createLinearGradient(0, 0, 0, 64); // 64是波形高度 + gradient.addColorStop(0, "rgb(106, 244, 249)"); // 顶部:青色 + gradient.addColorStop(0.5, "rgb(152, 151, 254)"); // 中间:蓝紫色 + gradient.addColorStop(1, "rgb(199, 59, 255)"); // 底部:紫色 + // 创建WaveSurfer实例并配置样式 const ws = WaveSurfer.create({ container: containerRef.current, - waveColor: "#3b82f6", // 波形颜色 - progressColor: "#1d4ed8", // 播放进度颜色 - cursorColor: "#1e40af", // 播放光标颜色 - height: 64, // 波形高度 - barWidth: 2, // 波形条宽度 - barGap: 1, // 波形条间距 - normalize: true, // 自动标准化波形 - url: audioUrl, // 音频文件URL + waveColor: gradient, // 使用渐变作为波形颜色 + progressColor: "rgb(152, 151, 254)", // 播放进度颜色 - 半透明白色,更优雅 + cursorColor: "rgb(152, 151, 254)", // 播放光标颜色 - 紫色 + height: 64, // 波形高度 + barWidth: 2, // 波形条宽度 + barGap: 1, // 波形条间距 + normalize: true, // 自动标准化波形 + url: audioUrl, // 音频文件URL }); // 监听播放状态变化事件,同步到外部状态 - ws.on("play", () => onPlayStateChange(true)); // 开始播放 - ws.on("pause", () => onPlayStateChange(false)); // 暂停播放 - ws.on("finish", () => onPlayStateChange(false)); // 播放结束 + ws.on("play", () => onPlayStateChange(true)); // 开始播放 + ws.on("pause", () => onPlayStateChange(false)); // 暂停播放 + ws.on("finish", () => onPlayStateChange(false)); // 播放结束 wavesurferRef.current = ws; diff --git a/components/ChatInputBox/ChatInputBox.tsx b/components/ChatInputBox/ChatInputBox.tsx index b6b080f..90614ce 100644 --- a/components/ChatInputBox/ChatInputBox.tsx +++ b/components/ChatInputBox/ChatInputBox.tsx @@ -23,7 +23,6 @@ import { Modal, Tooltip, Upload, - Spin, Popconfirm, Image, } from "antd"; @@ -44,9 +43,21 @@ import GlobalLoad from "../common/GlobalLoad"; const RenderTemplateStoryMode = ({ isOpen, onClose, + configOptions = { + mode: "auto" as "auto" | "manual", + resolution: "720p" as "720p" | "1080p" | "4k", + language: "english", + videoDuration: "1min", + }, }: { isOpen: boolean; onClose: () => void; + configOptions: { + mode: "auto" | "manual"; + resolution: "720p" | "1080p" | "4k"; + language: string; + videoDuration: string; + }; }) => { // 使用 hook 管理状态 const { @@ -59,17 +70,16 @@ const RenderTemplateStoryMode = ({ actionStory, setSelectedTemplate, setActiveRoleIndex, - setActiveRoleImage, + AvatarAndAnalyzeFeatures, setActiveRoleAudio, clearData, } = useTemplateStoryServiceHook(); // 使用上传文件hook const { uploadFile, isUploading } = useUploadFile(); - // 本地加载状态,用于 UI 反馈 const [localLoading, setLocalLoading] = useState(0); - + const router = useRouter(); // 组件挂载时获取模板列表 useEffect(() => { if (isOpen) { @@ -97,9 +107,18 @@ const RenderTemplateStoryMode = ({ }, 100); try { setLocalLoading(1); - // 假性的增加进度条 + // 获取当前用户信息 + const User = JSON.parse(localStorage.getItem("currentUser") || "{}"); - const projectId = await actionStory(); + if (!User.id) { + console.error("用户未登录"); + return; + } + const projectId = await actionStory(String(User.id), configOptions.mode, configOptions.resolution, configOptions.language); + if (projectId) { + // 跳转到电影详情页 + router.push(`/create/work-flow?episodeId=${projectId}`); + } console.log("Story action created:", projectId); onClose(); // 重置状态 @@ -113,31 +132,6 @@ const RenderTemplateStoryMode = ({ clearInterval(timer); } }; - - // 处理角色图片上传 - 已移至customRequest中处理 - // const handleRoleImageUpload = async (roleIndex: number, file: File) => { - // if (!file || !selectedTemplate) return; - // - // try { - // // 使用真正的文件上传功能 - // const imageUrl = await uploadFile(file, (progress) => { - // console.log(`上传进度: ${progress}%`); - // }); - // - // // 上传成功后,更新角色图片 - // setActiveRoleImage(imageUrl); - // } catch (error) { - // console.error("图片上传失败:", error); - // // 这里可以添加错误提示 - // } - // }; - - // 删除角色图片 - const handleDeleteRoleImage = (roleIndex: number) => { - if (selectedTemplate) { - setActiveRoleImage(""); - } - }; // 模板列表渲染 const templateListRender = () => { return ( @@ -172,7 +166,7 @@ const RenderTemplateStoryMode = ({ {/* 模板信息头部 - 增加顶部空间 */}
{/* 左侧图片 */} -
+
@@ -194,7 +188,13 @@ const RenderTemplateStoryMode = ({ > {selectedTemplate.name} -
+

- {/* 左侧:当前选中角色的音频与照片更改 - 精简版本 */} + {/* 左侧:音频部分 */}

- {/* 图片上传部分 - 精简 */} -
-
- + {/* 音频操作区域 - 使用新的 AudioRecorder 组件 */} +
+ { + setActiveRoleAudio(audioUrl); + }} + onAudioDeleted={() => { + setActiveRoleAudio(""); + }} + /> +
+
+ + {/* 右侧:角色图片缩略图列表 - 精简 */} +
+ {selectedTemplate.storyRole.map((role, index: number) => ( +
+ + + + + {/* 上传按钮 - 右上角 */} + { // 验证文件类型 @@ -238,14 +272,14 @@ const RenderTemplateStoryMode = ({ return false; } - // 验证文件大小 (5MB) - const isLt5M = file.size / 1024 / 1024 < 10; - if (!isLt5M) { + // 验证文件大小 (10MB) + const isLt10M = file.size / 1024 / 1024 < 10; + if (!isLt10M) { console.error("图片大小不能超过10MB"); return false; } - return true; // 允许文件通过验证 + return true; }} customRequest={async ({ file, onSuccess, onError }) => { try { @@ -264,13 +298,10 @@ const RenderTemplateStoryMode = ({ console.log(`上传进度: ${progress}%`); } ); - console.log("图片上传成功,URL:", uploadedUrl); // 上传成功后,更新角色图片 - setActiveRoleImage(uploadedUrl); - - // 调用成功回调 + await AvatarAndAnalyzeFeatures(uploadedUrl); onSuccess?.(uploadedUrl); } catch (error) { console.error("图片上传失败:", error); @@ -278,100 +309,16 @@ const RenderTemplateStoryMode = ({ } }} > - {activeRole?.photo_url ? ( -
- Character Portrait - {/* */} -
-
- -
Change Photo
-
-
-
- ) : ( -
- {isUploading ? ( - <> - - 上传中... - - ) : ( - <> - - Upload Photo - - )} -
- )} +
-
- - {/* 音频部分 - 精简版本 */} -
- {/* 音频操作区域 - 使用新的 AudioRecorder 组件 */} -
- { - setActiveRoleAudio(audioUrl); - }} - onAudioDeleted={() => { - setActiveRoleAudio(""); - }} - /> -
-
-
- - {/* 右侧:角色图片缩略图列表 - 精简 */} -
-

- Characters -

- {selectedTemplate.storyRole.map((role, index: number) => ( - - - ))}
@@ -415,20 +362,21 @@ const RenderTemplateStoryMode = ({
} > -
- {/* 弹窗头部 */} -
-

- Template Story Selection -

-
- + +
+ {/* 弹窗头部 */} +
+

+ Template Story Selection +

+
+
{templateListRender()}
{storyEditorRender()}
- -
+
+ ); @@ -681,6 +629,7 @@ export function ChatInputBox() { {/* 模板故事弹窗 */} setIsTemplateModalOpen(false)} /> @@ -840,11 +789,11 @@ const PhotoStoryModal = ({ onClose(); }; const router = useRouter(); - const taskProgressRef = useRef(taskProgress); + const taskProgressRef = useRef(taskProgress); - useEffect(() => { - taskProgressRef.current = taskProgress; - }, [taskProgress]); + useEffect(() => { + taskProgressRef.current = taskProgress; + }, [taskProgress]); // 处理图片上传 const handleImageUpload = async (e: any) => { const target = e.target as HTMLImageElement; @@ -954,7 +903,7 @@ const PhotoStoryModal = ({ Story inspiration { // 如果裁剪的头像加载失败,回退到原图 const target = e.target as HTMLImageElement; diff --git a/components/common/GlobalLoad.tsx b/components/common/GlobalLoad.tsx index d3c2cd4..c03e213 100644 --- a/components/common/GlobalLoad.tsx +++ b/components/common/GlobalLoad.tsx @@ -43,10 +43,9 @@ export default function GlobalLoad({ {Boolean(progress) && }
); - return (
- + {children}
diff --git a/components/pages/create-to-video2.tsx b/components/pages/create-to-video2.tsx index 2022262..1832697 100644 --- a/components/pages/create-to-video2.tsx +++ b/components/pages/create-to-video2.tsx @@ -12,7 +12,7 @@ import { ChatInputBox } from '@/components/ChatInputBox/ChatInputBox'; // ideaText已迁移到ChatInputBox组件中 -export function CreateToVideo2() { +export default function CreateToVideo2() { const router = useRouter(); const searchParams = useSearchParams(); const projectId = searchParams.get('projectId') ? parseInt(searchParams.get('projectId')!) : 0;