import { ImageStoryEntity } from "../domain/Entities"; import { useUploadFile } from "../domain/service"; import { ImageStoryUseCase } from "../usecase/imageStoryUseCase"; import { useState, useCallback, useMemo } 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 }>; /** 上传图片并分析 */ uploadAndAnalyzeImage: (imageUrl: string) => Promise; /** 触发文件选择并自动分析 */ triggerFileSelectionAndAnalyze: () => Promise; /** 触发生成剧本函数 */ generateScript: () => Promise; /** 更新故事类型 */ updateStoryType: (storyType: string) => void; /** 更新故事内容 */ updateStoryContent: (content: string) => void; /** 更新角色名称并同步到相关数据 */ updateCharacterName: (oldName: string, newName: string) => void; /** 重置图片故事数据 */ resetImageStory: () => void; /** 完全重置到初始状态(包括预置数据) */ resetToInitialState: () => void; } export const useImageStoryServiceHook = (): UseImageStoryService => { // 基础状态 const [imageStory, setImageStory] = useState>({ imageUrl: "", storyType: "auto", }); // 图片相关状态 const [activeImageUrl, setActiveImageUrl] = useState("https://cdn.qikongjian.com/1755316261_gmw4yq.mp4?vframe/jpg/offset/1"); // 故事内容状态(统一管理用户输入和AI分析结果),预置假数据 const [storyContent, setStoryContent] = useState( "在一个遥远的未来城市,机器人与人类共存,但一场突如其来的危机打破了平衡。艾米丽,阿尔法,博士互相帮助,共同解决危机。" ); // 分析结果状态 /** 角色头像及名称,预置假数据 */ const [charactersAnalysis, setCharactersAnalysis] = useState< CharacterAnalysis[] >([ { role_name: "艾米丽", region: { x: 0.2, y: 0.2, width: 0.2, height: 0.2, }, id: "1" }, { role_name: "阿尔法", region: { x: 0.4, y: 0.4, width: 0.2, height: 0.2, }, id: "2" }, { role_name: "博士", region: { x: 0.6, y: 0.6, width: 0.2, height: 0.2, }, id: "3" }, ]); /** 分类数组,预置假数据 */ const [potentialGenres, setPotentialGenres] = useState([ "科幻", "冒险", "悬疑", ]); // 分类状态 const [selectedCategory, setSelectedCategory] = useState("Auto"); // 流程状态 const [isLoading, setIsLoading] = useState(false); const [hasAnalyzed, setHasAnalyzed] = useState(true); // 使用上传文件Hook const { uploadFile } = useUploadFile(); /** 图片故事用例实例 */ const imageStoryUseCase = useMemo(() => new ImageStoryUseCase(), []); /** * 根据角色区域信息生成头像URL * @param character - 角色信息 * @param imageUrl - 源图片URL */ const generateAvatarFromRegion = useCallback((character: CharacterAnalysis, imageUrl: string) => { // 创建图片对象 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 (imageUrl: string): Promise => { try { setIsLoading(true); // 调用用例处理图片上传和分析 await imageStoryUseCase.handleImageUpload(imageUrl); // 获取更新后的数据 const updatedStory = imageStoryUseCase.getStoryLogline(); const updatedCharacters = imageStoryUseCase.getCharactersAnalysis(); const updatedGenres = imageStoryUseCase.getPotentialGenres(); const updatedImageStory = imageStoryUseCase.getImageStory(); // 更新所有响应式状态 setCharactersAnalysis(updatedCharacters); setPotentialGenres(updatedGenres); setImageStory(updatedImageStory); // 将AI分析的故事内容直接更新到统一的故事内容字段 setStoryContent(updatedStory || ""); // 设置第一个分类为默认选中 if (updatedGenres.length > 0) { setSelectedCategory(updatedGenres[0]); } // 标记已分析 setHasAnalyzed(true); } catch (error) { console.error("图片上传分析失败:", error); throw error; } finally { setIsLoading(false); } }, [imageStoryUseCase] ); /** * 触发生成剧本函数 * @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); }, []); /** * 更新角色名称并同步到相关数据 * @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 ) ); // 同步更新故事内容中的角色名称 setStoryContent(prev => { // 使用正则表达式进行全局替换,确保大小写匹配 const regex = new RegExp(`\\b${oldName}\\b`, 'g'); return prev.replace(regex, newName); }); }, []); /** * 重置图片故事数据 */ const resetImageStory = useCallback((): void => { imageStoryUseCase.resetImageStory(); // 清理生成的头像URL,避免内存泄漏 setCharactersAnalysis(prev => { prev.forEach(char => { if (char.avatarUrl) { URL.revokeObjectURL(char.avatarUrl); } }); return []; }); // 重置所有状态 setImageStory({ imageUrl: "", storyType: "auto", }); setActiveImageUrl(""); setStoryContent(""); setPotentialGenres([]); setSelectedCategory("auto"); setHasAnalyzed(false); setIsLoading(false); }, [imageStoryUseCase]); /** * 完全重置到初始状态(包括预置数据) */ const resetToInitialState = useCallback((): void => { // 清理生成的头像URL,避免内存泄漏 setCharactersAnalysis(prev => { prev.forEach(char => { if (char.avatarUrl) { URL.revokeObjectURL(char.avatarUrl); } }); return []; }); // 重置所有状态到初始值 setImageStory({ imageUrl: "", storyType: "auto", }); setActiveImageUrl("https://cdn.qikongjian.com/1755316261_gmw4yq.mp4?vframe/jpg/offset/1"); setStoryContent("在一个遥远的未来城市,机器人与人类共存,但一场突如其来的危机打破了平衡。艾米丽,阿尔法,博士互相帮助,共同解决危机。"); setCharactersAnalysis([ { role_name: "艾米丽", region: { x: 20, y: 20, width: 20, height: 20, }, id: "1" }, { role_name: "阿尔法", region: { x: 40, y: 40, width: 20, height: 20, }, id: "2" }, { role_name: "博士", region: { x: 60, y: 60, width: 20, height: 20, }, id: "3" }, ]); setPotentialGenres(["科幻", "冒险", "悬疑"]); setSelectedCategory("auto"); setHasAnalyzed(true); setIsLoading(false); }, []); /** * 触发文件选择并自动分析 */ const triggerFileSelectionAndAnalyze = 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, })); // 自动开始分析 await uploadAndAnalyzeImage(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, uploadAndAnalyzeImage]); return { imageStory, activeImageUrl, storyContent, charactersAnalysis, potentialGenres, selectedCategory, isLoading, hasAnalyzed, avatarComputed, uploadAndAnalyzeImage, triggerFileSelectionAndAnalyze, generateScript, updateStoryType, updateStoryContent, updateCharacterName, resetImageStory, resetToInitialState, }; };