新的阶段

This commit is contained in:
海龙 2025-08-21 23:46:38 +08:00
parent e405f4bd7d
commit 85687f5840
5 changed files with 271 additions and 143 deletions

View File

@ -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请求参数
*/ */

View File

@ -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}`
);
};

View File

@ -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,8 +226,9 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
return []; return [];
} }
return charactersAnalysis.map((character) => { return charactersAnalysis
console.log('character', character) .map((character) => {
console.log("character", character);
// 如果已经有头像URL直接返回 // 如果已经有头像URL直接返回
if (character.crop_url) { if (character.crop_url) {
return { return {
@ -234,7 +244,8 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
// 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,11 +254,9 @@ 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 = () => {
const newImageStory = await imageStoryUseCase.handleImageUpload(
activeImageUrl
);
setOriginalUserDescription(storyContent); setOriginalUserDescription(storyContent);
// 获取更新后的数据 // 获取更新后的数据
const updatedStory = imageStoryUseCase.storyLogline; const updatedStory = imageStoryUseCase.storyLogline;
@ -265,6 +274,34 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
// 标记已分析 // 标记已分析
setHasAnalyzed(true); setHasAnalyzed(true);
};
// 调用用例处理图片上传和分析
const taskId = await imageStoryUseCase.handleImageUpload(activeImageUrl);
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) { } 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,7 +451,8 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
}); });
}, [uploadFile]); }, [uploadFile]);
const actionMovie = useCallback(async ( const actionMovie = useCallback(
async (
user_id: string, user_id: string,
mode: "auto" | "manual" = "auto", mode: "auto" | "manual" = "auto",
resolution: "720p" | "1080p" | "4k" = "720p", resolution: "720p" | "1080p" | "4k" = "720p",
@ -424,13 +461,13 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
try { try {
if (hasAnalyzed) { if (hasAnalyzed) {
// 从charactersAnalysis中提取whisk_caption字段组成数组 // 从charactersAnalysis中提取whisk_caption字段组成数组
const character_briefs = charactersAnalysis.map(char => { const character_briefs = charactersAnalysis.map((char) => {
return { return {
name:char.role_name, name: char.role_name,
image_url:char.crop_url, image_url: char.crop_url,
character_analysis:JSON.parse(char.whisk_caption).character_analysis character_analysis: JSON.parse(char.whisk_caption)
} .character_analysis,
};
}); });
const params: CreateMovieProjectV2Request = { const params: CreateMovieProjectV2Request = {
@ -445,24 +482,34 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
}; };
// 调用create_movie_project_v2接口 // 调用create_movie_project_v2接口
const result = await createMovieProjectV2(params) const result = await createMovieProjectV2(params);
return result.data; return result.data;
} }
} catch (error) { } catch (error) {
console.error("创建电影项目失败:", error); console.error("创建电影项目失败:", error);
} }
}, [hasAnalyzed, storyContent, charactersAnalysis, selectedCategory, activeImageUrl]); },
[
hasAnalyzed,
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 =
await imageStoryUseCase.uploadCharacterAvatarAndAnalyzeFeatures(
uploadFile uploadFile
); );
@ -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,

View File

@ -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
),
}; };
// 清理临时元素 // 清理临时元素

View File

@ -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);
} }
}; };