完成接口

This commit is contained in:
海龙 2025-08-03 15:07:32 +08:00
parent 7960573a12
commit e983a10037
9 changed files with 250 additions and 204 deletions

View File

@ -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<T> {
@ -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<ApiResponse<any>>('/text_to_script/generate_script_stream', request,{
responseType: 'stream',
@ -588,8 +588,10 @@ export const generateScriptStream = async (request: {
* @returns Promise<ApiResponse<应用结果>>
*/
export const applyScriptToShot = async (request: {
/** 项目ID */
projectId: string;
/** 剧本*/
script: string;
scriptText: string;
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/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<ApiResponse<保存结果>>
*/
export const saveScript = async (request: {
/** 项目ID */
projectId: string;
/** 剧本文本 */
scriptText: string;
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/save_script', request);
};
/**
*
* @param request
* @returns Promise<ApiResponse<{ projectId: string }>>
*/
export const createProject = async (request: {
/** 用户提示词 */
userPrompt: string;
/** 剧本内容 */
scriptContent: string;
}): Promise<ApiResponse<{
/** 项目ID */
projectId: string;
}>> => {
return post<ApiResponse<{
projectId: string;
}>>('/movie/create_project', request);
};

View File

@ -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<void>;
/** 根据项目ID获取已存在的剧本数据 */
fetchProjectScript: (projectId: string) => Promise<void>;
/** 设置当前聚焦的剧本片段 */
setFocusedSlice: (sliceId: string) => void;
/** 清除聚焦状态 */
clearFocusedSlice: () => void;
/** 快速更新当前聚焦的剧本片段文本(无防抖) */
updateScriptSliceText: (text: string, metaData?: any) => void;
/** 更新用户提示词 */
updateUserPrompt: (prompt: string) => void;
/** 重置剧本内容到初始状态 */
resetScript: () => void;
/** AI生成剧本 */
generateScript: (prompt: string) => Promise<void>;
/** 应用剧本 */
applyScript: () => Promise<void>;
/** 更新聚焦剧本片段 */
UpdateFocusedSlice: (text: string) => void;
/** 中断剧本生成 */
abortGenerateScript: () => void;
/** 保存剧本 */
saveScript: () => Promise<void>;
/** 创建项目 */
createProject: () => Promise<void>;
}
/**
* Hook
*
*
*
*/
export const useScriptService = (): UseScriptService => {
// 响应式状态
const [scriptText, setScriptText] = useState<string>("");
const [scriptSlices, setScriptSlices] = useState<ScriptSlice[]>([]);
const [focusedSliceId, setFocusedSliceId] = useState<string>("");
const [scriptSliceText, setScriptSliceText] = useState<string>("");
const [userPrompt, setUserPrompt] = useState<string>("");
const [initialScriptText, setInitialScriptText] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [projectId, setProjectId] = useState<string>("");
// UseCase实例
const [scriptEditUseCase, setScriptEditUseCase] = useState<ScriptEditUseCase | null>(null);
// 防抖定时器
const [debounceTimer, setDebounceTimer] = useState<NodeJS.Timeout | null>(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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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,
};
};

View File

@ -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 {

View File

@ -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;
}

View File

@ -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 {*}

View File

@ -7,7 +7,7 @@ describe('ScriptService 业务逻辑测试', () => {
* generateScriptStream
*/
const stream = await generateScriptStream({
prompt: '一个年轻人在咖啡店里等待他的约会对象,心情紧张地摆弄着手机。'
text: '一个年轻人在咖啡店里等待他的约会对象,心情紧张地摆弄着手机。'
});
let allData = '';

View File

@ -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<void>
*/
async generateScript(prompt: string): Promise<void> {
async generateScript(prompt: string, stream_callback?: (data: any) => void): Promise<void> {
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<void>
*/
async applyScript(): Promise<void> {
async applyScript(projectId: string): Promise<void> {
try {
this.loading = true;
// 调用应用剧本接口
const response = await applyScriptToShot({
script: this.scriptValueObject.toString(),
projectId,
scriptText: this.scriptValueObject.toString(),
});
if (!response.successful) {

View File

@ -20,7 +20,8 @@
],
"paths": {
"@/*": ["./*"]
}
},
"maxNodeModuleJsDepth":0
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]

View File

@ -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<T extends (...args: any[]) => any>(func: T, delay: number=100): (...args: Parameters<T>) => void {
if (typeof delay !== 'number' || delay < 0) {
throw new Error('throttle: 第二个参数必须是非负数');
}
let lastCall = 0;
return (...args: Parameters<T>) => {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
func(...args);
}
};
}