From 85687f5840aa06ed2e59a1cf134d623bdca42b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=B7=E9=BE=99?= Date: Thu, 21 Aug 2025 23:46:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E7=9A=84=E9=98=B6=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/DTO/movie_start_dto.ts | 35 +++ api/movie_start.ts | 21 +- app/service/Interaction/ImageStoryService.ts | 218 ++++++++++++------- app/service/usecase/imageStoryUseCase.ts | 70 ++++-- components/ChatInputBox/ChatInputBox.tsx | 70 +++--- 5 files changed, 271 insertions(+), 143 deletions(-) diff --git a/api/DTO/movie_start_dto.ts b/api/DTO/movie_start_dto.ts index 5bac19c..55a5ce5 100644 --- a/api/DTO/movie_start_dto.ts +++ b/api/DTO/movie_start_dto.ts @@ -45,6 +45,41 @@ export interface MovieStartDTO { /** 错误信息 */ error: string | null; } + +/** 电影故事任务详情 */ +export interface MovieStoryTaskDetail { + /** 任务ID */ + task_id: string; + /** 状态 */ + status: string; + /** 进度 */ + progress: number; + /** 当前步骤 */ + current_step: string; + /** 步骤消息 */ + step_message: string; + /** 已用时间 */ + elapsed_time: number; + /** 预计剩余时间 */ + estimated_remaining: number | null; + /** 错误信息 */ + error_message: string | null; + /** 结果 */ + result: MovieStartDTO; +} + +/**图片分析出故事的任务相关数据,用于轮询查状态 */ +export interface StoryAnalysisTask{ + /** 任务ID */ + task_id:string; + /** 状态 */ + status:string; + /** 消息 */ + message:string; + /** 预计时长 */ + estimated_duration:number; +} + /** * 创建电影项目V2请求参数 照片生成电影 */ diff --git a/api/movie_start.ts b/api/movie_start.ts index 13509c3..c3b9e8c 100644 --- a/api/movie_start.ts +++ b/api/movie_start.ts @@ -1,5 +1,11 @@ import { ApiResponse } from "./common"; -import { CreateMovieProjectV2Request, CreateMovieProjectResponse, MovieStartDTO } from "./DTO/movie_start_dto"; +import { + CreateMovieProjectV2Request, + CreateMovieProjectResponse, + MovieStartDTO, + StoryAnalysisTask, + MovieStoryTaskDetail, +} from "./DTO/movie_start_dto"; import { get, post } from "./request"; import { StoryTemplateEntity, @@ -30,7 +36,7 @@ export const AIGenerateImageStory = async (request: { image_url: string; user_text: string; }) => { - return await post>( + return await post>( "/movie_story/generate", request ); @@ -63,3 +69,14 @@ export const createMovieProjectV3 = async ( request ); }; + +/** + * 获取电影故事任务详情 + * @param taskId - 任务ID + * @returns Promise> + */ +export const getMovieStoryTask = async (taskId: string) => { + return await get>( + `/movie_story/task/${taskId}` + ); +}; diff --git a/app/service/Interaction/ImageStoryService.ts b/app/service/Interaction/ImageStoryService.ts index 19ed29e..297fe6b 100644 --- a/app/service/Interaction/ImageStoryService.ts +++ b/app/service/Interaction/ImageStoryService.ts @@ -8,7 +8,11 @@ import { Dispatch, SetStateAction, } from "react"; -import { CharacterAnalysis, CreateMovieProjectV2Request, CreateMovieProjectResponse } from "@/api/DTO/movie_start_dto"; +import { + CharacterAnalysis, + CreateMovieProjectV2Request, + CreateMovieProjectResponse, +} from "@/api/DTO/movie_start_dto"; import { createMovieProjectV2 } from "@/api/movie_start"; interface UseImageStoryService { @@ -32,6 +36,8 @@ interface UseImageStoryService { avatarComputed: Array<{ name: string; url: string }>; /** 原始用户描述 */ originalUserDescription: string; + /** 分析任务进度 */ + taskProgress: number; /** 上传图片并分析 */ uploadAndAnalyzeImage: () => Promise; /** 触发文件选择 */ @@ -52,13 +58,15 @@ interface UseImageStoryService { mode?: "auto" | "manual", resolution?: "720p" | "1080p" | "4k", language?: string - ) => Promise; + ) => Promise; /** 设置角色分析 */ setCharactersAnalysis: Dispatch>; /** 设置原始用户描述 */ setOriginalUserDescription: Dispatch>; /** 上传人物头像并分析特征,替换旧的角色数据 */ - uploadCharacterAvatarAndAnalyzeFeatures: (characterName: string) => Promise; + uploadCharacterAvatarAndAnalyzeFeatures: ( + characterName: string + ) => Promise; } export const useImageStoryServiceHook = (): UseImageStoryService => { @@ -91,7 +99,8 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { // 流程状态 const [isLoading, setIsLoading] = useState(false); const [hasAnalyzed, setHasAnalyzed] = useState(false); - + /** 分析任务进度 */ + const [taskProgress, setTaskProgress] = useState(0); // 使用上传文件Hook const { uploadFile } = useUploadFile(); @@ -217,24 +226,26 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { return []; } - return charactersAnalysis.map((character) => { - console.log('character', character) - // 如果已经有头像URL,直接返回 - if (character.crop_url) { - return { - name: character.role_name, - url: character.crop_url, - }; - } + return charactersAnalysis + .map((character) => { + console.log("character", character); + // 如果已经有头像URL,直接返回 + if (character.crop_url) { + return { + name: character.role_name, + url: character.crop_url, + }; + } - // // 异步生成头像URL - // generateAvatarFromRegion(character, activeImageUrl); + // // 异步生成头像URL + // generateAvatarFromRegion(character, activeImageUrl); - // return { - // name: character.role_name, - // url: "", // 初始为空,异步生成完成后会更新 - // }; - }).filter(Boolean) as { name: string; url: string }[]; + // return { + // name: character.role_name, + // url: "", // 初始为空,异步生成完成后会更新 + // }; + }) + .filter(Boolean) as { name: string; url: string }[]; }, [charactersAnalysis, activeImageUrl, generateAvatarFromRegion]); /** * 上传图片并分析 @@ -243,28 +254,54 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { const uploadAndAnalyzeImage = useCallback(async (): Promise => { try { setIsLoading(true); + setTaskProgress(1); + + const setData = () => { + setOriginalUserDescription(storyContent); + // 获取更新后的数据 + const updatedStory = imageStoryUseCase.storyLogline; + const updatedCharacters = imageStoryUseCase.charactersAnalysis; + const updatedGenres = imageStoryUseCase.potentialGenres; + const updatedImageStory = imageStoryUseCase.imageStory; + setSelectedCategory(imageStoryUseCase.potentialGenres[0]); + // 更新所有响应式状态 + setCharactersAnalysis(updatedCharacters); + setPotentialGenres(updatedGenres); + setImageStory(updatedImageStory); + + // 将AI分析的故事内容直接更新到统一的故事内容字段 + updateStoryContent(updatedStory || ""); + + // 标记已分析 + setHasAnalyzed(true); + }; // 调用用例处理图片上传和分析 - const newImageStory = await imageStoryUseCase.handleImageUpload( - activeImageUrl - ); - setOriginalUserDescription(storyContent); - // 获取更新后的数据 - const updatedStory = imageStoryUseCase.storyLogline; - const updatedCharacters = imageStoryUseCase.charactersAnalysis; - const updatedGenres = imageStoryUseCase.potentialGenres; - const updatedImageStory = imageStoryUseCase.imageStory; - setSelectedCategory(imageStoryUseCase.potentialGenres[0]); - // 更新所有响应式状态 - setCharactersAnalysis(updatedCharacters); - setPotentialGenres(updatedGenres); - setImageStory(updatedImageStory); + const taskId = await imageStoryUseCase.handleImageUpload(activeImageUrl); - // 将AI分析的故事内容直接更新到统一的故事内容字段 - updateStoryContent(updatedStory || ""); - - // 标记已分析 - setHasAnalyzed(true); + for await (const result of await imageStoryUseCase.pollTaskStatus( + taskId + )) { + setTaskProgress(result.progress); + switch (result.status) { + case "submitted": + break; + case "processing": + setData(); + break; + case "completed": + setData(); + setHasAnalyzed(true); + setTaskProgress(0); + return + case "failed": + setHasAnalyzed(false); + setTaskProgress(0); + return + default: + break; + } + } } catch (error) { console.error("图片上传分析失败:", error); setHasAnalyzed(false); @@ -275,7 +312,6 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { } }, [imageStoryUseCase, activeImageUrl, storyContent]); - /** * 更新故事类型 * @param {string} storyType - 新的故事类型 @@ -415,56 +451,67 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { }); }, [uploadFile]); - const actionMovie = useCallback(async ( - user_id: string, - mode: "auto" | "manual" = "auto", - resolution: "720p" | "1080p" | "4k" = "720p", - language: string = "English" - ) => { - try { - if (hasAnalyzed) { - // 从charactersAnalysis中提取whisk_caption字段组成数组 - const character_briefs = charactersAnalysis.map(char => { + const actionMovie = useCallback( + async ( + user_id: string, + mode: "auto" | "manual" = "auto", + resolution: "720p" | "1080p" | "4k" = "720p", + language: string = "English" + ) => { + try { + if (hasAnalyzed) { + // 从charactersAnalysis中提取whisk_caption字段组成数组 + const character_briefs = charactersAnalysis.map((char) => { + return { + name: char.role_name, + image_url: char.crop_url, + character_analysis: JSON.parse(char.whisk_caption) + .character_analysis, + }; + }); - return { - name:char.role_name, - image_url:char.crop_url, - character_analysis:JSON.parse(char.whisk_caption).character_analysis - } - }); + const params: CreateMovieProjectV2Request = { + script: storyContent, + user_id, + mode, + resolution, + genre: selectedCategory, + character_briefs, + language, + image_url: activeImageUrl, + }; - const params: CreateMovieProjectV2Request = { - script: storyContent, - user_id, - mode, - resolution, - genre: selectedCategory, - character_briefs, - language, - image_url: activeImageUrl, - }; - - // 调用create_movie_project_v2接口 - const result = await createMovieProjectV2(params) - return result.data; + // 调用create_movie_project_v2接口 + const result = await createMovieProjectV2(params); + return result.data; + } + } catch (error) { + console.error("创建电影项目失败:", error); } - } catch (error) { - console.error("创建电影项目失败:", error); - } - }, [hasAnalyzed, storyContent, charactersAnalysis, selectedCategory, activeImageUrl]); + }, + [ + hasAnalyzed, + storyContent, + charactersAnalysis, + selectedCategory, + activeImageUrl, + ] + ); - /** - * 上传人物头像并分析特征,替换旧的角色数据 - * @param {string} characterName - 角色名称 - */ - const uploadCharacterAvatarAndAnalyzeFeatures = useCallback(async (characterName: string): Promise => { + /** + * 上传人物头像并分析特征,替换旧的角色数据 + * @param {string} characterName - 角色名称 + */ + const uploadCharacterAvatarAndAnalyzeFeatures = useCallback( + async (characterName: string): Promise => { try { setIsLoading(true); // 调用用例处理人物头像上传和特征分析 - const result = await imageStoryUseCase.uploadCharacterAvatarAndAnalyzeFeatures( - uploadFile - ); + const result = + await imageStoryUseCase.uploadCharacterAvatarAndAnalyzeFeatures( + uploadFile + ); // 用新的头像和特征描述替换旧的角色数据 setCharactersAnalysis((prev) => @@ -473,7 +520,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { ? { ...char, crop_url: result.crop_url, - whisk_caption: result.whisk_caption + whisk_caption: result.whisk_caption, } : char ) @@ -486,7 +533,9 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { } finally { setIsLoading(false); } - }, [imageStoryUseCase, uploadFile]); + }, + [imageStoryUseCase, uploadFile] + ); return { imageStory, @@ -499,6 +548,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { hasAnalyzed, avatarComputed, originalUserDescription, + taskProgress, setCharactersAnalysis, uploadAndAnalyzeImage, triggerFileSelection, diff --git a/app/service/usecase/imageStoryUseCase.ts b/app/service/usecase/imageStoryUseCase.ts index 65119ae..6ce85ef 100644 --- a/app/service/usecase/imageStoryUseCase.ts +++ b/app/service/usecase/imageStoryUseCase.ts @@ -1,5 +1,5 @@ import { ImageStoryEntity } from "../domain/Entities"; -import { AIGenerateImageStory } from "@/api/movie_start"; +import { AIGenerateImageStory, getMovieStoryTask } from "@/api/movie_start"; import { MovieStartDTO, CharacterAnalysis } from "@/api/DTO/movie_start_dto"; import { generateCharacterBrief } from "@/api/video_flow"; @@ -32,6 +32,7 @@ export class ImageStoryUseCase { /** 是否正在上传 */ isUploading: boolean = false; + constructor() {} /** @@ -88,10 +89,9 @@ export class ImageStoryUseCase { /** * 使用AI分析图片 - * @returns {Promise} + * @returns {Promise} */ async analyzeImageWithAI() { - console.log("this.imageStory.imageUrl", this.imageStory.imageUrl); try { // // 调用AI分析接口 @@ -100,27 +100,53 @@ export class ImageStoryUseCase { user_text: this.imageStory.imageStory || "", }); - if (response.successful && response.data) { - // ! 后端实际返回的是对象 但是由于前端只是做字符串数据的转交,所以这里就处理成字符串 - // ! 至于为什么这里是前端来处理,因为后端这个数据,很多时候都说要以对象方式使用,唯独给AI时,是字符串 - // ! 然后后端就不处理这个东西了,就给前端来处理了,真 懒 - response.data.characters_analysis.forEach((character) => { - character.whisk_caption = JSON.stringify(character.whisk_caption); - }); - // 解析并存储新的数据结构 - this.parseAndStoreAnalysisData(response.data); - - // 组合成ImageStoryEntity - this.composeImageStoryEntity(response.data); - return this.imageStory; - } else { - throw new Error("AI分析失败"); - } + return response.data.task_id; } catch (error) { console.error("AI分析失败:", error); throw error; } } + /** + * 轮询查状态 + * @param taskId - 任务ID + * @param interval - 轮询间隔时间 + */ + async pollTaskStatus(taskId: string, interval: number = 1000) { + // 好老套方案,但是有效 + let self = this; + return { + async *[Symbol.asyncIterator]() { + while (true) { + + const response = await getMovieStoryTask(taskId); + console.log("taskId", taskId,response); + if (response.successful && response.data) { + if (response.data.result) { + response.data.result.characters_analysis?.forEach((character) => { + character.whisk_caption = JSON.stringify( + character.whisk_caption + ); + }); + // 解析并存储新的数据结构 + self.parseAndStoreAnalysisData(response.data.result); + + // 组合成ImageStoryEntity + self.composeImageStoryEntity(response.data.result); + } + + yield { + status: response.data.status, + imageStory: self.imageStory, + progress: response.data.progress, + }; + } else { + throw new Error("AI分析失败"); + } + await new Promise((resolve) => setTimeout(resolve, interval)); + } + }, + }; + } /** * 解析并存储分析数据到类属性中 @@ -159,7 +185,7 @@ export class ImageStoryUseCase { this.setImageStory({ ...this.imageStory, imageAnalysis: data.story_logline || "", - storyType: data.potential_genres[0] || "", // 使用第一个分类作为故事类型 + storyType: data.potential_genres?.[0] || "", // 使用第一个分类作为故事类型 roleImage, }); } @@ -271,7 +297,9 @@ export class ImageStoryUseCase { // 3. 返回新的头像URL和特征描述,用于替换旧数据 const result = { crop_url: imageUrl, - whisk_caption: JSON.stringify(analysisResult.data.character_brief), + whisk_caption: JSON.stringify( + analysisResult.data.character_brief + ), }; // 清理临时元素 diff --git a/components/ChatInputBox/ChatInputBox.tsx b/components/ChatInputBox/ChatInputBox.tsx index b24f48c..b6b080f 100644 --- a/components/ChatInputBox/ChatInputBox.tsx +++ b/components/ChatInputBox/ChatInputBox.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { ChevronDown, ChevronUp, @@ -819,6 +819,7 @@ const PhotoStoryModal = ({ selectedCategory, isLoading, hasAnalyzed, + taskProgress, updateStoryType, updateStoryContent, updateCharacterName, @@ -839,6 +840,11 @@ const PhotoStoryModal = ({ onClose(); }; const router = useRouter(); + const taskProgressRef = useRef(taskProgress); + + useEffect(() => { + taskProgressRef.current = taskProgress; + }, [taskProgress]); // 处理图片上传 const handleImageUpload = async (e: any) => { const target = e.target as HTMLImageElement; @@ -889,9 +895,10 @@ const PhotoStoryModal = ({ let timeout = 100; let timer: NodeJS.Timeout; timer = setInterval(() => { + const currentProgress = taskProgressRef.current; setLocalLoading((prev) => { - if (prev >= 95) { - return 95; + if (prev >= currentProgress && currentProgress != 0) { + return currentProgress; } return prev + 0.1; }); @@ -899,17 +906,8 @@ const PhotoStoryModal = ({ try { await uploadAndAnalyzeImage(); } finally { - timeout = 10; clearInterval(timer); - timer = setInterval(() => { - setLocalLoading((prev) => { - if (prev >= 100) { - clearInterval(timer); - return 0; - } - return prev + 1; - }); - }, timeout); + setLocalLoading(0); } }; @@ -942,22 +940,22 @@ const PhotoStoryModal = ({
{/* 左侧:图片上传 */}
-
{activeImageUrl ? (
- Story inspiration + Story inspiration -
- {avatar.name} { - // 如果裁剪的头像加载失败,回退到原图 - const target = e.target as HTMLImageElement; - target.src = activeImageUrl; - }} - /> +
+ {avatar.name} { + // 如果裁剪的头像加载失败,回退到原图 + const target = e.target as HTMLImageElement; + target.src = activeImageUrl; + }} + /> {/* 删除角色按钮 - 使用Tooltip并调整z-index避免被遮挡 */}