import { ImageStoryEntity } from "../domain/Entities"; import { useUploadFile } from "../domain/service"; import { ImageStoryUseCase } from "../usecase/imageStoryUseCase"; import { useState, useCallback, useMemo, Dispatch, SetStateAction, } from "react"; import { CharacterAnalysis, CreateMovieProjectV2Request, CreateMovieProjectResponse, } from "@/api/DTO/movie_start_dto"; import { MovieProjectService, MovieProjectMode } from "./MovieProjectService"; interface UseImageStoryService { /** 当前图片故事数据 */ imageStory: Partial; /** 当前活跃的图片地址 */ activeImageUrl: string; /** 故事内容(用户输入或AI分析结果) */ storyContent: string; /** 角色头像及名称数据 */ charactersAnalysis: CharacterAnalysis[]; /** 分类数据 */ potentialGenres: string[]; /** 当前选中的分类 */ selectedCategory: string; /** 是否正在加载中(上传或分析) */ isLoading: boolean; /** 是否已经分析过图片 */ hasAnalyzed: boolean; /** 计算后的角色头像数据 */ avatarComputed: Array<{ name: string; url: string }>; /** 原始用户描述 */ originalUserDescription: string; /** 分析任务进度 */ taskProgress: number; /** 上传图片并分析 */ uploadAndAnalyzeImage: () => Promise; /** 触发文件选择 */ triggerFileSelection: () => Promise; /** 更新故事类型 */ updateStoryType: (storyType: string) => void; /** 更新故事内容 */ updateStoryContent: (content: string) => void; /** 更新角色名称并同步到相关数据 */ updateCharacterName: (oldName: string, newName: string) => void; /** 同步角色名称到故事内容 */ syncRoleNameToContent: (oldName: string, newName: string) => void; /** 重置图片故事数据 */ resetImageStory: (showAnalysisState?: boolean) => void; /** 生成动作电影 */ actionMovie: ( user_id: string, mode?: "auto" | "manual", resolution?: "720p" | "1080p" | "4k", language?: string ) => Promise<{ project_id: string } | undefined>; /** 设置角色分析 */ setCharactersAnalysis: Dispatch>; /** 设置原始用户描述 */ setOriginalUserDescription: Dispatch>; /** 上传人物头像并分析特征,替换旧的角色数据 */ uploadCharacterAvatarAndAnalyzeFeatures: ( characterName: string ) => Promise; } export const useImageStoryServiceHook = (): UseImageStoryService => { // 基础状态 const [imageStory, setImageStory] = useState>({ imageUrl: "", storyType: "", }); // 图片相关状态 const [activeImageUrl, setActiveImageUrl] = useState(""); // 故事内容状态(统一管理用户输入和AI分析结果) const [storyContent, setStoryContent] = useState(""); // 原始用户描述 const [originalUserDescription, setOriginalUserDescription] = useState(""); // 分析结果状态 /** 角色头像及名称 */ const [charactersAnalysis, setCharactersAnalysis] = useState< CharacterAnalysis[] >([]); /** 分类数组 */ const [potentialGenres, setPotentialGenres] = useState([]); // 分类状态 const [selectedCategory, setSelectedCategory] = useState(""); // 流程状态 const [isLoading, setIsLoading] = useState(false); const [hasAnalyzed, setHasAnalyzed] = useState(false); /** 分析任务进度 */ const [taskProgress, setTaskProgress] = useState(0); // 使用上传文件Hook const { uploadFile } = useUploadFile(); const [taskId, setTaskId] = useState(""); /** 图片故事用例实例 */ const imageStoryUseCase = useMemo(() => new ImageStoryUseCase(), []); /** * 根据角色区域信息生成头像URL * @param character - 角色信息 * @param imageUrl - 源图片URL */ const generateAvatarFromRegion = useCallback( (character: CharacterAnalysis, imageUrl: string) => { if ( !character.region || !character.region.width || !character.region.height ) { return; } // 创建图片对象 const img = new Image(); img.crossOrigin = "anonymous"; // 处理跨域问题 img.onload = () => { try { // 根据百分比计算实际的像素坐标 const cropX = Math.round(character.region!.x * img.width); const cropY = Math.round(character.region!.y * img.height); const cropWidth = Math.round(character.region!.width * img.width); const cropHeight = Math.round(character.region!.height * img.height); console.log(cropX, cropY, cropWidth, cropHeight); // 验证裁剪区域是否有效 if (cropWidth <= 0 || cropHeight <= 0) { console.error("裁剪区域无效:", { cropWidth, cropHeight }); return; } if ( cropX + cropWidth > img.width || cropY + cropHeight > img.height ) { console.error("裁剪区域超出图片边界"); return; } // 创建canvas元素用于图片裁剪 const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (!ctx) { console.error("无法创建canvas上下文"); return; } // 设置canvas尺寸为裁剪后的尺寸 canvas.width = cropWidth; canvas.height = cropHeight; // 清除canvas内容 ctx.clearRect(0, 0, cropWidth, cropHeight); // 在canvas上绘制裁剪后的图片部分 ctx.drawImage( img, cropX, cropY, cropWidth, cropHeight, // 源图片裁剪区域 0, 0, cropWidth, cropHeight // 目标canvas区域 ); // 将canvas转换为blob并创建临时URL canvas.toBlob( (blob) => { if (blob) { const url = URL.createObjectURL(blob); console.log("成功生成头像URL:", url, "大小:", blob.size); // 更新角色头像URL setCharactersAnalysis((prev) => prev.map((char) => char.role_name === character.role_name ? { ...char, avatarUrl: url } : char ) ); } else { console.error("Canvas转Blob失败"); } }, "image/jpeg", 0.9 ); // 清理canvas canvas.remove(); } catch (error) { console.error("生成角色头像失败:", error); } }; img.onerror = () => { console.error("加载图片失败:", imageUrl); }; // 开始加载图片 img.src = imageUrl; }, [setCharactersAnalysis] ); /** * 根据角色框选数据计算头像URL * 从图片中裁剪出对应的角色头像部分 */ const avatarComputed = useMemo(() => { if (!activeImageUrl || charactersAnalysis.length === 0) { return []; } 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); // return { // name: character.role_name, // url: "", // 初始为空,异步生成完成后会更新 // }; }) .filter(Boolean) as { name: string; url: string }[]; }, [charactersAnalysis, activeImageUrl, generateAvatarFromRegion]); /** * 上传图片并分析 * @param {string} imageUrl - 已上传的图片URL */ 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 taskId = await imageStoryUseCase.handleImageUpload(activeImageUrl); setTaskId(taskId); 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); throw error; } finally { setIsLoading(false); } }, [imageStoryUseCase, activeImageUrl, storyContent]); /** * 更新故事类型 * @param {string} storyType - 新的故事类型 */ const updateStoryType = useCallback( (storyType: string): void => { imageStoryUseCase.updateStoryType(storyType); setImageStory((prev) => ({ ...prev, storyType })); setSelectedCategory(storyType); }, [imageStoryUseCase] ); /** * 更新故事内容 * @param {string} content - 新的故事内容 */ const updateStoryContent = useCallback( (content: string): void => { setStoryContent(content); imageStoryUseCase.updateStoryContent(content); }, [imageStoryUseCase] ); /** * 同步角色名称到故事内容 * @param {string} oldName - 旧的角色名称 * @param {string} newName - 新的角色名称 */ const syncRoleNameToContent = useCallback( (oldName: string, newName: string) => { // 更新故事内容中的角色标签 setStoryContent((prev) => { // 匹配新的角色标签格式 Dezhong Huang const regex = new RegExp(`]*>${oldName}<\/role>`, "g"); const content = prev.replace(regex, `${newName}`); imageStoryUseCase.updateStoryContent(content); return content; }); }, [imageStoryUseCase] ); /** * 更新角色名称并同步到相关数据 * @param {string} oldName - 旧的角色名称 * @param {string} newName - 新的角色名称 */ const updateCharacterName = useCallback( (oldName: string, newName: string): void => { // 更新角色分析数据中的名称 setCharactersAnalysis((prev) => prev.map((char) => char.role_name === oldName ? { ...char, role_name: newName } : char ) ); // 同步更新故事内容中的角色名称 syncRoleNameToContent(oldName, newName); }, [syncRoleNameToContent] ); /** * 重置图片故事数据 */ const resetImageStory = useCallback((): void => { imageStoryUseCase.resetImageStory(); // 清理生成的头像URL,避免内存泄漏 setCharactersAnalysis([]); // 重置所有状态 setImageStory({ imageUrl: "", storyType: "", }); setActiveImageUrl(""); updateStoryContent(""); setPotentialGenres([]); setSelectedCategory(""); setHasAnalyzed(false); setIsLoading(false); setOriginalUserDescription(""); }, [imageStoryUseCase]); /** * 触发文件选择并自动分析 */ const triggerFileSelection = useCallback(async (): Promise => { return new Promise((resolve, reject) => { // 创建文件输入元素 const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = "image/*"; fileInput.style.display = "none"; fileInput.onchange = async (e) => { try { const target = e.target as HTMLInputElement; if (target.files && target.files[0]) { setIsLoading(true); // 使用传入的文件上传函数 const uploadedImageUrl = await uploadFile( target.files[0], (progress) => { console.log("上传进度:", progress); } ); // 设置图片URL setActiveImageUrl(uploadedImageUrl); setImageStory((prev) => ({ ...prev, imageUrl: uploadedImageUrl, })); } resolve(); } catch (error) { reject(error); } finally { setIsLoading(false); // 清理DOM document.body.removeChild(fileInput); } }; fileInput.oncancel = () => { document.body.removeChild(fileInput); reject(); }; document.body.appendChild(fileInput); fileInput.click(); }); }, [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) => { return { name: char.role_name, image_url: char.crop_url, character_analysis: char.role_name+":"+JSON.parse(char.whisk_caption) ?.character_analysis?.brief, }; }); const params: CreateMovieProjectV2Request = { script: storyContent, user_id, mode, resolution, genre: selectedCategory, character_briefs, language, image_url: activeImageUrl, project_id:taskId }; // 调用create_movie_project_v2接口 const result = await MovieProjectService.createProject( MovieProjectMode.IMAGE, params ); return { project_id: result.project_id }; } } catch (error) { console.error("创建电影项目失败:", error); } }, [ hasAnalyzed, storyContent, charactersAnalysis, selectedCategory, activeImageUrl, ] ); /** * 上传人物头像并分析特征,替换旧的角色数据 * @param {string} characterName - 角色名称 */ const uploadCharacterAvatarAndAnalyzeFeatures = useCallback( async (characterName: string): Promise => { try { setIsLoading(true); // 调用用例处理人物头像上传和特征分析 const result = await imageStoryUseCase.uploadCharacterAvatarAndAnalyzeFeatures( uploadFile ); // 用新的头像和特征描述替换旧的角色数据 setCharactersAnalysis((prev) => prev.map((char) => char.role_name === characterName ? { ...char, crop_url: result.crop_url, whisk_caption: result.whisk_caption, } : char ) ); console.log("人物头像和特征描述更新成功:", result); } catch (error) { console.error("人物头像上传和特征分析失败:", error); throw error; } finally { setIsLoading(false); } }, [imageStoryUseCase, uploadFile] ); return { imageStory, activeImageUrl, storyContent, charactersAnalysis, potentialGenres, selectedCategory, isLoading, hasAnalyzed, avatarComputed, originalUserDescription, taskProgress, setCharactersAnalysis, uploadAndAnalyzeImage, triggerFileSelection, updateStoryType, updateStoryContent, updateCharacterName, syncRoleNameToContent, resetImageStory, setOriginalUserDescription, actionMovie, uploadCharacterAvatarAndAnalyzeFeatures, }; };