推进模板生成故事,图片生成故事的一些优化,以及错误情况的报错提示

This commit is contained in:
海龙 2025-08-21 18:08:30 +08:00
parent 4c427aba97
commit 9295fe8b2a
14 changed files with 808 additions and 264 deletions

View File

@ -2,151 +2,178 @@
* AI分析返回DTO
*/
export interface CharacterRegion {
/** x坐标 */
x: number;
/** y坐标 */
y: number;
/** 区域宽度 */
width: number;
/** 区域高度 */
height: number;
/** x坐标 */
x: number;
/** y坐标 */
y: number;
/** 区域宽度 */
width: number;
/** 区域高度 */
height: number;
}
/** 角色分析信息 */
export interface CharacterAnalysis {
/** 角色头像URL */
crop_url: string;
/** 角色ID */
id: string;
/** 角色名称 */
role_name: string;
/** 角色区域 */
region: CharacterRegion|null;
/** 角色描述 */
whisk_caption: string;
/** 角色头像URL可选用于存储裁剪后的头像 */
avatarUrl?: string;
/** 角色头像URL */
crop_url: string;
/** 角色ID */
id: string;
/** 角色名称 */
role_name: string;
/** 角色区域 */
region: CharacterRegion | null;
/** 角色描述 */
whisk_caption: string;
/** 角色头像URL可选用于存储裁剪后的头像 */
avatarUrl?: string;
}
/** 图片故事AI分析返回结构 */
export interface MovieStartDTO {
/** 是否成功 */
success: boolean;
/** 故事梗概 */
story_logline: string;
/** 分类数据 */
potential_genres: string[];
/** 角色头像及名称 */
characters_analysis: CharacterAnalysis[];
/** 图片URL */
image_url: string;
/** 用户输入文本 */
user_text: string;
/** 错误信息 */
error: string | null;
/** 是否成功 */
success: boolean;
/** 故事梗概 */
story_logline: string;
/** 分类数据 */
potential_genres: string[];
/** 角色头像及名称 */
characters_analysis: CharacterAnalysis[];
/** 图片URL */
image_url: string;
/** 用户输入文本 */
user_text: string;
/** 错误信息 */
error: string | null;
}
/**
* V2请求参数
* V2请求参数
*/
export interface CreateMovieProjectV2Request {
/** 剧本内容 */
script: string;
/** 用户ID */
user_id: string;
/** 模式auto | manual */
mode: "auto" | "manual";
/** 分辨率720p | 1080p | 4k */
resolution: "720p" | "1080p" | "4k";
/** 类型 */
genre: string;
/** 角色简介数组 */
character_briefs: string[];
/** 语言 */
language: string;
/** 图片URL */
image_url: string;
}
/** 剧本内容 */
script: string;
/** 用户ID */
user_id: string;
/** 模式auto | manual */
mode: "auto" | "manual";
/** 分辨率720p | 1080p | 4k */
resolution: "720p" | "1080p" | "4k";
/** 类型 */
genre: string;
/** 角色简介数组 */
character_briefs: string[];
/** 语言 */
language: string;
/** 图片URL */
image_url: string;
}
/**
* V2响应数据
*/
export interface CreateMovieProjectV2Response {
/** 原始文本 */
original_text: string;
/** 项目ID */
project_id: string;
/** 视频数据 */
video: Record<string, any>;
/** 扩展数据2 */
ext2: Record<string, any>;
/** 多语言视频 */
multilingual_video: Record<string, any>;
/** 制作手册 */
production_bible: string;
/** 项目名称 */
name: string;
/** 音乐数据 */
music: Record<string, any>;
/** 扩展数据3 */
ext3: Record<string, any>;
/** 语言 */
language: string;
/** 分镜头 */
storyboard: string;
/** 状态 */
status: string;
/** 最终视频 */
final_video: Record<string, any>;
/** 扩展数据4 */
ext4: string;
/** 产品代码 */
pcode: string;
/** 制作手册JSON */
production_bible_json: Record<string, any>;
/** 步骤 */
step: string;
/** 最终简单视频 */
final_simple_video: Record<string, any>;
/** 扩展数据5 */
ext5: string;
/** 产品代码版本 */
pcode_version: string;
/** 分辨率 */
resolution: string;
/** 剧本分镜头 */
script_shots: string;
/** 最后消息 */
last_message: string;
/** 扩展数据 */
ext: Record<string, any>;
/** 角色草稿 */
character_draft: any;
/** 模式 */
mode: string;
/** 提示词 */
prompts: string;
/** 草图 */
sketch: Record<string, any>;
/** 当前任务ID */
current_task_id: string;
/** 创建时间 */
created_at: string;
/** ID */
id: number;
/** 描述 */
description: string;
/** 镜头草图 */
shot_sketch: Record<string, any>;
/** 扩展数据1 */
ext1: Record<string, any>;
/** 当前计划ID */
current_plan_id: string;
/** 用户ID */
user_id: string;
/** 生成的剧本 */
generated_script: string;
/** 角色数据 */
character: Record<string, any>;
/** 更新时间 */
updated_at: string;
}
/**
* V2响应数据
*/
export interface CreateMovieProjectResponse {
/** 原始文本 */
original_text: string;
/** 项目ID */
project_id: string;
/** 视频数据 */
video: Record<string, any>;
/** 扩展数据2 */
ext2: Record<string, any>;
/** 多语言视频 */
multilingual_video: Record<string, any>;
/** 制作手册 */
production_bible: string;
/** 项目名称 */
name: string;
/** 音乐数据 */
music: Record<string, any>;
/** 扩展数据3 */
ext3: Record<string, any>;
/** 语言 */
language: string;
/** 分镜头 */
storyboard: string;
/** 状态 */
status: string;
/** 最终视频 */
final_video: Record<string, any>;
/** 扩展数据4 */
ext4: string;
/** 产品代码 */
pcode: string;
/** 制作手册JSON */
production_bible_json: Record<string, any>;
/** 步骤 */
step: string;
/** 最终简单视频 */
final_simple_video: Record<string, any>;
/** 扩展数据5 */
ext5: string;
/** 产品代码版本 */
pcode_version: string;
/** 分辨率 */
resolution: string;
/** 剧本分镜头 */
script_shots: string;
/** 最后消息 */
last_message: string;
/** 扩展数据 */
ext: Record<string, any>;
/** 角色草稿 */
character_draft: any;
/** 模式 */
mode: string;
/** 提示词 */
prompts: string;
/** 草图 */
sketch: Record<string, any>;
/** 当前任务ID */
current_task_id: string;
/** 创建时间 */
created_at: string;
/** ID */
id: number;
/** 描述 */
description: string;
/** 镜头草图 */
shot_sketch: Record<string, any>;
/** 扩展数据1 */
ext1: Record<string, any>;
/** 当前计划ID */
current_plan_id: string;
/** 用户ID */
user_id: string;
/** 生成的剧本 */
generated_script: string;
/** 角色数据 */
character: Record<string, any>;
/** 更新时间 */
updated_at: string;
}
/**
* V3请求参数
*/
export interface CreateMovieProjectV3Request {
/** 用户ID */
user_id: string;
/** 模式auto | manual */
mode: "auto" | "manual";
/** 分辨率720p | 1080p | 4k */
resolution: "720p" | "1080p" | "4k";
/** 类型 */
genre: string;
/** 语言 */
language: string;
/**模板id */
template_id: string;
/**故事角色 */
storyRole: {
/**角色名 */
role_name: string;
/**照片URL */
photo_url: string;
/**声音URL */
voice_url: string;
}[];
}

37
api/errorHandle.ts Normal file
View File

@ -0,0 +1,37 @@
import { message } from "antd";
import { debounce } from "lodash";
/**
* HTTP错误码对应的用户友好提示信息
*/
const HTTP_ERROR_MESSAGES: Record<number, string> = {
0: "Please try again if the network is abnormal. If it happens again, please contact us.",
400: "Request parameter error, please check your input.",
401: "Login expired, please log in again.",
403: "Insufficient permissions to access this resource.",
404: "Requested resource does not exist.",
408: "Request timeout, please try again.",
429: "Too many requests, please try again later.",
502: "Gateway error, please try again later.",
503: "Service temporarily unavailable, please try again later.",
504: "Gateway timeout, please try again later.",
};
/**
*
*/
const DEFAULT_ERROR_MESSAGE =
"Please try again if the network is abnormal. If it happens again, please contact us.";
/**
*
* @param code - HTTP错误码
* @param customMessage -
*/
export const errorHandle = debounce(
(code: number, customMessage?: string): void => {
const errorMessage =
customMessage || HTTP_ERROR_MESSAGES[code] || DEFAULT_ERROR_MESSAGE;
message.error(errorMessage);
},
100
);

View File

@ -1,5 +1,5 @@
import { ApiResponse } from "./common";
import { MovieStartDTO } from "./DTO/movie_start_dto";
import { CreateMovieProjectV2Request, CreateMovieProjectResponse, MovieStartDTO } from "./DTO/movie_start_dto";
import { get, post } from "./request";
import {
StoryTemplateEntity,
@ -35,3 +35,31 @@ export const AIGenerateImageStory = async (request: {
request
);
};
/**
* V2
* @param request -
* @returns Promise<ApiResponse<CreateMovieProjectResponse>>
*/
export const createMovieProjectV2 = async (
request: CreateMovieProjectV2Request
) => {
return post<ApiResponse<CreateMovieProjectResponse>>(
"/movie/create_movie_project_v2",
request
);
};
/**
* V3
* @param request -
* @returns Promise<ApiResponse<CreateMovieProjectResponse>>
*/
export const createMovieProjectV3 = async (
request: CreateMovieProjectV2Request
) => {
return post<ApiResponse<CreateMovieProjectResponse>>(
"/movie/create_movie_project_v3",
request
);
};

View File

@ -1,5 +1,7 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, AxiosHeaders } from 'axios';
import { message } from "antd";
import { BASE_URL } from './constants'
import { errorHandle } from './errorHandle';
// 创建 axios 实例
const request: AxiosInstance = axios.create({
baseURL: BASE_URL, // 设置基础URL
@ -28,37 +30,21 @@ request.interceptors.request.use(
request.interceptors.response.use(
(response: AxiosResponse) => {
// 直接返回响应数据
if (!response.data?.successful) {
// TODO 暂时固定报错信息,后续根据后端返回的错误码进行处理
errorHandle(0);
}
return response.data;
},
(error) => {
if (error.response) {
switch (error.response.status) {
case 401:
// 未授权,清除 token 并跳转到登录页
localStorage?.removeItem('token');
window.location.href = '/login';
break;
case 403:
// 权限不足
console.error('没有权限访问该资源');
break;
case 404:
// 资源不存在
console.error('请求的资源不存在');
break;
case 500:
// 服务器错误
console.error('服务器错误');
break;
default:
console.error('请求失败:', error.response.data.message || '未知错误');
}
errorHandle(error.response.status);
} else if (error.request) {
// 请求已发出但没有收到响应
console.error('网络错误,请检查您的网络连接');
errorHandle(0);
} else {
// 请求配置出错
console.error('请求配置错误:', error.message);
// 检修
console.error(error);
}
return Promise.reject(error);
}
@ -83,7 +69,6 @@ export const put = <T>(url: string, data?: any, config?: AxiosRequestConfig): Pr
export const del = <T>(url: string, config?: AxiosRequestConfig): Promise<T> => {
return request.delete(url, config);
};
// utils/streamJsonPost.ts
export async function streamJsonPost<T = any>(
url: string,

View File

@ -18,7 +18,6 @@ import { task_item, VideoSegmentEntityAdapter } from "@/app/service/adapter/oldE
import { VideoFlowProjectResponse, NewCharacterItem, NewCharacterListResponse, CharacterListByProjectWithHighlightResponse, CharacterUpdateAndRegenerateRequest, CharacterUpdateAndRegenerateResponse } from "./DTO/movieEdit";
import { RoleResponse } from "./DTO/movieEdit";
import { RoleRecognitionResponse } from "./DTO/movieEdit";
import { CreateMovieProjectV2Request, CreateMovieProjectV2Response } from "./DTO/movie_start_dto";
/**
*
@ -1206,18 +1205,3 @@ export const modifyCharacterOrScene = async (request: {
/**
* V2
* @param request -
* @returns Promise<ApiResponse<CreateMovieProjectV2Response>>
*/
export const createMovieProjectV2 = async (
request: CreateMovieProjectV2Request
)=> {
return post<ApiResponse<CreateMovieProjectV2Response>>(
"/movie/create_movie_project_v2",
request
);
};

View File

@ -8,9 +8,8 @@ import {
Dispatch,
SetStateAction,
} from "react";
import { CharacterAnalysis, CreateMovieProjectV2Request, CreateMovieProjectV2Response } from "@/api/DTO/movie_start_dto";
import { createMovieProjectV2 } from "@/api/video_flow";
import { ApiResponse } from "@/api/common";
import { CharacterAnalysis, CreateMovieProjectV2Request, CreateMovieProjectResponse } from "@/api/DTO/movie_start_dto";
import { createMovieProjectV2 } from "@/api/movie_start";
interface UseImageStoryService {
/** 当前图片故事数据 */
@ -53,7 +52,7 @@ interface UseImageStoryService {
mode?: "auto" | "manual",
resolution?: "720p" | "1080p" | "4k",
language?: string
) => Promise<CreateMovieProjectV2Response|undefined>;
) => Promise<CreateMovieProjectResponse|undefined>;
/** 设置角色分析 */
setCharactersAnalysis: Dispatch<SetStateAction<CharacterAnalysis[]>>;
/** 设置原始用户描述 */
@ -425,7 +424,12 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
try {
if (hasAnalyzed) {
// 从charactersAnalysis中提取whisk_caption字段组成数组
const character_briefs = charactersAnalysis.map(char => char.whisk_caption);
const character_briefs = charactersAnalysis.map(char => {
const obj = JSON.parse(char.whisk_caption);
obj.character_analysis.role_name = char.role_name;
return JSON.stringify(obj);
});
const params: CreateMovieProjectV2Request = {
script: storyContent,

View File

@ -36,6 +36,8 @@ interface UseTemplateStoryService {
setActiveRoleImage: (imageUrl: string) => void;
/** 设置当前活跃角色的音频URL */
setActiveRoleAudio: (audioUrl: string) => void;
/**清空数据 */
clearData: () => void;
}
export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
@ -140,7 +142,7 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
]
}
]);
}, 1000);
}, 10000);
});
setTemplateStoryList(templates);
@ -162,9 +164,9 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
try {
setIsLoading(true);
const projectId = await templateStoryUseCase.actionStory(selectedTemplate);
return projectId;
console.log('selectedTemplate', selectedTemplate)
// const projectId = await templateStoryUseCase.actionStory(selectedTemplate);
return 'projectId';
} catch (err) {
console.error('生成电影失败:', err);
throw err;
@ -226,5 +228,10 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
setActiveRoleIndex: handleSetActiveRoleIndex,
setActiveRoleImage,
setActiveRoleAudio,
clearData: () => {
setTemplateStoryList([]);
setSelectedTemplate(null);
setActiveRoleIndex(0);
}
};
};

View File

@ -125,3 +125,29 @@ export function useLoadScriptText(loading: boolean): { loadingText: string } {
}, [loading, tests.length]);
return { loadingText: tests[currentIndex] };
}
/**
*
* @param {File | Blob} audioFile - Blob
* @returns {Promise<number>}
*/
export const getAudioDuration = (audioFile: File | Blob): Promise<number> => {
return new Promise((resolve, reject) => {
const audio = new Audio();
const url = URL.createObjectURL(audioFile);
audio.addEventListener('loadedmetadata', () => {
const duration = audio.duration;
URL.revokeObjectURL(url);
resolve(duration);
});
audio.addEventListener('error', (error) => {
URL.revokeObjectURL(url);
reject(new Error(`无法读取音频文件: ${error.message}`));
});
audio.src = url;
});
};

View File

@ -0,0 +1,410 @@
describe("flow 提示词优化", () => {
it("图片分析人物 提示词", () => {
const prompt = `
使姿
****
1. ****
-
-
- ,
-
-
2. ****
-
-
-
-
-
- 线
3. **** 30
- 使
-
-
- 10
4. **** 20
- logo
- logo
- logo或装饰品
-
****
- 姿
- ,
-
-
-
`;
const eng = `You are a professional image analysis model tasked with analyzing a full-body photo of a person and extracting their inherent attributes (basic information, appearance characteristics, clothing, and accessories) for use in a video generation model. The description should focus on the static characteristics of the person, excluding emotions, actions, posture, background, or surroundings. Ensure the language is clear, concise, and unambiguous, and output structured according to the following categories. Avoid using unknown or vague terms; provide precise descriptions and numerical values.
**Output Format**:
1. **Basic Information**:
- Gender: (e.g., male, female)
- Age: Specify an exact age
- Body Type: Provide a specific body type description, including weight
- Estimated Height: Specify a specific value
- Race: Estimate the person's race.
2. **Detailed Facial Features**: Requires a predicted value.
- Eyes: Size, position, eye shape, and spacing.
- Nose: Size, position, bridge height, nose width, and nose tip angle.
- Mouth: Size, position, mouth shape, and mouth angle.
- Ears: Size, position, and ear shape.
- Face Shape: Provide a detailed description of the face shape.
- Forehead: Size, position, and forehead shape.
3. **Appearance Features**: Each paragraph should contain at least 50 words.
- Skin Color: Specific color information should be provided; avoid vague concepts such as light or dark.
Hair: Color, length, hairstyle, whether it covers part of the face, and if so, which part.
- Facial features: Makeup, glasses, or lack thereof. For beards, eyebrows, and other hair, please describe in detail the area covered, the amount of hair, and color.
- Other notable features: (For example, tattoos, scars, or jewelry, indicate "none" or "unknown"). If present, please describe their appearance in detail.
4. **Clothing and Accessories**: Each paragraph should contain at least 20 words.
- Upper Body: Top style, color, material, whether it is long-sleeved or short-sleeved, whether it is tight-fitting or loose-fitting, whether it has a pattern, logo, or decorations.
- Lower Body: Pants style, color, material, whether they are long pants or shorts, whether they are tight-fitting or loose-fitting, whether they have a pattern, logo, or decorations.
- Shoes: Shoe style, color, material, whether they are sneakers or leather shoes, whether they have a logo, or decorations. Accessories: Hats, glasses, scarves, gloves, bags, watches, necklaces, earrings, rings, bracelets, anklets, belts, ties, bow ties
**Requirements**:
- Describe only the inherent attributes of the person, ignoring emotions, movements, poses, background, environment, or any dynamic information.
- Ensure the description is accurate and objective, based on the content of the photo. Avoid vague terms, but instead provide reasonable numerical values or precise descriptors. Avoid vague terms such as "suitable," "moderate," and "appropriate."
- If details are blurry or unavailable, provide a reasonable guess; avoid "unknown" or "unsure."
- Output should be plain text, segmented according to the above categories, and concise, suitable for direct use as prompts for video generation models.
- Do not include the photo itself or any image data; provide only text descriptions.
`;
});
it("b0ecf36c-8b34-461f-8cb5-bbb0d26e1fa8 分镜4 5 6 ", () => {
/**
* *
* *
*/
const shot4 = `
==== Continuous Shots ====
#### A. Director's Directive
Role: You are IMF, an expert cinematic video generation AI. I am the Film Director. Your task is to translate my detailed storyboard and production bible into a photorealistic, cinematic sequence. Adhere strictly to the shot descriptions, character details, and overall aesthetic.
#### B. Narrative Prompt for IMF
--- SCENE [S01] (Location: [SC-01: Game Studio]) ---
* **Narrative Goal:** The scene's dramatic heart is the quiet, shared vindication of two contrasting partners, whose ridiculed passion project achieves ultimate success, culminating in a single, cathartic gesture that solidifies their bond and validates their struggle.
* **Scene Environment Settings:**
* **Time of Day:** Night, approx. 3 AM. The air is still and silent.
* **Weather:** Clear. (Interior Scene)
* **Lighting:** High-contrast and moody. The primary light source is the cool, blue-white glow from the \`PROP-02: Analytics Monitor\`, casting deep shadows across the cluttered office. A single, soft spotlight might fall near the central mascot statue, but the overall feeling is of darkness punctuated by a single point of victory.
* **Character Blocking:** The scene begins with \`[CH-01: Wei]\` seated alone at his desk. When \`[CH-02: Kai]\` enters, he walks from the door (frame right) to stand beside Wei's desk. Later, they both move to a new blocking position, standing on either side of the \`[PROP-03: Mascot Statue]\` in the center of the room.
**Master Character List:**
*[CH-01] Wei
1. **Basic Information**:
- Gender: Male
- Age: 45
- Body Type: The person has an average build, with a slight protrusion of the abdomen, estimated to weigh 75 kg.
- Height Estimation: 175 cm
- Race: East Asian
2. **Facial Details**:
- Eyes: The eyes are of medium size, approximately 28 mm in length and 10 mm in width. They are almond-shaped with a slight upward slant at the outer corners. The palpebral fissures are wide, and the inner canthal angles are sharp, while the outer canthal angles are slightly tilted upward. The distance between the eyes is approximately 32 mm. The pupil color is dark brown, and the iris features are not distinguishable.
- Nose: The nose is approximately 50 mm long and 35 mm wide at the base. The nose bridge is low and broad. The root of the nose is wide, and the tip is slightly rounded. The nostrils are directed slightly forward. The angle of the nose tip is approximately 95 degrees. The nose bridge appears straight.
- Mouth: The mouth is approximately 50 mm wide, with a slight downturn at the corners. The lips are of medium thickness, with a defined vermilion border. The mouth is positioned centrally on the face. The lips are a light pink color with no clear lip line.
- Ears: The ears are of average size, approximately 65 mm in length and 35 mm in width. The auricle is oval-shaped, and the earlobe is detached. The top of the ear is positioned at the same height as the eyebrows, and the bottom is at the same height as the nose. The earlobe has a rounded outline.
- Face Shape: The face shape is rounded with a soft jawline and a full chin. The cheekbones are not prominent. The width-to-height ratio is approximately 1:1.3. The chin is short and rounded. The face is mostly symmetrical.
- Forehead: The forehead is high and wide, approximately 60 mm in height. The hairline is straight and slightly receding. The forehead is positioned high on the face, with a distance of approximately 35 mm between the forehead and eyebrows.
3. **Appearance**:
- Skin Color: The person's skin is a light beige color with a hint of a yellow undertone, consistent with Fitzpatrick scale type 3. The skin appears smooth with no visible blemishes. The skin on the face is a consistent color with no signs of redness or discoloration.
- Hair: The hair is black and short, styled in a crew cut, and does not cover any part of the face. The hair is neatly trimmed and appears to be slightly thinning at the crown. The temples are also showing some gray hairs, which are mixed in with the black hair.
- Facial Features: The person is wearing a pair of clear-framed, rectangular glasses. The eyebrows are black, short, and slightly thick, with a natural shape. There is no visible facial hair or makeup.
- Other Notable Features: The person has a yellow and blue bracelet on their left wrist, which appears to be made of cloth or rubber. There is a watch on the left wrist, but it is not clearly visible. The person is holding a folded, white cloth in their left hand.
4. **Clothing and Accessories**:
- Upper Body: The person is wearing a loose-fitting white T-shirt made of cotton. It has a small, faint, blue graphic on the left chest area. The T-shirt is short-sleeved and has no other visible patterns or logos.
- Bottoms: The person is wearing loose-fitting shorts that appear to be made of a terry cloth-like material. The shorts are a bright green color with a raised, textured, grid-like pattern. They are knee-length.
- Shoes: The shoes are not visible in the photo.
- Accessories: The person has a yellow and blue bracelet on their left wrist. A watch is visible on the same wrist, but its details are obscured. He is also wearing a pair of glasses.
*[CH-02] Kai
1. **Basic Information**:
- Gender: Male
- Age: 35
- Body Type: The person has a rounded, overweight build, weighing approximately 85 kg.
- Height Estimation: 170 cm
- Race: East Asian
2. **Facial Details**:
- Eyes: The eyes are narrow and slightly asymmetrical, with the right eye appearing slightly smaller than the left. The palpebral fissures are approximately 25 mm in length and 8 mm in width. The inner canthal angle is slightly upward, while the outer canthal angle is slightly downward. The distance between the eyes is approximately 30 mm. The pupil color is dark brown, and the iris is not clearly visible.
- Nose: The nose is approximately 45 mm in length and 40 mm in width at the base. The bridge is low and broad. The root of the nose is wide, the tip is rounded and bulbous, and the wings are broad. The nostrils are directed slightly forward. The angle of the nose tip is approximately 95 degrees. The nose bridge appears curved.
- Mouth: The mouth is approximately 55 mm wide, with a neutral expression. The lips are of medium thickness, with a defined cupid's bow and a slightly downturned mouth angle. The mouth is positioned centrally on the face. The lip color is a light pink, and the lip line is well-defined.
- Ears: The ears are of average size, approximately 60 mm in length and 35 mm in width. The auricle is oval-shaped, and the earlobe is detached. The top of the ear is positioned at the same height as the eyes, while the bottom is at the same height as the nose. The earlobe has a rounded outline.
- Face Shape: The face is round with a soft, undefined jawline. The cheekbones are not prominent. The width-to-height ratio is approximately 1:1.1. The chin is short and rounded. The face is mostly symmetrical.
- Forehead: The forehead is high and wide, approximately 70 mm in height. The hairline is rounded and slightly receding at the temples. The forehead is positioned high on the face, with a distance of approximately 40 mm between the forehead and eyebrows.
3. **Appearance**:
- Skin Color: The person's skin is a light beige color with a warm undertone. There are several dark moles on the lower legs, with one particularly noticeable on the left shin. The skin has a smooth texture with no visible blemishes. The skin on the face is of a uniform tone.
- Hair: The hair is black, curly, and styled in a short afro. The length of the hair is approximately 10 cm, extending out from the scalp. The hair is thick and voluminous, but does not cover any part of the face. The hairline is slightly receding at the temples and forehead.
- Facial Features: The person is not wearing glasses. The eyebrows are thick, black, and have a slight arch. There is a small amount of dark facial hair on the upper lip, appearing as a thin mustache, along with some stubble on the chin. There is no visible makeup.
- Other Notable Features: The person has a number of small, dark moles on their legs. There are no visible tattoos, piercings, or other jewelry.
4. **Clothing and Accessories**:
- Upper Body: The person is wearing a loose-fitting, short-sleeved t-shirt in a light pink color. The shirt is made of cotton and features a stylized white logo on the chest.
- Bottoms: The person is wearing black cargo shorts made of a synthetic material. The shorts are loose-fitting and have multiple pockets on the sides.
- Shoes: The person is wearing white and beige sneakers. The shoes are low-cut and have a thick, layered sole. They appear to be made of leather or a synthetic equivalent.
- Accessories: No accessories are visible besides a single, dark sock on the right foot that is peeking out from the top of the sneaker.
--- Clip [E01-S01-C07] Data ---
* **Shot Type:** Medium Close-up (Reaction Shot)
* **Frame Description:** Back on Wei. He doesn't look at Kai. His eyes remain fixed on the number one on the screen. He gives a single, slow, deliberate nod.
* **Key Action / Focus:** Wei's stoic, minimalist confirmation.
* **Shot Continuity & Action Timeline:** State Check: \`[CH-01: Wei]\` is seated. \`[CH-02: Kai]\` is standing beside him (off-screen). | Timeline: [0s-2s] Wei executes a single, slow nod.
* **Atmosphere / Mood:** Grave, certain, profound.
* **Cinematography Blueprint:**
* **Composition:** Same framing as C02 to emphasize the continuity of his state.
* **Camera Motion:** Static.
* **Dialogue & Performance:**
* **Line:** "Its real."
* **Language:** english
* **Delivery:** Flat, factual, almost toneless. The emotion is in its absence.
* **Speaker:** \`[CH-01: Wei]\`
* **Shot 1:** A static close-up on **Kai [CH-02]'s** face, seen in profile. The cool light from the monitor acts as the key light, illuminating his awestruck expression. His typically energetic demeanor is hushed, his eyes wide as he absorbs the information on the screen. The mood is one of breathless, dawning reality.
Kai [CH-02]: So. Its real.
* **Shot 2:** A cut to a medium close-up of **Wei [CH-01]**, matching the earlier framing. He does not look at Kai. His eyes remain fixed on the monitor, his expression grave and certain. He gives a single, slow, deliberate nod, a profound and minimalist confirmation.
Wei [CH-01]: Its real.
#### C. Digital Production Bible
0. **Dialogue Language:** english
1. **Overall Project Style:**
Geographic Setting & Ethnicity: Mainland China, primarily Han Chinese characters.
Format: This entire video is strictly Flawless, borderless, full-frame 16:9 video presentation. The visual style emulates the high dynamic range and rich color technology of the Arri Alexa camera, but the composition is designed for the 16:9 broadcast standard, ensuring there are no black edges or letterboxing.
Aesthetics: Modern urban drama, hyper-realistic, 4K. The primary visual approach is a cinematic shallow depth of field (), creating a moody, intimate atmosphere that isolates the characters from the surrounding chaos of their office, focusing on their internal emotional states.
Technical Blueprint:
Color Science: natural skin tones, true-to-life colors, ARRI REVEAL Color Science look, AWG4 color space
Dynamic Range and Tonality: wide dynamic range, 17 stops of latitude, soft film-like highlight roll-off, LogC4 tonal curve
Frame Rate: 24fps (24fps, to create classic cinematic motion blur)
**Master Scene List:**
* [SC-01] SC-01: Game Studio
A medium-sized, open-plan office space with a lived-in, chaotic-creative aesthetic. The architecture is modern, with exposed ceiling ductwork and polished concrete floors. However, every surface is covered: walls are a dense collage of concept art, technical flowcharts, and notes; desks are cluttered with multiple monitors, drawing tablets, personal trinkets, and an archeological array of empty coffee cups and energy drink cans. A massive whiteboard wall is entirely filled with layers of code and diagrams. The entire space is a shrine to the intense, exhausting process of creation.
**Master Props List:**
* [PROP-02] A sleek, modern 27-inch 4K computer monitor with a thin bezel, set to a dark mode UI. It displays a real-time graph with a glowing electric blue line. The crucial text "GLOBAL DOWNLOADS - RANK #1" is displayed prominently, with the "#1" in a stark, celebratory Fire Engine Red ( RGB: 206, 32, 41).
The objective, undeniable proof of their success. It's the central focus of the scene's opening and the single source of light, acting as a symbolic beacon of their achievement.
* [PROP-03] A life-sized (approx. 6'5") statue of a stylized cartoon basketball player in a heroic pose—one arm bent, index finger pointing to the sky. It's painted in bright, glossy enamel colors. The base is a heavy black plinth with "SKY-HIGH SLAM" inscribed in a bold, energetic white font.
The physical embodiment of their game and their long-held dream. It represents the "joke" that became a monumental success and is the origin of the partners' shared victory gesture. Handled by CH-02 Kai.
5. **Core Atmosphere:**
Primary Mood: Quiet Catharsis. The atmosphere is hushed, intimate, and thick with the weight of shared history and struggle. It is a moment of profound exhaustion mixed with the silent, deeply felt triumph of vindication. The mood is not one of loud celebration, but of a heavy burden being lifted, replaced by a shared, unspoken understanding.
`;
const shot5 = `
==== Continuous Shots ====
#### A. Director's Directive
Role: You are IMF, an expert cinematic video generation AI. I am the Film Director. Your task is to translate my detailed storyboard and production bible into a photorealistic, cinematic sequence. Adhere strictly to the shot descriptions, character details, and overall aesthetic.
#### B. Narrative Prompt for IMF
--- SCENE [S01] (Location: [SC-01: Game Studio]) ---
* **Narrative Goal:** The scene's dramatic heart is the quiet, shared vindication of two contrasting partners, whose ridiculed passion project achieves ultimate success, culminating in a single, cathartic gesture that solidifies their bond and validates their struggle.
* **Scene Environment Settings:**
* **Time of Day:** Night, approx. 3 AM. The air is still and silent.
* **Weather:** Clear. (Interior Scene)
* **Lighting:** High-contrast and moody. The primary light source is the cool, blue-white glow from the \`PROP-02: Analytics Monitor\`, casting deep shadows across the cluttered office. A single, soft spotlight might fall near the central mascot statue, but the overall feeling is of darkness punctuated by a single point of victory.
* **Character Blocking:** The scene begins with \`[CH-01: Wei]\` seated alone at his desk. When \`[CH-02: Kai]\` enters, he walks from the door (frame right) to stand beside Wei's desk. Later, they both move to a new blocking position, standing on either side of the \`[PROP-03: Mascot Statue]\` in the center of the room.
2. **Master Character List:**
*[CH-01] Wei
1. **Basic Information**:
- Gender: Male
- Age: 45
- Body Type: The person has an average build, with a slight protrusion of the abdomen, estimated to weigh 75 kg.
- Height Estimation: 175 cm
- Race: East Asian
2. **Facial Details**:
- Eyes: The eyes are of medium size, approximately 28 mm in length and 10 mm in width. They are almond-shaped with a slight upward slant at the outer corners. The palpebral fissures are wide, and the inner canthal angles are sharp, while the outer canthal angles are slightly tilted upward. The distance between the eyes is approximately 32 mm. The pupil color is dark brown, and the iris features are not distinguishable.
- Nose: The nose is approximately 50 mm long and 35 mm wide at the base. The nose bridge is low and broad. The root of the nose is wide, and the tip is slightly rounded. The nostrils are directed slightly forward. The angle of the nose tip is approximately 95 degrees. The nose bridge appears straight.
- Mouth: The mouth is approximately 50 mm wide, with a slight downturn at the corners. The lips are of medium thickness, with a defined vermilion border. The mouth is positioned centrally on the face. The lips are a light pink color with no clear lip line.
- Ears: The ears are of average size, approximately 65 mm in length and 35 mm in width. The auricle is oval-shaped, and the earlobe is detached. The top of the ear is positioned at the same height as the eyebrows, and the bottom is at the same height as the nose. The earlobe has a rounded outline.
- Face Shape: The face shape is rounded with a soft jawline and a full chin. The cheekbones are not prominent. The width-to-height ratio is approximately 1:1.3. The chin is short and rounded. The face is mostly symmetrical.
- Forehead: The forehead is high and wide, approximately 60 mm in height. The hairline is straight and slightly receding. The forehead is positioned high on the face, with a distance of approximately 35 mm between the forehead and eyebrows.
3. **Appearance**:
- Skin Color: The person's skin is a light beige color with a hint of a yellow undertone, consistent with Fitzpatrick scale type 3. The skin appears smooth with no visible blemishes. The skin on the face is a consistent color with no signs of redness or discoloration.
- Hair: The hair is black and short, styled in a crew cut, and does not cover any part of the face. The hair is neatly trimmed and appears to be slightly thinning at the crown. The temples are also showing some gray hairs, which are mixed in with the black hair.
- Facial Features: The person is wearing a pair of clear-framed, rectangular glasses. The eyebrows are black, short, and slightly thick, with a natural shape. There is no visible facial hair or makeup.
- Other Notable Features: The person has a yellow and blue bracelet on their left wrist, which appears to be made of cloth or rubber. There is a watch on the left wrist, but it is not clearly visible. The person is holding a folded, white cloth in their left hand.
4. **Clothing and Accessories**:
- Upper Body: The person is wearing a loose-fitting white T-shirt made of cotton. It has a small, faint, blue graphic on the left chest area. The T-shirt is short-sleeved and has no other visible patterns or logos.
- Bottoms: The person is wearing loose-fitting shorts that appear to be made of a terry cloth-like material. The shorts are a bright green color with a raised, textured, grid-like pattern. They are knee-length.
- Shoes: The shoes are not visible in the photo.
- Accessories: The person has a yellow and blue bracelet on their left wrist. A watch is visible on the same wrist, but its details are obscured. He is also wearing a pair of glasses.
*[CH-02] Kai
1. **Basic Information**:
- Gender: Male
- Age: 35
- Body Type: The person has a rounded, overweight build, weighing approximately 85 kg.
- Height Estimation: 170 cm
- Race: East Asian
2. **Facial Details**:
- Eyes: The eyes are narrow and slightly asymmetrical, with the right eye appearing slightly smaller than the left. The palpebral fissures are approximately 25 mm in length and 8 mm in width. The inner canthal angle is slightly upward, while the outer canthal angle is slightly downward. The distance between the eyes is approximately 30 mm. The pupil color is dark brown, and the iris is not clearly visible.
- Nose: The nose is approximately 45 mm in length and 40 mm in width at the base. The bridge is low and broad. The root of the nose is wide, the tip is rounded and bulbous, and the wings are broad. The nostrils are directed slightly forward. The angle of the nose tip is approximately 95 degrees. The nose bridge appears curved.
- Mouth: The mouth is approximately 55 mm wide, with a neutral expression. The lips are of medium thickness, with a defined cupid's bow and a slightly downturned mouth angle. The mouth is positioned centrally on the face. The lip color is a light pink, and the lip line is well-defined.
- Ears: The ears are of average size, approximately 60 mm in length and 35 mm in width. The auricle is oval-shaped, and the earlobe is detached. The top of the ear is positioned at the same height as the eyes, while the bottom is at the same height as the nose. The earlobe has a rounded outline.
- Face Shape: The face is round with a soft, undefined jawline. The cheekbones are not prominent. The width-to-height ratio is approximately 1:1.1. The chin is short and rounded. The face is mostly symmetrical.
- Forehead: The forehead is high and wide, approximately 70 mm in height. The hairline is rounded and slightly receding at the temples. The forehead is positioned high on the face, with a distance of approximately 40 mm between the forehead and eyebrows.
3. **Appearance**:
- Skin Color: The person's skin is a light beige color with a warm undertone. There are several dark moles on the lower legs, with one particularly noticeable on the left shin. The skin has a smooth texture with no visible blemishes. The skin on the face is of a uniform tone.
- Hair: The hair is black, curly, and styled in a short afro. The length of the hair is approximately 10 cm, extending out from the scalp. The hair is thick and voluminous, but does not cover any part of the face. The hairline is slightly receding at the temples and forehead.
- Facial Features: The person is not wearing glasses. The eyebrows are thick, black, and have a slight arch. There is a small amount of dark facial hair on the upper lip, appearing as a thin mustache, along with some stubble on the chin. There is no visible makeup.
- Other Notable Features: The person has a number of small, dark moles on their legs. There are no visible tattoos, piercings, or other jewelry.
4. **Clothing and Accessories**:
- Upper Body: The person is wearing a loose-fitting, short-sleeved t-shirt in a light pink color. The shirt is made of cotton and features a stylized white logo on the chest.
- Bottoms: The person is wearing black cargo shorts made of a synthetic material. The shorts are loose-fitting and have multiple pockets on the sides.
- Shoes: The person is wearing white and beige sneakers. The shoes are low-cut and have a thick, layered sole. They appear to be made of leather or a synthetic equivalent.
- Accessories: No accessories are visible besides a single, dark sock on the right foot that is peeking out from the top of the sneaker.
--- Clip [E01-S01-C08] Data ---
* **Shot Type:** Two-Shot
* **Frame Description:** Kai's smile widens into a grin. He claps a hand firmly on Wei's shoulder. The camera angle is from the side, capturing both of them in profile, lit by the monitor.
* **Key Action / Focus:** The first moment of physical contact and shared celebration.
* **Shot Continuity & Action Timeline:** State Check: \`[CH-01: Wei]\` is seated. \`[CH-02: Kai]\` is standing beside him. | Timeline: [0s-1s] Kai's smile widens. [1s-2s] He claps his hand on Wei's shoulder.
* **Atmosphere / Mood:** Exuberant relief, camaraderie.
* **Cinematography Blueprint:**
* **Composition:** A classic two-shot, showing their connection. Shallow depth of field.
* **Camera Motion:** Static.
* **Dialogue & Performance:**
* **Line:** "They laughed at us, Wei. Remember that budget meeting? They said we were building a ghost."
* **Language:** english
* **Delivery:** A rush of triumphant memory, his voice gaining strength and volume.
* **Speaker:** \`[CH-02: Kai]\`
* **Shot 1:** A static two-shot from the side, capturing both **Kai [CH-02]** and **Wei [CH-01]** in profile, lit by the monitor's glow. The background is soft due to a shallow depth of field. Kai's quiet awe breaks as his smile widens into a broad, exuberant grin. He claps a hand firmly on Wei's shoulder, a moment of camaraderie. His voice, filled with a rush of triumphant memory, gains strength and volume as he speaks.
Kai [CH-02]: They laughed at us, Wei. Remember that budget meeting? They said we were building a ghost.
#### C. Digital Production Bible
0. **Dialogue Language:** english
1. **Overall Project Style:**
Geographic Setting & Ethnicity: Mainland China, primarily Han Chinese characters.
Format: This entire video is strictly Flawless, borderless, full-frame 16:9 video presentation. The visual style emulates the high dynamic range and rich color technology of the Arri Alexa camera, but the composition is designed for the 16:9 broadcast standard, ensuring there are no black edges or letterboxing.
Aesthetics: Modern urban drama, hyper-realistic, 4K. The primary visual approach is a cinematic shallow depth of field (), creating a moody, intimate atmosphere that isolates the characters from the surrounding chaos of their office, focusing on their internal emotional states.
Technical Blueprint:
Color Science: natural skin tones, true-to-life colors, ARRI REVEAL Color Science look, AWG4 color space
Dynamic Range and Tonality: wide dynamic range, 17 stops of latitude, soft film-like highlight roll-off, LogC4 tonal curve
Frame Rate: 24fps (24fps, to create classic cinematic motion blur)
4. **Master Scene List:**
* [SC-01] SC-01: Game Studio
A medium-sized, open-plan office space with a lived-in, chaotic-creative aesthetic. The architecture is modern, with exposed ceiling ductwork and polished concrete floors. However, every surface is covered: walls are a dense collage of concept art, technical flowcharts, and notes; desks are cluttered with multiple monitors, drawing tablets, personal trinkets, and an archeological array of empty coffee cups and energy drink cans. A massive whiteboard wall is entirely filled with layers of code and diagrams. The entire space is a shrine to the intense, exhausting process of creation.
4. **Master Props List:**
* [PROP-02] A sleek, modern 27-inch 4K computer monitor with a thin bezel, set to a dark mode UI. It displays a real-time graph with a glowing electric blue line. The crucial text "GLOBAL DOWNLOADS - RANK #1" is displayed prominently, with the "#1" in a stark, celebratory Fire Engine Red ( RGB: 206, 32, 41).
The objective, undeniable proof of their success. It's the central focus of the scene's opening and the single source of light, acting as a symbolic beacon of their achievement.
* [PROP-03] A life-sized (approx. 6'5") statue of a stylized cartoon basketball player in a heroic pose—one arm bent, index finger pointing to the sky. It's painted in bright, glossy enamel colors. The base is a heavy black plinth with "SKY-HIGH SLAM" inscribed in a bold, energetic white font.
The physical embodiment of their game and their long-held dream. It represents the "joke" that became a monumental success and is the origin of the partners' shared victory gesture. Handled by CH-02 Kai.
5. **Core Atmosphere:**
Primary Mood: Quiet Catharsis. The atmosphere is hushed, intimate, and thick with the weight of shared history and struggle. It is a moment of profound exhaustion mixed with the silent, deeply felt triumph of vindication. The mood is not one of loud celebration, but of a heavy burden being lifted, replaced by a shared, unspoken understanding.
`;
});
});

View File

@ -2,11 +2,11 @@
import React, { useState, useRef, useEffect } from "react";
import { Mic, MicOff, Upload, Play, Pause, Trash2, X } from "lucide-react";
import { Tooltip, Upload as AntdUpload } from "antd";
import { Tooltip, Upload as AntdUpload, message } from "antd";
import { InboxOutlined } from "@ant-design/icons";
import WaveSurfer from "wavesurfer.js";
import { useUploadFile } from "../../app/service/domain/service";
import { getAudioDuration, useUploadFile } from "../../app/service/domain/service";
// 自定义样式
const audioRecorderStyles = `
@ -84,7 +84,6 @@ export function AudioRecorder({
audioUrl,
onAudioRecorded,
onAudioDeleted,
title = "请上传参考音频",
showCloseButton = false,
onClose,
}: AudioRecorderProps) {
@ -112,11 +111,20 @@ export function AudioRecorder({
}
};
mediaRecorder.onstop = () => {
mediaRecorder.onstop = async () => {
const audioBlob = new Blob(chunksRef.current, { type: "audio/wav" });
const audioUrl = URL.createObjectURL(audioBlob);
onAudioRecorded(audioBlob, audioUrl);
const duration = await getAudioDuration(audioBlob);
// 无论时长是否符合要求,都要释放麦克风权限
stream.getTracks().forEach((track) => track.stop());
if(duration>20||duration<10){
message.warning("Please keep your audio between 10 and 20 seconds to help us better learn your voice.")
return;
}
onAudioRecorded(audioBlob, audioUrl);
};
mediaRecorder.start();
@ -180,13 +188,23 @@ export function AudioRecorder({
<Tooltip
title="Please clearly read the story description above and record a 15-second audio file for upload"
placement="top"
overlayClassName="max-w-xs"
classNames={{ root: "max-w-xs" }}
>
<div>
<AntdUpload.Dragger
accept="audio/*"
beforeUpload={() => false}
beforeUpload={async(file) => {
console.log("beforeUpload 被调用:", file);
// 移除 return false让文件能够正常进入 customRequest
const duration = await getAudioDuration(file);
if(duration>20||duration<10){
message.warning("Please keep your audio between 10 and 20 seconds to help us better learn your voice.")
return false;
}
return true;
}}
customRequest={async ({ file, onSuccess, onError }) => {
console.log("customRequest 被调用,文件:", file);
try {
const fileObj = file as File;
console.log(
@ -219,6 +237,12 @@ export function AudioRecorder({
showUploadList={false}
className="bg-transparent border-dashed border-white/20 hover:border-white/40"
disabled={isUploading}
onChange={(info) => {
console.log("Upload onChange 事件:", info);
}}
onDrop={(e) => {
console.log("文件拖拽事件:", e);
}}
>
<div className="text-2xl text-white/40 mb-2">
<InboxOutlined />
@ -390,14 +414,36 @@ export function AudioRecorder({
);
}
/**
*
*/
interface WaveformPlayerProps {
/** 音频文件的URL地址 */
audioUrl: string;
/** 当前播放状态true表示正在播放false表示暂停或停止 */
isPlaying: boolean;
/** 播放状态变化时的回调函数,用于同步外部播放状态 */
onPlayStateChange: (isPlaying: boolean) => void;
/** 音量大小范围0-1默认为1最大音量 */
volume?: number;
/** 是否静音true表示静音false表示有声音默认为false */
isMuted?: boolean;
}
/**
*
*
* 使 WaveSurfer.js
*
*
* @param {string} audioUrl - URL地址
* @param {boolean} isPlaying -
* @param {function} onPlayStateChange -
* @param {number} [volume=1] - 0-1
* @param {boolean} [isMuted=false] -
* @returns {JSX.Element}
* ```
*/
function WaveformPlayer({
audioUrl,
isPlaying,
@ -405,47 +451,61 @@ function WaveformPlayer({
volume = 1,
isMuted = false,
}: WaveformPlayerProps) {
/** 容器DOM元素的引用用于挂载WaveSurfer实例 */
const containerRef = useRef<HTMLDivElement | null>(null);
/** WaveSurfer实例的引用用于控制音频播放 */
const wavesurferRef = useRef<WaveSurfer | null>(null);
/**
* WaveSurfer实例
* audioUrl变化时重新创建实例
*/
useEffect(() => {
if (!containerRef.current || !audioUrl) return;
// 创建WaveSurfer实例并配置样式
const ws = WaveSurfer.create({
container: containerRef.current,
waveColor: "#3b82f6",
progressColor: "#1d4ed8",
cursorColor: "#1e40af",
height: 64,
barWidth: 2,
barGap: 1,
normalize: true,
url: audioUrl,
waveColor: "#3b82f6", // 波形颜色
progressColor: "#1d4ed8", // 播放进度颜色
cursorColor: "#1e40af", // 播放光标颜色
height: 64, // 波形高度
barWidth: 2, // 波形条宽度
barGap: 1, // 波形条间距
normalize: true, // 自动标准化波形
url: audioUrl, // 音频文件URL
});
// 监听播放状态变化
ws.on("play", () => onPlayStateChange(true));
ws.on("pause", () => onPlayStateChange(false));
ws.on("finish", () => onPlayStateChange(false));
// 监听播放状态变化事件,同步到外部状态
ws.on("play", () => onPlayStateChange(true)); // 开始播放
ws.on("pause", () => onPlayStateChange(false)); // 暂停播放
ws.on("finish", () => onPlayStateChange(false)); // 播放结束
wavesurferRef.current = ws;
// 组件卸载时销毁WaveSurfer实例释放资源
return () => {
ws.destroy();
};
}, [audioUrl, onPlayStateChange]);
// 同步外部播放状态和音量
/**
*
* isPlayingvolume或isMuted变化时WaveSurfer实例
*/
useEffect(() => {
if (!wavesurferRef.current) return;
// 同步播放状态如果外部状态为播放但WaveSurfer未播放则开始播放
if (isPlaying && !wavesurferRef.current.isPlaying()) {
wavesurferRef.current.play();
} else if (!isPlaying && wavesurferRef.current.isPlaying()) {
}
// 如果外部状态为暂停但WaveSurfer正在播放则暂停播放
else if (!isPlaying && wavesurferRef.current.isPlaying()) {
wavesurferRef.current.pause();
}
// 设置音量
// 设置音量静音时设为0否则使用传入的音量值
const currentVolume = isMuted ? 0 : volume;
wavesurferRef.current.setVolume(currentVolume);
}, [isPlaying, volume, isMuted]);

View File

@ -53,6 +53,7 @@ const RenderTemplateStoryMode = ({
setActiveRoleIndex,
setActiveRoleImage,
setActiveRoleAudio,
clearData
} = useTemplateStoryServiceHook();
// 本地加载状态,用于 UI 反馈
@ -147,11 +148,7 @@ const RenderTemplateStoryMode = ({
};
// 故事编辑器渲染
const storyEditorRender = () => {
return isLoading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
</div>
) : selectedTemplate ? (
return selectedTemplate ? (
<div className="relative h-full">
{/* 模板信息头部 - 增加顶部空间 */}
<div className="flex gap-3 py-4 border-b border-white/[0.1] h-[300px]">
@ -283,7 +280,11 @@ const RenderTemplateStoryMode = ({
Characters
</h4>
{selectedTemplate.storyRole.map((role, index: number) => (
<Tooltip key={index} title={role.role_name} placement="left">
<Tooltip
key={index}
title={role.role_name}
placement="left"
>
<button
data-alt={`character-thumbnail-${index}`}
className={`w-full aspect-square rounded-lg overflow-hidden border-2 transition-all duration-200 hover:scale-105 ${
@ -304,37 +305,9 @@ const RenderTemplateStoryMode = ({
</div>
</div>
</div>
{/* 弹窗底部操作 - 只保留 Action 按钮 */}
{/* <div className="relative group flex justify-end mt-10">
<div className="relative w-40 h-12 opacity-90 overflow-hidden rounded-xl bg-black z-10">
<div className="absolute z-10 -translate-x-32 group-hover:translate-x-[20rem] ease-in transistion-all duration-700 h-full w-32 bg-gradient-to-r from-gray-500 to-white/10 opacity-30 -skew-x-12"></div>
<div className="absolute flex items-center justify-center text-white z-[1] opacity-90 rounded-2xl inset-0.5 bg-black">
<button
name="text"
className="input font-semibold text-base h-full opacity-90 w-full px-10 py-2 rounded-xl bg-black flex items-center justify-center"
onClick={handleConfirm}
>
{localLoading ? (
<>
<Loader2 className="w-3 h-3 animate-spin" />
Actioning...
</>
) : (
<>
<Clapperboard className="w-5 h-5" />
Action
</>
)}
</button>
</div>
<div className="absolute duration-1000 group-hover:animate-spin w-full h-[80px] bg-gradient-to-r from-[rgb(106,244,249)] to-[rgb(199,59,255)] blur-[30px]"></div>
</div>
</div> */}
<div className=" absolute bottom-0 right-0">
<ActionButton
isCreating={localLoading>0}
isCreating={localLoading > 0}
handleCreateVideo={handleConfirm}
icon={<Clapperboard className="w-5 h-5" />}
/>
@ -348,7 +321,7 @@ const RenderTemplateStoryMode = ({
<p className="text-sm">Please try again later</p>
</div>
</div>
);
)
};
return (
<>
@ -356,14 +329,13 @@ const RenderTemplateStoryMode = ({
open={isOpen}
onCancel={() => {
// 清空所有选中的内容数据
setSelectedTemplate(null);
setActiveRoleIndex(0);
clearData()
onClose();
}}
footer={null}
width="60%"
style={{ maxWidth: "800px", marginTop: "0vh" }}
className="template-modal"
className="photo-story-modal !pb-0 rounded-lg bg-white/[0.08] backdrop-blur-[20px] [&_.ant-modal-content]:bg-white/[0.00]"
closeIcon={
<div className="w-6 h-6 bg-white/10 rounded-full flex items-center justify-center hover:bg-white/20 transition-colors">
<span className="text-white/70 text-lg leading-none flex items-center justify-center">
@ -379,10 +351,12 @@ const RenderTemplateStoryMode = ({
Template Story Selection
</h2>
</div>
<div className="flex gap-4">
{templateListRender()}
<div className="flex-1">{storyEditorRender()}</div>
</div>
<GlobalLoad show={isLoading} progress={0}>
<div className="flex gap-4">
{templateListRender()}
<div className="flex-1">{storyEditorRender()}</div>
</div>
</GlobalLoad>
</div>
</Modal>
</>
@ -797,7 +771,9 @@ const PhotoStoryModal = ({
// 处理图片上传
const handleImageUpload = async (e: any) => {
const target = e.target as HTMLImageElement;
if (!(target.tagName == "IMG" || e.target.dataset.alt =="image-upload-area")) {
if (
!(target.tagName == "IMG" || e.target.dataset.alt == "image-upload-area")
) {
return;
}
e.preventDefault();
@ -839,7 +815,7 @@ const PhotoStoryModal = ({
};
const handleAnalyzeImage = async () => {
let timeout = 100
let timeout = 100;
let timer: NodeJS.Timeout;
timer = setInterval(() => {
setLocalLoading((prev) => {
@ -851,8 +827,8 @@ const PhotoStoryModal = ({
}, timeout);
try {
await uploadAndAnalyzeImage();
} finally {
timeout=10
} finally {
timeout = 10;
clearInterval(timer);
timer = setInterval(() => {
setLocalLoading((prev) => {
@ -863,7 +839,6 @@ const PhotoStoryModal = ({
return prev + 1;
});
}, timeout);
}
};
@ -883,7 +858,7 @@ const PhotoStoryModal = ({
</div>
}
>
<GlobalLoad show={localLoading>0} progress={localLoading}>
<GlobalLoad show={localLoading > 0} progress={localLoading}>
<div className="rounded-2xl">
{/* 弹窗头部 */}
<div className="flex items-center gap-3 p-2 border-b border-white/[0.1]">

View File

@ -50,7 +50,7 @@ const TemplateCard: React.FC<TemplateCardProps> = ({
<div className="floating-circle floating-circle-2"></div>
<div className="floating-circle floating-circle-3"></div>
<div className="front-content-overlay">
<div className="free-badge">Free</div>
<div className="free-badge">boutique</div>
<div className="text-content">
<h3 className="card-title">{title}</h3>
<p className="card-description">{description}</p>

View File

@ -3,6 +3,7 @@
import { useEffect, useState } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { checkAuth, isAuthenticated } from '@/lib/auth';
import GlobalLoad from '../common/GlobalLoad';
interface AuthGuardProps {
children: React.ReactNode;
@ -58,7 +59,7 @@ export default function AuthGuard({ children }: AuthGuardProps) {
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<GlobalLoad show={true} progress={0} />
</div>
);
}

View File

@ -5,7 +5,7 @@ interface GlobalLoadProps {
/** 是否显示加载状态 */
show: boolean;
/** 子元素内容 */
children: ReactNode;
children?: ReactNode;
/** 进度条数值 0-100非必须 */
progress?: number;
/** 旋转圆环直径,非必须 */
@ -23,7 +23,7 @@ interface GlobalLoadProps {
*/
export default function GlobalLoad({
show,
children,
children=<></>,
progress,
spinnerDiameter,
progressWidth,
@ -40,7 +40,7 @@ export default function GlobalLoad({
className="!w-max !h-max !flex flex-col items-center justify-center gap-4 -translate-x-1/2 -translate-y-1/2"
>
{showSpinner && <TailwindSpinner diameter={spinnerDiameter} />}
{progress && <TailwindLinearLoader progress={progress} width={progressWidth} />}
{Boolean(progress) && <TailwindLinearLoader progress={progress as number} width={progressWidth} />}
</div>
);