diff --git a/api/video_flow.ts b/api/video_flow.ts index 6580e3c..03f4e5b 100644 --- a/api/video_flow.ts +++ b/api/video_flow.ts @@ -2,6 +2,7 @@ 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 } from '@/app/service/domain/Entities'; // API 响应类型 interface BaseApiResponse { @@ -86,7 +87,7 @@ export interface Character { // 剧本到分镜头提示词模型 export interface ScenePrompts { scenes: Scene[]; // 分场景列表 - characters?: Character[]; // 角色列表 + characters?: Character[]; // 角色列表 summary?: string; // 剧情概要 scene?: string; // 场景描述 atmosphere?: string; // 氛围描述 @@ -102,7 +103,7 @@ export interface ScriptToSceneRequest { project_type: ProjectTypeEnum.SCRIPT_TO_VIDEO; } -// 视频转分镜头请求接口 +// 视频转分镜头请求接口 export interface VideoToSceneRequest { video_url: string; episode_id: number; @@ -183,8 +184,8 @@ export const convertVideoToScene = async ( // 新-获取剧集详情 export const detailScriptEpisodeNew = async (data: { project_id: string }): Promise> => { - return post>('/movie/get_movie_project_detail', data); -}; + return post>('/movie/get_movie_project_detail', data); +}; // 获取 title 接口 export const getScriptTitle = async (data: { project_id: string }): Promise> => { @@ -214,4 +215,127 @@ export const getShotSketchJson = async (data: { project_id: string }): Promise> => { return post('/movie/video_json', data); -}; \ No newline at end of file +}; + +/** + * 重新生成角色 + * @param request - 重新生成角色请求参数 + * @returns Promise> + */ +export const regenerateRole = async (request: { + /** 角色提示词 */ + prompt: string; + /** 标签类型列表 */ + tagTypes: (number | string)[]; + /** 角色ID(可选,如果重新生成现有角色) */ + roleId?: string; +}): Promise> => { + return post>('/movie/regenerate_role', request); +}; + +/** + * 应用角色到分镜 + * @param request - 应用角色请求参数 + * @returns Promise> + */ +export const applyRoleToShots = async (request: { + /** 角色ID */ + roleId: string; + /** 分镜ID列表 */ + shotIds: string[]; +}): Promise> => { + return post>('/movie/apply_role_to_shots', request); +}; + +/** + * 修改标签 + * @param request - 修改标签请求参数 + * @returns Promise> + */ +export const updateTag = async (request: { + /** 标签ID */ + tagId: string; + /** 新的标签内容 */ + content: string|number; +}): Promise> => { + return post>('/movie/update_tag', request); +}; + +/** + * 修改文案 + * @param request - 修改文案请求参数 + * @returns Promise> + */ +export const updateText = async (request: { + /** 文案ID */ + textId: string; + /** 新的文案内容 */ + content: string; +}): Promise> => { + return post>('/movie/update_text', request); +}; + +/** + * 重新生成场景 + * @param request - 重新生成场景请求参数 + * @returns Promise> + */ +export const regenerateScene = async (request: { + /** 场景提示词 */ + prompt: string; + /** 标签类型列表 */ + tagTypes: (number | string)[]; + /** 场景ID(可选,如果重新生成现有场景) */ + sceneId?: string; +}): Promise> => { + return post>('/movie/regenerate_scene', request); +}; + +/** + * 应用场景到分镜 + * @param request - 应用场景请求参数 + * @returns Promise> + */ +export const applySceneToShots = async (request: { + /** 场景ID */ + sceneId: string; + /** 分镜ID列表 */ + shotIds: string[]; +}): Promise> => { + return post>('/movie/apply_scene_to_shots', request); +}; + +/** + * 获取角色应用到的分镜列表 + * @param request - 获取角色分镜列表请求参数 + * @returns Promise> + */ +export const getRoleShots = async (request: { + /** 角色ID */ + roleId: string; +}): Promise> => { + return post>('/movie/get_role_shots', request); +}; + +/** + * 获取场景应用到的分镜列表 + * @param request - 获取场景分镜列表请求参数 + * @returns Promise> + */ +export const getSceneShots = async (request: { + /** 场景ID */ + sceneId: string; +}): Promise> => { + return post>('/movie/get_scene_shots', request); +}; + diff --git a/app/service/Interaction/RoleService.puml b/app/service/Interaction/RoleService.puml new file mode 100644 index 0000000..4d470a2 --- /dev/null +++ b/app/service/Interaction/RoleService.puml @@ -0,0 +1,151 @@ +@startuml RoleService Hook 架构图 + +!theme plain +skinparam backgroundColor #FFFFFF +skinparam componentStyle rectangle + +' 主要模块 +package "RoleService Hook" as RoleServiceHook { + component "响应式状态管理" as StateManagement + component "计算属性" as ComputedProps + component "角色操作方法" as RoleOperations + component "文本操作方法" as TextOperations + component "标签操作方法" as TagOperations + component "分镜操作方法" as ShotOperations +} + +' API层 +package "API接口层" as APILayer { + component "角色相关API" as RoleAPI { + [regenerateRole()] + [applyRoleToShots()] + [getRoleShots()] + } + + component "文本相关API" as TextAPI { + [updateText()] + } + + component "标签相关API" as TagAPI { + [updateTag()] + } +} + +' UseCase层 +package "UseCase层" as UseCaseLayer { + component "RoleEditUseCase" as RoleEditUseCase { + [AIgenerateRole(prompt, tags)] + [applyRole(shotIds)] + } + + component "TextEditUseCase" as TextEditUseCase { + [updateText(content)] + [getOptimizedContent()] + } + + component "TagEditUseCase" as TagEditUseCase { + [updateTag(content)] + } +} + +' Domain层 +package "Domain层" as DomainLayer { + component "实体定义" as Entities { + [RoleEntity] + [AITextEntity] + [TagEntity] + [ShotEntity] + } + + component "可编辑项" as Items { + [RoleItem] + [TextItem] + [TagItem] + [ShotItem] + } +} + +' React Hook +package "React Hook" as ReactHook { + component "useState" as UseState + component "useCallback" as UseCallback + component "useMemo" as UseMemo +} + +' 依赖关系 +' Hook内部依赖 +RoleServiceHook --> StateManagement : 管理状态 +RoleServiceHook --> ComputedProps : 计算属性 +RoleServiceHook --> RoleOperations : 角色操作 +RoleServiceHook --> TextOperations : 文本操作 +RoleServiceHook --> TagOperations : 标签操作 +RoleServiceHook --> ShotOperations : 分镜操作 + +' 操作方法依赖UseCase +RoleOperations --> RoleEditUseCase : 调用 +TextOperations --> TextEditUseCase : 调用 +TagOperations --> TagEditUseCase : 调用 +ShotOperations --> RoleEditUseCase : 调用 + +' UseCase依赖API +RoleEditUseCase --> RoleAPI : 调用 +TextEditUseCase --> TextAPI : 调用 +TagEditUseCase --> TagAPI : 调用 + +' 状态管理依赖Domain +StateManagement --> Items : 使用 +ComputedProps --> Entities : 计算 +RoleOperations --> Items : 操作 +TextOperations --> Items : 操作 +TagOperations --> Items : 操作 +ShotOperations --> Items : 操作 + +' React Hook依赖 +RoleServiceHook --> UseState : 状态管理 +RoleServiceHook --> UseCallback : 方法优化 +RoleServiceHook --> UseMemo : 计算优化 + +' 数据流 +note right of StateManagement + 响应式状态: + - roleList: 角色列表 + - selectedRole: 当前选中角色 + - currentRoleText: 当前AI文本 + - currentRoleTags: 当前标签列表 + - shotSelectionList: 分镜选择列表 +end note + +note right of ComputedProps + 计算属性: + - roleImageUrl: 角色图片URL + - isAllShotsSelected: 是否全选 + - selectedShotsCount: 选中数量 +end note + +note right of RoleOperations + 角色操作: + - selectRole: 选择角色 + - regenerateRole: 重新生成 +end note + +note right of TextOperations + 文本操作: + - optimizeRoleText: 优化文本 + - updateRoleText: 修改文本 +end note + +note right of TagOperations + 标签操作: + - updateTagContent: 修改标签 +end note + +note right of ShotOperations + 分镜操作: + - fetchRoleShots: 获取分镜列表 + - selectAllShots: 全选 + - invertShotSelection: 反选 + - toggleShotSelection: 切换选择 + - applyRoleToSelectedShots: 应用角色 +end note + +@enduml diff --git a/app/service/Interaction/RoleService.ts b/app/service/Interaction/RoleService.ts new file mode 100644 index 0000000..908ab31 --- /dev/null +++ b/app/service/Interaction/RoleService.ts @@ -0,0 +1,391 @@ +import { useState, useCallback, useMemo } from 'react'; +import { RoleEntity, TagEntity, AITextEntity, ShotEntity } from '../domain/Entities'; +import { RoleItem, TagItem, TextItem, ShotItem } from '../domain/Item'; +import { RoleEditUseCase } from '../usecase/RoleEditUseCase'; +import { TagEditUseCase } from '../usecase/TagEditUseCase'; +import { TextEditUseCase } from '../usecase/TextEditUseCase'; +import { getRoleShots } from '@/api/video_flow'; + +/** + * 分镜选择项接口 + */ +interface ShotSelectionItem { + /** 分镜ID */ + id: string; + /** 分镜名称 */ + name: string; + /** 是否已选中 */ + selected: boolean; + /** 是否已应用角色 */ + applied: boolean; + /** 分镜数据 */ + shot: ShotEntity; +} + +/** + * 角色服务Hook返回值接口 + */ +interface UseRoleService { + // 响应式数据 + /** 角色列表 */ + roleList: RoleItem[]; + /** 当前选中的角色 */ + selectedRole: RoleItem | null; + /** 当前角色的AI文本 */ + currentRoleText: TextItem | null; + /** 当前角色的标签列表 */ + currentRoleTags: TagItem[]; + /** 角色图片URL */ + roleImageUrl: string; + /** 分镜选择列表 */ + shotSelectionList: ShotSelectionItem[]; + /** 是否全选分镜 */ + isAllShotsSelected: boolean; + /** 已选中的分镜数量 */ + selectedShotsCount: number; + + // 操作方法 + /** 选择角色 */ + selectRole: (roleId: string) => void; + /** 设置当前角色的AI文本和标签 */ + setCurrentRoleData: (text: TextItem, tags: TagItem[]) => void; + /** 优化AI文本 */ + optimizeRoleText: () => Promise; + /** 修改AI文本 */ + updateRoleText: (newContent: string) => Promise; + /** 修改标签内容 */ + updateTagContent: (tagId: string, newContent: string | number) => Promise; + /** 重新生成角色 */ + regenerateRole: () => Promise; + /** 获取角色出现的分镜列表 */ + fetchRoleShots: () => Promise; + /** 切换全选与全不选 */ + toggleSelectAllShots: () => void; + /** 选择/取消选择单个分镜 */ + toggleShotSelection: (shotId: string) => void; + /** 应用角色到选中的分镜 */ + applyRoleToSelectedShots: () => Promise; +} + +/** + * 角色服务Hook + * 提供角色相关的所有响应式功能和业务逻辑 + */ +export const useRoleServiceHook = (): UseRoleService => { + // 响应式状态 + const [roleList, setRoleList] = useState([]); + const [selectedRole, setSelectedRole] = useState(null); + const [currentRoleText, setCurrentRoleText] = useState(null); + const [currentRoleTags, setCurrentRoleTags] = useState([]); + const [shotSelectionList, setShotSelectionList] = useState([]); + + // UseCase实例 - 在角色选择时初始化 + const [roleEditUseCase, setRoleEditUseCase] = useState(null); + const [textEditUseCase, setTextEditUseCase] = useState(null); + const [tagEditUseCases, setTagEditUseCases] = useState>(new Map()); + + // 计算属性 + /** + * 角色图片URL + * @description 获取当前选中角色的图片URL + */ + const roleImageUrl = useMemo(() => { + return selectedRole?.entity.imageUrl || ''; + }, [selectedRole]); + + /** + * 是否全选分镜 + * @description 判断是否所有分镜都被选中 + */ + const isAllShotsSelected = useMemo(() => { + return shotSelectionList.length > 0 && shotSelectionList.every(shot => shot.selected); + }, [shotSelectionList]); + + /** + * 已选中的分镜数量 + * @description 获取当前选中的分镜数量 + */ + const selectedShotsCount = useMemo(() => { + return shotSelectionList.filter(shot => shot.selected).length; + }, [shotSelectionList]); + + /** + * 选择角色 + * @description 根据角色ID选择角色,并初始化相关的UseCase实例 + * @param roleId 角色ID + */ + const selectRole = useCallback((roleId: string) => { + const role = roleList.find(r => r.entity.id === roleId); + if (role) { + setSelectedRole(role); + + // 初始化UseCase实例 + setRoleEditUseCase(new RoleEditUseCase(role)); + setTextEditUseCase(null); // 文本UseCase在获取到文本后初始化 + setTagEditUseCases(new Map()); // 标签UseCase在获取到标签后初始化 + + setCurrentRoleText(null); + setCurrentRoleTags([]); + } + }, [roleList]); + + /** + * 优化AI文本 + * @description 对当前角色的AI文本进行优化,无文本时不可进行优化 + * @throws {Error} 当没有可优化的文本内容或UseCase未初始化时抛出错误 + * @returns {Promise} 优化完成后的Promise + */ + const optimizeRoleText = useCallback(async () => { + if (!textEditUseCase) { + throw new Error('文本编辑UseCase未初始化'); + } + + if (!currentRoleText || !currentRoleText.entity.content) { + throw new Error('没有可优化的文本内容'); + } + + const optimizedContent = await textEditUseCase.getOptimizedContent(); + + // 更新文本内容 + const updatedTextItem = await textEditUseCase.updateText(optimizedContent); + setCurrentRoleText(updatedTextItem); + }, [textEditUseCase, currentRoleText]); + + /** + * 修改AI文本 + * @description 手动修改当前角色的AI文本内容 + * @param newContent 新的文本内容 + * @throws {Error} 当没有可编辑的文本或UseCase未初始化时抛出错误 + * @returns {Promise} 修改完成后的Promise + */ + const updateRoleText = useCallback(async (newContent: string) => { + if (!textEditUseCase) { + throw new Error('文本编辑UseCase未初始化'); + } + + if (!currentRoleText) { + throw new Error('没有可编辑的文本'); + } + + const updatedTextItem = await textEditUseCase.updateText(newContent); + setCurrentRoleText(updatedTextItem); + }, [textEditUseCase, currentRoleText]); + + /** + * 修改标签内容 + * @description 修改指定标签的内容 + * @param tagId 标签ID + * @param newContent 新的标签内容 + * @throws {Error} 当标签不存在或UseCase未初始化时抛出错误 + * @returns {Promise} 修改完成后的Promise + */ + const updateTagContent = useCallback(async (tagId: string, newContent: string | number) => { + const tagEditUseCase = tagEditUseCases.get(tagId); + if (!tagEditUseCase) { + throw new Error(`标签编辑UseCase未初始化,标签ID: ${tagId}`); + } + + const updatedTagItem = await tagEditUseCase.updateTag(newContent); + + // 更新标签列表 + setCurrentRoleTags(prev => + prev.map(tag => + tag.entity.id === tagId + ? updatedTagItem + : tag + ) + ); + }, [tagEditUseCases]); + + /** + * 重新生成角色 + * @description 使用AI文本和标签重新生成角色 + * @throws {Error} 当缺少重新生成角色所需的数据或UseCase未初始化时抛出错误 + * @returns {Promise} 重新生成完成后的Promise + */ + const regenerateRole = useCallback(async () => { + if (!roleEditUseCase) { + throw new Error('角色编辑UseCase未初始化'); + } + + if (!selectedRole || !currentRoleText || currentRoleTags.length === 0) { + throw new Error('缺少重新生成角色所需的数据'); + } + + const newRoleEntity = await roleEditUseCase.AIgenerateRole(currentRoleText, currentRoleTags); + + // 更新角色 + const newRoleItem = new RoleItem(newRoleEntity); + setSelectedRole(newRoleItem); + + // 更新角色列表 + setRoleList(prev => + prev.map(role => + role.entity.id === newRoleEntity.id ? newRoleItem : role + ) + ); + }, [roleEditUseCase, selectedRole, currentRoleText, currentRoleTags]); + + /** + * 获取角色出现的分镜列表 + * @description 获取当前角色应用到的分镜列表,包括已应用状态 + * @throws {Error} 当未选择角色或API调用失败时抛出错误 + * @returns {Promise} 获取完成后的Promise + */ + const fetchRoleShots = useCallback(async () => { + if (!selectedRole) { + throw new Error('请先选择角色'); + } + + try { + const response = await getRoleShots({ + roleId: selectedRole.entity.id + }); + + if (response.successful) { + const { shots, appliedShotIds } = response.data; + + const shotSelectionItems: ShotSelectionItem[] = shots.map(shot => ({ + id: shot.id, + name: shot.name, + selected: false, + applied: appliedShotIds.includes(shot.id), // 根据API返回的已应用列表判断 + shot + })); + + setShotSelectionList(shotSelectionItems); + } else { + throw new Error(`获取角色分镜列表失败: ${response.message}`); + } + } catch (error) { + console.error('获取角色分镜列表失败:', error); + throw error; + } + }, [selectedRole]); + + /** + * 切换全选与全不选 + * @description 如果当前是全选状态则全不选,否则全选 + */ + const toggleSelectAllShots = useCallback(() => { + setShotSelectionList(prev => { + const isAllSelected = prev.length > 0 && prev.every(shot => shot.selected); + return prev.map(shot => ({ ...shot, selected: !isAllSelected })); + }); + }, []); + + /** + * 选择/取消选择单个分镜 + * @description 切换指定分镜的选择状态 + * @param shotId 分镜ID + */ + const toggleShotSelection = useCallback((shotId: string) => { + setShotSelectionList(prev => + prev.map(shot => + shot.id === shotId + ? { ...shot, selected: !shot.selected } + : shot + ) + ); + }, []); + + /** + * 应用角色到选中的分镜 + * @description 将当前角色应用到选中的分镜,并更新应用状态 + * @throws {Error} 当未选择角色、未选择分镜或UseCase未初始化时抛出错误 + * @returns {Promise} 应用完成后的Promise + */ + const applyRoleToSelectedShots = useCallback(async () => { + if (!roleEditUseCase) { + throw new Error('角色编辑UseCase未初始化'); + } + + if (!selectedRole) { + throw new Error('请先选择角色'); + } + + const selectedShotIds = shotSelectionList + .filter(shot => shot.selected) + .map(shot => shot.id); + + if (selectedShotIds.length === 0) { + throw new Error('请先选择要应用的分镜'); + } + + await roleEditUseCase.applyRole(selectedShotIds); + + // 更新分镜列表,标记已应用 + setShotSelectionList(prev => + prev.map(shot => + selectedShotIds.includes(shot.id) + ? { ...shot, applied: true, selected: false } + : shot + ) + ); + }, [roleEditUseCase, selectedRole, shotSelectionList]); + + /** + * 设置当前角色的AI文本和标签 + * @description 设置当前角色的AI文本和标签,并初始化对应的UseCase + * @param text AI文本项 + * @param tags 标签项列表 + */ + const setCurrentRoleData = useCallback((text: TextItem, tags: TagItem[]) => { + setCurrentRoleText(text); + setCurrentRoleTags(tags); + + // 初始化文本UseCase + if (text) { + setTextEditUseCase(new TextEditUseCase(text)); + } + + // 初始化标签UseCase + const newTagEditUseCases = new Map(); + tags.forEach(tag => { + newTagEditUseCases.set(tag.entity.id, new TagEditUseCase(tag)); + }); + setTagEditUseCases(newTagEditUseCases); + }, []); + + return { + // 响应式数据 + /** 角色列表 */ + roleList, + /** 当前选中的角色 */ + selectedRole, + /** 当前角色的AI文本 */ + currentRoleText, + /** 当前角色的标签列表 */ + currentRoleTags, + /** 角色图片URL */ + roleImageUrl, + /** 分镜选择列表 */ + shotSelectionList, + /** 是否全选分镜 */ + isAllShotsSelected, + /** 已选中的分镜数量 */ + selectedShotsCount, + + // 操作方法 + /** 选择角色 */ + selectRole, + /** 设置当前角色的AI文本和标签 */ + setCurrentRoleData, + /** 优化AI文本 */ + optimizeRoleText, + /** 修改AI文本 */ + updateRoleText, + /** 修改标签内容 */ + updateTagContent, + /** 重新生成角色 */ + regenerateRole, + /** 获取角色出现的分镜列表 */ + fetchRoleShots, + /** 切换全选与全不选 */ + toggleSelectAllShots, + /** 选择/取消选择单个分镜 */ + toggleShotSelection, + /** 应用角色到选中的分镜 */ + applyRoleToSelectedShots + }; +}; diff --git a/app/service/Interaction/SceneService.ts b/app/service/Interaction/SceneService.ts new file mode 100644 index 0000000..0fd927c --- /dev/null +++ b/app/service/Interaction/SceneService.ts @@ -0,0 +1,301 @@ +import { useState, useCallback, useMemo } from 'react'; +import { SceneEntity, TagEntity, AITextEntity, ShotEntity } from '../domain/Entities'; +import { SceneItem, TagItem, TextItem, ShotItem } from '../domain/Item'; +import { SceneEditUseCase } from '../usecase/SceneEditUseCase'; +import { TagEditUseCase } from '../usecase/TagEditUseCase'; +import { TextEditUseCase } from '../usecase/TextEditUseCase'; +import { getSceneShots } from '@/api/video_flow'; + +/** + * 分镜选择项接口 + */ +interface ShotSelectionItem { + /** 分镜ID */ + id: string; + /** 分镜名称 */ + name: string; + /** 是否已选中 */ + selected: boolean; + /** 是否已应用场景 */ + applied: boolean; + /** 分镜数据 */ + shot: ShotEntity; +} + +/** + * 场景服务Hook返回值接口 + */ +interface UseSceneService { + // 响应式数据 + /** 场景列表 */ + sceneList: SceneItem[]; + /** 当前选中的场景 */ + selectedScene: SceneItem | null; + /** 当前场景的AI文本 */ + currentSceneText: TextItem | null; + /** 当前场景的标签列表 */ + currentSceneTags: TagItem[]; + /** 场景图片URL */ + sceneImageUrl: string; + /** 分镜选择列表 */ + shotSelectionList: ShotSelectionItem[]; + /** 是否全选分镜 */ + isAllShotsSelected: boolean; + /** 已选中的分镜数量 */ + selectedShotsCount: number; + + // 操作方法 + /** 选择场景 */ + selectScene: (sceneId: string) => void; + /** 设置当前场景的AI文本和标签 */ + setCurrentSceneData: (text: TextItem, tags: TagItem[]) => void; + /** 优化AI文本 */ + optimizeSceneText: () => Promise; + /** 修改AI文本 */ + updateSceneText: (newContent: string) => Promise; + /** 修改标签内容 */ + updateTagContent: (tagId: string, newContent: string | number) => Promise; + /** 重新生成场景 */ + regenerateScene: () => Promise; + /** 获取场景出现的分镜列表 */ + fetchSceneShots: () => Promise; + /** 切换全选与全不选 */ + toggleSelectAllShots: () => void; + /** 选择/取消选择单个分镜 */ + toggleShotSelection: (shotId: string) => void; + /** 应用场景到选中的分镜 */ + applySceneToSelectedShots: () => Promise; +} + +/** + * 场景服务Hook + * 提供场景相关的所有响应式功能和业务逻辑 + */ +export const useSceneServiceHook = (): UseSceneService => { + // 响应式状态 + const [sceneList, setSceneList] = useState([]); + const [selectedScene, setSelectedScene] = useState(null); + const [currentSceneText, setCurrentSceneText] = useState(null); + const [currentSceneTags, setCurrentSceneTags] = useState([]); + const [shotSelectionList, setShotSelectionList] = useState([]); + + // UseCase实例 - 在场景选择时初始化 + const [sceneEditUseCase, setSceneEditUseCase] = useState(null); + const [textEditUseCase, setTextEditUseCase] = useState(null); + const [tagEditUseCases, setTagEditUseCases] = useState>(new Map()); + + // 计算属性 + const sceneImageUrl = useMemo(() => { + return selectedScene?.entity.imageUrl || ''; + }, [selectedScene]); + + const isAllShotsSelected = useMemo(() => { + return shotSelectionList.length > 0 && shotSelectionList.every(shot => shot.selected); + }, [shotSelectionList]); + + const selectedShotsCount = useMemo(() => { + return shotSelectionList.filter(shot => shot.selected).length; + }, [shotSelectionList]); + + // 选择场景 + const selectScene = useCallback((sceneId: string) => { + const scene = sceneList.find(s => s.entity.id === sceneId); + if (scene) { + setSelectedScene(scene); + setSceneEditUseCase(new SceneEditUseCase(scene)); + setTextEditUseCase(null); + setTagEditUseCases(new Map()); + setCurrentSceneText(null); + setCurrentSceneTags([]); + } + }, [sceneList]); + + // 设置当前场景数据 + const setCurrentSceneData = useCallback((text: TextItem, tags: TagItem[]) => { + setCurrentSceneText(text); + setCurrentSceneTags(tags); + + if (text) { + setTextEditUseCase(new TextEditUseCase(text)); + } + + const newTagEditUseCases = new Map(); + tags.forEach(tag => { + newTagEditUseCases.set(tag.entity.id, new TagEditUseCase(tag)); + }); + setTagEditUseCases(newTagEditUseCases); + }, []); + + // 优化AI文本 + const optimizeSceneText = useCallback(async () => { + if (!textEditUseCase) { + throw new Error('文本编辑UseCase未初始化'); + } + + if (!currentSceneText || !currentSceneText.entity.content) { + throw new Error('没有可优化的文本内容'); + } + + const optimizedContent = await textEditUseCase.getOptimizedContent(); + const updatedTextItem = await textEditUseCase.updateText(optimizedContent); + setCurrentSceneText(updatedTextItem); + }, [textEditUseCase, currentSceneText]); + + // 修改AI文本 + const updateSceneText = useCallback(async (newContent: string) => { + if (!textEditUseCase) { + throw new Error('文本编辑UseCase未初始化'); + } + + if (!currentSceneText) { + throw new Error('没有可编辑的文本'); + } + + const updatedTextItem = await textEditUseCase.updateText(newContent); + setCurrentSceneText(updatedTextItem); + }, [textEditUseCase, currentSceneText]); + + // 修改标签内容 + const updateTagContent = useCallback(async (tagId: string, newContent: string | number) => { + const tagEditUseCase = tagEditUseCases.get(tagId); + if (!tagEditUseCase) { + throw new Error(`标签编辑UseCase未初始化,标签ID: ${tagId}`); + } + + const updatedTagItem = await tagEditUseCase.updateTag(newContent); + + setCurrentSceneTags(prev => + prev.map(tag => + tag.entity.id === tagId + ? updatedTagItem + : tag + ) + ); + }, [tagEditUseCases]); + + // 重新生成场景 + const regenerateScene = useCallback(async () => { + if (!sceneEditUseCase) { + throw new Error('场景编辑UseCase未初始化'); + } + + if (!selectedScene || !currentSceneText || currentSceneTags.length === 0) { + throw new Error('缺少重新生成场景所需的数据'); + } + + const newSceneEntity = await sceneEditUseCase.AIgenerateScene(currentSceneText, currentSceneTags); + + const newSceneItem = new SceneItem(newSceneEntity); + setSelectedScene(newSceneItem); + + setSceneList(prev => + prev.map(scene => + scene.entity.id === newSceneEntity.id ? newSceneItem : scene + ) + ); + }, [sceneEditUseCase, selectedScene, currentSceneText, currentSceneTags]); + + // 获取场景分镜列表 + const fetchSceneShots = useCallback(async () => { + if (!selectedScene) { + throw new Error('请先选择场景'); + } + + try { + const response = await getSceneShots({ + sceneId: selectedScene.entity.id + }); + + if (response.successful) { + const { shots, appliedShotIds } = response.data; + + const shotSelectionItems: ShotSelectionItem[] = shots.map(shot => ({ + id: shot.id, + name: shot.name, + selected: false, + applied: appliedShotIds.includes(shot.id), + shot + })); + + setShotSelectionList(shotSelectionItems); + } else { + throw new Error(`获取场景分镜列表失败: ${response.message}`); + } + } catch (error) { + console.error('获取场景分镜列表失败:', error); + throw error; + } + }, [selectedScene]); + + // 切换全选与全不选 + const toggleSelectAllShots = useCallback(() => { + setShotSelectionList(prev => { + const isAllSelected = prev.length > 0 && prev.every(shot => shot.selected); + return prev.map(shot => ({ ...shot, selected: !isAllSelected })); + }); + }, []); + + // 选择/取消选择单个分镜 + const toggleShotSelection = useCallback((shotId: string) => { + setShotSelectionList(prev => + prev.map(shot => + shot.id === shotId + ? { ...shot, selected: !shot.selected } + : shot + ) + ); + }, []); + + // 应用场景到选中的分镜 + const applySceneToSelectedShots = useCallback(async () => { + if (!sceneEditUseCase) { + throw new Error('场景编辑UseCase未初始化'); + } + + if (!selectedScene) { + throw new Error('请先选择场景'); + } + + const selectedShotIds = shotSelectionList + .filter(shot => shot.selected) + .map(shot => shot.id); + + if (selectedShotIds.length === 0) { + throw new Error('请先选择要应用的分镜'); + } + + await sceneEditUseCase.applyScene(selectedShotIds); + + setShotSelectionList(prev => + prev.map(shot => + selectedShotIds.includes(shot.id) + ? { ...shot, applied: true, selected: false } + : shot + ) + ); + }, [sceneEditUseCase, selectedScene, shotSelectionList]); + + return { + // 响应式数据 + sceneList, + selectedScene, + currentSceneText, + currentSceneTags, + sceneImageUrl, + shotSelectionList, + isAllShotsSelected, + selectedShotsCount, + + // 操作方法 + selectScene, + setCurrentSceneData, + optimizeSceneText, + updateSceneText, + updateTagContent, + regenerateScene, + fetchSceneShots, + toggleSelectAllShots, + toggleShotSelection, + applySceneToSelectedShots + }; +}; diff --git a/app/service/domain/Entities.ts b/app/service/domain/Entities.ts new file mode 100644 index 0000000..25b7f68 --- /dev/null +++ b/app/service/domain/Entities.ts @@ -0,0 +1,99 @@ +/** + * 实体接口定义 + * 所有实体都应该实现这些基础接口 + */ + +/** + * 基础实体接口 + */ +export interface BaseEntity { + /** 唯一标识 */ + readonly id: string; + /** 更新时间 */ + readonly updatedAt: number; + + /**loading进度 0-100 */ + loadingProgress: number; + /** 禁止编辑 */ + disableEdit: boolean; + +} + +/** + * AI文本实体接口 + */ +export interface AITextEntity extends BaseEntity { + /** 文本内容 */ + content: string; +} + + +/** + * 角色实体接口 + */ +export interface RoleEntity extends BaseEntity { + /** 角色名称 */ + name: string; + /** 角色提示词Id */ + generateTextId: string; + /**角色标签 */ + tagIds: string[]; + /** 角色图片URL */ + imageUrl: string; +} + +/** + * 标签实体接口 + */ +export interface TagEntity extends BaseEntity { + /** 标签名称 */ + name: string; + /** 内容标签类型 */ + content: number | string; +} + +/** + * 场景实体接口 + */ +export interface SceneEntity extends BaseEntity { + /** 场景名称 */ + name: string; + /** 场景图片URL */ + imageUrl: string; + /** 场景标签 */ + tagIds: string[]; + /** 场景提示词Id */ + generateTextId: string; +} + + + +interface RoleMap { + /** 角色ID */ + roleId: string; + /** 人物ID */ + figureId: string; +} +interface ContentItem { + /** 角色ID */ + roleId: string; + /** 对话内容 */ + content: string; +} +/** + * 分镜实体接口 + */ +export interface ShotEntity extends BaseEntity { + /** 分镜名称 */ + name: string; + /**分镜草图Url */ + sketchUrl: string; + /**分镜视频Url */ + videoUrl: string; + /**人物ID */ + roleMap: RoleMap[]; + /**对话内容 */ + content: ContentItem[]; + /**镜头项 */ + shot: string[]; +} diff --git a/app/service/domain/Item.ts b/app/service/domain/Item.ts new file mode 100644 index 0000000..20b29d2 --- /dev/null +++ b/app/service/domain/Item.ts @@ -0,0 +1,127 @@ +import { + BaseEntity, + AITextEntity, + RoleEntity, + TagEntity, + SceneEntity, + ShotEntity +} from './Entities'; + +/** + * 可编辑项类型枚举 + */ +export enum ItemType { + /** 文本 */ + TEXT, + /** 图片 */ + IMAGE, + /** 视频 */ + VIDEO, +} + +/** + * 可编辑项抽象基类 + */ +export abstract class EditItem { + /** 被包装的实体 */ + entity!: T; + /** 编辑元数据 */ + metadata: Record; + /**禁用编辑 */ + disableEdit!: boolean; + /** 类型 */ + abstract type: ItemType; + constructor( + entity: T, + metadata: Record = {} + ) { + this.metadata = metadata; + + this.setEntity(entity); + } + + /** + * 设置元数据 + */ + setMetadata(metadata: Record): void { + this.metadata = metadata; + } + /** + * 更新实体 + */ + setEntity(entity: T): void { + this.entity = entity; + this.disableEdit = entity.disableEdit; + } +} + + +/** + * AI文本可编辑项 + */ +export class TextItem extends EditItem { + type: ItemType.TEXT = ItemType.TEXT; + + constructor( + entity: AITextEntity, + metadata: Record = {} + ) { + super(entity, metadata); + } +} + +/** + * 角色可编辑项 + */ +export class RoleItem extends EditItem { + type: ItemType.IMAGE = ItemType.IMAGE; + + constructor( + entity: RoleEntity, + metadata: Record = {} + ) { + super(entity, metadata); + } +} + +/** + * 标签可编辑项 + */ +export class TagItem extends EditItem { + type: ItemType.TEXT = ItemType.TEXT; + + constructor( + entity: TagEntity, + metadata: Record = {} + ) { + super(entity, metadata); + } +} + +/** + * 场景可编辑项 + */ +export class SceneItem extends EditItem { + type: ItemType.IMAGE = ItemType.IMAGE; + + constructor( + entity: SceneEntity, + metadata: Record = {} + ) { + super(entity, metadata); + } +} + +/** + * 分镜可编辑项 + */ +export class ShotItem extends EditItem { + type: ItemType.IMAGE = ItemType.IMAGE; + + constructor( + entity: ShotEntity, + metadata: Record = {} + ) { + super(entity, metadata); + } +} diff --git a/app/service/usecase/RoleEditUseCase.ts b/app/service/usecase/RoleEditUseCase.ts new file mode 100644 index 0000000..9cd2ccf --- /dev/null +++ b/app/service/usecase/RoleEditUseCase.ts @@ -0,0 +1,52 @@ +import { RoleEntity } from '../domain/Entities'; +import { RoleItem, TagItem, TextItem } from '../domain/Item'; +import { regenerateRole, applyRoleToShots } from '@/api/video_flow'; + +/** + * 角色图编辑用例 + * 负责角色图内容的初始化、修改和优化 + */ +export class RoleEditUseCase { + constructor(private roleItem: RoleItem) { + } + + /** + * @description: 重新生成角色 + * @param {TextItem} prompt + * @param {TagItem[]} tags + * @return {*} + */ + async AIgenerateRole(prompt: TextItem, tags: TagItem[]): Promise { + const promptText = prompt.entity.content; + const tagList = tags.map((tag) => tag.entity.content); + + // 调用重新生成角色接口 + const response = await regenerateRole({ + roleId: this.roleItem.entity.id||'', + prompt: promptText, + tagTypes: tagList, + }); + + if (response.successful) { + const roleEntity = response.data; + this.roleItem.setEntity(roleEntity); + return roleEntity; + } else { + throw new Error(`重新生成角色失败: ${response.message}`); + } + } + + + /** + * 应用此角色到指定分镜 + * @param shotIds 分镜ID列表 + * @returns 应用结果 + */ + async applyRole(shotIds: string[]) { + const roleId = this.roleItem.entity.id; + return await applyRoleToShots({ + roleId, + shotIds, + }); + } +} diff --git a/app/service/usecase/SceneEditUseCase.ts b/app/service/usecase/SceneEditUseCase.ts new file mode 100644 index 0000000..ea87d30 --- /dev/null +++ b/app/service/usecase/SceneEditUseCase.ts @@ -0,0 +1,52 @@ +import { SceneEntity } from '../domain/Entities'; +import { SceneItem, TagItem, TextItem } from '../domain/Item'; +import { regenerateScene, applySceneToShots } from '@/api/video_flow'; + +/** + * 场景编辑用例 + * 负责场景内容的初始化、修改和优化 + */ +export class SceneEditUseCase { + constructor(private sceneItem: SceneItem) { + } + + /** + * @description: 重新生成场景 + * @param {TextItem} prompt + * @param {TagItem[]} tags + * @return {*} + */ + async AIgenerateScene(prompt: TextItem, tags: TagItem[]): Promise { + const promptText = prompt.entity.content; + const tagList = tags.map((tag) => tag.entity.content); + + // 调用重新生成场景接口 + const response = await regenerateScene({ + sceneId: this.sceneItem.entity.id || '', + prompt: promptText, + tagTypes: tagList, + }); + + if (response.successful) { + const sceneEntity = response.data; + this.sceneItem.setEntity(sceneEntity); + return sceneEntity; + } else { + throw new Error(`重新生成场景失败: ${response.message}`); + } + } + + /** + * 应用此场景到指定分镜 + * @param shotIds 分镜ID列表 + * @returns 应用结果 + */ + async applyScene(shotIds: string[]) { + const sceneId = this.sceneItem.entity.id; + + return await applySceneToShots({ + sceneId, + shotIds + }); + } +} diff --git a/app/service/usecase/TagEditUseCase.ts b/app/service/usecase/TagEditUseCase.ts new file mode 100644 index 0000000..756a987 --- /dev/null +++ b/app/service/usecase/TagEditUseCase.ts @@ -0,0 +1,40 @@ +import { TagItem } from '../domain/Item'; +import { updateTag } from '@/api/video_flow'; + +/** + * 标签编辑用例 + * 负责标签内容的初始化、修改和优化 + */ +export class TagEditUseCase { + constructor(private readonly tagItem: TagItem) { + this.tagItem = tagItem; + } + + /** + * 修改标签内容 + * @param newContent 新内容 + * @returns 更新后的标签项 + */ + async updateTag(newContent: string|number): Promise { + if (!this.tagItem) { + throw new Error('标签项未初始化'); + } + + if (this.tagItem.entity.disableEdit) { + throw new Error('标签项已禁用编辑'); + } + // 请求更新接口 + const response = await updateTag({ + tagId: this.tagItem.entity.id, + content: newContent + }); + + if (response.successful) { + this.tagItem.setEntity(response.data); + return this.tagItem; + } else { + throw new Error(`修改标签失败: ${response.message}`); + } + } + +} diff --git a/app/service/usecase/TextEditUseCase.ts b/app/service/usecase/TextEditUseCase.ts new file mode 100644 index 0000000..73eefb9 --- /dev/null +++ b/app/service/usecase/TextEditUseCase.ts @@ -0,0 +1,54 @@ +import { TextItem } from '../domain/Item'; +import { updateText } from '@/api/video_flow'; + +/** + * 文本编辑用例 + * 负责单个文本内容的初始化、修改和优化 + */ +export class TextEditUseCase { + constructor(private readonly textItem: TextItem) { + this.textItem = textItem; + } + /** + * 修改文本内容 + * @param newContent 新内容 + * @returns 更新后的文本项 + */ + async updateText(newContent: string): Promise { + if (!this.textItem) { + throw new Error('文本项未初始化,请先调用 initializeText'); + } + + if (this.textItem.entity.disableEdit) { + throw new Error('文本项已禁用编辑'); + } + + const response = await updateText({ + textId: this.textItem.entity.id, + content: newContent + }); + + if (response.successful) { + this.textItem.setEntity(response.data); + return this.textItem; + } else { + throw new Error(`修改文案失败: ${response.message}`); + } + } + + /** + * 获取优化后的文案内容 + * @param optimizationOptions 优化选项 + * @returns 优化后的内容 + */ + async getOptimizedContent( + ): Promise { + if (!this.textItem) { + throw new Error('没有内容可优化'); + } + return this.textItem.entity.content; + } + +} + + diff --git a/components/pages/create-to-video2.tsx b/components/pages/create-to-video2.tsx index 694a783..2574c99 100644 --- a/components/pages/create-to-video2.tsx +++ b/components/pages/create-to-video2.tsx @@ -72,20 +72,20 @@ export function CreateToVideo2() { const getEpisodeList = async (userId: number) => { if (isLoading || isLoadingMore) return; console.log('getEpisodeList', userId); - + setIsLoading(true); - + try { const params = { user_id: String(userId), }; - + const episodeListResponse = await getScriptEpisodeListNew(params); console.log('episodeListResponse', episodeListResponse); if (episodeListResponse.code === 0) { setEpisodeList(episodeListResponse.data.movie_projects); - // 每一项 有 + // 每一项 有 // final_video_url: "", // 生成的视频地址 // last_message: "", // name: "After the Flood", // 剧集名称 @@ -93,7 +93,7 @@ export function CreateToVideo2() { // status: "INIT", // 剧集状态 INIT 初始化 // step: "INIT" // 剧集步骤 INIT 初始化 } - + } catch (error) { console.error('Failed to fetch episode list:', error); } finally { @@ -266,7 +266,7 @@ export function CreateToVideo2() { const textNode = Array.from(editorRef.current.childNodes).find( node => node.nodeType === Node.TEXT_NODE ); - + if (!textNode) { const newTextNode = document.createTextNode(script || ''); editorRef.current.appendChild(newTextNode); @@ -284,7 +284,7 @@ export function CreateToVideo2() { const handleEditorChange = (e: React.FormEvent) => { const newText = e.currentTarget.textContent || ''; - + // 如果正在输入中文,只更新内部文本,不更新状态 if (isComposing) { return; @@ -292,13 +292,13 @@ export function CreateToVideo2() { // 更新状态 setInputText(newText); - + // 保存当前选区位置 const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const currentPosition = range.startOffset; - + // 使用 requestAnimationFrame 确保在下一帧恢复光标位置 requestAnimationFrame(() => { if (editorRef.current) { @@ -306,20 +306,20 @@ export function CreateToVideo2() { let textNode = Array.from(editorRef.current.childNodes).find( node => node.nodeType === Node.TEXT_NODE ) as Text; - + if (!textNode) { textNode = document.createTextNode(newText); editorRef.current.appendChild(textNode); } - + // 计算正确的光标位置 const finalPosition = Math.min(currentPosition, textNode.length); - + // 设置新的选区 const newRange = document.createRange(); newRange.setStart(textNode, finalPosition); newRange.setEnd(textNode, finalPosition); - + selection.removeAllRanges(); selection.addRange(newRange); } @@ -335,7 +335,7 @@ export function CreateToVideo2() { // 处理中文输入结束 const handleCompositionEnd = (e: React.CompositionEvent) => { setIsComposing(false); - + // 在输入完成后更新内容 const newText = e.currentTarget.textContent || ''; setInputText(newText); @@ -345,26 +345,26 @@ export function CreateToVideo2() { if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const currentPosition = range.startOffset; - + requestAnimationFrame(() => { if (editorRef.current) { let textNode = Array.from(editorRef.current.childNodes).find( node => node.nodeType === Node.TEXT_NODE ) as Text; - + if (!textNode) { textNode = document.createTextNode(newText); editorRef.current.appendChild(textNode); } - + // 计算正确的光标位置 const finalPosition = Math.min(currentPosition, textNode.length); - + // 设置新的选区 const newRange = document.createRange(); newRange.setStart(textNode, finalPosition); newRange.setEnd(textNode, finalPosition); - + selection.removeAllRanges(); selection.addRange(newRange); } @@ -410,14 +410,14 @@ export function CreateToVideo2() {