From e983a10037b47fc67543b4c2208040a19fe04ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=B7=E9=BE=99?= Date: Sun, 3 Aug 2025 15:07:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/video_flow.ts | 45 +++- app/service/Interaction/ScriptService.ts | 301 +++++++++-------------- app/service/Interaction/ShotService.ts | 3 +- app/service/domain/Entities.ts | 14 +- app/service/domain/valueObject.ts | 17 +- app/service/test/Script.test.ts | 2 +- app/service/usecase/ScriptEditUseCase.ts | 36 ++- tsconfig.json | 3 +- utils/tools.ts | 33 ++- 9 files changed, 250 insertions(+), 204 deletions(-) diff --git a/api/video_flow.ts b/api/video_flow.ts index 181fc55..8fb0305 100644 --- a/api/video_flow.ts +++ b/api/video_flow.ts @@ -2,8 +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, ContentItem } from '@/app/service/domain/Entities'; -import { ScriptSlice } from "@/app/service/domain/valueObject"; +import { AITextEntity, RoleEntity, SceneEntity, ShotEntity, TagEntity } from '@/app/service/domain/Entities'; +import { ContentItem, LensType, ScriptSlice } from "@/app/service/domain/valueObject"; // API 响应类型 interface BaseApiResponse { @@ -471,7 +471,7 @@ export const regenerateShot = async (request: { /** 分镜ID */ shotId?: string; /** 镜头描述 */ - shotPrompt?: string; + shotPrompt?: LensType[]; /** 对话内容 */ dialogueContent?: ContentItem[]; /** 角色ID替换参数,格式为{oldId:string,newId:string}[] */ @@ -575,7 +575,7 @@ export const getShotVideoScript = async (request: { */ export const generateScriptStream = async (request: { /** 剧本提示词 */ - prompt: string; + text: string; }) => { return post>('/text_to_script/generate_script_stream', request,{ responseType: 'stream', @@ -588,8 +588,10 @@ export const generateScriptStream = async (request: { * @returns Promise> */ export const applyScriptToShot = async (request: { + /** 项目ID */ + projectId: string; /** 剧本*/ - script: string; + scriptText: string; }): Promise> => { return post>('/movie/apply_script_to_shot', request); }; @@ -612,3 +614,36 @@ export const getProjectScript = async (request: { scriptText: string; }>>('/movie/get_project_script', request); }; + +/** + * 保存剧本 + * @param request 保存剧本请求参数 + * @returns Promise> + */ +export const saveScript = async (request: { + /** 项目ID */ + projectId: string; + /** 剧本文本 */ + scriptText: string; +}): Promise> => { + return post>('/movie/save_script', request); +}; + +/** + * 创建项目 + * @param request 创建项目请求参数 + * @returns Promise> + */ +export const createProject = async (request: { + /** 用户提示词 */ + userPrompt: string; + /** 剧本内容 */ + scriptContent: string; +}): Promise> => { + return post>('/movie/create_project', request); +}; diff --git a/app/service/Interaction/ScriptService.ts b/app/service/Interaction/ScriptService.ts index 073d8ef..3406049 100644 --- a/app/service/Interaction/ScriptService.ts +++ b/app/service/Interaction/ScriptService.ts @@ -1,7 +1,8 @@ import { useState, useCallback, useMemo } from "react"; import { ScriptSlice } from "../domain/valueObject"; import { ScriptEditUseCase } from "../usecase/ScriptEditUseCase"; -import { getProjectScript } from "../../../api/video_flow"; +import { getProjectScript, saveScript as saveScriptAPI, createProject as createProjectAPI } from "../../../api/video_flow"; +import { throttle } from "@/utils/tools"; /** * 剧本服务Hook接口 @@ -13,113 +14,83 @@ export interface UseScriptService { scriptText: string; /** 剧本片段列表 */ scriptSlices: ScriptSlice[]; - /** 当前聚焦的剧本片段ID */ - focusedSliceId: string; - /** 当前聚焦的剧本片段 */ - focusedSlice: ScriptSlice | null; - /** 当前聚焦的剧本片段文本 */ - scriptSliceText: string; /** 用户提示词(可编辑) */ userPrompt: string; /** 加载状态 */ loading: boolean; - /** 错误信息 */ - error: string | null; + /** 项目ID */ + projectId: string; // 操作方法 - /** 获取剧本数据(用户提示词) */ + /** 获取根据用户想法调用接口AI生成剧本(用户提示词) */ fetchScriptData: (prompt: string) => Promise; /** 根据项目ID获取已存在的剧本数据 */ fetchProjectScript: (projectId: 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; + /** 中断剧本生成 */ + abortGenerateScript: () => void; + /** 保存剧本 */ + saveScript: () => Promise; + /** 创建项目 */ + createProject: () => Promise; } /** * 剧本服务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); - + const [projectId, setProjectId] = useState(""); // 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]); - - /** - * 获取剧本数据(用户提示词) + * 初始化,ai生成剧本(用户提示词) * @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); + await newScriptEditUseCase.generateScript(prompt,throttle((newContent)=>{ + // 获取生成的剧本文本 + const generatedScriptText = newScriptEditUseCase.toString(); + setScriptText(generatedScriptText); + // 获取剧本片段列表 + const slices = newScriptEditUseCase.getScriptSlices(); + setScriptSlices(slices); - // 获取生成的剧本文本 - const generatedScriptText = newScriptEditUseCase.toString(); - setScriptText(generatedScriptText); - - // 获取剧本片段列表 - const slices = newScriptEditUseCase.getScriptSlices(); - setScriptSlices(slices); + // 保存初始剧本文本(只在第一次获取时保存) + if (!initialScriptText) { + setInitialScriptText(generatedScriptText); + } + })); } catch (error) { console.error('获取剧本数据失败:', error); - setError(error instanceof Error ? error.message : '获取剧本数据失败'); throw error; } finally { setLoading(false); @@ -133,13 +104,12 @@ export const useScriptService = (): UseScriptService => { const fetchProjectScript = useCallback(async (projectId: string): Promise => { try { setLoading(true); - setError(null); - // 清空当前状态 setScriptText(""); setScriptSlices([]); - setFocusedSliceId(""); - setScriptSliceText(""); + + // 设置项目ID + setProjectId(projectId); // 调用API获取项目剧本数据 const response = await getProjectScript({ projectId }); @@ -153,9 +123,9 @@ export const useScriptService = (): UseScriptService => { // 更新用户提示词状态 setUserPrompt(prompt); - // 保存初始提示词(只在第一次获取时保存) + // 保存初始剧本文本(只在第一次获取时保存) if (!initialScriptText) { - setInitialScriptText(prompt); + setInitialScriptText(scriptText); } // 创建新的剧本编辑用例并初始化数据 @@ -171,84 +141,12 @@ export const useScriptService = (): UseScriptService => { } 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 新的用户提示词 @@ -261,41 +159,16 @@ export const useScriptService = (): UseScriptService => { * 重置剧本内容到初始状态 */ const resetScript = useCallback((): void => { - if (initialScriptText) { - // 重新调用AI生成剧本(fetchScriptData会自动清空状态) - fetchScriptData(initialScriptText); + if (initialScriptText && scriptEditUseCase) { + // 重置剧本文本到初始状态 + setScriptText(initialScriptText); + // 更新现有剧本编辑用例的数据 + scriptEditUseCase.updateScript(initialScriptText); + // 从UseCase获取解析后的剧本片段 + const scriptSlices = scriptEditUseCase.getScriptSlices(); + setScriptSlices(scriptSlices); } - }, [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]); + }, [initialScriptText, scriptEditUseCase]); /** * 应用剧本 @@ -303,44 +176,114 @@ export const useScriptService = (): UseScriptService => { const applyScript = useCallback(async (): Promise => { try { setLoading(true); - setError(null); if (!scriptEditUseCase) { throw new Error("剧本编辑用例未初始化"); } - await scriptEditUseCase.applyScript(); + await scriptEditUseCase.applyScript(projectId); } catch (error) { console.error("应用剧本失败:", error); - setError(error instanceof Error ? error.message : "应用剧本失败"); throw error; } finally { setLoading(false); } + }, [scriptEditUseCase, projectId]); + + /** + * 中断剧本生成 + */ + const abortGenerateScript = useCallback((): void => { + if (scriptEditUseCase) { + scriptEditUseCase.abortGenerateScript(); + setLoading(false); + } }, [scriptEditUseCase]); + /** + * 保存剧本 + */ + const saveScript = useCallback(async (): Promise => { + try { + setLoading(true); + + if (!projectId) { + throw new Error("项目ID未设置"); + } + + if (!scriptEditUseCase) { + throw new Error("剧本编辑用例未初始化"); + } + + // 调用保存剧本接口 + const scriptText = scriptEditUseCase.toString(); + const response = await saveScriptAPI({ projectId, scriptText }); + + if (!response.successful) { + throw new Error(response.message || '保存剧本失败'); + } + + console.log("剧本保存成功"); + + } catch (error) { + console.error("保存剧本失败:", error); + throw error; + } finally { + setLoading(false); + } + }, [projectId, scriptEditUseCase]); + + /** + * 创建项目 + * @throws {Error} - 创建项目失败时抛出异常 + */ + const createProject = useCallback(async (): Promise => { + try { + setLoading(true); + + // 直接使用当前state中的userPrompt和scriptText + const currentUserPrompt = userPrompt; + const currentScriptContent = scriptText; + + const response = await createProjectAPI({ + userPrompt: currentUserPrompt, + scriptContent: currentScriptContent + }); + + if (!response.successful) { + throw new Error(response.message || '创建项目失败'); + } + + const { projectId: newProjectId } = response.data; + setProjectId(newProjectId); + + console.log("项目创建成功"); + + } catch (error) { + console.error("创建项目失败:", error); + throw error; + } finally { + setLoading(false); + } + }, [userPrompt, scriptText]); + return { // 响应式状态 scriptText, scriptSlices, - focusedSliceId, - focusedSlice, - scriptSliceText, userPrompt, loading, - error, + projectId, // 操作方法 fetchScriptData, fetchProjectScript, - setFocusedSlice, - clearFocusedSlice, - updateScriptSliceText, updateUserPrompt, resetScript, - generateScript, applyScript, - UpdateFocusedSlice + abortGenerateScript, + saveScript, + createProject, }; }; diff --git a/app/service/Interaction/ShotService.ts b/app/service/Interaction/ShotService.ts index 477eb1c..2bf722f 100644 --- a/app/service/Interaction/ShotService.ts +++ b/app/service/Interaction/ShotService.ts @@ -3,9 +3,8 @@ import { ShotEntity, RoleEntity, SceneEntity, - ContentItem, } from "../domain/Entities"; -import { ScriptSlice, ScriptValueObject } from "../domain/valueObject"; +import { ContentItem, ScriptSlice, ScriptValueObject } from "../domain/valueObject"; import { ShotItem } from "../domain/Item"; import { ShotEditUseCase } from "../usecase/ShotEditUsecase"; import { diff --git a/app/service/domain/Entities.ts b/app/service/domain/Entities.ts index 1805af7..57948e5 100644 --- a/app/service/domain/Entities.ts +++ b/app/service/domain/Entities.ts @@ -3,6 +3,8 @@ * 所有实体都应该实现这些基础接口 */ +import { ContentItem, LensType } from "./valueObject"; + /** * 基础实体接口 */ @@ -66,13 +68,7 @@ export interface SceneEntity extends BaseEntity { generateTextId: string; } -/**对话内容项 */ -export interface ContentItem { - /** 角色ID */ - roleId: string; - /** 对话内容 */ - content: string; -} + /**分镜进度 */ export enum ShotStatus { /** 草稿加载中 */ @@ -82,6 +78,8 @@ export enum ShotStatus { /** 完成 */ finished = 2, } + + /** * 分镜实体接口 */ @@ -101,7 +99,7 @@ export interface ShotEntity extends BaseEntity { /**对话内容 */ content: ContentItem[]; /**镜头项 */ - shot: string[]; + lens: LensType[]; /**分镜剧本Id */ scriptId: string; } diff --git a/app/service/domain/valueObject.ts b/app/service/domain/valueObject.ts index 7ffe47f..8b2ff94 100644 --- a/app/service/domain/valueObject.ts +++ b/app/service/domain/valueObject.ts @@ -21,7 +21,22 @@ export interface ScriptSlice { /** 元数据 */ metaData: any; } - +/**对话内容项 */ +export interface ContentItem { + /** 角色ID */ + roleId: string; + /** 对话内容 */ + content: string; +} +/**镜头值对象 */ +export interface LensType { + /** 镜头名称 */ + name: string; + /** 镜头描述 */ + content: string; + /**运镜描述 */ + movement: string; +} /** * @description: 剧本 值对象,将剧本文本转换为剧本对象 * @return {*} diff --git a/app/service/test/Script.test.ts b/app/service/test/Script.test.ts index b89797c..1a5443a 100644 --- a/app/service/test/Script.test.ts +++ b/app/service/test/Script.test.ts @@ -7,7 +7,7 @@ describe('ScriptService 业务逻辑测试', () => { * 测试 generateScriptStream 流式接口,持续监听数据直到流结束 */ const stream = await generateScriptStream({ - prompt: '一个年轻人在咖啡店里等待他的约会对象,心情紧张地摆弄着手机。' + text: '一个年轻人在咖啡店里等待他的约会对象,心情紧张地摆弄着手机。' }); let allData = ''; diff --git a/app/service/usecase/ScriptEditUseCase.ts b/app/service/usecase/ScriptEditUseCase.ts index 8183c77..b9340d0 100644 --- a/app/service/usecase/ScriptEditUseCase.ts +++ b/app/service/usecase/ScriptEditUseCase.ts @@ -4,6 +4,7 @@ import { generateScriptStream, applyScriptToShot } from "@/api/video_flow"; export class ScriptEditUseCase { loading: boolean = false; private scriptValueObject: ScriptValueObject; + private abortController: AbortController | null = null; constructor(script: string) { this.scriptValueObject = new ScriptValueObject(script); @@ -12,15 +13,19 @@ export class ScriptEditUseCase { /** * @description: AI生成剧本方法 * @param prompt 剧本提示词 + * @param stream_callback 流式数据回调函数 * @returns Promise */ - async generateScript(prompt: string): Promise { + async generateScript(prompt: string, stream_callback?: (data: any) => void): Promise { try { this.loading = true; + // 创建新的中断控制器 + this.abortController = new AbortController(); + // 使用API接口生成剧本 const response = await generateScriptStream({ - prompt, + text: prompt, }); if (!response.successful) { @@ -29,15 +34,37 @@ export class ScriptEditUseCase { // 使用for await处理流式数据 for await (const data of response.data) { + // 检查是否被中断 + if (this.abortController.signal.aborted) { + console.log("剧本生成被中断"); + break; + } + // TODO: 根据流式数据更新剧本片段 // 这里需要根据实际的流式数据格式来处理 // 可能需要将流式数据转换为ScriptSlice并添加到scriptValueObject中 + stream_callback?.(data); } } catch (error) { + if (this.abortController?.signal.aborted) { + console.log("剧本生成被中断"); + return; + } console.error("AI生成剧本出错:", error); throw error; } finally { this.loading = false; + this.abortController = null; + } + } + + /** + * @description: 中断剧本生成 + */ + abortGenerateScript(): void { + if (this.abortController) { + this.abortController.abort(); + this.loading = false; } } @@ -45,13 +72,14 @@ export class ScriptEditUseCase { * @description: 应用剧本方法 * @returns Promise */ - async applyScript(): Promise { + async applyScript(projectId: string): Promise { try { this.loading = true; // 调用应用剧本接口 const response = await applyScriptToShot({ - script: this.scriptValueObject.toString(), + projectId, + scriptText: this.scriptValueObject.toString(), }); if (!response.successful) { diff --git a/tsconfig.json b/tsconfig.json index c714696..b4c7e29 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,8 @@ ], "paths": { "@/*": ["./*"] - } + }, + "maxNodeModuleJsDepth":0 }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] diff --git a/utils/tools.ts b/utils/tools.ts index 111410f..29034b5 100644 --- a/utils/tools.ts +++ b/utils/tools.ts @@ -2,10 +2,37 @@ import { ScriptSlice, ScriptSliceType } from "@/app/service/domain/valueObject"; export function parseScriptEntity(text: string):ScriptSlice { const scriptSlice:ScriptSlice={ - type:ScriptSliceType.text, - text:text, - metaData:{} + // 生成唯一ID,单次使用即可 + id: `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`, + type: ScriptSliceType.text, + text: text, + metaData: {} } return scriptSlice; } + + +/** + * @description 节流函数,限制函数在指定时间间隔内只执行一次 + * @param {Function} func - 需要被节流的函数 + * @param {number} delay - 节流时间间隔(毫秒) + * @returns {Function} - 节流后的新函数 + * @throws {Error} - 如果参数类型不正确 + * @example + * const throttledFn = throttle(() => { console.log('触发'); }, 1000); + * window.addEventListener('resize', throttledFn); + */ +export function throttle any>(func: T, delay: number=100): (...args: Parameters) => void { + if (typeof delay !== 'number' || delay < 0) { + throw new Error('throttle: 第二个参数必须是非负数'); + } + let lastCall = 0; + return (...args: Parameters) => { + const now = Date.now(); + if (now - lastCall >= delay) { + lastCall = now; + func(...args); + } + }; +}