新增剧本增强功能,支持剧本流式处理和状态管理,更新相关服务以整合暂停和恢复计划功能,优化剧本更新逻辑。

This commit is contained in:
海龙 2025-08-06 20:08:42 +08:00
parent 735cd7ec68
commit 693f7599f1
3 changed files with 345 additions and 60 deletions

View File

@ -653,14 +653,6 @@ export const saveScript = async (request: {
project_id: string;
/** 剧本文本 */
generated_script: string;
/** 剧情梗概 */
synopsis: string;
/** 剧情类型 */
categories: string[];
/** 主角 */
protagonist: string;
/** 框架 */
framework: string;
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>("/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<ApiResponse<any>> => {
return post("/api/v1/video/pause", request);
};
export const resumePlanFlow = async (request: {
/** 项目ID */
project_id: string;
/** 计划ID */
plan_id: string;
}): Promise<ApiResponse<any>> => {
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<void>
*/
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<void> => {
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;
}
})
})
};

View File

@ -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<void>;
/** 根据项目ID初始化已有剧本 */
initializeFromProject: (projectId: string) => Promise<void>;
/** 修改剧本 */
updateScript: (scriptText: string) => void;
updateScript: (scriptText: string) => Promise<void>;
/** 应用剧本到视频生成流程 */
applyScript: () => Promise<void>;
/** 中断视频任务 */
abortVideoTask: () => Promise<void>;
/** 聚焦处理函数 */
focusHandler: (field: 'synopsis' | 'categories' | 'protagonist' | 'incitingIncident' | 'problem' | 'conflict' | 'stakes' | 'characterArc') => Promise<void>;
/** 增强剧本 */
enhanceScript: () => Promise<void>;
/** 设置AI优化要求 */
setAiOptimizing: Dispatch<SetStateAction<string>>;
// 修改字段的set函数
/** 设置故事梗概 */
@ -81,6 +88,8 @@ export const useScriptService = (): UseScriptService => {
const [characterArc, setCharacterArc] = useState<string>("");
const [projectId, setProjectId] = useState<string>("");
const [planId, setPlanId] = useState<string>("");
const [aiOptimizing, setAiOptimizing] = useState<string>("");
const [focusedField, setFocusedField] = useState<string>("");
// UseCase实例
const [scriptEditUseCase, setScriptEditUseCase] = useState<ScriptEditUseCase | null>(null);
@ -180,22 +189,16 @@ export const useScriptService = (): UseScriptService => {
*
* @param scriptText
*/
const updateScript = useCallback((scriptText: string): void => {
const updateScript = useCallback(async (): Promise<void> => {
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<string>) => {
const newValue = typeof value === 'function' ? value(synopsis) : value;
setSynopsis(newValue);
if (scriptEditUseCase) {
scriptEditUseCase.updateStoryField('synopsis', newValue);
}
}, [synopsis, scriptEditUseCase]);
const setCategoriesWrapper = useCallback((value: SetStateAction<string[]>) => {
const newValue = typeof value === 'function' ? value(categories) : value;
setCategories(newValue);
if (scriptEditUseCase) {
scriptEditUseCase.updateStoryField('categories', newValue);
}
}, [categories, scriptEditUseCase]);
const setProtagonistWrapper = useCallback((value: SetStateAction<string>) => {
const newValue = typeof value === 'function' ? value(protagonist) : value;
setProtagonist(newValue);
if (scriptEditUseCase) {
scriptEditUseCase.updateStoryField('protagonist', newValue);
}
}, [protagonist, scriptEditUseCase]);
const setIncitingIncidentWrapper = useCallback((value: SetStateAction<string>) => {
const newValue = typeof value === 'function' ? value(incitingIncident) : value;
setIncitingIncident(newValue);
if (scriptEditUseCase) {
scriptEditUseCase.updateStoryField('incitingIncident', newValue);
}
}, [incitingIncident, scriptEditUseCase]);
const setProblemWrapper = useCallback((value: SetStateAction<string>) => {
const newValue = typeof value === 'function' ? value(problem) : value;
setProblem(newValue);
if (scriptEditUseCase) {
scriptEditUseCase.updateStoryField('problem', newValue);
}
}, [problem, scriptEditUseCase]);
const setConflictWrapper = useCallback((value: SetStateAction<string>) => {
const newValue = typeof value === 'function' ? value(conflict) : value;
setConflict(newValue);
if (scriptEditUseCase) {
scriptEditUseCase.updateStoryField('conflict', newValue);
}
}, [conflict, scriptEditUseCase]);
const setStakesWrapper = useCallback((value: SetStateAction<string>) => {
const newValue = typeof value === 'function' ? value(stakes) : value;
setStakes(newValue);
if (scriptEditUseCase) {
scriptEditUseCase.updateStoryField('stakes', newValue);
}
}, [stakes, scriptEditUseCase]);
const setCharacterArcWrapper = useCallback((value: SetStateAction<string>) => {
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<void> => {
try {
// 如果当前已经有聚焦的字段,先处理暂停/继续逻辑
// 设置新的聚焦字段
setFocusedField(field);
} catch (error) {
console.error("聚焦处理失败:", error);
throw error;
}
}, []);
/**
*
*/
const enhanceScript = useCallback(async (): Promise<void> => {
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,
};
};

View File

@ -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<void>
*/
async generateScript(prompt: string, stream_callback?: (data: any) => void): Promise<void> {
async generateScript(
prompt: string,
stream_callback?: (data: any) => void
): Promise<void> {
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<void>
*/
async enhanceScript(
newScript: string|string[],
key: ScriptEditKey,
aiOptimizing: string,
stream_callback?: (data: any) => void
): Promise<void> {
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<void>
*/
async saveScript(projectId: string): Promise<void> {
async saveScript(
projectId: string,
): Promise<void> {
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<void>
*/
async applyScript(projectId: string,planId: string): Promise<void> {
async applyScript(projectId: string, planId: string): Promise<void> {
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();
}
}