From 693f7599f1a2ce724d810db9db559cfdd0037060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=B7=E9=BE=99?= Date: Wed, 6 Aug 2025 20:08:42 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=89=A7=E6=9C=AC=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81=E5=89=A7?= =?UTF-8?q?=E6=9C=AC=E6=B5=81=E5=BC=8F=E5=A4=84=E7=90=86=E5=92=8C=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=AE=A1=E7=90=86=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=9C=8D=E5=8A=A1=E4=BB=A5=E6=95=B4=E5=90=88=E6=9A=82?= =?UTF-8?q?=E5=81=9C=E5=92=8C=E6=81=A2=E5=A4=8D=E8=AE=A1=E5=88=92=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E5=89=A7=E6=9C=AC=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=80=BB=E8=BE=91=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/video_flow.ts | 78 +++++++++- app/service/Interaction/ScriptService.ts | 182 +++++++++++++++++++---- app/service/usecase/ScriptEditUseCase.ts | 145 ++++++++++++++---- 3 files changed, 345 insertions(+), 60 deletions(-) diff --git a/api/video_flow.ts b/api/video_flow.ts index 3a2a0d4..7b25f46 100644 --- a/api/video_flow.ts +++ b/api/video_flow.ts @@ -653,14 +653,6 @@ export const saveScript = async (request: { project_id: string; /** 剧本文本 */ generated_script: string; - /** 剧情梗概 */ - synopsis: string; - /** 剧情类型 */ - categories: string[]; - /** 主角 */ - protagonist: string; - /** 框架 */ - framework: string; }): Promise> => { return post>("/movie/update_generated_script", request); }; @@ -680,6 +672,24 @@ export const abortVideoTask = async (request: { return post("/api/v1/video/abort", request); }; +export const pausePlanFlow = async (request: { + /** 项目ID */ + project_id: string; + /** 计划ID */ + plan_id: string; +}): Promise> => { + return post("/api/v1/video/pause", request); +}; + +export const resumePlanFlow = async (request: { + /** 项目ID */ + project_id: string; + /** 计划ID */ + plan_id: string; +}): Promise> => { + return post("/api/v1/video/resume", request); +}; + export const createMovieProjectV1 = async (request: { /** 剧本内容 */ script: string; @@ -701,3 +711,55 @@ export const createMovieProjectV1 = async (request: { status: string; }>>("/movie/create_movie_project_v1", request); }; + +/** + * 增强剧本流式接口 + * @param request - 增强剧本请求参数 + * @param onData - 流式数据回调 + * @returns Promise + */ +export const enhanceScriptStream = ( + request: { + /** 原始剧本文本 */ + original_script: string; + /** 故事梗概 */ + synopsis?: string; + /** 故事分类 */ + categories?: string[]; + /** 主角名称 */ + protagonist?: string; + /** 激励事件 */ + incitingIncident?: string; + /** 问题与新目标 */ + problem?: string; + /** 冲突与障碍 */ + conflict?: string; + /** 赌注 */ + stakes?: string; + /** 人物弧线完成 */ + characterArc?: string; + /** AI优化要求 */ + aiOptimizing: string; + }, + onData: (data: any) => void +): Promise => { + return new Promise((resolve, reject) => { + streamJsonPost("/movie/enhance_script", request, (data) => { + switch(data.status) { + case 'streaming': + onData(data.content); + break; + + case 'completed': + console.log('剧本增强完成:', data.message); + resolve() + return; + + case 'error': + console.error('剧本增强失败:', data.message); + reject(data.message) + return; + } + }) + }) +}; diff --git a/app/service/Interaction/ScriptService.ts b/app/service/Interaction/ScriptService.ts index db1dbaf..d6e09a8 100644 --- a/app/service/Interaction/ScriptService.ts +++ b/app/service/Interaction/ScriptService.ts @@ -1,6 +1,6 @@ import { useState, useCallback, Dispatch, SetStateAction } from "react"; -import { ScriptEditUseCase } from "../usecase/ScriptEditUseCase"; -import { getProjectScript, abortVideoTask } from "../../../api/video_flow"; +import { ScriptEditUseCase,ScriptEditKey } from "../usecase/ScriptEditUseCase"; +import { getProjectScript, abortVideoTask, pausePlanFlow, resumePlanFlow } from "../../../api/video_flow"; /** * 剧本服务Hook接口 @@ -30,18 +30,25 @@ export interface UseScriptService { projectId: string; /** 计划ID */ planId: string; - + /** AI优化要求 */ + aiOptimizing: string; // 操作方法 /** 根据用户想法生成剧本并自动创建项目 */ generateScriptFromIdea: (idea: string) => Promise; /** 根据项目ID初始化已有剧本 */ initializeFromProject: (projectId: string) => Promise; /** 修改剧本 */ - updateScript: (scriptText: string) => void; + updateScript: (scriptText: string) => Promise; /** 应用剧本到视频生成流程 */ applyScript: () => Promise; /** 中断视频任务 */ abortVideoTask: () => Promise; + /** 聚焦处理函数 */ + focusHandler: (field: 'synopsis' | 'categories' | 'protagonist' | 'incitingIncident' | 'problem' | 'conflict' | 'stakes' | 'characterArc') => Promise; + /** 增强剧本 */ + enhanceScript: () => Promise; + /** 设置AI优化要求 */ + setAiOptimizing: Dispatch>; // 修改字段的set函数 /** 设置故事梗概 */ @@ -81,6 +88,8 @@ export const useScriptService = (): UseScriptService => { const [characterArc, setCharacterArc] = useState(""); const [projectId, setProjectId] = useState(""); const [planId, setPlanId] = useState(""); + const [aiOptimizing, setAiOptimizing] = useState(""); + const [focusedField, setFocusedField] = useState(""); // UseCase实例 const [scriptEditUseCase, setScriptEditUseCase] = useState(null); @@ -180,22 +189,16 @@ export const useScriptService = (): UseScriptService => { * 修改剧本 * @param scriptText 新的剧本文本 */ - const updateScript = useCallback((scriptText: string): void => { + const updateScript = useCallback(async (): Promise => { if (scriptEditUseCase) { - scriptEditUseCase.updateScript(scriptText); - // 更新解析后的故事详情 const storyDetails = scriptEditUseCase.getStoryDetails(); - setSynopsis(storyDetails.synopsis || ""); - setCategories(storyDetails.categories || []); - setProtagonist(storyDetails.protagonist || ""); - setIncitingIncident(storyDetails.incitingIncident || ""); - setProblem(storyDetails.problem || ""); - setConflict(storyDetails.conflict || ""); - setStakes(storyDetails.stakes || ""); - setCharacterArc(storyDetails.characterArc || ""); + // 如果有项目ID,则保存剧本 + if (projectId) { + await scriptEditUseCase.saveScript(projectId); + } } - }, [scriptEditUseCase]); + }, [scriptEditUseCase, projectId]); /** * 应用剧本到视频生成流程 @@ -250,6 +253,130 @@ export const useScriptService = (): UseScriptService => { } }, [projectId, planId]); + // 封装的setter函数,同时更新hook状态和scriptEditUseCase中的值对象 + const setSynopsisWrapper = useCallback((value: SetStateAction) => { + const newValue = typeof value === 'function' ? value(synopsis) : value; + setSynopsis(newValue); + if (scriptEditUseCase) { + scriptEditUseCase.updateStoryField('synopsis', newValue); + } + }, [synopsis, scriptEditUseCase]); + + const setCategoriesWrapper = useCallback((value: SetStateAction) => { + const newValue = typeof value === 'function' ? value(categories) : value; + setCategories(newValue); + if (scriptEditUseCase) { + scriptEditUseCase.updateStoryField('categories', newValue); + } + }, [categories, scriptEditUseCase]); + + const setProtagonistWrapper = useCallback((value: SetStateAction) => { + const newValue = typeof value === 'function' ? value(protagonist) : value; + setProtagonist(newValue); + if (scriptEditUseCase) { + scriptEditUseCase.updateStoryField('protagonist', newValue); + } + }, [protagonist, scriptEditUseCase]); + + const setIncitingIncidentWrapper = useCallback((value: SetStateAction) => { + const newValue = typeof value === 'function' ? value(incitingIncident) : value; + setIncitingIncident(newValue); + if (scriptEditUseCase) { + scriptEditUseCase.updateStoryField('incitingIncident', newValue); + } + }, [incitingIncident, scriptEditUseCase]); + + const setProblemWrapper = useCallback((value: SetStateAction) => { + const newValue = typeof value === 'function' ? value(problem) : value; + setProblem(newValue); + if (scriptEditUseCase) { + scriptEditUseCase.updateStoryField('problem', newValue); + } + }, [problem, scriptEditUseCase]); + + const setConflictWrapper = useCallback((value: SetStateAction) => { + const newValue = typeof value === 'function' ? value(conflict) : value; + setConflict(newValue); + if (scriptEditUseCase) { + scriptEditUseCase.updateStoryField('conflict', newValue); + } + }, [conflict, scriptEditUseCase]); + + const setStakesWrapper = useCallback((value: SetStateAction) => { + const newValue = typeof value === 'function' ? value(stakes) : value; + setStakes(newValue); + if (scriptEditUseCase) { + scriptEditUseCase.updateStoryField('stakes', newValue); + } + }, [stakes, scriptEditUseCase]); + + const setCharacterArcWrapper = useCallback((value: SetStateAction) => { + const newValue = typeof value === 'function' ? value(characterArc) : value; + setCharacterArc(newValue); + if (scriptEditUseCase) { + scriptEditUseCase.updateStoryField('characterArc', newValue); + } + }, [characterArc, scriptEditUseCase]); + + /** + * 聚焦处理函数 + */ + const focusHandler = useCallback(async (field: ScriptEditKey): Promise => { + try { + // 如果当前已经有聚焦的字段,先处理暂停/继续逻辑 + + // 设置新的聚焦字段 + setFocusedField(field); + + } catch (error) { + console.error("聚焦处理失败:", error); + throw error; + } + }, []); + + /** + * 增强剧本 + */ + const enhanceScript = useCallback(async (): Promise => { + try { + setLoading(true); + + if (!scriptEditUseCase) { + throw new Error("剧本编辑用例未初始化"); + } + + // 调用增强剧本方法 + await scriptEditUseCase.enhanceScript( + synopsis, + focusedField as ScriptEditKey, + aiOptimizing, + (content: any) => { + // 获取解析后的故事详情 + const storyDetails = scriptEditUseCase.getStoryDetails(); + setSynopsis(storyDetails.synopsis || ""); + setCategories(storyDetails.categories || []); + setProtagonist(storyDetails.protagonist || ""); + setIncitingIncident(storyDetails.incitingIncident || ""); + setProblem(storyDetails.problem || ""); + setConflict(storyDetails.conflict || ""); + setStakes(storyDetails.stakes || ""); + setCharacterArc(storyDetails.characterArc || ""); + } + ); + + // 如果有项目ID,则保存增强后的剧本 + if (projectId) { + await scriptEditUseCase.saveScript(projectId); + } + + } catch (error) { + console.error("增强剧本失败:", error); + throw error; + } finally { + setLoading(false); + } + }, [scriptEditUseCase, synopsis, focusedField, aiOptimizing, projectId]); + return { // 响应式状态 loading, @@ -263,6 +390,7 @@ export const useScriptService = (): UseScriptService => { characterArc, projectId, planId, + aiOptimizing, // 操作方法 generateScriptFromIdea, @@ -270,15 +398,17 @@ export const useScriptService = (): UseScriptService => { updateScript, applyScript, abortVideoTask: abortVideoTaskHandler, - - // 修改字段的set函数 - setSynopsis, - setCategories, - setProtagonist, - setIncitingIncident, - setProblem, - setConflict, - setStakes, - setCharacterArc, + focusHandler, + enhanceScript, + setAiOptimizing, + // 封装的set函数 + setSynopsis: setSynopsisWrapper, + setCategories: setCategoriesWrapper, + setProtagonist: setProtagonistWrapper, + setIncitingIncident: setIncitingIncidentWrapper, + setProblem: setProblemWrapper, + setConflict: setConflictWrapper, + setStakes: setStakesWrapper, + setCharacterArc: setCharacterArcWrapper, }; }; diff --git a/app/service/usecase/ScriptEditUseCase.ts b/app/service/usecase/ScriptEditUseCase.ts index 83764f9..4fd0a09 100644 --- a/app/service/usecase/ScriptEditUseCase.ts +++ b/app/service/usecase/ScriptEditUseCase.ts @@ -1,5 +1,13 @@ import { ScriptSlice, ScriptValueObject } from "../domain/valueObject"; -import { generateScriptStream, applyScriptToShot, createMovieProjectV1, saveScript } from "@/api/video_flow"; +import { + generateScriptStream, + applyScriptToShot, + createMovieProjectV1, + saveScript, + enhanceScriptStream, +} from "@/api/video_flow"; + +export type ScriptEditKey = 'synopsis' | 'categories' | 'protagonist' | 'incitingIncident' | 'problem' | 'conflict' | 'stakes' | 'characterArc'; export class ScriptEditUseCase { loading: boolean = false; @@ -16,7 +24,10 @@ export class ScriptEditUseCase { * @param stream_callback 流式数据回调函数 * @returns Promise */ - async generateScript(prompt: string, stream_callback?: (data: any) => void): Promise { + async generateScript( + prompt: string, + stream_callback?: (data: any) => void + ): Promise { try { this.loading = true; @@ -24,14 +35,15 @@ export class ScriptEditUseCase { this.abortController = new AbortController(); // 使用API接口生成剧本 - await generateScriptStream({ + await generateScriptStream( + { text: prompt, - },(content)=>{ - stream_callback?.(content) - this.scriptValueObject.parseFromString(content) - }); - - + }, + (content) => { + stream_callback?.(content); + this.scriptValueObject.parseFromString(content); + } + ); } catch (error) { if (this.abortController?.signal.aborted) { console.log("剧本生成被中断"); @@ -55,6 +67,53 @@ export class ScriptEditUseCase { } } + /** + * @description: 增强剧本方法 + * @param scriptData 剧本数据 + * @param stream_callback 流式数据回调函数 + * @returns Promise + */ + async enhanceScript( + newScript: string|string[], + key: ScriptEditKey, + aiOptimizing: string, + stream_callback?: (data: any) => void + ): Promise { + try { + this.loading = true; + + // 创建新的中断控制器 + this.abortController = new AbortController(); + + // 获取当前剧本文本 + const originalScript = this.scriptValueObject.toString(); + // 清空当前剧本 + this.scriptValueObject = new ScriptValueObject(""); + // 使用API接口增强剧本 + await enhanceScriptStream( + { + original_script: originalScript, + [key]: newScript, + aiOptimizing, + }, + (content) => { + stream_callback?.(content); + this.scriptValueObject.parseFromString(content); + } + ); + } catch (error) { + if (this.abortController?.signal.aborted) { + console.log("剧本增强被中断"); + return; + } + console.error("AI增强剧本出错:", error); + throw error; + } finally { + this.loading = false; + this.abortController = null; + } + } + /** * @description: 创建项目 * @param prompt 用户提示词 @@ -75,7 +134,7 @@ export class ScriptEditUseCase { script: prompt, user_id: userId, mode, - resolution + resolution, }); if (!response.successful) { @@ -91,9 +150,12 @@ export class ScriptEditUseCase { /** * @description: 保存剧本 * @param projectId 项目ID + * @param scriptData 剧本数据 * @returns Promise */ - async saveScript(projectId: string): Promise { + async saveScript( + projectId: string, + ): Promise { try { this.loading = true; @@ -104,10 +166,6 @@ export class ScriptEditUseCase { const response = await saveScript({ project_id: projectId, generated_script: scriptText, - synopsis: this.scriptValueObject.storyDetails.synopsis, - categories: this.scriptValueObject.storyDetails.categories, - protagonist: this.scriptValueObject.storyDetails.protagonist, - framework: this.scriptValueObject.storyDetails.mergeframework() }); if (!response.successful) { @@ -125,14 +183,14 @@ export class ScriptEditUseCase { * @description: 应用剧本方法 * @returns Promise */ - async applyScript(projectId: string,planId: string): Promise { + async applyScript(projectId: string, planId: string): Promise { try { this.loading = true; // 调用应用剧本接口 const response = await applyScriptToShot({ project_id: projectId, - plan_id: planId + plan_id: planId, }); if (!response.successful) { @@ -162,6 +220,50 @@ export class ScriptEditUseCase { getStoryDetails() { return this.scriptValueObject.storyDetails.toObject(); } + /** + * @description: 更新剧本 + * @param scriptText 新的剧本文本 + */ + updateScript(scriptText: string): void { + this.scriptValueObject = new ScriptValueObject(scriptText); + } + + /** + * @description: 更新故事详情字段 + * @param field 字段名 + * @param value 字段值 + */ + updateStoryField(field: string, value: string | string[]): void { + const storyDetails = this.scriptValueObject.storyDetails; + + switch (field) { + case 'synopsis': + storyDetails.synopsis = value as string; + break; + case 'categories': + storyDetails.categories = value as string[]; + break; + case 'protagonist': + storyDetails.protagonist = value as string; + break; + case 'incitingIncident': + storyDetails.incitingIncident = value as string; + break; + case 'problem': + storyDetails.problem = value as string; + break; + case 'conflict': + storyDetails.conflict = value as string; + break; + case 'stakes': + storyDetails.stakes = value as string; + break; + case 'characterArc': + storyDetails.characterArc = value as string; + break; + } + } + /** * @description: 获取加载状态 * @returns boolean @@ -170,14 +272,6 @@ export class ScriptEditUseCase { return this.loading; } - /** - * @description: 更新剧本 - * @param scriptText 剧本文本字符串 - */ - updateScript(scriptText: string): void { - this.scriptValueObject = new ScriptValueObject(scriptText); - } - /** * @description: 将当前剧本片段转换为字符串 * @returns string @@ -185,5 +279,4 @@ export class ScriptEditUseCase { toString(): string { return this.scriptValueObject.toString(); } - }