diff --git a/api/DTO/movie_start_dto.ts b/api/DTO/movie_start_dto.ts new file mode 100644 index 0000000..299f01b --- /dev/null +++ b/api/DTO/movie_start_dto.ts @@ -0,0 +1,43 @@ +/** + * 图片故事AI分析返回DTO + */ +export interface CharacterRegion { + /** x坐标 */ + x: number; + /** y坐标 */ + y: number; + /** 区域宽度 */ + width: number; + /** 区域高度 */ + height: number; +} + +/** 角色分析信息 */ +export interface CharacterAnalysis { + /** 角色ID */ + id: string; + /** 角色名称 */ + role_name: string; + /** 角色区域 */ + region: CharacterRegion; + /** 角色头像URL(可选,用于存储裁剪后的头像) */ + avatarUrl?: string; +} + +/** 图片故事AI分析返回结构 */ +export interface MovieStartDTO { + /** 是否成功 */ + success: boolean; + /** 故事梗概 */ + story_logline: string; + /** 分类数据 */ + potential_genres: string[]; + /** 角色头像及名称 */ + characters_analysis: CharacterAnalysis[]; + /** 图片URL */ + image_url: string; + /** 用户输入文本 */ + user_text: string; + /** 错误信息 */ + error: string | null; +} diff --git a/api/movie_start.ts b/api/movie_start.ts index 99c2423..1753b88 100644 --- a/api/movie_start.ts +++ b/api/movie_start.ts @@ -1,4 +1,5 @@ import { ApiResponse } from "./common"; +import { MovieStartDTO } from "./DTO/movie_start_dto"; import { get, post } from "./request"; import { StoryTemplateEntity, @@ -25,9 +26,12 @@ export const actionTemplateStory = async (template: StoryTemplateEntity) => { /** * AI分析图片,生成分析结果 */ -export const AIGenerateImageStory = async (imageStory: ImageStoryEntity) => { - return await post>( - "/image-story/ai-generate", - imageStory +export const AIGenerateImageStory = async (request: { + imageUrl: string; + user_text: string; +}) => { + return await post>( + "/movie_story/generate", + request ); }; diff --git a/app/service/Interaction/ImageStoryService.ts b/app/service/Interaction/ImageStoryService.ts index 748bfe7..ffbb253 100644 --- a/app/service/Interaction/ImageStoryService.ts +++ b/app/service/Interaction/ImageStoryService.ts @@ -2,22 +2,27 @@ 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; - /** 分析故事结果内容 */ - analyzedStoryContent: string; + /** 故事内容(用户输入或AI分析结果) */ + storyContent: string; + /** 角色头像及名称数据 */ + charactersAnalysis: CharacterAnalysis[]; + /** 分类数据 */ + potentialGenres: string[]; /** 当前选中的分类 */ selectedCategory: string; - /** 是否正在分析图片 */ - isAnalyzing: boolean; - /** 是否正在上传 */ - isUploading: boolean; - /** 故事类型选项 */ - storyTypeOptions: Array<{ key: string; label: string }>; + /** 是否正在加载中(上传或分析) */ + isLoading: boolean; + /** 是否已经分析过图片 */ + hasAnalyzed: boolean; + /** 计算后的角色头像数据 */ + avatarComputed: Array<{ name: string; url: string }>; /** 上传图片并分析 */ uploadAndAnalyzeImage: (imageUrl: string) => Promise; /** 触发文件选择并自动分析 */ @@ -28,68 +33,242 @@ interface UseImageStoryService { updateStoryType: (storyType: string) => void; /** 更新故事内容 */ updateStoryContent: (content: string) => void; + /** 更新角色名称并同步到相关数据 */ + updateCharacterName: (oldName: string, newName: string) => void; /** 重置图片故事数据 */ resetImageStory: () => void; + /** 完全重置到初始状态(包括预置数据) */ + resetToInitialState: () => void; } -export const useImageStoryServiceHook = ( -): UseImageStoryService => { +export const useImageStoryServiceHook = (): UseImageStoryService => { + // 基础状态 const [imageStory, setImageStory] = useState>({ imageUrl: "", - imageStory: "", storyType: "auto", }); - const [isAnalyzing, setIsAnalyzing] = useState(false); - const [isUploading, setIsUploading] = useState(false); + + // 图片相关状态 + 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(), []); - /** 当前活跃的图片地址 */ - const [activeImageUrl, setActiveImageUrl] = useState(""); + /** + * 根据角色区域信息生成头像URL + * @param character - 角色信息 + * @param imageUrl - 源图片URL + */ + const generateAvatarFromRegion = useCallback((character: CharacterAnalysis, imageUrl: string) => { + // 创建图片对象 + const img = new Image(); + img.crossOrigin = 'anonymous'; // 处理跨域问题 - /** 分析故事结果内容 */ - const [analyzedStoryContent, setAnalyzedStoryContent] = useState(""); + 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); - /** 当前选中的分类 */ - const [selectedCategory, setSelectedCategory] = useState("auto"); - /** 故事类型选项 */ - const storyTypeOptions = useMemo(() => imageStoryUseCase.getStoryTypeOptions(), [imageStoryUseCase]); + // 验证裁剪区域是否有效 + 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 { - setIsUploading(true); - setIsAnalyzing(true); + const uploadAndAnalyzeImage = useCallback( + async (imageUrl: string): Promise => { + try { + setIsLoading(true); - // 调用用例处理图片上传和分析 - await imageStoryUseCase.handleImageUpload(imageUrl); + // 调用用例处理图片上传和分析 + await imageStoryUseCase.handleImageUpload(imageUrl); - // 获取更新后的数据 - const updatedStory = imageStoryUseCase.getImageStory(); - setImageStory(updatedStory); + // 获取更新后的数据 + const updatedStory = imageStoryUseCase.getStoryLogline(); + const updatedCharacters = imageStoryUseCase.getCharactersAnalysis(); + const updatedGenres = imageStoryUseCase.getPotentialGenres(); + const updatedImageStory = imageStoryUseCase.getImageStory(); - // 更新活跃状态 - setActiveImageUrl(imageUrl); - setAnalyzedStoryContent(updatedStory.imageStory || ""); - setSelectedCategory(updatedStory.storyType || "auto"); + // 更新所有响应式状态 + setCharactersAnalysis(updatedCharacters); + setPotentialGenres(updatedGenres); + setImageStory(updatedImageStory); + // 将AI分析的故事内容直接更新到统一的故事内容字段 + setStoryContent(updatedStory || ""); + // 设置第一个分类为默认选中 + if (updatedGenres.length > 0) { + setSelectedCategory(updatedGenres[0]); + } - } catch (error) { - console.error('图片上传分析失败:', error); - throw error; - } finally { - setIsUploading(false); - setIsAnalyzing(false); - } - }, [imageStoryUseCase]); + // 标记已分析 + setHasAnalyzed(true); + } catch (error) { + console.error("图片上传分析失败:", error); + throw error; + } finally { + setIsLoading(false); + } + }, + [imageStoryUseCase] + ); /** * 触发生成剧本函数 @@ -97,15 +276,16 @@ export const useImageStoryServiceHook = ( */ const generateScript = useCallback(async (): Promise => { if (!activeImageUrl) { - throw new Error('请先上传图片'); + throw new Error("请先上传图片"); } - if (!analyzedStoryContent) { - throw new Error('请先输入或生成故事内容'); + const finalStoryContent = storyContent; + if (!finalStoryContent.trim()) { + throw new Error("请先输入或生成故事内容"); } try { - setIsAnalyzing(true); + setIsLoading(true); // 这里可以调用后端API生成剧本 // 暂时返回一个模拟的剧本ID @@ -117,110 +297,218 @@ export const useImageStoryServiceHook = ( return scriptId; } catch (error) { - console.error('生成剧本失败:', error); + console.error("生成剧本失败:", error); throw error; } finally { - setIsAnalyzing(false); + setIsLoading(false); } - }, [activeImageUrl, analyzedStoryContent, imageStory]); + }, [activeImageUrl, storyContent]); /** * 更新故事类型 * @param {string} storyType - 新的故事类型 */ - const updateStoryType = useCallback((storyType: string): void => { - imageStoryUseCase.updateStoryType(storyType); - setImageStory(prev => ({ ...prev, storyType })); - setSelectedCategory(storyType); - }, [imageStoryUseCase]); + 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 => { - imageStoryUseCase.updateStoryContent(content); - setImageStory(prev => ({ ...prev, imageStory: content })); - setAnalyzedStoryContent(content); + 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 + ) + ); - }, [imageStoryUseCase]); + // 同步更新故事内容中的角色名称 + 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: "", - imageStory: "", storyType: "auto", }); - // 重置活跃状态 setActiveImageUrl(""); - setAnalyzedStoryContent(""); + setStoryContent(""); + setPotentialGenres([]); setSelectedCategory("auto"); - setIsAnalyzing(false); - setIsUploading(false); - - + 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"; + 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]) { - // 使用传入的文件上传函数 - const uploadedImageUrl = await uploadFile(target.files[0], (progress) => { - console.log("上传进度:", progress); - }); - console.log('uploadedImageUrl', uploadedImageUrl) - // await uploadAndAnalyzeImage(uploadedImageUrl); - setActiveImageUrl(uploadedImageUrl); + 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); } - resolve(); - } catch (error) { - reject(error); - } finally { - // 清理DOM + }; + + fileInput.oncancel = () => { document.body.removeChild(fileInput); - } - }; + reject(); + }; - fileInput.oncancel = () => { - document.body.removeChild(fileInput); - reject(); - }; - - document.body.appendChild(fileInput); - fileInput.click(); - }); - }, [uploadFile]); + document.body.appendChild(fileInput); + fileInput.click(); + }); + }, [uploadFile, uploadAndAnalyzeImage]); return { imageStory, activeImageUrl, - analyzedStoryContent, + storyContent, + charactersAnalysis, + potentialGenres, selectedCategory, - isAnalyzing, - isUploading, - storyTypeOptions, + isLoading, + hasAnalyzed, + avatarComputed, uploadAndAnalyzeImage, triggerFileSelectionAndAnalyze, generateScript, updateStoryType, updateStoryContent, + updateCharacterName, resetImageStory, + resetToInitialState, }; }; diff --git a/app/service/domain/Entities.ts b/app/service/domain/Entities.ts index afe79cb..9fb5941 100644 --- a/app/service/domain/Entities.ts +++ b/app/service/domain/Entities.ts @@ -96,6 +96,24 @@ export interface ImageStoryEntity { imageAnalysis: string; /** 故事分类 */ storyType: string; + /**角色头像 */ + roleImage: { + /** 名称 */ + name: string; + /** 头像URL(本地create的url) */ + avatar_url: string; + /** 角色区域 小数比例形式 */ + region: { + /** x坐标 */ + x: number; + /** y坐标 */ + y: number; + /** 宽度 */ + width: number; + /** 高度 */ + height: number; + }; + }[]; } /** * 故事模板实体接口 diff --git a/app/service/usecase/imageStoryUseCase.ts b/app/service/usecase/imageStoryUseCase.ts index 30b314d..812f4ca 100644 --- a/app/service/usecase/imageStoryUseCase.ts +++ b/app/service/usecase/imageStoryUseCase.ts @@ -1,5 +1,6 @@ import { ImageStoryEntity } from "../domain/Entities"; import { AIGenerateImageStory } from "@/api/movie_start"; +import { MovieStartDTO, CharacterAnalysis } from "@/api/DTO/movie_start_dto"; /** * 图片故事用例 @@ -7,17 +8,26 @@ import { AIGenerateImageStory } from "@/api/movie_start"; */ export class ImageStoryUseCase { /** 当前图片故事数据 */ - private imageStory: Partial = { + imageStory: Partial = { imageUrl: "", imageStory: "", storyType: "auto", }; + /** 故事梗概 */ + storyLogline: string = ""; + + /** 角色头像及名称数据 */ + charactersAnalysis: CharacterAnalysis[] = []; + + /** 分类数据 */ + potentialGenres: string[] = []; + /** 是否正在分析图片 */ - private isAnalyzing: boolean = false; + isAnalyzing: boolean = false; /** 是否正在上传 */ - private isUploading: boolean = false; + isUploading: boolean = false; constructor() {} @@ -29,6 +39,30 @@ export class ImageStoryUseCase { return { ...this.imageStory }; } + /** + * 获取故事梗概 + * @returns {string} 故事梗概 + */ + getStoryLogline(): string { + return this.storyLogline; + } + + /** + * 获取角色分析数据 + * @returns {CharacterAnalysis[]} 角色分析数据 + */ + getCharactersAnalysis(): CharacterAnalysis[] { + return [...this.charactersAnalysis]; + } + + /** + * 获取分类数据 + * @returns {string[]} 分类数据 + */ + getPotentialGenres(): string[] { + return [...this.potentialGenres]; + } + /** * 获取分析状态 * @returns {boolean} 是否正在分析 @@ -62,6 +96,9 @@ export class ImageStoryUseCase { imageStory: "", storyType: "auto", }; + this.storyLogline = ""; + this.charactersAnalysis = []; + this.potentialGenres = []; this.isAnalyzing = false; this.isUploading = false; } @@ -96,20 +133,20 @@ export class ImageStoryUseCase { * 使用AI分析图片 * @returns {Promise} */ - private async analyzeImageWithAI(): Promise { + async analyzeImageWithAI(): Promise { try { // 调用AI分析接口 - const response = await AIGenerateImageStory(this.imageStory as ImageStoryEntity); + const response = await AIGenerateImageStory({ + imageUrl: this.imageStory.imageUrl || "", + user_text: this.imageStory.imageStory || "", + }); if (response.successful && response.data) { - const { imageAnalysis, category } = response.data; + // 解析并存储新的数据结构 + this.parseAndStoreAnalysisData(response.data); - // 更新分析结果和分类 - this.setImageStory({ - imageAnalysis, - storyType: category || "auto", - imageStory: imageAnalysis, // 将AI分析结果作为默认故事内容 - }); + // 组合成ImageStoryEntity + this.composeImageStoryEntity(response.data); } else { throw new Error("AI分析失败"); } @@ -119,6 +156,46 @@ export class ImageStoryUseCase { } } + /** + * 解析并存储分析数据到类属性中 + * @param {MovieStartDTO} data - AI分析返回的数据 + */ + parseAndStoreAnalysisData(data: MovieStartDTO): void { + // 存储故事梗概 + this.storyLogline = data.story_logline || ""; + + // 存储角色头像及名称数据 + this.charactersAnalysis = data.characters_analysis || []; + + // 存储分类数据 + this.potentialGenres = data.potential_genres || []; + } + + /** + * 组合成ImageStoryEntity + * @param {MovieStartDTO} data - AI分析返回的数据 + */ + composeImageStoryEntity(data: MovieStartDTO): void { + // 将角色数据转换为ImageStoryEntity需要的格式 + const roleImage = data.characters_analysis?.map(character => ({ + name: character.role_name, + avatar_url: "", // 这里需要根据实际情况设置头像URL + region: { + x: character.region.x, + y: character.region.y, + width: character.region.width, + height: character.region.height, + } + })) || []; + + // 更新ImageStoryEntity + this.setImageStory({ + imageAnalysis: data.story_logline || "", + storyType: data.potential_genres?.[0] || "auto", // 使用第一个分类作为故事类型 + roleImage, + }); + } + /** * 更新故事类型 * @param {string} storyType - 新的故事类型 @@ -137,7 +214,7 @@ export class ImageStoryUseCase { /** * 获取故事类型选项 - * @returns {Array<{key: string, label: string}>} 故事类型选项数组 + * @returns {Array<{key: string, label: string}> 故事类型选项数组 */ getStoryTypeOptions(): Array<{ key: string; label: string }> { return [ @@ -149,5 +226,53 @@ export class ImageStoryUseCase { { key: "comedy", label: "Comedy" }, ]; } + + /** + * 处理角色数据,解析并存储到类属性中 + * @param {CharacterAnalysis[]} characters - 角色分析数据 + */ + processCharacterData(characters: CharacterAnalysis[]): void { + this.charactersAnalysis = characters.map(character => ({ + ...character, + region: { + x: character.region.x, + y: character.region.y, + width: character.region.width, + height: character.region.height, + } + })); + } + + /** + * 获取指定角色的区域坐标 + * @param {string} characterName - 角色名称 + * @returns {CharacterAnalysis['region'] | null} 角色区域坐标,如果未找到则返回null + */ + getCharacterRegion(characterName: string): CharacterAnalysis['region'] | null { + const character = this.charactersAnalysis.find(char => char.role_name === characterName); + return character ? character.region : null; + } + + /** + * 更新角色头像URL + * @param {string} characterName - 角色名称 + * @param {string} avatarUrl - 头像URL + */ + updateCharacterAvatar(characterName: string, avatarUrl: string): void { + const character = this.charactersAnalysis.find(char => char.role_name === characterName); + if (character) { + // 更新角色头像URL(这里需要根据实际的数据结构来调整) + // 由于CharacterAnalysis接口中没有avatar_url字段,这里只是示例 + console.log(`更新角色 ${characterName} 的头像URL: ${avatarUrl}`); + } + } + + /** + * 获取所有角色名称 + * @returns {string[]} 角色名称数组 + */ + getAllCharacterNames(): string[] { + return this.charactersAnalysis.map(char => char.role_name); + } } diff --git a/components/common/ChatInputBox.tsx b/components/common/ChatInputBox.tsx index 0bd7966..9ea8656 100644 --- a/components/common/ChatInputBox.tsx +++ b/components/common/ChatInputBox.tsx @@ -1,12 +1,6 @@ "use client"; -import { - useState, - useRef, - useEffect, - forwardRef, - useImperativeHandle, -} from "react"; +import { useState, useRef, useEffect } from "react"; import { ChevronDown, ChevronUp, @@ -23,8 +17,11 @@ import { Plus, LayoutTemplate, ImagePlay, + Sparkles, + RotateCcw, + Settings, } from "lucide-react"; -import { Dropdown, Modal, Tooltip, Upload, Image } from "antd"; +import { Dropdown, Modal, Tooltip, Upload, Image, Spin } from "antd"; import { PlusOutlined, UploadOutlined, EyeOutlined } from "@ant-design/icons"; import { StoryTemplateEntity } from "@/app/service/domain/Entities"; import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService"; @@ -133,6 +130,14 @@ const customAudioPlayerStyles = ` .scale-102 { transform: scale(1.02); } + + /* 文本截断类 */ + .line-clamp-3 { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + } `; /**模板故事模式弹窗组件 */ @@ -486,31 +491,13 @@ export function ChatInputBox() { // 模板故事弹窗状态 const [isTemplateModalOpen, setIsTemplateModalOpen] = useState(false); - // 图片故事模式状态 - const [isPhotoStoryMode, setIsPhotoStoryMode] = useState(false); + // 图片故事弹窗状态 + const [isPhotoStoryModalOpen, setIsPhotoStoryModalOpen] = useState(false); // 共享状态 - 需要在不同渲染函数间共享 const [script, setScript] = useState(""); // 用户输入的脚本内容 const router = useRouter(); - // 子组件引用,用于调用子组件方法 - const photoStoryModeRef = useRef<{ - enterPhotoStoryMode: () => void; - getStoryContent: () => string; - }>(null); - - // 响应式管理输入框内容:当在图片故事模式下,从子组件获取故事内容 - useEffect(() => { - if (isPhotoStoryMode && photoStoryModeRef.current) { - const storyContent = photoStoryModeRef.current.getStoryContent(); - if (storyContent) { - setScript(storyContent); - } - } - }, [isPhotoStoryMode]); - - - const [loadingIdea, setLoadingIdea] = useState(false); // 获取创意建议时的加载状态 const [isCreating, setIsCreating] = useState(false); // 视频创建过程中的加载状态 @@ -522,10 +509,8 @@ export function ChatInputBox() { videoDuration: "1min", }); - // 退出图片故事模式 - const exitPhotoStoryMode = () => { - setIsPhotoStoryMode(false); - }; + // 配置项显示控制状态 + const [showConfigOptions, setShowConfigOptions] = useState(false); const handleGetIdea = () => { if (loadingIdea) return; @@ -538,22 +523,14 @@ export function ChatInputBox() { }, 3000); }; - - - // 当进入图片故事模式时,调用子组件方法 - const enterPhotoStoryMode = () => { - setIsPhotoStoryMode(true); - - // 调用子组件的进入方法 - if (photoStoryModeRef.current) { - photoStoryModeRef.current.enterPhotoStoryMode(); - } - }; - // Handle creating video const handleCreateVideo = async () => { setIsCreating(true); + if (!script) { + setIsCreating(false); + return; + } const User = JSON.parse(localStorage.getItem("currentUser") || "{}"); // 创建剧集数据 @@ -563,12 +540,12 @@ export function ChatInputBox() { mode: configOptions.mode, resolution: configOptions.resolution, language: configOptions.language, - video_duration: configOptions.videoDuration + video_duration: configOptions.videoDuration, }; // 调用创建剧集API const episodeResponse = await createScriptEpisodeNew(episodeData); - console.log('episodeResponse', episodeResponse); + console.log("episodeResponse", episodeResponse); if (episodeResponse.code !== 0) { console.error(`创建剧集失败: ${episodeResponse.message}`); alert(`创建剧集失败: ${episodeResponse.message}`); @@ -578,7 +555,7 @@ export function ChatInputBox() { // let episodeId = '9c34fcc4-c8d8-44fc-879e-9bd56f608c76'; router.push(`/create/work-flow?episodeId=${episodeId}`); setIsCreating(false); - } + }; return (
@@ -606,138 +583,148 @@ export function ChatInputBox() { {/* 主要内容区域 - 简化层级,垂直居中 */}
- {/* 输入框和Action按钮 - 只在展开状态显示 */} + {/* 右上角齿轮图标和配置项 */} {!isExpanded && ( -
- {/* 模式选择下拉菜单 */} - - - - Template Story - -
- ), - }, - { - key: "photo", - label: ( -
- - - Photo Story - -
- ), - }, - ], - onClick: ({ key }) => { - if (key === "template") { - setIsTemplateModalOpen(true); - } else if (key === "photo") { - enterPhotoStoryMode(); - } - console.log("Selected mode:", key); - }, +
+ {/* 配置项显示区域 - 从齿轮图标右侧边缘滑入 */} +
- - - - {/* 图片故事模式UI - 始终渲染,通过ref调用方法 */} - - -
- {/* 简单的文本输入框 */} - setScript(e.target.value)} - placeholder="Describe the content you want to action..." - className="flex-1 w-0 pl-[10px] pr-[10px] py-[10px] rounded-[10px] leading-[20px] text-sm border-none bg-transparent text-white placeholder:text-white/[0.40] focus:outline-none" - /> - - {/* 获取创意按钮 */} - {!script && ( -
- Get an - -
- )} + + setConfigOptions((prev) => ({ ...prev, [key]: value })) + } + compact={true} + />
- {/* Action按钮 */} -
-
-
+ {/* 配置项显示控制按钮 - 齿轮图标,位置完全固定 */} + + + +
+ )} -
+ {/* 输入框和Action按钮 - 只在展开状态显示 */} + {!isExpanded && ( +
+ {/* 第一行:输入框 */} +
+ {/* 文本输入框 - 改为textarea */} +