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 { VideoFlowProjectResponse, NewCharacterItem, NewCharacterListResponse, CharacterListByProjectWithHighlightResponse, CharacterUpdateAndRegenerateRequest, CharacterUpdateAndRegenerateResponse } from "./allMovieType";
|
||||
import { RoleResponse } from "@/app/service/usecase/RoleEditUseCase";
|
||||
import { RoleRecognitionResponse } from "@/app/service/usecase/ShotEditUsecase";
|
||||
|
||||
// API 响应类型
|
||||
interface BaseApiResponse<T> {
|
||||
@ -820,7 +821,7 @@ export const updateShotPrompt = async (request: {
|
||||
/**
|
||||
* 人脸识别接口
|
||||
* @param request - 人脸识别请求参数
|
||||
* @returns Promise<ApiResponse<any>> 人脸识别结果
|
||||
* @returns Promise<ApiResponse<RoleRecognitionResponse>> 人脸识别结果
|
||||
*/
|
||||
export const faceRecognition = async (request: {
|
||||
/** 项目ID */
|
||||
@ -829,8 +830,8 @@ export const faceRecognition = async (request: {
|
||||
video_id: string;
|
||||
/** 目标图片URL */
|
||||
target_image_url: string;
|
||||
}): Promise<ApiResponse<any>> => {
|
||||
return post<ApiResponse<any>>("/character/face_recognition", request);
|
||||
}): Promise<ApiResponse<RoleRecognitionResponse>> => {
|
||||
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;
|
||||
/** 获取视频当前帧并上传到七牛云 */
|
||||
filterRole: (video: HTMLVideoElement, projectId: string, videoId: string) => Promise<string>;
|
||||
filterRole: (video: HTMLVideoElement) => Promise<string>;
|
||||
/** 设置角色简单数据 */
|
||||
setSimpleCharacter: (characters: SimpleCharacter[]) => void;
|
||||
}
|
||||
@ -75,8 +75,6 @@ export const useShotService = (): UseShotService => {
|
||||
|
||||
const segments = await vidoEditUseCase.getVideoSegmentList(projectId);
|
||||
setProjectId(projectId);
|
||||
console.log('segments', segments);
|
||||
|
||||
setVideoSegments(segments);
|
||||
setIntervalIdHandler();
|
||||
} catch (error) {
|
||||
@ -97,6 +95,9 @@ export const useShotService = (): UseShotService => {
|
||||
// 定义定时任务,每5秒执行一次
|
||||
const newIntervalId = setInterval(async () => {
|
||||
try {
|
||||
if (!projectId) {
|
||||
return;
|
||||
}
|
||||
const segments = await vidoEditUseCase.getVideoSegmentList(projectId);
|
||||
|
||||
setVideoSegments(prevSegments => {
|
||||
@ -345,7 +346,7 @@ export const useShotService = (): UseShotService => {
|
||||
|
||||
// 上传到七牛云
|
||||
const imageUrl = await uploadToQiniu(file, token);
|
||||
|
||||
console.log('imageUrl', imageUrl);
|
||||
// 调用用例中的识别角色方法
|
||||
if (vidoEditUseCase) {
|
||||
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[] 匹配后的视频片段列表
|
||||
* @throws Error 当有视频片段未匹配到video_id时
|
||||
*/
|
||||
/**
|
||||
* @description 为视频片段匹配对应的video_id,并输出调试信息
|
||||
* @param segments 视频片段列表
|
||||
* @param detail 项目详情数据
|
||||
* @returns VideoSegmentEntity[] 匹配后的视频片段列表
|
||||
* @throws Error 当有视频片段未匹配到video_id时
|
||||
*/
|
||||
private matchVideoSegmentsWithIds(
|
||||
segments: VideoSegmentEntity[],
|
||||
detail: VideoFlowProjectResponse
|
||||
): VideoSegmentEntity[] {
|
||||
console.log('[matchVideoSegmentsWithIds] 输入segments:', segments);
|
||||
console.log('[matchVideoSegmentsWithIds] 输入detail:', detail);
|
||||
|
||||
|
||||
const projectData = detail.data as any;
|
||||
const videoData = projectData?.data?.video?.data;
|
||||
const videoData = detail?.data?.video?.data;
|
||||
|
||||
if (!videoData || !Array.isArray(videoData)) {
|
||||
console.log('[matchVideoSegmentsWithIds] videoData无效,直接返回原segments');
|
||||
return segments;
|
||||
}
|
||||
|
||||
@ -78,6 +86,7 @@ export class VideoSegmentEditUseCase {
|
||||
});
|
||||
}
|
||||
});
|
||||
console.log('[matchVideoSegmentsWithIds] urlToVideoMap keys:', Array.from(urlToVideoMap.keys()));
|
||||
|
||||
// 为每个视频片段匹配video_id并重新创建实体
|
||||
const updatedSegments: VideoSegmentEntity[] = [];
|
||||
@ -95,12 +104,15 @@ export class VideoSegmentEditUseCase {
|
||||
id: videoItem.video_id
|
||||
};
|
||||
updatedSegments.push(updatedSegment);
|
||||
console.log(`[matchVideoSegmentsWithIds] segment "${segment.name}" 匹配到 video_id:`, videoItem.video_id);
|
||||
} else {
|
||||
// 如果没有匹配到,保持原样
|
||||
updatedSegments.push(segment);
|
||||
console.log(`[matchVideoSegmentsWithIds] segment "${segment.name}" 未匹配到video_id, 保持原样`);
|
||||
}
|
||||
} else {
|
||||
updatedSegments.push(segment);
|
||||
console.log(`[matchVideoSegmentsWithIds] segment "${segment.name}" videoUrl无效, 保持原样`);
|
||||
}
|
||||
});
|
||||
|
||||
@ -110,10 +122,12 @@ export class VideoSegmentEditUseCase {
|
||||
);
|
||||
|
||||
if (unmatchedSegments.length > 0) {
|
||||
console.warn('以下视频片段未匹配到video_id:', unmatchedSegments.map(s => ({ name: s.name, videoUrl: s.videoUrl })));
|
||||
throw new Error(`有 ${unmatchedSegments.length} 个视频片段未匹配到对应的video_id`);
|
||||
console.warn('[matchVideoSegmentsWithIds] 以下视频片段未匹配到video_id:', unmatchedSegments.map(s => ({ name: s.name, videoUrl: s.videoUrl })));
|
||||
// throw new Error(`有 ${unmatchedSegments.length} 个视频片段未匹配到对应的video_id`);
|
||||
}
|
||||
|
||||
console.log('[matchVideoSegmentsWithIds] 匹配后segments:', 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;
|
||||
};
|
||||
|
||||
export const PersonDetectionScene: React.FC<Props> = ({
|
||||
backgroundImage,
|
||||
videoSrc,
|
||||
detections,
|
||||
export const PersonDetectionScene: React.FC<Props> = ({
|
||||
backgroundImage,
|
||||
videoSrc,
|
||||
detections,
|
||||
triggerScan,
|
||||
onScanStart,
|
||||
onScanTimeout,
|
||||
@ -192,6 +192,7 @@ export const PersonDetectionScene: React.FC<Props> = ({
|
||||
className="absolute inset-0 w-full h-full object-cover z-0"
|
||||
autoPlay
|
||||
muted={false}
|
||||
crossOrigin="anonymous"
|
||||
loop
|
||||
controls
|
||||
/>
|
||||
@ -245,7 +246,7 @@ export const PersonDetectionScene: React.FC<Props> = ({
|
||||
) : (
|
||||
<motion.div
|
||||
initial={{ scale: 0.8 }}
|
||||
animate={{
|
||||
animate={{
|
||||
scale: [0.8, 1.1, 1],
|
||||
borderColor: ['rgb(239, 68, 68)', 'rgb(239, 68, 68)', 'rgb(239, 68, 68)']
|
||||
}}
|
||||
@ -375,7 +376,7 @@ export const PersonDetectionScene: React.FC<Props> = ({
|
||||
{/* 人物识别框和浮签 */}
|
||||
<AnimatePresence>
|
||||
{detections.map((person, index) => {
|
||||
|
||||
|
||||
return (
|
||||
<div key={person.id} className="cursor-pointer" onClick={() => {
|
||||
onPersonClick?.(person);
|
||||
|
||||
@ -45,7 +45,7 @@ export function ShotTabContent({
|
||||
if (shotData.length > 0) {
|
||||
setSelectedSegment(shotData[selectedIndex]);
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
}, [selectedIndex, shotData]);
|
||||
|
||||
// 处理扫描开始
|
||||
const handleScan = () => {
|
||||
@ -101,7 +101,7 @@ export function ShotTabContent({
|
||||
|
||||
// 确认替换角色
|
||||
const handleConfirmReplace = (selectedShots: string[], addToLibrary: boolean) => {
|
||||
|
||||
|
||||
};
|
||||
|
||||
// 点击按钮重新生成
|
||||
@ -252,7 +252,7 @@ export function ShotTabContent({
|
||||
);
|
||||
})}
|
||||
</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 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"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
@ -296,8 +296,8 @@ export function ShotTabContent({
|
||||
<motion.button
|
||||
onClick={() => handleScan()}
|
||||
className={`p-2 backdrop-blur-sm transition-colors z-10 rounded-full
|
||||
${scanState === 'detected'
|
||||
? 'bg-cyan-500/50 hover:bg-cyan-500/70 text-white'
|
||||
${scanState === 'detected'
|
||||
? 'bg-cyan-500/50 hover:bg-cyan-500/70 text-white'
|
||||
: 'bg-black/50 hover:bg-black/70 text-white'
|
||||
}`}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
@ -336,7 +336,7 @@ export function ShotTabContent({
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<motion.button
|
||||
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"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
@ -346,7 +346,7 @@ export function ShotTabContent({
|
||||
</motion.button>
|
||||
<motion.button
|
||||
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"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
@ -357,7 +357,7 @@ export function ShotTabContent({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</motion.div>
|
||||
|
||||
<FloatingGlassPanel
|
||||
@ -381,4 +381,4 @@ export function ShotTabContent({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user