video-flow-b/app/service/Interaction/RoleShotService.ts
2025-08-18 20:20:31 +08:00

257 lines
8.2 KiB
TypeScript

import { useState, useCallback, useMemo } from 'react';
import { RoleEntity, VideoSegmentEntity } from '../domain/Entities';
import { RoleEditUseCase } from '../usecase/RoleEditUseCase';
import { getCharacterShots, applyRoleToShots, CharacterReplacement } from '@/api/video_flow';
import { SaveEditUseCase } from '../usecase/SaveEditUseCase';
/**
* 扩展的视频片段实体,包含选择状态
*/
interface ExtendedVideoSegmentEntity extends VideoSegmentEntity {
/** 是否已选中 */
selected?: boolean;
/** 是否已应用 */
applied?: boolean;
}
/**
* 角色视频片段服务Hook返回值接口
*/
interface UseRoleShotService {
// 响应式数据
/** 分镜选择列表 */
shotSelectionList: ExtendedVideoSegmentEntity[];
/** 是否全选分镜 */
isAllVideoSegmentSelected: boolean;
/** 已选中的分镜数量 */
selectedVideoSegmentCount: number;
/** 当前选中的角色ID */
selectedRoleId: string | null;
/** 是否正在替换中 */
isReplacing: boolean;
// 操作方法
/** 获取角色出现的分镜列表 */
fetchRoleShots: (roleId: string) => Promise<void>;
/** 切换全选与全不选 */
toggleSelectAllShots: () => void;
/** 选择/取消选择单个分镜 */
toggleShotSelection: (shotId: string) => void;
/** 应用角色到选中的分镜 */
applyRoleToSelectedShots: (newRole: RoleEntity) => Promise<void>;
/** 清空选择列表 */
clearShotSelection: () => void;
/** 设置选中的角色 */
setSelectedRole: (role: RoleEntity) => void;
/** 设置草稿箱角色列表 */
setDraftRoleList: (roleList: RoleEntity[]) => void;
}
/**
* 角色视频片段服务Hook
* 提供角色与视频片段交互的所有响应式功能和业务逻辑
*/
export const useRoleShotServiceHook = (projectId: string,selectRole?:RoleEntity,roleList?:RoleEntity[]): UseRoleShotService => {
// 响应式状态
const [shotSelectionList, setShotSelectionList] = useState<ExtendedVideoSegmentEntity[]>([]);
const [selectedRoleId, setSelectedRoleId] = useState<string | null>(null);
const [selectedRole, setSelectedRole] = useState<RoleEntity | null>(selectRole || null);
const [isReplacing, setIsReplacing] = useState<boolean>(false); // 全局替换状态
const [draftRoleList, setDraftRoleList] = useState<RoleEntity[]>(roleList || []);
// 计算属性
/**
* 是否全选分镜
* @description 判断是否所有分镜都被选中
*/
const isAllVideoSegmentSelected = useMemo(() => {
return shotSelectionList.length > 0 && shotSelectionList.every(shot => shot.selected);
}, [shotSelectionList]);
/**
* 已选中的分镜数量
* @description 获取当前选中的分镜数量
*/
const selectedVideoSegmentCount = useMemo(() => {
return shotSelectionList.filter(shot => shot.selected).length;
}, [shotSelectionList]);
/**
* 获取角色出现的分镜列表
* @description 获取当前角色应用到的分镜列表,包括已应用状态
* @param roleName 角色名称
* @throws {Error} 当API调用失败时抛出错误
* @returns {Promise<void>} 获取完成后的Promise
*/
const fetchRoleShots = useCallback(async (roleName: string) => {
try {
// 调用新的真实接口获取角色在项目中的场景列表
const response = await getCharacterShots({
project_id: projectId,
character_name: roleName
});
if (response.successful) {
const { scenes, total_count } = response.data;
// 将scenes数据转换为ExtendedVideoSegmentEntity[]数据结构
const extendedShots: ExtendedVideoSegmentEntity[] = scenes.map(scene => ({
id: scene.video_id,
name: `视频片段_${scene.video_id}`,
sketchUrl: "",
videoUrl: scene.video_urls,// 保持为string[]类型
status: scene.video_status !== null? scene.video_status : scene.video_urls.length>0?1:0, // 默认为已完成状态
lens: [],
selected: false,
applied: true // 由于是通过角色查询到的,所以都是已应用的
}));
setShotSelectionList(extendedShots);
setSelectedRoleId(roleName);
} else {
throw new Error(`获取角色场景列表失败: ${response.message}`);
}
} catch (error) {
console.error('获取角色场景列表失败:', error);
throw error;
}
}, [projectId]);
/**
* 切换全选与全不选
* @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 (newRole: RoleEntity) => {
try {
// 检查是否有选中的角色
if (!selectedRole||!selectedRole.generateText ) {
throw new Error('请先选择要应用的角色');
}
// 检查是否正在替换中
if (isReplacing) {
throw new Error('正在替换中,请等待完成后再试');
}
// 设置全局替换状态为true
setIsReplacing(true);
// 构建角色替换参数
const characterReplacements: CharacterReplacement[] = [
{
original_name: selectedRole.name,
original_description: selectedRole.generateText,
new_name: newRole.name,
new_description: newRole.generateText
}
];
const newDraftRoleList = draftRoleList.filter(role =>{
if(role.name===selectedRole.name){
role.fromDraft = true
}
return role.fromDraft
});
console.log('newDraftRoleList', newDraftRoleList)
// 循环调用接口,为每个选中的分镜单独调用
const res = await Promise.all( shotSelectionList.map(async (shot) => {
// 调用应用角色到分镜接口(不等待完成)
return applyRoleToShots({
project_id: projectId,
shot_id: shot.id, // 单个分镜ID
character_replacements: characterReplacements,
wait_for_completion: false, // 不等待完成,异步处理
character_draft: JSON.stringify(newDraftRoleList)
})
}))
SaveEditUseCase.setVideoTasks([
...SaveEditUseCase.videoTasks,
...res.map(item=>{
return {
task_id: item.data.task_id,
video_ids: [item.data.shot_id]
}
})
])
console.log([
...SaveEditUseCase.videoTasks,
...res.map(item=>{
return {
task_id: item.data.task_id,
video_ids: [item.data.shot_id]
}
})
]);
// 延迟重置替换状态,给用户一些反馈时间
setTimeout(() => {
setIsReplacing(false);
}, 2000);
} catch (error) {
console.error('应用角色到分镜失败:', error);
// 出错时也要重置替换状态
setIsReplacing(false);
throw error;
}
}, [selectedRole, isReplacing, draftRoleList, shotSelectionList, projectId]);
/**
* 清空选择列表
* @description 清空所有分镜的选择状态
*/
const clearShotSelection = useCallback(() => {
setShotSelectionList(prev =>
prev.map(shot => ({ ...shot, selected: false }))
);
}, []);
return {
// 响应式数据
shotSelectionList,
isAllVideoSegmentSelected,
selectedVideoSegmentCount,
selectedRoleId,
isReplacing,
// 操作方法
fetchRoleShots,
toggleSelectAllShots,
toggleShotSelection,
applyRoleToSelectedShots,
clearShotSelection,
setSelectedRole,
setDraftRoleList
};
};