新的阶段

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;
}
/** 电影故事任务详情 */
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请求参数
*/

View File

@ -1,5 +1,11 @@
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 {
StoryTemplateEntity,
@ -30,7 +36,7 @@ export const AIGenerateImageStory = async (request: {
image_url: string;
user_text: string;
}) => {
return await post<ApiResponse<MovieStartDTO>>(
return await post<ApiResponse<StoryAnalysisTask>>(
"/movie_story/generate",
request
);
@ -63,3 +69,14 @@ export const createMovieProjectV3 = async (
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,
SetStateAction,
} 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";
interface UseImageStoryService {
@ -32,6 +36,8 @@ interface UseImageStoryService {
avatarComputed: Array<{ name: string; url: string }>;
/** 原始用户描述 */
originalUserDescription: string;
/** 分析任务进度 */
taskProgress: number;
/** 上传图片并分析 */
uploadAndAnalyzeImage: () => Promise<void>;
/** 触发文件选择 */
@ -52,13 +58,15 @@ interface UseImageStoryService {
mode?: "auto" | "manual",
resolution?: "720p" | "1080p" | "4k",
language?: string
) => Promise<CreateMovieProjectResponse|undefined>;
) => Promise<CreateMovieProjectResponse | undefined>;
/** 设置角色分析 */
setCharactersAnalysis: Dispatch<SetStateAction<CharacterAnalysis[]>>;
/** 设置原始用户描述 */
setOriginalUserDescription: Dispatch<SetStateAction<string>>;
/** 上传人物头像并分析特征,替换旧的角色数据 */
uploadCharacterAvatarAndAnalyzeFeatures: (characterName: string) => Promise<void>;
uploadCharacterAvatarAndAnalyzeFeatures: (
characterName: string
) => Promise<void>;
}
export const useImageStoryServiceHook = (): UseImageStoryService => {
@ -91,7 +99,8 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
// 流程状态
const [isLoading, setIsLoading] = useState(false);
const [hasAnalyzed, setHasAnalyzed] = useState(false);
/** 分析任务进度 */
const [taskProgress, setTaskProgress] = useState(0);
// 使用上传文件Hook
const { uploadFile } = useUploadFile();
@ -217,24 +226,26 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
return [];
}
return charactersAnalysis.map((character) => {
console.log('character', character)
// 如果已经有头像URL直接返回
if (character.crop_url) {
return {
name: character.role_name,
url: character.crop_url,
};
}
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);
// // 异步生成头像URL
// generateAvatarFromRegion(character, activeImageUrl);
// return {
// name: character.role_name,
// url: "", // 初始为空,异步生成完成后会更新
// };
}).filter(Boolean) as { name: string; url: string }[];
// return {
// name: character.role_name,
// url: "", // 初始为空,异步生成完成后会更新
// };
})
.filter(Boolean) as { name: string; url: string }[];
}, [charactersAnalysis, activeImageUrl, generateAvatarFromRegion]);
/**
*
@ -243,28 +254,54 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
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 newImageStory = await imageStoryUseCase.handleImageUpload(
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);
const taskId = await imageStoryUseCase.handleImageUpload(activeImageUrl);
// 将AI分析的故事内容直接更新到统一的故事内容字段
updateStoryContent(updatedStory || "");
// 标记已分析
setHasAnalyzed(true);
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);
@ -275,7 +312,6 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
}
}, [imageStoryUseCase, activeImageUrl, storyContent]);
/**
*
* @param {string} storyType -
@ -415,56 +451,67 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
});
}, [uploadFile]);
const actionMovie = useCallback(async (
user_id: string,
mode: "auto" | "manual" = "auto",
resolution: "720p" | "1080p" | "4k" = "720p",
language: string = "English"
) => {
try {
if (hasAnalyzed) {
// 从charactersAnalysis中提取whisk_caption字段组成数组
const character_briefs = charactersAnalysis.map(char => {
const actionMovie = useCallback(
async (
user_id: string,
mode: "auto" | "manual" = "auto",
resolution: "720p" | "1080p" | "4k" = "720p",
language: string = "English"
) => {
try {
if (hasAnalyzed) {
// 从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 {
name:char.role_name,
image_url:char.crop_url,
character_analysis:JSON.parse(char.whisk_caption).character_analysis
}
});
const params: CreateMovieProjectV2Request = {
script: storyContent,
user_id,
mode,
resolution,
genre: selectedCategory,
character_briefs,
language,
image_url: activeImageUrl,
};
const params: CreateMovieProjectV2Request = {
script: storyContent,
user_id,
mode,
resolution,
genre: selectedCategory,
character_briefs,
language,
image_url: activeImageUrl,
};
// 调用create_movie_project_v2接口
const result = await createMovieProjectV2(params)
return result.data;
// 调用create_movie_project_v2接口
const result = await createMovieProjectV2(params);
return result.data;
}
} catch (error) {
console.error("创建电影项目失败:", error);
}
} catch (error) {
console.error("创建电影项目失败:", error);
}
}, [hasAnalyzed, storyContent, charactersAnalysis, selectedCategory, activeImageUrl]);
},
[
hasAnalyzed,
storyContent,
charactersAnalysis,
selectedCategory,
activeImageUrl,
]
);
/**
*
* @param {string} characterName -
*/
const uploadCharacterAvatarAndAnalyzeFeatures = useCallback(async (characterName: string): Promise<void> => {
/**
*
* @param {string} characterName -
*/
const uploadCharacterAvatarAndAnalyzeFeatures = useCallback(
async (characterName: string): Promise<void> => {
try {
setIsLoading(true);
// 调用用例处理人物头像上传和特征分析
const result = await imageStoryUseCase.uploadCharacterAvatarAndAnalyzeFeatures(
uploadFile
);
const result =
await imageStoryUseCase.uploadCharacterAvatarAndAnalyzeFeatures(
uploadFile
);
// 用新的头像和特征描述替换旧的角色数据
setCharactersAnalysis((prev) =>
@ -473,7 +520,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
? {
...char,
crop_url: result.crop_url,
whisk_caption: result.whisk_caption
whisk_caption: result.whisk_caption,
}
: char
)
@ -486,7 +533,9 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
} finally {
setIsLoading(false);
}
}, [imageStoryUseCase, uploadFile]);
},
[imageStoryUseCase, uploadFile]
);
return {
imageStory,
@ -499,6 +548,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
hasAnalyzed,
avatarComputed,
originalUserDescription,
taskProgress,
setCharactersAnalysis,
uploadAndAnalyzeImage,
triggerFileSelection,

View File

@ -1,5 +1,5 @@
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 { generateCharacterBrief } from "@/api/video_flow";
@ -32,6 +32,7 @@ export class ImageStoryUseCase {
/** 是否正在上传 */
isUploading: boolean = false;
constructor() {}
/**
@ -88,10 +89,9 @@ export class ImageStoryUseCase {
/**
* 使AI分析图片
* @returns {Promise<void>}
* @returns {Promise<string>}
*/
async analyzeImageWithAI() {
console.log("this.imageStory.imageUrl", this.imageStory.imageUrl);
try {
//
// 调用AI分析接口
@ -100,27 +100,53 @@ export class ImageStoryUseCase {
user_text: this.imageStory.imageStory || "",
});
if (response.successful && response.data) {
// ! 后端实际返回的是对象 但是由于前端只是做字符串数据的转交,所以这里就处理成字符串
// ! 至于为什么这里是前端来处理因为后端这个数据很多时候都说要以对象方式使用唯独给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分析失败");
}
return response.data.task_id;
} catch (error) {
console.error("AI分析失败:", 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.imageStory,
imageAnalysis: data.story_logline || "",
storyType: data.potential_genres[0] || "", // 使用第一个分类作为故事类型
storyType: data.potential_genres?.[0] || "", // 使用第一个分类作为故事类型
roleImage,
});
}
@ -271,7 +297,9 @@ export class ImageStoryUseCase {
// 3. 返回新的头像URL和特征描述用于替换旧数据
const result = {
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";
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import {
ChevronDown,
ChevronUp,
@ -819,6 +819,7 @@ const PhotoStoryModal = ({
selectedCategory,
isLoading,
hasAnalyzed,
taskProgress,
updateStoryType,
updateStoryContent,
updateCharacterName,
@ -839,6 +840,11 @@ const PhotoStoryModal = ({
onClose();
};
const router = useRouter();
const taskProgressRef = useRef(taskProgress);
useEffect(() => {
taskProgressRef.current = taskProgress;
}, [taskProgress]);
// 处理图片上传
const handleImageUpload = async (e: any) => {
const target = e.target as HTMLImageElement;
@ -889,9 +895,10 @@ const PhotoStoryModal = ({
let timeout = 100;
let timer: NodeJS.Timeout;
timer = setInterval(() => {
const currentProgress = taskProgressRef.current;
setLocalLoading((prev) => {
if (prev >= 95) {
return 95;
if (prev >= currentProgress && currentProgress != 0) {
return currentProgress;
}
return prev + 0.1;
});
@ -899,17 +906,8 @@ const PhotoStoryModal = ({
try {
await uploadAndAnalyzeImage();
} finally {
timeout = 10;
clearInterval(timer);
timer = setInterval(() => {
setLocalLoading((prev) => {
if (prev >= 100) {
clearInterval(timer);
return 0;
}
return prev + 1;
});
}, timeout);
setLocalLoading(0);
}
};
@ -942,22 +940,22 @@ const PhotoStoryModal = ({
<div className="flex items-start gap-4">
{/* 左侧:图片上传 */}
<div className="flex-shrink-0">
<div
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 ${
activeImageUrl
? "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"
}`}
<div
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 ${
activeImageUrl
? "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"
}`}
onClick={handleImageUpload}
>
{activeImageUrl ? (
<div className="relative w-full h-full">
<img
src={activeImageUrl}
alt="Story inspiration"
className="w-full h-full object-cover rounded-lg bg-white/[0.05]"
/>
<img
src={activeImageUrl}
alt="Story inspiration"
className="w-full h-full object-cover rounded-lg bg-white/[0.05]"
/>
<Popconfirm
title="Clear all content"
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}`}
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">
<img
src={avatar.url}
alt={avatar.name}
className="w-full h-full object-cover bg-white/[0.05]"
onError={(e) => {
// 如果裁剪的头像加载失败,回退到原图
const target = e.target as HTMLImageElement;
target.src = activeImageUrl;
}}
/>
<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
src={avatar.url}
alt={avatar.name}
className="w-full h-full object-cover bg-white/[0.05]"
onError={(e) => {
// 如果裁剪的头像加载失败,回退到原图
const target = e.target as HTMLImageElement;
target.src = activeImageUrl;
}}
/>
{/* 删除角色按钮 - 使用Tooltip并调整z-index避免被遮挡 */}
<Tooltip
title="Remove this character from the movie"