diff --git a/api/DTO/movie_start_dto.ts b/api/DTO/movie_start_dto.ts index b56d85c..3e3a06b 100644 --- a/api/DTO/movie_start_dto.ts +++ b/api/DTO/movie_start_dto.ts @@ -84,6 +84,8 @@ export interface StoryAnalysisTask{ * 创建电影项目V2请求参数 照片生成电影 */ export interface CreateMovieProjectV2Request { + /** 任务ID */ + project_id:string; /** 剧本内容 */ script: string; /** 用户ID */ @@ -98,7 +100,7 @@ export interface CreateMovieProjectV2Request { character_briefs: { name:string; image_url:string; - character_analysis:Record; + character_analysis:string; }[]; /** 语言 */ language: string; @@ -198,7 +200,7 @@ export interface CreateMovieProjectV3Request { user_id: string; /** 模式:auto | manual */ mode: "auto" | "manual"; - /** 分辨率:720p | 1080p | 4k */ + /** 分辨率:720p | 1080p | "4k" */ resolution: "720p" | "1080p" | "4k"; /** 语言 */ language: string; @@ -219,4 +221,85 @@ export interface CreateMovieProjectV3Request { /**声音URL */ voice_url: string; }[]; + /** 可填写的变量字段 */ + fillable_content?: { + /** 字段名称 */ + field_name: string; + /** 字段类型 */ + field_type: string; + /** 字段值 */ + field_value?: string; + /** 字段描述 */ + field_description?: string; + /** 字段元数据 */ + field_meta?: Record; + }[]; +} + +/** + * Gemini文本转图像请求参数 + */ +export interface GeminiTextToImageRequest { + /** 提示词 */ + prompt: string; +} + +/** + * Gemini文本转图像响应数据 + */ +export interface GeminiTextToImageData { + /** 生成的图像URL */ + image_url: string; +} + +/** + * Gemini文本转图像响应 + */ +export interface GeminiTextToImageResponse { + /** 响应码 */ + code: number; + /** 响应消息 */ + message: string; + /** 响应数据 */ + data: GeminiTextToImageData; + /** 是否成功 */ + successful: boolean; +} + +/** + * 文本转图像请求参数 + */ +export interface TextToImageRequest { + /** 图像描述 */ + description: string; +} + +/** + * 文本转图像响应数据 + */ +export interface TextToImageData { + /** 生成的图像URL */ + image_url: string; + /** 本地图像路径 */ + local_image_path: string; + /** 是否成功 */ + success: boolean; + /** 描述 */ + description: string; + /** 宽高比 */ + aspect_ratio: string; +} + +/** + * 文本转图像响应 + */ +export interface TextToImageResponse { + /** 响应码 */ + code: number; + /** 响应消息 */ + message: string; + /** 响应数据 */ + data: TextToImageData; + /** 是否成功 */ + successful: boolean; } diff --git a/api/movie_start.ts b/api/movie_start.ts index f8e9c37..1c53a8a 100644 --- a/api/movie_start.ts +++ b/api/movie_start.ts @@ -6,6 +6,10 @@ import { StoryAnalysisTask, MovieStoryTaskDetail, CreateMovieProjectV3Request, + GeminiTextToImageRequest, + GeminiTextToImageResponse, + TextToImageRequest, + TextToImageResponse, } from "./DTO/movie_start_dto"; import { get, post } from "./request"; import { @@ -78,3 +82,31 @@ export const getMovieStoryTask = async (taskId: string) => { `/movie_story/task/${taskId}` ); }; + +/** + * Gemini文本转图像生成 + * @param request - 文本转图像请求参数 + * @returns Promise + */ +export const generateGeminiTextToImage = async ( + request: GeminiTextToImageRequest +): Promise => { + return await post( + "/gemini-text-to-image/generate", + request + ); +}; + +/** + * 文本转图像生成 + * @param request - 文本转图像请求参数 + * @returns Promise + */ +export const generateTextToImage = async ( + request: TextToImageRequest +): Promise => { + return await post( + "/text-to-image/draw", + request + ); +}; diff --git a/app/service/Interaction/ImageStoryService.ts b/app/service/Interaction/ImageStoryService.ts index 297fe6b..9deaeee 100644 --- a/app/service/Interaction/ImageStoryService.ts +++ b/app/service/Interaction/ImageStoryService.ts @@ -103,6 +103,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { const [taskProgress, setTaskProgress] = useState(0); // 使用上传文件Hook const { uploadFile } = useUploadFile(); + const [taskId, setTaskId] = useState(""); /** 图片故事用例实例 */ const imageStoryUseCase = useMemo(() => new ImageStoryUseCase(), []); @@ -278,7 +279,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { // 调用用例处理图片上传和分析 const taskId = await imageStoryUseCase.handleImageUpload(activeImageUrl); - + setTaskId(taskId); for await (const result of await imageStoryUseCase.pollTaskStatus( taskId )) { @@ -465,8 +466,8 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { return { name: char.role_name, image_url: char.crop_url, - character_analysis: JSON.parse(char.whisk_caption) - .character_analysis, + character_analysis: char.role_name+":"+JSON.parse(char.whisk_caption) + ?.character_analysis?.brief, }; }); @@ -479,6 +480,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { character_briefs, language, image_url: activeImageUrl, + project_id:taskId }; // 调用create_movie_project_v2接口 diff --git a/app/service/Interaction/templateStoryService.ts b/app/service/Interaction/templateStoryService.ts index 4159746..d6ca392 100644 --- a/app/service/Interaction/templateStoryService.ts +++ b/app/service/Interaction/templateStoryService.ts @@ -1,6 +1,11 @@ import { message } from "antd"; import { StoryTemplateEntity } from "../domain/Entities"; import { useUploadFile } from "../domain/service"; +import { debounce } from "lodash"; +import { TemplateStoryUseCase } from "../usecase/templateStoryUseCase"; +import { useState, useCallback, useMemo } from "react"; +import { createMovieProjectV3, generateTextToImage } from "@/api/movie_start"; +import { CreateMovieProjectV3Request } from "@/api/DTO/movie_start_dto"; /** 模板角色接口 */ interface TemplateRole { @@ -11,11 +16,6 @@ interface TemplateRole { /** 声音URL */ voice_url: string; } -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 { /** 模板列表 */ @@ -42,9 +42,9 @@ interface UseTemplateStoryService { actionStory: ( user_id: string, mode: "auto" | "manual", - resolution: "720p" | "1080p" | "4k" , + resolution: "720p" | "1080p" | "4k", language: string - ) => Promise; + ) => Promise; /** 设置选中的模板 */ setSelectedTemplate: (template: StoryTemplateEntity | null) => void; /** 设置活跃角色索引 */ @@ -55,7 +55,13 @@ interface UseTemplateStoryService { /**清空数据 */ clearData: () => void; /** 上传人物头像并分析 */ - AvatarAndAnalyzeFeatures: (imageUrl: string) => Promise; + AvatarAndAnalyzeFeatures: (imageUrl: string, roleName?: string) => Promise; + /** 更新指定角色的图片 */ + updateRoleImage: (roleName: string, imageUrl: string) => void; + /** 更新变量字段值 */ + updateFillableContentField: (fieldName: string, fieldValue: string) => void; + /** 带防抖的失焦处理函数 */ + handleFieldBlur: (fieldName: string, fieldValue: string) => void; } export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { @@ -96,7 +102,7 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { setTemplateStoryList(templates); setSelectedTemplate(templates[0]); setActiveRoleIndex(0); - console.log(selectedTemplate, activeRoleIndex) + console.log(selectedTemplate, activeRoleIndex); } catch (err) { console.error("获取模板列表失败:", err); } finally { @@ -115,7 +121,7 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { * 设置当前活跃角色的图片URL */ const setActiveRoleData = useCallback( - (imageUrl: string, desc: string): void => { + (imageUrl: string): void => { if ( !selectedTemplate || activeRoleIndex < 0 || @@ -125,12 +131,11 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { return; } try { - - const character_brief = { - name: selectedTemplate.storyRole[activeRoleIndex].role_name, - image_url: imageUrl, - character_analysis: JSON.parse(desc).character_analysis, - }; + // const character_brief = { + // name: selectedTemplate.storyRole[activeRoleIndex].role_name, + // image_url: imageUrl, + // character_analysis: JSON.parse(desc).character_analysis, + // }; const updatedTemplate = { ...selectedTemplate, storyRole: selectedTemplate.storyRole.map((role, index) => @@ -138,7 +143,6 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { ? { ...role, photo_url: imageUrl, - role_description: character_brief, } : role ), @@ -177,22 +181,48 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { [selectedTemplate, activeRoleIndex] ); + /** + * 更新变量字段值 + */ + const updateFillableContentField = useCallback( + (fieldName: string, fieldValue: string): void => { + if (!selectedTemplate) return; + + const updatedTemplate = { + ...selectedTemplate, + fillable_content: selectedTemplate.fillable_content.map((field) => + field.field_name === fieldName + ? { ...field, field_value: fieldValue } + : field + ), + }; + setSelectedTemplate(updatedTemplate); + }, + [selectedTemplate] + ); + + /** * 上传人物头像并分析特征,替换旧的角色数据 - * @param {string} characterName - 角色名称 + * @param {string} imageUrl - 图片URL + * @param {string} roleName - 角色名称(可选,如果不提供则使用当前活跃角色) */ const AvatarAndAnalyzeFeatures = useCallback( - async (imageUrl: string): Promise => { + async (imageUrl: string, roleName?: string): Promise => { try { setIsLoading(true); - // 调用用例处理人物头像上传和特征分析 - const result = await templateStoryUseCase.AvatarAndAnalyzeFeatures( - imageUrl - ); + // 如果提供了角色名称,更新指定角色;否则更新当前活跃角色 + if (roleName) { + updateRoleImage(roleName, imageUrl); + } else { + setActiveRoleData(imageUrl); + } - setActiveRoleData(result.crop_url, result.whisk_caption); - console.log("人物头像和特征描述更新成功:", result); + // 调用用例处理人物头像上传和特征分析 + // const result = await templateStoryUseCase.AvatarAndAnalyzeFeatures( + // imageUrl + // ); } catch (error) { console.error("人物头像上传和特征分析失败:", error); throw error; @@ -200,8 +230,64 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { setIsLoading(false); } }, - [templateStoryUseCase] + [setActiveRoleData] ); + + /** + * 更新指定角色的图片 + * @param {string} roleName - 角色名称 + * @param {string} imageUrl - 新的图片URL + */ + const updateRoleImage = useCallback( + (roleName: string, imageUrl: string): void => { + if (!selectedTemplate) return; + + const updatedTemplate = { + ...selectedTemplate, + storyRole: selectedTemplate.storyRole.map((role) => + role.role_name === roleName + ? { ...role, photo_url: imageUrl } + : role + ), + }; + + setSelectedTemplate(updatedTemplate); + }, + [selectedTemplate] + ); + /** + * 带防抖的失焦处理函数 + * @param {string} fieldName - 字段名称 + * @param {string} fieldValue - 字段值 + */ + const handleFieldBlur = useCallback( + debounce(async (fieldName: string, fieldValue: string): Promise => { + try { + // 设置 loading 状态 + setIsLoading(true); + + // 调用图片生成接口 + const result = await generateTextToImage({ + description: fieldValue + }); + + if (result.successful && result.data?.image_url) { + // 更新对应角色的图片 + updateRoleImage(fieldName, result.data.image_url); + console.log(`字段 ${fieldName} 图片生成成功:`, result.data.image_url); + } else { + console.error(`字段 ${fieldName} 图片生成失败:`, result.message); + } + } catch (error) { + console.error(`字段 ${fieldName} 处理失败:`, error); + } finally { + // 清除 loading 状态 + setIsLoading(false); + } + }, 500), + [updateRoleImage, setIsLoading] + ); + const actionStory = useCallback( async ( user_id: string, @@ -209,20 +295,28 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { resolution: "720p" | "1080p" | "4k" = "720p", language: string = "English" ) => { + console.log('selectedTemplate', selectedTemplate) try { - const params: CreateMovieProjectV3Request = { - user_id, - mode, - resolution, - storyRole: selectedTemplate?.storyRole || [], - language, - template_id: selectedTemplate?.template_id || "", - }; + // 设置 loading 状态 + setIsLoading(true); - const result = await createMovieProjectV3(params); - return result.data.project_id as string; + const params: CreateMovieProjectV3Request = { + user_id, + mode, + resolution, + storyRole: selectedTemplate?.storyRole || [], + language, + template_id: selectedTemplate?.template_id || "", + fillable_content: selectedTemplate?.fillable_content || [], + }; + console.log("params", params); + const result = await createMovieProjectV3(params); + return result.data.project_id as string; } catch (error) { console.error("创建电影项目失败:", error); + } finally { + // 清除 loading 状态 + setIsLoading(false); } }, [selectedTemplate] @@ -239,6 +333,9 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { setActiveRoleIndex: handleSetActiveRoleIndex, setActiveRoleAudio, AvatarAndAnalyzeFeatures, + updateRoleImage, + updateFillableContentField, + handleFieldBlur, clearData: () => { setTemplateStoryList([]); setSelectedTemplate(null); diff --git a/app/service/domain/Entities.ts b/app/service/domain/Entities.ts index 322c1eb..5c8f186 100644 --- a/app/service/domain/Entities.ts +++ b/app/service/domain/Entities.ts @@ -88,7 +88,7 @@ export interface VideoSegmentEntity { video_status: number | null; }[]; /**视频片段状态 0:视频加载中 1:任务已完成 2:任务失败 */ - status: number|null; + status: number | null; /**镜头项 */ lens: LensType[]; } @@ -152,9 +152,23 @@ export interface StoryTemplateEntity { image_url: string; character_analysis: Record; }; + /**照片URL */ photo_url: string; /**声音URL */ voice_url: string; }[]; + /**可填的内容 */ + fillable_content: { + /** 字段名称 */ + field_name: string; + /** 字段类型 */ + field_type: string; + /** 字段值 */ + field_value?: string; + /** 字段描述 */ + field_description?: string; + /** 字段元数据 */ + field_meta?: Record; + }[]; } diff --git a/components/ChatInputBox/ChatInputBox.tsx b/components/ChatInputBox/ChatInputBox.tsx index 8055c03..79e65f8 100644 --- a/components/ChatInputBox/ChatInputBox.tsx +++ b/components/ChatInputBox/ChatInputBox.tsx @@ -18,14 +18,7 @@ import { Sparkles, Settings, } from "lucide-react"; -import { - Dropdown, - Modal, - Tooltip, - Upload, - Popconfirm, - Image, -} from "antd"; +import { Dropdown, Modal, Tooltip, Upload, Popconfirm, Image } from "antd"; import { UploadOutlined } from "@ant-design/icons"; import { StoryTemplateEntity } from "@/app/service/domain/Entities"; import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService"; @@ -72,6 +65,8 @@ const RenderTemplateStoryMode = ({ setActiveRoleIndex, AvatarAndAnalyzeFeatures, setActiveRoleAudio, + updateFillableContentField, + handleFieldBlur, clearData, } = useTemplateStoryServiceHook(); @@ -114,7 +109,12 @@ const RenderTemplateStoryMode = ({ console.error("用户未登录"); return; } - const projectId = await actionStory(String(User.id), configOptions.mode, configOptions.resolution, configOptions.language); + const projectId = await actionStory( + String(User.id), + configOptions.mode, + configOptions.resolution, + configOptions.language + ); if (projectId) { // 跳转到电影详情页 router.push(`/create/work-flow?episodeId=${projectId}`); @@ -204,9 +204,166 @@ const RenderTemplateStoryMode = ({ + {/* 变量字段填写区域 */} + {selectedTemplate?.fillable_content && + selectedTemplate.fillable_content.length > 0 && ( +
+

+ Template Configuration +

+
+ {selectedTemplate.fillable_content.map((field, index) => ( +
+ {/* 字段名称 */} +
+ {/* 图片缩略图 - 显示对应角色的图片 */} +
+ +
+ role.role_name === field.field_name + )?.photo_url || "/assets/empty_video.png" + } + alt={field.field_name} + className="w-full h-full object-cover" + preview={{ + mask: null, + maskClassName: "hidden", + }} + fallback="/assets/empty_video.png" + /> +
+
+ + {/* 上传按钮 - 右上角 */} + { + // 验证文件类型 + const isImage = file.type.startsWith("image/"); + if (!isImage) { + console.error("只能上传图片文件"); + return false; + } + + // 验证文件大小 (5MB) + const isLt5M = file.size / 1024 / 1024 < 5; + if (!isLt5M) { + console.error("图片大小不能超过5MB"); + return false; + } + + return true; + }} + customRequest={async ({ + file, + onSuccess, + onError, + }) => { + try { + const fileObj = file as File; + console.log( + "开始上传字段图片文件:", + fileObj.name, + fileObj.type, + fileObj.size + ); + + // 使用 hook 上传文件到七牛云 + const uploadedUrl = await uploadFile( + fileObj, + (progress) => { + console.log(`上传进度: ${progress}%`); + } + ); + console.log( + "字段图片上传成功,URL:", + uploadedUrl + ); + + // 调用 AvatarAndAnalyzeFeatures 更新对应角色的图片 + await AvatarAndAnalyzeFeatures( + uploadedUrl, + field.field_name + ); + + onSuccess?.(uploadedUrl); + } catch (error) { + console.error("字段图片上传失败:", error); + onError?.(error as Error); + } + }} + > + + +
+
+ + {/* 输入框 */} +
+ + updateFillableContentField( + field.field_name, + e.target.value + ) + } + onBlur={(e) => + handleFieldBlur(field.field_name, e.target.value) + } + placeholder={`${field.field_description}`} + className="w-full px-3 py-2 pr-10 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200" + /> + + {/* 问号提示 - 直接放在输入框内部右侧 */} + {field.field_description && ( + +
+ + ? + +
+
+ )} +
+
+ ))} +
+
+ )} {/* 角色自定义部分 - 精简布局 */} -
+ {/*

{/* 紧凑布局 */} -
+ {/*
{/* 左侧:音频部分 */} -
+ {/*
{/* 音频操作区域 - 使用新的 AudioRecorder 组件 */} -
+ {/*
{ @@ -233,7 +390,7 @@ const RenderTemplateStoryMode = ({
{/* 右侧:角色图片缩略图列表 - 精简 */} -
+ {/*
{selectedTemplate.storyRole.map((role, index: number) => (
@@ -260,7 +417,7 @@ const RenderTemplateStoryMode = ({ {/* 上传按钮 - 右上角 */} - + {/*
-
-
+
*/} +
0} handleCreateVideo={handleConfirm} @@ -371,7 +528,7 @@ const RenderTemplateStoryMode = ({

-
+
{templateListRender()}
{storyEditorRender()}