diff --git a/api/constants.ts b/api/constants.ts index 02fd0d9..4abafbc 100644 --- a/api/constants.ts +++ b/api/constants.ts @@ -1 +1 @@ -export const BASE_URL = "https://77.smartvideo.py.qikongjian.com" +export const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL diff --git a/api/request.ts b/api/request.ts index 421a929..36fc35d 100644 --- a/api/request.ts +++ b/api/request.ts @@ -28,6 +28,8 @@ request.interceptors.request.use( request.interceptors.response.use( (response: AxiosResponse) => { // 直接返回响应数据 + console.log('?????????????????????????',Object.keys(response.data)); + return response.data; }, (error) => { diff --git a/api/video_flow.ts b/api/video_flow.ts index 8fb0305..02cc529 100644 --- a/api/video_flow.ts +++ b/api/video_flow.ts @@ -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 { @@ -17,12 +27,12 @@ interface BaseApiResponse { 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 { - category: 'sketch' | 'character' | 'video' | 'music' | 'final_video'; +export interface StreamData { + 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; @@ -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> => { - return post>('/movie/get_movie_project_detail', data); +export const detailScriptEpisodeNew = async (data: { + project_id: string; +}): Promise> => { + return post>("/movie/get_movie_project_detail", data); }; // 获取 title 接口 -export const getScriptTitle = async (data: { project_id: string }): Promise> => { - return post>('/movie/get_movie_project_description', data); -} +export const getScriptTitle = async (data: { + project_id: string; +}): Promise> => { + return post>("/movie/get_movie_project_description", data); +}; // 获取 数据 全量(需轮询) -export const getRunningStreamData = async (data: { project_id: string }): Promise> => { - return post>('/movie/get_status', data); +export const getRunningStreamData = async (data: { + project_id: string; +}): Promise> => { + return post>("/movie/get_status", data); }; // 获取 脚本 接口 -export const getScriptTags = async (data: { project_id: string }): Promise => { - return post('/movie/text_to_script_tags', data); +export const getScriptTags = async (data: { + project_id: string; +}): Promise => { + return post("/movie/text_to_script_tags", data); }; // 获取 loading-场景 接口 -export const getSceneJson = async (data: { project_id: string }): Promise> => { - return post('/movie/scene_json', data); +export const getSceneJson = async (data: { + project_id: string; +}): Promise> => { + return post("/movie/scene_json", data); }; // 获取 loading-分镜 接口 -export const getShotSketchJson = async (data: { project_id: string }): Promise> => { - return post('/movie/shot_sketch_json', data); +export const getShotSketchJson = async (data: { + project_id: string; +}): Promise> => { + return post("/movie/shot_sketch_json", data); }; // 获取 loading-视频 接口 -export const getVideoJson = async (data: { project_id: string }): Promise> => { - return post('/movie/video_json', data); +export const getVideoJson = async (data: { + project_id: string; +}): Promise> => { + return post("/movie/video_json", data); }; /** @@ -231,7 +257,7 @@ export const regenerateRole = async (request: { /** 角色ID(可选,如果重新生成现有角色) */ roleId?: string; }): Promise> => { - return post>('/movie/regenerate_role', request); + return post>("/movie/regenerate_role", request); }; /** @@ -245,7 +271,7 @@ export const applyRoleToShots = async (request: { /** 分镜ID列表 */ shotIds: string[]; }): Promise> => { - return post>('/movie/apply_role_to_shots', request); + return post>("/movie/apply_role_to_shots", request); }; /** @@ -256,13 +282,15 @@ export const applyRoleToShots = async (request: { export const getRoleShots = async (request: { /** 角色ID */ roleId: string; -}): Promise> => { - return post>('/movie/get_role_shots', request); +}): Promise< + ApiResponse<{ + /** 分镜列表 */ + shots: VideoSegmentEntity[]; + /** 已应用的分镜ID列表 */ + appliedShotIds: string[]; + }> +> => { + return post>("/movie/get_role_shots", request); }; /** @@ -274,7 +302,7 @@ export const getRoleList = async (request: { /** 项目ID */ projectId: string; }): Promise> => { - return post>('/movie/get_role_list', request); + return post>("/movie/get_role_list", request); }; /** @@ -285,21 +313,25 @@ export const getRoleList = async (request: { export const getRoleData = async (request: { /** 角色ID */ roleId: string; -}): Promise> => { - return post>('/movie/get_role_data', request); +}): Promise< + ApiResponse<{ + /** AI文本数据 */ + text: AITextEntity; + /** 标签列表 */ + tags: TagEntity[]; + }> +> => { + return post>("/movie/get_role_data", request); }; /** * 获取用户角色库 * @returns Promise> */ -export const getUserRoleLibrary = async (): Promise> => { - return post>('/movie/get_user_role_library', {}); +export const getUserRoleLibrary = async (): Promise< + ApiResponse +> => { + return post>("/movie/get_user_role_library", {}); }; /** @@ -313,10 +345,9 @@ export const replaceRole = async (request: { /** 替换的角色ID */ replaceRoleId: string; }): Promise> => { - return post>('/movie/replace_role', request); + return post>("/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> => { - return post>('/movie/update_tag', request); + return post>("/movie/update_tag", request); }; /** @@ -342,7 +373,7 @@ export const updateText = async (request: { /** 新的文案内容 */ content: string; }): Promise> => { - return post>('/movie/update_text', request); + return post>("/movie/update_text", request); }; /** @@ -358,7 +389,7 @@ export const regenerateScene = async (request: { /** 场景ID(可选,如果重新生成现有场景) */ sceneId?: string; }): Promise> => { - return post>('/movie/regenerate_scene', request); + return post>("/movie/regenerate_scene", request); }; /** @@ -372,7 +403,7 @@ export const applySceneToShots = async (request: { /** 分镜ID列表 */ shotIds: string[]; }): Promise> => { - return post>('/movie/apply_scene_to_shots', request); + return post>("/movie/apply_scene_to_shots", request); }; /** @@ -383,13 +414,15 @@ export const applySceneToShots = async (request: { export const getSceneData = async (request: { /** 场景ID */ sceneId: string; -}): Promise> => { - return post>('/movie/get_scene_data', request); +}): Promise< + ApiResponse<{ + /** AI文本数据 */ + text: AITextEntity; + /** 标签列表 */ + tags: TagEntity[]; + }> +> => { + return post>("/movie/get_scene_data", request); }; /** @@ -401,7 +434,7 @@ export const getSceneList = async (request: { /** 项目ID */ projectId: string; }): Promise> => { - return post>('/movie/get_scene_list', request); + return post>("/movie/get_scene_list", request); }; /** @@ -412,13 +445,15 @@ export const getSceneList = async (request: { export const getSceneShots = async (request: { /** 场景ID */ sceneId: string; -}): Promise> => { - return post>('/movie/get_scene_shots', request); +}): Promise< + ApiResponse<{ + /** 分镜列表 */ + shots: VideoSegmentEntity[]; + /** 已应用的分镜ID列表 */ + appliedShotIds: string[]; + }> +> => { + return post>("/movie/get_scene_shots", request); }; /** @@ -430,7 +465,7 @@ export const getShotRoles = async (request: { /** 分镜ID */ shotId: string; }): Promise> => { - return post>('/movie/get_shot_roles', request); + return post>("/movie/get_shot_roles", request); }; /** @@ -442,7 +477,7 @@ export const getShotScenes = async (request: { /** 分镜ID */ shotId: string; }): Promise> => { - return post>('/movie/get_shot_scenes', request); + return post>("/movie/get_shot_scenes", request); }; /** @@ -453,13 +488,15 @@ export const getShotScenes = async (request: { export const getShotData = async (request: { /** 分镜ID */ shotId: string; -}): Promise> => { - return post>('/movie/get_shot_data', request); +}): Promise< + ApiResponse<{ + /** AI文本数据 */ + text: AITextEntity; + /** 标签列表 */ + tags: TagEntity[]; + }> +> => { + return post>("/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> => { - return post>('/movie/regenerate_shot', request); +}): Promise> => { + return post>("/movie/regenerate_shot", request); }; /** @@ -497,8 +534,8 @@ export const updateShotContent = async (request: { /** 对话内容 */ content: string; }>; -}): Promise> => { - return post>('/movie/update_shot_content', request); +}): Promise> => { + return post>("/movie/update_shot_content", request); }; /** @@ -509,35 +546,8 @@ export const updateShotContent = async (request: { export const getShotList = async (request: { /** 项目ID */ projectId: string; -}): Promise> => { - return post>('/movie/get_shot_list', request); -}; - -// /** -// * 获取分镜详情 -// * @param request - 获取分镜详情请求参数 -// * @returns Promise> -// */ -// export const getShotDetail = async (request: { -// /** 分镜ID */ -// shotId: string; -// }): Promise> => { -// return post>('/movie/get_shot_detail', request); -// }; - - -/** - * 修改分镜镜头 - * @param request - 修改分镜镜头请求参数 - * @returns Promise> - */ -export const updateShotShot = async (request: { - /** 分镜ID */ - shotId: string; - /** 新的镜头数据 */ - shot: string[]; -}): Promise> => { - return post>('/movie/update_shot_shot', request); +}): Promise> => { + return post>("/movie/get_shot_list", request); }; /** @@ -553,7 +563,7 @@ export const replaceShotRole = async (request: { /** 新角色ID */ newRoleId: string; }): Promise> => { - return post>('/movie/replace_shot_role', request); + return post>("/movie/replace_shot_role", request); }; /** @@ -565,7 +575,7 @@ export const getShotVideoScript = async (request: { /** 分镜ID */ shotId: string; }): Promise> => { - return post>('/movie/get_shot_video_script', request); + return post>("/movie/get_shot_video_script", request); }; /** @@ -573,13 +583,30 @@ export const getShotVideoScript = async (request: { * @param request - AI生成剧本请求参数 * @returns Promise> */ -export const generateScriptStream = async (request: { - /** 剧本提示词 */ - text: string; -}) => { - return post>('/text_to_script/generate_script_stream', request,{ - responseType: 'stream', - }); +export const generateScriptStream = ( + request: { + /** 剧本提示词 */ + text: string; + }, + onData: (data: any) => void +): Promise => { + 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> => { - return post>('/movie/apply_script_to_shot', request); + return post>("/movie/apply_script_to_shot", request); }; /** @@ -603,16 +630,20 @@ export const applyScriptToShot = async (request: { export const getProjectScript = async (request: { /** 项目ID */ projectId: string; -}): Promise> => { - return post>('/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> => { - return post>('/movie/save_script', request); + return post>("/movie/save_script", request); }; /** @@ -639,11 +670,15 @@ export const createProject = async (request: { userPrompt: string; /** 剧本内容 */ scriptContent: string; -}): Promise> => { - return post>('/movie/create_project', request); + }> +> => { + return post< + ApiResponse<{ + projectId: string; + }> + >("/movie/create_project", request); }; diff --git a/app/service/Interaction/RoleService.ts b/app/service/Interaction/RoleService.ts index d03be59..bed1b91 100644 --- a/app/service/Interaction/RoleService.ts +++ b/app/service/Interaction/RoleService.ts @@ -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; } /** diff --git a/app/service/Interaction/SceneService.ts b/app/service/Interaction/SceneService.ts index f05960c..9467f1b 100644 --- a/app/service/Interaction/SceneService.ts +++ b/app/service/Interaction/SceneService.ts @@ -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; } /** diff --git a/app/service/Interaction/ScriptService.ts b/app/service/Interaction/ScriptService.ts index 3406049..6868dcf 100644 --- a/app/service/Interaction/ScriptService.ts +++ b/app/service/Interaction/ScriptService.ts @@ -79,6 +79,8 @@ export const useScriptService = (): UseScriptService => { // 获取生成的剧本文本 const generatedScriptText = newScriptEditUseCase.toString(); setScriptText(generatedScriptText); + console.log(scriptText); + // 获取剧本片段列表 const slices = newScriptEditUseCase.getScriptSlices(); setScriptSlices(slices); diff --git a/app/service/Interaction/ShotService.ts b/app/service/Interaction/ShotService.ts index 2bf722f..99e6f5c 100644 --- a/app/service/Interaction/ShotService.ts +++ b/app/service/Interaction/ShotService.ts @@ -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; - /** 选择分镜并获取详情 */ - selectShot: (shotId: string) => Promise; - /** 修改分镜对话内容 */ - updateShotContent: ( + /** 获取视频片段列表 */ + fetchVideoSegmentList: (projectId: string) => Promise; + /** 选择视频片段并获取详情 */ + selectVideoSegment: (videoSegmentId: string) => Promise; + /** 修改视频片段对话内容 */ + updateVideoSegmentContent: ( newContent: Array<{ roleId: string; content: string }> ) => Promise; - /** 修改分镜镜头 */ - updateShotShot: (newShot: string[]) => Promise; /** 获取用户角色库 */ fetchUserRoleLibrary: () => Promise; - /** 替换分镜角色 */ - replaceShotRole: (oldRoleId: string, newRoleId: string) => Promise; - /** 获取分镜视频剧本内容 */ - fetchShotVideoScript: () => Promise; - /** 获取分镜关联的角色信息 */ - getShotRoles: () => Promise; - /** 获取分镜关联的场景信息 */ - getShotScenes: () => Promise; + /** 替换视频片段角色 */ + replaceVideoSegmentRole: (oldRoleId: string, newRoleId: string) => Promise; + /** 获取视频片段视频剧本内容 */ + fetchVideoSegmentVideoScript: () => Promise; + /** 获取视频片段关联的角色信息 */ + getVideoSegmentRoles: () => Promise; + /** 获取视频片段关联的场景信息 */ + getVideoSegmentScenes: () => Promise; - /** 重新生成分镜 */ - 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([]); - const [selectedShot, setSelectedShot] = useState(null); - const [shotSketchData, setShotSketchData] = useState(null); - const [shotVideoData, setShotVideoData] = useState(null); + const [videoSegmentList, setVideoSegmentList] = useState([]); + const [selectedVideoSegment, setSelectedVideoSegment] = useState(null); + const [videoSegmentSketchData, setVideoSegmentSketchData] = useState(null); + const [videoSegmentVideoData, setVideoSegmentVideoData] = useState(null); const [userRoleLibrary, setUserRoleLibrary] = useState([]); - const [shotVideoScript, setShotVideoScript] = useState([]); + const [videoSegmentVideoScript, setVideoSegmentVideoScript] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [projectId, setProjectId] = useState(""); // UseCase实例 - const [shotEditUseCase, setShotEditUseCase] = - useState(null); + const [videoSegmentEditUseCase, setVideoSegmentEditUseCase] = + useState(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 角色信息列表 */ - const getShotRoles = useCallback(async (): Promise => { - if (!shotEditUseCase) { - throw new Error("分镜编辑用例未初始化"); + const getVideoSegmentRoles = useCallback(async (): Promise => { + if (!videoSegmentEditUseCase) { + throw new Error("视频片段编辑用例未初始化"); } - return await shotEditUseCase.getShotRoles(); - }, [shotEditUseCase]); + return await videoSegmentEditUseCase.getVideoSegmentRoles(); + }, [videoSegmentEditUseCase]); /** - * 获取分镜关联的场景信息 - * @description 获取当前分镜可以使用的场景列表 + * 获取视频片段关联的场景信息 + * @description 获取当前视频片段可以使用的场景列表 * @returns Promise 场景信息列表 */ - const getShotScenes = useCallback(async (): Promise => { - if (!shotEditUseCase) { - throw new Error("分镜编辑用例未初始化"); + const getVideoSegmentScenes = useCallback(async (): Promise => { + 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, }; }; diff --git a/app/service/domain/Entities.ts b/app/service/domain/Entities.ts index 57948e5..3783150 100644 --- a/app/service/domain/Entities.ts +++ b/app/service/domain/Entities.ts @@ -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; } diff --git a/app/service/domain/Item.ts b/app/service/domain/Item.ts index a90c305..7100277 100644 --- a/app/service/domain/Item.ts +++ b/app/service/domain/Item.ts @@ -4,7 +4,7 @@ import { RoleEntity, TagEntity, SceneEntity, - ShotEntity + VideoSegmentEntity } from './Entities'; /** @@ -113,13 +113,13 @@ export class SceneItem extends EditItem { } /** - * 分镜可编辑项 + * 视频片段可编辑项 */ -export class ShotItem extends EditItem { +export class VideoSegmentItem extends EditItem { type: ItemType = ItemType.IMAGE; constructor( - entity: ShotEntity, + entity: VideoSegmentEntity, metadata: Record = {} ) { super(entity, metadata); diff --git a/app/service/domain/valueObject.ts b/app/service/domain/valueObject.ts index 8b2ff94..ee3cff7 100644 --- a/app/service/domain/valueObject.ts +++ b/app/service/domain/valueObject.ts @@ -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]) + ); + } } diff --git a/app/service/test/Role.test.ts b/app/service/test/Role.test.ts index 1148c26..09bf75b 100644 --- a/app/service/test/Role.test.ts +++ b/app/service/test/Role.test.ts @@ -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', diff --git a/app/service/test/Scene.test.ts b/app/service/test/Scene.test.ts index 7247665..1b7fb0d 100644 --- a/app/service/test/Scene.test.ts +++ b/app/service/test/Scene.test.ts @@ -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', diff --git a/app/service/test/Script.test.ts b/app/service/test/Script.test.ts index 1a5443a..a4188f2 100644 --- a/app/service/test/Script.test.ts +++ b/app/service/test/Script.test.ts @@ -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秒超时 }); diff --git a/app/service/test/Shot.test.ts b/app/service/test/Shot.test.ts new file mode 100644 index 0000000..7dd91ef --- /dev/null +++ b/app/service/test/Shot.test.ts @@ -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'); + }); +}); diff --git a/app/service/usecase/ScriptEditUseCase.ts b/app/service/usecase/ScriptEditUseCase.ts index b9340d0..29d4f44 100644 --- a/app/service/usecase/ScriptEditUseCase.ts +++ b/app/service/usecase/ScriptEditUseCase.ts @@ -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): 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; - } } diff --git a/app/service/usecase/ShotEditUsecase.ts b/app/service/usecase/ShotEditUsecase.ts index 7981b5f..3ccae1f 100644 --- a/app/service/usecase/ShotEditUsecase.ts +++ b/app/service/usecase/ShotEditUsecase.ts @@ -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 角色信息列表 * @throws {Error} 当API调用失败时抛出错误 */ - async getShotRoles(): Promise { - const shotId = this.shotItem.entity.id; + async getVideoSegmentRoles(): Promise { + 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 场景信息列表 * @throws {Error} 当API调用失败时抛出错误 */ - async getShotScenes(): Promise { - const shotId = this.shotItem.entity.id; + async getVideoSegmentScenes(): Promise { + 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 重新生成的分镜实体 + * @returns Promise 重新生成的视频片段实体 * @throws {Error} 当API调用失败时抛出错误 */ - async regenerateShot( - shotPrompt: string, + async regenerateVideoSegment( + shotPrompt: LensType[], dialogueContent: ContentItem[], roleReplaceParams: { oldId: string; newId: string }[], sceneReplaceParams: { oldId: string; newId: string }[] - ): Promise { - const shotId = this.shotItem.entity.id; + ): Promise { + 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 修改后的分镜实体 + * @returns Promise 修改后的视频片段实体 * @throws {Error} 当API调用失败时抛出错误 */ - async updateShotContent(newContent: Array<{ roleId: string; content: string }>): Promise { - const shotId = this.shotItem.entity.id; + async updateVideoSegmentContent(newContent: Array<{ roleId: string; content: string }>): Promise { + 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(); } } } diff --git a/jest.config.js b/jest.config.js index 028307e..ae64901 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,4 +6,5 @@ module.exports = { moduleNameMapper: { '^@/(.*)$': '/$1', // 支持 Next.js 的 @ 别名 }, + testTimeout: 30000, // 全局设置30秒超时 }; diff --git a/next.config.js b/next.config.js index 3571675..45a8971 100644 --- a/next.config.js +++ b/next.config.js @@ -4,10 +4,10 @@ const nextConfig = { eslint: { ignoreDuringBuilds: true, }, - images: { - unoptimized: true + images: { + unoptimized: true }, - + webpack: (config, { dev, isServer }) => { if (dev && !isServer) { // 开发环境优化 diff --git a/tsconfig.json b/tsconfig.json index b4c7e29..09d7454 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"] } diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..2c47856 --- /dev/null +++ b/tsconfig.test.json @@ -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" + ] +} diff --git a/utils/tools.ts b/utils/tools.ts index 29034b5..00d1729 100644 --- a/utils/tools.ts +++ b/utils/tools.ts @@ -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; }