forked from 77media/video-flow
新的阶段
This commit is contained in:
parent
e405f4bd7d
commit
85687f5840
@ -45,6 +45,41 @@ export interface MovieStartDTO {
|
|||||||
/** 错误信息 */
|
/** 错误信息 */
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 电影故事任务详情 */
|
||||||
|
export interface MovieStoryTaskDetail {
|
||||||
|
/** 任务ID */
|
||||||
|
task_id: string;
|
||||||
|
/** 状态 */
|
||||||
|
status: string;
|
||||||
|
/** 进度 */
|
||||||
|
progress: number;
|
||||||
|
/** 当前步骤 */
|
||||||
|
current_step: string;
|
||||||
|
/** 步骤消息 */
|
||||||
|
step_message: string;
|
||||||
|
/** 已用时间 */
|
||||||
|
elapsed_time: number;
|
||||||
|
/** 预计剩余时间 */
|
||||||
|
estimated_remaining: number | null;
|
||||||
|
/** 错误信息 */
|
||||||
|
error_message: string | null;
|
||||||
|
/** 结果 */
|
||||||
|
result: MovieStartDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**图片分析出故事的任务相关数据,用于轮询查状态 */
|
||||||
|
export interface StoryAnalysisTask{
|
||||||
|
/** 任务ID */
|
||||||
|
task_id:string;
|
||||||
|
/** 状态 */
|
||||||
|
status:string;
|
||||||
|
/** 消息 */
|
||||||
|
message:string;
|
||||||
|
/** 预计时长 */
|
||||||
|
estimated_duration:number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建电影项目V2请求参数 照片生成电影
|
* 创建电影项目V2请求参数 照片生成电影
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
import { ApiResponse } from "./common";
|
import { ApiResponse } from "./common";
|
||||||
import { CreateMovieProjectV2Request, CreateMovieProjectResponse, MovieStartDTO } from "./DTO/movie_start_dto";
|
import {
|
||||||
|
CreateMovieProjectV2Request,
|
||||||
|
CreateMovieProjectResponse,
|
||||||
|
MovieStartDTO,
|
||||||
|
StoryAnalysisTask,
|
||||||
|
MovieStoryTaskDetail,
|
||||||
|
} from "./DTO/movie_start_dto";
|
||||||
import { get, post } from "./request";
|
import { get, post } from "./request";
|
||||||
import {
|
import {
|
||||||
StoryTemplateEntity,
|
StoryTemplateEntity,
|
||||||
@ -30,7 +36,7 @@ export const AIGenerateImageStory = async (request: {
|
|||||||
image_url: string;
|
image_url: string;
|
||||||
user_text: string;
|
user_text: string;
|
||||||
}) => {
|
}) => {
|
||||||
return await post<ApiResponse<MovieStartDTO>>(
|
return await post<ApiResponse<StoryAnalysisTask>>(
|
||||||
"/movie_story/generate",
|
"/movie_story/generate",
|
||||||
request
|
request
|
||||||
);
|
);
|
||||||
@ -63,3 +69,14 @@ export const createMovieProjectV3 = async (
|
|||||||
request
|
request
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取电影故事任务详情
|
||||||
|
* @param taskId - 任务ID
|
||||||
|
* @returns Promise<ApiResponse<MovieStoryTaskDetail>>
|
||||||
|
*/
|
||||||
|
export const getMovieStoryTask = async (taskId: string) => {
|
||||||
|
return await get<ApiResponse<MovieStoryTaskDetail>>(
|
||||||
|
`/movie_story/task/${taskId}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@ -8,7 +8,11 @@ import {
|
|||||||
Dispatch,
|
Dispatch,
|
||||||
SetStateAction,
|
SetStateAction,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { CharacterAnalysis, CreateMovieProjectV2Request, CreateMovieProjectResponse } from "@/api/DTO/movie_start_dto";
|
import {
|
||||||
|
CharacterAnalysis,
|
||||||
|
CreateMovieProjectV2Request,
|
||||||
|
CreateMovieProjectResponse,
|
||||||
|
} from "@/api/DTO/movie_start_dto";
|
||||||
import { createMovieProjectV2 } from "@/api/movie_start";
|
import { createMovieProjectV2 } from "@/api/movie_start";
|
||||||
|
|
||||||
interface UseImageStoryService {
|
interface UseImageStoryService {
|
||||||
@ -32,6 +36,8 @@ interface UseImageStoryService {
|
|||||||
avatarComputed: Array<{ name: string; url: string }>;
|
avatarComputed: Array<{ name: string; url: string }>;
|
||||||
/** 原始用户描述 */
|
/** 原始用户描述 */
|
||||||
originalUserDescription: string;
|
originalUserDescription: string;
|
||||||
|
/** 分析任务进度 */
|
||||||
|
taskProgress: number;
|
||||||
/** 上传图片并分析 */
|
/** 上传图片并分析 */
|
||||||
uploadAndAnalyzeImage: () => Promise<void>;
|
uploadAndAnalyzeImage: () => Promise<void>;
|
||||||
/** 触发文件选择 */
|
/** 触发文件选择 */
|
||||||
@ -52,13 +58,15 @@ interface UseImageStoryService {
|
|||||||
mode?: "auto" | "manual",
|
mode?: "auto" | "manual",
|
||||||
resolution?: "720p" | "1080p" | "4k",
|
resolution?: "720p" | "1080p" | "4k",
|
||||||
language?: string
|
language?: string
|
||||||
) => Promise<CreateMovieProjectResponse|undefined>;
|
) => Promise<CreateMovieProjectResponse | undefined>;
|
||||||
/** 设置角色分析 */
|
/** 设置角色分析 */
|
||||||
setCharactersAnalysis: Dispatch<SetStateAction<CharacterAnalysis[]>>;
|
setCharactersAnalysis: Dispatch<SetStateAction<CharacterAnalysis[]>>;
|
||||||
/** 设置原始用户描述 */
|
/** 设置原始用户描述 */
|
||||||
setOriginalUserDescription: Dispatch<SetStateAction<string>>;
|
setOriginalUserDescription: Dispatch<SetStateAction<string>>;
|
||||||
/** 上传人物头像并分析特征,替换旧的角色数据 */
|
/** 上传人物头像并分析特征,替换旧的角色数据 */
|
||||||
uploadCharacterAvatarAndAnalyzeFeatures: (characterName: string) => Promise<void>;
|
uploadCharacterAvatarAndAnalyzeFeatures: (
|
||||||
|
characterName: string
|
||||||
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useImageStoryServiceHook = (): UseImageStoryService => {
|
export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||||
@ -91,7 +99,8 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
|||||||
// 流程状态
|
// 流程状态
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [hasAnalyzed, setHasAnalyzed] = useState(false);
|
const [hasAnalyzed, setHasAnalyzed] = useState(false);
|
||||||
|
/** 分析任务进度 */
|
||||||
|
const [taskProgress, setTaskProgress] = useState(0);
|
||||||
// 使用上传文件Hook
|
// 使用上传文件Hook
|
||||||
const { uploadFile } = useUploadFile();
|
const { uploadFile } = useUploadFile();
|
||||||
|
|
||||||
@ -217,24 +226,26 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return charactersAnalysis.map((character) => {
|
return charactersAnalysis
|
||||||
console.log('character', character)
|
.map((character) => {
|
||||||
// 如果已经有头像URL,直接返回
|
console.log("character", character);
|
||||||
if (character.crop_url) {
|
// 如果已经有头像URL,直接返回
|
||||||
return {
|
if (character.crop_url) {
|
||||||
name: character.role_name,
|
return {
|
||||||
url: character.crop_url,
|
name: character.role_name,
|
||||||
};
|
url: character.crop_url,
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// // 异步生成头像URL
|
// // 异步生成头像URL
|
||||||
// generateAvatarFromRegion(character, activeImageUrl);
|
// generateAvatarFromRegion(character, activeImageUrl);
|
||||||
|
|
||||||
// return {
|
// return {
|
||||||
// name: character.role_name,
|
// name: character.role_name,
|
||||||
// url: "", // 初始为空,异步生成完成后会更新
|
// url: "", // 初始为空,异步生成完成后会更新
|
||||||
// };
|
// };
|
||||||
}).filter(Boolean) as { name: string; url: string }[];
|
})
|
||||||
|
.filter(Boolean) as { name: string; url: string }[];
|
||||||
}, [charactersAnalysis, activeImageUrl, generateAvatarFromRegion]);
|
}, [charactersAnalysis, activeImageUrl, generateAvatarFromRegion]);
|
||||||
/**
|
/**
|
||||||
* 上传图片并分析
|
* 上传图片并分析
|
||||||
@ -243,28 +254,54 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
|||||||
const uploadAndAnalyzeImage = useCallback(async (): Promise<void> => {
|
const uploadAndAnalyzeImage = useCallback(async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
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 newImageStory = await imageStoryUseCase.handleImageUpload(
|
const taskId = await imageStoryUseCase.handleImageUpload(activeImageUrl);
|
||||||
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分析的故事内容直接更新到统一的故事内容字段
|
for await (const result of await imageStoryUseCase.pollTaskStatus(
|
||||||
updateStoryContent(updatedStory || "");
|
taskId
|
||||||
|
)) {
|
||||||
// 标记已分析
|
setTaskProgress(result.progress);
|
||||||
setHasAnalyzed(true);
|
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) {
|
} catch (error) {
|
||||||
console.error("图片上传分析失败:", error);
|
console.error("图片上传分析失败:", error);
|
||||||
setHasAnalyzed(false);
|
setHasAnalyzed(false);
|
||||||
@ -275,7 +312,6 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
|||||||
}
|
}
|
||||||
}, [imageStoryUseCase, activeImageUrl, storyContent]);
|
}, [imageStoryUseCase, activeImageUrl, storyContent]);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新故事类型
|
* 更新故事类型
|
||||||
* @param {string} storyType - 新的故事类型
|
* @param {string} storyType - 新的故事类型
|
||||||
@ -415,56 +451,67 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
|||||||
});
|
});
|
||||||
}, [uploadFile]);
|
}, [uploadFile]);
|
||||||
|
|
||||||
const actionMovie = useCallback(async (
|
const actionMovie = useCallback(
|
||||||
user_id: string,
|
async (
|
||||||
mode: "auto" | "manual" = "auto",
|
user_id: string,
|
||||||
resolution: "720p" | "1080p" | "4k" = "720p",
|
mode: "auto" | "manual" = "auto",
|
||||||
language: string = "English"
|
resolution: "720p" | "1080p" | "4k" = "720p",
|
||||||
) => {
|
language: string = "English"
|
||||||
try {
|
) => {
|
||||||
if (hasAnalyzed) {
|
try {
|
||||||
// 从charactersAnalysis中提取whisk_caption字段组成数组
|
if (hasAnalyzed) {
|
||||||
const character_briefs = charactersAnalysis.map(char => {
|
// 从charactersAnalysis中提取whisk_caption字段组成数组
|
||||||
|
const character_briefs = charactersAnalysis.map((char) => {
|
||||||
|
return {
|
||||||
|
name: char.role_name,
|
||||||
|
image_url: char.crop_url,
|
||||||
|
character_analysis: JSON.parse(char.whisk_caption)
|
||||||
|
.character_analysis,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
const params: CreateMovieProjectV2Request = {
|
||||||
name:char.role_name,
|
script: storyContent,
|
||||||
image_url:char.crop_url,
|
user_id,
|
||||||
character_analysis:JSON.parse(char.whisk_caption).character_analysis
|
mode,
|
||||||
}
|
resolution,
|
||||||
});
|
genre: selectedCategory,
|
||||||
|
character_briefs,
|
||||||
|
language,
|
||||||
|
image_url: activeImageUrl,
|
||||||
|
};
|
||||||
|
|
||||||
const params: CreateMovieProjectV2Request = {
|
// 调用create_movie_project_v2接口
|
||||||
script: storyContent,
|
const result = await createMovieProjectV2(params);
|
||||||
user_id,
|
return result.data;
|
||||||
mode,
|
}
|
||||||
resolution,
|
} catch (error) {
|
||||||
genre: selectedCategory,
|
console.error("创建电影项目失败:", error);
|
||||||
character_briefs,
|
|
||||||
language,
|
|
||||||
image_url: activeImageUrl,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 调用create_movie_project_v2接口
|
|
||||||
const result = await createMovieProjectV2(params)
|
|
||||||
return result.data;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
},
|
||||||
console.error("创建电影项目失败:", error);
|
[
|
||||||
}
|
hasAnalyzed,
|
||||||
}, [hasAnalyzed, storyContent, charactersAnalysis, selectedCategory, activeImageUrl]);
|
storyContent,
|
||||||
|
charactersAnalysis,
|
||||||
|
selectedCategory,
|
||||||
|
activeImageUrl,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传人物头像并分析特征,替换旧的角色数据
|
* 上传人物头像并分析特征,替换旧的角色数据
|
||||||
* @param {string} characterName - 角色名称
|
* @param {string} characterName - 角色名称
|
||||||
*/
|
*/
|
||||||
const uploadCharacterAvatarAndAnalyzeFeatures = useCallback(async (characterName: string): Promise<void> => {
|
const uploadCharacterAvatarAndAnalyzeFeatures = useCallback(
|
||||||
|
async (characterName: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// 调用用例处理人物头像上传和特征分析
|
// 调用用例处理人物头像上传和特征分析
|
||||||
const result = await imageStoryUseCase.uploadCharacterAvatarAndAnalyzeFeatures(
|
const result =
|
||||||
uploadFile
|
await imageStoryUseCase.uploadCharacterAvatarAndAnalyzeFeatures(
|
||||||
);
|
uploadFile
|
||||||
|
);
|
||||||
|
|
||||||
// 用新的头像和特征描述替换旧的角色数据
|
// 用新的头像和特征描述替换旧的角色数据
|
||||||
setCharactersAnalysis((prev) =>
|
setCharactersAnalysis((prev) =>
|
||||||
@ -473,7 +520,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
|||||||
? {
|
? {
|
||||||
...char,
|
...char,
|
||||||
crop_url: result.crop_url,
|
crop_url: result.crop_url,
|
||||||
whisk_caption: result.whisk_caption
|
whisk_caption: result.whisk_caption,
|
||||||
}
|
}
|
||||||
: char
|
: char
|
||||||
)
|
)
|
||||||
@ -486,7 +533,9 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [imageStoryUseCase, uploadFile]);
|
},
|
||||||
|
[imageStoryUseCase, uploadFile]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
imageStory,
|
imageStory,
|
||||||
@ -499,6 +548,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
|||||||
hasAnalyzed,
|
hasAnalyzed,
|
||||||
avatarComputed,
|
avatarComputed,
|
||||||
originalUserDescription,
|
originalUserDescription,
|
||||||
|
taskProgress,
|
||||||
setCharactersAnalysis,
|
setCharactersAnalysis,
|
||||||
uploadAndAnalyzeImage,
|
uploadAndAnalyzeImage,
|
||||||
triggerFileSelection,
|
triggerFileSelection,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ImageStoryEntity } from "../domain/Entities";
|
import { ImageStoryEntity } from "../domain/Entities";
|
||||||
import { AIGenerateImageStory } from "@/api/movie_start";
|
import { AIGenerateImageStory, getMovieStoryTask } from "@/api/movie_start";
|
||||||
import { MovieStartDTO, CharacterAnalysis } from "@/api/DTO/movie_start_dto";
|
import { MovieStartDTO, CharacterAnalysis } from "@/api/DTO/movie_start_dto";
|
||||||
import { generateCharacterBrief } from "@/api/video_flow";
|
import { generateCharacterBrief } from "@/api/video_flow";
|
||||||
|
|
||||||
@ -32,6 +32,7 @@ export class ImageStoryUseCase {
|
|||||||
|
|
||||||
/** 是否正在上传 */
|
/** 是否正在上传 */
|
||||||
isUploading: boolean = false;
|
isUploading: boolean = false;
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,10 +89,9 @@ export class ImageStoryUseCase {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用AI分析图片
|
* 使用AI分析图片
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
async analyzeImageWithAI() {
|
async analyzeImageWithAI() {
|
||||||
console.log("this.imageStory.imageUrl", this.imageStory.imageUrl);
|
|
||||||
try {
|
try {
|
||||||
//
|
//
|
||||||
// 调用AI分析接口
|
// 调用AI分析接口
|
||||||
@ -100,27 +100,53 @@ export class ImageStoryUseCase {
|
|||||||
user_text: this.imageStory.imageStory || "",
|
user_text: this.imageStory.imageStory || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.successful && response.data) {
|
return response.data.task_id;
|
||||||
// ! 后端实际返回的是对象 但是由于前端只是做字符串数据的转交,所以这里就处理成字符串
|
|
||||||
// ! 至于为什么这里是前端来处理,因为后端这个数据,很多时候都说要以对象方式使用,唯独给AI时,是字符串
|
|
||||||
// ! 然后后端就不处理这个东西了,就给前端来处理了,真 懒
|
|
||||||
response.data.characters_analysis.forEach((character) => {
|
|
||||||
character.whisk_caption = JSON.stringify(character.whisk_caption);
|
|
||||||
});
|
|
||||||
// 解析并存储新的数据结构
|
|
||||||
this.parseAndStoreAnalysisData(response.data);
|
|
||||||
|
|
||||||
// 组合成ImageStoryEntity
|
|
||||||
this.composeImageStoryEntity(response.data);
|
|
||||||
return this.imageStory;
|
|
||||||
} else {
|
|
||||||
throw new Error("AI分析失败");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("AI分析失败:", error);
|
console.error("AI分析失败:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 轮询查状态
|
||||||
|
* @param taskId - 任务ID
|
||||||
|
* @param interval - 轮询间隔时间
|
||||||
|
*/
|
||||||
|
async pollTaskStatus(taskId: string, interval: number = 1000) {
|
||||||
|
// 好老套方案,但是有效
|
||||||
|
let self = this;
|
||||||
|
return {
|
||||||
|
async *[Symbol.asyncIterator]() {
|
||||||
|
while (true) {
|
||||||
|
|
||||||
|
const response = await getMovieStoryTask(taskId);
|
||||||
|
console.log("taskId", taskId,response);
|
||||||
|
if (response.successful && response.data) {
|
||||||
|
if (response.data.result) {
|
||||||
|
response.data.result.characters_analysis?.forEach((character) => {
|
||||||
|
character.whisk_caption = JSON.stringify(
|
||||||
|
character.whisk_caption
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// 解析并存储新的数据结构
|
||||||
|
self.parseAndStoreAnalysisData(response.data.result);
|
||||||
|
|
||||||
|
// 组合成ImageStoryEntity
|
||||||
|
self.composeImageStoryEntity(response.data.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield {
|
||||||
|
status: response.data.status,
|
||||||
|
imageStory: self.imageStory,
|
||||||
|
progress: response.data.progress,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error("AI分析失败");
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, interval));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析并存储分析数据到类属性中
|
* 解析并存储分析数据到类属性中
|
||||||
@ -159,7 +185,7 @@ export class ImageStoryUseCase {
|
|||||||
this.setImageStory({
|
this.setImageStory({
|
||||||
...this.imageStory,
|
...this.imageStory,
|
||||||
imageAnalysis: data.story_logline || "",
|
imageAnalysis: data.story_logline || "",
|
||||||
storyType: data.potential_genres[0] || "", // 使用第一个分类作为故事类型
|
storyType: data.potential_genres?.[0] || "", // 使用第一个分类作为故事类型
|
||||||
roleImage,
|
roleImage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -271,7 +297,9 @@ export class ImageStoryUseCase {
|
|||||||
// 3. 返回新的头像URL和特征描述,用于替换旧数据
|
// 3. 返回新的头像URL和特征描述,用于替换旧数据
|
||||||
const result = {
|
const result = {
|
||||||
crop_url: imageUrl,
|
crop_url: imageUrl,
|
||||||
whisk_caption: JSON.stringify(analysisResult.data.character_brief),
|
whisk_caption: JSON.stringify(
|
||||||
|
analysisResult.data.character_brief
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清理临时元素
|
// 清理临时元素
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
@ -819,6 +819,7 @@ const PhotoStoryModal = ({
|
|||||||
selectedCategory,
|
selectedCategory,
|
||||||
isLoading,
|
isLoading,
|
||||||
hasAnalyzed,
|
hasAnalyzed,
|
||||||
|
taskProgress,
|
||||||
updateStoryType,
|
updateStoryType,
|
||||||
updateStoryContent,
|
updateStoryContent,
|
||||||
updateCharacterName,
|
updateCharacterName,
|
||||||
@ -839,6 +840,11 @@ const PhotoStoryModal = ({
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const taskProgressRef = useRef(taskProgress);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
taskProgressRef.current = taskProgress;
|
||||||
|
}, [taskProgress]);
|
||||||
// 处理图片上传
|
// 处理图片上传
|
||||||
const handleImageUpload = async (e: any) => {
|
const handleImageUpload = async (e: any) => {
|
||||||
const target = e.target as HTMLImageElement;
|
const target = e.target as HTMLImageElement;
|
||||||
@ -889,9 +895,10 @@ const PhotoStoryModal = ({
|
|||||||
let timeout = 100;
|
let timeout = 100;
|
||||||
let timer: NodeJS.Timeout;
|
let timer: NodeJS.Timeout;
|
||||||
timer = setInterval(() => {
|
timer = setInterval(() => {
|
||||||
|
const currentProgress = taskProgressRef.current;
|
||||||
setLocalLoading((prev) => {
|
setLocalLoading((prev) => {
|
||||||
if (prev >= 95) {
|
if (prev >= currentProgress && currentProgress != 0) {
|
||||||
return 95;
|
return currentProgress;
|
||||||
}
|
}
|
||||||
return prev + 0.1;
|
return prev + 0.1;
|
||||||
});
|
});
|
||||||
@ -899,17 +906,8 @@ const PhotoStoryModal = ({
|
|||||||
try {
|
try {
|
||||||
await uploadAndAnalyzeImage();
|
await uploadAndAnalyzeImage();
|
||||||
} finally {
|
} finally {
|
||||||
timeout = 10;
|
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
timer = setInterval(() => {
|
setLocalLoading(0);
|
||||||
setLocalLoading((prev) => {
|
|
||||||
if (prev >= 100) {
|
|
||||||
clearInterval(timer);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return prev + 1;
|
|
||||||
});
|
|
||||||
}, timeout);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -942,22 +940,22 @@ const PhotoStoryModal = ({
|
|||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
{/* 左侧:图片上传 */}
|
{/* 左侧:图片上传 */}
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<div
|
<div
|
||||||
data-alt="image-upload-area"
|
data-alt="image-upload-area"
|
||||||
className={`w-32 h-32 rounded-lg flex flex-col items-center justify-center transition-all duration-300 cursor-pointer ${
|
className={`w-32 h-32 rounded-lg flex flex-col items-center justify-center transition-all duration-300 cursor-pointer ${
|
||||||
activeImageUrl
|
activeImageUrl
|
||||||
? "border-2 border-white/20 bg-white/[0.05]"
|
? "border-2 border-white/20 bg-white/[0.05]"
|
||||||
: "border-2 border-dashed border-white/20 bg-white/[0.02] hover:border-white/40 hover:bg-white/[0.05] hover:scale-105"
|
: "border-2 border-dashed border-white/20 bg-white/[0.02] hover:border-white/40 hover:bg-white/[0.05] hover:scale-105"
|
||||||
}`}
|
}`}
|
||||||
onClick={handleImageUpload}
|
onClick={handleImageUpload}
|
||||||
>
|
>
|
||||||
{activeImageUrl ? (
|
{activeImageUrl ? (
|
||||||
<div className="relative w-full h-full">
|
<div className="relative w-full h-full">
|
||||||
<img
|
<img
|
||||||
src={activeImageUrl}
|
src={activeImageUrl}
|
||||||
alt="Story inspiration"
|
alt="Story inspiration"
|
||||||
className="w-full h-full object-cover rounded-lg bg-white/[0.05]"
|
className="w-full h-full object-cover rounded-lg bg-white/[0.05]"
|
||||||
/>
|
/>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="Clear all content"
|
title="Clear all content"
|
||||||
description="Are you sure you want to clear all content? This action cannot be undone."
|
description="Are you sure you want to clear all content? This action cannot be undone."
|
||||||
@ -1003,17 +1001,17 @@ const PhotoStoryModal = ({
|
|||||||
key={`${avatar.name}-${index}`}
|
key={`${avatar.name}-${index}`}
|
||||||
className="flex flex-col items-center"
|
className="flex flex-col items-center"
|
||||||
>
|
>
|
||||||
<div className="relative w-20 h-20 rounded-sm overflow-hidden bg-white/[0.05] border border-white/[0.1] mb-2 group cursor-pointer">
|
<div className="relative w-20 h-20 rounded-sm overflow-hidden bg-white/[0.05] border border-white/[0.1] mb-2 group cursor-pointer">
|
||||||
<img
|
<img
|
||||||
src={avatar.url}
|
src={avatar.url}
|
||||||
alt={avatar.name}
|
alt={avatar.name}
|
||||||
className="w-full h-full object-cover bg-white/[0.05]"
|
className="w-full h-full object-cover bg-white/[0.05]"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
// 如果裁剪的头像加载失败,回退到原图
|
// 如果裁剪的头像加载失败,回退到原图
|
||||||
const target = e.target as HTMLImageElement;
|
const target = e.target as HTMLImageElement;
|
||||||
target.src = activeImageUrl;
|
target.src = activeImageUrl;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* 删除角色按钮 - 使用Tooltip并调整z-index避免被遮挡 */}
|
{/* 删除角色按钮 - 使用Tooltip并调整z-index避免被遮挡 */}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title="Remove this character from the movie"
|
title="Remove this character from the movie"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user