forked from 77media/video-flow
477 lines
14 KiB
TypeScript
477 lines
14 KiB
TypeScript
import { VideoFlowProjectResponse } from "@/api/allMovieType";
|
||
import { task_item, VideoSegmentEntityAdapter } from "../adapter/oldErrAdapter";
|
||
import { VideoSegmentEntity } from "../domain/Entities";
|
||
import { LensType } from "../domain/valueObject";
|
||
import {
|
||
getShotList,
|
||
regenerateShot,
|
||
optimizeShotContent,
|
||
updateShotPrompt,
|
||
detailScriptEpisodeNew,
|
||
faceRecognition,
|
||
batchUpdateVideoSegments,
|
||
} from "@/api/video_flow";
|
||
|
||
/**
|
||
* 视频片段编辑用例
|
||
* 负责视频片段内容的初始化、修改和优化
|
||
*/
|
||
export class VideoSegmentEditUseCase {
|
||
private loading: boolean = false;
|
||
|
||
/**
|
||
* @description 获取视频片段列表
|
||
* @param projectId 项目ID
|
||
* @returns Promise<VideoSegmentEntity[]> 视频片段列表
|
||
*/
|
||
async getVideoSegmentList(projectId: string): Promise<VideoSegmentEntity[]> {
|
||
try {
|
||
this.loading = true;
|
||
|
||
const response = await getShotList({ project_id: projectId });
|
||
|
||
if (!response.successful) {
|
||
throw new Error(response.message || "获取视频片段列表失败");
|
||
}
|
||
|
||
const Segments = VideoSegmentEntityAdapter.toVideoSegmentEntity(response.data) || [];
|
||
const detail = await detailScriptEpisodeNew({ project_id: projectId });
|
||
if (!detail.successful || !detail.data) {
|
||
throw new Error(detail.message || "获取视频片段列表失败");
|
||
}
|
||
// 匹配视频片段ID
|
||
return this.matchVideoSegmentsWithIds(Segments, detail.data);
|
||
} catch (error) {
|
||
console.error("获取视频片段列表失败:", error);
|
||
throw error;
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @description 为视频片段匹配对应的video_id
|
||
* @param segments 视频片段列表
|
||
* @param detail 项目详情数据
|
||
* @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 videoData = detail?.data?.video?.data;
|
||
|
||
if (!videoData || !Array.isArray(videoData)) {
|
||
console.log('[matchVideoSegmentsWithIds] videoData无效,直接返回原segments');
|
||
return segments;
|
||
}
|
||
|
||
// 建立URL到VideoItem的映射表,提高查找效率
|
||
const urlToVideoMap = new Map<string, any>();
|
||
videoData.forEach(videoItem => {
|
||
if (videoItem.urls && Array.isArray(videoItem.urls)) {
|
||
videoItem.urls.forEach((url: string) => {
|
||
urlToVideoMap.set(url, videoItem);
|
||
});
|
||
}
|
||
});
|
||
console.log('[matchVideoSegmentsWithIds] urlToVideoMap keys:', Array.from(urlToVideoMap.keys()));
|
||
|
||
// 为每个视频片段匹配video_id并重新创建实体
|
||
const updatedSegments: VideoSegmentEntity[] = [];
|
||
|
||
segments.forEach(segment => {
|
||
if (segment.videoUrl && Array.isArray(segment.videoUrl)) {
|
||
// 查找匹配的视频项
|
||
const matchedVideo = segment.videoUrl.find(url => urlToVideoMap.has(url));
|
||
const videoItem = matchedVideo ? urlToVideoMap.get(matchedVideo) : null;
|
||
|
||
if (videoItem) {
|
||
// 创建新的实体实例,设置正确的id
|
||
const updatedSegment: VideoSegmentEntity = {
|
||
...segment,
|
||
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无效, 保持原样`);
|
||
}
|
||
});
|
||
|
||
// 检查是否所有视频片段都匹配上了id
|
||
const unmatchedSegments = updatedSegments.filter(segment =>
|
||
segment.id.startsWith('video_mock_') || !segment.id
|
||
);
|
||
|
||
if (unmatchedSegments.length > 0) {
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* @description 保存分镜提示词数据
|
||
* @param project_id 项目ID
|
||
* @param shot_id 分镜ID
|
||
* @param shot_descriptions 镜头描述数据
|
||
* @returns Promise<any> 保存结果
|
||
*/
|
||
async saveShotPrompt(
|
||
project_id: string,
|
||
shot_id: string,
|
||
shot_descriptions: task_item
|
||
): Promise<any> {
|
||
try {
|
||
this.loading = true;
|
||
|
||
const response = await updateShotPrompt({
|
||
project_id,
|
||
shot_id,
|
||
shot_descriptions,
|
||
});
|
||
|
||
if (!response.successful) {
|
||
throw new Error(response.message || "保存分镜提示词数据失败");
|
||
}
|
||
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error("保存分镜提示词数据失败:", error);
|
||
throw error;
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @description 通过视频镜头描述数据重新生成视频
|
||
* @param project_id 项目ID
|
||
* @param shot_Lens 镜头描述数据
|
||
* @param shot_id 视频片段ID(可选,如果重新生成现有片段)
|
||
* @returns Promise<VideoSegmentEntity> 重新生成的视频片段
|
||
*/
|
||
async regenerateVideoSegment(
|
||
project_id: string,
|
||
shot_Lens: LensType[],
|
||
shot_id?: string,
|
||
// roleReplaceParams?: { oldId: string; newId: string }[],
|
||
// sceneReplaceParams?: { oldId: string; newId: string }[]
|
||
): Promise<VideoSegmentEntity> {
|
||
try {
|
||
this.loading = true;
|
||
const shot_descriptions = VideoSegmentEntityAdapter.lensTypeToTaskItem(shot_Lens);
|
||
// 如果有shot_id,先保存分镜数据
|
||
if (shot_id) {
|
||
await this.saveShotPrompt(project_id, shot_id, shot_descriptions);
|
||
}
|
||
|
||
const response = await regenerateShot({
|
||
project_id,
|
||
shot_id,
|
||
shot_descriptions,
|
||
// roleReplaceParams,
|
||
// sceneReplaceParams,
|
||
});
|
||
|
||
if (!response.successful) {
|
||
throw new Error(response.message || "重新生成视频片段失败");
|
||
}
|
||
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error("重新生成视频片段失败:", error);
|
||
throw error;
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @description 通过AI优化镜头数据(包含对话内容)
|
||
* @param shotId 视频片段ID
|
||
* @param userRequirement 用户优化需求
|
||
* @param lensData 镜头数据数组
|
||
* @returns Promise<LensType[]> 优化后的镜头数据
|
||
*/
|
||
async optimizeVideoContent(
|
||
shotId: string,
|
||
userRequirement: string,
|
||
lensData: LensType[]
|
||
): Promise<LensType[]> {
|
||
try {
|
||
this.loading = true;
|
||
|
||
// 调用AI优化接口
|
||
const response = await optimizeShotContent({
|
||
shotId,
|
||
userRequirement,
|
||
lensData,
|
||
});
|
||
|
||
if (!response.successful) {
|
||
throw new Error(response.message || "AI优化视频内容失败");
|
||
}
|
||
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error("AI优化视频内容失败:", error);
|
||
throw error;
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @description 批量更新除当前选中片段外的所有视频片段
|
||
* @param projectId 项目ID
|
||
* @param currentSegmentId 当前选中的视频片段ID
|
||
* @param optimizedDescription 优化后的描述文本
|
||
* @param keywords 关键词列表
|
||
* @returns Promise<boolean> 更新是否成功
|
||
*/
|
||
async batchUpdateOtherVideoSegments(
|
||
projectId: string,
|
||
currentSegmentId: string,
|
||
optimizedDescription: string,
|
||
keywords: string[]
|
||
): Promise<boolean> {
|
||
try {
|
||
this.loading = true;
|
||
|
||
// 获取当前项目的视频片段列表
|
||
const segments = await this.getVideoSegmentList(projectId);
|
||
|
||
// 过滤出除当前选中片段外的所有片段
|
||
const otherSegments = segments.filter(segment => segment.id !== currentSegmentId);
|
||
|
||
if (otherSegments.length === 0) {
|
||
console.log('没有其他视频片段需要更新');
|
||
return true;
|
||
}
|
||
|
||
// 准备更新数据
|
||
const updateData = otherSegments.map(segment => ({
|
||
shot_id: segment.id,
|
||
video_urls: segment.videoUrl || [],
|
||
status: segment.status,
|
||
optimized_description: optimizedDescription,
|
||
keywords: keywords
|
||
}));
|
||
|
||
// 调用批量更新API
|
||
const response = await batchUpdateVideoSegments({
|
||
project_id: projectId,
|
||
segments: updateData
|
||
});
|
||
|
||
if (!response.successful) {
|
||
throw new Error(response.message || '批量更新视频片段失败');
|
||
}
|
||
|
||
console.log(`成功更新 ${response.data.success_count} 个视频片段,失败 ${response.data.failed_count} 个`);
|
||
|
||
return response.data.failed_count === 0;
|
||
} catch (error) {
|
||
console.error('批量更新视频片段失败:', error);
|
||
throw error;
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @description 获取加载状态
|
||
* @returns boolean 是否正在加载
|
||
*/
|
||
isLoading(): boolean {
|
||
return this.loading;
|
||
}
|
||
|
||
/**
|
||
* @description 识别视频帧中的角色
|
||
* @param projectId 项目ID
|
||
* @param videoId 视频ID
|
||
* @param targetImageUrl 目标图片URL
|
||
* @returns Promise<any> 识别结果
|
||
*/
|
||
async recognizeRoleFromImage(
|
||
projectId: string,
|
||
videoId: string,
|
||
targetImageUrl: string
|
||
): Promise<any> {
|
||
try {
|
||
this.loading = true;
|
||
|
||
const response = await faceRecognition({
|
||
project_id: projectId,
|
||
video_id: videoId,
|
||
target_image_url: targetImageUrl,
|
||
});
|
||
|
||
if (!response.successful) {
|
||
throw new Error(response.message || "人脸识别失败");
|
||
}
|
||
|
||
// 打印返回结果
|
||
console.log('人脸识别接口返回结果:', response);
|
||
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error("人脸识别失败:", error);
|
||
throw error;
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
/**
|
||
* 边界框坐标信息
|
||
* @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"
|
||
}
|
||
]
|
||
}
|