forked from 77media/video-flow
新增分镜服务Hook,支持分镜列表、详情获取、内容更新、角色和场景信息获取等功能;更新API接口以支持新功能,优化状态管理和数据处理逻辑。
This commit is contained in:
parent
8c47eae0a6
commit
291c18ad86
@ -2,7 +2,7 @@ import { post } from './request';
|
|||||||
import { ProjectTypeEnum } from '@/app/model/enums';
|
import { ProjectTypeEnum } from '@/app/model/enums';
|
||||||
import { ApiResponse } from '@/api/common';
|
import { ApiResponse } from '@/api/common';
|
||||||
import { BASE_URL } from './constants'
|
import { BASE_URL } from './constants'
|
||||||
import { AITextEntity, RoleEntity, SceneEntity, ShotEntity, TagEntity } from '@/app/service/domain/Entities';
|
import { AITextEntity, RoleEntity, SceneEntity, ShotEntity, TagEntity, ScriptSliceEntity } from '@/app/service/domain/Entities';
|
||||||
|
|
||||||
// API 响应类型
|
// API 响应类型
|
||||||
interface BaseApiResponse<T> {
|
interface BaseApiResponse<T> {
|
||||||
@ -247,6 +247,75 @@ export const applyRoleToShots = async (request: {
|
|||||||
return post<ApiResponse<any>>('/movie/apply_role_to_shots', request);
|
return post<ApiResponse<any>>('/movie/apply_role_to_shots', request);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色应用到的分镜列表
|
||||||
|
* @param request - 获取角色分镜列表请求参数
|
||||||
|
* @returns Promise<ApiResponse<分镜列表>>
|
||||||
|
*/
|
||||||
|
export const getRoleShots = async (request: {
|
||||||
|
/** 角色ID */
|
||||||
|
roleId: string;
|
||||||
|
}): Promise<ApiResponse<{
|
||||||
|
/** 分镜列表 */
|
||||||
|
shots: ShotEntity[];
|
||||||
|
/** 已应用的分镜ID列表 */
|
||||||
|
appliedShotIds: string[];
|
||||||
|
}>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/get_role_shots', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色列表
|
||||||
|
* @param request - 获取角色列表请求参数
|
||||||
|
* @returns Promise<ApiResponse<角色实体列表>>
|
||||||
|
*/
|
||||||
|
export const getRoleList = async (request: {
|
||||||
|
/** 项目ID */
|
||||||
|
projectId: string;
|
||||||
|
}): Promise<ApiResponse<RoleEntity[]>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/get_role_list', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色数据
|
||||||
|
* @param request - 获取角色数据请求参数
|
||||||
|
* @returns Promise<ApiResponse<{ AI文本数据, 标签列表 }>>
|
||||||
|
*/
|
||||||
|
export const getRoleData = async (request: {
|
||||||
|
/** 角色ID */
|
||||||
|
roleId: string;
|
||||||
|
}): Promise<ApiResponse<{
|
||||||
|
/** AI文本数据 */
|
||||||
|
text: AITextEntity;
|
||||||
|
/** 标签列表 */
|
||||||
|
tags: TagEntity[];
|
||||||
|
}>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/get_role_data', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户角色库
|
||||||
|
* @returns Promise<ApiResponse<角色实体列表>>
|
||||||
|
*/
|
||||||
|
export const getUserRoleLibrary = async (): Promise<ApiResponse<RoleEntity[]>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/get_user_role_library', {});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换角色
|
||||||
|
* @param request - 替换角色请求参数
|
||||||
|
* @returns Promise<ApiResponse<替换结果>>
|
||||||
|
*/
|
||||||
|
export const replaceRole = async (request: {
|
||||||
|
/** 当前角色ID */
|
||||||
|
currentRoleId: string;
|
||||||
|
/** 替换的角色ID */
|
||||||
|
replaceRoleId: string;
|
||||||
|
}): Promise<ApiResponse<any>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/replace_role', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改标签
|
* 修改标签
|
||||||
* @param request - 修改标签请求参数
|
* @param request - 修改标签请求参数
|
||||||
@ -305,52 +374,6 @@ export const applySceneToShots = async (request: {
|
|||||||
return post<ApiResponse<any>>('/movie/apply_scene_to_shots', request);
|
return post<ApiResponse<any>>('/movie/apply_scene_to_shots', request);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取角色应用到的分镜列表
|
|
||||||
* @param request - 获取角色分镜列表请求参数
|
|
||||||
* @returns Promise<ApiResponse<分镜列表>>
|
|
||||||
*/
|
|
||||||
export const getRoleShots = async (request: {
|
|
||||||
/** 角色ID */
|
|
||||||
roleId: string;
|
|
||||||
}): Promise<ApiResponse<{
|
|
||||||
/** 分镜列表 */
|
|
||||||
shots: ShotEntity[];
|
|
||||||
/** 已应用的分镜ID列表 */
|
|
||||||
appliedShotIds: string[];
|
|
||||||
}>> => {
|
|
||||||
return post<ApiResponse<any>>('/movie/get_role_shots', request);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取角色列表
|
|
||||||
* @param request - 获取角色列表请求参数
|
|
||||||
* @returns Promise<ApiResponse<角色实体列表>>
|
|
||||||
*/
|
|
||||||
export const getRoleList = async (request: {
|
|
||||||
/** 项目ID */
|
|
||||||
projectId: string;
|
|
||||||
}): Promise<ApiResponse<RoleEntity[]>> => {
|
|
||||||
return post<ApiResponse<any>>('/movie/get_role_list', request);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取角色数据
|
|
||||||
* @param request - 获取角色数据请求参数
|
|
||||||
* @returns Promise<ApiResponse<{ AI文本数据, 标签列表 }>>
|
|
||||||
*/
|
|
||||||
export const getRoleData = async (request: {
|
|
||||||
/** 角色ID */
|
|
||||||
roleId: string;
|
|
||||||
}): Promise<ApiResponse<{
|
|
||||||
/** AI文本数据 */
|
|
||||||
text: AITextEntity;
|
|
||||||
/** 标签列表 */
|
|
||||||
tags: TagEntity[];
|
|
||||||
}>> => {
|
|
||||||
return post<ApiResponse<any>>('/movie/get_role_data', request);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取场景数据
|
* 获取场景数据
|
||||||
* @param request - 获取场景数据请求参数
|
* @param request - 获取场景数据请求参数
|
||||||
@ -398,24 +421,171 @@ export const getSceneShots = async (request: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户角色库
|
* 获取分镜关联的角色信息列表
|
||||||
|
* @param request - 获取分镜角色信息请求参数
|
||||||
* @returns Promise<ApiResponse<角色实体列表>>
|
* @returns Promise<ApiResponse<角色实体列表>>
|
||||||
*/
|
*/
|
||||||
export const getUserRoleLibrary = async (): Promise<ApiResponse<RoleEntity[]>> => {
|
export const getShotRoles = async (request: {
|
||||||
return post<ApiResponse<any>>('/movie/get_user_role_library', {});
|
/** 分镜ID */
|
||||||
|
shotId: string;
|
||||||
|
}): Promise<ApiResponse<RoleEntity[]>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/get_shot_roles', request);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 替换角色
|
* 获取分镜关联的场景信息列表
|
||||||
* @param request - 替换角色请求参数
|
* @param request - 获取分镜场景信息请求参数
|
||||||
* @returns Promise<ApiResponse<替换结果>>
|
* @returns Promise<ApiResponse<场景实体列表>>
|
||||||
*/
|
*/
|
||||||
export const replaceRole = async (request: {
|
export const getShotScenes = async (request: {
|
||||||
/** 当前角色ID */
|
/** 分镜ID */
|
||||||
currentRoleId: string;
|
shotId: string;
|
||||||
/** 替换的角色ID */
|
}): Promise<ApiResponse<SceneEntity[]>> => {
|
||||||
replaceRoleId: string;
|
return post<ApiResponse<any>>('/movie/get_shot_scenes', request);
|
||||||
}): Promise<ApiResponse<any>> => {
|
|
||||||
return post<ApiResponse<any>>('/movie/replace_role', request);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分镜详细数据
|
||||||
|
* @param request - 获取分镜数据请求参数
|
||||||
|
* @returns Promise<ApiResponse<{ AI文本数据, 标签列表 }>>
|
||||||
|
*/
|
||||||
|
export const getShotData = async (request: {
|
||||||
|
/** 分镜ID */
|
||||||
|
shotId: string;
|
||||||
|
}): Promise<ApiResponse<{
|
||||||
|
/** AI文本数据 */
|
||||||
|
text: AITextEntity;
|
||||||
|
/** 标签列表 */
|
||||||
|
tags: TagEntity[];
|
||||||
|
}>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/get_shot_data', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新生成分镜
|
||||||
|
* @param request - 重新生成分镜请求参数
|
||||||
|
* @returns Promise<ApiResponse<重新生成的分镜>>
|
||||||
|
*/
|
||||||
|
export const regenerateShot = async (request: {
|
||||||
|
/** 分镜ID */
|
||||||
|
shotId: string;
|
||||||
|
/** 镜头描述 */
|
||||||
|
shotPrompt: string;
|
||||||
|
/** 对话内容 */
|
||||||
|
dialogueContent: string;
|
||||||
|
/** 角色ID替换参数,格式为{oldId:string,newId:string}[] */
|
||||||
|
roleReplaceParams: { oldId: string; newId: string }[];
|
||||||
|
/** 场景ID替换参数,格式为{oldId:string,newId:string}[] */
|
||||||
|
sceneReplaceParams: { oldId: string; newId: string }[];
|
||||||
|
}): Promise<ApiResponse<ShotEntity>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/regenerate_shot', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改分镜对话内容
|
||||||
|
* @param request - 修改分镜对话内容请求参数
|
||||||
|
* @returns Promise<ApiResponse<修改后的分镜>>
|
||||||
|
*/
|
||||||
|
export const updateShotContent = async (request: {
|
||||||
|
/** 分镜ID */
|
||||||
|
shotId: string;
|
||||||
|
/** 新的对话内容,ContentItem数量和ID顺序不能变,只能修改content字段 */
|
||||||
|
content: Array<{
|
||||||
|
/** 角色ID */
|
||||||
|
roleId: string;
|
||||||
|
/** 对话内容 */
|
||||||
|
content: string;
|
||||||
|
}>;
|
||||||
|
}): Promise<ApiResponse<ShotEntity>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/update_shot_content', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分镜列表
|
||||||
|
* @param request - 获取分镜列表请求参数
|
||||||
|
* @returns Promise<ApiResponse<分镜实体列表>>
|
||||||
|
*/
|
||||||
|
export const getShotList = async (request: {
|
||||||
|
/** 项目ID */
|
||||||
|
projectId: string;
|
||||||
|
}): Promise<ApiResponse<ShotEntity[]>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/get_shot_list', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分镜详情
|
||||||
|
* @param request - 获取分镜详情请求参数
|
||||||
|
* @returns Promise<ApiResponse<分镜实体>>
|
||||||
|
*/
|
||||||
|
export const getShotDetail = async (request: {
|
||||||
|
/** 分镜ID */
|
||||||
|
shotId: string;
|
||||||
|
}): Promise<ApiResponse<ShotEntity>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/get_shot_detail', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分镜草图数据
|
||||||
|
* @param request - 获取分镜草图数据请求参数
|
||||||
|
* @returns Promise<ApiResponse<草图数据URL>>
|
||||||
|
*/
|
||||||
|
export const getShotSketchData = async (request: {
|
||||||
|
/** 分镜ID */
|
||||||
|
shotId: string;
|
||||||
|
}): Promise<ApiResponse<string>> => {
|
||||||
|
return post<ApiResponse<string>>('/movie/get_shot_sketch_data', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分镜视频数据
|
||||||
|
* @param request - 获取分镜视频数据请求参数
|
||||||
|
* @returns Promise<ApiResponse<视频数据URL>>
|
||||||
|
*/
|
||||||
|
export const getShotVideoData = async (request: {
|
||||||
|
/** 分镜ID */
|
||||||
|
shotId: string;
|
||||||
|
}): Promise<ApiResponse<string>> => {
|
||||||
|
return post<ApiResponse<string>>('/movie/get_shot_video_data', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改分镜镜头
|
||||||
|
* @param request - 修改分镜镜头请求参数
|
||||||
|
* @returns Promise<ApiResponse<修改后的分镜>>
|
||||||
|
*/
|
||||||
|
export const updateShotShot = async (request: {
|
||||||
|
/** 分镜ID */
|
||||||
|
shotId: string;
|
||||||
|
/** 新的镜头数据 */
|
||||||
|
shot: string[];
|
||||||
|
}): Promise<ApiResponse<ShotEntity>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/update_shot_shot', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换分镜角色
|
||||||
|
* @param request - 替换分镜角色请求参数
|
||||||
|
* @returns Promise<ApiResponse<替换结果>>
|
||||||
|
*/
|
||||||
|
export const replaceShotRole = async (request: {
|
||||||
|
/** 分镜ID */
|
||||||
|
shotId: string;
|
||||||
|
/** 旧角色ID */
|
||||||
|
oldRoleId: string;
|
||||||
|
/** 新角色ID */
|
||||||
|
newRoleId: string;
|
||||||
|
}): Promise<ApiResponse<any>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/replace_shot_role', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分镜视频剧本内容
|
||||||
|
* @param request - 获取分镜视频剧本请求参数
|
||||||
|
* @returns Promise<ApiResponse<剧本片段列表>>
|
||||||
|
*/
|
||||||
|
export const getShotVideoScript = async (request: {
|
||||||
|
/** 分镜ID */
|
||||||
|
shotId: string;
|
||||||
|
}): Promise<ApiResponse<ScriptSliceEntity[]>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/get_shot_video_script', request);
|
||||||
|
};
|
||||||
|
|||||||
383
app/service/Interaction/ShotService.ts
Normal file
383
app/service/Interaction/ShotService.ts
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
import { useState, useCallback, useMemo } from 'react';
|
||||||
|
import { ShotEntity, RoleEntity, SceneEntity, ScriptSliceEntity } from '../domain/Entities';
|
||||||
|
import { ShotItem } from '../domain/Item';
|
||||||
|
import { ShotEditUseCase } from '../usecase/ShotEditUsecase';
|
||||||
|
import {
|
||||||
|
getShotList,
|
||||||
|
getShotDetail,
|
||||||
|
updateShotContent,
|
||||||
|
updateShotShot,
|
||||||
|
getUserRoleLibrary,
|
||||||
|
replaceShotRole,
|
||||||
|
getShotVideoScript
|
||||||
|
} from '@/api/video_flow';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分镜服务Hook接口
|
||||||
|
* 定义分镜服务Hook的所有状态和操作方法
|
||||||
|
*/
|
||||||
|
export interface UseShotService {
|
||||||
|
// 响应式状态
|
||||||
|
/** 分镜列表 */
|
||||||
|
shotList: ShotEntity[];
|
||||||
|
/** 当前选中的分镜 */
|
||||||
|
selectedShot: ShotItem | null;
|
||||||
|
/** 当前分镜的草图数据URL */
|
||||||
|
shotSketchData: string | null;
|
||||||
|
/** 当前分镜的视频数据URL */
|
||||||
|
shotVideoData: string | null;
|
||||||
|
|
||||||
|
/** 用户角色库 */
|
||||||
|
userRoleLibrary: RoleEntity[];
|
||||||
|
/** 当前分镜的视频剧本片段 */
|
||||||
|
shotVideoScript: ScriptSliceEntity[];
|
||||||
|
/** 加载状态 */
|
||||||
|
loading: boolean;
|
||||||
|
/** 错误信息 */
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// 操作方法
|
||||||
|
/** 获取分镜列表 */
|
||||||
|
fetchShotList: (projectId: string) => Promise<void>;
|
||||||
|
/** 选择分镜并获取详情 */
|
||||||
|
selectShot: (shotId: string) => Promise<void>;
|
||||||
|
/** 修改分镜对话内容 */
|
||||||
|
updateShotContent: (newContent: Array<{ roleId: string; content: string }>) => Promise<void>;
|
||||||
|
/** 修改分镜镜头 */
|
||||||
|
updateShotShot: (newShot: string[]) => Promise<void>;
|
||||||
|
/** 获取用户角色库 */
|
||||||
|
fetchUserRoleLibrary: () => Promise<void>;
|
||||||
|
/** 替换分镜角色 */
|
||||||
|
replaceShotRole: (oldRoleId: string, newRoleId: string) => Promise<void>;
|
||||||
|
/** 获取分镜视频剧本内容 */
|
||||||
|
fetchShotVideoScript: () => Promise<void>;
|
||||||
|
/** 获取分镜关联的角色信息 */
|
||||||
|
getShotRoles: () => Promise<RoleEntity[]>;
|
||||||
|
/** 获取分镜关联的场景信息 */
|
||||||
|
getShotScenes: () => Promise<SceneEntity[]>;
|
||||||
|
|
||||||
|
/** 重新生成分镜 */
|
||||||
|
regenerateShot: (
|
||||||
|
shotPrompt: string,
|
||||||
|
dialogueContent: string,
|
||||||
|
roleReplaceParams: { oldId: string; newId: string }[],
|
||||||
|
sceneReplaceParams: { oldId: string; newId: string }[]
|
||||||
|
) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分镜服务Hook
|
||||||
|
* 提供分镜相关的所有状态管理和操作方法
|
||||||
|
* 包括分镜列表管理、分镜选择、数据获取、内容修改等功能
|
||||||
|
*/
|
||||||
|
export const useShotService = (): UseShotService => {
|
||||||
|
// 响应式状态
|
||||||
|
const [shotList, setShotList] = useState<ShotEntity[]>([]);
|
||||||
|
const [selectedShot, setSelectedShot] = useState<ShotItem | null>(null);
|
||||||
|
const [shotSketchData, setShotSketchData] = useState<string | null>(null);
|
||||||
|
const [shotVideoData, setShotVideoData] = useState<string | null>(null);
|
||||||
|
const [userRoleLibrary, setUserRoleLibrary] = useState<RoleEntity[]>([]);
|
||||||
|
const [shotVideoScript, setShotVideoScript] = useState<ScriptSliceEntity[]>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// UseCase实例
|
||||||
|
const [shotEditUseCase, setShotEditUseCase] = useState<ShotEditUseCase | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分镜列表
|
||||||
|
* @description 根据项目ID获取所有分镜列表
|
||||||
|
* @param projectId 项目ID
|
||||||
|
*/
|
||||||
|
const fetchShotList = useCallback(async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const response = await getShotList({ projectId });
|
||||||
|
if (response.successful) {
|
||||||
|
setShotList(response.data);
|
||||||
|
} else {
|
||||||
|
setError(`获取分镜列表失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(`获取分镜列表失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择分镜并获取详情
|
||||||
|
* @description 根据分镜ID获取分镜详情,并初始化相关的UseCase和数据
|
||||||
|
* @param shotId 分镜ID
|
||||||
|
*/
|
||||||
|
const selectShot = useCallback(async (shotId: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// 获取分镜详情
|
||||||
|
const response = await getShotDetail({ shotId });
|
||||||
|
if (response.successful) {
|
||||||
|
const shotEntity = response.data;
|
||||||
|
const shotItem = new ShotItem(shotEntity);
|
||||||
|
setSelectedShot(shotItem);
|
||||||
|
|
||||||
|
// 初始化UseCase
|
||||||
|
const newShotEditUseCase = new ShotEditUseCase(shotItem);
|
||||||
|
setShotEditUseCase(newShotEditUseCase);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 从分镜实体中获取草图数据和视频数据
|
||||||
|
setShotSketchData(shotEntity.sketchUrl || null);
|
||||||
|
setShotVideoData(shotEntity.videoUrl || null);
|
||||||
|
} else {
|
||||||
|
setError(`获取分镜详情失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(`选择分镜失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改分镜对话内容
|
||||||
|
* @description 更新分镜的对话内容,ContentItem数量和ID顺序不能变,只能修改content字段
|
||||||
|
* @param newContent 新的对话内容数组
|
||||||
|
*/
|
||||||
|
const updateShotContentHandler = useCallback(async (newContent: Array<{ roleId: string; content: string }>) => {
|
||||||
|
if (!shotEditUseCase) {
|
||||||
|
setError('分镜编辑用例未初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const updatedShot = await shotEditUseCase.updateShotContent(newContent);
|
||||||
|
setSelectedShot(new ShotItem(updatedShot));
|
||||||
|
} catch (err) {
|
||||||
|
setError(`修改分镜对话内容失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [shotEditUseCase]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改分镜镜头
|
||||||
|
* @description 更新分镜的镜头数据
|
||||||
|
* @param newShot 新的镜头数据
|
||||||
|
*/
|
||||||
|
const updateShotShotHandler = useCallback(async (newShot: string[]) => {
|
||||||
|
if (!selectedShot) {
|
||||||
|
setError('未选择分镜');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const response = await updateShotShot({
|
||||||
|
shotId: selectedShot.entity.id,
|
||||||
|
shot: newShot
|
||||||
|
});
|
||||||
|
if (response.successful) {
|
||||||
|
const updatedShot = response.data;
|
||||||
|
setSelectedShot(new ShotItem(updatedShot));
|
||||||
|
} else {
|
||||||
|
setError(`修改分镜镜头失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(`修改分镜镜头失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [selectedShot]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户角色库
|
||||||
|
* @description 获取当前用户的所有角色库数据
|
||||||
|
*/
|
||||||
|
const fetchUserRoleLibrary = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const response = await getUserRoleLibrary();
|
||||||
|
if (response.successful) {
|
||||||
|
setUserRoleLibrary(response.data);
|
||||||
|
} else {
|
||||||
|
setError(`获取用户角色库失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(`获取用户角色库失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换分镜角色
|
||||||
|
* @description 将分镜中的某个角色替换为角色库中的另一个角色
|
||||||
|
* @param oldRoleId 旧角色ID
|
||||||
|
* @param newRoleId 新角色ID
|
||||||
|
*/
|
||||||
|
const replaceShotRoleHandler = useCallback(async (oldRoleId: string, newRoleId: string) => {
|
||||||
|
if (!selectedShot) {
|
||||||
|
setError('未选择分镜');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const response = await replaceShotRole({
|
||||||
|
shotId: selectedShot.entity.id,
|
||||||
|
oldRoleId,
|
||||||
|
newRoleId
|
||||||
|
});
|
||||||
|
if (response.successful) {
|
||||||
|
// 重新获取分镜详情
|
||||||
|
await selectShot(selectedShot.entity.id);
|
||||||
|
} else {
|
||||||
|
setError(`替换分镜角色失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(`替换分镜角色失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [selectedShot, selectShot]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分镜视频剧本内容
|
||||||
|
* @description 获取分镜视频的剧本内容,通过接口返回多个ScriptSliceEntity片段
|
||||||
|
*/
|
||||||
|
const fetchShotVideoScript = useCallback(async () => {
|
||||||
|
if (!selectedShot) {
|
||||||
|
setError('未选择分镜');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const response = await getShotVideoScript({ shotId: selectedShot.entity.id });
|
||||||
|
if (response.successful) {
|
||||||
|
setShotVideoScript(response.data);
|
||||||
|
} else {
|
||||||
|
setError(`获取分镜视频剧本失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(`获取分镜视频剧本失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [selectedShot]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分镜关联的角色信息
|
||||||
|
* @description 获取当前分镜可以使用的角色列表
|
||||||
|
* @returns Promise<RoleEntity[]> 角色信息列表
|
||||||
|
*/
|
||||||
|
const getShotRoles = useCallback(async (): Promise<RoleEntity[]> => {
|
||||||
|
if (!shotEditUseCase) {
|
||||||
|
throw new Error('分镜编辑用例未初始化');
|
||||||
|
}
|
||||||
|
return await shotEditUseCase.getShotRoles();
|
||||||
|
}, [shotEditUseCase]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分镜关联的场景信息
|
||||||
|
* @description 获取当前分镜可以使用的场景列表
|
||||||
|
* @returns Promise<SceneEntity[]> 场景信息列表
|
||||||
|
*/
|
||||||
|
const getShotScenes = useCallback(async (): Promise<SceneEntity[]> => {
|
||||||
|
if (!shotEditUseCase) {
|
||||||
|
throw new Error('分镜编辑用例未初始化');
|
||||||
|
}
|
||||||
|
return await shotEditUseCase.getShotScenes();
|
||||||
|
}, [shotEditUseCase]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新生成分镜
|
||||||
|
* @description 使用镜头、对话内容、角色ID替换参数、场景ID替换参数重新生成分镜
|
||||||
|
* @param shotPrompt 镜头描述
|
||||||
|
* @param dialogueContent 对话内容
|
||||||
|
* @param roleReplaceParams 角色ID替换参数,格式为{oldId:string,newId:string}[]
|
||||||
|
* @param sceneReplaceParams 场景ID替换参数,格式为{oldId:string,newId:string}[]
|
||||||
|
*/
|
||||||
|
const regenerateShot = useCallback(async (
|
||||||
|
shotPrompt: string,
|
||||||
|
dialogueContent: string,
|
||||||
|
roleReplaceParams: { oldId: string; newId: string }[],
|
||||||
|
sceneReplaceParams: { oldId: string; newId: string }[]
|
||||||
|
) => {
|
||||||
|
if (!shotEditUseCase) {
|
||||||
|
setError('分镜编辑用例未初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const updatedShot = await shotEditUseCase.regenerateShot(
|
||||||
|
shotPrompt,
|
||||||
|
dialogueContent,
|
||||||
|
roleReplaceParams,
|
||||||
|
sceneReplaceParams
|
||||||
|
);
|
||||||
|
setSelectedShot(new ShotItem(updatedShot));
|
||||||
|
} catch (err) {
|
||||||
|
setError(`重新生成分镜失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [shotEditUseCase]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 响应式状态 - 用于UI组件订阅和渲染
|
||||||
|
/** 分镜列表 - 当前项目的所有分镜数据 */
|
||||||
|
shotList,
|
||||||
|
/** 当前选中的分镜 - 包含分镜实体和编辑状态 */
|
||||||
|
selectedShot,
|
||||||
|
/** 当前分镜的草图数据URL - 从分镜实体中获取的草图图片链接 */
|
||||||
|
shotSketchData,
|
||||||
|
/** 当前分镜的视频数据URL - 从分镜实体中获取的视频链接 */
|
||||||
|
shotVideoData,
|
||||||
|
|
||||||
|
/** 用户角色库 - 当前用户可用的所有角色数据 */
|
||||||
|
userRoleLibrary,
|
||||||
|
/** 当前分镜的视频剧本片段 - 通过接口获取的剧本内容 */
|
||||||
|
shotVideoScript,
|
||||||
|
/** 加载状态 - 标识当前是否有异步操作正在进行 */
|
||||||
|
loading,
|
||||||
|
/** 错误信息 - 记录最近一次操作的错误信息 */
|
||||||
|
error,
|
||||||
|
|
||||||
|
// 操作方法 - 提供给UI组件调用的业务逻辑方法
|
||||||
|
/** 获取分镜列表 - 根据项目ID获取所有分镜数据 */
|
||||||
|
fetchShotList,
|
||||||
|
/** 选择分镜并获取详情 - 选择指定分镜并初始化相关数据 */
|
||||||
|
selectShot,
|
||||||
|
/** 修改分镜对话内容 - 更新分镜的对话内容,保持ContentItem结构不变 */
|
||||||
|
updateShotContent: updateShotContentHandler,
|
||||||
|
/** 修改分镜镜头 - 更新分镜的镜头数据 */
|
||||||
|
updateShotShot: updateShotShotHandler,
|
||||||
|
/** 获取用户角色库 - 获取当前用户的所有角色数据 */
|
||||||
|
fetchUserRoleLibrary,
|
||||||
|
/** 替换分镜角色 - 将分镜中的角色替换为角色库中的另一个角色 */
|
||||||
|
replaceShotRole: replaceShotRoleHandler,
|
||||||
|
/** 获取分镜视频剧本内容 - 通过接口获取视频剧本片段 */
|
||||||
|
fetchShotVideoScript,
|
||||||
|
/** 获取分镜关联的角色信息 - 获取当前分镜可用的角色列表 */
|
||||||
|
getShotRoles,
|
||||||
|
/** 获取分镜关联的场景信息 - 获取当前分镜可用的场景列表 */
|
||||||
|
getShotScenes,
|
||||||
|
/** 重新生成分镜 - 使用新参数重新生成分镜内容 */
|
||||||
|
regenerateShot,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -65,19 +65,21 @@ export interface SceneEntity extends BaseEntity {
|
|||||||
/** 场景提示词Id */
|
/** 场景提示词Id */
|
||||||
generateTextId: string;
|
generateTextId: string;
|
||||||
}
|
}
|
||||||
|
/**对话内容项 */
|
||||||
interface RoleMap {
|
|
||||||
/** 角色ID */
|
|
||||||
roleId: string;
|
|
||||||
/** 人物ID */
|
|
||||||
figureId: string;
|
|
||||||
}
|
|
||||||
interface ContentItem {
|
interface ContentItem {
|
||||||
/** 角色ID */
|
/** 角色ID */
|
||||||
roleId: string;
|
roleId: string;
|
||||||
/** 对话内容 */
|
/** 对话内容 */
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
export enum ShotStatus {
|
||||||
|
/** 草稿加载中 */
|
||||||
|
sketchLoading = 0,
|
||||||
|
/** 视频加载中 */
|
||||||
|
videoLoading = 1,
|
||||||
|
/** 完成 */
|
||||||
|
finished = 2,
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 分镜实体接口
|
* 分镜实体接口
|
||||||
*/
|
*/
|
||||||
@ -88,10 +90,40 @@ export interface ShotEntity extends BaseEntity {
|
|||||||
sketchUrl: string;
|
sketchUrl: string;
|
||||||
/**分镜视频Url */
|
/**分镜视频Url */
|
||||||
videoUrl: string;
|
videoUrl: string;
|
||||||
/**人物ID */
|
/**分镜状态 */
|
||||||
roleMap: RoleMap[];
|
status: ShotStatus;
|
||||||
|
/**角色ID列表 */
|
||||||
|
roleList: string[];
|
||||||
|
/**场景ID列表 */
|
||||||
|
sceneList: string[];
|
||||||
/**对话内容 */
|
/**对话内容 */
|
||||||
content: ContentItem[];
|
content: ContentItem[];
|
||||||
/**镜头项 */
|
/**镜头项 */
|
||||||
shot: string[];
|
shot: string[];
|
||||||
|
/**分镜剧本 */
|
||||||
|
scriptId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**不同类型 将有不同元数据 */
|
||||||
|
export enum ScriptSliceType {
|
||||||
|
/** 文本 */
|
||||||
|
text = 'text',
|
||||||
|
/** 高亮 */
|
||||||
|
highlight = 'highlight',
|
||||||
|
/**角色 */
|
||||||
|
role = 'role',
|
||||||
|
/** 场景 */
|
||||||
|
scene = 'scene',
|
||||||
|
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 剧本片段实体接口
|
||||||
|
*/
|
||||||
|
export interface ScriptSliceEntity extends BaseEntity {
|
||||||
|
/** 类型 */
|
||||||
|
type: ScriptSliceType;
|
||||||
|
/** 剧本内容 */
|
||||||
|
text: string;
|
||||||
|
/** 元数据 */
|
||||||
|
metaData: any;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -116,7 +116,7 @@ export class SceneItem extends EditItem<SceneEntity> {
|
|||||||
* 分镜可编辑项
|
* 分镜可编辑项
|
||||||
*/
|
*/
|
||||||
export class ShotItem extends EditItem<ShotEntity> {
|
export class ShotItem extends EditItem<ShotEntity> {
|
||||||
type: ItemType.IMAGE = ItemType.IMAGE;
|
type: ItemType = ItemType.IMAGE;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
entity: ShotEntity,
|
entity: ShotEntity,
|
||||||
@ -124,4 +124,10 @@ export class ShotItem extends EditItem<ShotEntity> {
|
|||||||
) {
|
) {
|
||||||
super(entity, metadata);
|
super(entity, metadata);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 更新为视频状态
|
||||||
|
*/
|
||||||
|
updateToVideoStatus(): void {
|
||||||
|
this.type = ItemType.VIDEO;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { RoleEditUseCase } from '../usecase/RoleEditUseCase';
|
|||||||
import { TextEditUseCase } from '../usecase/TextEditUseCase';
|
import { TextEditUseCase } from '../usecase/TextEditUseCase';
|
||||||
import { TagEditUseCase } from '../usecase/TagEditUseCase';
|
import { TagEditUseCase } from '../usecase/TagEditUseCase';
|
||||||
import { RoleItem, TextItem, TagItem } from '../domain/Item';
|
import { RoleItem, TextItem, TagItem } from '../domain/Item';
|
||||||
import { RoleEntity, AITextEntity, TagEntity, ShotEntity } from '../domain/Entities';
|
import { RoleEntity, AITextEntity, TagEntity, ShotEntity, ShotStatus } from '../domain/Entities';
|
||||||
|
|
||||||
// Mock API模块
|
// Mock API模块
|
||||||
jest.mock('@/api/video_flow', () => ({
|
jest.mock('@/api/video_flow', () => ({
|
||||||
@ -71,7 +71,10 @@ describe('RoleService 业务逻辑测试', () => {
|
|||||||
name: '分镜1',
|
name: '分镜1',
|
||||||
sketchUrl: 'http://example.com/sketch1.jpg',
|
sketchUrl: 'http://example.com/sketch1.jpg',
|
||||||
videoUrl: 'http://example.com/video1.mp4',
|
videoUrl: 'http://example.com/video1.mp4',
|
||||||
roleMap: [],
|
roleList: [],
|
||||||
|
sceneList: [],
|
||||||
|
status: ShotStatus.sketchLoading,
|
||||||
|
scriptId: 'script1',
|
||||||
content: [],
|
content: [],
|
||||||
shot: [],
|
shot: [],
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
|
|||||||
565
app/service/test/Scene.test.ts
Normal file
565
app/service/test/Scene.test.ts
Normal file
@ -0,0 +1,565 @@
|
|||||||
|
import { getSceneList, getSceneData, updateText, updateTag, regenerateScene, getSceneShots, applySceneToShots } from '@/api/video_flow';
|
||||||
|
import { SceneEditUseCase } from '../usecase/SceneEditUseCase';
|
||||||
|
import { TextEditUseCase } from '../usecase/TextEditUseCase';
|
||||||
|
import { TagEditUseCase } from '../usecase/TagEditUseCase';
|
||||||
|
import { SceneItem, TextItem, TagItem } from '../domain/Item';
|
||||||
|
import { SceneEntity, AITextEntity, TagEntity, ShotEntity, ShotStatus } from '../domain/Entities';
|
||||||
|
|
||||||
|
// Mock API模块
|
||||||
|
jest.mock('@/api/video_flow', () => ({
|
||||||
|
getSceneList: jest.fn(),
|
||||||
|
getSceneData: jest.fn(),
|
||||||
|
updateText: jest.fn(),
|
||||||
|
updateTag: jest.fn(),
|
||||||
|
regenerateScene: jest.fn(),
|
||||||
|
getSceneShots: jest.fn(),
|
||||||
|
applySceneToShots: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock UseCase模块
|
||||||
|
jest.mock('../usecase/SceneEditUseCase');
|
||||||
|
jest.mock('../usecase/TextEditUseCase');
|
||||||
|
jest.mock('../usecase/TagEditUseCase');
|
||||||
|
|
||||||
|
// Mock Domain模块
|
||||||
|
jest.mock('../domain/Item', () => ({
|
||||||
|
SceneItem: jest.fn(),
|
||||||
|
TextItem: jest.fn(),
|
||||||
|
TagItem: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('SceneService 业务逻辑测试', () => {
|
||||||
|
let mockSceneEditUseCase: jest.Mocked<SceneEditUseCase>;
|
||||||
|
let mockTextEditUseCase: jest.Mocked<TextEditUseCase>;
|
||||||
|
let mockTagEditUseCase: jest.Mocked<TagEditUseCase>;
|
||||||
|
|
||||||
|
// 测试数据
|
||||||
|
const mockSceneEntity: SceneEntity = {
|
||||||
|
id: 'scene1',
|
||||||
|
name: '测试场景',
|
||||||
|
imageUrl: 'http://example.com/scene1.jpg',
|
||||||
|
tagIds: ['tag1', 'tag2'],
|
||||||
|
generateTextId: 'text1',
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
loadingProgress: 100,
|
||||||
|
disableEdit: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTextEntity: AITextEntity = {
|
||||||
|
id: 'text1',
|
||||||
|
content: '这是AI生成的场景文本内容',
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
loadingProgress: 100,
|
||||||
|
disableEdit: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTagEntity1: TagEntity = {
|
||||||
|
id: 'tag1',
|
||||||
|
name: '场景标签1',
|
||||||
|
content: '场景标签内容1',
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
loadingProgress: 100,
|
||||||
|
disableEdit: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTagEntity2: TagEntity = {
|
||||||
|
id: 'tag2',
|
||||||
|
name: '场景标签2',
|
||||||
|
content: '场景标签内容2',
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
loadingProgress: 100,
|
||||||
|
disableEdit: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockShotEntity: ShotEntity = {
|
||||||
|
id: 'shot1',
|
||||||
|
name: '分镜1',
|
||||||
|
sketchUrl: 'http://example.com/sketch1.jpg',
|
||||||
|
videoUrl: 'http://example.com/video1.mp4',
|
||||||
|
roleList: [],
|
||||||
|
sceneList: [],
|
||||||
|
content: [],
|
||||||
|
status: ShotStatus.sketchLoading,
|
||||||
|
shot: [],
|
||||||
|
scriptId: 'script1',
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
loadingProgress: 100,
|
||||||
|
disableEdit: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// 设置Mock UseCase实例
|
||||||
|
mockSceneEditUseCase = {
|
||||||
|
AIgenerateScene: jest.fn(),
|
||||||
|
applyScene: jest.fn(),
|
||||||
|
refreshSceneData: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
mockTextEditUseCase = {
|
||||||
|
getOptimizedContent: jest.fn(),
|
||||||
|
updateText: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
mockTagEditUseCase = {
|
||||||
|
updateTag: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// 设置Mock构造函数
|
||||||
|
(SceneEditUseCase as jest.MockedClass<typeof SceneEditUseCase>).mockImplementation(() => mockSceneEditUseCase);
|
||||||
|
(TextEditUseCase as jest.MockedClass<typeof TextEditUseCase>).mockImplementation(() => mockTextEditUseCase);
|
||||||
|
(TagEditUseCase as jest.MockedClass<typeof TagEditUseCase>).mockImplementation(() => mockTagEditUseCase);
|
||||||
|
|
||||||
|
// 设置Mock Item构造函数
|
||||||
|
(SceneItem as jest.MockedClass<typeof SceneItem>).mockImplementation((entity) => ({
|
||||||
|
entity,
|
||||||
|
metadata: {},
|
||||||
|
disableEdit: entity.disableEdit,
|
||||||
|
type: 3,
|
||||||
|
} as any));
|
||||||
|
|
||||||
|
(TextItem as jest.MockedClass<typeof TextItem>).mockImplementation((entity) => ({
|
||||||
|
entity,
|
||||||
|
metadata: {},
|
||||||
|
disableEdit: entity.disableEdit,
|
||||||
|
type: 0,
|
||||||
|
} as any));
|
||||||
|
|
||||||
|
(TagItem as jest.MockedClass<typeof TagItem>).mockImplementation((entity) => ({
|
||||||
|
entity,
|
||||||
|
metadata: {},
|
||||||
|
disableEdit: entity.disableEdit,
|
||||||
|
type: 2,
|
||||||
|
} as any));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('数据初始化测试', () => {
|
||||||
|
it('应该成功获取场景列表', async () => {
|
||||||
|
const mockScenes = [mockSceneEntity];
|
||||||
|
(getSceneList as jest.Mock).mockResolvedValue({
|
||||||
|
successful: true,
|
||||||
|
data: mockScenes,
|
||||||
|
message: 'success',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getSceneList({ projectId: 'project1' });
|
||||||
|
|
||||||
|
expect(getSceneList).toHaveBeenCalledWith({ projectId: 'project1' });
|
||||||
|
expect(result.successful).toBe(true);
|
||||||
|
expect(result.data).toEqual(mockScenes);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('获取场景列表失败时应该返回错误信息', async () => {
|
||||||
|
(getSceneList as jest.Mock).mockResolvedValue({
|
||||||
|
successful: false,
|
||||||
|
message: '获取失败',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getSceneList({ projectId: 'project1' });
|
||||||
|
|
||||||
|
expect(result.successful).toBe(false);
|
||||||
|
expect(result.message).toBe('获取失败');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该成功获取场景数据', async () => {
|
||||||
|
(getSceneData as jest.Mock).mockResolvedValue({
|
||||||
|
successful: true,
|
||||||
|
data: {
|
||||||
|
text: mockTextEntity,
|
||||||
|
tags: [mockTagEntity1, mockTagEntity2],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getSceneData({ sceneId: 'scene1' });
|
||||||
|
|
||||||
|
expect(getSceneData).toHaveBeenCalledWith({ sceneId: 'scene1' });
|
||||||
|
expect(result.successful).toBe(true);
|
||||||
|
expect(result.data.text).toEqual(mockTextEntity);
|
||||||
|
expect(result.data.tags).toEqual([mockTagEntity1, mockTagEntity2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('修改文本和标签测试', () => {
|
||||||
|
it('应该成功修改AI文本', async () => {
|
||||||
|
const updatedTextEntity = { ...mockTextEntity, content: '更新后的场景文本' };
|
||||||
|
(updateText as jest.Mock).mockResolvedValue({
|
||||||
|
successful: true,
|
||||||
|
data: updatedTextEntity,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await updateText({
|
||||||
|
textId: 'text1',
|
||||||
|
content: '新的场景文本内容'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateText).toHaveBeenCalledWith({
|
||||||
|
textId: 'text1',
|
||||||
|
content: '新的场景文本内容'
|
||||||
|
});
|
||||||
|
expect(result.successful).toBe(true);
|
||||||
|
expect(result.data.content).toBe('更新后的场景文本');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该成功修改标签内容', async () => {
|
||||||
|
const updatedTagEntity = { ...mockTagEntity1, content: '更新后的场景标签' };
|
||||||
|
(updateTag as jest.Mock).mockResolvedValue({
|
||||||
|
successful: true,
|
||||||
|
data: updatedTagEntity,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await updateTag({
|
||||||
|
tagId: 'tag1',
|
||||||
|
content: '新的场景标签内容'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateTag).toHaveBeenCalledWith({
|
||||||
|
tagId: 'tag1',
|
||||||
|
content: '新的场景标签内容'
|
||||||
|
});
|
||||||
|
expect(result.successful).toBe(true);
|
||||||
|
expect(result.data.content).toBe('更新后的场景标签');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('文本AI优化测试', () => {
|
||||||
|
it('应该成功优化AI文本', async () => {
|
||||||
|
const optimizedContent = '优化后的场景文本内容';
|
||||||
|
const updatedTextEntity = { ...mockTextEntity, content: optimizedContent };
|
||||||
|
|
||||||
|
mockTextEditUseCase.getOptimizedContent.mockResolvedValue(optimizedContent);
|
||||||
|
mockTextEditUseCase.updateText.mockResolvedValue({
|
||||||
|
entity: updatedTextEntity,
|
||||||
|
metadata: {},
|
||||||
|
disableEdit: false,
|
||||||
|
type: 0,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
(updateText as jest.Mock).mockResolvedValue({
|
||||||
|
successful: true,
|
||||||
|
data: updatedTextEntity,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 模拟优化流程
|
||||||
|
const optimizedContentResult = await mockTextEditUseCase.getOptimizedContent();
|
||||||
|
const updateResult = await mockTextEditUseCase.updateText(optimizedContentResult);
|
||||||
|
|
||||||
|
expect(mockTextEditUseCase.getOptimizedContent).toHaveBeenCalled();
|
||||||
|
expect(mockTextEditUseCase.updateText).toHaveBeenCalledWith(optimizedContent);
|
||||||
|
expect(updateResult.entity.content).toBe(optimizedContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('没有文本内容时优化应该抛出错误', async () => {
|
||||||
|
const emptyTextEntity = { ...mockTextEntity, content: '' };
|
||||||
|
mockTextEditUseCase.getOptimizedContent.mockRejectedValue(new Error('没有可优化的文本内容'));
|
||||||
|
|
||||||
|
await expect(mockTextEditUseCase.getOptimizedContent()).rejects.toThrow('没有可优化的文本内容');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('重新生成场景测试', () => {
|
||||||
|
it('应该成功重新生成场景', async () => {
|
||||||
|
const newSceneEntity = { ...mockSceneEntity, id: 'scene2', name: '新场景' };
|
||||||
|
(regenerateScene as jest.Mock).mockResolvedValue({
|
||||||
|
successful: true,
|
||||||
|
data: newSceneEntity,
|
||||||
|
});
|
||||||
|
|
||||||
|
mockSceneEditUseCase.AIgenerateScene.mockResolvedValue(newSceneEntity);
|
||||||
|
|
||||||
|
const result = await regenerateScene({
|
||||||
|
prompt: '重新生成场景',
|
||||||
|
tagTypes: ['tag1', 'tag2'],
|
||||||
|
sceneId: 'scene1'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(regenerateScene).toHaveBeenCalledWith({
|
||||||
|
prompt: '重新生成场景',
|
||||||
|
tagTypes: ['tag1', 'tag2'],
|
||||||
|
sceneId: 'scene1'
|
||||||
|
});
|
||||||
|
expect(result.successful).toBe(true);
|
||||||
|
expect(result.data.id).toBe('scene2');
|
||||||
|
expect(result.data.name).toBe('新场景');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('重新生成场景失败时应该返回错误信息', async () => {
|
||||||
|
(regenerateScene as jest.Mock).mockResolvedValue({
|
||||||
|
successful: false,
|
||||||
|
message: '重新生成失败',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await regenerateScene({
|
||||||
|
prompt: '重新生成场景',
|
||||||
|
tagTypes: ['tag1', 'tag2'],
|
||||||
|
sceneId: 'scene1'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.successful).toBe(false);
|
||||||
|
expect(result.message).toBe('重新生成失败');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('重新获取场景数据测试', () => {
|
||||||
|
it('应该成功重新获取场景数据并更新实体', async () => {
|
||||||
|
const mockTextEntity = {
|
||||||
|
id: 'text1',
|
||||||
|
content: '更新后的场景AI文本',
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
loadingProgress: 100,
|
||||||
|
disableEdit: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTags = [
|
||||||
|
{
|
||||||
|
id: 'tag1',
|
||||||
|
name: '更新场景标签1',
|
||||||
|
content: '更新场景内容1',
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
loadingProgress: 100,
|
||||||
|
disableEdit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tag2',
|
||||||
|
name: '更新场景标签2',
|
||||||
|
content: '更新场景内容2',
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
loadingProgress: 100,
|
||||||
|
disableEdit: false,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
(getSceneData as jest.Mock).mockResolvedValue({
|
||||||
|
successful: true,
|
||||||
|
data: {
|
||||||
|
text: mockTextEntity,
|
||||||
|
tags: mockTags,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 模拟SceneItem的setEntity方法
|
||||||
|
const mockSetEntity = jest.fn();
|
||||||
|
(SceneItem as jest.MockedClass<typeof SceneItem>).mockImplementation((entity) => ({
|
||||||
|
entity,
|
||||||
|
metadata: {},
|
||||||
|
disableEdit: entity.disableEdit,
|
||||||
|
type: 3,
|
||||||
|
setEntity: mockSetEntity,
|
||||||
|
} as any));
|
||||||
|
|
||||||
|
const sceneItem = new SceneItem(mockSceneEntity);
|
||||||
|
const useCase = new SceneEditUseCase(sceneItem);
|
||||||
|
|
||||||
|
const result = await useCase.refreshSceneData();
|
||||||
|
|
||||||
|
expect(getSceneData).toHaveBeenCalledWith({ sceneId: 'scene1' });
|
||||||
|
expect(result.text).toEqual(mockTextEntity);
|
||||||
|
expect(result.tags).toEqual(mockTags);
|
||||||
|
expect(mockSetEntity).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
generateTextId: 'text1',
|
||||||
|
tagIds: ['tag1', 'tag2'],
|
||||||
|
updatedAt: expect.any(Number),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('场景ID不存在时应该抛出错误', async () => {
|
||||||
|
const emptySceneEntity = { ...mockSceneEntity, id: '' };
|
||||||
|
const sceneItem = new SceneItem(emptySceneEntity);
|
||||||
|
const useCase = new SceneEditUseCase(sceneItem);
|
||||||
|
|
||||||
|
await expect(useCase.refreshSceneData()).rejects.toThrow('场景ID不存在,无法获取场景数据');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('API调用失败时应该抛出错误', async () => {
|
||||||
|
(getSceneData as jest.Mock).mockResolvedValue({
|
||||||
|
successful: false,
|
||||||
|
message: '获取失败',
|
||||||
|
});
|
||||||
|
|
||||||
|
const sceneItem = new SceneItem(mockSceneEntity);
|
||||||
|
const useCase = new SceneEditUseCase(sceneItem);
|
||||||
|
|
||||||
|
await expect(useCase.refreshSceneData()).rejects.toThrow('获取场景数据失败: 获取失败');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('场景应用到多个分镜测试', () => {
|
||||||
|
it('应该成功获取场景分镜列表', async () => {
|
||||||
|
const mockShots = [mockShotEntity];
|
||||||
|
(getSceneShots as jest.Mock).mockResolvedValue({
|
||||||
|
successful: true,
|
||||||
|
data: {
|
||||||
|
shots: mockShots,
|
||||||
|
appliedShotIds: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getSceneShots({ sceneId: 'scene1' });
|
||||||
|
|
||||||
|
expect(getSceneShots).toHaveBeenCalledWith({ sceneId: 'scene1' });
|
||||||
|
expect(result.successful).toBe(true);
|
||||||
|
expect(result.data.shots).toEqual(mockShots);
|
||||||
|
expect(result.data.appliedShotIds).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该成功应用场景到选中的分镜', async () => {
|
||||||
|
(applySceneToShots as jest.Mock).mockResolvedValue({
|
||||||
|
successful: true,
|
||||||
|
data: { success: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
mockSceneEditUseCase.applyScene.mockResolvedValue({} as any);
|
||||||
|
|
||||||
|
const result = await applySceneToShots({
|
||||||
|
sceneId: 'scene1',
|
||||||
|
shotIds: ['shot1', 'shot2']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(applySceneToShots).toHaveBeenCalledWith({
|
||||||
|
sceneId: 'scene1',
|
||||||
|
shotIds: ['shot1', 'shot2']
|
||||||
|
});
|
||||||
|
expect(result.successful).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应用场景失败时应该返回错误信息', async () => {
|
||||||
|
(applySceneToShots as jest.Mock).mockResolvedValue({
|
||||||
|
successful: false,
|
||||||
|
message: '应用失败',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await applySceneToShots({
|
||||||
|
sceneId: 'scene1',
|
||||||
|
shotIds: ['shot1']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.successful).toBe(false);
|
||||||
|
expect(result.message).toBe('应用失败');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确处理已应用的分镜状态', async () => {
|
||||||
|
const mockShots = [mockShotEntity];
|
||||||
|
(getSceneShots as jest.Mock).mockResolvedValue({
|
||||||
|
successful: true,
|
||||||
|
data: {
|
||||||
|
shots: mockShots,
|
||||||
|
appliedShotIds: ['shot1'], // 分镜1已应用
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getSceneShots({ sceneId: 'scene1' });
|
||||||
|
|
||||||
|
expect(result.data.appliedShotIds).toEqual(['shot1']);
|
||||||
|
expect(result.data.shots).toEqual(mockShots);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('UseCase业务逻辑测试', () => {
|
||||||
|
it('SceneEditUseCase应该正确初始化', () => {
|
||||||
|
const sceneItem = new SceneItem(mockSceneEntity);
|
||||||
|
const useCase = new SceneEditUseCase(sceneItem);
|
||||||
|
|
||||||
|
expect(SceneEditUseCase).toHaveBeenCalledWith(sceneItem);
|
||||||
|
expect(useCase).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TextEditUseCase应该正确初始化', () => {
|
||||||
|
const textItem = new TextItem(mockTextEntity);
|
||||||
|
const useCase = new TextEditUseCase(textItem);
|
||||||
|
|
||||||
|
expect(TextEditUseCase).toHaveBeenCalledWith(textItem);
|
||||||
|
expect(useCase).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TagEditUseCase应该正确初始化', () => {
|
||||||
|
const tagItem = new TagItem(mockTagEntity1);
|
||||||
|
const useCase = new TagEditUseCase(tagItem);
|
||||||
|
|
||||||
|
expect(TagEditUseCase).toHaveBeenCalledWith(tagItem);
|
||||||
|
expect(useCase).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Domain实体测试', () => {
|
||||||
|
it('SceneItem应该正确包装SceneEntity', () => {
|
||||||
|
const sceneItem = new SceneItem(mockSceneEntity);
|
||||||
|
|
||||||
|
expect(SceneItem).toHaveBeenCalledWith(mockSceneEntity);
|
||||||
|
expect(sceneItem.entity).toEqual(mockSceneEntity);
|
||||||
|
expect(sceneItem.disableEdit).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TextItem应该正确包装AITextEntity', () => {
|
||||||
|
const textItem = new TextItem(mockTextEntity);
|
||||||
|
|
||||||
|
expect(TextItem).toHaveBeenCalledWith(mockTextEntity);
|
||||||
|
expect(textItem.entity).toEqual(mockTextEntity);
|
||||||
|
expect(textItem.disableEdit).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TagItem应该正确包装TagEntity', () => {
|
||||||
|
const tagItem = new TagItem(mockTagEntity1);
|
||||||
|
|
||||||
|
expect(TagItem).toHaveBeenCalledWith(mockTagEntity1);
|
||||||
|
expect(tagItem.entity).toEqual(mockTagEntity1);
|
||||||
|
expect(tagItem.disableEdit).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('错误处理测试', () => {
|
||||||
|
it('API调用失败时应该正确处理错误', async () => {
|
||||||
|
(getSceneList as jest.Mock).mockRejectedValue(new Error('网络错误'));
|
||||||
|
|
||||||
|
await expect(getSceneList({ projectId: 'project1' })).rejects.toThrow('网络错误');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('API返回失败状态时应该正确处理', async () => {
|
||||||
|
(getSceneList as jest.Mock).mockResolvedValue({
|
||||||
|
successful: false,
|
||||||
|
message: '服务器错误',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getSceneList({ projectId: 'project1' });
|
||||||
|
|
||||||
|
expect(result.successful).toBe(false);
|
||||||
|
expect(result.message).toBe('服务器错误');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('UseCase未初始化时应该抛出相应错误', async () => {
|
||||||
|
const sceneItem = new SceneItem(mockSceneEntity);
|
||||||
|
const useCase = new SceneEditUseCase(sceneItem);
|
||||||
|
|
||||||
|
// 模拟UseCase未初始化的情况
|
||||||
|
mockSceneEditUseCase.AIgenerateScene.mockRejectedValue(new Error('场景编辑UseCase未初始化'));
|
||||||
|
|
||||||
|
await expect(useCase.AIgenerateScene({} as any, [])).rejects.toThrow('场景编辑UseCase未初始化');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('场景数据完整性测试', () => {
|
||||||
|
it('应该验证场景实体的完整性', () => {
|
||||||
|
const sceneItem = new SceneItem(mockSceneEntity);
|
||||||
|
|
||||||
|
expect(sceneItem.entity.id).toBe('scene1');
|
||||||
|
expect(sceneItem.entity.name).toBe('测试场景');
|
||||||
|
expect(sceneItem.entity.imageUrl).toBe('http://example.com/scene1.jpg');
|
||||||
|
expect(sceneItem.entity.tagIds).toEqual(['tag1', 'tag2']);
|
||||||
|
expect(sceneItem.entity.generateTextId).toBe('text1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该验证文本实体的完整性', () => {
|
||||||
|
const textItem = new TextItem(mockTextEntity);
|
||||||
|
|
||||||
|
expect(textItem.entity.id).toBe('text1');
|
||||||
|
expect(textItem.entity.content).toBe('这是AI生成的场景文本内容');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该验证标签实体的完整性', () => {
|
||||||
|
const tagItem = new TagItem(mockTagEntity1);
|
||||||
|
|
||||||
|
expect(tagItem.entity.id).toBe('tag1');
|
||||||
|
expect(tagItem.entity.name).toBe('场景标签1');
|
||||||
|
expect(tagItem.entity.content).toBe('场景标签内容1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { SceneEntity } from '../domain/Entities';
|
import { SceneEntity, AITextEntity, TagEntity } from '../domain/Entities';
|
||||||
import { SceneItem, TagItem, TextItem } from '../domain/Item';
|
import { SceneItem, TagItem, TextItem } from '../domain/Item';
|
||||||
import { regenerateScene, applySceneToShots } from '@/api/video_flow';
|
import { regenerateScene, applySceneToShots, getSceneData } from '@/api/video_flow';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 场景编辑用例
|
* 场景编辑用例
|
||||||
@ -49,4 +49,42 @@ export class SceneEditUseCase {
|
|||||||
shotIds
|
shotIds
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新获取当前场景的数据
|
||||||
|
* @description 从服务器重新获取当前场景的AI文本和标签数据,并更新当前实体
|
||||||
|
* @returns Promise<{ text: AITextEntity; tags: TagEntity[] }> 场景相关的AI文本和标签数据
|
||||||
|
* @throws {Error} 当API调用失败时抛出错误
|
||||||
|
*/
|
||||||
|
async refreshSceneData(): Promise<{ text: AITextEntity; tags: TagEntity[] }> {
|
||||||
|
const sceneId = this.sceneItem.entity.id;
|
||||||
|
|
||||||
|
if (!sceneId) {
|
||||||
|
throw new Error('场景ID不存在,无法获取场景数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getSceneData({
|
||||||
|
sceneId: sceneId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.successful) {
|
||||||
|
// 更新当前场景的实体数据
|
||||||
|
const { text, tags } = response.data;
|
||||||
|
|
||||||
|
// 更新场景实体中的相关字段
|
||||||
|
const updatedSceneEntity = {
|
||||||
|
...this.sceneItem.entity,
|
||||||
|
generateTextId: text.id, // 更新AI文本ID
|
||||||
|
tagIds: tags.map(tag => tag.id), // 更新标签ID列表
|
||||||
|
updatedAt: Date.now(), // 更新时间戳
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新当前UseCase中的实体
|
||||||
|
this.sceneItem.setEntity(updatedSceneEntity);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(`获取场景数据失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
203
app/service/usecase/ShotEditUsecase.ts
Normal file
203
app/service/usecase/ShotEditUsecase.ts
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import { ShotEntity, RoleEntity, SceneEntity, AITextEntity, TagEntity } from '../domain/Entities';
|
||||||
|
import { ShotItem, RoleItem, SceneItem, TextItem, TagItem } from '../domain/Item';
|
||||||
|
import {
|
||||||
|
getShotRoles,
|
||||||
|
getShotScenes,
|
||||||
|
getShotData,
|
||||||
|
regenerateShot,
|
||||||
|
updateShotContent
|
||||||
|
} from '@/api/video_flow';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分镜编辑用例
|
||||||
|
* 负责分镜内容的初始化、修改和优化
|
||||||
|
*/
|
||||||
|
export class ShotEditUseCase {
|
||||||
|
constructor(private shotItem: ShotItem) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分镜关联的角色信息列表
|
||||||
|
* @description 获取当前分镜可以使用的角色列表
|
||||||
|
* @returns Promise<RoleEntity[]> 角色信息列表
|
||||||
|
* @throws {Error} 当API调用失败时抛出错误
|
||||||
|
*/
|
||||||
|
async getShotRoles(): Promise<RoleEntity[]> {
|
||||||
|
const shotId = this.shotItem.entity.id;
|
||||||
|
|
||||||
|
if (!shotId) {
|
||||||
|
throw new Error('分镜ID不存在,无法获取角色信息');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getShotRoles({
|
||||||
|
shotId: shotId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.successful) {
|
||||||
|
return response.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(`获取分镜角色信息失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分镜关联的场景信息列表
|
||||||
|
* @description 获取当前分镜可以使用的场景列表
|
||||||
|
* @returns Promise<SceneEntity[]> 场景信息列表
|
||||||
|
* @throws {Error} 当API调用失败时抛出错误
|
||||||
|
*/
|
||||||
|
async getShotScenes(): Promise<SceneEntity[]> {
|
||||||
|
const shotId = this.shotItem.entity.id;
|
||||||
|
|
||||||
|
if (!shotId) {
|
||||||
|
throw new Error('分镜ID不存在,无法获取场景信息');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getShotScenes({
|
||||||
|
shotId: shotId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.successful) {
|
||||||
|
return response.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(`获取分镜场景信息失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新获取当前分镜信息
|
||||||
|
* @description 从服务器重新获取当前分镜的详细数据,并更新当前实体
|
||||||
|
* @returns Promise<{ text: AITextEntity; tags: TagEntity[] }> 分镜相关的AI文本和标签数据
|
||||||
|
* @throws {Error} 当API调用失败时抛出错误
|
||||||
|
*/
|
||||||
|
async refreshShotData(): Promise<{ text: AITextEntity; tags: TagEntity[] }> {
|
||||||
|
const shotId = this.shotItem.entity.id;
|
||||||
|
|
||||||
|
if (!shotId) {
|
||||||
|
throw new Error('分镜ID不存在,无法获取分镜数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getShotData({
|
||||||
|
shotId: shotId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.successful) {
|
||||||
|
// 更新当前分镜的实体数据
|
||||||
|
const { text, tags } = response.data;
|
||||||
|
|
||||||
|
// 更新分镜实体中的相关字段
|
||||||
|
const updatedShotEntity = {
|
||||||
|
...this.shotItem.entity,
|
||||||
|
generateTextId: text.id, // 更新AI文本ID
|
||||||
|
tagIds: tags.map((tag: TagEntity) => tag.id), // 更新标签ID列表
|
||||||
|
updatedAt: Date.now(), // 更新时间戳
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新当前UseCase中的实体
|
||||||
|
this.shotItem.setEntity(updatedShotEntity);
|
||||||
|
// 检查状态是否需要更新为视频状态
|
||||||
|
this.checkAndUpdateVideoStatus(updatedShotEntity);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(`获取分镜数据失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新生成当前分镜
|
||||||
|
* @description 使用镜头、对话内容、角色ID替换参数、场景ID替换参数重新生成分镜
|
||||||
|
* @param shotPrompt 镜头描述
|
||||||
|
* @param dialogueContent 对话内容
|
||||||
|
* @param roleReplaceParams 角色ID替换参数,格式为{oldId:string,newId:string}[]
|
||||||
|
* @param sceneReplaceParams 场景ID替换参数,格式为{oldId:string,newId:string}[]
|
||||||
|
* @returns Promise<ShotEntity> 重新生成的分镜实体
|
||||||
|
* @throws {Error} 当API调用失败时抛出错误
|
||||||
|
*/
|
||||||
|
async regenerateShot(
|
||||||
|
shotPrompt: string,
|
||||||
|
dialogueContent: string,
|
||||||
|
roleReplaceParams: { oldId: string; newId: string }[],
|
||||||
|
sceneReplaceParams: { oldId: string; newId: string }[]
|
||||||
|
): Promise<ShotEntity> {
|
||||||
|
const shotId = this.shotItem.entity.id;
|
||||||
|
|
||||||
|
if (!shotId) {
|
||||||
|
throw new Error('分镜ID不存在,无法重新生成分镜');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用重新生成分镜接口
|
||||||
|
const response = await regenerateShot({
|
||||||
|
shotId: shotId,
|
||||||
|
shotPrompt: shotPrompt,
|
||||||
|
dialogueContent: dialogueContent,
|
||||||
|
roleReplaceParams: roleReplaceParams,
|
||||||
|
sceneReplaceParams: sceneReplaceParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.successful) {
|
||||||
|
const shotEntity = response.data;
|
||||||
|
this.shotItem.setEntity(shotEntity);
|
||||||
|
// 检查状态是否需要更新为视频状态
|
||||||
|
this.checkAndUpdateVideoStatus(shotEntity);
|
||||||
|
return shotEntity;
|
||||||
|
} else {
|
||||||
|
throw new Error(`重新生成分镜失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改分镜对话内容
|
||||||
|
* @description 更新分镜的对话内容,ContentItem数量和ID顺序不能变,只能修改content字段
|
||||||
|
* @param newContent 新的对话内容数组
|
||||||
|
* @returns Promise<ShotEntity> 修改后的分镜实体
|
||||||
|
* @throws {Error} 当API调用失败时抛出错误
|
||||||
|
*/
|
||||||
|
async updateShotContent(newContent: Array<{ roleId: string; content: string }>): Promise<ShotEntity> {
|
||||||
|
const shotId = this.shotItem.entity.id;
|
||||||
|
|
||||||
|
if (!shotId) {
|
||||||
|
throw new Error('分镜ID不存在,无法修改对话内容');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证ContentItem数量和ID顺序
|
||||||
|
const currentContent = this.shotItem.entity.content;
|
||||||
|
if (newContent.length !== currentContent.length) {
|
||||||
|
throw new Error('ContentItem数量不能改变');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证角色ID顺序
|
||||||
|
for (let i = 0; i < newContent.length; i++) {
|
||||||
|
if (newContent[i].roleId !== currentContent[i].roleId) {
|
||||||
|
throw new Error('ContentItem的roleId顺序不能改变');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await updateShotContent({
|
||||||
|
shotId: shotId,
|
||||||
|
content: newContent,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.successful) {
|
||||||
|
const shotEntity = response.data;
|
||||||
|
this.shotItem.setEntity(shotEntity);
|
||||||
|
// 检查状态是否需要更新为视频状态
|
||||||
|
this.checkAndUpdateVideoStatus(shotEntity);
|
||||||
|
return shotEntity;
|
||||||
|
} else {
|
||||||
|
throw new Error(`修改分镜对话内容失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查并更新视频状态
|
||||||
|
* @description 当分镜状态变为视频加载中或完成时,调用updateToVideoStatus
|
||||||
|
* @param shotEntity 分镜实体
|
||||||
|
*/
|
||||||
|
private checkAndUpdateVideoStatus(shotEntity: ShotEntity): void {
|
||||||
|
// 当状态为视频加载中或完成时,更新为视频状态
|
||||||
|
if (shotEntity.status === 1 || shotEntity.status === 2) { // videoLoading 或 finished
|
||||||
|
this.shotItem.updateToVideoStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,9 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user