forked from 77media/video-flow
完成接口
This commit is contained in:
parent
7960573a12
commit
e983a10037
@ -2,8 +2,8 @@ import { post } from './request';
|
|||||||
import { ProjectTypeEnum } from '@/app/model/enums';
|
import { ProjectTypeEnum } from '@/app/model/enums';
|
||||||
import { ApiResponse } from '@/api/common';
|
import { ApiResponse } from '@/api/common';
|
||||||
import { BASE_URL } from './constants'
|
import { BASE_URL } from './constants'
|
||||||
import { AITextEntity, RoleEntity, SceneEntity, ShotEntity, TagEntity, ContentItem } from '@/app/service/domain/Entities';
|
import { AITextEntity, RoleEntity, SceneEntity, ShotEntity, TagEntity } from '@/app/service/domain/Entities';
|
||||||
import { ScriptSlice } from "@/app/service/domain/valueObject";
|
import { ContentItem, LensType, ScriptSlice } from "@/app/service/domain/valueObject";
|
||||||
|
|
||||||
// API 响应类型
|
// API 响应类型
|
||||||
interface BaseApiResponse<T> {
|
interface BaseApiResponse<T> {
|
||||||
@ -471,7 +471,7 @@ export const regenerateShot = async (request: {
|
|||||||
/** 分镜ID */
|
/** 分镜ID */
|
||||||
shotId?: string;
|
shotId?: string;
|
||||||
/** 镜头描述 */
|
/** 镜头描述 */
|
||||||
shotPrompt?: string;
|
shotPrompt?: LensType[];
|
||||||
/** 对话内容 */
|
/** 对话内容 */
|
||||||
dialogueContent?: ContentItem[];
|
dialogueContent?: ContentItem[];
|
||||||
/** 角色ID替换参数,格式为{oldId:string,newId:string}[] */
|
/** 角色ID替换参数,格式为{oldId:string,newId:string}[] */
|
||||||
@ -575,7 +575,7 @@ export const getShotVideoScript = async (request: {
|
|||||||
*/
|
*/
|
||||||
export const generateScriptStream = async (request: {
|
export const generateScriptStream = async (request: {
|
||||||
/** 剧本提示词 */
|
/** 剧本提示词 */
|
||||||
prompt: string;
|
text: string;
|
||||||
}) => {
|
}) => {
|
||||||
return post<ApiResponse<any>>('/text_to_script/generate_script_stream', request,{
|
return post<ApiResponse<any>>('/text_to_script/generate_script_stream', request,{
|
||||||
responseType: 'stream',
|
responseType: 'stream',
|
||||||
@ -588,8 +588,10 @@ export const generateScriptStream = async (request: {
|
|||||||
* @returns Promise<ApiResponse<应用结果>>
|
* @returns Promise<ApiResponse<应用结果>>
|
||||||
*/
|
*/
|
||||||
export const applyScriptToShot = async (request: {
|
export const applyScriptToShot = async (request: {
|
||||||
|
/** 项目ID */
|
||||||
|
projectId: string;
|
||||||
/** 剧本*/
|
/** 剧本*/
|
||||||
script: string;
|
scriptText: string;
|
||||||
}): Promise<ApiResponse<any>> => {
|
}): Promise<ApiResponse<any>> => {
|
||||||
return post<ApiResponse<any>>('/movie/apply_script_to_shot', request);
|
return post<ApiResponse<any>>('/movie/apply_script_to_shot', request);
|
||||||
};
|
};
|
||||||
@ -612,3 +614,36 @@ export const getProjectScript = async (request: {
|
|||||||
scriptText: string;
|
scriptText: string;
|
||||||
}>>('/movie/get_project_script', request);
|
}>>('/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);
|
||||||
|
};
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { useState, useCallback, useMemo } from "react";
|
import { useState, useCallback, useMemo } from "react";
|
||||||
import { ScriptSlice } from "../domain/valueObject";
|
import { ScriptSlice } from "../domain/valueObject";
|
||||||
import { ScriptEditUseCase } from "../usecase/ScriptEditUseCase";
|
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接口
|
* 剧本服务Hook接口
|
||||||
@ -13,113 +14,83 @@ export interface UseScriptService {
|
|||||||
scriptText: string;
|
scriptText: string;
|
||||||
/** 剧本片段列表 */
|
/** 剧本片段列表 */
|
||||||
scriptSlices: ScriptSlice[];
|
scriptSlices: ScriptSlice[];
|
||||||
/** 当前聚焦的剧本片段ID */
|
|
||||||
focusedSliceId: string;
|
|
||||||
/** 当前聚焦的剧本片段 */
|
|
||||||
focusedSlice: ScriptSlice | null;
|
|
||||||
/** 当前聚焦的剧本片段文本 */
|
|
||||||
scriptSliceText: string;
|
|
||||||
/** 用户提示词(可编辑) */
|
/** 用户提示词(可编辑) */
|
||||||
userPrompt: string;
|
userPrompt: string;
|
||||||
/** 加载状态 */
|
/** 加载状态 */
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
/** 错误信息 */
|
/** 项目ID */
|
||||||
error: string | null;
|
projectId: string;
|
||||||
|
|
||||||
// 操作方法
|
// 操作方法
|
||||||
/** 获取剧本数据(用户提示词) */
|
/** 获取根据用户想法调用接口AI生成剧本(用户提示词) */
|
||||||
fetchScriptData: (prompt: string) => Promise<void>;
|
fetchScriptData: (prompt: string) => Promise<void>;
|
||||||
/** 根据项目ID获取已存在的剧本数据 */
|
/** 根据项目ID获取已存在的剧本数据 */
|
||||||
fetchProjectScript: (projectId: string) => Promise<void>;
|
fetchProjectScript: (projectId: string) => Promise<void>;
|
||||||
/** 设置当前聚焦的剧本片段 */
|
|
||||||
setFocusedSlice: (sliceId: string) => void;
|
|
||||||
/** 清除聚焦状态 */
|
|
||||||
clearFocusedSlice: () => void;
|
|
||||||
/** 快速更新当前聚焦的剧本片段文本(无防抖) */
|
|
||||||
updateScriptSliceText: (text: string, metaData?: any) => void;
|
|
||||||
/** 更新用户提示词 */
|
/** 更新用户提示词 */
|
||||||
updateUserPrompt: (prompt: string) => void;
|
updateUserPrompt: (prompt: string) => void;
|
||||||
/** 重置剧本内容到初始状态 */
|
/** 重置剧本内容到初始状态 */
|
||||||
resetScript: () => void;
|
resetScript: () => void;
|
||||||
/** AI生成剧本 */
|
|
||||||
generateScript: (prompt: string) => Promise<void>;
|
|
||||||
/** 应用剧本 */
|
/** 应用剧本 */
|
||||||
applyScript: () => Promise<void>;
|
applyScript: () => Promise<void>;
|
||||||
/** 更新聚焦剧本片段 */
|
/** 中断剧本生成 */
|
||||||
UpdateFocusedSlice: (text: string) => void;
|
abortGenerateScript: () => void;
|
||||||
|
/** 保存剧本 */
|
||||||
|
saveScript: () => Promise<void>;
|
||||||
|
/** 创建项目 */
|
||||||
|
createProject: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 剧本服务Hook
|
* 剧本服务Hook
|
||||||
* 提供剧本相关的所有状态管理和操作方法
|
* 提供剧本相关的所有状态管理和操作方法
|
||||||
* 包括剧本数据获取、片段管理、聚焦状态、防抖更新等功能
|
* 包括剧本数据获取、片段管理等功能
|
||||||
*/
|
*/
|
||||||
export const useScriptService = (): UseScriptService => {
|
export const useScriptService = (): UseScriptService => {
|
||||||
// 响应式状态
|
// 响应式状态
|
||||||
const [scriptText, setScriptText] = useState<string>("");
|
const [scriptText, setScriptText] = useState<string>("");
|
||||||
const [scriptSlices, setScriptSlices] = useState<ScriptSlice[]>([]);
|
const [scriptSlices, setScriptSlices] = useState<ScriptSlice[]>([]);
|
||||||
const [focusedSliceId, setFocusedSliceId] = useState<string>("");
|
|
||||||
const [scriptSliceText, setScriptSliceText] = useState<string>("");
|
|
||||||
const [userPrompt, setUserPrompt] = useState<string>("");
|
const [userPrompt, setUserPrompt] = useState<string>("");
|
||||||
const [initialScriptText, setInitialScriptText] = useState<string>("");
|
const [initialScriptText, setInitialScriptText] = useState<string>("");
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [projectId, setProjectId] = useState<string>("");
|
||||||
|
|
||||||
// UseCase实例
|
// UseCase实例
|
||||||
const [scriptEditUseCase, setScriptEditUseCase] = useState<ScriptEditUseCase | null>(null);
|
const [scriptEditUseCase, setScriptEditUseCase] = useState<ScriptEditUseCase | null>(null);
|
||||||
|
|
||||||
// 防抖定时器
|
|
||||||
const [debounceTimer, setDebounceTimer] = useState<NodeJS.Timeout | null>(null);
|
|
||||||
const DEBOUNCE_DELAY = 300;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当前聚焦的剧本片段
|
* 初始化,ai生成剧本(用户提示词)
|
||||||
*/
|
|
||||||
const focusedSlice = useMemo(() => {
|
|
||||||
return scriptSlices.find(slice => slice.id === focusedSliceId) || null;
|
|
||||||
}, [scriptSlices, focusedSliceId]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取剧本数据(用户提示词)
|
|
||||||
* @param prompt 用户提示词
|
* @param prompt 用户提示词
|
||||||
*/
|
*/
|
||||||
const fetchScriptData = useCallback(async (prompt: string): Promise<void> => {
|
const fetchScriptData = useCallback(async (prompt: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
|
||||||
|
|
||||||
// 清空当前状态
|
// 清空当前状态
|
||||||
setScriptText("");
|
setScriptText("");
|
||||||
setScriptSlices([]);
|
setScriptSlices([]);
|
||||||
setFocusedSliceId("");
|
|
||||||
setScriptSliceText("");
|
|
||||||
|
|
||||||
// 更新用户提示词状态
|
// 更新用户提示词状态
|
||||||
setUserPrompt(prompt);
|
setUserPrompt(prompt);
|
||||||
|
|
||||||
// 保存初始提示词(只在第一次获取时保存)
|
|
||||||
if (!initialScriptText) {
|
|
||||||
setInitialScriptText(prompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新的剧本编辑用例
|
// 创建新的剧本编辑用例
|
||||||
const newScriptEditUseCase = new ScriptEditUseCase('');
|
const newScriptEditUseCase = new ScriptEditUseCase('');
|
||||||
setScriptEditUseCase(newScriptEditUseCase);
|
setScriptEditUseCase(newScriptEditUseCase);
|
||||||
|
|
||||||
// 调用AI生成剧本
|
// 调用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();
|
if (!initialScriptText) {
|
||||||
setScriptText(generatedScriptText);
|
setInitialScriptText(generatedScriptText);
|
||||||
|
}
|
||||||
// 获取剧本片段列表
|
}));
|
||||||
const slices = newScriptEditUseCase.getScriptSlices();
|
|
||||||
setScriptSlices(slices);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取剧本数据失败:', error);
|
console.error('获取剧本数据失败:', error);
|
||||||
setError(error instanceof Error ? error.message : '获取剧本数据失败');
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -133,13 +104,12 @@ export const useScriptService = (): UseScriptService => {
|
|||||||
const fetchProjectScript = useCallback(async (projectId: string): Promise<void> => {
|
const fetchProjectScript = useCallback(async (projectId: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
|
||||||
|
|
||||||
// 清空当前状态
|
// 清空当前状态
|
||||||
setScriptText("");
|
setScriptText("");
|
||||||
setScriptSlices([]);
|
setScriptSlices([]);
|
||||||
setFocusedSliceId("");
|
|
||||||
setScriptSliceText("");
|
// 设置项目ID
|
||||||
|
setProjectId(projectId);
|
||||||
|
|
||||||
// 调用API获取项目剧本数据
|
// 调用API获取项目剧本数据
|
||||||
const response = await getProjectScript({ projectId });
|
const response = await getProjectScript({ projectId });
|
||||||
@ -153,9 +123,9 @@ export const useScriptService = (): UseScriptService => {
|
|||||||
// 更新用户提示词状态
|
// 更新用户提示词状态
|
||||||
setUserPrompt(prompt);
|
setUserPrompt(prompt);
|
||||||
|
|
||||||
// 保存初始提示词(只在第一次获取时保存)
|
// 保存初始剧本文本(只在第一次获取时保存)
|
||||||
if (!initialScriptText) {
|
if (!initialScriptText) {
|
||||||
setInitialScriptText(prompt);
|
setInitialScriptText(scriptText);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新的剧本编辑用例并初始化数据
|
// 创建新的剧本编辑用例并初始化数据
|
||||||
@ -171,84 +141,12 @@ export const useScriptService = (): UseScriptService => {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取项目剧本数据失败:', error);
|
console.error('获取项目剧本数据失败:', error);
|
||||||
setError(error instanceof Error ? error.message : '获取项目剧本数据失败');
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [initialScriptText]);
|
}, [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 新的用户提示词
|
* @param prompt 新的用户提示词
|
||||||
@ -261,41 +159,16 @@ export const useScriptService = (): UseScriptService => {
|
|||||||
* 重置剧本内容到初始状态
|
* 重置剧本内容到初始状态
|
||||||
*/
|
*/
|
||||||
const resetScript = useCallback((): void => {
|
const resetScript = useCallback((): void => {
|
||||||
if (initialScriptText) {
|
if (initialScriptText && scriptEditUseCase) {
|
||||||
// 重新调用AI生成剧本(fetchScriptData会自动清空状态)
|
// 重置剧本文本到初始状态
|
||||||
fetchScriptData(initialScriptText);
|
setScriptText(initialScriptText);
|
||||||
|
// 更新现有剧本编辑用例的数据
|
||||||
|
scriptEditUseCase.updateScript(initialScriptText);
|
||||||
|
// 从UseCase获取解析后的剧本片段
|
||||||
|
const scriptSlices = scriptEditUseCase.getScriptSlices();
|
||||||
|
setScriptSlices(scriptSlices);
|
||||||
}
|
}
|
||||||
}, [initialScriptText, fetchScriptData]);
|
}, [initialScriptText, scriptEditUseCase]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用剧本
|
* 应用剧本
|
||||||
@ -303,44 +176,114 @@ export const useScriptService = (): UseScriptService => {
|
|||||||
const applyScript = useCallback(async (): Promise<void> => {
|
const applyScript = useCallback(async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
|
||||||
|
|
||||||
if (!scriptEditUseCase) {
|
if (!scriptEditUseCase) {
|
||||||
throw new Error("剧本编辑用例未初始化");
|
throw new Error("剧本编辑用例未初始化");
|
||||||
}
|
}
|
||||||
|
|
||||||
await scriptEditUseCase.applyScript();
|
await scriptEditUseCase.applyScript(projectId);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("应用剧本失败:", error);
|
console.error("应用剧本失败:", error);
|
||||||
setError(error instanceof Error ? error.message : "应用剧本失败");
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
}, [scriptEditUseCase, projectId]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 中断剧本生成
|
||||||
|
*/
|
||||||
|
const abortGenerateScript = useCallback((): void => {
|
||||||
|
if (scriptEditUseCase) {
|
||||||
|
scriptEditUseCase.abortGenerateScript();
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}, [scriptEditUseCase]);
|
}, [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 {
|
return {
|
||||||
// 响应式状态
|
// 响应式状态
|
||||||
scriptText,
|
scriptText,
|
||||||
scriptSlices,
|
scriptSlices,
|
||||||
focusedSliceId,
|
|
||||||
focusedSlice,
|
|
||||||
scriptSliceText,
|
|
||||||
userPrompt,
|
userPrompt,
|
||||||
loading,
|
loading,
|
||||||
error,
|
projectId,
|
||||||
|
|
||||||
// 操作方法
|
// 操作方法
|
||||||
fetchScriptData,
|
fetchScriptData,
|
||||||
fetchProjectScript,
|
fetchProjectScript,
|
||||||
setFocusedSlice,
|
|
||||||
clearFocusedSlice,
|
|
||||||
updateScriptSliceText,
|
|
||||||
updateUserPrompt,
|
updateUserPrompt,
|
||||||
resetScript,
|
resetScript,
|
||||||
generateScript,
|
|
||||||
applyScript,
|
applyScript,
|
||||||
UpdateFocusedSlice
|
abortGenerateScript,
|
||||||
|
saveScript,
|
||||||
|
createProject,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,9 +3,8 @@ import {
|
|||||||
ShotEntity,
|
ShotEntity,
|
||||||
RoleEntity,
|
RoleEntity,
|
||||||
SceneEntity,
|
SceneEntity,
|
||||||
ContentItem,
|
|
||||||
} from "../domain/Entities";
|
} from "../domain/Entities";
|
||||||
import { ScriptSlice, ScriptValueObject } from "../domain/valueObject";
|
import { ContentItem, ScriptSlice, ScriptValueObject } from "../domain/valueObject";
|
||||||
import { ShotItem } from "../domain/Item";
|
import { ShotItem } from "../domain/Item";
|
||||||
import { ShotEditUseCase } from "../usecase/ShotEditUsecase";
|
import { ShotEditUseCase } from "../usecase/ShotEditUsecase";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
* 所有实体都应该实现这些基础接口
|
* 所有实体都应该实现这些基础接口
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ContentItem, LensType } from "./valueObject";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基础实体接口
|
* 基础实体接口
|
||||||
*/
|
*/
|
||||||
@ -66,13 +68,7 @@ export interface SceneEntity extends BaseEntity {
|
|||||||
generateTextId: string;
|
generateTextId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**对话内容项 */
|
|
||||||
export interface ContentItem {
|
|
||||||
/** 角色ID */
|
|
||||||
roleId: string;
|
|
||||||
/** 对话内容 */
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
/**分镜进度 */
|
/**分镜进度 */
|
||||||
export enum ShotStatus {
|
export enum ShotStatus {
|
||||||
/** 草稿加载中 */
|
/** 草稿加载中 */
|
||||||
@ -82,6 +78,8 @@ export enum ShotStatus {
|
|||||||
/** 完成 */
|
/** 完成 */
|
||||||
finished = 2,
|
finished = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分镜实体接口
|
* 分镜实体接口
|
||||||
*/
|
*/
|
||||||
@ -101,7 +99,7 @@ export interface ShotEntity extends BaseEntity {
|
|||||||
/**对话内容 */
|
/**对话内容 */
|
||||||
content: ContentItem[];
|
content: ContentItem[];
|
||||||
/**镜头项 */
|
/**镜头项 */
|
||||||
shot: string[];
|
lens: LensType[];
|
||||||
/**分镜剧本Id */
|
/**分镜剧本Id */
|
||||||
scriptId: string;
|
scriptId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,22 @@ export interface ScriptSlice {
|
|||||||
/** 元数据 */
|
/** 元数据 */
|
||||||
metaData: any;
|
metaData: any;
|
||||||
}
|
}
|
||||||
|
/**对话内容项 */
|
||||||
|
export interface ContentItem {
|
||||||
|
/** 角色ID */
|
||||||
|
roleId: string;
|
||||||
|
/** 对话内容 */
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
/**镜头值对象 */
|
||||||
|
export interface LensType {
|
||||||
|
/** 镜头名称 */
|
||||||
|
name: string;
|
||||||
|
/** 镜头描述 */
|
||||||
|
content: string;
|
||||||
|
/**运镜描述 */
|
||||||
|
movement: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* @description: 剧本 值对象,将剧本文本转换为剧本对象
|
* @description: 剧本 值对象,将剧本文本转换为剧本对象
|
||||||
* @return {*}
|
* @return {*}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ describe('ScriptService 业务逻辑测试', () => {
|
|||||||
* 测试 generateScriptStream 流式接口,持续监听数据直到流结束
|
* 测试 generateScriptStream 流式接口,持续监听数据直到流结束
|
||||||
*/
|
*/
|
||||||
const stream = await generateScriptStream({
|
const stream = await generateScriptStream({
|
||||||
prompt: '一个年轻人在咖啡店里等待他的约会对象,心情紧张地摆弄着手机。'
|
text: '一个年轻人在咖啡店里等待他的约会对象,心情紧张地摆弄着手机。'
|
||||||
});
|
});
|
||||||
|
|
||||||
let allData = '';
|
let allData = '';
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { generateScriptStream, applyScriptToShot } from "@/api/video_flow";
|
|||||||
export class ScriptEditUseCase {
|
export class ScriptEditUseCase {
|
||||||
loading: boolean = false;
|
loading: boolean = false;
|
||||||
private scriptValueObject: ScriptValueObject;
|
private scriptValueObject: ScriptValueObject;
|
||||||
|
private abortController: AbortController | null = null;
|
||||||
|
|
||||||
constructor(script: string) {
|
constructor(script: string) {
|
||||||
this.scriptValueObject = new ScriptValueObject(script);
|
this.scriptValueObject = new ScriptValueObject(script);
|
||||||
@ -12,15 +13,19 @@ export class ScriptEditUseCase {
|
|||||||
/**
|
/**
|
||||||
* @description: AI生成剧本方法
|
* @description: AI生成剧本方法
|
||||||
* @param prompt 剧本提示词
|
* @param prompt 剧本提示词
|
||||||
|
* @param stream_callback 流式数据回调函数
|
||||||
* @returns Promise<void>
|
* @returns Promise<void>
|
||||||
*/
|
*/
|
||||||
async generateScript(prompt: string): Promise<void> {
|
async generateScript(prompt: string, stream_callback?: (data: any) => void): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
|
// 创建新的中断控制器
|
||||||
|
this.abortController = new AbortController();
|
||||||
|
|
||||||
// 使用API接口生成剧本
|
// 使用API接口生成剧本
|
||||||
const response = await generateScriptStream({
|
const response = await generateScriptStream({
|
||||||
prompt,
|
text: prompt,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.successful) {
|
if (!response.successful) {
|
||||||
@ -29,15 +34,37 @@ export class ScriptEditUseCase {
|
|||||||
|
|
||||||
// 使用for await处理流式数据
|
// 使用for await处理流式数据
|
||||||
for await (const data of response.data) {
|
for await (const data of response.data) {
|
||||||
|
// 检查是否被中断
|
||||||
|
if (this.abortController.signal.aborted) {
|
||||||
|
console.log("剧本生成被中断");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: 根据流式数据更新剧本片段
|
// TODO: 根据流式数据更新剧本片段
|
||||||
// 这里需要根据实际的流式数据格式来处理
|
// 这里需要根据实际的流式数据格式来处理
|
||||||
// 可能需要将流式数据转换为ScriptSlice并添加到scriptValueObject中
|
// 可能需要将流式数据转换为ScriptSlice并添加到scriptValueObject中
|
||||||
|
stream_callback?.(data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (this.abortController?.signal.aborted) {
|
||||||
|
console.log("剧本生成被中断");
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error("AI生成剧本出错:", error);
|
console.error("AI生成剧本出错:", error);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
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: 应用剧本方法
|
* @description: 应用剧本方法
|
||||||
* @returns Promise<void>
|
* @returns Promise<void>
|
||||||
*/
|
*/
|
||||||
async applyScript(): Promise<void> {
|
async applyScript(projectId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
// 调用应用剧本接口
|
// 调用应用剧本接口
|
||||||
const response = await applyScriptToShot({
|
const response = await applyScriptToShot({
|
||||||
script: this.scriptValueObject.toString(),
|
projectId,
|
||||||
|
scriptText: this.scriptValueObject.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.successful) {
|
if (!response.successful) {
|
||||||
|
|||||||
@ -20,7 +20,8 @@
|
|||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
}
|
},
|
||||||
|
"maxNodeModuleJsDepth":0
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
|
|||||||
@ -2,10 +2,37 @@ import { ScriptSlice, ScriptSliceType } from "@/app/service/domain/valueObject";
|
|||||||
|
|
||||||
export function parseScriptEntity(text: string):ScriptSlice {
|
export function parseScriptEntity(text: string):ScriptSlice {
|
||||||
const scriptSlice:ScriptSlice={
|
const scriptSlice:ScriptSlice={
|
||||||
type:ScriptSliceType.text,
|
// 生成唯一ID,单次使用即可
|
||||||
text:text,
|
id: `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`,
|
||||||
metaData:{}
|
type: ScriptSliceType.text,
|
||||||
|
text: text,
|
||||||
|
metaData: {}
|
||||||
|
|
||||||
}
|
}
|
||||||
return scriptSlice;
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user