diff --git a/api/video_flow.ts b/api/video_flow.ts index 88f9705..f667e70 100644 --- a/api/video_flow.ts +++ b/api/video_flow.ts @@ -2,7 +2,8 @@ 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, ScriptSliceEntity } from '@/app/service/domain/Entities'; +import { AITextEntity, RoleEntity, SceneEntity, ShotEntity, TagEntity, ContentItem } from '@/app/service/domain/Entities'; +import { ScriptSlice } from "@/app/service/domain/valueObject"; // API 响应类型 interface BaseApiResponse { @@ -57,10 +58,10 @@ interface TaskData { } // 流式数据类型 -export interface StreamData { +export interface StreamData { category: 'sketch' | 'character' | 'video' | 'music' | 'final_video'; message: string; - data: any; + data: T; status: 'running' | 'completed'; total?: number; completed?: number; @@ -468,15 +469,15 @@ export const getShotData = async (request: { */ export const regenerateShot = async (request: { /** 分镜ID */ - shotId: string; + shotId?: string; /** 镜头描述 */ - shotPrompt: string; + shotPrompt?: string; /** 对话内容 */ - dialogueContent: string; + dialogueContent?: ContentItem[]; /** 角色ID替换参数,格式为{oldId:string,newId:string}[] */ - roleReplaceParams: { oldId: string; newId: string }[]; + roleReplaceParams?: { oldId: string; newId: string }[]; /** 场景ID替换参数,格式为{oldId:string,newId:string}[] */ - sceneReplaceParams: { oldId: string; newId: string }[]; + sceneReplaceParams?: { oldId: string; newId: string }[]; }): Promise> => { return post>('/movie/regenerate_shot', request); }; @@ -512,41 +513,18 @@ export const getShotList = async (request: { 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 getShotDetail = async (request: { +// /** 分镜ID */ +// shotId: string; +// }): Promise> => { +// return post>('/movie/get_shot_detail', request); +// }; -/** - * 获取分镜草图数据 - * @param request - 获取分镜草图数据请求参数 - * @returns Promise> - */ -export const getShotSketchData = async (request: { - /** 分镜ID */ - shotId: string; -}): Promise> => { - return post>('/movie/get_shot_sketch_data', request); -}; - -/** - * 获取分镜视频数据 - * @param request - 获取分镜视频数据请求参数 - * @returns Promise> - */ -export const getShotVideoData = async (request: { - /** 分镜ID */ - shotId: string; -}): Promise> => { - return post>('/movie/get_shot_video_data', request); -}; /** * 修改分镜镜头 @@ -586,6 +564,32 @@ export const replaceShotRole = async (request: { export const getShotVideoScript = async (request: { /** 分镜ID */ shotId: string; -}): Promise> => { +}): Promise> => { return post>('/movie/get_shot_video_script', request); }; + +/** + * AI生成剧本流式接口 + * @param request - AI生成剧本请求参数 + * @returns Promise> + */ +export const generateScriptStream = async (request: { + /** 剧本提示词 */ + prompt: string; +}) => { + return post>('/movie/generate_script_stream', request,{ + responseType: 'stream', + }); +}; + +/** + * 应用剧本 + * @param request - 应用剧本请求参数 + * @returns Promise> + */ +export const applyScriptToShot = async (request: { + /** 剧本*/ + script: string; +}): Promise> => { + return post>('/movie/apply_script_to_shot', request); +}; diff --git a/app/service/Interaction/ScriptService.ts b/app/service/Interaction/ScriptService.ts new file mode 100644 index 0000000..4c0b5cf --- /dev/null +++ b/app/service/Interaction/ScriptService.ts @@ -0,0 +1,290 @@ +import { useState, useCallback, useMemo } from "react"; +import { ScriptSlice } from "../domain/valueObject"; +import { ScriptEditUseCase } from "../usecase/ScriptEditUseCase"; + +/** + * 剧本服务Hook接口 + * 定义剧本服务Hook的所有状态和操作方法 + */ +export interface UseScriptService { + // 响应式状态 + /** 当前剧本文本 */ + scriptText: string; + /** 剧本片段列表 */ + scriptSlices: ScriptSlice[]; + /** 当前聚焦的剧本片段ID */ + focusedSliceId: string; + /** 当前聚焦的剧本片段 */ + focusedSlice: ScriptSlice | null; + /** 当前聚焦的剧本片段文本 */ + scriptSliceText: string; + /** 用户提示词(可编辑) */ + userPrompt: string; + /** 加载状态 */ + loading: boolean; + /** 错误信息 */ + error: string | null; + + // 操作方法 + /** 获取剧本数据(用户提示词) */ + fetchScriptData: (prompt: string) => Promise; + /** 设置当前聚焦的剧本片段 */ + setFocusedSlice: (sliceId: string) => void; + /** 清除聚焦状态 */ + clearFocusedSlice: () => void; + /** 快速更新当前聚焦的剧本片段文本(无防抖) */ + updateScriptSliceText: (text: string, metaData?: any) => void; + /** 更新用户提示词 */ + updateUserPrompt: (prompt: string) => void; + /** 重置剧本内容到初始状态 */ + resetScript: () => void; + /** AI生成剧本 */ + generateScript: (prompt: string) => Promise; + /** 应用剧本 */ + applyScript: () => Promise; + /** 更新聚焦剧本片段 */ + UpdateFocusedSlice: (text: string) => void; +} + +/** + * 剧本服务Hook + * 提供剧本相关的所有状态管理和操作方法 + * 包括剧本数据获取、片段管理、聚焦状态、防抖更新等功能 + */ +export const useScriptService = (): UseScriptService => { + // 响应式状态 + const [scriptText, setScriptText] = useState(""); + const [scriptSlices, setScriptSlices] = useState([]); + const [focusedSliceId, setFocusedSliceId] = useState(""); + const [scriptSliceText, setScriptSliceText] = useState(""); + const [userPrompt, setUserPrompt] = useState(""); + const [initialScriptText, setInitialScriptText] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // UseCase实例 + const [scriptEditUseCase, setScriptEditUseCase] = useState(null); + + // 防抖定时器 + const [debounceTimer, setDebounceTimer] = useState(null); + const DEBOUNCE_DELAY = 300; + + /** + * 当前聚焦的剧本片段 + */ + const focusedSlice = useMemo(() => { + return scriptSlices.find(slice => slice.id === focusedSliceId) || null; + }, [scriptSlices, focusedSliceId]); + + /** + * 获取剧本数据(用户提示词) + * @param prompt 用户提示词 + */ + const fetchScriptData = useCallback(async (prompt: string): Promise => { + try { + setLoading(true); + setError(null); + + // 清空当前状态 + setScriptText(""); + setScriptSlices([]); + setFocusedSliceId(""); + setScriptSliceText(""); + + // 更新用户提示词状态 + setUserPrompt(prompt); + + // 保存初始提示词(只在第一次获取时保存) + if (!initialScriptText) { + setInitialScriptText(prompt); + } + + // 创建新的剧本编辑用例 + const newScriptEditUseCase = new ScriptEditUseCase(''); + setScriptEditUseCase(newScriptEditUseCase); + + // 调用AI生成剧本 + await newScriptEditUseCase.generateScript(prompt); + + // 获取生成的剧本文本 + const generatedScriptText = newScriptEditUseCase.toString(); + setScriptText(generatedScriptText); + + // 获取剧本片段列表 + const slices = newScriptEditUseCase.getScriptSlices(); + setScriptSlices(slices); + + } catch (error) { + console.error('获取剧本数据失败:', error); + setError(error instanceof Error ? error.message : '获取剧本数据失败'); + throw error; + } finally { + setLoading(false); + } + }, [initialScriptText]); + + + + /** + * 设置当前聚焦的剧本片段 + * @param sliceId 剧本片段ID + */ + const setFocusedSlice = useCallback((sliceId: string): void => { + setFocusedSliceId(sliceId); + + // 同步输入框文本为当前聚焦片段的文本 + const focusedSlice = scriptSlices.find(slice => slice.id === sliceId); + if (focusedSlice) { + setScriptSliceText(focusedSlice.text); + } else { + setScriptSliceText(""); + } + }, [scriptSlices]); + + /** + * 清除聚焦状态 + */ + const clearFocusedSlice = useCallback((): void => { + setFocusedSliceId(""); + setScriptSliceText(""); + }, []); + + /** + * 快速更新输入框文本(无防抖) + * @param text 新的文本内容 + * @param metaData 新的元数据 + */ + const updateScriptSliceText = useCallback((text: string, metaData?: any): void => { + setScriptSliceText(text); + + // 自动触发防抖更新值对象 + // 清除之前的定时器 + if (debounceTimer) { + clearTimeout(debounceTimer); + } + + // 设置新的防抖定时器 + const timer = setTimeout(() => { + UpdateFocusedSlice(text, metaData); + }, DEBOUNCE_DELAY); + + setDebounceTimer(timer); + }, [debounceTimer]); + + /** + * 执行更新聚焦剧本片段 + * @param text 新的文本内容 + * @param metaData 新的元数据 + */ + const UpdateFocusedSlice = useCallback((text: string, metaData?: any): void => { + if (!focusedSliceId || !scriptEditUseCase) { + return; + } + + const success = scriptEditUseCase.updateScriptSlice( + focusedSliceId, + text, + metaData + ); + + if (success) { + // 更新本地片段列表 + const slices = scriptEditUseCase.getScriptSlices(); + setScriptSlices(slices); + } + }, [focusedSliceId, scriptEditUseCase]); + + /** + * 更新用户提示词 + * @param prompt 新的用户提示词 + */ + const updateUserPrompt = useCallback((prompt: string): void => { + setUserPrompt(prompt); + }, []); + + /** + * 重置剧本内容到初始状态 + */ + const resetScript = useCallback((): void => { + if (initialScriptText) { + // 重新调用AI生成剧本(fetchScriptData会自动清空状态) + fetchScriptData(initialScriptText); + } + }, [initialScriptText, fetchScriptData]); + + + + /** + * AI生成剧本 + * @param prompt 剧本提示词 + */ + const generateScript = useCallback(async (prompt: string): Promise => { + try { + setLoading(true); + setError(null); + + if (!scriptEditUseCase) { + throw new Error("剧本编辑用例未初始化"); + } + + await scriptEditUseCase.generateScript(prompt); + + // 更新片段列表(这里需要根据实际的流式数据处理逻辑来调整) + const slices = scriptEditUseCase.getScriptSlices(); + setScriptSlices(slices); + + } catch (error) { + console.error("AI生成剧本失败:", error); + setError(error instanceof Error ? error.message : "AI生成剧本失败"); + throw error; + } finally { + setLoading(false); + } + }, [scriptEditUseCase]); + + /** + * 应用剧本 + */ + const applyScript = useCallback(async (): Promise => { + try { + setLoading(true); + setError(null); + + if (!scriptEditUseCase) { + throw new Error("剧本编辑用例未初始化"); + } + + await scriptEditUseCase.applyScript(); + + } catch (error) { + console.error("应用剧本失败:", error); + setError(error instanceof Error ? error.message : "应用剧本失败"); + throw error; + } finally { + setLoading(false); + } + }, [scriptEditUseCase]); + + return { + // 响应式状态 + scriptText, + scriptSlices, + focusedSliceId, + focusedSlice, + scriptSliceText, + userPrompt, + loading, + error, + + // 操作方法 + fetchScriptData, + setFocusedSlice, + clearFocusedSlice, + updateScriptSliceText, + updateUserPrompt, + resetScript, + generateScript, + applyScript, + UpdateFocusedSlice + }; +}; diff --git a/app/service/Interaction/ShotService.ts b/app/service/Interaction/ShotService.ts index 855c466..477eb1c 100644 --- a/app/service/Interaction/ShotService.ts +++ b/app/service/Interaction/ShotService.ts @@ -1,16 +1,22 @@ -import { useState, useCallback, useMemo } from 'react'; -import { ShotEntity, RoleEntity, SceneEntity, ScriptSliceEntity } from '../domain/Entities'; -import { ShotItem } from '../domain/Item'; -import { ShotEditUseCase } from '../usecase/ShotEditUsecase'; +import { useState, useCallback, useMemo } from "react"; +import { + ShotEntity, + RoleEntity, + SceneEntity, + ContentItem, +} from "../domain/Entities"; +import { ScriptSlice, ScriptValueObject } from "../domain/valueObject"; +import { ShotItem } from "../domain/Item"; +import { ShotEditUseCase } from "../usecase/ShotEditUsecase"; import { getShotList, - getShotDetail, + // getShotDetail, updateShotContent, updateShotShot, getUserRoleLibrary, replaceShotRole, - getShotVideoScript -} from '@/api/video_flow'; + getShotVideoScript, +} from "@/api/video_flow"; /** * 分镜服务Hook接口 @@ -25,12 +31,12 @@ export interface UseShotService { /** 当前分镜的草图数据URL */ shotSketchData: string | null; /** 当前分镜的视频数据URL */ - shotVideoData: string | null; + shotVideoData: string[] | null; /** 用户角色库 */ userRoleLibrary: RoleEntity[]; /** 当前分镜的视频剧本片段 */ - shotVideoScript: ScriptSliceEntity[]; + shotVideoScript: ScriptSlice[]; /** 加载状态 */ loading: boolean; /** 错误信息 */ @@ -42,7 +48,9 @@ export interface UseShotService { /** 选择分镜并获取详情 */ selectShot: (shotId: string) => Promise; /** 修改分镜对话内容 */ - updateShotContent: (newContent: Array<{ roleId: string; content: string }>) => Promise; + updateShotContent: ( + newContent: Array<{ roleId: string; content: string }> + ) => Promise; /** 修改分镜镜头 */ updateShotShot: (newShot: string[]) => Promise; /** 获取用户角色库 */ @@ -59,7 +67,7 @@ export interface UseShotService { /** 重新生成分镜 */ regenerateShot: ( shotPrompt: string, - dialogueContent: string, + dialogueContent: ContentItem[], roleReplaceParams: { oldId: string; newId: string }[], sceneReplaceParams: { oldId: string; newId: string }[] ) => Promise; @@ -75,14 +83,15 @@ export const useShotService = (): UseShotService => { const [shotList, setShotList] = useState([]); const [selectedShot, setSelectedShot] = useState(null); const [shotSketchData, setShotSketchData] = useState(null); - const [shotVideoData, setShotVideoData] = useState(null); + const [shotVideoData, setShotVideoData] = useState(null); const [userRoleLibrary, setUserRoleLibrary] = useState([]); - const [shotVideoScript, setShotVideoScript] = useState([]); + const [shotVideoScript, setShotVideoScript] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - + const [projectId, setProjectId] = useState(""); // UseCase实例 - const [shotEditUseCase, setShotEditUseCase] = useState(null); + const [shotEditUseCase, setShotEditUseCase] = + useState(null); /** * 获取分镜列表 @@ -90,6 +99,7 @@ export const useShotService = (): UseShotService => { * @param projectId 项目ID */ const fetchShotList = useCallback(async (projectId: string) => { + setProjectId(projectId); try { setLoading(true); setError(null); @@ -100,7 +110,9 @@ export const useShotService = (): UseShotService => { setError(`获取分镜列表失败: ${response.message}`); } } catch (err) { - setError(`获取分镜列表失败: ${err instanceof Error ? err.message : '未知错误'}`); + setError( + `获取分镜列表失败: ${err instanceof Error ? err.message : "未知错误"}` + ); } finally { setLoading(false); } @@ -117,26 +129,28 @@ export const useShotService = (): UseShotService => { setError(null); // 获取分镜详情 - const response = await getShotDetail({ shotId }); - if (response.successful) { - const shotEntity = response.data; - const shotItem = new ShotItem(shotEntity); - setSelectedShot(shotItem); - - // 初始化UseCase - const newShotEditUseCase = new ShotEditUseCase(shotItem); - setShotEditUseCase(newShotEditUseCase); - - - - // 从分镜实体中获取草图数据和视频数据 - setShotSketchData(shotEntity.sketchUrl || null); - setShotVideoData(shotEntity.videoUrl || null); - } else { - setError(`获取分镜详情失败: ${response.message}`); + await fetchShotList(projectId); + const shotEntity = shotList.find( + (shot: ShotEntity) => shot.id === shotId + ); + if (!shotEntity) { + setError(`分镜不存在: ${shotId}`); + return; } + const shotItem = new ShotItem(shotEntity); + setSelectedShot(shotItem); + + // 初始化UseCase + const newShotEditUseCase = new ShotEditUseCase(shotItem); + setShotEditUseCase(newShotEditUseCase); + + // 从分镜实体中获取草图数据和视频数据 + setShotSketchData(shotEntity.sketchUrl || null); + setShotVideoData(shotEntity.videoUrl || null); } catch (err) { - setError(`选择分镜失败: ${err instanceof Error ? err.message : '未知错误'}`); + setError( + `选择分镜失败: ${err instanceof Error ? err.message : "未知错误"}` + ); } finally { setLoading(false); } @@ -147,54 +161,66 @@ export const useShotService = (): UseShotService => { * @description 更新分镜的对话内容,ContentItem数量和ID顺序不能变,只能修改content字段 * @param newContent 新的对话内容数组 */ - const updateShotContentHandler = useCallback(async (newContent: Array<{ roleId: string; content: string }>) => { - if (!shotEditUseCase) { - setError('分镜编辑用例未初始化'); - return; - } + const updateShotContentHandler = useCallback( + async (newContent: Array<{ roleId: string; content: string }>) => { + if (!shotEditUseCase) { + setError("分镜编辑用例未初始化"); + return; + } - try { - setLoading(true); - setError(null); - const updatedShot = await shotEditUseCase.updateShotContent(newContent); - setSelectedShot(new ShotItem(updatedShot)); - } catch (err) { - setError(`修改分镜对话内容失败: ${err instanceof Error ? err.message : '未知错误'}`); - } finally { - setLoading(false); - } - }, [shotEditUseCase]); + try { + setLoading(true); + setError(null); + const updatedShot = await shotEditUseCase.updateShotContent(newContent); + setSelectedShot(new ShotItem(updatedShot)); + } catch (err) { + setError( + `修改分镜对话内容失败: ${ + err instanceof Error ? err.message : "未知错误" + }` + ); + } finally { + setLoading(false); + } + }, + [shotEditUseCase] + ); /** * 修改分镜镜头 * @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}`); + const updateShotShotHandler = useCallback( + async (newShot: string[]) => { + if (!selectedShot) { + setError("未选择分镜"); + return; } - } catch (err) { - setError(`修改分镜镜头失败: ${err instanceof Error ? err.message : '未知错误'}`); - } finally { - setLoading(false); - } - }, [selectedShot]); + + 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] + ); /** * 获取用户角色库 @@ -211,7 +237,9 @@ export const useShotService = (): UseShotService => { setError(`获取用户角色库失败: ${response.message}`); } } catch (err) { - setError(`获取用户角色库失败: ${err instanceof Error ? err.message : '未知错误'}`); + setError( + `获取用户角色库失败: ${err instanceof Error ? err.message : "未知错误"}` + ); } finally { setLoading(false); } @@ -223,32 +251,37 @@ export const useShotService = (): UseShotService => { * @param oldRoleId 旧角色ID * @param newRoleId 新角色ID */ - const replaceShotRoleHandler = useCallback(async (oldRoleId: string, newRoleId: string) => { - if (!selectedShot) { - setError('未选择分镜'); - return; - } - - try { - setLoading(true); - setError(null); - const response = await replaceShotRole({ - shotId: selectedShot.entity.id, - oldRoleId, - newRoleId - }); - if (response.successful) { - // 重新获取分镜详情 - await selectShot(selectedShot.entity.id); - } else { - setError(`替换分镜角色失败: ${response.message}`); + const replaceShotRoleHandler = useCallback( + async (oldRoleId: string, newRoleId: string) => { + if (!selectedShot) { + setError("未选择分镜"); + return; } - } catch (err) { - setError(`替换分镜角色失败: ${err instanceof Error ? err.message : '未知错误'}`); - } finally { - setLoading(false); - } - }, [selectedShot, selectShot]); + + try { + setLoading(true); + setError(null); + const response = await replaceShotRole({ + shotId: selectedShot.entity.id, + oldRoleId, + newRoleId, + }); + if (response.successful) { + // 重新获取分镜详情 + await selectShot(selectedShot.entity.id); + } else { + setError(`替换分镜角色失败: ${response.message}`); + } + } catch (err) { + setError( + `替换分镜角色失败: ${err instanceof Error ? err.message : "未知错误"}` + ); + } finally { + setLoading(false); + } + }, + [selectedShot, selectShot] + ); /** * 获取分镜视频剧本内容 @@ -256,21 +289,28 @@ export const useShotService = (): UseShotService => { */ const fetchShotVideoScript = useCallback(async () => { if (!selectedShot) { - setError('未选择分镜'); + setError("未选择分镜"); return; } try { setLoading(true); setError(null); - const response = await getShotVideoScript({ shotId: selectedShot.entity.id }); + const response = await getShotVideoScript({ + shotId: selectedShot.entity.id, + }); if (response.successful) { - setShotVideoScript(response.data); + const script = new ScriptValueObject(response.data); + setShotVideoScript(script.scriptSlices); } else { setError(`获取分镜视频剧本失败: ${response.message}`); } } catch (err) { - setError(`获取分镜视频剧本失败: ${err instanceof Error ? err.message : '未知错误'}`); + setError( + `获取分镜视频剧本失败: ${ + err instanceof Error ? err.message : "未知错误" + }` + ); } finally { setLoading(false); } @@ -283,7 +323,7 @@ export const useShotService = (): UseShotService => { */ const getShotRoles = useCallback(async (): Promise => { if (!shotEditUseCase) { - throw new Error('分镜编辑用例未初始化'); + throw new Error("分镜编辑用例未初始化"); } return await shotEditUseCase.getShotRoles(); }, [shotEditUseCase]); @@ -295,13 +335,11 @@ export const useShotService = (): UseShotService => { */ const getShotScenes = useCallback(async (): Promise => { if (!shotEditUseCase) { - throw new Error('分镜编辑用例未初始化'); + throw new Error("分镜编辑用例未初始化"); } return await shotEditUseCase.getShotScenes(); }, [shotEditUseCase]); - - /** * 重新生成分镜 * @description 使用镜头、对话内容、角色ID替换参数、场景ID替换参数重新生成分镜 @@ -310,33 +348,38 @@ export const useShotService = (): UseShotService => { * @param roleReplaceParams 角色ID替换参数,格式为{oldId:string,newId:string}[] * @param sceneReplaceParams 场景ID替换参数,格式为{oldId:string,newId:string}[] */ - const regenerateShot = useCallback(async ( - shotPrompt: string, - dialogueContent: string, - roleReplaceParams: { oldId: string; newId: string }[], - sceneReplaceParams: { oldId: string; newId: string }[] - ) => { - if (!shotEditUseCase) { - setError('分镜编辑用例未初始化'); - return; - } + const regenerateShot = useCallback( + async ( + shotPrompt: string, + dialogueContent: ContentItem[], + roleReplaceParams: { oldId: string; newId: string }[], + sceneReplaceParams: { oldId: string; newId: string }[] + ) => { + if (!shotEditUseCase) { + setError("分镜编辑用例未初始化"); + return; + } - try { - setLoading(true); - setError(null); - const updatedShot = await shotEditUseCase.regenerateShot( - shotPrompt, - dialogueContent, - roleReplaceParams, - sceneReplaceParams - ); - setSelectedShot(new ShotItem(updatedShot)); - } catch (err) { - setError(`重新生成分镜失败: ${err instanceof Error ? err.message : '未知错误'}`); - } finally { - setLoading(false); - } - }, [shotEditUseCase]); + try { + setLoading(true); + setError(null); + const updatedShot = await shotEditUseCase.regenerateShot( + shotPrompt, + dialogueContent, + roleReplaceParams, + sceneReplaceParams + ); + setSelectedShot(new ShotItem(updatedShot)); + } catch (err) { + setError( + `重新生成分镜失败: ${err instanceof Error ? err.message : "未知错误"}` + ); + } finally { + setLoading(false); + } + }, + [shotEditUseCase] + ); return { // 响应式状态 - 用于UI组件订阅和渲染 diff --git a/app/service/domain/Entities.ts b/app/service/domain/Entities.ts index 534b2c1..1805af7 100644 --- a/app/service/domain/Entities.ts +++ b/app/service/domain/Entities.ts @@ -65,13 +65,15 @@ export interface SceneEntity extends BaseEntity { /** 场景提示词Id */ generateTextId: string; } + /**对话内容项 */ -interface ContentItem { +export interface ContentItem { /** 角色ID */ roleId: string; /** 对话内容 */ content: string; } +/**分镜进度 */ export enum ShotStatus { /** 草稿加载中 */ sketchLoading = 0, @@ -89,7 +91,7 @@ export interface ShotEntity extends BaseEntity { /**分镜草图Url */ sketchUrl: string; /**分镜视频Url */ - videoUrl: string; + videoUrl: string[]; /**分镜状态 */ status: ShotStatus; /**角色ID列表 */ @@ -100,30 +102,8 @@ export interface ShotEntity extends BaseEntity { content: ContentItem[]; /**镜头项 */ shot: string[]; - /**分镜剧本 */ + /**分镜剧本Id */ scriptId: string; } -/**不同类型 将有不同元数据 */ -export enum ScriptSliceType { - /** 文本 */ - text = 'text', - /** 高亮 */ - highlight = 'highlight', - /**角色 */ - role = 'role', - /** 场景 */ - scene = 'scene', -} -/** - * 剧本片段实体接口 - */ -export interface ScriptSliceEntity extends BaseEntity { - /** 类型 */ - type: ScriptSliceType; - /** 剧本内容 */ - text: string; - /** 元数据 */ - metaData: any; -} diff --git a/app/service/domain/valueObject.ts b/app/service/domain/valueObject.ts new file mode 100644 index 0000000..7ffe47f --- /dev/null +++ b/app/service/domain/valueObject.ts @@ -0,0 +1,78 @@ +/**不同类型 将有不同元数据 */ + +export enum ScriptSliceType { + /** 文本 */ + text = "text", + /**角色 */ + role = "role", + /** 场景 */ + scene = "scene", +} +/** + * 剧本片段值对象接口 + */ +export interface ScriptSlice { + /**唯一标识符 */ + readonly id: string; + /** 类型 */ + type: ScriptSliceType; + /** 剧本内容 */ + text: string; + /** 元数据 */ + metaData: any; +} + +/** + * @description: 剧本 值对象,将剧本文本转换为剧本对象 + * @return {*} + */ +export class ScriptValueObject { + scriptSlices: ScriptSlice[] = []; + + /** + * @description: 构造函数,初始化剧本 + * @param scriptText 剧本文本字符串 + */ + constructor(scriptText?: string) { + if (scriptText) { + this.parseFromString(scriptText); + } + } + + /** + * @description: 从字符串解析剧本片段 + * @param scriptText 剧本文本字符串 + */ + parseFromString(scriptText: string): void { + // TODO: 实现字符串解析逻辑 + } + + /** + * @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(""); + } + +} diff --git a/app/service/usecase/ScriptEditUseCase.ts b/app/service/usecase/ScriptEditUseCase.ts new file mode 100644 index 0000000..8183c77 --- /dev/null +++ b/app/service/usecase/ScriptEditUseCase.ts @@ -0,0 +1,124 @@ +import { ScriptSlice, ScriptValueObject } from "../domain/valueObject"; +import { generateScriptStream, applyScriptToShot } from "@/api/video_flow"; + +export class ScriptEditUseCase { + loading: boolean = false; + private scriptValueObject: ScriptValueObject; + + constructor(script: string) { + this.scriptValueObject = new ScriptValueObject(script); + } + + /** + * @description: AI生成剧本方法 + * @param prompt 剧本提示词 + * @returns Promise + */ + async generateScript(prompt: string): Promise { + try { + this.loading = true; + + // 使用API接口生成剧本 + const response = await generateScriptStream({ + prompt, + }); + + if (!response.successful) { + throw new Error(response.message || "AI生成剧本失败"); + } + + // 使用for await处理流式数据 + for await (const data of response.data) { + // TODO: 根据流式数据更新剧本片段 + // 这里需要根据实际的流式数据格式来处理 + // 可能需要将流式数据转换为ScriptSlice并添加到scriptValueObject中 + } + } catch (error) { + console.error("AI生成剧本出错:", error); + throw error; + } finally { + this.loading = false; + } + } + + /** + * @description: 应用剧本方法 + * @returns Promise + */ + async applyScript(): Promise { + try { + this.loading = true; + + // 调用应用剧本接口 + const response = await applyScriptToShot({ + script: this.scriptValueObject.toString(), + }); + + if (!response.successful) { + throw new Error(response.message || "应用剧本失败"); + } + + console.log("剧本应用成功"); + } catch (error) { + console.error("应用剧本失败:", error); + throw error; + } finally { + this.loading = false; + } + } + + /** + * @description: 获取当前剧本片段列表 + * @returns ScriptSlice[] + */ + getScriptSlices(): ScriptSlice[] { + return this.scriptValueObject.scriptSlices; + } + + /** + * @description: 获取加载状态 + * @returns boolean + */ + isLoading(): boolean { + return this.loading; + } + + /** + * @description: 更新剧本 + * @param scriptText 剧本文本字符串 + */ + updateScript(scriptText: string): void { + this.scriptValueObject = new ScriptValueObject(scriptText); + } + + /** + * @description: 将当前剧本片段转换为字符串 + * @returns string + */ + toString(): string { + 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 0397ccc..7981b5f 100644 --- a/app/service/usecase/ShotEditUsecase.ts +++ b/app/service/usecase/ShotEditUsecase.ts @@ -1,4 +1,4 @@ -import { ShotEntity, RoleEntity, SceneEntity, AITextEntity, TagEntity } from '../domain/Entities'; +import { ShotEntity, RoleEntity, SceneEntity, AITextEntity, TagEntity, ContentItem } from '../domain/Entities'; import { ShotItem, RoleItem, SceneItem, TextItem, TagItem } from '../domain/Item'; import { getShotRoles, @@ -116,7 +116,7 @@ export class ShotEditUseCase { */ async regenerateShot( shotPrompt: string, - dialogueContent: string, + dialogueContent: ContentItem[], roleReplaceParams: { oldId: string; newId: string }[], sceneReplaceParams: { oldId: string; newId: string }[] ): Promise { diff --git a/app/service/usecase/index.ts b/app/service/usecase/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/utils/tools.ts b/utils/tools.ts new file mode 100644 index 0000000..111410f --- /dev/null +++ b/utils/tools.ts @@ -0,0 +1,11 @@ +import { ScriptSlice, ScriptSliceType } from "@/app/service/domain/valueObject"; + +export function parseScriptEntity(text: string):ScriptSlice { + const scriptSlice:ScriptSlice={ + type:ScriptSliceType.text, + text:text, + metaData:{} + + } + return scriptSlice; +}