模板测试

This commit is contained in:
海龙 2025-08-25 17:29:00 +08:00
parent fc19d6e25b
commit ef53a577d4
6 changed files with 445 additions and 60 deletions

View File

@ -84,6 +84,8 @@ export interface StoryAnalysisTask{
* V2请求参数
*/
export interface CreateMovieProjectV2Request {
/** 任务ID */
project_id:string;
/** 剧本内容 */
script: string;
/** 用户ID */
@ -98,7 +100,7 @@ export interface CreateMovieProjectV2Request {
character_briefs: {
name:string;
image_url:string;
character_analysis:Record<string,any>;
character_analysis:string;
}[];
/** 语言 */
language: string;
@ -198,7 +200,7 @@ export interface CreateMovieProjectV3Request {
user_id: string;
/** 模式auto | manual */
mode: "auto" | "manual";
/** 分辨率720p | 1080p | 4k */
/** 分辨率720p | 1080p | "4k" */
resolution: "720p" | "1080p" | "4k";
/** 语言 */
language: string;
@ -219,4 +221,85 @@ export interface CreateMovieProjectV3Request {
/**声音URL */
voice_url: string;
}[];
/** 可填写的变量字段 */
fillable_content?: {
/** 字段名称 */
field_name: string;
/** 字段类型 */
field_type: string;
/** 字段值 */
field_value?: string;
/** 字段描述 */
field_description?: string;
/** 字段元数据 */
field_meta?: Record<string, any>;
}[];
}
/**
* Gemini文本转图像请求参数
*/
export interface GeminiTextToImageRequest {
/** 提示词 */
prompt: string;
}
/**
* Gemini文本转图像响应数据
*/
export interface GeminiTextToImageData {
/** 生成的图像URL */
image_url: string;
}
/**
* Gemini文本转图像响应
*/
export interface GeminiTextToImageResponse {
/** 响应码 */
code: number;
/** 响应消息 */
message: string;
/** 响应数据 */
data: GeminiTextToImageData;
/** 是否成功 */
successful: boolean;
}
/**
*
*/
export interface TextToImageRequest {
/** 图像描述 */
description: string;
}
/**
*
*/
export interface TextToImageData {
/** 生成的图像URL */
image_url: string;
/** 本地图像路径 */
local_image_path: string;
/** 是否成功 */
success: boolean;
/** 描述 */
description: string;
/** 宽高比 */
aspect_ratio: string;
}
/**
*
*/
export interface TextToImageResponse {
/** 响应码 */
code: number;
/** 响应消息 */
message: string;
/** 响应数据 */
data: TextToImageData;
/** 是否成功 */
successful: boolean;
}

View File

@ -6,6 +6,10 @@ import {
StoryAnalysisTask,
MovieStoryTaskDetail,
CreateMovieProjectV3Request,
GeminiTextToImageRequest,
GeminiTextToImageResponse,
TextToImageRequest,
TextToImageResponse,
} from "./DTO/movie_start_dto";
import { get, post } from "./request";
import {
@ -78,3 +82,31 @@ export const getMovieStoryTask = async (taskId: string) => {
`/movie_story/task/${taskId}`
);
};
/**
* Gemini文本转图像生成
* @param request -
* @returns Promise<GeminiTextToImageResponse>
*/
export const generateGeminiTextToImage = async (
request: GeminiTextToImageRequest
): Promise<GeminiTextToImageResponse> => {
return await post<GeminiTextToImageResponse>(
"/gemini-text-to-image/generate",
request
);
};
/**
*
* @param request -
* @returns Promise<TextToImageResponse>
*/
export const generateTextToImage = async (
request: TextToImageRequest
): Promise<TextToImageResponse> => {
return await post<TextToImageResponse>(
"/text-to-image/draw",
request
);
};

View File

@ -103,6 +103,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
const [taskProgress, setTaskProgress] = useState(0);
// 使用上传文件Hook
const { uploadFile } = useUploadFile();
const [taskId, setTaskId] = useState<string>("");
/** 图片故事用例实例 */
const imageStoryUseCase = useMemo(() => new ImageStoryUseCase(), []);
@ -278,7 +279,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
// 调用用例处理图片上传和分析
const taskId = await imageStoryUseCase.handleImageUpload(activeImageUrl);
setTaskId(taskId);
for await (const result of await imageStoryUseCase.pollTaskStatus(
taskId
)) {
@ -465,8 +466,8 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
return {
name: char.role_name,
image_url: char.crop_url,
character_analysis: JSON.parse(char.whisk_caption)
.character_analysis,
character_analysis: char.role_name+":"+JSON.parse(char.whisk_caption)
?.character_analysis?.brief,
};
});
@ -479,6 +480,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
character_briefs,
language,
image_url: activeImageUrl,
project_id:taskId
};
// 调用create_movie_project_v2接口

View File

@ -1,6 +1,11 @@
import { message } from "antd";
import { StoryTemplateEntity } from "../domain/Entities";
import { useUploadFile } from "../domain/service";
import { debounce } from "lodash";
import { TemplateStoryUseCase } from "../usecase/templateStoryUseCase";
import { useState, useCallback, useMemo } from "react";
import { createMovieProjectV3, generateTextToImage } from "@/api/movie_start";
import { CreateMovieProjectV3Request } from "@/api/DTO/movie_start_dto";
/** 模板角色接口 */
interface TemplateRole {
@ -11,11 +16,6 @@ interface TemplateRole {
/** 声音URL */
voice_url: string;
}
import { TemplateStoryUseCase } from "../usecase/templateStoryUseCase";
import { getUploadToken, uploadToQiniu } from "@/api/common";
import { useState, useCallback, useMemo } from "react";
import { createMovieProjectV3 } from "@/api/movie_start";
import { CreateMovieProjectV3Request } from "@/api/DTO/movie_start_dto";
interface UseTemplateStoryService {
/** 模板列表 */
@ -42,9 +42,9 @@ interface UseTemplateStoryService {
actionStory: (
user_id: string,
mode: "auto" | "manual",
resolution: "720p" | "1080p" | "4k" ,
resolution: "720p" | "1080p" | "4k",
language: string
) => Promise<string|undefined>;
) => Promise<string | undefined>;
/** 设置选中的模板 */
setSelectedTemplate: (template: StoryTemplateEntity | null) => void;
/** 设置活跃角色索引 */
@ -55,7 +55,13 @@ interface UseTemplateStoryService {
/**清空数据 */
clearData: () => void;
/** 上传人物头像并分析 */
AvatarAndAnalyzeFeatures: (imageUrl: string) => Promise<void>;
AvatarAndAnalyzeFeatures: (imageUrl: string, roleName?: string) => Promise<void>;
/** 更新指定角色的图片 */
updateRoleImage: (roleName: string, imageUrl: string) => void;
/** 更新变量字段值 */
updateFillableContentField: (fieldName: string, fieldValue: string) => void;
/** 带防抖的失焦处理函数 */
handleFieldBlur: (fieldName: string, fieldValue: string) => void;
}
export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
@ -96,7 +102,7 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
setTemplateStoryList(templates);
setSelectedTemplate(templates[0]);
setActiveRoleIndex(0);
console.log(selectedTemplate, activeRoleIndex)
console.log(selectedTemplate, activeRoleIndex);
} catch (err) {
console.error("获取模板列表失败:", err);
} finally {
@ -115,7 +121,7 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
* URL
*/
const setActiveRoleData = useCallback(
(imageUrl: string, desc: string): void => {
(imageUrl: string): void => {
if (
!selectedTemplate ||
activeRoleIndex < 0 ||
@ -125,12 +131,11 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
return;
}
try {
const character_brief = {
name: selectedTemplate.storyRole[activeRoleIndex].role_name,
image_url: imageUrl,
character_analysis: JSON.parse(desc).character_analysis,
};
// const character_brief = {
// name: selectedTemplate.storyRole[activeRoleIndex].role_name,
// image_url: imageUrl,
// character_analysis: JSON.parse(desc).character_analysis,
// };
const updatedTemplate = {
...selectedTemplate,
storyRole: selectedTemplate.storyRole.map((role, index) =>
@ -138,7 +143,6 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
? {
...role,
photo_url: imageUrl,
role_description: character_brief,
}
: role
),
@ -177,22 +181,48 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
[selectedTemplate, activeRoleIndex]
);
/**
*
*/
const updateFillableContentField = useCallback(
(fieldName: string, fieldValue: string): void => {
if (!selectedTemplate) return;
const updatedTemplate = {
...selectedTemplate,
fillable_content: selectedTemplate.fillable_content.map((field) =>
field.field_name === fieldName
? { ...field, field_value: fieldValue }
: field
),
};
setSelectedTemplate(updatedTemplate);
},
[selectedTemplate]
);
/**
*
* @param {string} characterName -
* @param {string} imageUrl - URL
* @param {string} roleName - 使
*/
const AvatarAndAnalyzeFeatures = useCallback(
async (imageUrl: string): Promise<void> => {
async (imageUrl: string, roleName?: string): Promise<void> => {
try {
setIsLoading(true);
// 调用用例处理人物头像上传和特征分析
const result = await templateStoryUseCase.AvatarAndAnalyzeFeatures(
imageUrl
);
// 如果提供了角色名称,更新指定角色;否则更新当前活跃角色
if (roleName) {
updateRoleImage(roleName, imageUrl);
} else {
setActiveRoleData(imageUrl);
}
setActiveRoleData(result.crop_url, result.whisk_caption);
console.log("人物头像和特征描述更新成功:", result);
// 调用用例处理人物头像上传和特征分析
// const result = await templateStoryUseCase.AvatarAndAnalyzeFeatures(
// imageUrl
// );
} catch (error) {
console.error("人物头像上传和特征分析失败:", error);
throw error;
@ -200,8 +230,64 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
setIsLoading(false);
}
},
[templateStoryUseCase]
[setActiveRoleData]
);
/**
*
* @param {string} roleName -
* @param {string} imageUrl - URL
*/
const updateRoleImage = useCallback(
(roleName: string, imageUrl: string): void => {
if (!selectedTemplate) return;
const updatedTemplate = {
...selectedTemplate,
storyRole: selectedTemplate.storyRole.map((role) =>
role.role_name === roleName
? { ...role, photo_url: imageUrl }
: role
),
};
setSelectedTemplate(updatedTemplate);
},
[selectedTemplate]
);
/**
*
* @param {string} fieldName -
* @param {string} fieldValue -
*/
const handleFieldBlur = useCallback(
debounce(async (fieldName: string, fieldValue: string): Promise<void> => {
try {
// 设置 loading 状态
setIsLoading(true);
// 调用图片生成接口
const result = await generateTextToImage({
description: fieldValue
});
if (result.successful && result.data?.image_url) {
// 更新对应角色的图片
updateRoleImage(fieldName, result.data.image_url);
console.log(`字段 ${fieldName} 图片生成成功:`, result.data.image_url);
} else {
console.error(`字段 ${fieldName} 图片生成失败:`, result.message);
}
} catch (error) {
console.error(`字段 ${fieldName} 处理失败:`, error);
} finally {
// 清除 loading 状态
setIsLoading(false);
}
}, 500),
[updateRoleImage, setIsLoading]
);
const actionStory = useCallback(
async (
user_id: string,
@ -209,7 +295,11 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
resolution: "720p" | "1080p" | "4k" = "720p",
language: string = "English"
) => {
console.log('selectedTemplate', selectedTemplate)
try {
// 设置 loading 状态
setIsLoading(true);
const params: CreateMovieProjectV3Request = {
user_id,
mode,
@ -217,12 +307,16 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
storyRole: selectedTemplate?.storyRole || [],
language,
template_id: selectedTemplate?.template_id || "",
fillable_content: selectedTemplate?.fillable_content || [],
};
console.log("params", params);
const result = await createMovieProjectV3(params);
return result.data.project_id as string;
} catch (error) {
console.error("创建电影项目失败:", error);
} finally {
// 清除 loading 状态
setIsLoading(false);
}
},
[selectedTemplate]
@ -239,6 +333,9 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
setActiveRoleIndex: handleSetActiveRoleIndex,
setActiveRoleAudio,
AvatarAndAnalyzeFeatures,
updateRoleImage,
updateFillableContentField,
handleFieldBlur,
clearData: () => {
setTemplateStoryList([]);
setSelectedTemplate(null);

View File

@ -88,7 +88,7 @@ export interface VideoSegmentEntity {
video_status: number | null;
}[];
/**视频片段状态 0:视频加载中 1:任务已完成 2:任务失败 */
status: number|null;
status: number | null;
/**镜头项 */
lens: LensType[];
}
@ -152,9 +152,23 @@ export interface StoryTemplateEntity {
image_url: string;
character_analysis: Record<string, any>;
};
/**照片URL */
photo_url: string;
/**声音URL */
voice_url: string;
}[];
/**可填的内容 */
fillable_content: {
/** 字段名称 */
field_name: string;
/** 字段类型 */
field_type: string;
/** 字段值 */
field_value?: string;
/** 字段描述 */
field_description?: string;
/** 字段元数据 */
field_meta?: Record<string, any>;
}[];
}

View File

@ -18,14 +18,7 @@ import {
Sparkles,
Settings,
} from "lucide-react";
import {
Dropdown,
Modal,
Tooltip,
Upload,
Popconfirm,
Image,
} from "antd";
import { Dropdown, Modal, Tooltip, Upload, Popconfirm, Image } from "antd";
import { UploadOutlined } from "@ant-design/icons";
import { StoryTemplateEntity } from "@/app/service/domain/Entities";
import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService";
@ -72,6 +65,8 @@ const RenderTemplateStoryMode = ({
setActiveRoleIndex,
AvatarAndAnalyzeFeatures,
setActiveRoleAudio,
updateFillableContentField,
handleFieldBlur,
clearData,
} = useTemplateStoryServiceHook();
@ -114,7 +109,12 @@ const RenderTemplateStoryMode = ({
console.error("用户未登录");
return;
}
const projectId = await actionStory(String(User.id), configOptions.mode, configOptions.resolution, configOptions.language);
const projectId = await actionStory(
String(User.id),
configOptions.mode,
configOptions.resolution,
configOptions.language
);
if (projectId) {
// 跳转到电影详情页
router.push(`/create/work-flow?episodeId=${projectId}`);
@ -204,9 +204,166 @@ const RenderTemplateStoryMode = ({
</div>
</div>
</div>
{/* 变量字段填写区域 */}
{selectedTemplate?.fillable_content &&
selectedTemplate.fillable_content.length > 0 && (
<div className="p-4 border-t border-white/10">
<h3
data-alt="variables-section-title"
className="text-lg font-semibold text-white mb-4"
>
Template Configuration
</h3>
<div className="space-y-4">
{selectedTemplate.fillable_content.map((field, index) => (
<div
key={index}
data-alt={`variable-field-${index}`}
className="flex items-center gap-4"
>
{/* 字段名称 */}
<div className="w-22 flex items-center gap-2">
{/* 图片缩略图 - 显示对应角色的图片 */}
<div className="relative group flex items-center justify-center">
<Tooltip title={field.field_name} placement="top">
<div
data-alt={`field-thumbnail-${index}`}
className="w-16 h-16 rounded-lg overflow-hidden border border-white/20 bg-white/[0.05] flex items-center justify-center cursor-pointer hover:scale-105 transition-all duration-200"
>
<Image
src={
selectedTemplate?.storyRole?.find(
(role) => role.role_name === field.field_name
)?.photo_url || "/assets/empty_video.png"
}
alt={field.field_name}
className="w-full h-full object-cover"
preview={{
mask: null,
maskClassName: "hidden",
}}
fallback="/assets/empty_video.png"
/>
</div>
</Tooltip>
{/* 上传按钮 - 右上角 */}
<Upload
name="fieldImage"
showUploadList={false}
beforeUpload={(file) => {
// 验证文件类型
const isImage = file.type.startsWith("image/");
if (!isImage) {
console.error("只能上传图片文件");
return false;
}
// 验证文件大小 (5MB)
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
console.error("图片大小不能超过5MB");
return false;
}
return true;
}}
customRequest={async ({
file,
onSuccess,
onError,
}) => {
try {
const fileObj = file as File;
console.log(
"开始上传字段图片文件:",
fileObj.name,
fileObj.type,
fileObj.size
);
// 使用 hook 上传文件到七牛云
const uploadedUrl = await uploadFile(
fileObj,
(progress) => {
console.log(`上传进度: ${progress}%`);
}
);
console.log(
"字段图片上传成功URL:",
uploadedUrl
);
// 调用 AvatarAndAnalyzeFeatures 更新对应角色的图片
await AvatarAndAnalyzeFeatures(
uploadedUrl,
field.field_name
);
onSuccess?.(uploadedUrl);
} catch (error) {
console.error("字段图片上传失败:", error);
onError?.(error as Error);
}
}}
>
<button
data-alt={`field-upload-button-${index}`}
className="absolute -top-1 -right-1 w-6 h-6 bg-blue-500 hover:bg-blue-600 text-white rounded-full flex items-center justify-center transition-all duration-200 opacity-0 group-hover:opacity-100 hover:scale-110 shadow-lg"
title="change field image"
>
<UploadOutlined className="w-3.5 h-3.5" />
</button>
</Upload>
</div>
</div>
{/* 输入框 */}
<div className="flex-1 flex items-center relative">
<input
type="text"
value={field.field_value || ""}
onChange={(e) =>
updateFillableContentField(
field.field_name,
e.target.value
)
}
onBlur={(e) =>
handleFieldBlur(field.field_name, e.target.value)
}
placeholder={`${field.field_description}`}
className="w-full px-3 py-2 pr-10 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200"
/>
{/* 问号提示 - 直接放在输入框内部右侧 */}
{field.field_description && (
<Tooltip
title={field.field_description}
placement="top"
classNames={{
root: "max-w-xs",
}}
>
<div
data-alt={`field-help-${index}`}
className="absolute right-3 top-1/2 -translate-y-1/2 w-5 h-5 rounded-full bg-white/10 border border-white/20 flex items-center justify-center cursor-help hover:bg-white/20 transition-colors duration-200"
>
<span className="text-white text-xs font-bold">
?
</span>
</div>
</Tooltip>
)}
</div>
</div>
))}
</div>
</div>
)}
{/* 角色自定义部分 - 精简布局 */}
<div className="p-4">
{/* <div className="p-4">
<h3
data-alt="roles-section-title"
className="text-lg font-semibold text-white mb-4"
@ -215,11 +372,11 @@ const RenderTemplateStoryMode = ({
</h3>
{/* 紧凑布局 */}
<div className="mb-6 flex gap-4">
{/* <div className="mb-6 flex gap-4">
{/* 左侧:音频部分 */}
<div className="flex-1 space-y-4">
{/* <div className="flex-1 space-y-4">
{/* 音频操作区域 - 使用新的 AudioRecorder 组件 */}
<div className="">
{/* <div className="">
<AudioRecorder
audioUrl={activeRole?.voice_url || ""}
onAudioRecorded={(audioBlob, audioUrl) => {
@ -233,7 +390,7 @@ const RenderTemplateStoryMode = ({
</div>
{/* 右侧:角色图片缩略图列表 - 精简 */}
<div className="w-24 flex flex-col gap-y-[.6rem] ">
{/* <div className="w-24 flex flex-col gap-y-[.6rem] ">
{selectedTemplate.storyRole.map((role, index: number) => (
<div key={index} className="relative group">
<Tooltip title={role.role_name} placement="left">
@ -260,7 +417,7 @@ const RenderTemplateStoryMode = ({
</Tooltip>
{/* 上传按钮 - 右上角 */}
<Tooltip title="更换角色头像" placement="top">
{/* <Tooltip title="" placement="top">
<Upload
name="avatar"
showUploadList={false}
@ -322,8 +479,8 @@ const RenderTemplateStoryMode = ({
))}
</div>
</div>
</div>
<div className=" absolute -bottom-4 right-0">
</div> */}
<div className=" absolute -bottom-8 right-0">
<ActionButton
isCreating={localLoading > 0}
handleCreateVideo={handleConfirm}
@ -371,7 +528,7 @@ const RenderTemplateStoryMode = ({
</h2>
</div>
<div className="flex gap-4 pb-4 ">
<div className="flex gap-4 pb-8 ">
{templateListRender()}
<div className="flex-1">{storyEditorRender()}</div>
</div>