forked from 77media/video-flow
更新分镜服务Hook,支持对话内容和角色替换参数的可选性;新增AI生成剧本流式接口和应用剧本功能;优化分镜视频数据结构,支持多个视频URL;删除不再使用的分镜详情和视频数据获取API接口。
This commit is contained in:
parent
291c18ad86
commit
e42f5269ca
@ -2,7 +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, ScriptSliceEntity } from '@/app/service/domain/Entities';
|
||||
import { AITextEntity, RoleEntity, SceneEntity, ShotEntity, TagEntity, ContentItem } from '@/app/service/domain/Entities';
|
||||
import { ScriptSlice } from "@/app/service/domain/valueObject";
|
||||
|
||||
// API 响应类型
|
||||
interface BaseApiResponse<T> {
|
||||
@ -57,10 +58,10 @@ interface TaskData {
|
||||
}
|
||||
|
||||
// 流式数据类型
|
||||
export interface StreamData {
|
||||
export interface StreamData<T=any> {
|
||||
category: 'sketch' | 'character' | 'video' | 'music' | 'final_video';
|
||||
message: string;
|
||||
data: any;
|
||||
data: T;
|
||||
status: 'running' | 'completed';
|
||||
total?: number;
|
||||
completed?: number;
|
||||
@ -468,15 +469,15 @@ export const getShotData = async (request: {
|
||||
*/
|
||||
export const regenerateShot = async (request: {
|
||||
/** 分镜ID */
|
||||
shotId: string;
|
||||
shotId?: string;
|
||||
/** 镜头描述 */
|
||||
shotPrompt: string;
|
||||
shotPrompt?: string;
|
||||
/** 对话内容 */
|
||||
dialogueContent: string;
|
||||
dialogueContent?: ContentItem[];
|
||||
/** 角色ID替换参数,格式为{oldId:string,newId:string}[] */
|
||||
roleReplaceParams: { oldId: string; newId: string }[];
|
||||
roleReplaceParams?: { oldId: string; newId: string }[];
|
||||
/** 场景ID替换参数,格式为{oldId:string,newId:string}[] */
|
||||
sceneReplaceParams: { oldId: string; newId: string }[];
|
||||
sceneReplaceParams?: { oldId: string; newId: string }[];
|
||||
}): Promise<ApiResponse<ShotEntity>> => {
|
||||
return post<ApiResponse<any>>('/movie/regenerate_shot', request);
|
||||
};
|
||||
@ -512,41 +513,18 @@ export const getShotList = async (request: {
|
||||
return post<ApiResponse<any>>('/movie/get_shot_list', request);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取分镜详情
|
||||
* @param request - 获取分镜详情请求参数
|
||||
* @returns Promise<ApiResponse<分镜实体>>
|
||||
*/
|
||||
export const getShotDetail = async (request: {
|
||||
/** 分镜ID */
|
||||
shotId: string;
|
||||
}): Promise<ApiResponse<ShotEntity>> => {
|
||||
return post<ApiResponse<any>>('/movie/get_shot_detail', request);
|
||||
};
|
||||
// /**
|
||||
// * 获取分镜详情
|
||||
// * @param request - 获取分镜详情请求参数
|
||||
// * @returns Promise<ApiResponse<分镜实体>>
|
||||
// */
|
||||
// export const getShotDetail = async (request: {
|
||||
// /** 分镜ID */
|
||||
// shotId: string;
|
||||
// }): Promise<ApiResponse<ShotEntity>> => {
|
||||
// return post<ApiResponse<any>>('/movie/get_shot_detail', request);
|
||||
// };
|
||||
|
||||
/**
|
||||
* 获取分镜草图数据
|
||||
* @param request - 获取分镜草图数据请求参数
|
||||
* @returns Promise<ApiResponse<草图数据URL>>
|
||||
*/
|
||||
export const getShotSketchData = async (request: {
|
||||
/** 分镜ID */
|
||||
shotId: string;
|
||||
}): Promise<ApiResponse<string>> => {
|
||||
return post<ApiResponse<string>>('/movie/get_shot_sketch_data', request);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取分镜视频数据
|
||||
* @param request - 获取分镜视频数据请求参数
|
||||
* @returns Promise<ApiResponse<视频数据URL>>
|
||||
*/
|
||||
export const getShotVideoData = async (request: {
|
||||
/** 分镜ID */
|
||||
shotId: string;
|
||||
}): Promise<ApiResponse<string>> => {
|
||||
return post<ApiResponse<string>>('/movie/get_shot_video_data', request);
|
||||
};
|
||||
|
||||
/**
|
||||
* 修改分镜镜头
|
||||
@ -586,6 +564,32 @@ export const replaceShotRole = async (request: {
|
||||
export const getShotVideoScript = async (request: {
|
||||
/** 分镜ID */
|
||||
shotId: string;
|
||||
}): Promise<ApiResponse<ScriptSliceEntity[]>> => {
|
||||
}): Promise<ApiResponse<string>> => {
|
||||
return post<ApiResponse<any>>('/movie/get_shot_video_script', request);
|
||||
};
|
||||
|
||||
/**
|
||||
* AI生成剧本流式接口
|
||||
* @param request - AI生成剧本请求参数
|
||||
* @returns Promise<ApiResponse<流式数据>>
|
||||
*/
|
||||
export const generateScriptStream = async (request: {
|
||||
/** 剧本提示词 */
|
||||
prompt: string;
|
||||
}) => {
|
||||
return post<ApiResponse<any>>('/movie/generate_script_stream', request,{
|
||||
responseType: 'stream',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 应用剧本
|
||||
* @param request - 应用剧本请求参数
|
||||
* @returns Promise<ApiResponse<应用结果>>
|
||||
*/
|
||||
export const applyScriptToShot = async (request: {
|
||||
/** 剧本*/
|
||||
script: string;
|
||||
}): Promise<ApiResponse<any>> => {
|
||||
return post<ApiResponse<any>>('/movie/apply_script_to_shot', request);
|
||||
};
|
||||
|
||||
290
app/service/Interaction/ScriptService.ts
Normal file
290
app/service/Interaction/ScriptService.ts
Normal file
@ -0,0 +1,290 @@
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { ScriptSlice } from "../domain/valueObject";
|
||||
import { ScriptEditUseCase } from "../usecase/ScriptEditUseCase";
|
||||
|
||||
/**
|
||||
* 剧本服务Hook接口
|
||||
* 定义剧本服务Hook的所有状态和操作方法
|
||||
*/
|
||||
export interface UseScriptService {
|
||||
// 响应式状态
|
||||
/** 当前剧本文本 */
|
||||
scriptText: string;
|
||||
/** 剧本片段列表 */
|
||||
scriptSlices: ScriptSlice[];
|
||||
/** 当前聚焦的剧本片段ID */
|
||||
focusedSliceId: string;
|
||||
/** 当前聚焦的剧本片段 */
|
||||
focusedSlice: ScriptSlice | null;
|
||||
/** 当前聚焦的剧本片段文本 */
|
||||
scriptSliceText: string;
|
||||
/** 用户提示词(可编辑) */
|
||||
userPrompt: string;
|
||||
/** 加载状态 */
|
||||
loading: boolean;
|
||||
/** 错误信息 */
|
||||
error: string | null;
|
||||
|
||||
// 操作方法
|
||||
/** 获取剧本数据(用户提示词) */
|
||||
fetchScriptData: (prompt: 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 剧本服务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);
|
||||
|
||||
// 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]);
|
||||
|
||||
/**
|
||||
* 获取剧本数据(用户提示词)
|
||||
* @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);
|
||||
|
||||
// 获取生成的剧本文本
|
||||
const generatedScriptText = newScriptEditUseCase.toString();
|
||||
setScriptText(generatedScriptText);
|
||||
|
||||
// 获取剧本片段列表
|
||||
const slices = newScriptEditUseCase.getScriptSlices();
|
||||
setScriptSlices(slices);
|
||||
|
||||
} 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 新的用户提示词
|
||||
*/
|
||||
const updateUserPrompt = useCallback((prompt: string): void => {
|
||||
setUserPrompt(prompt);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 重置剧本内容到初始状态
|
||||
*/
|
||||
const resetScript = useCallback((): void => {
|
||||
if (initialScriptText) {
|
||||
// 重新调用AI生成剧本(fetchScriptData会自动清空状态)
|
||||
fetchScriptData(initialScriptText);
|
||||
}
|
||||
}, [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]);
|
||||
|
||||
/**
|
||||
* 应用剧本
|
||||
*/
|
||||
const applyScript = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (!scriptEditUseCase) {
|
||||
throw new Error("剧本编辑用例未初始化");
|
||||
}
|
||||
|
||||
await scriptEditUseCase.applyScript();
|
||||
|
||||
} catch (error) {
|
||||
console.error("应用剧本失败:", error);
|
||||
setError(error instanceof Error ? error.message : "应用剧本失败");
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [scriptEditUseCase]);
|
||||
|
||||
return {
|
||||
// 响应式状态
|
||||
scriptText,
|
||||
scriptSlices,
|
||||
focusedSliceId,
|
||||
focusedSlice,
|
||||
scriptSliceText,
|
||||
userPrompt,
|
||||
loading,
|
||||
error,
|
||||
|
||||
// 操作方法
|
||||
fetchScriptData,
|
||||
setFocusedSlice,
|
||||
clearFocusedSlice,
|
||||
updateScriptSliceText,
|
||||
updateUserPrompt,
|
||||
resetScript,
|
||||
generateScript,
|
||||
applyScript,
|
||||
UpdateFocusedSlice
|
||||
};
|
||||
};
|
||||
@ -1,16 +1,22 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { ShotEntity, RoleEntity, SceneEntity, ScriptSliceEntity } from '../domain/Entities';
|
||||
import { ShotItem } from '../domain/Item';
|
||||
import { ShotEditUseCase } from '../usecase/ShotEditUsecase';
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import {
|
||||
ShotEntity,
|
||||
RoleEntity,
|
||||
SceneEntity,
|
||||
ContentItem,
|
||||
} from "../domain/Entities";
|
||||
import { ScriptSlice, ScriptValueObject } from "../domain/valueObject";
|
||||
import { ShotItem } from "../domain/Item";
|
||||
import { ShotEditUseCase } from "../usecase/ShotEditUsecase";
|
||||
import {
|
||||
getShotList,
|
||||
getShotDetail,
|
||||
// getShotDetail,
|
||||
updateShotContent,
|
||||
updateShotShot,
|
||||
getUserRoleLibrary,
|
||||
replaceShotRole,
|
||||
getShotVideoScript
|
||||
} from '@/api/video_flow';
|
||||
getShotVideoScript,
|
||||
} from "@/api/video_flow";
|
||||
|
||||
/**
|
||||
* 分镜服务Hook接口
|
||||
@ -25,12 +31,12 @@ export interface UseShotService {
|
||||
/** 当前分镜的草图数据URL */
|
||||
shotSketchData: string | null;
|
||||
/** 当前分镜的视频数据URL */
|
||||
shotVideoData: string | null;
|
||||
shotVideoData: string[] | null;
|
||||
|
||||
/** 用户角色库 */
|
||||
userRoleLibrary: RoleEntity[];
|
||||
/** 当前分镜的视频剧本片段 */
|
||||
shotVideoScript: ScriptSliceEntity[];
|
||||
shotVideoScript: ScriptSlice[];
|
||||
/** 加载状态 */
|
||||
loading: boolean;
|
||||
/** 错误信息 */
|
||||
@ -42,7 +48,9 @@ export interface UseShotService {
|
||||
/** 选择分镜并获取详情 */
|
||||
selectShot: (shotId: string) => Promise<void>;
|
||||
/** 修改分镜对话内容 */
|
||||
updateShotContent: (newContent: Array<{ roleId: string; content: string }>) => Promise<void>;
|
||||
updateShotContent: (
|
||||
newContent: Array<{ roleId: string; content: string }>
|
||||
) => Promise<void>;
|
||||
/** 修改分镜镜头 */
|
||||
updateShotShot: (newShot: string[]) => Promise<void>;
|
||||
/** 获取用户角色库 */
|
||||
@ -59,7 +67,7 @@ export interface UseShotService {
|
||||
/** 重新生成分镜 */
|
||||
regenerateShot: (
|
||||
shotPrompt: string,
|
||||
dialogueContent: string,
|
||||
dialogueContent: ContentItem[],
|
||||
roleReplaceParams: { oldId: string; newId: string }[],
|
||||
sceneReplaceParams: { oldId: string; newId: string }[]
|
||||
) => Promise<void>;
|
||||
@ -75,14 +83,15 @@ export const useShotService = (): UseShotService => {
|
||||
const [shotList, setShotList] = useState<ShotEntity[]>([]);
|
||||
const [selectedShot, setSelectedShot] = useState<ShotItem | null>(null);
|
||||
const [shotSketchData, setShotSketchData] = useState<string | null>(null);
|
||||
const [shotVideoData, setShotVideoData] = useState<string | null>(null);
|
||||
const [shotVideoData, setShotVideoData] = useState<string[] | null>(null);
|
||||
const [userRoleLibrary, setUserRoleLibrary] = useState<RoleEntity[]>([]);
|
||||
const [shotVideoScript, setShotVideoScript] = useState<ScriptSliceEntity[]>([]);
|
||||
const [shotVideoScript, setShotVideoScript] = useState<ScriptSlice[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [projectId, setProjectId] = useState<string>("");
|
||||
// UseCase实例
|
||||
const [shotEditUseCase, setShotEditUseCase] = useState<ShotEditUseCase | null>(null);
|
||||
const [shotEditUseCase, setShotEditUseCase] =
|
||||
useState<ShotEditUseCase | null>(null);
|
||||
|
||||
/**
|
||||
* 获取分镜列表
|
||||
@ -90,6 +99,7 @@ export const useShotService = (): UseShotService => {
|
||||
* @param projectId 项目ID
|
||||
*/
|
||||
const fetchShotList = useCallback(async (projectId: string) => {
|
||||
setProjectId(projectId);
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@ -100,7 +110,9 @@ export const useShotService = (): UseShotService => {
|
||||
setError(`获取分镜列表失败: ${response.message}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`获取分镜列表失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||
setError(
|
||||
`获取分镜列表失败: ${err instanceof Error ? err.message : "未知错误"}`
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -117,26 +129,28 @@ export const useShotService = (): UseShotService => {
|
||||
setError(null);
|
||||
|
||||
// 获取分镜详情
|
||||
const response = await getShotDetail({ shotId });
|
||||
if (response.successful) {
|
||||
const shotEntity = response.data;
|
||||
const shotItem = new ShotItem(shotEntity);
|
||||
setSelectedShot(shotItem);
|
||||
|
||||
// 初始化UseCase
|
||||
const newShotEditUseCase = new ShotEditUseCase(shotItem);
|
||||
setShotEditUseCase(newShotEditUseCase);
|
||||
|
||||
|
||||
|
||||
// 从分镜实体中获取草图数据和视频数据
|
||||
setShotSketchData(shotEntity.sketchUrl || null);
|
||||
setShotVideoData(shotEntity.videoUrl || null);
|
||||
} else {
|
||||
setError(`获取分镜详情失败: ${response.message}`);
|
||||
await fetchShotList(projectId);
|
||||
const shotEntity = shotList.find(
|
||||
(shot: ShotEntity) => shot.id === shotId
|
||||
);
|
||||
if (!shotEntity) {
|
||||
setError(`分镜不存在: ${shotId}`);
|
||||
return;
|
||||
}
|
||||
const shotItem = new ShotItem(shotEntity);
|
||||
setSelectedShot(shotItem);
|
||||
|
||||
// 初始化UseCase
|
||||
const newShotEditUseCase = new ShotEditUseCase(shotItem);
|
||||
setShotEditUseCase(newShotEditUseCase);
|
||||
|
||||
// 从分镜实体中获取草图数据和视频数据
|
||||
setShotSketchData(shotEntity.sketchUrl || null);
|
||||
setShotVideoData(shotEntity.videoUrl || null);
|
||||
} catch (err) {
|
||||
setError(`选择分镜失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||
setError(
|
||||
`选择分镜失败: ${err instanceof Error ? err.message : "未知错误"}`
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -147,54 +161,66 @@ export const useShotService = (): UseShotService => {
|
||||
* @description 更新分镜的对话内容,ContentItem数量和ID顺序不能变,只能修改content字段
|
||||
* @param newContent 新的对话内容数组
|
||||
*/
|
||||
const updateShotContentHandler = useCallback(async (newContent: Array<{ roleId: string; content: string }>) => {
|
||||
if (!shotEditUseCase) {
|
||||
setError('分镜编辑用例未初始化');
|
||||
return;
|
||||
}
|
||||
const updateShotContentHandler = useCallback(
|
||||
async (newContent: Array<{ roleId: string; content: string }>) => {
|
||||
if (!shotEditUseCase) {
|
||||
setError("分镜编辑用例未初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const updatedShot = await shotEditUseCase.updateShotContent(newContent);
|
||||
setSelectedShot(new ShotItem(updatedShot));
|
||||
} catch (err) {
|
||||
setError(`修改分镜对话内容失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [shotEditUseCase]);
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const updatedShot = await shotEditUseCase.updateShotContent(newContent);
|
||||
setSelectedShot(new ShotItem(updatedShot));
|
||||
} catch (err) {
|
||||
setError(
|
||||
`修改分镜对话内容失败: ${
|
||||
err instanceof Error ? err.message : "未知错误"
|
||||
}`
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[shotEditUseCase]
|
||||
);
|
||||
|
||||
/**
|
||||
* 修改分镜镜头
|
||||
* @description 更新分镜的镜头数据
|
||||
* @param newShot 新的镜头数据
|
||||
*/
|
||||
const updateShotShotHandler = useCallback(async (newShot: string[]) => {
|
||||
if (!selectedShot) {
|
||||
setError('未选择分镜');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await updateShotShot({
|
||||
shotId: selectedShot.entity.id,
|
||||
shot: newShot
|
||||
});
|
||||
if (response.successful) {
|
||||
const updatedShot = response.data;
|
||||
setSelectedShot(new ShotItem(updatedShot));
|
||||
} else {
|
||||
setError(`修改分镜镜头失败: ${response.message}`);
|
||||
const updateShotShotHandler = useCallback(
|
||||
async (newShot: string[]) => {
|
||||
if (!selectedShot) {
|
||||
setError("未选择分镜");
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`修改分镜镜头失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedShot]);
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await updateShotShot({
|
||||
shotId: selectedShot.entity.id,
|
||||
shot: newShot,
|
||||
});
|
||||
if (response.successful) {
|
||||
const updatedShot = response.data;
|
||||
setSelectedShot(new ShotItem(updatedShot));
|
||||
} else {
|
||||
setError(`修改分镜镜头失败: ${response.message}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(
|
||||
`修改分镜镜头失败: ${err instanceof Error ? err.message : "未知错误"}`
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[selectedShot]
|
||||
);
|
||||
|
||||
/**
|
||||
* 获取用户角色库
|
||||
@ -211,7 +237,9 @@ export const useShotService = (): UseShotService => {
|
||||
setError(`获取用户角色库失败: ${response.message}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`获取用户角色库失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||
setError(
|
||||
`获取用户角色库失败: ${err instanceof Error ? err.message : "未知错误"}`
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -223,32 +251,37 @@ export const useShotService = (): UseShotService => {
|
||||
* @param oldRoleId 旧角色ID
|
||||
* @param newRoleId 新角色ID
|
||||
*/
|
||||
const replaceShotRoleHandler = useCallback(async (oldRoleId: string, newRoleId: string) => {
|
||||
if (!selectedShot) {
|
||||
setError('未选择分镜');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await replaceShotRole({
|
||||
shotId: selectedShot.entity.id,
|
||||
oldRoleId,
|
||||
newRoleId
|
||||
});
|
||||
if (response.successful) {
|
||||
// 重新获取分镜详情
|
||||
await selectShot(selectedShot.entity.id);
|
||||
} else {
|
||||
setError(`替换分镜角色失败: ${response.message}`);
|
||||
const replaceShotRoleHandler = useCallback(
|
||||
async (oldRoleId: string, newRoleId: string) => {
|
||||
if (!selectedShot) {
|
||||
setError("未选择分镜");
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`替换分镜角色失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedShot, selectShot]);
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await replaceShotRole({
|
||||
shotId: selectedShot.entity.id,
|
||||
oldRoleId,
|
||||
newRoleId,
|
||||
});
|
||||
if (response.successful) {
|
||||
// 重新获取分镜详情
|
||||
await selectShot(selectedShot.entity.id);
|
||||
} else {
|
||||
setError(`替换分镜角色失败: ${response.message}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(
|
||||
`替换分镜角色失败: ${err instanceof Error ? err.message : "未知错误"}`
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[selectedShot, selectShot]
|
||||
);
|
||||
|
||||
/**
|
||||
* 获取分镜视频剧本内容
|
||||
@ -256,21 +289,28 @@ export const useShotService = (): UseShotService => {
|
||||
*/
|
||||
const fetchShotVideoScript = useCallback(async () => {
|
||||
if (!selectedShot) {
|
||||
setError('未选择分镜');
|
||||
setError("未选择分镜");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await getShotVideoScript({ shotId: selectedShot.entity.id });
|
||||
const response = await getShotVideoScript({
|
||||
shotId: selectedShot.entity.id,
|
||||
});
|
||||
if (response.successful) {
|
||||
setShotVideoScript(response.data);
|
||||
const script = new ScriptValueObject(response.data);
|
||||
setShotVideoScript(script.scriptSlices);
|
||||
} else {
|
||||
setError(`获取分镜视频剧本失败: ${response.message}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`获取分镜视频剧本失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||
setError(
|
||||
`获取分镜视频剧本失败: ${
|
||||
err instanceof Error ? err.message : "未知错误"
|
||||
}`
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -283,7 +323,7 @@ export const useShotService = (): UseShotService => {
|
||||
*/
|
||||
const getShotRoles = useCallback(async (): Promise<RoleEntity[]> => {
|
||||
if (!shotEditUseCase) {
|
||||
throw new Error('分镜编辑用例未初始化');
|
||||
throw new Error("分镜编辑用例未初始化");
|
||||
}
|
||||
return await shotEditUseCase.getShotRoles();
|
||||
}, [shotEditUseCase]);
|
||||
@ -295,13 +335,11 @@ export const useShotService = (): UseShotService => {
|
||||
*/
|
||||
const getShotScenes = useCallback(async (): Promise<SceneEntity[]> => {
|
||||
if (!shotEditUseCase) {
|
||||
throw new Error('分镜编辑用例未初始化');
|
||||
throw new Error("分镜编辑用例未初始化");
|
||||
}
|
||||
return await shotEditUseCase.getShotScenes();
|
||||
}, [shotEditUseCase]);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 重新生成分镜
|
||||
* @description 使用镜头、对话内容、角色ID替换参数、场景ID替换参数重新生成分镜
|
||||
@ -310,33 +348,38 @@ export const useShotService = (): UseShotService => {
|
||||
* @param roleReplaceParams 角色ID替换参数,格式为{oldId:string,newId:string}[]
|
||||
* @param sceneReplaceParams 场景ID替换参数,格式为{oldId:string,newId:string}[]
|
||||
*/
|
||||
const regenerateShot = useCallback(async (
|
||||
shotPrompt: string,
|
||||
dialogueContent: string,
|
||||
roleReplaceParams: { oldId: string; newId: string }[],
|
||||
sceneReplaceParams: { oldId: string; newId: string }[]
|
||||
) => {
|
||||
if (!shotEditUseCase) {
|
||||
setError('分镜编辑用例未初始化');
|
||||
return;
|
||||
}
|
||||
const regenerateShot = useCallback(
|
||||
async (
|
||||
shotPrompt: string,
|
||||
dialogueContent: ContentItem[],
|
||||
roleReplaceParams: { oldId: string; newId: string }[],
|
||||
sceneReplaceParams: { oldId: string; newId: string }[]
|
||||
) => {
|
||||
if (!shotEditUseCase) {
|
||||
setError("分镜编辑用例未初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const updatedShot = await shotEditUseCase.regenerateShot(
|
||||
shotPrompt,
|
||||
dialogueContent,
|
||||
roleReplaceParams,
|
||||
sceneReplaceParams
|
||||
);
|
||||
setSelectedShot(new ShotItem(updatedShot));
|
||||
} catch (err) {
|
||||
setError(`重新生成分镜失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [shotEditUseCase]);
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const updatedShot = await shotEditUseCase.regenerateShot(
|
||||
shotPrompt,
|
||||
dialogueContent,
|
||||
roleReplaceParams,
|
||||
sceneReplaceParams
|
||||
);
|
||||
setSelectedShot(new ShotItem(updatedShot));
|
||||
} catch (err) {
|
||||
setError(
|
||||
`重新生成分镜失败: ${err instanceof Error ? err.message : "未知错误"}`
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[shotEditUseCase]
|
||||
);
|
||||
|
||||
return {
|
||||
// 响应式状态 - 用于UI组件订阅和渲染
|
||||
|
||||
@ -65,13 +65,15 @@ export interface SceneEntity extends BaseEntity {
|
||||
/** 场景提示词Id */
|
||||
generateTextId: string;
|
||||
}
|
||||
|
||||
/**对话内容项 */
|
||||
interface ContentItem {
|
||||
export interface ContentItem {
|
||||
/** 角色ID */
|
||||
roleId: string;
|
||||
/** 对话内容 */
|
||||
content: string;
|
||||
}
|
||||
/**分镜进度 */
|
||||
export enum ShotStatus {
|
||||
/** 草稿加载中 */
|
||||
sketchLoading = 0,
|
||||
@ -89,7 +91,7 @@ export interface ShotEntity extends BaseEntity {
|
||||
/**分镜草图Url */
|
||||
sketchUrl: string;
|
||||
/**分镜视频Url */
|
||||
videoUrl: string;
|
||||
videoUrl: string[];
|
||||
/**分镜状态 */
|
||||
status: ShotStatus;
|
||||
/**角色ID列表 */
|
||||
@ -100,30 +102,8 @@ export interface ShotEntity extends BaseEntity {
|
||||
content: ContentItem[];
|
||||
/**镜头项 */
|
||||
shot: string[];
|
||||
/**分镜剧本 */
|
||||
/**分镜剧本Id */
|
||||
scriptId: string;
|
||||
}
|
||||
|
||||
/**不同类型 将有不同元数据 */
|
||||
export enum ScriptSliceType {
|
||||
/** 文本 */
|
||||
text = 'text',
|
||||
/** 高亮 */
|
||||
highlight = 'highlight',
|
||||
/**角色 */
|
||||
role = 'role',
|
||||
/** 场景 */
|
||||
scene = 'scene',
|
||||
|
||||
}
|
||||
/**
|
||||
* 剧本片段实体接口
|
||||
*/
|
||||
export interface ScriptSliceEntity extends BaseEntity {
|
||||
/** 类型 */
|
||||
type: ScriptSliceType;
|
||||
/** 剧本内容 */
|
||||
text: string;
|
||||
/** 元数据 */
|
||||
metaData: any;
|
||||
}
|
||||
|
||||
78
app/service/domain/valueObject.ts
Normal file
78
app/service/domain/valueObject.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/**不同类型 将有不同元数据 */
|
||||
|
||||
export enum ScriptSliceType {
|
||||
/** 文本 */
|
||||
text = "text",
|
||||
/**角色 */
|
||||
role = "role",
|
||||
/** 场景 */
|
||||
scene = "scene",
|
||||
}
|
||||
/**
|
||||
* 剧本片段值对象接口
|
||||
*/
|
||||
export interface ScriptSlice {
|
||||
/**唯一标识符 */
|
||||
readonly id: string;
|
||||
/** 类型 */
|
||||
type: ScriptSliceType;
|
||||
/** 剧本内容 */
|
||||
text: string;
|
||||
/** 元数据 */
|
||||
metaData: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 剧本 值对象,将剧本文本转换为剧本对象
|
||||
* @return {*}
|
||||
*/
|
||||
export class ScriptValueObject {
|
||||
scriptSlices: ScriptSlice[] = [];
|
||||
|
||||
/**
|
||||
* @description: 构造函数,初始化剧本
|
||||
* @param scriptText 剧本文本字符串
|
||||
*/
|
||||
constructor(scriptText?: string) {
|
||||
if (scriptText) {
|
||||
this.parseFromString(scriptText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 从字符串解析剧本片段
|
||||
* @param scriptText 剧本文本字符串
|
||||
*/
|
||||
parseFromString(scriptText: string): void {
|
||||
// TODO: 实现字符串解析逻辑
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 检测剧本片段类型
|
||||
* @param text 文本内容
|
||||
* @returns ScriptSliceType
|
||||
*/
|
||||
private detectScriptType(text: string): ScriptSliceType {
|
||||
// TODO: 实现类型检测逻辑
|
||||
return ScriptSliceType.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 提取元数据
|
||||
* @param text 文本内容
|
||||
* @returns any
|
||||
*/
|
||||
private extractMetadata(text: string): any {
|
||||
// TODO: 实现元数据提取逻辑
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 将剧本片段转换为字符串
|
||||
* @returns string
|
||||
*/
|
||||
toString(): string {
|
||||
return this.scriptSlices.map((slice) => slice.text).join("");
|
||||
}
|
||||
|
||||
}
|
||||
124
app/service/usecase/ScriptEditUseCase.ts
Normal file
124
app/service/usecase/ScriptEditUseCase.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { ScriptSlice, ScriptValueObject } from "../domain/valueObject";
|
||||
import { generateScriptStream, applyScriptToShot } from "@/api/video_flow";
|
||||
|
||||
export class ScriptEditUseCase {
|
||||
loading: boolean = false;
|
||||
private scriptValueObject: ScriptValueObject;
|
||||
|
||||
constructor(script: string) {
|
||||
this.scriptValueObject = new ScriptValueObject(script);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: AI生成剧本方法
|
||||
* @param prompt 剧本提示词
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async generateScript(prompt: string): Promise<void> {
|
||||
try {
|
||||
this.loading = true;
|
||||
|
||||
// 使用API接口生成剧本
|
||||
const response = await generateScriptStream({
|
||||
prompt,
|
||||
});
|
||||
|
||||
if (!response.successful) {
|
||||
throw new Error(response.message || "AI生成剧本失败");
|
||||
}
|
||||
|
||||
// 使用for await处理流式数据
|
||||
for await (const data of response.data) {
|
||||
// TODO: 根据流式数据更新剧本片段
|
||||
// 这里需要根据实际的流式数据格式来处理
|
||||
// 可能需要将流式数据转换为ScriptSlice并添加到scriptValueObject中
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("AI生成剧本出错:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 应用剧本方法
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async applyScript(): Promise<void> {
|
||||
try {
|
||||
this.loading = true;
|
||||
|
||||
// 调用应用剧本接口
|
||||
const response = await applyScriptToShot({
|
||||
script: this.scriptValueObject.toString(),
|
||||
});
|
||||
|
||||
if (!response.successful) {
|
||||
throw new Error(response.message || "应用剧本失败");
|
||||
}
|
||||
|
||||
console.log("剧本应用成功");
|
||||
} catch (error) {
|
||||
console.error("应用剧本失败:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 获取当前剧本片段列表
|
||||
* @returns ScriptSlice[]
|
||||
*/
|
||||
getScriptSlices(): ScriptSlice[] {
|
||||
return this.scriptValueObject.scriptSlices;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 获取加载状态
|
||||
* @returns boolean
|
||||
*/
|
||||
isLoading(): boolean {
|
||||
return this.loading;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 更新剧本
|
||||
* @param scriptText 剧本文本字符串
|
||||
*/
|
||||
updateScript(scriptText: string): void {
|
||||
this.scriptValueObject = new ScriptValueObject(scriptText);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 将当前剧本片段转换为字符串
|
||||
* @returns string
|
||||
*/
|
||||
toString(): string {
|
||||
return this.scriptValueObject.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 更新指定ID的剧本片段
|
||||
* @param id 剧本片段唯一标识
|
||||
* @param text 新的文本内容
|
||||
* @param metaData 新的元数据
|
||||
* @returns boolean 是否更新成功
|
||||
*/
|
||||
updateScriptSlice(id: string, text: string, metaData?: Record<string, any>): boolean {
|
||||
const index = this.scriptValueObject.scriptSlices.findIndex(slice => slice.id === id);
|
||||
if (index === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 只更新text和metaData字段,保持其他字段不变
|
||||
this.scriptValueObject.scriptSlices[index] = {
|
||||
...this.scriptValueObject.scriptSlices[index],
|
||||
text,
|
||||
metaData: metaData || this.scriptValueObject.scriptSlices[index].metaData
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { ShotEntity, RoleEntity, SceneEntity, AITextEntity, TagEntity } from '../domain/Entities';
|
||||
import { ShotEntity, RoleEntity, SceneEntity, AITextEntity, TagEntity, ContentItem } from '../domain/Entities';
|
||||
import { ShotItem, RoleItem, SceneItem, TextItem, TagItem } from '../domain/Item';
|
||||
import {
|
||||
getShotRoles,
|
||||
@ -116,7 +116,7 @@ export class ShotEditUseCase {
|
||||
*/
|
||||
async regenerateShot(
|
||||
shotPrompt: string,
|
||||
dialogueContent: string,
|
||||
dialogueContent: ContentItem[],
|
||||
roleReplaceParams: { oldId: string; newId: string }[],
|
||||
sceneReplaceParams: { oldId: string; newId: string }[]
|
||||
): Promise<ShotEntity> {
|
||||
|
||||
0
app/service/usecase/index.ts
Normal file
0
app/service/usecase/index.ts
Normal file
11
utils/tools.ts
Normal file
11
utils/tools.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { ScriptSlice, ScriptSliceType } from "@/app/service/domain/valueObject";
|
||||
|
||||
export function parseScriptEntity(text: string):ScriptSlice {
|
||||
const scriptSlice:ScriptSlice={
|
||||
type:ScriptSliceType.text,
|
||||
text:text,
|
||||
metaData:{}
|
||||
|
||||
}
|
||||
return scriptSlice;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user