forked from 77media/video-flow
304 lines
8.8 KiB
TypeScript
304 lines
8.8 KiB
TypeScript
import { ImageStoryEntity } from "../domain/Entities";
|
||
import { AIGenerateImageStory } from "@/api/movie_start";
|
||
import { MovieStartDTO, CharacterAnalysis } from "@/api/DTO/movie_start_dto";
|
||
import { analyzeImageDescription } from "@/api/video_flow";
|
||
|
||
/**
|
||
* 图片故事用例
|
||
* 负责管理图片故事模式的业务逻辑,包括图片上传、AI分析和故事生成
|
||
*/
|
||
export class ImageStoryUseCase {
|
||
/** 当前图片故事数据 */
|
||
imageStory: ImageStoryEntity = {
|
||
id: "",
|
||
imageAnalysis: "",
|
||
roleImage: [],
|
||
imageUrl: "",
|
||
imageStory: "",
|
||
storyType: "",
|
||
};
|
||
|
||
/** 故事梗概 */
|
||
storyLogline: string = "";
|
||
|
||
/** 角色头像及名称数据 */
|
||
charactersAnalysis: CharacterAnalysis[] = [];
|
||
|
||
/** 分类数据 */
|
||
potentialGenres: string[] = [];
|
||
|
||
/** 是否正在分析图片 */
|
||
isAnalyzing: boolean = false;
|
||
|
||
/** 是否正在上传 */
|
||
isUploading: boolean = false;
|
||
constructor() {}
|
||
|
||
/**
|
||
* 设置图片故事数据
|
||
* @param {Partial<ImageStoryEntity>} data - 要设置的图片故事数据
|
||
*/
|
||
setImageStory(data: Partial<ImageStoryEntity>): void {
|
||
this.imageStory = { ...this.imageStory, ...data };
|
||
}
|
||
|
||
/**
|
||
* 重置图片故事数据
|
||
*/
|
||
resetImageStory(): void {
|
||
this.imageStory = {
|
||
id: "",
|
||
imageAnalysis: "",
|
||
roleImage: [],
|
||
imageUrl: "",
|
||
imageStory: "",
|
||
storyType: "",
|
||
};
|
||
this.storyLogline = "";
|
||
this.charactersAnalysis = [];
|
||
this.potentialGenres = [];
|
||
this.isAnalyzing = false;
|
||
this.isUploading = false;
|
||
}
|
||
|
||
/**
|
||
* 处理图片上传
|
||
* @param {string} imageUrl - 已上传的图片URL
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async handleImageUpload(imageUrl: string) {
|
||
try {
|
||
this.isUploading = false; // 图片已上传,设置上传状态为false
|
||
this.isAnalyzing = true;
|
||
console.log("imageUrl", imageUrl);
|
||
// 设置上传后的图片URL
|
||
this.setImageStory({ imageUrl });
|
||
|
||
// 调用AI分析接口
|
||
return await this.analyzeImageWithAI();
|
||
} catch (error) {
|
||
console.error("图片分析失败:", error);
|
||
// 分析失败时清空图片URL
|
||
this.setImageStory({ imageUrl: "" });
|
||
throw error;
|
||
} finally {
|
||
this.isAnalyzing = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 使用AI分析图片
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async analyzeImageWithAI() {
|
||
console.log("this.imageStory.imageUrl", this.imageStory.imageUrl);
|
||
try {
|
||
//
|
||
// 调用AI分析接口
|
||
const response = await AIGenerateImageStory({
|
||
image_url: this.imageStory.imageUrl || "",
|
||
user_text: this.imageStory.imageStory || "",
|
||
});
|
||
|
||
if (response.successful && response.data) {
|
||
// 解析并存储新的数据结构
|
||
this.parseAndStoreAnalysisData(response.data);
|
||
|
||
// 组合成ImageStoryEntity
|
||
this.composeImageStoryEntity(response.data);
|
||
return this.imageStory;
|
||
} else {
|
||
throw new Error("AI分析失败");
|
||
}
|
||
} catch (error) {
|
||
console.error("AI分析失败:", error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析并存储分析数据到类属性中
|
||
* @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 || 0,
|
||
y: character.region?.y || 0,
|
||
width: character.region?.width || 0,
|
||
height: character.region?.height || 0,
|
||
},
|
||
})) || [];
|
||
|
||
// 更新ImageStoryEntity
|
||
this.setImageStory({
|
||
...this.imageStory,
|
||
imageAnalysis: data.story_logline || "",
|
||
storyType: data.potential_genres[0] || "", // 使用第一个分类作为故事类型
|
||
roleImage,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 更新故事类型
|
||
* @param {string} storyType - 新的故事类型
|
||
*/
|
||
updateStoryType(storyType: string): void {
|
||
this.setImageStory({ storyType });
|
||
}
|
||
|
||
/**
|
||
* 更新故事内容
|
||
* @param {string} storyContent - 新的故事内容
|
||
*/
|
||
updateStoryContent(storyContent: string): void {
|
||
this.setImageStory({ imageStory: storyContent });
|
||
}
|
||
/**
|
||
* 处理角色数据,解析并存储到类属性中
|
||
* @param {CharacterAnalysis[]} characters - 角色分析数据
|
||
*/
|
||
processCharacterData(characters: CharacterAnalysis[]): void {
|
||
this.charactersAnalysis = characters.map((character) => ({
|
||
...character,
|
||
region: {
|
||
x: character.region?.x || 0,
|
||
y: character.region?.y || 0,
|
||
width: character.region?.width || 0,
|
||
height: character.region?.height || 0,
|
||
},
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* 获取指定角色的区域坐标
|
||
* @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);
|
||
}
|
||
|
||
/**
|
||
* 上传人物头像并分析特征,替换旧的角色数据
|
||
* @param {Function} uploadFile - 文件上传函数
|
||
* @returns {Promise<{crop_url: string, whisk_caption: string}>} 返回新的头像URL和特征描述
|
||
*/
|
||
async uploadCharacterAvatarAndAnalyzeFeatures(
|
||
uploadFile: (file: File) => Promise<string>
|
||
): Promise<{ crop_url: string; whisk_caption: string }> {
|
||
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 file = target.files[0];
|
||
|
||
// 直接在这里处理上传和分析逻辑
|
||
try {
|
||
// 1. 上传人物头像图片
|
||
const imageUrl = await uploadFile(file);
|
||
|
||
// 2. 调用AI分析接口获取人物特征描述
|
||
const analysisResult = await analyzeImageDescription({
|
||
image_url: imageUrl,
|
||
});
|
||
|
||
if (!analysisResult.successful || !analysisResult.data) {
|
||
throw new Error("人物特征分析失败");
|
||
}
|
||
|
||
// 3. 返回新的头像URL和特征描述,用于替换旧数据
|
||
const result = {
|
||
crop_url: imageUrl,
|
||
whisk_caption: analysisResult.data.description,
|
||
};
|
||
|
||
// 清理临时元素
|
||
document.body.removeChild(fileInput);
|
||
resolve(result);
|
||
} catch (error) {
|
||
// 清理临时元素
|
||
if (document.body.contains(fileInput)) {
|
||
document.body.removeChild(fileInput);
|
||
}
|
||
reject(error);
|
||
}
|
||
} else {
|
||
reject(new Error("未选择文件"));
|
||
}
|
||
} catch (error) {
|
||
// 清理临时元素
|
||
if (document.body.contains(fileInput)) {
|
||
document.body.removeChild(fileInput);
|
||
}
|
||
reject(error);
|
||
}
|
||
};
|
||
|
||
fileInput.oncancel = () => {
|
||
document.body.removeChild(fileInput);
|
||
reject(new Error("用户取消选择"));
|
||
};
|
||
|
||
// 添加到DOM并触发点击
|
||
document.body.appendChild(fileInput);
|
||
fileInput.click();
|
||
});
|
||
}
|
||
}
|