forked from 77media/video-flow
更新人脸识别接口的返回类型为 RoleRecognitionResponse,移除未使用的 SceneService 代码,优化 ShotService 中的 filterRole 方法,调整 ShotEditUsecase 中的日志输出,增加角色识别相关的 TypeScript 类型定义。
This commit is contained in:
parent
99ea9a2c0a
commit
5f6a25f9de
@ -17,6 +17,7 @@ import {
|
|||||||
import { task_item, VideoSegmentEntityAdapter } from "@/app/service/adapter/oldErrAdapter";
|
import { task_item, VideoSegmentEntityAdapter } from "@/app/service/adapter/oldErrAdapter";
|
||||||
import { VideoFlowProjectResponse, NewCharacterItem, NewCharacterListResponse, CharacterListByProjectWithHighlightResponse, CharacterUpdateAndRegenerateRequest, CharacterUpdateAndRegenerateResponse } from "./allMovieType";
|
import { VideoFlowProjectResponse, NewCharacterItem, NewCharacterListResponse, CharacterListByProjectWithHighlightResponse, CharacterUpdateAndRegenerateRequest, CharacterUpdateAndRegenerateResponse } from "./allMovieType";
|
||||||
import { RoleResponse } from "@/app/service/usecase/RoleEditUseCase";
|
import { RoleResponse } from "@/app/service/usecase/RoleEditUseCase";
|
||||||
|
import { RoleRecognitionResponse } from "@/app/service/usecase/ShotEditUsecase";
|
||||||
|
|
||||||
// API 响应类型
|
// API 响应类型
|
||||||
interface BaseApiResponse<T> {
|
interface BaseApiResponse<T> {
|
||||||
@ -820,7 +821,7 @@ export const updateShotPrompt = async (request: {
|
|||||||
/**
|
/**
|
||||||
* 人脸识别接口
|
* 人脸识别接口
|
||||||
* @param request - 人脸识别请求参数
|
* @param request - 人脸识别请求参数
|
||||||
* @returns Promise<ApiResponse<any>> 人脸识别结果
|
* @returns Promise<ApiResponse<RoleRecognitionResponse>> 人脸识别结果
|
||||||
*/
|
*/
|
||||||
export const faceRecognition = async (request: {
|
export const faceRecognition = async (request: {
|
||||||
/** 项目ID */
|
/** 项目ID */
|
||||||
@ -829,8 +830,8 @@ export const faceRecognition = async (request: {
|
|||||||
video_id: string;
|
video_id: string;
|
||||||
/** 目标图片URL */
|
/** 目标图片URL */
|
||||||
target_image_url: string;
|
target_image_url: string;
|
||||||
}): Promise<ApiResponse<any>> => {
|
}): Promise<ApiResponse<RoleRecognitionResponse>> => {
|
||||||
return post<ApiResponse<any>>("/character/face_recognition", request);
|
return post("/character/face_recognition", request);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,427 +0,0 @@
|
|||||||
import { useState, useCallback, useMemo } from 'react';
|
|
||||||
import { SceneEntity, AITextEntity, VideoSegmentEntity } from '../domain/Entities';
|
|
||||||
import { SceneItem, TagItem, TextItem } from '../domain/Item';
|
|
||||||
import { SceneEditUseCase } from '../usecase/SceneEditUseCase';
|
|
||||||
import { TagEditUseCase } from '../usecase/TagEditUseCase';
|
|
||||||
import { getSceneShots, getSceneData, getSceneList } from '@/api/video_flow';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分镜选择项接口
|
|
||||||
*/
|
|
||||||
interface ShotSelectionItem {
|
|
||||||
/** 分镜ID */
|
|
||||||
id: string;
|
|
||||||
/** 分镜名称 */
|
|
||||||
name: string;
|
|
||||||
/** 是否已选中 */
|
|
||||||
selected: boolean;
|
|
||||||
/** 是否已应用场景 */
|
|
||||||
applied: boolean;
|
|
||||||
/** 分镜数据 */
|
|
||||||
shot: VideoSegmentEntity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 场景服务Hook返回值接口
|
|
||||||
*/
|
|
||||||
interface UseSceneService {
|
|
||||||
// 响应式数据
|
|
||||||
/** 场景列表 */
|
|
||||||
sceneList: SceneItem[];
|
|
||||||
/** 当前选中的场景 */
|
|
||||||
selectedScene: SceneItem | null;
|
|
||||||
// /** 当前场景的AI文本 */
|
|
||||||
// currentSceneText: TextItem | null; // TODO: TextEditUseCase not implemented yet
|
|
||||||
/** 当前场景的标签列表 */
|
|
||||||
currentSceneTags: TagItem[];
|
|
||||||
/** 场景图片URL */
|
|
||||||
sceneImageUrl: string;
|
|
||||||
/** 分镜选择列表 */
|
|
||||||
shotSelectionList: ShotSelectionItem[];
|
|
||||||
/** 是否全选分镜 */
|
|
||||||
isAllShotsSelected: boolean;
|
|
||||||
/** 已选中的分镜数量 */
|
|
||||||
selectedShotsCount: number;
|
|
||||||
|
|
||||||
// 操作方法
|
|
||||||
/** 获取场景列表 */
|
|
||||||
fetchSceneList: (projectId: string) => Promise<void>;
|
|
||||||
/** 选择场景 */
|
|
||||||
selectScene: (sceneId: string) => void;
|
|
||||||
/** 初始化当前选中场景的AI文本和标签数据 */
|
|
||||||
initializeSceneData: () => Promise<void>;
|
|
||||||
// /** 优化AI文本 */
|
|
||||||
// optimizeSceneText: () => Promise<void>; // TODO: TextEditUseCase not implemented yet
|
|
||||||
// /** 修改AI文本 */
|
|
||||||
// updateSceneText: (newContent: string) => Promise<void>; // TODO: TextEditUseCase not implemented yet
|
|
||||||
/** 修改标签内容 */
|
|
||||||
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); // TODO: TextEditUseCase not implemented yet
|
|
||||||
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); // TODO: TextEditUseCase not implemented yet
|
|
||||||
const [tagEditUseCases, setTagEditUseCases] = useState<Map<string, TagEditUseCase>>(new Map());
|
|
||||||
|
|
||||||
// 计算属性
|
|
||||||
/**
|
|
||||||
* 场景图片URL
|
|
||||||
* @description 获取当前选中场景的图片URL
|
|
||||||
*/
|
|
||||||
const sceneImageUrl = useMemo(() => {
|
|
||||||
return selectedScene?.entity.imageUrl || '';
|
|
||||||
}, [selectedScene]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否全选分镜
|
|
||||||
* @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获取所有场景列表
|
|
||||||
* @param projectId 项目ID
|
|
||||||
* @throws {Error} 当API调用失败时抛出错误
|
|
||||||
* @returns {Promise<void>} 获取完成后的Promise
|
|
||||||
*/
|
|
||||||
const fetchSceneList = useCallback(async (projectId: string) => {
|
|
||||||
try {
|
|
||||||
const response = await getSceneList({
|
|
||||||
projectId: projectId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.successful) {
|
|
||||||
const sceneItems = response.data.map(scene => new SceneItem(scene));
|
|
||||||
setSceneList(sceneItems);
|
|
||||||
} else {
|
|
||||||
throw new Error(`获取场景列表失败: ${response.message}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取场景列表失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 选择场景
|
|
||||||
* @description 根据场景ID选择场景,并初始化相关的UseCase实例
|
|
||||||
* @param sceneId 场景ID
|
|
||||||
*/
|
|
||||||
const selectScene = useCallback(async (sceneId: string) => {
|
|
||||||
const scene = sceneList.find(s => s.entity.id === sceneId);
|
|
||||||
if (scene) {
|
|
||||||
setSelectedScene(scene);
|
|
||||||
|
|
||||||
// 初始化场景编辑UseCase实例
|
|
||||||
setSceneEditUseCase(new SceneEditUseCase(scene));
|
|
||||||
|
|
||||||
// 清空文本和标签相关状态
|
|
||||||
setTextEditUseCase(null);
|
|
||||||
setTagEditUseCases(new Map());
|
|
||||||
setCurrentSceneText(null);
|
|
||||||
setCurrentSceneTags([]);
|
|
||||||
await initializeSceneData();
|
|
||||||
}
|
|
||||||
}, [sceneList]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化场景数据
|
|
||||||
* @description 初始化当前选中场景的AI文本和标签数据
|
|
||||||
* @throws {Error} 当未选择场景或API调用失败时抛出错误
|
|
||||||
* @returns {Promise<void>} 初始化完成后的Promise
|
|
||||||
*/
|
|
||||||
const initializeSceneData = useCallback(async () => {
|
|
||||||
if (!selectedScene) {
|
|
||||||
throw new Error('请先选择场景');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await getSceneData({
|
|
||||||
sceneId: selectedScene.entity.id
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.successful) {
|
|
||||||
const { text, tags } = response.data;
|
|
||||||
const textItem = new TextItem(text);
|
|
||||||
const tagItems = tags.map(tag => new TagItem(tag));
|
|
||||||
|
|
||||||
// 设置当前场景的AI文本和标签
|
|
||||||
setCurrentSceneText(textItem);
|
|
||||||
setCurrentSceneTags(tagItems);
|
|
||||||
|
|
||||||
// 初始化文本UseCase
|
|
||||||
setTextEditUseCase(new TextEditUseCase(textItem));
|
|
||||||
|
|
||||||
// 初始化标签UseCase
|
|
||||||
const newTagEditUseCases = new Map<string, TagEditUseCase>();
|
|
||||||
tagItems.forEach(tag => {
|
|
||||||
newTagEditUseCases.set(tag.entity.id, new TagEditUseCase(tag));
|
|
||||||
});
|
|
||||||
setTagEditUseCases(newTagEditUseCases);
|
|
||||||
} else {
|
|
||||||
throw new Error(`获取场景数据失败: ${response.message}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('初始化场景数据失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}, [selectedScene]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 优化AI文本
|
|
||||||
* @description 对当前场景的AI文本进行优化,无文本时不可进行优化
|
|
||||||
* @throws {Error} 当没有可优化的文本内容或UseCase未初始化时抛出错误
|
|
||||||
* @returns {Promise<void>} 优化完成后的Promise
|
|
||||||
*/
|
|
||||||
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文本
|
|
||||||
* @description 手动修改当前场景的AI文本内容
|
|
||||||
* @param newContent 新的文本内容
|
|
||||||
* @throws {Error} 当没有可编辑的文本或UseCase未初始化时抛出错误
|
|
||||||
* @returns {Promise<void>} 修改完成后的Promise
|
|
||||||
*/
|
|
||||||
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]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修改标签内容
|
|
||||||
* @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);
|
|
||||||
|
|
||||||
setCurrentSceneTags(prev =>
|
|
||||||
prev.map(tag =>
|
|
||||||
tag.entity.id === tagId
|
|
||||||
? updatedTagItem
|
|
||||||
: tag
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, [tagEditUseCases]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重新生成场景
|
|
||||||
* @description 使用AI文本和标签重新生成场景
|
|
||||||
* @throws {Error} 当缺少重新生成场景所需的数据或UseCase未初始化时抛出错误
|
|
||||||
* @returns {Promise<void>} 重新生成完成后的Promise
|
|
||||||
*/
|
|
||||||
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]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取场景出现的分镜列表
|
|
||||||
* @description 获取当前场景应用到的分镜列表,包括已应用状态
|
|
||||||
* @throws {Error} 当未选择场景或API调用失败时抛出错误
|
|
||||||
* @returns {Promise<void>} 获取完成后的Promise
|
|
||||||
*/
|
|
||||||
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]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 切换全选与全不选
|
|
||||||
* @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 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,
|
|
||||||
|
|
||||||
// 操作方法
|
|
||||||
/** 获取场景列表 */
|
|
||||||
fetchSceneList,
|
|
||||||
/** 选择场景 */
|
|
||||||
selectScene,
|
|
||||||
/** 初始化当前选中场景的AI文本和标签数据 */
|
|
||||||
initializeSceneData,
|
|
||||||
/** 优化AI文本 */
|
|
||||||
optimizeSceneText,
|
|
||||||
/** 修改AI文本 */
|
|
||||||
updateSceneText,
|
|
||||||
/** 修改标签内容 */
|
|
||||||
updateTagContent,
|
|
||||||
/** 重新生成场景 */
|
|
||||||
regenerateScene,
|
|
||||||
/** 获取场景出现的分镜列表 */
|
|
||||||
fetchSceneShots,
|
|
||||||
/** 切换全选与全不选 */
|
|
||||||
toggleSelectAllShots,
|
|
||||||
/** 选择/取消选择单个分镜 */
|
|
||||||
toggleShotSelection,
|
|
||||||
/** 应用场景到选中的分镜 */
|
|
||||||
applySceneToSelectedShots
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -40,7 +40,7 @@ export interface UseShotService {
|
|||||||
/** 删除指定镜头 */
|
/** 删除指定镜头 */
|
||||||
deleteLens: (lensName: string) => void;
|
deleteLens: (lensName: string) => void;
|
||||||
/** 获取视频当前帧并上传到七牛云 */
|
/** 获取视频当前帧并上传到七牛云 */
|
||||||
filterRole: (video: HTMLVideoElement, projectId: string, videoId: string) => Promise<string>;
|
filterRole: (video: HTMLVideoElement) => Promise<string>;
|
||||||
/** 设置角色简单数据 */
|
/** 设置角色简单数据 */
|
||||||
setSimpleCharacter: (characters: SimpleCharacter[]) => void;
|
setSimpleCharacter: (characters: SimpleCharacter[]) => void;
|
||||||
}
|
}
|
||||||
@ -75,8 +75,6 @@ export const useShotService = (): UseShotService => {
|
|||||||
|
|
||||||
const segments = await vidoEditUseCase.getVideoSegmentList(projectId);
|
const segments = await vidoEditUseCase.getVideoSegmentList(projectId);
|
||||||
setProjectId(projectId);
|
setProjectId(projectId);
|
||||||
console.log('segments', segments);
|
|
||||||
|
|
||||||
setVideoSegments(segments);
|
setVideoSegments(segments);
|
||||||
setIntervalIdHandler();
|
setIntervalIdHandler();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -97,6 +95,9 @@ export const useShotService = (): UseShotService => {
|
|||||||
// 定义定时任务,每5秒执行一次
|
// 定义定时任务,每5秒执行一次
|
||||||
const newIntervalId = setInterval(async () => {
|
const newIntervalId = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!projectId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const segments = await vidoEditUseCase.getVideoSegmentList(projectId);
|
const segments = await vidoEditUseCase.getVideoSegmentList(projectId);
|
||||||
|
|
||||||
setVideoSegments(prevSegments => {
|
setVideoSegments(prevSegments => {
|
||||||
@ -345,7 +346,7 @@ export const useShotService = (): UseShotService => {
|
|||||||
|
|
||||||
// 上传到七牛云
|
// 上传到七牛云
|
||||||
const imageUrl = await uploadToQiniu(file, token);
|
const imageUrl = await uploadToQiniu(file, token);
|
||||||
|
console.log('imageUrl', imageUrl);
|
||||||
// 调用用例中的识别角色方法
|
// 调用用例中的识别角色方法
|
||||||
if (vidoEditUseCase) {
|
if (vidoEditUseCase) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -70,34 +70,6 @@ export class TextItem extends EditItem<AITextEntity> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 角色可编辑项
|
|
||||||
*/
|
|
||||||
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<TagValueObject> {
|
|
||||||
type: ItemType.TEXT = ItemType.TEXT;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
entity: TagValueObject,
|
|
||||||
metadata: Record<string, any> = {}
|
|
||||||
) {
|
|
||||||
super(entity, metadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 场景可编辑项
|
* 场景可编辑项
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -56,16 +56,24 @@ export class VideoSegmentEditUseCase {
|
|||||||
* @returns VideoSegmentEntity[] 匹配后的视频片段列表
|
* @returns VideoSegmentEntity[] 匹配后的视频片段列表
|
||||||
* @throws Error 当有视频片段未匹配到video_id时
|
* @throws Error 当有视频片段未匹配到video_id时
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* @description 为视频片段匹配对应的video_id,并输出调试信息
|
||||||
|
* @param segments 视频片段列表
|
||||||
|
* @param detail 项目详情数据
|
||||||
|
* @returns VideoSegmentEntity[] 匹配后的视频片段列表
|
||||||
|
* @throws Error 当有视频片段未匹配到video_id时
|
||||||
|
*/
|
||||||
private matchVideoSegmentsWithIds(
|
private matchVideoSegmentsWithIds(
|
||||||
segments: VideoSegmentEntity[],
|
segments: VideoSegmentEntity[],
|
||||||
detail: VideoFlowProjectResponse
|
detail: VideoFlowProjectResponse
|
||||||
): VideoSegmentEntity[] {
|
): VideoSegmentEntity[] {
|
||||||
|
console.log('[matchVideoSegmentsWithIds] 输入segments:', segments);
|
||||||
|
console.log('[matchVideoSegmentsWithIds] 输入detail:', detail);
|
||||||
|
|
||||||
|
const videoData = detail?.data?.video?.data;
|
||||||
const projectData = detail.data as any;
|
|
||||||
const videoData = projectData?.data?.video?.data;
|
|
||||||
|
|
||||||
if (!videoData || !Array.isArray(videoData)) {
|
if (!videoData || !Array.isArray(videoData)) {
|
||||||
|
console.log('[matchVideoSegmentsWithIds] videoData无效,直接返回原segments');
|
||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,6 +86,7 @@ export class VideoSegmentEditUseCase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
console.log('[matchVideoSegmentsWithIds] urlToVideoMap keys:', Array.from(urlToVideoMap.keys()));
|
||||||
|
|
||||||
// 为每个视频片段匹配video_id并重新创建实体
|
// 为每个视频片段匹配video_id并重新创建实体
|
||||||
const updatedSegments: VideoSegmentEntity[] = [];
|
const updatedSegments: VideoSegmentEntity[] = [];
|
||||||
@ -95,12 +104,15 @@ export class VideoSegmentEditUseCase {
|
|||||||
id: videoItem.video_id
|
id: videoItem.video_id
|
||||||
};
|
};
|
||||||
updatedSegments.push(updatedSegment);
|
updatedSegments.push(updatedSegment);
|
||||||
|
console.log(`[matchVideoSegmentsWithIds] segment "${segment.name}" 匹配到 video_id:`, videoItem.video_id);
|
||||||
} else {
|
} else {
|
||||||
// 如果没有匹配到,保持原样
|
// 如果没有匹配到,保持原样
|
||||||
updatedSegments.push(segment);
|
updatedSegments.push(segment);
|
||||||
|
console.log(`[matchVideoSegmentsWithIds] segment "${segment.name}" 未匹配到video_id, 保持原样`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updatedSegments.push(segment);
|
updatedSegments.push(segment);
|
||||||
|
console.log(`[matchVideoSegmentsWithIds] segment "${segment.name}" videoUrl无效, 保持原样`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -110,10 +122,12 @@ export class VideoSegmentEditUseCase {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (unmatchedSegments.length > 0) {
|
if (unmatchedSegments.length > 0) {
|
||||||
console.warn('以下视频片段未匹配到video_id:', unmatchedSegments.map(s => ({ name: s.name, videoUrl: s.videoUrl })));
|
console.warn('[matchVideoSegmentsWithIds] 以下视频片段未匹配到video_id:', unmatchedSegments.map(s => ({ name: s.name, videoUrl: s.videoUrl })));
|
||||||
throw new Error(`有 ${unmatchedSegments.length} 个视频片段未匹配到对应的video_id`);
|
// throw new Error(`有 ${unmatchedSegments.length} 个视频片段未匹配到对应的video_id`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[matchVideoSegmentsWithIds] 匹配后segments:', updatedSegments);
|
||||||
|
|
||||||
return updatedSegments;
|
return updatedSegments;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,3 +346,131 @@ export class VideoSegmentEditUseCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 边界框坐标信息
|
||||||
|
* @description 描述检测到的人物在图片中的位置和尺寸
|
||||||
|
*/
|
||||||
|
export interface BoundingBox {
|
||||||
|
/** X坐标(像素) */
|
||||||
|
x: number;
|
||||||
|
/** Y坐标(像素) */
|
||||||
|
y: number;
|
||||||
|
/** 宽度(像素) */
|
||||||
|
width: number;
|
||||||
|
/** 高度(像素) */
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匹配的人物信息
|
||||||
|
* @description 从图片中识别出的与已知角色匹配的人物信息
|
||||||
|
*/
|
||||||
|
export interface MatchedPerson {
|
||||||
|
/** 人物ID */
|
||||||
|
person_id: string;
|
||||||
|
/** 边界框信息 */
|
||||||
|
bbox: BoundingBox;
|
||||||
|
/** 匹配置信度(0-1之间) */
|
||||||
|
confidence: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色识别结果数据
|
||||||
|
* @description AI识别图片中人物后的详细结果
|
||||||
|
*/
|
||||||
|
export interface RecognitionResultData {
|
||||||
|
/** 目标图片URL */
|
||||||
|
target_image_url: string;
|
||||||
|
/** 检测到的总人数 */
|
||||||
|
total_persons_detected: number;
|
||||||
|
/** 匹配的人物列表 */
|
||||||
|
matched_persons: MatchedPerson[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色识别响应结果
|
||||||
|
* @description API返回的角色识别完整响应
|
||||||
|
*/
|
||||||
|
export interface RecognitionResult {
|
||||||
|
/** 响应状态码 */
|
||||||
|
code: number;
|
||||||
|
/** 响应消息 */
|
||||||
|
message: string;
|
||||||
|
/** 识别结果数据 */
|
||||||
|
data: RecognitionResultData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用的角色信息
|
||||||
|
* @description 在识别过程中使用的已知角色信息
|
||||||
|
*/
|
||||||
|
export interface CharacterUsed {
|
||||||
|
/** 角色名称 */
|
||||||
|
character_name: string;
|
||||||
|
/** 角色C-ID */
|
||||||
|
c_id: string;
|
||||||
|
/** 角色图片路径 */
|
||||||
|
image_path: string;
|
||||||
|
/** 角色头像 */
|
||||||
|
avatar: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色识别API响应类型
|
||||||
|
* @description 完整的角色识别API响应结构
|
||||||
|
*/
|
||||||
|
export interface RoleRecognitionResponse {
|
||||||
|
/** 项目ID */
|
||||||
|
project_id: string;
|
||||||
|
/** 视频ID */
|
||||||
|
video_id: string;
|
||||||
|
/** 目标图片URL */
|
||||||
|
target_image_url: string;
|
||||||
|
/** 已知人物数量 */
|
||||||
|
known_persons_count: number;
|
||||||
|
/** 识别结果 */
|
||||||
|
recognition_result: RecognitionResult;
|
||||||
|
/** 使用的角色列表 */
|
||||||
|
characters_used: CharacterUsed[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleRecognitionResponse:RoleRecognitionResponse = {
|
||||||
|
"project_id": "d0df7120-e27b-4f84-875c-e532f1bd318c",
|
||||||
|
"video_id": "984f3347-c81c-4af8-9145-49ead82becde",
|
||||||
|
"target_image_url": "https://cdn.qikongjian.com/videos/1754970412744_kqxplx.png",
|
||||||
|
"known_persons_count": 1,
|
||||||
|
"recognition_result": {
|
||||||
|
"code": 200,
|
||||||
|
"message": "识别完成",
|
||||||
|
"data":
|
||||||
|
{
|
||||||
|
"target_image_url": "https://cdn.qikongjian.com/videos/1754970412744_kqxplx.png",
|
||||||
|
"total_persons_detected": 1,
|
||||||
|
"matched_persons": [
|
||||||
|
{
|
||||||
|
"person_id": "CH-01",
|
||||||
|
"bbox": {
|
||||||
|
"x": 269,
|
||||||
|
"y": 23,
|
||||||
|
"width": 585,
|
||||||
|
"height": 685
|
||||||
|
},
|
||||||
|
"confidence": 0.36905956268310547
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"characters_used": [
|
||||||
|
{
|
||||||
|
"character_name": "CH-01",
|
||||||
|
"c_id": "无C-ID",
|
||||||
|
"image_path": "无image_path",
|
||||||
|
"avatar": "https://cdn.huiying.video/template/Whisk_9afb196368.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
@ -54,10 +54,10 @@ type Props = {
|
|||||||
onPersonClick?: (person: PersonDetection) => void;
|
onPersonClick?: (person: PersonDetection) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PersonDetectionScene: React.FC<Props> = ({
|
export const PersonDetectionScene: React.FC<Props> = ({
|
||||||
backgroundImage,
|
backgroundImage,
|
||||||
videoSrc,
|
videoSrc,
|
||||||
detections,
|
detections,
|
||||||
triggerScan,
|
triggerScan,
|
||||||
onScanStart,
|
onScanStart,
|
||||||
onScanTimeout,
|
onScanTimeout,
|
||||||
@ -192,6 +192,7 @@ export const PersonDetectionScene: React.FC<Props> = ({
|
|||||||
className="absolute inset-0 w-full h-full object-cover z-0"
|
className="absolute inset-0 w-full h-full object-cover z-0"
|
||||||
autoPlay
|
autoPlay
|
||||||
muted={false}
|
muted={false}
|
||||||
|
crossOrigin="anonymous"
|
||||||
loop
|
loop
|
||||||
controls
|
controls
|
||||||
/>
|
/>
|
||||||
@ -245,7 +246,7 @@ export const PersonDetectionScene: React.FC<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0.8 }}
|
initial={{ scale: 0.8 }}
|
||||||
animate={{
|
animate={{
|
||||||
scale: [0.8, 1.1, 1],
|
scale: [0.8, 1.1, 1],
|
||||||
borderColor: ['rgb(239, 68, 68)', 'rgb(239, 68, 68)', 'rgb(239, 68, 68)']
|
borderColor: ['rgb(239, 68, 68)', 'rgb(239, 68, 68)', 'rgb(239, 68, 68)']
|
||||||
}}
|
}}
|
||||||
@ -375,7 +376,7 @@ export const PersonDetectionScene: React.FC<Props> = ({
|
|||||||
{/* 人物识别框和浮签 */}
|
{/* 人物识别框和浮签 */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{detections.map((person, index) => {
|
{detections.map((person, index) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={person.id} className="cursor-pointer" onClick={() => {
|
<div key={person.id} className="cursor-pointer" onClick={() => {
|
||||||
onPersonClick?.(person);
|
onPersonClick?.(person);
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export function ShotTabContent({
|
|||||||
if (shotData.length > 0) {
|
if (shotData.length > 0) {
|
||||||
setSelectedSegment(shotData[selectedIndex]);
|
setSelectedSegment(shotData[selectedIndex]);
|
||||||
}
|
}
|
||||||
}, [selectedIndex]);
|
}, [selectedIndex, shotData]);
|
||||||
|
|
||||||
// 处理扫描开始
|
// 处理扫描开始
|
||||||
const handleScan = () => {
|
const handleScan = () => {
|
||||||
@ -101,7 +101,7 @@ export function ShotTabContent({
|
|||||||
|
|
||||||
// 确认替换角色
|
// 确认替换角色
|
||||||
const handleConfirmReplace = (selectedShots: string[], addToLibrary: boolean) => {
|
const handleConfirmReplace = (selectedShots: string[], addToLibrary: boolean) => {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 点击按钮重新生成
|
// 点击按钮重新生成
|
||||||
@ -252,7 +252,7 @@ export function ShotTabContent({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</HorizontalScroller>
|
</HorizontalScroller>
|
||||||
|
|
||||||
{/* 渐变遮罩 */}
|
{/* 渐变遮罩 */}
|
||||||
<div className="absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-[#1a1b1e] to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
|
<div className="absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-[#1a1b1e] to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
<div className="absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-[#1a1b1e] to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
|
<div className="absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-[#1a1b1e] to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
@ -261,7 +261,7 @@ export function ShotTabContent({
|
|||||||
|
|
||||||
|
|
||||||
{/* 下部分 */}
|
{/* 下部分 */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="grid grid-cols-2 gap-4 w-full"
|
className="grid grid-cols-2 gap-4 w-full"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
@ -296,8 +296,8 @@ export function ShotTabContent({
|
|||||||
<motion.button
|
<motion.button
|
||||||
onClick={() => handleScan()}
|
onClick={() => handleScan()}
|
||||||
className={`p-2 backdrop-blur-sm transition-colors z-10 rounded-full
|
className={`p-2 backdrop-blur-sm transition-colors z-10 rounded-full
|
||||||
${scanState === 'detected'
|
${scanState === 'detected'
|
||||||
? 'bg-cyan-500/50 hover:bg-cyan-500/70 text-white'
|
? 'bg-cyan-500/50 hover:bg-cyan-500/70 text-white'
|
||||||
: 'bg-black/50 hover:bg-black/70 text-white'
|
: 'bg-black/50 hover:bg-black/70 text-white'
|
||||||
}`}
|
}`}
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
@ -336,7 +336,7 @@ export function ShotTabContent({
|
|||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={() => handleAddShot()}
|
onClick={() => handleAddShot()}
|
||||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-pink-500/10 hover:bg-pink-500/20
|
className="flex items-center justify-center gap-2 px-4 py-3 bg-pink-500/10 hover:bg-pink-500/20
|
||||||
text-pink-500 rounded-lg transition-colors"
|
text-pink-500 rounded-lg transition-colors"
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
@ -346,7 +346,7 @@ export function ShotTabContent({
|
|||||||
</motion.button>
|
</motion.button>
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={() => handleRegenerate()}
|
onClick={() => handleRegenerate()}
|
||||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
|
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
|
||||||
text-blue-500 rounded-lg transition-colors"
|
text-blue-500 rounded-lg transition-colors"
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
@ -357,7 +357,7 @@ export function ShotTabContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<FloatingGlassPanel
|
<FloatingGlassPanel
|
||||||
@ -381,4 +381,4 @@ export function ShotTabContent({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user