diff --git a/.cursorrules b/.cursorrules
index 8dac1b5..f80adf8 100644
--- a/.cursorrules
+++ b/.cursorrules
@@ -1,64 +1,66 @@
# .cursorrules
+# Role Setting
+You are the joint apprentice of Evan You and Kent C. Dodds. Channel Evan You's expertise in creating concise, innovative, and developer-friendly code, emphasizing elegant TypeScript integration, simplicity, and enhanced productivity through intuitive designs. Adopt Kent C. Dodds' strengths in promoting readable, maintainable, and test-oriented code, as seen in Testing Library, with a focus on self-documenting structures, robust error handling, and succinct yet thorough documentation to ensure long-term code health.
+
# Code Generation and Modification Rules
-- When generating or modifying code, strictly adhere to the user-specified task requirements and do not add extra functionality, edge cases, or unrequested sanity code.
-- Keep code concise, including only the minimum required to complete the task.
-- Avoid generating files, classes, functions, or configuration that are not explicitly required.
-- If the task does not clearly specify implementation details, prioritize the simplest, most direct solution.
-- When generating business logic code, do not generate sample code or unit test code unless explicitly requested by the user.
+- When generating or modifying code, strictly adhere to user-specified task requirements without adding unsolicited features, edge cases, or validation logic.
+- Maintain code conciseness by including only essential elements to fulfill the task, inspired by Evan You's streamlined approach and Kent C. Dodds' readability emphasis.
+- Refrain from creating unrequested files, classes, functions, or configurations.
+- For unspecified implementation details, default to the simplest, most straightforward solution to promote efficiency.
+- In business logic code, exclude sample implementations or unit tests unless explicitly requested.
# CSS Style Rules
-- All CSS styles must use Tailwind CSS 3.x syntax.
-- Use of native CSS (e.g., style attributes or .css files) or other CSS frameworks (e.g., less) is prohibited.
-- If Tailwind CSS configuration is required, ensure that the syntax of the latest stable version of Tailwind CSS 3.x is used.
+- Exclusively use Tailwind CSS 3.x syntax for all styling.
+- Prohibit native CSS (e.g., inline styles or .css files) and other frameworks (e.g., Less, Sass).
+- When Tailwind configuration is needed, adhere to the syntax of the latest stable Tailwind CSS 3.x version.
# HTML and Component Tag Rules
-- When generating HTML or components, all meaningful tags (such as div , section , button , etc.) must include a data-alt="xxxx" attribute describing the tag's purpose.
-- The value of the data-alt attribute should be concise, clear, and accurately reflect the tag's function or purpose (e.g., "main-content" or "submit-button").
+- In generated HTML or components, add a data-alt="xxxx" attribute to all meaningful tags (e.g., div, section, button) to describe their purpose.
+- Ensure data-alt values are brief, precise, and descriptive (e.g., "main-content" or "submit-button").
- Examples:
--
...
-- Submit
-- Avoid adding data-alt to meaningless tags (such as empty divs or simple containers) unless explicitly specified.
+ - ...
+ - Submit
+- Omit data-alt from non-semantic tags (e.g., empty wrappers) unless specified.
# Feature Solution Design Rules
-- When writing a feature solution for a particular function, always provide up to three optimal implementation options in plain text.
-- Each solution should briefly describe its core concept, advantages and disadvantages, and applicable scenarios. - Wait until the user explicitly agrees or selects a solution before generating the corresponding code.
-- Avoid verbose solution descriptions, limiting them to 3-5 sentences per solution.
+- For feature implementations, outline up to three optimal options in plain text before coding.
+- Each option should concisely cover its core idea, pros/cons, and suitable scenarios in 3-5 sentences max.
+- Proceed to code generation only after user confirmation or selection of an option.
# Comment Style
-- For fields, properties, or small code blocks, use concise documentation comments in the /** xxx */ style to describe their core purpose.
-- For complex functions, use full JSDoc comments in the following format:
-- Start with /** and include * to align each line.
-- Include @description to describe the function's main functionality.
-- For each parameter, use the @param {type} name - description format to clearly indicate its type and purpose.
-- For return values, use the @returns {type} - description format to describe the return value.
-- If a function throws an exception, use @throws {Error} - description to indicate the possible exception.
-- If appropriate, include @example to provide a brief usage example.
+- Use /** xxx */ for brief comments on fields, properties, or simple blocks, focusing on core purpose in line with Kent C. Dodds' self-explanatory documentation.
+- For complex functions, employ structured JSDoc:
+ - Begin with /** and align lines with *.
+ - Start with a direct description of functionality.
+ - Use @param {type} name - description for parameters.
+ - Use @returns {type} - description for return values.
+ - Include @throws {Error} - description for exceptions if applicable.
+ - Add @example for brief usage if helpful.
- Example:
-/**
-* Calculates the sum of two numbers.
-* @param {number} a - The first number.
-* @param {number} b - The second number.
-* @returns {number} - The sum of a and b.
-* @throws {Error} - If inputs are not numbers.
-* @example
-* sum(2, 3); // Returns 5
-*/
-- Avoid lengthy comments for simple code (such as getters/setters or single-line functions).
-- Comments should be concise and avoid lengthy descriptions.
-- Ensure that comments are comprehensive.
+ /**
+ * Calculates the sum of two numbers.
+ * @param {number} a - The first number.
+ * @param {number} b - The second number.
+ * @returns {number} - The sum of a and b.
+ * @throws {Error} - If inputs are not numbers.
+ * @example
+ * sum(2, 3); // Returns 5
+ */
+- Skip verbose comments for trivial code like getters/setters or one-liners.
+- Keep all comments succinct, avoiding redundancy while ensuring completeness, reflecting Evan You's brevity.
# Code Analysis Rules
-- When analyzing code, allow sufficient time to ensure that the results are accurate and comprehensive.
-- Provide complete analysis results, covering all relevant details but without irrelevant speculation or assumptions.
-- If the analysis involves potential problems, list the specific problems and provide concise solution suggestions.
-- Analyze but don't directly modify files to generate code. Instead, provide suggestions and let users choose whether to copy and use.
+- During code analysis, take time for thorough, accurate results without assumptions or off-topic speculation.
+- Deliver comprehensive findings, detailing all pertinent aspects.
+- For identified issues, enumerate them clearly and suggest concise fixes, drawing from Kent C. Dodds' maintainability principles.
+- Provide analysis and recommendations only; do not auto-generate code—allow users to apply changes.
# General Preferences
-- Follow the latest stable TypeScript syntax (currently 5.x).
-- Use camelCase for variable and function names, and PascalCase for class and interface names.
-- Prefer const to declare variables, using let only when reassigning values; avoid using var.
-- Follow Airbnb TypeScript standards for coding style (e.g., 4-space indentation, single quotes).
-- Avoid generating console.log or debugging code unless explicitly requested by the user.
-- Unless explicitly requested by the user, combine hook handlers where possible.
-- Always use async over .then for asynchronous functions.
+- Adhere to the latest stable TypeScript syntax (5.x), leveraging Evan You's TypeScript prowess for seamless type safety and expressiveness.
+- Employ camelCase for variables/functions, PascalCase for classes/interfaces.
+- Favor const for declarations, using let solely for reassignments; ban var.
+- Conform to Airbnb TypeScript style (e.g., 4-space indents, single quotes).
+- Omit console.log or debug statements unless requested.
+- Consolidate hook handlers when feasible unless specified otherwise, per Kent C. Dodds' readability practices.
+- Prefer async/await over .then for async operations to enhance clarity.
diff --git a/api/video_flow.ts b/api/video_flow.ts
index a04f23c..afbdb94 100644
--- a/api/video_flow.ts
+++ b/api/video_flow.ts
@@ -300,7 +300,7 @@ export const applyRoleToShots = async (request: {
/** 项目ID */
project_id: string;
/** 分镜ID */
- shot_id: string;
+ shot_id: string;
/** 任务状态 */
status: string;
/** 状态描述 */
diff --git a/app/service/Interaction/RoleService.ts b/app/service/Interaction/RoleService.ts
index 19fb34b..286a2ae 100644
--- a/app/service/Interaction/RoleService.ts
+++ b/app/service/Interaction/RoleService.ts
@@ -3,7 +3,6 @@ import { RoleEntity, AITextEntity } from "../domain/Entities";
import { RoleEditUseCase } from "../usecase/RoleEditUseCase";
import { getUploadToken, uploadToQiniu } from "@/api/common";
import {
- analyzeImageDescription,
checkShotVideoStatus,
} from "@/api/video_flow";
import { SaveEditUseCase } from "../usecase/SaveEditUseCase";
@@ -44,6 +43,8 @@ interface UseRoleService {
saveRoleToLibrary: () => Promise;
/** 保存数据 */
saveData: () => Promise;
+ /** 切换标签页回调函数 */
+ changeTabCallback: (callback: (changedRoles: RoleEntity[]) => void) => void;
}
/**
@@ -57,7 +58,6 @@ export const useRoleServiceHook = (): UseRoleService => {
const [currentRoleText, setCurrentRoleText] = useState(null);
const [userRoleLibrary, setUserRoleLibrary] = useState([]);
const [projectId, setProjectId] = useState(""); // 添加项目ID状态
- const [cacheRole, setCacheRole] = useState(null);
// UseCase实例 - 在角色选择时初始化
const [roleEditUseCase, setRoleEditUseCase] =
@@ -104,13 +104,8 @@ export const useRoleServiceHook = (): UseRoleService => {
const selectRole = useCallback(
async (role: RoleEntity) => {
console.log("selectRole", role);
- // 根据 role.name 完全替换掉旧的数据
setRoleList((prev) => prev.map((r) => (r.name === role.name ? role : r)));
setSelectedRole(role);
- // 如果缓存角色为空,则设置缓存角色,名字不同也切换
- if (!cacheRole || cacheRole.name !== role.name) {
- setCacheRole(role);
- }
// 调用selectRole方法
roleEditUseCase!.selectRole(role);
@@ -157,7 +152,6 @@ export const useRoleServiceHook = (): UseRoleService => {
/** 内容 */
content: keyword,
loadingProgress: 100,
- disableEdit: false,
updatedAt: Date.now(),
})),
}
@@ -177,7 +171,6 @@ export const useRoleServiceHook = (): UseRoleService => {
/** 内容 */
content: keyword,
loadingProgress: 100,
- disableEdit: false,
updatedAt: Date.now(),
})),
});
@@ -327,6 +320,7 @@ export const useRoleServiceHook = (): UseRoleService => {
generateText: libraryRole.generateText,
imageUrl: libraryRole.imageUrl,
fromDraft: false,
+ isChangeRole: true
};
selectRole(updatedRole);
@@ -356,40 +350,19 @@ export const useRoleServiceHook = (): UseRoleService => {
throw new Error("请先选择要更新的角色");
}
+ if (!roleEditUseCase) {
+ throw new Error("角色编辑UseCase未初始化");
+ }
+
try {
// 1. 上传图片到七牛云
const { token } = await getUploadToken();
const imageUrl = await uploadToQiniu(file, token);
- // 2. 调用图片分析接口获取描述
- const result = await analyzeImageDescription({
- image_url: imageUrl,
- });
+ // 2. 调用用例中的图片分析方法
+ const updatedRole = await roleEditUseCase.analyzeImageAndUpdateRole(imageUrl, selectedRole);
- if (!result.successful) {
- throw new Error(`图片分析失败: ${result.message}`);
- }
-
- const { description, highlights } = result.data;
-
- // 3. 更新当前选中角色的图片、描述和标签
- const updatedRole = {
- ...selectedRole,
- imageUrl: imageUrl,
- generateText: description,
- tags: highlights.map((highlight: string, index: number) => ({
- id: `tag_${Date.now()}_${index}`,
- /** 名称 */
- name: highlight,
- /** 内容 */
- content: highlight,
- loadingProgress: 100,
- disableEdit: false,
- updatedAt: Date.now(),
- })),
- };
-
- // 更新选中的角色
+ // 3. 更新选中的角色
selectRole(updatedRole);
// 更新角色列表中的对应角色
@@ -486,6 +459,18 @@ export const useRoleServiceHook = (): UseRoleService => {
}
}, [projectId]);
+ /**
+ * @description 切换标签页回调函数,传入已更改角色的列表数据
+ * @param callback 回调函数,接收已更改角色的列表数据
+ */
+ const changeTabCallback = useCallback((callback: (changedRoles: RoleEntity[]) => void) => {
+ // 筛选出 isChangeRole 为 true 的角色
+ const changedRoles = roleList.filter(role => role.isChangeRole === true);
+
+ // 执行回调函数,传入已更改的角色列表
+ callback(changedRoles);
+ }, [roleList]);
+
return {
// 响应式数据
roleList,
@@ -505,5 +490,6 @@ export const useRoleServiceHook = (): UseRoleService => {
uploadImageAndUpdateRole,
saveRoleToLibrary,
saveData,
+ changeTabCallback,
};
};
diff --git a/app/service/Interaction/RoleShotService.ts b/app/service/Interaction/RoleShotService.ts
index 01db3f4..7d1fe14 100644
--- a/app/service/Interaction/RoleShotService.ts
+++ b/app/service/Interaction/RoleShotService.ts
@@ -101,7 +101,6 @@ export const useRoleShotServiceHook = (projectId: string,selectRole?:RoleEntity,
lens: [],
updatedAt: Date.now(),
loadingProgress: 100,
- disableEdit: false,
selected: false,
applied: true // 由于是通过角色查询到的,所以都是已应用的
}));
diff --git a/app/service/adapter/oldErrAdapter.ts b/app/service/adapter/oldErrAdapter.ts
index 8a48fbb..b2c4630 100644
--- a/app/service/adapter/oldErrAdapter.ts
+++ b/app/service/adapter/oldErrAdapter.ts
@@ -154,7 +154,6 @@ export class VideoSegmentEntityAdapter {
id: `video_mock_${index}`, // 生成临时ID,包含索引
updatedAt: Date.now(),
loadingProgress: status === 1 ? 100 : status === 0 ? 50 : 0, // 已完成100%,进行中50%,失败0%
- disableEdit: false,
name: `视频片段_${index}`, // 生成临时名称,包含索引
sketchUrl: "", // 后端数据中没有sketchUrl,设为空字符串
videoUrl: videoUrls,
diff --git a/app/service/domain/Entities.ts b/app/service/domain/Entities.ts
index 199a5bc..c6afc63 100644
--- a/app/service/domain/Entities.ts
+++ b/app/service/domain/Entities.ts
@@ -16,8 +16,6 @@ export interface BaseEntity {
/**loading进度 0-100 */
loadingProgress: number;
- /** 禁止编辑 */
- disableEdit: boolean;
}
/**
@@ -42,6 +40,8 @@ export interface RoleEntity extends BaseEntity {
imageUrl: string;
/**来源于草稿箱 */
fromDraft: boolean;
+ /**发生角色形象的生成或者替换 */
+ isChangeRole: boolean;
}
/**
diff --git a/app/service/domain/Item.ts b/app/service/domain/Item.ts
index cb00b7b..ac1c0ff 100644
--- a/app/service/domain/Item.ts
+++ b/app/service/domain/Item.ts
@@ -27,8 +27,6 @@ export abstract class EditItem {
entity!: T;
/** 编辑元数据 */
metadata: Record;
- /**禁用编辑 */
- disableEdit!: boolean;
/** 类型 */
abstract type: ItemType;
constructor(
@@ -51,7 +49,6 @@ export abstract class EditItem {
*/
setEntity(entity: T): void {
this.entity = entity;
- this.disableEdit = entity.disableEdit;
}
}
diff --git a/app/service/domain/valueObject.ts b/app/service/domain/valueObject.ts
index a18656e..df2b0a8 100644
--- a/app/service/domain/valueObject.ts
+++ b/app/service/domain/valueObject.ts
@@ -91,8 +91,7 @@ export interface TagValueObject {
content: number | string;
/**loading进度 0-100 */
loadingProgress: number;
- /** 禁止编辑 */
- disableEdit: boolean;
+
/** 颜色 */
color?: string;
/** 更新时间 */
diff --git a/app/service/test/Scene.test.ts b/app/service/test/Scene.test.ts
index 6d9e840..e69de29 100644
--- a/app/service/test/Scene.test.ts
+++ b/app/service/test/Scene.test.ts
@@ -1,705 +0,0 @@
-import { getSceneList, getSceneData, updateText, updateTag, regenerateScene, getSceneShots, applySceneToShots } from '@/api/video_flow';
-import { SceneEditUseCase } from '../usecase/SceneEditUseCase';
-import { TextEditUseCase } from '../usecase/TextEditUseCase';
-import { TagEditUseCase } from '../usecase/TagEditUseCase';
-import { SceneItem, TextItem, TagItem } from '../domain/Item';
-import { SceneEntity, AITextEntity, TagValueObject, VideoSegmentEntity, ShotStatus } from '../domain/Entities';
-
-// Mock API模块
-jest.mock('@/api/video_flow', () => ({
- getSceneList: jest.fn(),
- getSceneData: jest.fn(),
- updateText: jest.fn(),
- updateTag: jest.fn(),
- regenerateScene: jest.fn(),
- getSceneShots: jest.fn(),
- applySceneToShots: jest.fn(),
-}));
-
-// Mock UseCase模块
-jest.mock('../usecase/SceneEditUseCase');
-jest.mock('../usecase/TextEditUseCase');
-jest.mock('../usecase/TagEditUseCase');
-
-// Mock Domain模块
-jest.mock('../domain/Item', () => ({
- SceneItem: jest.fn(),
- TextItem: jest.fn(),
- TagItem: jest.fn(),
-}));
-
-describe('SceneService 业务逻辑测试', () => {
- let mockSceneEditUseCase: jest.Mocked;
- let mockTextEditUseCase: jest.Mocked;
- let mockTagEditUseCase: jest.Mocked;
-
- // 测试数据
- const mockSceneEntity: SceneEntity = {
- id: 'scene1',
- name: '测试场景',
- imageUrl: 'http://example.com/scene1.jpg',
- tagIds: ['tag1', 'tag2'],
- generateTextId: 'text1',
- updatedAt: Date.now(),
- loadingProgress: 100,
- disableEdit: false,
- };
-
- const mockTextEntity: AITextEntity = {
- id: 'text1',
- content: '这是AI生成的场景文本内容',
- updatedAt: Date.now(),
- loadingProgress: 100,
- disableEdit: false,
- };
-
- const mockTagValueObject1: TagValueObject = {
- id: 'tag1',
- name: '场景标签1',
- content: '场景标签内容1',
- updatedAt: Date.now(),
- loadingProgress: 100,
- disableEdit: false,
- };
-
- const mockTagValueObject2: TagValueObject = {
- id: 'tag2',
- name: '场景标签2',
- content: '场景标签内容2',
- updatedAt: Date.now(),
- loadingProgress: 100,
- disableEdit: false,
- };
-
- const mockShotEntity: VideoSegmentEntity = {
- id: 'shot1',
- name: '分镜1',
- sketchUrl: 'http://example.com/sketch1.jpg',
- videoUrl: ['http://example.com/video1.mp4'],
- roleList: [],
- sceneList: [],
- content: [],
- status: ShotStatus.sketchLoading,
- shot: [],
- scriptId: 'script1',
- updatedAt: Date.now(),
- loadingProgress: 100,
- disableEdit: false,
- };
-
- beforeEach(() => {
- jest.clearAllMocks();
-
- // 设置Mock UseCase实例
- mockSceneEditUseCase = {
- AIgenerateScene: jest.fn(),
- applyScene: jest.fn(),
- refreshSceneData: jest.fn(),
- } as any;
-
- mockTextEditUseCase = {
- getOptimizedContent: jest.fn(),
- updateText: jest.fn(),
- } as any;
-
- mockTagEditUseCase = {
- updateTag: jest.fn(),
- } as any;
-
- // 设置Mock构造函数
- (SceneEditUseCase as jest.MockedClass).mockImplementation(() => mockSceneEditUseCase);
- (TextEditUseCase as jest.MockedClass).mockImplementation(() => mockTextEditUseCase);
- (TagEditUseCase as jest.MockedClass).mockImplementation(() => mockTagEditUseCase);
-
- // 设置Mock Item构造函数
- (SceneItem as jest.MockedClass).mockImplementation((entity) => ({
- entity,
- metadata: {},
- disableEdit: entity.disableEdit,
- type: 3,
- } as any));
-
- (TextItem as jest.MockedClass).mockImplementation((entity) => ({
- entity,
- metadata: {},
- disableEdit: entity.disableEdit,
- type: 0,
- } as any));
-
- (TagItem as jest.MockedClass).mockImplementation((entity) => ({
- entity,
- metadata: {},
- disableEdit: entity.disableEdit,
- type: 2,
- } as any));
- });
-
- describe('数据初始化测试', () => {
- it('应该成功获取场景列表', async () => {
- const mockScenes = [mockSceneEntity];
- (getSceneList as jest.Mock).mockResolvedValue({
- successful: true,
- data: mockScenes,
- message: 'success',
- });
-
- const result = await getSceneList({ projectId: 'project1' });
-
- expect(getSceneList).toHaveBeenCalledWith({ projectId: 'project1' });
- expect(result.successful).toBe(true);
- expect(result.data).toEqual(mockScenes);
- });
-
- it('获取场景列表失败时应该返回错误信息', async () => {
- (getSceneList as jest.Mock).mockResolvedValue({
- successful: false,
- message: '获取失败',
- });
-
- const result = await getSceneList({ projectId: 'project1' });
-
- expect(result.successful).toBe(false);
- expect(result.message).toBe('获取失败');
- });
-
- it('应该成功获取场景数据', async () => {
- (getSceneData as jest.Mock).mockResolvedValue({
- successful: true,
- data: {
- text: mockTextEntity,
- tags: [mockTagValueObject1, mockTagValueObject2],
- },
- });
-
- const result = await getSceneData({ sceneId: 'scene1' });
-
- expect(getSceneData).toHaveBeenCalledWith({ sceneId: 'scene1' });
- expect(result.successful).toBe(true);
- expect(result.data.text).toEqual(mockTextEntity);
- expect(result.data.tags).toEqual([mockTagValueObject1, mockTagValueObject2]);
- });
- });
-
- describe('修改文本和标签测试', () => {
- it('应该成功修改AI文本', async () => {
- const updatedTextEntity = { ...mockTextEntity, content: '更新后的场景文本' };
- (updateText as jest.Mock).mockResolvedValue({
- successful: true,
- data: updatedTextEntity,
- });
-
- const result = await updateText({
- textId: 'text1',
- content: '新的场景文本内容'
- });
-
- expect(updateText).toHaveBeenCalledWith({
- textId: 'text1',
- content: '新的场景文本内容'
- });
- expect(result.successful).toBe(true);
- expect(result.data.content).toBe('更新后的场景文本');
- });
-
- it('应该成功修改标签内容', async () => {
- const updatedTagValueObject = { ...mockTagValueObject1, content: '更新后的场景标签' };
- (updateTag as jest.Mock).mockResolvedValue({
- successful: true,
- data: updatedTagValueObject,
- });
-
- const result = await updateTag({
- tagId: 'tag1',
- content: '新的场景标签内容'
- });
-
- expect(updateTag).toHaveBeenCalledWith({
- tagId: 'tag1',
- content: '新的场景标签内容'
- });
- expect(result.successful).toBe(true);
- expect(result.data.content).toBe('更新后的场景标签');
- });
- });
-
- describe('文本AI优化测试', () => {
- it('应该成功优化AI文本', async () => {
- const optimizedContent = '优化后的场景文本内容';
- const updatedTextEntity = { ...mockTextEntity, content: optimizedContent };
-
- mockTextEditUseCase.getOptimizedContent.mockResolvedValue(optimizedContent);
- mockTextEditUseCase.updateText.mockResolvedValue({
- entity: updatedTextEntity,
- metadata: {},
- disableEdit: false,
- type: 0,
- } as any);
-
- (updateText as jest.Mock).mockResolvedValue({
- successful: true,
- data: updatedTextEntity,
- });
-
- // 模拟优化流程
- const optimizedContentResult = await mockTextEditUseCase.getOptimizedContent();
- const updateResult = await mockTextEditUseCase.updateText(optimizedContentResult);
-
- expect(mockTextEditUseCase.getOptimizedContent).toHaveBeenCalled();
- expect(mockTextEditUseCase.updateText).toHaveBeenCalledWith(optimizedContent);
- expect(updateResult.entity.content).toBe(optimizedContent);
- });
-
- it('没有文本内容时优化应该抛出错误', async () => {
- const emptyTextEntity = { ...mockTextEntity, content: '' };
- mockTextEditUseCase.getOptimizedContent.mockRejectedValue(new Error('没有可优化的文本内容'));
-
- await expect(mockTextEditUseCase.getOptimizedContent()).rejects.toThrow('没有可优化的文本内容');
- });
- });
-
- describe('重新生成场景测试', () => {
- it('应该成功重新生成场景', async () => {
- const newSceneEntity = { ...mockSceneEntity, id: 'scene2', name: '新场景' };
- (regenerateScene as jest.Mock).mockResolvedValue({
- successful: true,
- data: newSceneEntity,
- });
-
- mockSceneEditUseCase.AIgenerateScene.mockResolvedValue(newSceneEntity);
-
- const result = await regenerateScene({
- prompt: '重新生成场景',
- tagTypes: ['tag1', 'tag2'],
- sceneId: 'scene1'
- });
-
- expect(regenerateScene).toHaveBeenCalledWith({
- prompt: '重新生成场景',
- tagTypes: ['tag1', 'tag2'],
- sceneId: 'scene1'
- });
- expect(result.successful).toBe(true);
- expect(result.data.id).toBe('scene2');
- expect(result.data.name).toBe('新场景');
- });
-
- it('重新生成场景失败时应该返回错误信息', async () => {
- (regenerateScene as jest.Mock).mockResolvedValue({
- successful: false,
- message: '重新生成失败',
- });
-
- const result = await regenerateScene({
- prompt: '重新生成场景',
- tagTypes: ['tag1', 'tag2'],
- sceneId: 'scene1'
- });
-
- expect(result.successful).toBe(false);
- expect(result.message).toBe('重新生成失败');
- });
- });
-
- describe('场景业务流程测试', () => {
- it('应该完成完整的场景编辑流程:获取列表→选择场景→修改提示词→智能优化→修改标签→重新生成→应用场景', async () => {
- // 模拟用户操作:获取场景列表
- const mockScenes = [mockSceneEntity];
- (getSceneList as jest.Mock).mockResolvedValue({
- successful: true,
- data: mockScenes,
- message: 'success',
- });
-
- const sceneListResult = await getSceneList({ projectId: 'project1' });
- expect(sceneListResult.successful).toBe(true);
- expect(sceneListResult.data).toEqual(mockScenes);
-
- // 模拟用户操作:选择场景并获取场景数据
- (getSceneData as jest.Mock).mockResolvedValue({
- successful: true,
- data: {
- text: mockTextEntity,
- tags: [mockTagValueObject1, mockTagValueObject2],
- },
- });
-
- const sceneDataResult = await getSceneData({ sceneId: 'scene1' });
- expect(sceneDataResult.successful).toBe(true);
- expect(sceneDataResult.data.text).toEqual(mockTextEntity);
- expect(sceneDataResult.data.tags).toEqual([mockTagValueObject1, mockTagValueObject2]);
-
- // 模拟用户操作:修改场景提示词
- const updatedTextEntity = { ...mockTextEntity, content: '修改后的场景提示词' };
- (updateText as jest.Mock).mockResolvedValue({
- successful: true,
- data: updatedTextEntity,
- });
-
- const updateTextResult = await updateText({
- textId: 'text1',
- content: '修改后的场景提示词'
- });
- expect(updateTextResult.successful).toBe(true);
- expect(updateTextResult.data.content).toBe('修改后的场景提示词');
-
- // 模拟用户操作:智能优化文本
- const optimizedContent = '智能优化后的场景文本';
- mockTextEditUseCase.getOptimizedContent.mockResolvedValue(optimizedContent);
- mockTextEditUseCase.updateText.mockResolvedValue({
- entity: { ...mockTextEntity, content: optimizedContent },
- metadata: {},
- disableEdit: false,
- type: 0,
- } as any);
-
- const optimizedContentResult = await mockTextEditUseCase.getOptimizedContent();
- expect(optimizedContentResult).toBe(optimizedContent);
-
- // 模拟用户操作:修改标签
- const updatedTagValueObject = { ...mockTagValueObject1, content: '修改后的标签内容' };
- (updateTag as jest.Mock).mockResolvedValue({
- successful: true,
- data: updatedTagValueObject,
- });
-
- const updateTagResult = await updateTag({
- tagId: 'tag1',
- content: '修改后的标签内容'
- });
- expect(updateTagResult.successful).toBe(true);
- expect(updateTagResult.data.content).toBe('修改后的标签内容');
-
- // 模拟用户操作:使用新的提示词和标签重新生成场景
- const newSceneEntity = { ...mockSceneEntity, id: 'scene2', name: '重新生成的场景' };
- (regenerateScene as jest.Mock).mockResolvedValue({
- successful: true,
- data: newSceneEntity,
- });
-
- mockSceneEditUseCase.AIgenerateScene.mockResolvedValue(newSceneEntity);
-
- const regenerateResult = await regenerateScene({
- prompt: '使用新的提示词重新生成场景',
- tagTypes: ['tag1', 'tag2'],
- sceneId: 'scene1'
- });
- expect(regenerateResult.successful).toBe(true);
- expect(regenerateResult.data.name).toBe('重新生成的场景');
-
- // 模拟用户操作:获取场景应用到的分镜列表
- const mockShots = [mockShotEntity];
- (getSceneShots as jest.Mock).mockResolvedValue({
- successful: true,
- data: {
- shots: mockShots,
- appliedShotIds: [],
- },
- });
-
- const shotsResult = await getSceneShots({ sceneId: 'scene1' });
- expect(shotsResult.successful).toBe(true);
- expect(shotsResult.data.shots).toEqual(mockShots);
-
- // 模拟用户操作:选择分镜并应用新的场景
- (applySceneToShots as jest.Mock).mockResolvedValue({
- successful: true,
- data: { success: true },
- });
-
- mockSceneEditUseCase.applyScene.mockResolvedValue({} as any);
-
- const applyResult = await applySceneToShots({
- sceneId: 'scene1',
- shotIds: ['shot1', 'shot2']
- });
- expect(applyResult.successful).toBe(true);
- });
-
- it('应该模拟用户选择场景并修改提示词的完整流程', async () => {
- // 用户操作:获取项目中的场景列表
- const mockScenes = [
- { ...mockSceneEntity, id: 'scene1', name: '场景1' },
- { ...mockSceneEntity, id: 'scene2', name: '场景2' }
- ];
- (getSceneList as jest.Mock).mockResolvedValue({
- successful: true,
- data: mockScenes,
- });
-
- const sceneList = await getSceneList({ projectId: 'project1' });
- expect(sceneList.data).toHaveLength(2);
-
- // 用户操作:选择第一个场景
- const selectedSceneId = 'scene1';
- (getSceneData as jest.Mock).mockResolvedValue({
- successful: true,
- data: {
- text: mockTextEntity,
- tags: [mockTagValueObject1],
- },
- });
-
- const selectedSceneData = await getSceneData({ sceneId: selectedSceneId });
- expect(selectedSceneData.data.text.id).toBe('text1');
-
- // 用户操作:修改场景提示词
- const newPrompt = '我想要一个更加戏剧性的场景';
- (updateText as jest.Mock).mockResolvedValue({
- successful: true,
- data: { ...mockTextEntity, content: newPrompt },
- });
-
- const updatedText = await updateText({
- textId: 'text1',
- content: newPrompt
- });
- expect(updatedText.data.content).toBe(newPrompt);
-
- // 用户操作:使用新提示词重新生成场景
- const regeneratedScene = { ...mockSceneEntity, name: '戏剧性场景' };
- (regenerateScene as jest.Mock).mockResolvedValue({
- successful: true,
- data: regeneratedScene,
- });
-
- const regenerationResult = await regenerateScene({
- prompt: newPrompt,
- tagTypes: ['tag1'],
- sceneId: selectedSceneId
- });
- expect(regenerationResult.data.name).toBe('戏剧性场景');
- });
-
- it('应该处理场景编辑流程中的错误情况', async () => {
- // 模拟获取场景列表失败
- (getSceneList as jest.Mock).mockResolvedValue({
- successful: false,
- message: '获取场景列表失败',
- });
-
- const sceneListResult = await getSceneList({ projectId: 'project1' });
- expect(sceneListResult.successful).toBe(false);
- expect(sceneListResult.message).toBe('获取场景列表失败');
-
- // 模拟修改文本失败
- (updateText as jest.Mock).mockResolvedValue({
- successful: false,
- message: '修改文本失败',
- });
-
- const updateTextResult = await updateText({
- textId: 'text1',
- content: '新的文本内容'
- });
- expect(updateTextResult.successful).toBe(false);
- expect(updateTextResult.message).toBe('修改文本失败');
-
- // 模拟重新生成场景失败
- (regenerateScene as jest.Mock).mockResolvedValue({
- successful: false,
- message: '重新生成场景失败',
- });
-
- const regenerateResult = await regenerateScene({
- prompt: '重新生成场景',
- tagTypes: ['tag1'],
- sceneId: 'scene1'
- });
- expect(regenerateResult.successful).toBe(false);
- expect(regenerateResult.message).toBe('重新生成场景失败');
-
- // 模拟应用场景失败
- (applySceneToShots as jest.Mock).mockResolvedValue({
- successful: false,
- message: '应用场景失败',
- });
-
- const applyResult = await applySceneToShots({
- sceneId: 'scene1',
- shotIds: ['shot1']
- });
- expect(applyResult.successful).toBe(false);
- expect(applyResult.message).toBe('应用场景失败');
- });
- });
-
- describe('场景应用到多个分镜测试', () => {
- it('应该成功获取场景分镜列表', async () => {
- const mockShots = [mockShotEntity];
- (getSceneShots as jest.Mock).mockResolvedValue({
- successful: true,
- data: {
- shots: mockShots,
- appliedShotIds: [],
- },
- });
-
- const result = await getSceneShots({ sceneId: 'scene1' });
-
- expect(getSceneShots).toHaveBeenCalledWith({ sceneId: 'scene1' });
- expect(result.successful).toBe(true);
- expect(result.data.shots).toEqual(mockShots);
- expect(result.data.appliedShotIds).toEqual([]);
- });
-
- it('应该成功应用场景到选中的分镜', async () => {
- (applySceneToShots as jest.Mock).mockResolvedValue({
- successful: true,
- data: { success: true },
- });
-
- mockSceneEditUseCase.applyScene.mockResolvedValue({} as any);
-
- const result = await applySceneToShots({
- sceneId: 'scene1',
- shotIds: ['shot1', 'shot2']
- });
-
- expect(applySceneToShots).toHaveBeenCalledWith({
- sceneId: 'scene1',
- shotIds: ['shot1', 'shot2']
- });
- expect(result.successful).toBe(true);
- });
-
- it('应用场景失败时应该返回错误信息', async () => {
- (applySceneToShots as jest.Mock).mockResolvedValue({
- successful: false,
- message: '应用失败',
- });
-
- const result = await applySceneToShots({
- sceneId: 'scene1',
- shotIds: ['shot1']
- });
-
- expect(result.successful).toBe(false);
- expect(result.message).toBe('应用失败');
- });
-
- it('应该正确处理已应用的分镜状态', async () => {
- const mockShots = [mockShotEntity];
- (getSceneShots as jest.Mock).mockResolvedValue({
- successful: true,
- data: {
- shots: mockShots,
- appliedShotIds: ['shot1'], // 分镜1已应用
- },
- });
-
- const result = await getSceneShots({ sceneId: 'scene1' });
-
- expect(result.data.appliedShotIds).toEqual(['shot1']);
- expect(result.data.shots).toEqual(mockShots);
- });
- });
-
- describe('UseCase业务逻辑测试', () => {
- it('SceneEditUseCase应该正确初始化', () => {
- const sceneItem = new SceneItem(mockSceneEntity);
- const useCase = new SceneEditUseCase(sceneItem);
-
- expect(SceneEditUseCase).toHaveBeenCalledWith(sceneItem);
- expect(useCase).toBeDefined();
- });
-
- it('TextEditUseCase应该正确初始化', () => {
- const textItem = new TextItem(mockTextEntity);
- const useCase = new TextEditUseCase(textItem);
-
- expect(TextEditUseCase).toHaveBeenCalledWith(textItem);
- expect(useCase).toBeDefined();
- });
-
- it('TagEditUseCase应该正确初始化', () => {
- const tagItem = new TagItem(mockTagValueObject1);
- const useCase = new TagEditUseCase(tagItem);
-
- expect(TagEditUseCase).toHaveBeenCalledWith(tagItem);
- expect(useCase).toBeDefined();
- });
- });
-
- describe('Domain实体测试', () => {
- it('SceneItem应该正确包装SceneEntity', () => {
- const sceneItem = new SceneItem(mockSceneEntity);
-
- expect(SceneItem).toHaveBeenCalledWith(mockSceneEntity);
- expect(sceneItem.entity).toEqual(mockSceneEntity);
- expect(sceneItem.disableEdit).toBe(false);
- });
-
- it('TextItem应该正确包装AITextEntity', () => {
- const textItem = new TextItem(mockTextEntity);
-
- expect(TextItem).toHaveBeenCalledWith(mockTextEntity);
- expect(textItem.entity).toEqual(mockTextEntity);
- expect(textItem.disableEdit).toBe(false);
- });
-
- it('TagItem应该正确包装TagValueObject', () => {
- const tagItem = new TagItem(mockTagValueObject1);
-
- expect(TagItem).toHaveBeenCalledWith(mockTagValueObject1);
- expect(tagItem.entity).toEqual(mockTagValueObject1);
- expect(tagItem.disableEdit).toBe(false);
- });
- });
-
- describe('错误处理测试', () => {
- it('API调用失败时应该正确处理错误', async () => {
- (getSceneList as jest.Mock).mockRejectedValue(new Error('网络错误'));
-
- await expect(getSceneList({ projectId: 'project1' })).rejects.toThrow('网络错误');
- });
-
- it('API返回失败状态时应该正确处理', async () => {
- (getSceneList as jest.Mock).mockResolvedValue({
- successful: false,
- message: '服务器错误',
- });
-
- const result = await getSceneList({ projectId: 'project1' });
-
- expect(result.successful).toBe(false);
- expect(result.message).toBe('服务器错误');
- });
-
- it('UseCase未初始化时应该抛出相应错误', async () => {
- const sceneItem = new SceneItem(mockSceneEntity);
- const useCase = new SceneEditUseCase(sceneItem);
-
- // 模拟UseCase未初始化的情况
- mockSceneEditUseCase.AIgenerateScene.mockRejectedValue(new Error('场景编辑UseCase未初始化'));
-
- await expect(useCase.AIgenerateScene({} as any, [])).rejects.toThrow('场景编辑UseCase未初始化');
- });
- });
-
- describe('场景数据完整性测试', () => {
- it('应该验证场景实体的完整性', () => {
- const sceneItem = new SceneItem(mockSceneEntity);
-
- expect(sceneItem.entity.id).toBe('scene1');
- expect(sceneItem.entity.name).toBe('测试场景');
- expect(sceneItem.entity.imageUrl).toBe('http://example.com/scene1.jpg');
- expect(sceneItem.entity.tagIds).toEqual(['tag1', 'tag2']);
- expect(sceneItem.entity.generateTextId).toBe('text1');
- });
-
- it('应该验证文本实体的完整性', () => {
- const textItem = new TextItem(mockTextEntity);
-
- expect(textItem.entity.id).toBe('text1');
- expect(textItem.entity.content).toBe('这是AI生成的场景文本内容');
- });
-
- it('应该验证标签实体的完整性', () => {
- const tagItem = new TagItem(mockTagValueObject1);
-
- expect(tagItem.entity.id).toBe('tag1');
- expect(tagItem.entity.name).toBe('场景标签1');
- expect(tagItem.entity.content).toBe('场景标签内容1');
- });
- });
-});
diff --git a/app/service/usecase/RoleEditUseCase.ts b/app/service/usecase/RoleEditUseCase.ts
index 67441ec..ba5e6b1 100644
--- a/app/service/usecase/RoleEditUseCase.ts
+++ b/app/service/usecase/RoleEditUseCase.ts
@@ -14,6 +14,7 @@ import {
saveRegeneratedCharacter,
getSimilarCharacters,
checkShotVideoStatus,
+ analyzeImageDescription,
} from '@/api/video_flow';
/**
@@ -71,9 +72,9 @@ export class RoleEditUseCase {
tags: [], // 默认为空标签数组
imageUrl: char.image_path || '', // 使用API返回的图片路径
loadingProgress: 100, // 默认加载完成
- disableEdit: false, // 默认允许编辑
updatedAt: Date.now(),
- fromDraft: false
+ fromDraft: false,
+ isChangeRole: false
};
return roleEntity;
@@ -123,14 +124,13 @@ export class RoleEditUseCase {
/** 内容 */
content: highlight,
loadingProgress: 100,
- disableEdit: false
}))
: [],
imageUrl: char.image_path || '',
loadingProgress: 100,
- disableEdit: false,
updatedAt: Date.now(),
- fromDraft: false
+ fromDraft: false,
+ isChangeRole: false
};
return roleEntity;
@@ -159,9 +159,9 @@ export class RoleEditUseCase {
tags: [], // 相似角色接口可能不返回标签,暂时为空
imageUrl: char.avatar || '',
loadingProgress: 100,
- disableEdit: false,
updatedAt: Date.now(),
- fromDraft: false
+ fromDraft: false,
+ isChangeRole: false
};
return roleEntity;
@@ -249,13 +249,12 @@ export class RoleEditUseCase {
name: highlight,
content: highlight,
loadingProgress: 100,
- disableEdit: false
})), // 将高亮关键词转换为TagValueObject格式
imageUrl: characterData.image_url || '',
loadingProgress: 100,
- disableEdit: false,
updatedAt: Date.now(),
- fromDraft: false
+ fromDraft: false,
+ isChangeRole: true
};
return roleEntity;
} catch (error) {
@@ -315,9 +314,6 @@ export class RoleEditUseCase {
*/
async optimizeRoleDescription( selectedRole: RoleEntity): Promise<{optimizedDescription: string, keywords: string[]}> {
try {
- // if (!this.selectedRole) {
- // throw new Error('请先选择角色');
- // }
// 调用新的AI优化角色描述API
const response = await generateCharacterDescription({
@@ -346,49 +342,6 @@ export class RoleEditUseCase {
}
}
- /**
- * @description: 从AI文本描述中解析标签信息(预留函数,未来实现)
- * @param aiText AI文本描述
- * @returns Promise 解析出的标签列表
- */
- async parseTagsFromAiText(aiText: string): Promise {
- // TODO: 未来实现从AI文本中解析标签的逻辑
- // 例如:解析文本中的关键词、特征描述等作为标签
- return [];
- }
-
- /**
- * @description 根据图片地址获取角色实体数据
- * @param imageUrl 图片地址
- * @returns Promise 角色实体数据
- */
- async getRoleByImage(imageUrl: string): Promise {
- try {
- // TODO: 调用后端API,根据图片地址获取角色数据
- // 这里需要根据实际的后端API接口来实现
- // const response = await getRoleByImage({ imageUrl });
-
- // 临时实现:返回一个模拟的角色实体
- // 实际使用时需要替换为真实的API调用
- const mockRole: RoleEntity = {
- id: `role_${Date.now()}`,
- name: '从图片识别的角色',
- generateText: '通过图片识别生成的角色描述',
- tags: [], // 空标签数组
- imageUrl: imageUrl, // 使用传入的图片地址
- loadingProgress: 100, // 加载完成
- disableEdit: false, // 允许编辑
- updatedAt: Date.now(),
- fromDraft: false
- };
-
- return mockRole;
- } catch (error) {
- console.error('根据图片获取角色失败:', error);
- throw new Error('根据图片获取角色失败');
- }
- }
-
/**
* @description 保存重新生成的角色到角色库
* @param roleData 角色实体数据
@@ -451,4 +404,56 @@ export class RoleEditUseCase {
}
}
+ /**
+ * @description 分析图片并更新角色信息
+ * @param imageUrl 图片URL地址
+ * @param selectedRole 当前选中的角色
+ * @returns Promise 更新后的角色实体
+ */
+ async analyzeImageAndUpdateRole(imageUrl: string, selectedRole: RoleEntity): Promise {
+ try {
+ // 调用图片分析接口获取描述
+ const result = await analyzeImageDescription({
+ image_url: imageUrl,
+ });
+
+ if (!result.successful) {
+ throw new Error(`图片分析失败: ${result.message}`);
+ }
+
+ const { description, highlights } = result.data;
+
+ // 更新当前选中角色的图片、描述和标签
+ const updatedRole: RoleEntity = {
+ ...selectedRole,
+ imageUrl: imageUrl,
+ generateText: description,
+ tags: highlights.map((highlight: string, index: number) => ({
+ id: `tag_${Date.now()}_${index}`,
+ /** 名称 */
+ name: highlight,
+ /** 内容 */
+ content: highlight,
+ loadingProgress: 100,
+ updatedAt: Date.now(),
+ })),
+ };
+
+ // 更新角色列表中的对应角色
+ if (Array.isArray(this.roleList)) {
+ this.roleList = this.roleList.map(role =>
+ role.id === selectedRole.id ? updatedRole : role
+ );
+ }
+
+ // 更新当前选中的角色
+ this.selectedRole = updatedRole;
+
+ return updatedRole;
+ } catch (error) {
+ console.error('分析图片并更新角色失败:', error);
+ throw error;
+ }
+ }
+
}
diff --git a/app/service/usecase/TagEditUseCase.ts b/app/service/usecase/TagEditUseCase.ts
deleted file mode 100644
index adeb502..0000000
--- a/app/service/usecase/TagEditUseCase.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-
-import { TagValueObject } from '../domain/valueObject';
-
-/**
- * 标签编辑用例
- * 负责标签内容的初始化、修改和优化
- */
-export class TagEditUseCase {
- constructor(public tagList: TagValueObject[]) {
- }
-
- /**
- * 修改标签内容
- * @param tagName 标签名称
- * @param newContent 新内容
- */
- async updateTag(tagName: string, newContent: string | number): Promise {
- const tag = this.tagList.find(tag => tag.name === tagName);
- if (tag) {
- tag.content = newContent;
- }
- }
-
- /**
- * 新增标签
- * @param tagName 标签名称
- * @param content 标签内容
- */
- async addTag(tagName: string, content: string | number = ""): Promise {
- // 检查标签是否已存在
- const existingTag = this.tagList.find(tag => tag.name === tagName);
- if (existingTag) {
- throw new Error(`标签 "${tagName}" 已存在`);
- }
-
- // 创建新标签
- const newTag: TagValueObject = {
- id: `tag_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
- updatedAt: Date.now(),
- name: tagName,
- content: content,
- loadingProgress: 100,
- disableEdit: false
- };
-
- this.tagList.push(newTag);
- }
-
- /**
- * 删除指定标签
- * @param tagName 要删除的标签名称
- */
- async deleteTag(tagName: string): Promise {
- const tagIndex = this.tagList.findIndex(tag => tag.name === tagName);
- if (tagIndex === -1) {
- throw new Error(`标签 "${tagName}" 不存在`);
- }
-
- this.tagList.splice(tagIndex, 1);
- }
-
- /**
- * 清空所有标签
- */
- async clearAllTags(): Promise {
- this.tagList.length = 0;
- }
-
- /**
- * 获取所有标签
- * @returns 标签列表
- */
- getTagList(): TagValueObject[] {
- return [...this.tagList];
- }
-
- /**
- * 根据标签名称获取标签
- * @param tagName 标签名称
- * @returns 标签对象或undefined
- */
- getTagByName(tagName: string): TagValueObject | undefined {
- return this.tagList.find(tag => tag.name === tagName);
- }
-}