更新配置文件以支持测试和视频片段功能;在 jest.config.js 中设置全局测试超时为 30 秒;在 tsconfig.json 中添加新的编译选项以增强类型检查;新增 tsconfig.test.json 文件以支持测试环境配置;在 video_flow.ts 中重构 API 接口以使用 VideoSegmentEntity 替代 ShotEntity,并更新相关服务和测试用例以反映这些更改。

This commit is contained in:
海龙 2025-08-05 20:12:30 +08:00
parent 508065107a
commit ab10493248
21 changed files with 630 additions and 583 deletions

View File

@ -1 +1 @@
export const BASE_URL = "https://77.smartvideo.py.qikongjian.com"
export const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL

View File

@ -28,6 +28,8 @@ request.interceptors.request.use(
request.interceptors.response.use(
(response: AxiosResponse) => {
// 直接返回响应数据
console.log('?????????????????????????',Object.keys(response.data));
return response.data;
},
(error) => {

View File

@ -1,9 +1,19 @@
import { post } from './request';
import { ProjectTypeEnum } from '@/app/model/enums';
import { ApiResponse } from '@/api/common';
import { BASE_URL } from './constants'
import { AITextEntity, RoleEntity, SceneEntity, ShotEntity, TagEntity } from '@/app/service/domain/Entities';
import { ContentItem, LensType, ScriptSlice } from "@/app/service/domain/valueObject";
import { post, streamJsonPost } from "./request";
import { ProjectTypeEnum } from "@/app/model/enums";
import { ApiResponse } from "@/api/common";
import { BASE_URL } from "./constants";
import {
AITextEntity,
RoleEntity,
SceneEntity,
VideoSegmentEntity,
TagEntity,
} from "@/app/service/domain/Entities";
import {
ContentItem,
LensType,
ScriptSlice,
} from "@/app/service/domain/valueObject";
// API 响应类型
interface BaseApiResponse<T> {
@ -17,12 +27,12 @@ interface BaseApiResponse<T> {
interface EpisodeDetail {
project_id: string;
name: string;
status: 'running' | 'completed';
step: 'sketch' | 'character' | 'video' | 'music' | 'final_video';
status: "running" | "completed";
step: "sketch" | "character" | "video" | "music" | "final_video";
last_message: string;
data: TaskData | null;
mode: 'auto' | 'manual';
resolution: '1080p' | '4k';
mode: "auto" | "manual";
resolution: "1080p" | "4k";
}
// 任务数据类型
@ -58,11 +68,11 @@ interface TaskData {
}
// 流式数据类型
export interface StreamData<T=any> {
category: 'sketch' | 'character' | 'video' | 'music' | 'final_video';
export interface StreamData<T = any> {
category: "sketch" | "character" | "video" | "music" | "final_video";
message: string;
data: T;
status: 'running' | 'completed';
status: "running" | "completed";
total?: number;
completed?: number;
all_completed?: boolean;
@ -87,13 +97,13 @@ export interface Character {
// 剧本到分镜头提示词模型
export interface ScenePrompts {
scenes: Scene[]; // 分场景列表
characters?: Character[]; // 角色列表
summary?: string; // 剧情概要
scene?: string; // 场景描述
atmosphere?: string; // 氛围描述
episode_id?: number; // 剧集ID
total_shots?: string; // 总镜头数
scenes: Scene[]; // 分场景列表
characters?: Character[]; // 角色列表
summary?: string; // 剧情概要
scene?: string; // 场景描述
atmosphere?: string; // 氛围描述
episode_id?: number; // 剧集ID
total_shots?: string; // 总镜头数
}
// 剧本转分镜头请求接口
@ -113,7 +123,9 @@ export interface VideoToSceneRequest {
}
// 转换分镜头请求类型
export type ConvertScenePromptRequest = ScriptToSceneRequest | VideoToSceneRequest;
export type ConvertScenePromptRequest =
| ScriptToSceneRequest
| VideoToSceneRequest;
// 转换分镜头响应接口
export type ConvertScenePromptResponse = BaseApiResponse<ScenePrompts>;
@ -131,17 +143,17 @@ export const convertScenePrompt = async (
setTimeout(() => {
resolve({
code: 0,
message: 'success',
message: "success",
data: {
scenes: [],
characters: [],
summary: '',
scene: '',
atmosphere: '',
summary: "",
scene: "",
atmosphere: "",
episode_id: 0,
total_shots: ''
total_shots: "",
},
successful: true
successful: true,
});
}, 0);
});
@ -161,7 +173,7 @@ export const convertScriptToScene = async (
script,
episode_id,
script_id,
project_type: ProjectTypeEnum.SCRIPT_TO_VIDEO
project_type: ProjectTypeEnum.SCRIPT_TO_VIDEO,
});
};
@ -179,43 +191,57 @@ export const convertVideoToScene = async (
video_url,
episode_id,
script_id,
project_type: ProjectTypeEnum.VIDEO_TO_VIDEO
project_type: ProjectTypeEnum.VIDEO_TO_VIDEO,
});
};
// 新-获取剧集详情
export const detailScriptEpisodeNew = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/get_movie_project_detail', data);
export const detailScriptEpisodeNew = async (data: {
project_id: string;
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>("/movie/get_movie_project_detail", data);
};
// 获取 title 接口
export const getScriptTitle = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/get_movie_project_description', data);
}
export const getScriptTitle = async (data: {
project_id: string;
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>("/movie/get_movie_project_description", data);
};
// 获取 数据 全量(需轮询)
export const getRunningStreamData = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/get_status', data);
export const getRunningStreamData = async (data: {
project_id: string;
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>("/movie/get_status", data);
};
// 获取 脚本 接口
export const getScriptTags = async (data: { project_id: string }): Promise<any> => {
return post<any>('/movie/text_to_script_tags', data);
export const getScriptTags = async (data: {
project_id: string;
}): Promise<any> => {
return post<any>("/movie/text_to_script_tags", data);
};
// 获取 loading-场景 接口
export const getSceneJson = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
return post<any>('/movie/scene_json', data);
export const getSceneJson = async (data: {
project_id: string;
}): Promise<ApiResponse<any>> => {
return post<any>("/movie/scene_json", data);
};
// 获取 loading-分镜 接口
export const getShotSketchJson = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
return post<any>('/movie/shot_sketch_json', data);
export const getShotSketchJson = async (data: {
project_id: string;
}): Promise<ApiResponse<any>> => {
return post<any>("/movie/shot_sketch_json", data);
};
// 获取 loading-视频 接口
export const getVideoJson = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
return post<any>('/movie/video_json', data);
export const getVideoJson = async (data: {
project_id: string;
}): Promise<ApiResponse<any>> => {
return post<any>("/movie/video_json", data);
};
/**
@ -231,7 +257,7 @@ export const regenerateRole = async (request: {
/** 角色ID可选如果重新生成现有角色 */
roleId?: string;
}): Promise<ApiResponse<RoleEntity>> => {
return post<ApiResponse<any>>('/movie/regenerate_role', request);
return post<ApiResponse<any>>("/movie/regenerate_role", request);
};
/**
@ -245,7 +271,7 @@ export const applyRoleToShots = async (request: {
/** 分镜ID列表 */
shotIds: string[];
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/apply_role_to_shots', request);
return post<ApiResponse<any>>("/movie/apply_role_to_shots", request);
};
/**
@ -256,13 +282,15 @@ export const applyRoleToShots = async (request: {
export const getRoleShots = async (request: {
/** 角色ID */
roleId: string;
}): Promise<ApiResponse<{
/** 分镜列表 */
shots: ShotEntity[];
/** 已应用的分镜ID列表 */
appliedShotIds: string[];
}>> => {
return post<ApiResponse<any>>('/movie/get_role_shots', request);
}): Promise<
ApiResponse<{
/** 分镜列表 */
shots: VideoSegmentEntity[];
/** 已应用的分镜ID列表 */
appliedShotIds: string[];
}>
> => {
return post<ApiResponse<any>>("/movie/get_role_shots", request);
};
/**
@ -274,7 +302,7 @@ export const getRoleList = async (request: {
/** 项目ID */
projectId: string;
}): Promise<ApiResponse<RoleEntity[]>> => {
return post<ApiResponse<any>>('/movie/get_role_list', request);
return post<ApiResponse<any>>("/movie/get_role_list", request);
};
/**
@ -285,21 +313,25 @@ export const getRoleList = async (request: {
export const getRoleData = async (request: {
/** 角色ID */
roleId: string;
}): Promise<ApiResponse<{
/** AI文本数据 */
text: AITextEntity;
/** 标签列表 */
tags: TagEntity[];
}>> => {
return post<ApiResponse<any>>('/movie/get_role_data', request);
}): Promise<
ApiResponse<{
/** AI文本数据 */
text: AITextEntity;
/** 标签列表 */
tags: TagEntity[];
}>
> => {
return post<ApiResponse<any>>("/movie/get_role_data", request);
};
/**
*
* @returns Promise<ApiResponse<角色实体列表>>
*/
export const getUserRoleLibrary = async (): Promise<ApiResponse<RoleEntity[]>> => {
return post<ApiResponse<any>>('/movie/get_user_role_library', {});
export const getUserRoleLibrary = async (): Promise<
ApiResponse<RoleEntity[]>
> => {
return post<ApiResponse<any>>("/movie/get_user_role_library", {});
};
/**
@ -313,10 +345,9 @@ export const replaceRole = async (request: {
/** 替换的角色ID */
replaceRoleId: string;
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/replace_role', request);
return post<ApiResponse<any>>("/movie/replace_role", request);
};
/**
*
* @param request -
@ -326,9 +357,9 @@ export const updateTag = async (request: {
/** 标签ID */
tagId: string;
/** 新的标签内容 */
content: string|number;
content: string | number;
}): Promise<ApiResponse<TagEntity>> => {
return post<ApiResponse<any>>('/movie/update_tag', request);
return post<ApiResponse<any>>("/movie/update_tag", request);
};
/**
@ -342,7 +373,7 @@ export const updateText = async (request: {
/** 新的文案内容 */
content: string;
}): Promise<ApiResponse<AITextEntity>> => {
return post<ApiResponse<any>>('/movie/update_text', request);
return post<ApiResponse<any>>("/movie/update_text", request);
};
/**
@ -358,7 +389,7 @@ export const regenerateScene = async (request: {
/** 场景ID可选如果重新生成现有场景 */
sceneId?: string;
}): Promise<ApiResponse<SceneEntity>> => {
return post<ApiResponse<any>>('/movie/regenerate_scene', request);
return post<ApiResponse<any>>("/movie/regenerate_scene", request);
};
/**
@ -372,7 +403,7 @@ export const applySceneToShots = async (request: {
/** 分镜ID列表 */
shotIds: string[];
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/apply_scene_to_shots', request);
return post<ApiResponse<any>>("/movie/apply_scene_to_shots", request);
};
/**
@ -383,13 +414,15 @@ export const applySceneToShots = async (request: {
export const getSceneData = async (request: {
/** 场景ID */
sceneId: string;
}): Promise<ApiResponse<{
/** AI文本数据 */
text: AITextEntity;
/** 标签列表 */
tags: TagEntity[];
}>> => {
return post<ApiResponse<any>>('/movie/get_scene_data', request);
}): Promise<
ApiResponse<{
/** AI文本数据 */
text: AITextEntity;
/** 标签列表 */
tags: TagEntity[];
}>
> => {
return post<ApiResponse<any>>("/movie/get_scene_data", request);
};
/**
@ -401,7 +434,7 @@ export const getSceneList = async (request: {
/** 项目ID */
projectId: string;
}): Promise<ApiResponse<SceneEntity[]>> => {
return post<ApiResponse<any>>('/movie/get_scene_list', request);
return post<ApiResponse<any>>("/movie/get_scene_list", request);
};
/**
@ -412,13 +445,15 @@ export const getSceneList = async (request: {
export const getSceneShots = async (request: {
/** 场景ID */
sceneId: string;
}): Promise<ApiResponse<{
/** 分镜列表 */
shots: ShotEntity[];
/** 已应用的分镜ID列表 */
appliedShotIds: string[];
}>> => {
return post<ApiResponse<any>>('/movie/get_scene_shots', request);
}): Promise<
ApiResponse<{
/** 分镜列表 */
shots: VideoSegmentEntity[];
/** 已应用的分镜ID列表 */
appliedShotIds: string[];
}>
> => {
return post<ApiResponse<any>>("/movie/get_scene_shots", request);
};
/**
@ -430,7 +465,7 @@ export const getShotRoles = async (request: {
/** 分镜ID */
shotId: string;
}): Promise<ApiResponse<RoleEntity[]>> => {
return post<ApiResponse<any>>('/movie/get_shot_roles', request);
return post<ApiResponse<any>>("/movie/get_shot_roles", request);
};
/**
@ -442,7 +477,7 @@ export const getShotScenes = async (request: {
/** 分镜ID */
shotId: string;
}): Promise<ApiResponse<SceneEntity[]>> => {
return post<ApiResponse<any>>('/movie/get_shot_scenes', request);
return post<ApiResponse<any>>("/movie/get_shot_scenes", request);
};
/**
@ -453,13 +488,15 @@ export const getShotScenes = async (request: {
export const getShotData = async (request: {
/** 分镜ID */
shotId: string;
}): Promise<ApiResponse<{
/** AI文本数据 */
text: AITextEntity;
/** 标签列表 */
tags: TagEntity[];
}>> => {
return post<ApiResponse<any>>('/movie/get_shot_data', request);
}): Promise<
ApiResponse<{
/** AI文本数据 */
text: AITextEntity;
/** 标签列表 */
tags: TagEntity[];
}>
> => {
return post<ApiResponse<any>>("/movie/get_shot_data", request);
};
/**
@ -471,15 +508,15 @@ export const regenerateShot = async (request: {
/** 分镜ID */
shotId?: string;
/** 镜头描述 */
shotPrompt?: LensType[];
shotPrompt?: LensType[];
/** 对话内容 */
dialogueContent?: ContentItem[];
dialogueContent?: ContentItem[];
/** 角色ID替换参数格式为{oldId:string,newId:string}[] */
roleReplaceParams?: { oldId: string; newId: string }[];
/** 场景ID替换参数格式为{oldId:string,newId:string}[] */
sceneReplaceParams?: { oldId: string; newId: string }[];
}): Promise<ApiResponse<ShotEntity>> => {
return post<ApiResponse<any>>('/movie/regenerate_shot', request);
}): Promise<ApiResponse<VideoSegmentEntity>> => {
return post<ApiResponse<any>>("/movie/regenerate_shot", request);
};
/**
@ -497,8 +534,8 @@ export const updateShotContent = async (request: {
/** 对话内容 */
content: string;
}>;
}): Promise<ApiResponse<ShotEntity>> => {
return post<ApiResponse<any>>('/movie/update_shot_content', request);
}): Promise<ApiResponse<VideoSegmentEntity>> => {
return post<ApiResponse<any>>("/movie/update_shot_content", request);
};
/**
@ -509,35 +546,8 @@ export const updateShotContent = async (request: {
export const getShotList = async (request: {
/** 项目ID */
projectId: string;
}): Promise<ApiResponse<ShotEntity[]>> => {
return post<ApiResponse<any>>('/movie/get_shot_list', request);
};
// /**
// * 获取分镜详情
// * @param request - 获取分镜详情请求参数
// * @returns Promise<ApiResponse<分镜实体>>
// */
// export const getShotDetail = async (request: {
// /** 分镜ID */
// shotId: string;
// }): Promise<ApiResponse<ShotEntity>> => {
// return post<ApiResponse<any>>('/movie/get_shot_detail', request);
// };
/**
*
* @param request -
* @returns Promise<ApiResponse<修改后的分镜>>
*/
export const updateShotShot = async (request: {
/** 分镜ID */
shotId: string;
/** 新的镜头数据 */
shot: string[];
}): Promise<ApiResponse<ShotEntity>> => {
return post<ApiResponse<any>>('/movie/update_shot_shot', request);
}): Promise<ApiResponse<VideoSegmentEntity[]>> => {
return post<ApiResponse<any>>("/movie/get_shot_list", request);
};
/**
@ -553,7 +563,7 @@ export const replaceShotRole = async (request: {
/** 新角色ID */
newRoleId: string;
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/replace_shot_role', request);
return post<ApiResponse<any>>("/movie/replace_shot_role", request);
};
/**
@ -565,7 +575,7 @@ export const getShotVideoScript = async (request: {
/** 分镜ID */
shotId: string;
}): Promise<ApiResponse<string>> => {
return post<ApiResponse<any>>('/movie/get_shot_video_script', request);
return post<ApiResponse<any>>("/movie/get_shot_video_script", request);
};
/**
@ -573,13 +583,30 @@ export const getShotVideoScript = async (request: {
* @param request - AI生成剧本请求参数
* @returns Promise<ApiResponse<流式数据>>
*/
export const generateScriptStream = async (request: {
/** 剧本提示词 */
text: string;
}) => {
return post<ApiResponse<any>>('/text_to_script/generate_script_stream', request,{
responseType: 'stream',
});
export const generateScriptStream = (
request: {
/** 剧本提示词 */
text: string;
},
onData: (data: any) => void
): Promise<void> => {
return new Promise((resolve, reject) => {
streamJsonPost("/text_to_script/generate_script_stream", request, (data) => {
switch(data.status) {
case 'streaming':
onData(data.content);
break;
case 'completed':
console.log('生成完成:', data.message);
return;
case 'error':
console.error('生成失败:', data.message);
return;
}
})
})
};
/**
@ -593,7 +620,7 @@ export const applyScriptToShot = async (request: {
/** 剧本*/
scriptText: string;
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/apply_script_to_shot', request);
return post<ApiResponse<any>>("/movie/apply_script_to_shot", request);
};
/**
@ -603,16 +630,20 @@ export const applyScriptToShot = async (request: {
export const getProjectScript = async (request: {
/** 项目ID */
projectId: string;
}): Promise<ApiResponse<{
/** 用户提示词 */
prompt: string;
/** 生成的剧本文本 */
scriptText: string;
}>> => {
return post<ApiResponse<{
}): Promise<
ApiResponse<{
/** 用户提示词 */
prompt: string;
/** 生成的剧本文本 */
scriptText: string;
}>>('/movie/get_project_script', request);
}>
> => {
return post<
ApiResponse<{
prompt: string;
scriptText: string;
}>
>("/movie/get_project_script", request);
};
/**
@ -626,7 +657,7 @@ export const saveScript = async (request: {
/** 剧本文本 */
scriptText: string;
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/save_script', request);
return post<ApiResponse<any>>("/movie/save_script", request);
};
/**
@ -639,11 +670,15 @@ export const createProject = async (request: {
userPrompt: string;
/** 剧本内容 */
scriptContent: string;
}): Promise<ApiResponse<{
/** 项目ID */
projectId: string;
}>> => {
return post<ApiResponse<{
}): Promise<
ApiResponse<{
/** 项目ID */
projectId: string;
}>>('/movie/create_project', request);
}>
> => {
return post<
ApiResponse<{
projectId: string;
}>
>("/movie/create_project", request);
};

View File

@ -1,5 +1,5 @@
import { useState, useCallback, useMemo } from 'react';
import { RoleEntity, TagEntity, AITextEntity, ShotEntity } from '../domain/Entities';
import { RoleEntity, TagEntity, AITextEntity, VideoSegmentEntity } from '../domain/Entities';
import { RoleItem, TagItem, TextItem, ShotItem } from '../domain/Item';
import { RoleEditUseCase } from '../usecase/RoleEditUseCase';
import { TagEditUseCase } from '../usecase/TagEditUseCase';
@ -19,7 +19,7 @@ interface ShotSelectionItem {
/** 是否已应用角色 */
applied: boolean;
/** 分镜数据 */
shot: ShotEntity;
shot: VideoSegmentEntity;
}
/**

View File

@ -1,5 +1,5 @@
import { useState, useCallback, useMemo } from 'react';
import { SceneEntity, TagEntity, AITextEntity, ShotEntity } from '../domain/Entities';
import { SceneEntity, TagEntity, AITextEntity, VideoSegmentEntity } from '../domain/Entities';
import { SceneItem, TagItem, TextItem, ShotItem } from '../domain/Item';
import { SceneEditUseCase } from '../usecase/SceneEditUseCase';
import { TagEditUseCase } from '../usecase/TagEditUseCase';
@ -19,7 +19,7 @@ interface ShotSelectionItem {
/** 是否已应用场景 */
applied: boolean;
/** 分镜数据 */
shot: ShotEntity;
shot: VideoSegmentEntity;
}
/**

View File

@ -79,6 +79,8 @@ export const useScriptService = (): UseScriptService => {
// 获取生成的剧本文本
const generatedScriptText = newScriptEditUseCase.toString();
setScriptText(generatedScriptText);
console.log(scriptText);
// 获取剧本片段列表
const slices = newScriptEditUseCase.getScriptSlices();
setScriptSlices(slices);

View File

@ -1,71 +1,68 @@
import { useState, useCallback, useMemo } from "react";
import {
ShotEntity,
VideoSegmentEntity,
RoleEntity,
SceneEntity,
} from "../domain/Entities";
import { ContentItem, ScriptSlice, ScriptValueObject } from "../domain/valueObject";
import { ShotItem } from "../domain/Item";
import { ShotEditUseCase } from "../usecase/ShotEditUsecase";
import { ContentItem, ScriptSlice, ScriptValueObject, LensType } from "../domain/valueObject";
import { VideoSegmentItem } from "../domain/Item";
import { VideoSegmentEditUseCase } from "../usecase/ShotEditUsecase";
import {
getShotList,
// getShotDetail,
updateShotContent,
updateShotShot,
getUserRoleLibrary,
replaceShotRole,
getShotVideoScript,
} from "@/api/video_flow";
/**
* Hook接口
* Hook的所有状态和操作方法
* Hook接口
* Hook的所有状态和操作方法
*/
export interface UseShotService {
export interface UseVideoSegmentService {
// 响应式状态
/** 分镜列表 */
shotList: ShotEntity[];
/** 当前选中的分镜 */
selectedShot: ShotItem | null;
/** 当前分镜的草图数据URL */
shotSketchData: string | null;
/** 当前分镜的视频数据URL */
shotVideoData: string[] | null;
/** 视频片段列表 */
videoSegmentList: VideoSegmentEntity[];
/** 当前选中的视频片段 */
selectedVideoSegment: VideoSegmentItem | null;
/** 当前视频片段的草图数据URL */
videoSegmentSketchData: string | null;
/** 当前视频片段的视频数据URL */
videoSegmentVideoData: string[] | null;
/** 用户角色库 */
userRoleLibrary: RoleEntity[];
/** 当前分镜的视频剧本片段 */
shotVideoScript: ScriptSlice[];
/** 当前视频片段的视频剧本片段 */
videoSegmentVideoScript: ScriptSlice[];
/** 加载状态 */
loading: boolean;
/** 错误信息 */
error: string | null;
// 操作方法
/** 获取分镜列表 */
fetchShotList: (projectId: string) => Promise<void>;
/** 选择分镜并获取详情 */
selectShot: (shotId: string) => Promise<void>;
/** 修改分镜对话内容 */
updateShotContent: (
/** 获取视频片段列表 */
fetchVideoSegmentList: (projectId: string) => Promise<void>;
/** 选择视频片段并获取详情 */
selectVideoSegment: (videoSegmentId: string) => Promise<void>;
/** 修改视频片段对话内容 */
updateVideoSegmentContent: (
newContent: Array<{ roleId: string; content: string }>
) => Promise<void>;
/** 修改分镜镜头 */
updateShotShot: (newShot: string[]) => Promise<void>;
/** 获取用户角色库 */
fetchUserRoleLibrary: () => Promise<void>;
/** 替换分镜角色 */
replaceShotRole: (oldRoleId: string, newRoleId: string) => Promise<void>;
/** 获取分镜视频剧本内容 */
fetchShotVideoScript: () => Promise<void>;
/** 获取分镜关联的角色信息 */
getShotRoles: () => Promise<RoleEntity[]>;
/** 获取分镜关联的场景信息 */
getShotScenes: () => Promise<SceneEntity[]>;
/** 替换视频片段角色 */
replaceVideoSegmentRole: (oldRoleId: string, newRoleId: string) => Promise<void>;
/** 获取视频片段视频剧本内容 */
fetchVideoSegmentVideoScript: () => Promise<void>;
/** 获取视频片段关联的角色信息 */
getVideoSegmentRoles: () => Promise<RoleEntity[]>;
/** 获取视频片段关联的场景信息 */
getVideoSegmentScenes: () => Promise<SceneEntity[]>;
/** 重新生成分镜 */
regenerateShot: (
shotPrompt: string,
/** 重新生成视频片段 */
regenerateVideoSegment: (
shotPrompt: LensType[],
dialogueContent: ContentItem[],
roleReplaceParams: { oldId: string; newId: string }[],
sceneReplaceParams: { oldId: string; newId: string }[]
@ -73,44 +70,44 @@ export interface UseShotService {
}
/**
* Hook
*
*
* Hook
*
*
*/
export const useShotService = (): UseShotService => {
export const useVideoSegmentService = (): UseVideoSegmentService => {
// 响应式状态
const [shotList, setShotList] = useState<ShotEntity[]>([]);
const [selectedShot, setSelectedShot] = useState<ShotItem | null>(null);
const [shotSketchData, setShotSketchData] = useState<string | null>(null);
const [shotVideoData, setShotVideoData] = useState<string[] | null>(null);
const [videoSegmentList, setVideoSegmentList] = useState<VideoSegmentEntity[]>([]);
const [selectedVideoSegment, setSelectedVideoSegment] = useState<VideoSegmentItem | null>(null);
const [videoSegmentSketchData, setVideoSegmentSketchData] = useState<string | null>(null);
const [videoSegmentVideoData, setVideoSegmentVideoData] = useState<string[] | null>(null);
const [userRoleLibrary, setUserRoleLibrary] = useState<RoleEntity[]>([]);
const [shotVideoScript, setShotVideoScript] = useState<ScriptSlice[]>([]);
const [videoSegmentVideoScript, setVideoSegmentVideoScript] = useState<ScriptSlice[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [projectId, setProjectId] = useState<string>("");
// UseCase实例
const [shotEditUseCase, setShotEditUseCase] =
useState<ShotEditUseCase | null>(null);
const [videoSegmentEditUseCase, setVideoSegmentEditUseCase] =
useState<VideoSegmentEditUseCase | null>(null);
/**
*
* @description ID获取所有分镜列表
*
* @description ID获取所有视频片段列表
* @param projectId ID
*/
const fetchShotList = useCallback(async (projectId: string) => {
const fetchVideoSegmentList = useCallback(async (projectId: string) => {
setProjectId(projectId);
try {
setLoading(true);
setError(null);
const response = await getShotList({ projectId });
if (response.successful) {
setShotList(response.data);
setVideoSegmentList(response.data);
} else {
setError(`获取分镜列表失败: ${response.message}`);
setError(`获取视频片段列表失败: ${response.message}`);
}
} catch (err) {
setError(
`获取分镜列表失败: ${err instanceof Error ? err.message : "未知错误"}`
`获取视频片段列表失败: ${err instanceof Error ? err.message : "未知错误"}`
);
} finally {
setLoading(false);
@ -118,37 +115,37 @@ export const useShotService = (): UseShotService => {
}, []);
/**
*
* @description ID获取分镜详情UseCase和数据
* @param shotId ID
*
* @description ID获取视频片段详情UseCase和数据
* @param videoSegmentId ID
*/
const selectShot = useCallback(async (shotId: string) => {
const selectVideoSegment = useCallback(async (videoSegmentId: string) => {
try {
setLoading(true);
setError(null);
// 获取分镜详情
await fetchShotList(projectId);
const shotEntity = shotList.find(
(shot: ShotEntity) => shot.id === shotId
// 获取视频片段详情
await fetchVideoSegmentList(projectId);
const videoSegmentEntity = videoSegmentList.find(
(videoSegment: VideoSegmentEntity) => videoSegment.id === videoSegmentId
);
if (!shotEntity) {
setError(`分镜不存在: ${shotId}`);
if (!videoSegmentEntity) {
setError(`视频片段不存在: ${videoSegmentId}`);
return;
}
const shotItem = new ShotItem(shotEntity);
setSelectedShot(shotItem);
const videoSegmentItem = new VideoSegmentItem(videoSegmentEntity);
setSelectedVideoSegment(videoSegmentItem);
// 初始化UseCase
const newShotEditUseCase = new ShotEditUseCase(shotItem);
setShotEditUseCase(newShotEditUseCase);
const newVideoSegmentEditUseCase = new VideoSegmentEditUseCase(videoSegmentItem);
setVideoSegmentEditUseCase(newVideoSegmentEditUseCase);
// 从分镜实体中获取草图数据和视频数据
setShotSketchData(shotEntity.sketchUrl || null);
setShotVideoData(shotEntity.videoUrl || null);
// 从视频片段实体中获取草图数据和视频数据
setVideoSegmentSketchData(videoSegmentEntity.sketchUrl || null);
setVideoSegmentVideoData(videoSegmentEntity.videoUrl || null);
} catch (err) {
setError(
`选择分镜失败: ${err instanceof Error ? err.message : "未知错误"}`
`选择视频片段失败: ${err instanceof Error ? err.message : "未知错误"}`
);
} finally {
setLoading(false);
@ -156,25 +153,25 @@ export const useShotService = (): UseShotService => {
}, []);
/**
*
* @description ContentItem数量和ID顺序不能变content字段
*
* @description ContentItem数量和ID顺序不能变content字段
* @param newContent
*/
const updateShotContentHandler = useCallback(
const updateVideoSegmentContentHandler = useCallback(
async (newContent: Array<{ roleId: string; content: string }>) => {
if (!shotEditUseCase) {
setError("分镜编辑用例未初始化");
if (!videoSegmentEditUseCase) {
setError("视频片段编辑用例未初始化");
return;
}
try {
setLoading(true);
setError(null);
const updatedShot = await shotEditUseCase.updateShotContent(newContent);
setSelectedShot(new ShotItem(updatedShot));
const updatedVideoSegment = await videoSegmentEditUseCase.updateVideoSegmentContent(newContent);
setSelectedVideoSegment(new VideoSegmentItem(updatedVideoSegment));
} catch (err) {
setError(
`修改分镜对话内容失败: ${
`修改视频片段对话内容失败: ${
err instanceof Error ? err.message : "未知错误"
}`
);
@ -182,44 +179,9 @@ export const useShotService = (): UseShotService => {
setLoading(false);
}
},
[shotEditUseCase]
[videoSegmentEditUseCase]
);
/**
*
* @description
* @param newShot
*/
const updateShotShotHandler = useCallback(
async (newShot: string[]) => {
if (!selectedShot) {
setError("未选择分镜");
return;
}
try {
setLoading(true);
setError(null);
const response = await updateShotShot({
shotId: selectedShot.entity.id,
shot: newShot,
});
if (response.successful) {
const updatedShot = response.data;
setSelectedShot(new ShotItem(updatedShot));
} else {
setError(`修改分镜镜头失败: ${response.message}`);
}
} catch (err) {
setError(
`修改分镜镜头失败: ${err instanceof Error ? err.message : "未知错误"}`
);
} finally {
setLoading(false);
}
},
[selectedShot]
);
/**
*
@ -245,15 +207,15 @@ export const useShotService = (): UseShotService => {
}, []);
/**
*
* @description
*
* @description
* @param oldRoleId ID
* @param newRoleId ID
*/
const replaceShotRoleHandler = useCallback(
const replaceVideoSegmentRoleHandler = useCallback(
async (oldRoleId: string, newRoleId: string) => {
if (!selectedShot) {
setError("未选择分镜");
if (!selectedVideoSegment) {
setError("未选择视频片段");
return;
}
@ -261,34 +223,34 @@ export const useShotService = (): UseShotService => {
setLoading(true);
setError(null);
const response = await replaceShotRole({
shotId: selectedShot.entity.id,
shotId: selectedVideoSegment.entity.id,
oldRoleId,
newRoleId,
});
if (response.successful) {
// 重新获取分镜详情
await selectShot(selectedShot.entity.id);
// 重新获取视频片段详情
await selectVideoSegment(selectedVideoSegment.entity.id);
} else {
setError(`替换分镜角色失败: ${response.message}`);
setError(`替换视频片段角色失败: ${response.message}`);
}
} catch (err) {
setError(
`替换分镜角色失败: ${err instanceof Error ? err.message : "未知错误"}`
`替换视频片段角色失败: ${err instanceof Error ? err.message : "未知错误"}`
);
} finally {
setLoading(false);
}
},
[selectedShot, selectShot]
[selectedVideoSegment, selectVideoSegment]
);
/**
*
* @description ScriptSliceEntity片段
*
* @description ScriptSliceEntity片段
*/
const fetchShotVideoScript = useCallback(async () => {
if (!selectedShot) {
setError("未选择分镜");
const fetchVideoSegmentVideoScript = useCallback(async () => {
if (!selectedVideoSegment) {
setError("未选择视频片段");
return;
}
@ -296,130 +258,129 @@ export const useShotService = (): UseShotService => {
setLoading(true);
setError(null);
const response = await getShotVideoScript({
shotId: selectedShot.entity.id,
shotId: selectedVideoSegment.entity.id,
});
if (response.successful) {
const script = new ScriptValueObject(response.data);
setShotVideoScript(script.scriptSlices);
setVideoSegmentVideoScript([...script.scriptSlices]);
} else {
setError(`获取分镜视频剧本失败: ${response.message}`);
setError(`获取视频片段视频剧本失败: ${response.message}`);
}
} catch (err) {
setError(
`获取分镜视频剧本失败: ${
`获取视频片段视频剧本失败: ${
err instanceof Error ? err.message : "未知错误"
}`
);
} finally {
setLoading(false);
}
}, [selectedShot]);
}, [selectedVideoSegment]);
/**
*
* @description 使
*
* @description 使
* @returns Promise<RoleEntity[]>
*/
const getShotRoles = useCallback(async (): Promise<RoleEntity[]> => {
if (!shotEditUseCase) {
throw new Error("分镜编辑用例未初始化");
const getVideoSegmentRoles = useCallback(async (): Promise<RoleEntity[]> => {
if (!videoSegmentEditUseCase) {
throw new Error("视频片段编辑用例未初始化");
}
return await shotEditUseCase.getShotRoles();
}, [shotEditUseCase]);
return await videoSegmentEditUseCase.getVideoSegmentRoles();
}, [videoSegmentEditUseCase]);
/**
*
* @description 使
*
* @description 使
* @returns Promise<SceneEntity[]>
*/
const getShotScenes = useCallback(async (): Promise<SceneEntity[]> => {
if (!shotEditUseCase) {
throw new Error("分镜编辑用例未初始化");
const getVideoSegmentScenes = useCallback(async (): Promise<SceneEntity[]> => {
if (!videoSegmentEditUseCase) {
throw new Error("视频片段编辑用例未初始化");
}
return await shotEditUseCase.getShotScenes();
}, [shotEditUseCase]);
return await videoSegmentEditUseCase.getVideoSegmentScenes();
}, [videoSegmentEditUseCase]);
/**
*
* @description 使ID替换参数ID替换参数重新生成分镜
*
* @description 使ID替换参数ID替换参数重新生成视频片段
* @param shotPrompt
* @param dialogueContent
* @param roleReplaceParams ID替换参数{oldId:string,newId:string}[]
* @param sceneReplaceParams ID替换参数{oldId:string,newId:string}[]
*/
const regenerateShot = useCallback(
const regenerateVideoSegment = useCallback(
async (
shotPrompt: string,
shotPrompt: LensType[],
dialogueContent: ContentItem[],
roleReplaceParams: { oldId: string; newId: string }[],
sceneReplaceParams: { oldId: string; newId: string }[]
) => {
if (!shotEditUseCase) {
setError("分镜编辑用例未初始化");
if (!videoSegmentEditUseCase) {
setError("视频片段编辑用例未初始化");
return;
}
try {
setLoading(true);
setError(null);
const updatedShot = await shotEditUseCase.regenerateShot(
const updatedVideoSegment = await videoSegmentEditUseCase.regenerateVideoSegment(
shotPrompt,
dialogueContent,
roleReplaceParams,
sceneReplaceParams
);
setSelectedShot(new ShotItem(updatedShot));
setSelectedVideoSegment(new VideoSegmentItem(updatedVideoSegment));
} catch (err) {
setError(
`重新生成分镜失败: ${err instanceof Error ? err.message : "未知错误"}`
`重新生成视频片段失败: ${err instanceof Error ? err.message : "未知错误"}`
);
} finally {
setLoading(false);
}
},
[shotEditUseCase]
[videoSegmentEditUseCase]
);
return {
// 响应式状态 - 用于UI组件订阅和渲染
/** 分镜列表 - 当前项目的所有分镜数据 */
shotList,
/** 当前选中的分镜 - 包含分镜实体和编辑状态 */
selectedShot,
/** 当前分镜的草图数据URL - 从分镜实体中获取的草图图片链接 */
shotSketchData,
/** 当前分镜的视频数据URL - 从分镜实体中获取的视频链接 */
shotVideoData,
/** 视频片段列表 - 当前项目的所有视频片段数据 */
videoSegmentList,
/** 当前选中的视频片段 - 包含视频片段实体和编辑状态 */
selectedVideoSegment,
/** 当前视频片段的草图数据URL - 从视频片段实体中获取的草图图片链接 */
videoSegmentSketchData,
/** 当前视频片段的视频数据URL - 从视频片段实体中获取的视频链接 */
videoSegmentVideoData,
/** 用户角色库 - 当前用户可用的所有角色数据 */
userRoleLibrary,
/** 当前分镜的视频剧本片段 - 通过接口获取的剧本内容 */
shotVideoScript,
/** 当前视频片段的视频剧本片段 - 通过接口获取的剧本内容 */
videoSegmentVideoScript,
/** 加载状态 - 标识当前是否有异步操作正在进行 */
loading,
/** 错误信息 - 记录最近一次操作的错误信息 */
error,
// 操作方法 - 提供给UI组件调用的业务逻辑方法
/** 获取分镜列表 - 根据项目ID获取所有分镜数据 */
fetchShotList,
/** 选择分镜并获取详情 - 选择指定分镜并初始化相关数据 */
selectShot,
/** 修改分镜对话内容 - 更新分镜的对话内容保持ContentItem结构不变 */
updateShotContent: updateShotContentHandler,
/** 修改分镜镜头 - 更新分镜的镜头数据 */
updateShotShot: updateShotShotHandler,
/** 获取视频片段列表 - 根据项目ID获取所有视频片段数据 */
fetchVideoSegmentList,
/** 选择视频片段并获取详情 - 选择指定视频片段并初始化相关数据 */
selectVideoSegment,
/** 修改视频片段对话内容 - 更新视频片段的对话内容保持ContentItem结构不变 */
updateVideoSegmentContent: updateVideoSegmentContentHandler,
/** 获取用户角色库 - 获取当前用户的所有角色数据 */
fetchUserRoleLibrary,
/** 替换分镜角色 - 将分镜中的角色替换为角色库中的另一个角色 */
replaceShotRole: replaceShotRoleHandler,
/** 获取分镜视频剧本内容 - 通过接口获取视频剧本片段 */
fetchShotVideoScript,
/** 获取分镜关联的角色信息 - 获取当前分镜可用的角色列表 */
getShotRoles,
/** 获取分镜关联的场景信息 - 获取当前分镜可用的场景列表 */
getShotScenes,
/** 重新生成分镜 - 使用新参数重新生成分镜内容 */
regenerateShot,
/** 替换视频片段角色 - 将视频片段中的角色替换为角色库中的另一个角色 */
replaceVideoSegmentRole: replaceVideoSegmentRoleHandler,
/** 获取视频片段视频剧本内容 - 通过接口获取视频剧本片段 */
fetchVideoSegmentVideoScript,
/** 获取视频片段关联的角色信息 - 获取当前视频片段可用的角色列表 */
getVideoSegmentRoles,
/** 获取视频片段关联的场景信息 - 获取当前视频片段可用的场景列表 */
getVideoSegmentScenes,
/** 重新生成视频片段 - 使用新参数重新生成视频片段内容 */
regenerateVideoSegment,
};
};

View File

@ -69,8 +69,8 @@ export interface SceneEntity extends BaseEntity {
}
/**分镜进度 */
export enum ShotStatus {
/**视频片段进度 */
export enum VideoSegmentStatus {
/** 草稿加载中 */
sketchLoading = 0,
/** 视频加载中 */
@ -81,17 +81,17 @@ export enum ShotStatus {
/**
*
*
*/
export interface ShotEntity extends BaseEntity {
/** 分镜名称 */
export interface VideoSegmentEntity extends BaseEntity {
/** 视频片段名称 */
name: string;
/**分镜草图Url */
/**视频片段草图Url */
sketchUrl: string;
/**分镜视频Url */
/**视频片段视频Url */
videoUrl: string[];
/**分镜状态 */
status: ShotStatus;
/**视频片段状态 */
status: VideoSegmentStatus;
/**角色ID列表 */
roleList: string[];
/**场景ID列表 */
@ -100,7 +100,7 @@ export interface ShotEntity extends BaseEntity {
content: ContentItem[];
/**镜头项 */
lens: LensType[];
/**分镜剧本Id */
/**视频片段剧本Id */
scriptId: string;
}

View File

@ -4,7 +4,7 @@ import {
RoleEntity,
TagEntity,
SceneEntity,
ShotEntity
VideoSegmentEntity
} from './Entities';
/**
@ -113,13 +113,13 @@ export class SceneItem extends EditItem<SceneEntity> {
}
/**
*
*
*/
export class ShotItem extends EditItem<ShotEntity> {
export class VideoSegmentItem extends EditItem<VideoSegmentEntity> {
type: ItemType = ItemType.IMAGE;
constructor(
entity: ShotEntity,
entity: VideoSegmentEntity,
metadata: Record<string, any> = {}
) {
super(entity, metadata);

View File

@ -8,41 +8,118 @@ export enum ScriptSliceType {
/** 场景 */
scene = "scene",
}
/**
*
*
* @description
*/
export interface ScriptSlice {
/**唯一标识符 */
export class ScriptSlice {
/**唯一标识符 - 仅作为局部唯一性标识不作为全局Entity id */
readonly id: string;
/** 类型 */
type: ScriptSliceType;
readonly type: ScriptSliceType;
/** 剧本内容 */
text: string;
readonly text: string;
/** 元数据 */
metaData: any;
constructor(
id: string,
type: ScriptSliceType,
text: string,
metaData: any = {}
) {
this.id = id;
this.type = type;
this.text = text;
this.metaData = metaData;
}
/**
*
* @param other ScriptSlice实例
* @returns
*/
equals(other: ScriptSlice): boolean {
return (
this.type === other.type &&
this.text === other.text &&
JSON.stringify(this.metaData) === JSON.stringify(other.metaData)
);
}
}
/**对话内容项 */
export interface ContentItem {
/** 角色ID */
roleId: string;
/** 对话内容 */
content: string;
}
/**镜头值对象 */
export interface LensType {
/** 镜头名称 */
name: string;
/** 镜头描述 */
content: string;
/**运镜描述 */
movement: string;
}
/**
* @description:
* @return {*}
*
* @description
*/
export class ContentItem {
/** 角色ID */
readonly roleId: string;
/** 对话内容 */
readonly content: string;
constructor(roleId: string, content: string) {
this.roleId = roleId;
this.content = content;
}
/**
*
* @param other ContentItem实例
* @returns
*/
equals(other: ContentItem): boolean {
return this.roleId === other.roleId && this.content === other.content;
}
}
/**
*
* @description
*/
export class LensType {
/** 镜头名称 */
readonly name: string;
/** 镜头描述 */
readonly content: string;
/**运镜描述 */
readonly movement: string;
constructor(name: string, content: string, movement: string) {
this.name = name;
this.content = content;
this.movement = movement;
}
/**
*
* @param other LensType实例
* @returns
*/
equals(other: LensType): boolean {
return (
this.name === other.name &&
this.content === other.content &&
this.movement === other.movement
);
}
}
/**
*
* @description ScriptSlice的管理与行为
*/
export class ScriptValueObject {
scriptSlices: ScriptSlice[] = [];
/** 剧本片段数组 - 值对象数组 */
private readonly _scriptSlices: ScriptSlice[] = [];
/**
*
*/
get scriptSlices(): readonly ScriptSlice[] {
return [...this._scriptSlices];
}
/**
* @description:
@ -59,35 +136,29 @@ export class ScriptValueObject {
* @param scriptText
*/
parseFromString(scriptText: string): void {
// TODO: 实现字符串解析逻辑
console.log('scriptText', scriptText)
}
/**
* @description:
* @param text
* @returns ScriptSliceType
*/
private detectScriptType(text: string): ScriptSliceType {
// TODO: 实现类型检测逻辑
return ScriptSliceType.text;
}
/**
* @description:
* @param text
* @returns any
*/
private extractMetadata(text: string): any {
// TODO: 实现元数据提取逻辑
return {};
}
/**
* @description:
* @returns string
*/
toString(): string {
return this.scriptSlices.map((slice) => slice.text).join("");
return this._scriptSlices.map((slice) => slice.text).join("");
}
/**
*
* @param other ScriptValueObject实例
* @returns
*/
equals(other: ScriptValueObject): boolean {
if (this._scriptSlices.length !== other._scriptSlices.length) {
return false;
}
return this._scriptSlices.every((slice, index) =>
slice.equals(other._scriptSlices[index])
);
}
}

View File

@ -3,7 +3,7 @@ import { RoleEditUseCase } from '../usecase/RoleEditUseCase';
import { TextEditUseCase } from '../usecase/TextEditUseCase';
import { TagEditUseCase } from '../usecase/TagEditUseCase';
import { RoleItem, TextItem, TagItem } from '../domain/Item';
import { RoleEntity, AITextEntity, TagEntity, ShotEntity, ShotStatus } from '../domain/Entities';
import { RoleEntity, AITextEntity, TagEntity, VideoSegmentEntity, ShotStatus } from '../domain/Entities';
// Mock API模块
jest.mock('@/api/video_flow', () => ({
@ -66,7 +66,7 @@ describe('RoleService 业务逻辑测试', () => {
};
const mockShotEntity: ShotEntity = {
const mockShotEntity: VideoSegmentEntity = {
id: 'shot1',
name: '分镜1',
sketchUrl: 'http://example.com/sketch1.jpg',

View File

@ -3,7 +3,7 @@ import { SceneEditUseCase } from '../usecase/SceneEditUseCase';
import { TextEditUseCase } from '../usecase/TextEditUseCase';
import { TagEditUseCase } from '../usecase/TagEditUseCase';
import { SceneItem, TextItem, TagItem } from '../domain/Item';
import { SceneEntity, AITextEntity, TagEntity, ShotEntity, ShotStatus } from '../domain/Entities';
import { SceneEntity, AITextEntity, TagEntity, VideoSegmentEntity, ShotStatus } from '../domain/Entities';
// Mock API模块
jest.mock('@/api/video_flow', () => ({
@ -71,7 +71,7 @@ describe('SceneService 业务逻辑测试', () => {
disableEdit: false,
};
const mockShotEntity: ShotEntity = {
const mockShotEntity: VideoSegmentEntity = {
id: 'shot1',
name: '分镜1',
sketchUrl: 'http://example.com/sketch1.jpg',

View File

@ -1,46 +1,27 @@
import { generateScriptStream } from '../../../api/video_flow';
// Mock localStorage
global.localStorage = {
getItem: jest.fn(() => 'mock-token'),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
length: 0,
key: jest.fn(),
};
// Mock BASE_URL
jest.mock('../../../api/constants', () => ({
BASE_URL: 'http://127.0.0.1:8000'
}));
import { ScriptEditUseCase } from "../usecase/ScriptEditUseCase";
describe('ScriptService 业务逻辑测试', () => {
describe('generateScriptStream 真实接口测试', () => {
it('应该成功调用 generateScriptStream 接口并输出流数据', async () => {
/**
* generateScriptStream
*/
const stream = await generateScriptStream({
text: '一个年轻人在咖啡店里等待他的约会对象,心情紧张地摆弄着手机。'
});
let allData = '';
let isSuccessful: boolean | undefined = undefined;
let message: string | undefined = undefined;
// 假设 stream 是一个异步可迭代对象
try {
for await (const chunk of stream.data) {
console.log(chunk);
if (typeof chunk.data === 'string') {
allData += chunk.data;
}
if (typeof chunk.successful === 'boolean') {
isSuccessful = chunk.successful;
}
if (typeof chunk.message === 'string') {
message = chunk.message;
}
}
} catch (err) {
console.error('流式接口监听出错:', err);
throw err;
}
// 输出流式数据及状态
console.log('generateScriptStream 流式数据:', allData);
console.log('响应状态:', isSuccessful);
console.log('响应消息:', message);
expect(allData).toBeDefined();
expect(typeof isSuccessful).toBe('boolean');
});
});
// 创建新的剧本编辑用例
const newScriptEditUseCase = new ScriptEditUseCase('');
it("想法生成剧本", async () => {
const res = await newScriptEditUseCase.generateScript("我想拍一个关于爱情的故事",(content)=>{
console.log(content);
});
}, 300000); // 30秒超时
});

View File

@ -0,0 +1,13 @@
import { useVideoSegmentService } from '../Interaction/ShotService';
describe('VideoSegmentService 测试', () => {
it('初始化服务实例', () => {
const videoSegmentService = useVideoSegmentService();
expect(videoSegmentService).toBeDefined();
});
it('获取视频片段列表', () => {
const videoSegmentService = useVideoSegmentService();
expect(typeof videoSegmentService.fetchVideoSegmentList).toBe('function');
});
});

View File

@ -24,27 +24,12 @@ export class ScriptEditUseCase {
this.abortController = new AbortController();
// 使用API接口生成剧本
const response = await generateScriptStream({
text: prompt,
});
if (!response.successful) {
throw new Error(response.message || "AI生成剧本失败");
}
// 使用for await处理流式数据
for await (const data of response.data) {
// 检查是否被中断
if (this.abortController.signal.aborted) {
console.log("剧本生成被中断");
break;
}
// TODO: 根据流式数据更新剧本片段
// 这里需要根据实际的流式数据格式来处理
// 可能需要将流式数据转换为ScriptSlice并添加到scriptValueObject中
stream_callback?.(data);
}
await generateScriptStream({
text: prompt,
},(content)=>{
stream_callback?.(content)
this.scriptValueObject.parseFromString(content)
});
} catch (error) {
if (this.abortController?.signal.aborted) {
console.log("剧本生成被中断");
@ -100,7 +85,7 @@ export class ScriptEditUseCase {
* @returns ScriptSlice[]
*/
getScriptSlices(): ScriptSlice[] {
return this.scriptValueObject.scriptSlices;
return [...this.scriptValueObject.scriptSlices];
}
/**
@ -127,26 +112,4 @@ export class ScriptEditUseCase {
return this.scriptValueObject.toString();
}
/**
* @description: ID的剧本片段
* @param id
* @param text
* @param metaData
* @returns boolean
*/
updateScriptSlice(id: string, text: string, metaData?: Record<string, any>): boolean {
const index = this.scriptValueObject.scriptSlices.findIndex(slice => slice.id === id);
if (index === -1) {
return false;
}
// 只更新text和metaData字段保持其他字段不变
this.scriptValueObject.scriptSlices[index] = {
...this.scriptValueObject.scriptSlices[index],
text,
metaData: metaData || this.scriptValueObject.scriptSlices[index].metaData
};
return true;
}
}

View File

@ -1,5 +1,6 @@
import { ShotEntity, RoleEntity, SceneEntity, AITextEntity, TagEntity, ContentItem } from '../domain/Entities';
import { ShotItem, RoleItem, SceneItem, TextItem, TagItem } from '../domain/Item';
import { VideoSegmentEntity, RoleEntity, SceneEntity, AITextEntity, TagEntity } from '../domain/Entities';
import { ContentItem, LensType } from '../domain/valueObject';
import { VideoSegmentItem, RoleItem, SceneItem, TextItem, TagItem } from '../domain/Item';
import {
getShotRoles,
getShotScenes,
@ -9,126 +10,126 @@ import {
} from '@/api/video_flow';
/**
*
*
*
*
*/
export class ShotEditUseCase {
constructor(private shotItem: ShotItem) {
export class VideoSegmentEditUseCase {
constructor(private videoSegmentItem: VideoSegmentItem) {
}
/**
*
* @description 使
*
* @description 使
* @returns Promise<RoleEntity[]>
* @throws {Error} API调用失败时抛出错误
*/
async getShotRoles(): Promise<RoleEntity[]> {
const shotId = this.shotItem.entity.id;
async getVideoSegmentRoles(): Promise<RoleEntity[]> {
const videoSegmentId = this.videoSegmentItem.entity.id;
if (!shotId) {
throw new Error('分镜ID不存在无法获取角色信息');
if (!videoSegmentId) {
throw new Error('视频片段ID不存在无法获取角色信息');
}
const response = await getShotRoles({
shotId: shotId
shotId: videoSegmentId
});
if (response.successful) {
return response.data;
} else {
throw new Error(`获取分镜角色信息失败: ${response.message}`);
throw new Error(`获取视频片段角色信息失败: ${response.message}`);
}
}
/**
*
* @description 使
*
* @description 使
* @returns Promise<SceneEntity[]>
* @throws {Error} API调用失败时抛出错误
*/
async getShotScenes(): Promise<SceneEntity[]> {
const shotId = this.shotItem.entity.id;
async getVideoSegmentScenes(): Promise<SceneEntity[]> {
const videoSegmentId = this.videoSegmentItem.entity.id;
if (!shotId) {
throw new Error('分镜ID不存在无法获取场景信息');
if (!videoSegmentId) {
throw new Error('视频片段ID不存在无法获取场景信息');
}
const response = await getShotScenes({
shotId: shotId
shotId: videoSegmentId
});
if (response.successful) {
return response.data;
} else {
throw new Error(`获取分镜场景信息失败: ${response.message}`);
throw new Error(`获取视频片段场景信息失败: ${response.message}`);
}
}
/**
*
* @description
* @returns Promise<{ text: AITextEntity; tags: TagEntity[] }> AI文本和标签数据
*
* @description
* @returns Promise<{ text: AITextEntity; tags: TagEntity[] }> AI文本和标签数据
* @throws {Error} API调用失败时抛出错误
*/
async refreshShotData(): Promise<{ text: AITextEntity; tags: TagEntity[] }> {
const shotId = this.shotItem.entity.id;
async refreshVideoSegmentData(): Promise<{ text: AITextEntity; tags: TagEntity[] }> {
const videoSegmentId = this.videoSegmentItem.entity.id;
if (!shotId) {
throw new Error('分镜ID不存在无法获取分镜数据');
if (!videoSegmentId) {
throw new Error('视频片段ID不存在无法获取视频片段数据');
}
const response = await getShotData({
shotId: shotId
shotId: videoSegmentId
});
if (response.successful) {
// 更新当前分镜的实体数据
// 更新当前视频片段的实体数据
const { text, tags } = response.data;
// 更新分镜实体中的相关字段
const updatedShotEntity = {
...this.shotItem.entity,
// 更新视频片段实体中的相关字段
const updatedVideoSegmentEntity = {
...this.videoSegmentItem.entity,
generateTextId: text.id, // 更新AI文本ID
tagIds: tags.map((tag: TagEntity) => tag.id), // 更新标签ID列表
updatedAt: Date.now(), // 更新时间戳
};
// 更新当前UseCase中的实体
this.shotItem.setEntity(updatedShotEntity);
this.videoSegmentItem.setEntity(updatedVideoSegmentEntity);
// 检查状态是否需要更新为视频状态
this.checkAndUpdateVideoStatus(updatedShotEntity);
this.checkAndUpdateVideoStatus(updatedVideoSegmentEntity);
return response.data;
} else {
throw new Error(`获取分镜数据失败: ${response.message}`);
throw new Error(`获取视频片段数据失败: ${response.message}`);
}
}
/**
*
* @description 使ID替换参数ID替换参数重新生成分镜
*
* @description 使ID替换参数ID替换参数重新生成视频片段
* @param shotPrompt
* @param dialogueContent
* @param roleReplaceParams ID替换参数{oldId:string,newId:string}[]
* @param sceneReplaceParams ID替换参数{oldId:string,newId:string}[]
* @returns Promise<ShotEntity>
* @returns Promise<VideoSegmentEntity>
* @throws {Error} API调用失败时抛出错误
*/
async regenerateShot(
shotPrompt: string,
async regenerateVideoSegment(
shotPrompt: LensType[],
dialogueContent: ContentItem[],
roleReplaceParams: { oldId: string; newId: string }[],
sceneReplaceParams: { oldId: string; newId: string }[]
): Promise<ShotEntity> {
const shotId = this.shotItem.entity.id;
): Promise<VideoSegmentEntity> {
const videoSegmentId = this.videoSegmentItem.entity.id;
if (!shotId) {
throw new Error('分镜ID不存在无法重新生成分镜');
if (!videoSegmentId) {
throw new Error('视频片段ID不存在无法重新生成视频片段');
}
// 调用重新生成分镜接口
// 调用重新生成视频片段接口
const response = await regenerateShot({
shotId: shotId,
shotId: videoSegmentId,
shotPrompt: shotPrompt,
dialogueContent: dialogueContent,
roleReplaceParams: roleReplaceParams,
@ -136,32 +137,32 @@ export class ShotEditUseCase {
});
if (response.successful) {
const shotEntity = response.data;
this.shotItem.setEntity(shotEntity);
const videoSegmentEntity = response.data;
this.videoSegmentItem.setEntity(videoSegmentEntity);
// 检查状态是否需要更新为视频状态
this.checkAndUpdateVideoStatus(shotEntity);
return shotEntity;
this.checkAndUpdateVideoStatus(videoSegmentEntity);
return videoSegmentEntity;
} else {
throw new Error(`重新生成分镜失败: ${response.message}`);
throw new Error(`重新生成视频片段失败: ${response.message}`);
}
}
/**
*
* @description ContentItem数量和ID顺序不能变content字段
*
* @description ContentItem数量和ID顺序不能变content字段
* @param newContent
* @returns Promise<ShotEntity>
* @returns Promise<VideoSegmentEntity>
* @throws {Error} API调用失败时抛出错误
*/
async updateShotContent(newContent: Array<{ roleId: string; content: string }>): Promise<ShotEntity> {
const shotId = this.shotItem.entity.id;
async updateVideoSegmentContent(newContent: Array<{ roleId: string; content: string }>): Promise<VideoSegmentEntity> {
const videoSegmentId = this.videoSegmentItem.entity.id;
if (!shotId) {
throw new Error('分镜ID不存在无法修改对话内容');
if (!videoSegmentId) {
throw new Error('视频片段ID不存在无法修改对话内容');
}
// 验证ContentItem数量和ID顺序
const currentContent = this.shotItem.entity.content;
const currentContent = this.videoSegmentItem.entity.content;
if (newContent.length !== currentContent.length) {
throw new Error('ContentItem数量不能改变');
}
@ -174,30 +175,30 @@ export class ShotEditUseCase {
}
const response = await updateShotContent({
shotId: shotId,
shotId: videoSegmentId,
content: newContent,
});
if (response.successful) {
const shotEntity = response.data;
this.shotItem.setEntity(shotEntity);
const videoSegmentEntity = response.data;
this.videoSegmentItem.setEntity(videoSegmentEntity);
// 检查状态是否需要更新为视频状态
this.checkAndUpdateVideoStatus(shotEntity);
return shotEntity;
this.checkAndUpdateVideoStatus(videoSegmentEntity);
return videoSegmentEntity;
} else {
throw new Error(`修改分镜对话内容失败: ${response.message}`);
throw new Error(`修改视频片段对话内容失败: ${response.message}`);
}
}
/**
*
* @description updateToVideoStatus
* @param shotEntity
* @description updateToVideoStatus
* @param videoSegmentEntity
*/
private checkAndUpdateVideoStatus(shotEntity: ShotEntity): void {
private checkAndUpdateVideoStatus(videoSegmentEntity: VideoSegmentEntity): void {
// 当状态为视频加载中或完成时,更新为视频状态
if (shotEntity.status === 1 || shotEntity.status === 2) { // videoLoading 或 finished
this.shotItem.updateToVideoStatus();
if (videoSegmentEntity.status === 1 || videoSegmentEntity.status === 2) { // videoLoading 或 finished
this.videoSegmentItem.updateToVideoStatus();
}
}
}

View File

@ -6,4 +6,5 @@ module.exports = {
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1', // 支持 Next.js 的 @ 别名
},
testTimeout: 30000, // 全局设置30秒超时
};

View File

@ -21,8 +21,10 @@
"paths": {
"@/*": ["./*"]
},
"maxNodeModuleJsDepth":0
"maxNodeModuleJsDepth": 1,
"useDefineForClassFields": true,
"forceConsistentCasingInFileNames": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"exclude": ["node_modules", "**/*.test.ts", "**/*.spec.ts", ".next"]
}

16
tsconfig.test.json Normal file
View File

@ -0,0 +1,16 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"],
"isolatedModules": false
},
"include": [
"**/*.test.ts",
"**/*.test.tsx",
"**/*.spec.ts",
"**/*.spec.tsx"
],
"exclude": [
"node_modules"
]
}

View File

@ -1,14 +1,13 @@
import { ScriptSlice, ScriptSliceType } from "@/app/service/domain/valueObject";
export function parseScriptEntity(text: string):ScriptSlice {
const scriptSlice:ScriptSlice={
const scriptSlice = new ScriptSlice(
// 生成唯一ID单次使用即可
id: `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`,
type: ScriptSliceType.text,
text: text,
metaData: {}
}
`${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`,
ScriptSliceType.text,
text,
{}
);
return scriptSlice;
}