forked from 77media/video-flow
新增角色和场景服务Hook,支持角色和场景的生成、应用及标签修改功能;更新API接口以支持新功能。
This commit is contained in:
parent
39a3de215c
commit
bf4247cfae
@ -2,6 +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';
|
||||||
|
|
||||||
// API 响应类型
|
// API 响应类型
|
||||||
interface BaseApiResponse<T> {
|
interface BaseApiResponse<T> {
|
||||||
@ -215,3 +216,126 @@ export const getShotSketchJson = async (data: { project_id: string }): Promise<A
|
|||||||
export const getVideoJson = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
|
export const getVideoJson = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
|
||||||
return post<any>('/movie/video_json', data);
|
return post<any>('/movie/video_json', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新生成角色
|
||||||
|
* @param request - 重新生成角色请求参数
|
||||||
|
* @returns Promise<ApiResponse<RoleEntity>>
|
||||||
|
*/
|
||||||
|
export const regenerateRole = async (request: {
|
||||||
|
/** 角色提示词 */
|
||||||
|
prompt: string;
|
||||||
|
/** 标签类型列表 */
|
||||||
|
tagTypes: (number | string)[];
|
||||||
|
/** 角色ID(可选,如果重新生成现有角色) */
|
||||||
|
roleId?: string;
|
||||||
|
}): Promise<ApiResponse<RoleEntity>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/regenerate_role', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用角色到分镜
|
||||||
|
* @param request - 应用角色请求参数
|
||||||
|
* @returns Promise<ApiResponse<应用结果>>
|
||||||
|
*/
|
||||||
|
export const applyRoleToShots = async (request: {
|
||||||
|
/** 角色ID */
|
||||||
|
roleId: string;
|
||||||
|
/** 分镜ID列表 */
|
||||||
|
shotIds: string[];
|
||||||
|
}): Promise<ApiResponse<any>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/apply_role_to_shots', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改标签
|
||||||
|
* @param request - 修改标签请求参数
|
||||||
|
* @returns Promise<ApiResponse<修改后的标签>>
|
||||||
|
*/
|
||||||
|
export const updateTag = async (request: {
|
||||||
|
/** 标签ID */
|
||||||
|
tagId: string;
|
||||||
|
/** 新的标签内容 */
|
||||||
|
content: string|number;
|
||||||
|
}): Promise<ApiResponse<TagEntity>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/update_tag', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改文案
|
||||||
|
* @param request - 修改文案请求参数
|
||||||
|
* @returns Promise<ApiResponse<修改后的文案>>
|
||||||
|
*/
|
||||||
|
export const updateText = async (request: {
|
||||||
|
/** 文案ID */
|
||||||
|
textId: string;
|
||||||
|
/** 新的文案内容 */
|
||||||
|
content: string;
|
||||||
|
}): Promise<ApiResponse<AITextEntity>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/update_text', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新生成场景
|
||||||
|
* @param request - 重新生成场景请求参数
|
||||||
|
* @returns Promise<ApiResponse<重新生成的场景>>
|
||||||
|
*/
|
||||||
|
export const regenerateScene = async (request: {
|
||||||
|
/** 场景提示词 */
|
||||||
|
prompt: string;
|
||||||
|
/** 标签类型列表 */
|
||||||
|
tagTypes: (number | string)[];
|
||||||
|
/** 场景ID(可选,如果重新生成现有场景) */
|
||||||
|
sceneId?: string;
|
||||||
|
}): Promise<ApiResponse<SceneEntity>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/regenerate_scene', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用场景到分镜
|
||||||
|
* @param request - 应用场景请求参数
|
||||||
|
* @returns Promise<ApiResponse<应用结果>>
|
||||||
|
*/
|
||||||
|
export const applySceneToShots = async (request: {
|
||||||
|
/** 场景ID */
|
||||||
|
sceneId: string;
|
||||||
|
/** 分镜ID列表 */
|
||||||
|
shotIds: string[];
|
||||||
|
}): Promise<ApiResponse<any>> => {
|
||||||
|
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 getSceneShots = async (request: {
|
||||||
|
/** 场景ID */
|
||||||
|
sceneId: string;
|
||||||
|
}): Promise<ApiResponse<{
|
||||||
|
/** 分镜列表 */
|
||||||
|
shots: ShotEntity[];
|
||||||
|
/** 已应用的分镜ID列表 */
|
||||||
|
appliedShotIds: string[];
|
||||||
|
}>> => {
|
||||||
|
return post<ApiResponse<any>>('/movie/get_scene_shots', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
151
app/service/Interaction/RoleService.puml
Normal file
151
app/service/Interaction/RoleService.puml
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
@startuml RoleService Hook 架构图
|
||||||
|
|
||||||
|
!theme plain
|
||||||
|
skinparam backgroundColor #FFFFFF
|
||||||
|
skinparam componentStyle rectangle
|
||||||
|
|
||||||
|
' 主要模块
|
||||||
|
package "RoleService Hook" as RoleServiceHook {
|
||||||
|
component "响应式状态管理" as StateManagement
|
||||||
|
component "计算属性" as ComputedProps
|
||||||
|
component "角色操作方法" as RoleOperations
|
||||||
|
component "文本操作方法" as TextOperations
|
||||||
|
component "标签操作方法" as TagOperations
|
||||||
|
component "分镜操作方法" as ShotOperations
|
||||||
|
}
|
||||||
|
|
||||||
|
' API层
|
||||||
|
package "API接口层" as APILayer {
|
||||||
|
component "角色相关API" as RoleAPI {
|
||||||
|
[regenerateRole()]
|
||||||
|
[applyRoleToShots()]
|
||||||
|
[getRoleShots()]
|
||||||
|
}
|
||||||
|
|
||||||
|
component "文本相关API" as TextAPI {
|
||||||
|
[updateText()]
|
||||||
|
}
|
||||||
|
|
||||||
|
component "标签相关API" as TagAPI {
|
||||||
|
[updateTag()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
' UseCase层
|
||||||
|
package "UseCase层" as UseCaseLayer {
|
||||||
|
component "RoleEditUseCase" as RoleEditUseCase {
|
||||||
|
[AIgenerateRole(prompt, tags)]
|
||||||
|
[applyRole(shotIds)]
|
||||||
|
}
|
||||||
|
|
||||||
|
component "TextEditUseCase" as TextEditUseCase {
|
||||||
|
[updateText(content)]
|
||||||
|
[getOptimizedContent()]
|
||||||
|
}
|
||||||
|
|
||||||
|
component "TagEditUseCase" as TagEditUseCase {
|
||||||
|
[updateTag(content)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
' Domain层
|
||||||
|
package "Domain层" as DomainLayer {
|
||||||
|
component "实体定义" as Entities {
|
||||||
|
[RoleEntity]
|
||||||
|
[AITextEntity]
|
||||||
|
[TagEntity]
|
||||||
|
[ShotEntity]
|
||||||
|
}
|
||||||
|
|
||||||
|
component "可编辑项" as Items {
|
||||||
|
[RoleItem]
|
||||||
|
[TextItem]
|
||||||
|
[TagItem]
|
||||||
|
[ShotItem]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
' React Hook
|
||||||
|
package "React Hook" as ReactHook {
|
||||||
|
component "useState" as UseState
|
||||||
|
component "useCallback" as UseCallback
|
||||||
|
component "useMemo" as UseMemo
|
||||||
|
}
|
||||||
|
|
||||||
|
' 依赖关系
|
||||||
|
' Hook内部依赖
|
||||||
|
RoleServiceHook --> StateManagement : 管理状态
|
||||||
|
RoleServiceHook --> ComputedProps : 计算属性
|
||||||
|
RoleServiceHook --> RoleOperations : 角色操作
|
||||||
|
RoleServiceHook --> TextOperations : 文本操作
|
||||||
|
RoleServiceHook --> TagOperations : 标签操作
|
||||||
|
RoleServiceHook --> ShotOperations : 分镜操作
|
||||||
|
|
||||||
|
' 操作方法依赖UseCase
|
||||||
|
RoleOperations --> RoleEditUseCase : 调用
|
||||||
|
TextOperations --> TextEditUseCase : 调用
|
||||||
|
TagOperations --> TagEditUseCase : 调用
|
||||||
|
ShotOperations --> RoleEditUseCase : 调用
|
||||||
|
|
||||||
|
' UseCase依赖API
|
||||||
|
RoleEditUseCase --> RoleAPI : 调用
|
||||||
|
TextEditUseCase --> TextAPI : 调用
|
||||||
|
TagEditUseCase --> TagAPI : 调用
|
||||||
|
|
||||||
|
' 状态管理依赖Domain
|
||||||
|
StateManagement --> Items : 使用
|
||||||
|
ComputedProps --> Entities : 计算
|
||||||
|
RoleOperations --> Items : 操作
|
||||||
|
TextOperations --> Items : 操作
|
||||||
|
TagOperations --> Items : 操作
|
||||||
|
ShotOperations --> Items : 操作
|
||||||
|
|
||||||
|
' React Hook依赖
|
||||||
|
RoleServiceHook --> UseState : 状态管理
|
||||||
|
RoleServiceHook --> UseCallback : 方法优化
|
||||||
|
RoleServiceHook --> UseMemo : 计算优化
|
||||||
|
|
||||||
|
' 数据流
|
||||||
|
note right of StateManagement
|
||||||
|
响应式状态:
|
||||||
|
- roleList: 角色列表
|
||||||
|
- selectedRole: 当前选中角色
|
||||||
|
- currentRoleText: 当前AI文本
|
||||||
|
- currentRoleTags: 当前标签列表
|
||||||
|
- shotSelectionList: 分镜选择列表
|
||||||
|
end note
|
||||||
|
|
||||||
|
note right of ComputedProps
|
||||||
|
计算属性:
|
||||||
|
- roleImageUrl: 角色图片URL
|
||||||
|
- isAllShotsSelected: 是否全选
|
||||||
|
- selectedShotsCount: 选中数量
|
||||||
|
end note
|
||||||
|
|
||||||
|
note right of RoleOperations
|
||||||
|
角色操作:
|
||||||
|
- selectRole: 选择角色
|
||||||
|
- regenerateRole: 重新生成
|
||||||
|
end note
|
||||||
|
|
||||||
|
note right of TextOperations
|
||||||
|
文本操作:
|
||||||
|
- optimizeRoleText: 优化文本
|
||||||
|
- updateRoleText: 修改文本
|
||||||
|
end note
|
||||||
|
|
||||||
|
note right of TagOperations
|
||||||
|
标签操作:
|
||||||
|
- updateTagContent: 修改标签
|
||||||
|
end note
|
||||||
|
|
||||||
|
note right of ShotOperations
|
||||||
|
分镜操作:
|
||||||
|
- fetchRoleShots: 获取分镜列表
|
||||||
|
- selectAllShots: 全选
|
||||||
|
- invertShotSelection: 反选
|
||||||
|
- toggleShotSelection: 切换选择
|
||||||
|
- applyRoleToSelectedShots: 应用角色
|
||||||
|
end note
|
||||||
|
|
||||||
|
@enduml
|
||||||
391
app/service/Interaction/RoleService.ts
Normal file
391
app/service/Interaction/RoleService.ts
Normal file
@ -0,0 +1,391 @@
|
|||||||
|
import { useState, useCallback, useMemo } from 'react';
|
||||||
|
import { RoleEntity, TagEntity, AITextEntity, ShotEntity } from '../domain/Entities';
|
||||||
|
import { RoleItem, TagItem, TextItem, ShotItem } from '../domain/Item';
|
||||||
|
import { RoleEditUseCase } from '../usecase/RoleEditUseCase';
|
||||||
|
import { TagEditUseCase } from '../usecase/TagEditUseCase';
|
||||||
|
import { TextEditUseCase } from '../usecase/TextEditUseCase';
|
||||||
|
import { getRoleShots } from '@/api/video_flow';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分镜选择项接口
|
||||||
|
*/
|
||||||
|
interface ShotSelectionItem {
|
||||||
|
/** 分镜ID */
|
||||||
|
id: string;
|
||||||
|
/** 分镜名称 */
|
||||||
|
name: string;
|
||||||
|
/** 是否已选中 */
|
||||||
|
selected: boolean;
|
||||||
|
/** 是否已应用角色 */
|
||||||
|
applied: boolean;
|
||||||
|
/** 分镜数据 */
|
||||||
|
shot: ShotEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色服务Hook返回值接口
|
||||||
|
*/
|
||||||
|
interface UseRoleService {
|
||||||
|
// 响应式数据
|
||||||
|
/** 角色列表 */
|
||||||
|
roleList: RoleItem[];
|
||||||
|
/** 当前选中的角色 */
|
||||||
|
selectedRole: RoleItem | null;
|
||||||
|
/** 当前角色的AI文本 */
|
||||||
|
currentRoleText: TextItem | null;
|
||||||
|
/** 当前角色的标签列表 */
|
||||||
|
currentRoleTags: TagItem[];
|
||||||
|
/** 角色图片URL */
|
||||||
|
roleImageUrl: string;
|
||||||
|
/** 分镜选择列表 */
|
||||||
|
shotSelectionList: ShotSelectionItem[];
|
||||||
|
/** 是否全选分镜 */
|
||||||
|
isAllShotsSelected: boolean;
|
||||||
|
/** 已选中的分镜数量 */
|
||||||
|
selectedShotsCount: number;
|
||||||
|
|
||||||
|
// 操作方法
|
||||||
|
/** 选择角色 */
|
||||||
|
selectRole: (roleId: string) => void;
|
||||||
|
/** 设置当前角色的AI文本和标签 */
|
||||||
|
setCurrentRoleData: (text: TextItem, tags: TagItem[]) => void;
|
||||||
|
/** 优化AI文本 */
|
||||||
|
optimizeRoleText: () => Promise<void>;
|
||||||
|
/** 修改AI文本 */
|
||||||
|
updateRoleText: (newContent: string) => Promise<void>;
|
||||||
|
/** 修改标签内容 */
|
||||||
|
updateTagContent: (tagId: string, newContent: string | number) => Promise<void>;
|
||||||
|
/** 重新生成角色 */
|
||||||
|
regenerateRole: () => Promise<void>;
|
||||||
|
/** 获取角色出现的分镜列表 */
|
||||||
|
fetchRoleShots: () => Promise<void>;
|
||||||
|
/** 切换全选与全不选 */
|
||||||
|
toggleSelectAllShots: () => void;
|
||||||
|
/** 选择/取消选择单个分镜 */
|
||||||
|
toggleShotSelection: (shotId: string) => void;
|
||||||
|
/** 应用角色到选中的分镜 */
|
||||||
|
applyRoleToSelectedShots: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色服务Hook
|
||||||
|
* 提供角色相关的所有响应式功能和业务逻辑
|
||||||
|
*/
|
||||||
|
export const useRoleServiceHook = (): UseRoleService => {
|
||||||
|
// 响应式状态
|
||||||
|
const [roleList, setRoleList] = useState<RoleItem[]>([]);
|
||||||
|
const [selectedRole, setSelectedRole] = useState<RoleItem | null>(null);
|
||||||
|
const [currentRoleText, setCurrentRoleText] = useState<TextItem | null>(null);
|
||||||
|
const [currentRoleTags, setCurrentRoleTags] = useState<TagItem[]>([]);
|
||||||
|
const [shotSelectionList, setShotSelectionList] = useState<ShotSelectionItem[]>([]);
|
||||||
|
|
||||||
|
// UseCase实例 - 在角色选择时初始化
|
||||||
|
const [roleEditUseCase, setRoleEditUseCase] = useState<RoleEditUseCase | null>(null);
|
||||||
|
const [textEditUseCase, setTextEditUseCase] = useState<TextEditUseCase | null>(null);
|
||||||
|
const [tagEditUseCases, setTagEditUseCases] = useState<Map<string, TagEditUseCase>>(new Map());
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
/**
|
||||||
|
* 角色图片URL
|
||||||
|
* @description 获取当前选中角色的图片URL
|
||||||
|
*/
|
||||||
|
const roleImageUrl = useMemo(() => {
|
||||||
|
return selectedRole?.entity.imageUrl || '';
|
||||||
|
}, [selectedRole]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否全选分镜
|
||||||
|
* @description 判断是否所有分镜都被选中
|
||||||
|
*/
|
||||||
|
const isAllShotsSelected = useMemo(() => {
|
||||||
|
return shotSelectionList.length > 0 && shotSelectionList.every(shot => shot.selected);
|
||||||
|
}, [shotSelectionList]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已选中的分镜数量
|
||||||
|
* @description 获取当前选中的分镜数量
|
||||||
|
*/
|
||||||
|
const selectedShotsCount = useMemo(() => {
|
||||||
|
return shotSelectionList.filter(shot => shot.selected).length;
|
||||||
|
}, [shotSelectionList]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择角色
|
||||||
|
* @description 根据角色ID选择角色,并初始化相关的UseCase实例
|
||||||
|
* @param roleId 角色ID
|
||||||
|
*/
|
||||||
|
const selectRole = useCallback((roleId: string) => {
|
||||||
|
const role = roleList.find(r => r.entity.id === roleId);
|
||||||
|
if (role) {
|
||||||
|
setSelectedRole(role);
|
||||||
|
|
||||||
|
// 初始化UseCase实例
|
||||||
|
setRoleEditUseCase(new RoleEditUseCase(role));
|
||||||
|
setTextEditUseCase(null); // 文本UseCase在获取到文本后初始化
|
||||||
|
setTagEditUseCases(new Map()); // 标签UseCase在获取到标签后初始化
|
||||||
|
|
||||||
|
setCurrentRoleText(null);
|
||||||
|
setCurrentRoleTags([]);
|
||||||
|
}
|
||||||
|
}, [roleList]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优化AI文本
|
||||||
|
* @description 对当前角色的AI文本进行优化,无文本时不可进行优化
|
||||||
|
* @throws {Error} 当没有可优化的文本内容或UseCase未初始化时抛出错误
|
||||||
|
* @returns {Promise<void>} 优化完成后的Promise
|
||||||
|
*/
|
||||||
|
const optimizeRoleText = useCallback(async () => {
|
||||||
|
if (!textEditUseCase) {
|
||||||
|
throw new Error('文本编辑UseCase未初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentRoleText || !currentRoleText.entity.content) {
|
||||||
|
throw new Error('没有可优化的文本内容');
|
||||||
|
}
|
||||||
|
|
||||||
|
const optimizedContent = await textEditUseCase.getOptimizedContent();
|
||||||
|
|
||||||
|
// 更新文本内容
|
||||||
|
const updatedTextItem = await textEditUseCase.updateText(optimizedContent);
|
||||||
|
setCurrentRoleText(updatedTextItem);
|
||||||
|
}, [textEditUseCase, currentRoleText]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改AI文本
|
||||||
|
* @description 手动修改当前角色的AI文本内容
|
||||||
|
* @param newContent 新的文本内容
|
||||||
|
* @throws {Error} 当没有可编辑的文本或UseCase未初始化时抛出错误
|
||||||
|
* @returns {Promise<void>} 修改完成后的Promise
|
||||||
|
*/
|
||||||
|
const updateRoleText = useCallback(async (newContent: string) => {
|
||||||
|
if (!textEditUseCase) {
|
||||||
|
throw new Error('文本编辑UseCase未初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentRoleText) {
|
||||||
|
throw new Error('没有可编辑的文本');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedTextItem = await textEditUseCase.updateText(newContent);
|
||||||
|
setCurrentRoleText(updatedTextItem);
|
||||||
|
}, [textEditUseCase, currentRoleText]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改标签内容
|
||||||
|
* @description 修改指定标签的内容
|
||||||
|
* @param tagId 标签ID
|
||||||
|
* @param newContent 新的标签内容
|
||||||
|
* @throws {Error} 当标签不存在或UseCase未初始化时抛出错误
|
||||||
|
* @returns {Promise<void>} 修改完成后的Promise
|
||||||
|
*/
|
||||||
|
const updateTagContent = useCallback(async (tagId: string, newContent: string | number) => {
|
||||||
|
const tagEditUseCase = tagEditUseCases.get(tagId);
|
||||||
|
if (!tagEditUseCase) {
|
||||||
|
throw new Error(`标签编辑UseCase未初始化,标签ID: ${tagId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedTagItem = await tagEditUseCase.updateTag(newContent);
|
||||||
|
|
||||||
|
// 更新标签列表
|
||||||
|
setCurrentRoleTags(prev =>
|
||||||
|
prev.map(tag =>
|
||||||
|
tag.entity.id === tagId
|
||||||
|
? updatedTagItem
|
||||||
|
: tag
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [tagEditUseCases]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新生成角色
|
||||||
|
* @description 使用AI文本和标签重新生成角色
|
||||||
|
* @throws {Error} 当缺少重新生成角色所需的数据或UseCase未初始化时抛出错误
|
||||||
|
* @returns {Promise<void>} 重新生成完成后的Promise
|
||||||
|
*/
|
||||||
|
const regenerateRole = useCallback(async () => {
|
||||||
|
if (!roleEditUseCase) {
|
||||||
|
throw new Error('角色编辑UseCase未初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedRole || !currentRoleText || currentRoleTags.length === 0) {
|
||||||
|
throw new Error('缺少重新生成角色所需的数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRoleEntity = await roleEditUseCase.AIgenerateRole(currentRoleText, currentRoleTags);
|
||||||
|
|
||||||
|
// 更新角色
|
||||||
|
const newRoleItem = new RoleItem(newRoleEntity);
|
||||||
|
setSelectedRole(newRoleItem);
|
||||||
|
|
||||||
|
// 更新角色列表
|
||||||
|
setRoleList(prev =>
|
||||||
|
prev.map(role =>
|
||||||
|
role.entity.id === newRoleEntity.id ? newRoleItem : role
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [roleEditUseCase, selectedRole, currentRoleText, currentRoleTags]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色出现的分镜列表
|
||||||
|
* @description 获取当前角色应用到的分镜列表,包括已应用状态
|
||||||
|
* @throws {Error} 当未选择角色或API调用失败时抛出错误
|
||||||
|
* @returns {Promise<void>} 获取完成后的Promise
|
||||||
|
*/
|
||||||
|
const fetchRoleShots = useCallback(async () => {
|
||||||
|
if (!selectedRole) {
|
||||||
|
throw new Error('请先选择角色');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getRoleShots({
|
||||||
|
roleId: selectedRole.entity.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.successful) {
|
||||||
|
const { shots, appliedShotIds } = response.data;
|
||||||
|
|
||||||
|
const shotSelectionItems: ShotSelectionItem[] = shots.map(shot => ({
|
||||||
|
id: shot.id,
|
||||||
|
name: shot.name,
|
||||||
|
selected: false,
|
||||||
|
applied: appliedShotIds.includes(shot.id), // 根据API返回的已应用列表判断
|
||||||
|
shot
|
||||||
|
}));
|
||||||
|
|
||||||
|
setShotSelectionList(shotSelectionItems);
|
||||||
|
} else {
|
||||||
|
throw new Error(`获取角色分镜列表失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取角色分镜列表失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, [selectedRole]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换全选与全不选
|
||||||
|
* @description 如果当前是全选状态则全不选,否则全选
|
||||||
|
*/
|
||||||
|
const toggleSelectAllShots = useCallback(() => {
|
||||||
|
setShotSelectionList(prev => {
|
||||||
|
const isAllSelected = prev.length > 0 && prev.every(shot => shot.selected);
|
||||||
|
return prev.map(shot => ({ ...shot, selected: !isAllSelected }));
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择/取消选择单个分镜
|
||||||
|
* @description 切换指定分镜的选择状态
|
||||||
|
* @param shotId 分镜ID
|
||||||
|
*/
|
||||||
|
const toggleShotSelection = useCallback((shotId: string) => {
|
||||||
|
setShotSelectionList(prev =>
|
||||||
|
prev.map(shot =>
|
||||||
|
shot.id === shotId
|
||||||
|
? { ...shot, selected: !shot.selected }
|
||||||
|
: shot
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用角色到选中的分镜
|
||||||
|
* @description 将当前角色应用到选中的分镜,并更新应用状态
|
||||||
|
* @throws {Error} 当未选择角色、未选择分镜或UseCase未初始化时抛出错误
|
||||||
|
* @returns {Promise<void>} 应用完成后的Promise
|
||||||
|
*/
|
||||||
|
const applyRoleToSelectedShots = useCallback(async () => {
|
||||||
|
if (!roleEditUseCase) {
|
||||||
|
throw new Error('角色编辑UseCase未初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedRole) {
|
||||||
|
throw new Error('请先选择角色');
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedShotIds = shotSelectionList
|
||||||
|
.filter(shot => shot.selected)
|
||||||
|
.map(shot => shot.id);
|
||||||
|
|
||||||
|
if (selectedShotIds.length === 0) {
|
||||||
|
throw new Error('请先选择要应用的分镜');
|
||||||
|
}
|
||||||
|
|
||||||
|
await roleEditUseCase.applyRole(selectedShotIds);
|
||||||
|
|
||||||
|
// 更新分镜列表,标记已应用
|
||||||
|
setShotSelectionList(prev =>
|
||||||
|
prev.map(shot =>
|
||||||
|
selectedShotIds.includes(shot.id)
|
||||||
|
? { ...shot, applied: true, selected: false }
|
||||||
|
: shot
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [roleEditUseCase, selectedRole, shotSelectionList]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前角色的AI文本和标签
|
||||||
|
* @description 设置当前角色的AI文本和标签,并初始化对应的UseCase
|
||||||
|
* @param text AI文本项
|
||||||
|
* @param tags 标签项列表
|
||||||
|
*/
|
||||||
|
const setCurrentRoleData = useCallback((text: TextItem, tags: TagItem[]) => {
|
||||||
|
setCurrentRoleText(text);
|
||||||
|
setCurrentRoleTags(tags);
|
||||||
|
|
||||||
|
// 初始化文本UseCase
|
||||||
|
if (text) {
|
||||||
|
setTextEditUseCase(new TextEditUseCase(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化标签UseCase
|
||||||
|
const newTagEditUseCases = new Map<string, TagEditUseCase>();
|
||||||
|
tags.forEach(tag => {
|
||||||
|
newTagEditUseCases.set(tag.entity.id, new TagEditUseCase(tag));
|
||||||
|
});
|
||||||
|
setTagEditUseCases(newTagEditUseCases);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 响应式数据
|
||||||
|
/** 角色列表 */
|
||||||
|
roleList,
|
||||||
|
/** 当前选中的角色 */
|
||||||
|
selectedRole,
|
||||||
|
/** 当前角色的AI文本 */
|
||||||
|
currentRoleText,
|
||||||
|
/** 当前角色的标签列表 */
|
||||||
|
currentRoleTags,
|
||||||
|
/** 角色图片URL */
|
||||||
|
roleImageUrl,
|
||||||
|
/** 分镜选择列表 */
|
||||||
|
shotSelectionList,
|
||||||
|
/** 是否全选分镜 */
|
||||||
|
isAllShotsSelected,
|
||||||
|
/** 已选中的分镜数量 */
|
||||||
|
selectedShotsCount,
|
||||||
|
|
||||||
|
// 操作方法
|
||||||
|
/** 选择角色 */
|
||||||
|
selectRole,
|
||||||
|
/** 设置当前角色的AI文本和标签 */
|
||||||
|
setCurrentRoleData,
|
||||||
|
/** 优化AI文本 */
|
||||||
|
optimizeRoleText,
|
||||||
|
/** 修改AI文本 */
|
||||||
|
updateRoleText,
|
||||||
|
/** 修改标签内容 */
|
||||||
|
updateTagContent,
|
||||||
|
/** 重新生成角色 */
|
||||||
|
regenerateRole,
|
||||||
|
/** 获取角色出现的分镜列表 */
|
||||||
|
fetchRoleShots,
|
||||||
|
/** 切换全选与全不选 */
|
||||||
|
toggleSelectAllShots,
|
||||||
|
/** 选择/取消选择单个分镜 */
|
||||||
|
toggleShotSelection,
|
||||||
|
/** 应用角色到选中的分镜 */
|
||||||
|
applyRoleToSelectedShots
|
||||||
|
};
|
||||||
|
};
|
||||||
301
app/service/Interaction/SceneService.ts
Normal file
301
app/service/Interaction/SceneService.ts
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
import { useState, useCallback, useMemo } from 'react';
|
||||||
|
import { SceneEntity, TagEntity, AITextEntity, ShotEntity } from '../domain/Entities';
|
||||||
|
import { SceneItem, TagItem, TextItem, ShotItem } from '../domain/Item';
|
||||||
|
import { SceneEditUseCase } from '../usecase/SceneEditUseCase';
|
||||||
|
import { TagEditUseCase } from '../usecase/TagEditUseCase';
|
||||||
|
import { TextEditUseCase } from '../usecase/TextEditUseCase';
|
||||||
|
import { getSceneShots } from '@/api/video_flow';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分镜选择项接口
|
||||||
|
*/
|
||||||
|
interface ShotSelectionItem {
|
||||||
|
/** 分镜ID */
|
||||||
|
id: string;
|
||||||
|
/** 分镜名称 */
|
||||||
|
name: string;
|
||||||
|
/** 是否已选中 */
|
||||||
|
selected: boolean;
|
||||||
|
/** 是否已应用场景 */
|
||||||
|
applied: boolean;
|
||||||
|
/** 分镜数据 */
|
||||||
|
shot: ShotEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景服务Hook返回值接口
|
||||||
|
*/
|
||||||
|
interface UseSceneService {
|
||||||
|
// 响应式数据
|
||||||
|
/** 场景列表 */
|
||||||
|
sceneList: SceneItem[];
|
||||||
|
/** 当前选中的场景 */
|
||||||
|
selectedScene: SceneItem | null;
|
||||||
|
/** 当前场景的AI文本 */
|
||||||
|
currentSceneText: TextItem | null;
|
||||||
|
/** 当前场景的标签列表 */
|
||||||
|
currentSceneTags: TagItem[];
|
||||||
|
/** 场景图片URL */
|
||||||
|
sceneImageUrl: string;
|
||||||
|
/** 分镜选择列表 */
|
||||||
|
shotSelectionList: ShotSelectionItem[];
|
||||||
|
/** 是否全选分镜 */
|
||||||
|
isAllShotsSelected: boolean;
|
||||||
|
/** 已选中的分镜数量 */
|
||||||
|
selectedShotsCount: number;
|
||||||
|
|
||||||
|
// 操作方法
|
||||||
|
/** 选择场景 */
|
||||||
|
selectScene: (sceneId: string) => void;
|
||||||
|
/** 设置当前场景的AI文本和标签 */
|
||||||
|
setCurrentSceneData: (text: TextItem, tags: TagItem[]) => void;
|
||||||
|
/** 优化AI文本 */
|
||||||
|
optimizeSceneText: () => Promise<void>;
|
||||||
|
/** 修改AI文本 */
|
||||||
|
updateSceneText: (newContent: string) => Promise<void>;
|
||||||
|
/** 修改标签内容 */
|
||||||
|
updateTagContent: (tagId: string, newContent: string | number) => Promise<void>;
|
||||||
|
/** 重新生成场景 */
|
||||||
|
regenerateScene: () => Promise<void>;
|
||||||
|
/** 获取场景出现的分镜列表 */
|
||||||
|
fetchSceneShots: () => Promise<void>;
|
||||||
|
/** 切换全选与全不选 */
|
||||||
|
toggleSelectAllShots: () => void;
|
||||||
|
/** 选择/取消选择单个分镜 */
|
||||||
|
toggleShotSelection: (shotId: string) => void;
|
||||||
|
/** 应用场景到选中的分镜 */
|
||||||
|
applySceneToSelectedShots: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景服务Hook
|
||||||
|
* 提供场景相关的所有响应式功能和业务逻辑
|
||||||
|
*/
|
||||||
|
export const useSceneServiceHook = (): UseSceneService => {
|
||||||
|
// 响应式状态
|
||||||
|
const [sceneList, setSceneList] = useState<SceneItem[]>([]);
|
||||||
|
const [selectedScene, setSelectedScene] = useState<SceneItem | null>(null);
|
||||||
|
const [currentSceneText, setCurrentSceneText] = useState<TextItem | null>(null);
|
||||||
|
const [currentSceneTags, setCurrentSceneTags] = useState<TagItem[]>([]);
|
||||||
|
const [shotSelectionList, setShotSelectionList] = useState<ShotSelectionItem[]>([]);
|
||||||
|
|
||||||
|
// UseCase实例 - 在场景选择时初始化
|
||||||
|
const [sceneEditUseCase, setSceneEditUseCase] = useState<SceneEditUseCase | null>(null);
|
||||||
|
const [textEditUseCase, setTextEditUseCase] = useState<TextEditUseCase | null>(null);
|
||||||
|
const [tagEditUseCases, setTagEditUseCases] = useState<Map<string, TagEditUseCase>>(new Map());
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const sceneImageUrl = useMemo(() => {
|
||||||
|
return selectedScene?.entity.imageUrl || '';
|
||||||
|
}, [selectedScene]);
|
||||||
|
|
||||||
|
const isAllShotsSelected = useMemo(() => {
|
||||||
|
return shotSelectionList.length > 0 && shotSelectionList.every(shot => shot.selected);
|
||||||
|
}, [shotSelectionList]);
|
||||||
|
|
||||||
|
const selectedShotsCount = useMemo(() => {
|
||||||
|
return shotSelectionList.filter(shot => shot.selected).length;
|
||||||
|
}, [shotSelectionList]);
|
||||||
|
|
||||||
|
// 选择场景
|
||||||
|
const selectScene = useCallback((sceneId: string) => {
|
||||||
|
const scene = sceneList.find(s => s.entity.id === sceneId);
|
||||||
|
if (scene) {
|
||||||
|
setSelectedScene(scene);
|
||||||
|
setSceneEditUseCase(new SceneEditUseCase(scene));
|
||||||
|
setTextEditUseCase(null);
|
||||||
|
setTagEditUseCases(new Map());
|
||||||
|
setCurrentSceneText(null);
|
||||||
|
setCurrentSceneTags([]);
|
||||||
|
}
|
||||||
|
}, [sceneList]);
|
||||||
|
|
||||||
|
// 设置当前场景数据
|
||||||
|
const setCurrentSceneData = useCallback((text: TextItem, tags: TagItem[]) => {
|
||||||
|
setCurrentSceneText(text);
|
||||||
|
setCurrentSceneTags(tags);
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
setTextEditUseCase(new TextEditUseCase(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTagEditUseCases = new Map<string, TagEditUseCase>();
|
||||||
|
tags.forEach(tag => {
|
||||||
|
newTagEditUseCases.set(tag.entity.id, new TagEditUseCase(tag));
|
||||||
|
});
|
||||||
|
setTagEditUseCases(newTagEditUseCases);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 优化AI文本
|
||||||
|
const optimizeSceneText = useCallback(async () => {
|
||||||
|
if (!textEditUseCase) {
|
||||||
|
throw new Error('文本编辑UseCase未初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSceneText || !currentSceneText.entity.content) {
|
||||||
|
throw new Error('没有可优化的文本内容');
|
||||||
|
}
|
||||||
|
|
||||||
|
const optimizedContent = await textEditUseCase.getOptimizedContent();
|
||||||
|
const updatedTextItem = await textEditUseCase.updateText(optimizedContent);
|
||||||
|
setCurrentSceneText(updatedTextItem);
|
||||||
|
}, [textEditUseCase, currentSceneText]);
|
||||||
|
|
||||||
|
// 修改AI文本
|
||||||
|
const updateSceneText = useCallback(async (newContent: string) => {
|
||||||
|
if (!textEditUseCase) {
|
||||||
|
throw new Error('文本编辑UseCase未初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSceneText) {
|
||||||
|
throw new Error('没有可编辑的文本');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedTextItem = await textEditUseCase.updateText(newContent);
|
||||||
|
setCurrentSceneText(updatedTextItem);
|
||||||
|
}, [textEditUseCase, currentSceneText]);
|
||||||
|
|
||||||
|
// 修改标签内容
|
||||||
|
const updateTagContent = useCallback(async (tagId: string, newContent: string | number) => {
|
||||||
|
const tagEditUseCase = tagEditUseCases.get(tagId);
|
||||||
|
if (!tagEditUseCase) {
|
||||||
|
throw new Error(`标签编辑UseCase未初始化,标签ID: ${tagId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedTagItem = await tagEditUseCase.updateTag(newContent);
|
||||||
|
|
||||||
|
setCurrentSceneTags(prev =>
|
||||||
|
prev.map(tag =>
|
||||||
|
tag.entity.id === tagId
|
||||||
|
? updatedTagItem
|
||||||
|
: tag
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [tagEditUseCases]);
|
||||||
|
|
||||||
|
// 重新生成场景
|
||||||
|
const regenerateScene = useCallback(async () => {
|
||||||
|
if (!sceneEditUseCase) {
|
||||||
|
throw new Error('场景编辑UseCase未初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedScene || !currentSceneText || currentSceneTags.length === 0) {
|
||||||
|
throw new Error('缺少重新生成场景所需的数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSceneEntity = await sceneEditUseCase.AIgenerateScene(currentSceneText, currentSceneTags);
|
||||||
|
|
||||||
|
const newSceneItem = new SceneItem(newSceneEntity);
|
||||||
|
setSelectedScene(newSceneItem);
|
||||||
|
|
||||||
|
setSceneList(prev =>
|
||||||
|
prev.map(scene =>
|
||||||
|
scene.entity.id === newSceneEntity.id ? newSceneItem : scene
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [sceneEditUseCase, selectedScene, currentSceneText, currentSceneTags]);
|
||||||
|
|
||||||
|
// 获取场景分镜列表
|
||||||
|
const fetchSceneShots = useCallback(async () => {
|
||||||
|
if (!selectedScene) {
|
||||||
|
throw new Error('请先选择场景');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getSceneShots({
|
||||||
|
sceneId: selectedScene.entity.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.successful) {
|
||||||
|
const { shots, appliedShotIds } = response.data;
|
||||||
|
|
||||||
|
const shotSelectionItems: ShotSelectionItem[] = shots.map(shot => ({
|
||||||
|
id: shot.id,
|
||||||
|
name: shot.name,
|
||||||
|
selected: false,
|
||||||
|
applied: appliedShotIds.includes(shot.id),
|
||||||
|
shot
|
||||||
|
}));
|
||||||
|
|
||||||
|
setShotSelectionList(shotSelectionItems);
|
||||||
|
} else {
|
||||||
|
throw new Error(`获取场景分镜列表失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取场景分镜列表失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, [selectedScene]);
|
||||||
|
|
||||||
|
// 切换全选与全不选
|
||||||
|
const toggleSelectAllShots = useCallback(() => {
|
||||||
|
setShotSelectionList(prev => {
|
||||||
|
const isAllSelected = prev.length > 0 && prev.every(shot => shot.selected);
|
||||||
|
return prev.map(shot => ({ ...shot, selected: !isAllSelected }));
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 选择/取消选择单个分镜
|
||||||
|
const toggleShotSelection = useCallback((shotId: string) => {
|
||||||
|
setShotSelectionList(prev =>
|
||||||
|
prev.map(shot =>
|
||||||
|
shot.id === shotId
|
||||||
|
? { ...shot, selected: !shot.selected }
|
||||||
|
: shot
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 应用场景到选中的分镜
|
||||||
|
const applySceneToSelectedShots = useCallback(async () => {
|
||||||
|
if (!sceneEditUseCase) {
|
||||||
|
throw new Error('场景编辑UseCase未初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedScene) {
|
||||||
|
throw new Error('请先选择场景');
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedShotIds = shotSelectionList
|
||||||
|
.filter(shot => shot.selected)
|
||||||
|
.map(shot => shot.id);
|
||||||
|
|
||||||
|
if (selectedShotIds.length === 0) {
|
||||||
|
throw new Error('请先选择要应用的分镜');
|
||||||
|
}
|
||||||
|
|
||||||
|
await sceneEditUseCase.applyScene(selectedShotIds);
|
||||||
|
|
||||||
|
setShotSelectionList(prev =>
|
||||||
|
prev.map(shot =>
|
||||||
|
selectedShotIds.includes(shot.id)
|
||||||
|
? { ...shot, applied: true, selected: false }
|
||||||
|
: shot
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [sceneEditUseCase, selectedScene, shotSelectionList]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 响应式数据
|
||||||
|
sceneList,
|
||||||
|
selectedScene,
|
||||||
|
currentSceneText,
|
||||||
|
currentSceneTags,
|
||||||
|
sceneImageUrl,
|
||||||
|
shotSelectionList,
|
||||||
|
isAllShotsSelected,
|
||||||
|
selectedShotsCount,
|
||||||
|
|
||||||
|
// 操作方法
|
||||||
|
selectScene,
|
||||||
|
setCurrentSceneData,
|
||||||
|
optimizeSceneText,
|
||||||
|
updateSceneText,
|
||||||
|
updateTagContent,
|
||||||
|
regenerateScene,
|
||||||
|
fetchSceneShots,
|
||||||
|
toggleSelectAllShots,
|
||||||
|
toggleShotSelection,
|
||||||
|
applySceneToSelectedShots
|
||||||
|
};
|
||||||
|
};
|
||||||
99
app/service/domain/Entities.ts
Normal file
99
app/service/domain/Entities.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* 实体接口定义
|
||||||
|
* 所有实体都应该实现这些基础接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础实体接口
|
||||||
|
*/
|
||||||
|
export interface BaseEntity {
|
||||||
|
/** 唯一标识 */
|
||||||
|
readonly id: string;
|
||||||
|
/** 更新时间 */
|
||||||
|
readonly updatedAt: number;
|
||||||
|
|
||||||
|
/**loading进度 0-100 */
|
||||||
|
loadingProgress: number;
|
||||||
|
/** 禁止编辑 */
|
||||||
|
disableEdit: boolean;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI文本实体接口
|
||||||
|
*/
|
||||||
|
export interface AITextEntity extends BaseEntity {
|
||||||
|
/** 文本内容 */
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色实体接口
|
||||||
|
*/
|
||||||
|
export interface RoleEntity extends BaseEntity {
|
||||||
|
/** 角色名称 */
|
||||||
|
name: string;
|
||||||
|
/** 角色提示词Id */
|
||||||
|
generateTextId: string;
|
||||||
|
/**角色标签 */
|
||||||
|
tagIds: string[];
|
||||||
|
/** 角色图片URL */
|
||||||
|
imageUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签实体接口
|
||||||
|
*/
|
||||||
|
export interface TagEntity extends BaseEntity {
|
||||||
|
/** 标签名称 */
|
||||||
|
name: string;
|
||||||
|
/** 内容标签类型 */
|
||||||
|
content: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景实体接口
|
||||||
|
*/
|
||||||
|
export interface SceneEntity extends BaseEntity {
|
||||||
|
/** 场景名称 */
|
||||||
|
name: string;
|
||||||
|
/** 场景图片URL */
|
||||||
|
imageUrl: string;
|
||||||
|
/** 场景标签 */
|
||||||
|
tagIds: string[];
|
||||||
|
/** 场景提示词Id */
|
||||||
|
generateTextId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
interface RoleMap {
|
||||||
|
/** 角色ID */
|
||||||
|
roleId: string;
|
||||||
|
/** 人物ID */
|
||||||
|
figureId: string;
|
||||||
|
}
|
||||||
|
interface ContentItem {
|
||||||
|
/** 角色ID */
|
||||||
|
roleId: string;
|
||||||
|
/** 对话内容 */
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 分镜实体接口
|
||||||
|
*/
|
||||||
|
export interface ShotEntity extends BaseEntity {
|
||||||
|
/** 分镜名称 */
|
||||||
|
name: string;
|
||||||
|
/**分镜草图Url */
|
||||||
|
sketchUrl: string;
|
||||||
|
/**分镜视频Url */
|
||||||
|
videoUrl: string;
|
||||||
|
/**人物ID */
|
||||||
|
roleMap: RoleMap[];
|
||||||
|
/**对话内容 */
|
||||||
|
content: ContentItem[];
|
||||||
|
/**镜头项 */
|
||||||
|
shot: string[];
|
||||||
|
}
|
||||||
127
app/service/domain/Item.ts
Normal file
127
app/service/domain/Item.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
AITextEntity,
|
||||||
|
RoleEntity,
|
||||||
|
TagEntity,
|
||||||
|
SceneEntity,
|
||||||
|
ShotEntity
|
||||||
|
} from './Entities';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可编辑项类型枚举
|
||||||
|
*/
|
||||||
|
export enum ItemType {
|
||||||
|
/** 文本 */
|
||||||
|
TEXT,
|
||||||
|
/** 图片 */
|
||||||
|
IMAGE,
|
||||||
|
/** 视频 */
|
||||||
|
VIDEO,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可编辑项抽象基类
|
||||||
|
*/
|
||||||
|
export abstract class EditItem<T extends BaseEntity> {
|
||||||
|
/** 被包装的实体 */
|
||||||
|
entity!: T;
|
||||||
|
/** 编辑元数据 */
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
/**禁用编辑 */
|
||||||
|
disableEdit!: boolean;
|
||||||
|
/** 类型 */
|
||||||
|
abstract type: ItemType;
|
||||||
|
constructor(
|
||||||
|
entity: T,
|
||||||
|
metadata: Record<string, any> = {}
|
||||||
|
) {
|
||||||
|
this.metadata = metadata;
|
||||||
|
|
||||||
|
this.setEntity(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置元数据
|
||||||
|
*/
|
||||||
|
setMetadata(metadata: Record<string, any>): void {
|
||||||
|
this.metadata = metadata;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 更新实体
|
||||||
|
*/
|
||||||
|
setEntity(entity: T): void {
|
||||||
|
this.entity = entity;
|
||||||
|
this.disableEdit = entity.disableEdit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI文本可编辑项
|
||||||
|
*/
|
||||||
|
export class TextItem extends EditItem<AITextEntity> {
|
||||||
|
type: ItemType.TEXT = ItemType.TEXT;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
entity: AITextEntity,
|
||||||
|
metadata: Record<string, any> = {}
|
||||||
|
) {
|
||||||
|
super(entity, metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色可编辑项
|
||||||
|
*/
|
||||||
|
export class RoleItem extends EditItem<RoleEntity> {
|
||||||
|
type: ItemType.IMAGE = ItemType.IMAGE;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
entity: RoleEntity,
|
||||||
|
metadata: Record<string, any> = {}
|
||||||
|
) {
|
||||||
|
super(entity, metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签可编辑项
|
||||||
|
*/
|
||||||
|
export class TagItem extends EditItem<TagEntity> {
|
||||||
|
type: ItemType.TEXT = ItemType.TEXT;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
entity: TagEntity,
|
||||||
|
metadata: Record<string, any> = {}
|
||||||
|
) {
|
||||||
|
super(entity, metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景可编辑项
|
||||||
|
*/
|
||||||
|
export class SceneItem extends EditItem<SceneEntity> {
|
||||||
|
type: ItemType.IMAGE = ItemType.IMAGE;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
entity: SceneEntity,
|
||||||
|
metadata: Record<string, any> = {}
|
||||||
|
) {
|
||||||
|
super(entity, metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分镜可编辑项
|
||||||
|
*/
|
||||||
|
export class ShotItem extends EditItem<ShotEntity> {
|
||||||
|
type: ItemType.IMAGE = ItemType.IMAGE;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
entity: ShotEntity,
|
||||||
|
metadata: Record<string, any> = {}
|
||||||
|
) {
|
||||||
|
super(entity, metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
app/service/usecase/RoleEditUseCase.ts
Normal file
52
app/service/usecase/RoleEditUseCase.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { RoleEntity } from '../domain/Entities';
|
||||||
|
import { RoleItem, TagItem, TextItem } from '../domain/Item';
|
||||||
|
import { regenerateRole, applyRoleToShots } from '@/api/video_flow';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色图编辑用例
|
||||||
|
* 负责角色图内容的初始化、修改和优化
|
||||||
|
*/
|
||||||
|
export class RoleEditUseCase {
|
||||||
|
constructor(private roleItem: RoleItem) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 重新生成角色
|
||||||
|
* @param {TextItem} prompt
|
||||||
|
* @param {TagItem[]} tags
|
||||||
|
* @return {*}
|
||||||
|
*/
|
||||||
|
async AIgenerateRole(prompt: TextItem, tags: TagItem[]): Promise<RoleEntity> {
|
||||||
|
const promptText = prompt.entity.content;
|
||||||
|
const tagList = tags.map((tag) => tag.entity.content);
|
||||||
|
|
||||||
|
// 调用重新生成角色接口
|
||||||
|
const response = await regenerateRole({
|
||||||
|
roleId: this.roleItem.entity.id||'',
|
||||||
|
prompt: promptText,
|
||||||
|
tagTypes: tagList,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.successful) {
|
||||||
|
const roleEntity = response.data;
|
||||||
|
this.roleItem.setEntity(roleEntity);
|
||||||
|
return roleEntity;
|
||||||
|
} else {
|
||||||
|
throw new Error(`重新生成角色失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用此角色到指定分镜
|
||||||
|
* @param shotIds 分镜ID列表
|
||||||
|
* @returns 应用结果
|
||||||
|
*/
|
||||||
|
async applyRole(shotIds: string[]) {
|
||||||
|
const roleId = this.roleItem.entity.id;
|
||||||
|
return await applyRoleToShots({
|
||||||
|
roleId,
|
||||||
|
shotIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
52
app/service/usecase/SceneEditUseCase.ts
Normal file
52
app/service/usecase/SceneEditUseCase.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { SceneEntity } from '../domain/Entities';
|
||||||
|
import { SceneItem, TagItem, TextItem } from '../domain/Item';
|
||||||
|
import { regenerateScene, applySceneToShots } from '@/api/video_flow';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景编辑用例
|
||||||
|
* 负责场景内容的初始化、修改和优化
|
||||||
|
*/
|
||||||
|
export class SceneEditUseCase {
|
||||||
|
constructor(private sceneItem: SceneItem) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 重新生成场景
|
||||||
|
* @param {TextItem} prompt
|
||||||
|
* @param {TagItem[]} tags
|
||||||
|
* @return {*}
|
||||||
|
*/
|
||||||
|
async AIgenerateScene(prompt: TextItem, tags: TagItem[]): Promise<SceneEntity> {
|
||||||
|
const promptText = prompt.entity.content;
|
||||||
|
const tagList = tags.map((tag) => tag.entity.content);
|
||||||
|
|
||||||
|
// 调用重新生成场景接口
|
||||||
|
const response = await regenerateScene({
|
||||||
|
sceneId: this.sceneItem.entity.id || '',
|
||||||
|
prompt: promptText,
|
||||||
|
tagTypes: tagList,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.successful) {
|
||||||
|
const sceneEntity = response.data;
|
||||||
|
this.sceneItem.setEntity(sceneEntity);
|
||||||
|
return sceneEntity;
|
||||||
|
} else {
|
||||||
|
throw new Error(`重新生成场景失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用此场景到指定分镜
|
||||||
|
* @param shotIds 分镜ID列表
|
||||||
|
* @returns 应用结果
|
||||||
|
*/
|
||||||
|
async applyScene(shotIds: string[]) {
|
||||||
|
const sceneId = this.sceneItem.entity.id;
|
||||||
|
|
||||||
|
return await applySceneToShots({
|
||||||
|
sceneId,
|
||||||
|
shotIds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/service/usecase/TagEditUseCase.ts
Normal file
40
app/service/usecase/TagEditUseCase.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { TagItem } from '../domain/Item';
|
||||||
|
import { updateTag } from '@/api/video_flow';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签编辑用例
|
||||||
|
* 负责标签内容的初始化、修改和优化
|
||||||
|
*/
|
||||||
|
export class TagEditUseCase {
|
||||||
|
constructor(private readonly tagItem: TagItem) {
|
||||||
|
this.tagItem = tagItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改标签内容
|
||||||
|
* @param newContent 新内容
|
||||||
|
* @returns 更新后的标签项
|
||||||
|
*/
|
||||||
|
async updateTag(newContent: string|number): Promise<TagItem> {
|
||||||
|
if (!this.tagItem) {
|
||||||
|
throw new Error('标签项未初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.tagItem.entity.disableEdit) {
|
||||||
|
throw new Error('标签项已禁用编辑');
|
||||||
|
}
|
||||||
|
// 请求更新接口
|
||||||
|
const response = await updateTag({
|
||||||
|
tagId: this.tagItem.entity.id,
|
||||||
|
content: newContent
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.successful) {
|
||||||
|
this.tagItem.setEntity(response.data);
|
||||||
|
return this.tagItem;
|
||||||
|
} else {
|
||||||
|
throw new Error(`修改标签失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
54
app/service/usecase/TextEditUseCase.ts
Normal file
54
app/service/usecase/TextEditUseCase.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { TextItem } from '../domain/Item';
|
||||||
|
import { updateText } from '@/api/video_flow';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本编辑用例
|
||||||
|
* 负责单个文本内容的初始化、修改和优化
|
||||||
|
*/
|
||||||
|
export class TextEditUseCase {
|
||||||
|
constructor(private readonly textItem: TextItem) {
|
||||||
|
this.textItem = textItem;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 修改文本内容
|
||||||
|
* @param newContent 新内容
|
||||||
|
* @returns 更新后的文本项
|
||||||
|
*/
|
||||||
|
async updateText(newContent: string): Promise<TextItem> {
|
||||||
|
if (!this.textItem) {
|
||||||
|
throw new Error('文本项未初始化,请先调用 initializeText');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.textItem.entity.disableEdit) {
|
||||||
|
throw new Error('文本项已禁用编辑');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await updateText({
|
||||||
|
textId: this.textItem.entity.id,
|
||||||
|
content: newContent
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.successful) {
|
||||||
|
this.textItem.setEntity(response.data);
|
||||||
|
return this.textItem;
|
||||||
|
} else {
|
||||||
|
throw new Error(`修改文案失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取优化后的文案内容
|
||||||
|
* @param optimizationOptions 优化选项
|
||||||
|
* @returns 优化后的内容
|
||||||
|
*/
|
||||||
|
async getOptimizedContent(
|
||||||
|
): Promise<string> {
|
||||||
|
if (!this.textItem) {
|
||||||
|
throw new Error('没有内容可优化');
|
||||||
|
}
|
||||||
|
return this.textItem.entity.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
4014
package-lock.json
generated
4014
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user