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 } from "@/api/DTO/movie_start_dto"; 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; /** 上传图片并分析 */ uploadAndAnalyzeImage: () => Promise; /** 触发文件选择 */ triggerFileSelection: () => Promise; /** 触发生成剧本函数 */ generateScript: () => 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: () => Promise; /** 设置角色分析 */ setCharactersAnalysis: Dispatch>; /** 设置原始用户描述 */ setOriginalUserDescription: Dispatch>; } 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); // 使用上传文件Hook const { uploadFile } = useUploadFile(); /** 图片故事用例实例 */ 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) => { // 如果已经有头像URL,直接返回 if (character.avatarUrl) { return { name: character.role_name, url: character.avatarUrl, }; } // 异步生成头像URL generateAvatarFromRegion(character, activeImageUrl); return { name: character.role_name, url: "", // 初始为空,异步生成完成后会更新 }; }); }, [charactersAnalysis, activeImageUrl, generateAvatarFromRegion]); /** * 上传图片并分析 * @param {string} imageUrl - 已上传的图片URL */ const uploadAndAnalyzeImage = useCallback(async (): Promise => { try { setIsLoading(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); // 将AI分析的故事内容直接更新到统一的故事内容字段 updateStoryContent(updatedStory || ""); // 标记已分析 setHasAnalyzed(true); } catch (error) { console.error("图片上传分析失败:", error); setHasAnalyzed(false); throw error; } finally { setIsLoading(false); } }, [ activeImageUrl, imageStoryUseCase, storyContent, setOriginalUserDescription, ]); /** * 触发生成剧本函数 * @returns {Promise} 生成的剧本ID或内容 */ const generateScript = useCallback(async (): Promise => { if (!activeImageUrl) { throw new Error("请先上传图片"); } const finalStoryContent = storyContent; if (!finalStoryContent.trim()) { throw new Error("请先输入或生成故事内容"); } try { setIsLoading(true); // 这里可以调用后端API生成剧本 // 暂时返回一个模拟的剧本ID const scriptId = `script_${Date.now()}`; // TODO: 实现实际的剧本生成逻辑 // const response = await generateScriptFromImage(imageStory); // return response.scriptId; return scriptId; } catch (error) { console.error("生成剧本失败:", error); throw error; } finally { setIsLoading(false); } }, [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((prev) => { prev.forEach((char) => { if (char.avatarUrl) { URL.revokeObjectURL(char.avatarUrl); } }); return []; }); // 重置所有状态 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 (): Promise => { try { if (hasAnalyzed) { const params = { content: storyContent, category: selectedCategory, }; } } catch (error) { console.error("图片上传分析失败:", error); } }, [activeImageUrl, imageStoryUseCase]); return { imageStory, activeImageUrl, storyContent, charactersAnalysis, potentialGenres, selectedCategory, isLoading, hasAnalyzed, avatarComputed, originalUserDescription, setCharactersAnalysis, uploadAndAnalyzeImage, triggerFileSelection, generateScript, updateStoryType, updateStoryContent, updateCharacterName, syncRoleNameToContent, resetImageStory, setOriginalUserDescription, actionMovie, }; };