video-flow-b/app/service/Interaction/ImageStoryService.ts
2025-09-29 20:06:55 +08:00

577 lines
17 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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";
import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector'
import { getClientUserData } from "@/api/common";
interface UseImageStoryService {
/** 当前图片故事数据 */
imageStory: Partial<ImageStoryEntity>;
/** 当前活跃的图片地址 */
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<void>;
/** 触发文件选择 */
triggerFileSelection: () => Promise<void>;
/** 更新故事类型 */
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,
aspectRatio?: AspectRatioValue
) => Promise<{ project_id: string } | undefined>;
/** 设置角色分析 */
setCharactersAnalysis: Dispatch<SetStateAction<CharacterAnalysis[]>>;
/** 设置原始用户描述 */
setOriginalUserDescription: Dispatch<SetStateAction<string>>;
/** 上传人物头像并分析特征,替换旧的角色数据 */
uploadCharacterAvatarAndAnalyzeFeatures: (
characterName: string
) => Promise<void>;
}
export const useImageStoryServiceHook = (): UseImageStoryService => {
// 基础状态
const [imageStory, setImageStory] = useState<Partial<ImageStoryEntity>>({
imageUrl: "",
storyType: "",
});
// 图片相关状态
const [activeImageUrl, setActiveImageUrl] = useState<string>("");
// 故事内容状态统一管理用户输入和AI分析结果
const [storyContent, setStoryContent] = useState<string>("");
// 原始用户描述
const [originalUserDescription, setOriginalUserDescription] =
useState<string>("");
// 分析结果状态
/** 角色头像及名称 */
const [charactersAnalysis, setCharactersAnalysis] = useState<
CharacterAnalysis[]
>([]);
/** 分类数组 */
const [potentialGenres, setPotentialGenres] = useState<string[]>([]);
// 分类状态
const [selectedCategory, setSelectedCategory] = useState<string>("");
// 流程状态
const [isLoading, setIsLoading] = useState(false);
const [hasAnalyzed, setHasAnalyzed] = useState(false);
/** 分析任务进度 */
const [taskProgress, setTaskProgress] = useState(0);
// 使用上传文件Hook
const { uploadFile } = useUploadFile();
const [taskId, setTaskId] = useState<string>("");
/** 图片故事用例实例 */
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<void> => {
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) => {
// 匹配新的角色标签格式 <role id="C1">Dezhong Huang</role>
const regex = new RegExp(`<role[^>]*>${oldName}<\/role>`, "g");
const content = prev.replace(regex, `<role >${newName}</role>`);
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<void> => {
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",
aspectRatio?: AspectRatioValue
) => {
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,
user_data: getClientUserData(),
mode,
resolution,
genre: selectedCategory,
character_briefs,
language,
image_url: activeImageUrl,
project_id:taskId,
...(aspectRatio ? { aspect_ratio: aspectRatio } : {})
};
// 调用create_movie_project_v2接口
const result = await MovieProjectService.createProject(
MovieProjectMode.IMAGE,
params
);
return { project_id: result.project_id };
}
} catch (error) {
console.error("创建电影项目失败:", error);
throw error;
}
},
[
hasAnalyzed,
storyContent,
charactersAnalysis,
selectedCategory,
activeImageUrl,
]
);
/**
* 上传人物头像并分析特征,替换旧的角色数据
* @param {string} characterName - 角色名称
*/
const uploadCharacterAvatarAndAnalyzeFeatures = useCallback(
async (characterName: string): Promise<void> => {
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,
};
};