forked from 77media/video-flow
排队和token不足
This commit is contained in:
parent
dd5772c14f
commit
cf1c9823ea
@ -39,7 +39,7 @@ export const getUploadToken = async (timeoutMs: number = 10000): Promise<{ token
|
||||
// 添加超时控制
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.log(`请求超时(${timeoutMs / 1000}秒),正在中断请求...`)
|
||||
console.log(`Request timeout(${timeoutMs / 1000}s),aborting...`)
|
||||
controller.abort()
|
||||
}, timeoutMs)
|
||||
|
||||
@ -59,15 +59,15 @@ export const getUploadToken = async (timeoutMs: number = 10000): Promise<{ token
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error("获取token响应错误:", response.status, errorText)
|
||||
throw new Error(`获取token失败: ${response.status} ${response.statusText}`)
|
||||
console.error("Get token response error:", response.status, errorText)
|
||||
throw new Error(`Get token failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result: TokenWithDomainResponse | TokenResponse = await response.json()
|
||||
console.log("获取token响应:", result)
|
||||
console.log("Get token response:", result)
|
||||
|
||||
if (result.code === 0 && result.successful && result.data.token) {
|
||||
console.log("成功获取token")
|
||||
console.log("Successfully get token")
|
||||
// Support both old and new API response formats
|
||||
const domain = 'domain' in result.data ? result.data.domain : 'cdn.qikongjian.com'
|
||||
return {
|
||||
@ -75,17 +75,17 @@ export const getUploadToken = async (timeoutMs: number = 10000): Promise<{ token
|
||||
domain: domain
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.message || "获取token失败,服务器未返回有效token")
|
||||
throw new Error(result.message || "Get token failed, server did not return a valid token")
|
||||
}
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId)
|
||||
console.error("获取上传token失败:", error)
|
||||
console.error("Get upload token failed:", error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.name === "AbortError") {
|
||||
throw new Error("请求超时,请检查网络连接")
|
||||
throw new Error("Request timeout, please check your network connection")
|
||||
} else if (error.message.includes("Failed to fetch")) {
|
||||
throw new Error("网络连接失败,可能是CORS策略限制或服务器不可达")
|
||||
throw new Error("Network connection failed, possibly due to CORS policy or server unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,25 +129,25 @@ export const uploadToQiniu = async (
|
||||
try {
|
||||
const response: QiniuUploadResponse = JSON.parse(xhr.responseText)
|
||||
const qiniuUrl = `https://cdn.qikongjian.com/${response.key || uniqueFileName}`
|
||||
console.log("七牛云上传成功:", response)
|
||||
console.log("Qiniu cloud upload success:", response)
|
||||
resolve(qiniuUrl)
|
||||
} catch (error) {
|
||||
console.error("解析响应失败:", error, "原始响应:", xhr.responseText)
|
||||
reject(new Error(`解析上传响应失败: ${xhr.responseText}`))
|
||||
console.error("Parse response failed:", error, "Original response:", xhr.responseText)
|
||||
reject(new Error(`Parse upload response failed: ${xhr.responseText}`))
|
||||
}
|
||||
} else {
|
||||
console.error("七牛云上传失败:", xhr.status, xhr.statusText, xhr.responseText)
|
||||
reject(new Error(`上传失败: ${xhr.status} ${xhr.statusText}`))
|
||||
console.error("Qiniu cloud upload failed:", xhr.status, xhr.statusText, xhr.responseText)
|
||||
reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`))
|
||||
}
|
||||
})
|
||||
|
||||
xhr.addEventListener("error", (e) => {
|
||||
console.error("上传网络错误:", e)
|
||||
reject(new Error("网络错误,上传失败"))
|
||||
console.error("Upload network error:", e)
|
||||
reject(new Error("Network error, upload failed"))
|
||||
})
|
||||
|
||||
xhr.addEventListener("abort", () => {
|
||||
reject(new Error("上传被取消"))
|
||||
reject(new Error("Upload aborted"))
|
||||
})
|
||||
|
||||
xhr.open("POST", "https://up-z2.qiniup.com")
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
export const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL
|
||||
// export const BASE_URL ='http://192.168.120.36:8000'
|
||||
// export const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL
|
||||
export const BASE_URL ='http://192.168.120.5:8000'
|
||||
//
|
||||
|
||||
39
api/create_movie.ts
Normal file
39
api/create_movie.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import {
|
||||
CreateMovieProjectV2Request,
|
||||
CreateMovieProjectV3Request,
|
||||
} from "./DTO/movie_start_dto";
|
||||
import { post } from "./request";
|
||||
|
||||
import { withQueuePolling, QueueResponse } from './movie_queue';
|
||||
|
||||
// 新-创建接口
|
||||
export const createMovieProject = async (data: any): Promise<QueueResponse> => {
|
||||
const apiCall = (params: any) => post<QueueResponse>('/movie/create_movie_project', params);
|
||||
return withQueuePolling(apiCall, data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建电影项目V2
|
||||
* @param request - 创建项目请求参数
|
||||
* @returns Promise<QueueResponse>
|
||||
*/
|
||||
export const createMovieProjectV2 = async (
|
||||
request: CreateMovieProjectV2Request
|
||||
): Promise<QueueResponse> => {
|
||||
const apiCall = (params: CreateMovieProjectV2Request) =>
|
||||
post<QueueResponse>("/movie/create_movie_project_v2", params);
|
||||
return withQueuePolling(apiCall, request);
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建电影项目V3
|
||||
* @param request - 创建项目请求参数
|
||||
* @returns Promise<QueueResponse>
|
||||
*/
|
||||
export const createMovieProjectV3 = async (
|
||||
request: CreateMovieProjectV3Request
|
||||
): Promise<QueueResponse> => {
|
||||
const apiCall = (params: CreateMovieProjectV3Request) =>
|
||||
post<QueueResponse>("/movie/create_movie_project_v3", params);
|
||||
return withQueuePolling(apiCall, request);
|
||||
};
|
||||
@ -15,6 +15,7 @@ const HTTP_ERROR_MESSAGES: Record<number, string> = {
|
||||
502: "Gateway error, please try again later.",
|
||||
503: "Service temporarily unavailable, please try again later.",
|
||||
504: "Gateway timeout, please try again later.",
|
||||
4001: "Insufficient points, please recharge.",
|
||||
};
|
||||
|
||||
/**
|
||||
@ -32,7 +33,7 @@ const ERROR_HANDLERS: Record<number, () => void> = {
|
||||
// 跳转到登录页面
|
||||
window.location.href = '/login';
|
||||
},
|
||||
403: () => {
|
||||
4001: () => {
|
||||
// 显示积分不足通知
|
||||
import('../utils/notifications').then(({ showInsufficientPointsNotification }) => {
|
||||
showInsufficientPointsNotification();
|
||||
|
||||
161
api/movie_queue.ts
Normal file
161
api/movie_queue.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import { ApiResponse } from './common';
|
||||
import { showQueueNotification } from '../components/QueueBox/QueueNotification2';
|
||||
import { notification } from 'antd';
|
||||
|
||||
/** 队列状态枚举 */
|
||||
export enum QueueStatus {
|
||||
WAIT = 'wait', // 排队等待中
|
||||
READY = 'ready', // 可以开始处理
|
||||
PROCESS = 'process' // 处理中
|
||||
}
|
||||
|
||||
/** 队列响应数据接口 */
|
||||
export interface QueueResponseData {
|
||||
status: QueueStatus;
|
||||
position?: number;
|
||||
queue_length?: number;
|
||||
waiting?: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/** 队列响应接口 */
|
||||
export interface QueueResponse extends ApiResponse<QueueResponseData> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: QueueResponseData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理API轮询的配置选项
|
||||
*/
|
||||
interface PollConfig {
|
||||
/** 轮询间隔(ms), 默认10000ms */
|
||||
interval?: number;
|
||||
/** 最大轮询次数, 默认120次(20分钟) */
|
||||
maxAttempts?: number;
|
||||
/** 轮询成功后的回调 */
|
||||
onSuccess?: (data: any) => void;
|
||||
/** 轮询失败后的回调 */
|
||||
onError?: (error: Error) => void;
|
||||
/** 每次轮询时的回调 */
|
||||
onPolling?: (data: QueueResponseData) => void;
|
||||
/** 取消轮询时的回调 */
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
// 用于存储取消函数的映射
|
||||
const cancelTokens = new Map<string, () => void>();
|
||||
|
||||
/**
|
||||
* 统一的队列API调用封装
|
||||
*/
|
||||
export async function withQueuePolling<T>(
|
||||
apiCall: (params: T) => Promise<QueueResponse>,
|
||||
params: T,
|
||||
config: PollConfig = {}
|
||||
): Promise<QueueResponse> {
|
||||
const {
|
||||
interval = 10000,
|
||||
maxAttempts = 120,
|
||||
onSuccess,
|
||||
onError,
|
||||
onPolling,
|
||||
onCancel
|
||||
} = config;
|
||||
|
||||
let attempts = 0;
|
||||
let lastNotificationPosition: number | undefined;
|
||||
let isCancelled = false;
|
||||
|
||||
// 生成唯一的轮询ID
|
||||
const pollId = Math.random().toString(36).substring(7);
|
||||
|
||||
// 创建取消函数
|
||||
const cancel = () => {
|
||||
isCancelled = true;
|
||||
notification.destroy(); // 关闭通知
|
||||
onCancel?.();
|
||||
cancelTokens.delete(pollId);
|
||||
};
|
||||
|
||||
// 存储取消函数
|
||||
cancelTokens.set(pollId, cancel);
|
||||
|
||||
const poll = async (): Promise<QueueResponse> => {
|
||||
try {
|
||||
if (isCancelled) {
|
||||
throw new Error('操作已取消');
|
||||
}
|
||||
|
||||
const response = await apiCall(params);
|
||||
|
||||
// 处理队列状态
|
||||
if (response.code === 202 &&
|
||||
(response.data.status === QueueStatus.WAIT || response.data.status === QueueStatus.PROCESS)) {
|
||||
attempts++;
|
||||
|
||||
// 获取队列位置和等待时间
|
||||
const { position, waiting, status } = response.data;
|
||||
|
||||
// 如果位置发生变化,更新通知
|
||||
if ((position !== lastNotificationPosition || status === QueueStatus.PROCESS) &&
|
||||
position !== undefined && waiting !== undefined) {
|
||||
showQueueNotification(position, waiting, status, cancel);
|
||||
lastNotificationPosition = position;
|
||||
}
|
||||
|
||||
// 调用轮询回调
|
||||
onPolling?.(response.data);
|
||||
|
||||
// 检查是否达到最大尝试次数
|
||||
if (attempts >= maxAttempts) {
|
||||
notification.destroy(); // 关闭通知
|
||||
throw new Error('超过最大轮询次数限制');
|
||||
}
|
||||
|
||||
// 继续轮询
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
return poll();
|
||||
}
|
||||
|
||||
// 如果状态为ready,结束轮询
|
||||
if (response.code !== 202 && response.data) {
|
||||
notification.destroy(); // 关闭通知
|
||||
onSuccess?.(response.data);
|
||||
return response;
|
||||
}
|
||||
|
||||
notification.destroy(); // 关闭通知
|
||||
return response;
|
||||
} catch (error) {
|
||||
notification.destroy(); // 关闭通知
|
||||
if (error instanceof Error) {
|
||||
onError?.(error);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
cancelTokens.delete(pollId);
|
||||
}
|
||||
};
|
||||
|
||||
return poll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消指定的轮询
|
||||
*/
|
||||
export function cancelPolling(pollId: string) {
|
||||
const cancel = cancelTokens.get(pollId);
|
||||
if (cancel) {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有轮询
|
||||
*/
|
||||
export function cancelAllPolling() {
|
||||
// 将 Map 转换为数组后再遍历
|
||||
Array.from(cancelTokens.values()).forEach(cancel => cancel());
|
||||
cancelTokens.clear();
|
||||
}
|
||||
@ -1,11 +1,7 @@
|
||||
import { ApiResponse } from "./common";
|
||||
import {
|
||||
CreateMovieProjectV2Request,
|
||||
CreateMovieProjectResponse,
|
||||
MovieStartDTO,
|
||||
StoryAnalysisTask,
|
||||
MovieStoryTaskDetail,
|
||||
CreateMovieProjectV3Request,
|
||||
GeminiTextToImageRequest,
|
||||
GeminiTextToImageResponse,
|
||||
TextToImageRequest,
|
||||
@ -44,34 +40,6 @@ export const AIGenerateImageStory = async (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: CreateMovieProjectV3Request
|
||||
) => {
|
||||
return post<ApiResponse<CreateMovieProjectResponse>>(
|
||||
"/movie/create_movie_project_v3",
|
||||
request
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取电影故事任务详情
|
||||
* @param taskId - 任务ID
|
||||
|
||||
@ -49,19 +49,19 @@ request.interceptors.request.use(
|
||||
request.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
// 检查业务状态码
|
||||
if (response.data?.code !== 0) {
|
||||
if (response.data?.code !== 0 && response.data?.code !== 202) {
|
||||
// 处理业务层面的错误
|
||||
const businessCode = response.data?.code;
|
||||
const errorMessage = response.data?.message;
|
||||
|
||||
// 特殊处理 401 和 403 业务状态码
|
||||
// 特殊处理 401 和 4001 业务状态码
|
||||
if (businessCode === 401) {
|
||||
errorHandle(401, errorMessage);
|
||||
return Promise.reject(new Error(errorMessage));
|
||||
}
|
||||
|
||||
if (businessCode === 403) {
|
||||
errorHandle(403, errorMessage);
|
||||
if (businessCode === 4001) {
|
||||
errorHandle(4001, errorMessage);
|
||||
return Promise.reject(new Error(errorMessage));
|
||||
}
|
||||
|
||||
|
||||
@ -66,59 +66,6 @@ export const getScriptEpisodeListNew = async (data: any): Promise<ApiResponse<an
|
||||
return post<ApiResponse<any>>('/movie/list_movie_projects', data);
|
||||
};
|
||||
|
||||
// 新-创建接口
|
||||
export const createScriptEpisodeNew = async (data: any): Promise<ApiResponse<any>> => {
|
||||
return post<ApiResponse<any>>('/movie/create_movie_project', data);
|
||||
};
|
||||
|
||||
// 创建剧集
|
||||
export const createScriptEpisode = async (data: CreateScriptEpisodeRequest): Promise<ApiResponse<ScriptEpisode>> => {
|
||||
// return post<ApiResponse<ScriptEpisode>>('/script_episode/create', data);
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
id: 1,
|
||||
title: 'test',
|
||||
script_id: 1,
|
||||
status: 1,
|
||||
episode_sort: 1,
|
||||
creator_name: 'king',
|
||||
created_at: '2025-07-03 10:00:00',
|
||||
updated_at: '2025-07-03 10:00:00'
|
||||
},
|
||||
successful: true
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
// 更新剧集
|
||||
export const updateScriptEpisode = async (data: UpdateScriptEpisodeRequest): Promise<ApiResponse<ScriptEpisode>> => {
|
||||
// return post<ApiResponse<ScriptEpisode>>('/script_episode/update', data);
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
id: 1,
|
||||
title: 'test',
|
||||
script_id: 1,
|
||||
status: 1,
|
||||
episode_sort: 1,
|
||||
creator_name: 'king',
|
||||
created_at: '2025-07-03 10:00:00',
|
||||
updated_at: '2025-07-03 10:00:00'
|
||||
},
|
||||
successful: true
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
};
|
||||
|
||||
// 获取剧集详情
|
||||
export const detailScriptEpisode = async (data: detailScriptEpisodeRequest): Promise<ApiResponse<ScriptEpisode>> => {
|
||||
return post<ApiResponse<ScriptEpisode>>('/script_episode/detail', data);
|
||||
|
||||
@ -242,4 +242,12 @@ body {
|
||||
|
||||
.ant-notification-notice-wrapper {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-switch.ant-switch-checked {
|
||||
background: rgb(146 78 173 / 0.8);
|
||||
}
|
||||
|
||||
.ant-switch.ant-switch-checked:hover {
|
||||
background: rgb(146 78 173) !important;
|
||||
}
|
||||
@ -42,9 +42,14 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<title>AI Movie Flow - Create Amazing Videos with AI</title>
|
||||
<title>Movie Flow - Create Amazing Movies with AI</title>
|
||||
<meta name="description" content="Professional AI-powered video creation platform with advanced editing tools" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/x-icon" sizes="16x16" href="/favicon.ico?v=1" />
|
||||
<link rel="icon" type="image/x-icon" sizes="32x32" href="/favicon.ico?v=1" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico?v=1" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.ico?v=1" />
|
||||
</head>
|
||||
<body className="font-sans antialiased">
|
||||
<ConfigProvider
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
CreateMovieProjectV2Request,
|
||||
CreateMovieProjectResponse,
|
||||
} from "@/api/DTO/movie_start_dto";
|
||||
import { createMovieProjectV2 } from "@/api/movie_start";
|
||||
import { MovieProjectService, MovieProjectMode } from "./MovieProjectService";
|
||||
|
||||
interface UseImageStoryService {
|
||||
/** 当前图片故事数据 */
|
||||
@ -58,7 +58,7 @@ interface UseImageStoryService {
|
||||
mode?: "auto" | "manual",
|
||||
resolution?: "720p" | "1080p" | "4k",
|
||||
language?: string
|
||||
) => Promise<CreateMovieProjectResponse | undefined>;
|
||||
) => Promise<{ project_id: string } | undefined>;
|
||||
/** 设置角色分析 */
|
||||
setCharactersAnalysis: Dispatch<SetStateAction<CharacterAnalysis[]>>;
|
||||
/** 设置原始用户描述 */
|
||||
@ -484,8 +484,11 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
};
|
||||
|
||||
// 调用create_movie_project_v2接口
|
||||
const result = await createMovieProjectV2(params);
|
||||
return result.data;
|
||||
const result = await MovieProjectService.createProject(
|
||||
MovieProjectMode.IMAGE,
|
||||
params
|
||||
);
|
||||
return { project_id: result.project_id };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("创建电影项目失败:", error);
|
||||
|
||||
90
app/service/Interaction/MovieProjectService.ts
Normal file
90
app/service/Interaction/MovieProjectService.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { CreateMovieProjectV2Request, CreateMovieProjectV3Request } from "@/api/DTO/movie_start_dto";
|
||||
import { createMovieProject, createMovieProjectV2, createMovieProjectV3 } from "@/api/create_movie";
|
||||
import { QueueResponse, QueueStatus, withQueuePolling, QueueResponseData } from "@/api/movie_queue";
|
||||
import { message } from "antd";
|
||||
|
||||
/**
|
||||
* 电影项目创建模式
|
||||
*/
|
||||
export enum MovieProjectMode {
|
||||
/** 普通模式 */
|
||||
NORMAL = "normal",
|
||||
/** 照片生成模式 */
|
||||
IMAGE = "image",
|
||||
/** 模板生成模式 */
|
||||
TEMPLATE = "template"
|
||||
}
|
||||
|
||||
/** 创建项目响应数据 */
|
||||
export interface CreateProjectResponse {
|
||||
project_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的电影项目创建服务
|
||||
*/
|
||||
export class MovieProjectService {
|
||||
/**
|
||||
* 创建电影项目
|
||||
*/
|
||||
static async createProject<T>(
|
||||
mode: MovieProjectMode,
|
||||
params: T
|
||||
): Promise<CreateProjectResponse> {
|
||||
try {
|
||||
let apiCall: (p: T) => Promise<QueueResponse>;
|
||||
|
||||
switch (mode) {
|
||||
case MovieProjectMode.NORMAL:
|
||||
apiCall = createMovieProject as (p: T) => Promise<QueueResponse>;
|
||||
break;
|
||||
case MovieProjectMode.IMAGE:
|
||||
apiCall = createMovieProjectV2 as (p: T) => Promise<QueueResponse>;
|
||||
break;
|
||||
case MovieProjectMode.TEMPLATE:
|
||||
apiCall = createMovieProjectV3 as (p: T) => Promise<QueueResponse>;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`不支持的创建模式: ${mode}`);
|
||||
}
|
||||
|
||||
// 使用 withQueuePolling 包装 API 调用
|
||||
const result = await withQueuePolling(apiCall, params, {
|
||||
interval: 5000, // 5秒轮询一次
|
||||
maxAttempts: 120, // 最多轮询10分钟
|
||||
onPolling: (data: QueueResponseData) => {
|
||||
console.log("轮询状态:", data);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
if (error.message === '操作已取消') {
|
||||
message.info('已取消排队');
|
||||
} else {
|
||||
message.error(error instanceof Error ? error.message : "创建项目失败");
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
message.info('已取消排队');
|
||||
}
|
||||
});
|
||||
|
||||
// 处理其他错误状态
|
||||
if (result.code !== 0 && result.code !== 202) {
|
||||
throw new Error(result.message || "创建项目失败");
|
||||
}
|
||||
|
||||
// 从响应中提取 project_id
|
||||
const projectId = (result.data as any).project_id;
|
||||
if (!projectId) {
|
||||
throw new Error("创建项目失败:未返回项目ID");
|
||||
}
|
||||
|
||||
return { project_id: projectId };
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === '操作已取消') {
|
||||
throw error;
|
||||
}
|
||||
message.error(error instanceof Error ? error.message : "创建项目失败");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,8 @@ import { useUploadFile } from "../domain/service";
|
||||
import { debounce } from "lodash";
|
||||
import { TemplateStoryUseCase } from "../usecase/templateStoryUseCase";
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { createMovieProjectV3, generateTextToImage } from "@/api/movie_start";
|
||||
import { generateTextToImage } from "@/api/movie_start";
|
||||
import { MovieProjectService, MovieProjectMode } from "./MovieProjectService";
|
||||
import { CreateMovieProjectV3Request } from "@/api/DTO/movie_start_dto";
|
||||
|
||||
/** 模板角色接口 */
|
||||
@ -312,12 +313,11 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
|
||||
fillable_content: selectedTemplate?.fillable_content || [],
|
||||
};
|
||||
console.log("params", params);
|
||||
const result = await createMovieProjectV3(params);
|
||||
if((result.data as unknown) === 'message'){
|
||||
message.error(result.message);
|
||||
return;
|
||||
}
|
||||
return result.data.project_id as string;
|
||||
const result = await MovieProjectService.createProject(
|
||||
MovieProjectMode.TEMPLATE,
|
||||
params
|
||||
);
|
||||
return result.project_id;
|
||||
} catch (error) {
|
||||
console.error("创建电影项目失败:", error);
|
||||
} finally {
|
||||
|
||||
@ -35,7 +35,7 @@ import { AudioRecorder } from "./AudioRecorder";
|
||||
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { createMovieProjectV1 } from "@/api/video_flow";
|
||||
import { createScriptEpisodeNew } from "@/api/script_episode";
|
||||
import { MovieProjectService, MovieProjectMode } from "@/app/service/Interaction/MovieProjectService";
|
||||
import { useLoadScriptText, useUploadFile } from "@/app/service/domain/service";
|
||||
import { ActionButton } from "../common/ActionButton";
|
||||
import { HighlightEditor } from "../common/HighlightEditor";
|
||||
@ -656,17 +656,19 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
||||
};
|
||||
|
||||
// 调用创建剧集API
|
||||
const episodeResponse = await createScriptEpisodeNew(episodeData);
|
||||
console.log("episodeResponse", episodeResponse);
|
||||
if (episodeResponse.code !== 0) {
|
||||
console.error(`创建剧集失败: ${episodeResponse.message}`);
|
||||
alert(`创建剧集失败: ${episodeResponse.message}`);
|
||||
return;
|
||||
try {
|
||||
const result = await MovieProjectService.createProject(
|
||||
MovieProjectMode.NORMAL,
|
||||
episodeData
|
||||
);
|
||||
const episodeId = result.project_id;
|
||||
router.push(`/create/work-flow?episodeId=${episodeId}`);
|
||||
} catch (error) {
|
||||
console.error("创建剧集失败:", error);
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
let episodeId = episodeResponse.data.project_id;
|
||||
// let episodeId = '9c34fcc4-c8d8-44fc-879e-9bd56f608c76';
|
||||
router.push(`/create/work-flow?episodeId=${episodeId}`);
|
||||
setIsCreating(false);
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -94,7 +94,12 @@ const Workstation = () => (
|
||||
* @param position - 当前队列位置
|
||||
* @param estimatedMinutes - 预计等待分钟数
|
||||
*/
|
||||
export const showQueueNotification = (position: number, estimatedMinutes: number) => {
|
||||
export const showQueueNotification = (
|
||||
position: number,
|
||||
estimatedMinutes: number,
|
||||
status: string,
|
||||
onCancel: () => void
|
||||
) => {
|
||||
notification.open({
|
||||
message: null,
|
||||
description: (
|
||||
@ -118,7 +123,7 @@ export const showQueueNotification = (position: number, estimatedMinutes: number
|
||||
borderRadius: '6px',
|
||||
}}>
|
||||
<span style={{ marginRight: '8px' }}>🎬</span>
|
||||
您的作品正在第 {position} 位等待制作
|
||||
{status === 'process' ? `您的作品正在制作,请等待完成后再创建新作品` : `您的作品正在第 ${position} 位等待制作`}
|
||||
</div>
|
||||
|
||||
{/* 预计等待时间 */}
|
||||
@ -127,12 +132,15 @@ export const showQueueNotification = (position: number, estimatedMinutes: number
|
||||
color: 'rgba(255, 255, 255, 0.65)',
|
||||
marginBottom: '12px',
|
||||
}}>
|
||||
预计等待时间:约 {estimatedMinutes} 分钟
|
||||
{status !== 'process' && `预计等待时间:约 ${estimatedMinutes} 分钟`}
|
||||
</div>
|
||||
|
||||
{/* 取消按钮 */}
|
||||
<button
|
||||
onClick={() => notification.destroy()}
|
||||
onClick={() => {
|
||||
onCancel?.();
|
||||
notification.destroy();
|
||||
}}
|
||||
style={{
|
||||
color: 'rgb(250 173 20 / 90%)',
|
||||
background: 'transparent',
|
||||
@ -148,7 +156,7 @@ export const showQueueNotification = (position: number, estimatedMinutes: number
|
||||
}}
|
||||
data-alt="cancel-queue-button"
|
||||
>
|
||||
取消制作 →
|
||||
取消排队 →
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
|
||||
@ -13,14 +13,14 @@ export function DateDivider({ timestamp }: DateDividerProps) {
|
||||
|
||||
// 判断是否是今天
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return '今天';
|
||||
return 'Today';
|
||||
}
|
||||
// 判断是否是昨天
|
||||
if (date.toDateString() === yesterday.toDateString()) {
|
||||
return '昨天';
|
||||
return 'Yesterday';
|
||||
}
|
||||
// 其他日期显示完整日期
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
|
||||
@ -12,9 +12,9 @@ export function ProgressBar({ value, total = 100, label }: ProgressBarProps) {
|
||||
return (
|
||||
<div className="my-2" data-alt="progress-bar">
|
||||
{label ? <div className="mb-1 text-xs opacity-80">{label}</div> : null}
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1 overflow-hidden">
|
||||
<motion.div
|
||||
className="h-2 bg-green-500 rounded-full"
|
||||
className="h-1 bg-[#6fd0d3] rounded-full"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${pct}%` }}
|
||||
transition={{ duration: 0.5 }}
|
||||
|
||||
@ -137,7 +137,7 @@ export default function SmartChatBox({
|
||||
unCheckedChildren="System push: Off"
|
||||
checked={systemPush}
|
||||
onChange={toggleSystemPush}
|
||||
className="ml-2"
|
||||
className="ml-2 "
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs opacity-70">
|
||||
|
||||
@ -42,9 +42,9 @@ export function TopBar({
|
||||
const currentUser: User = JSON.parse(
|
||||
localStorage.getItem("currentUser") || "{}"
|
||||
);
|
||||
const pathname = usePathname()
|
||||
const [mounted, setMounted] = React.useState(false);
|
||||
const [isLogin, setIsLogin] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const [isManagingSubscription, setIsManagingSubscription] = useState(false);
|
||||
const [subscriptionStatus, setSubscriptionStatus] = useState<string>('');
|
||||
const [credits, setCredits] = useState<number>(100);
|
||||
|
||||
@ -5,7 +5,7 @@ import { ArrowLeft, ListOrdered, Play, Loader2, Pause, MoreHorizontal, Edit2, Ch
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import './style/create-to-video2.css';
|
||||
|
||||
import { createScriptEpisodeNew, getScriptEpisodeListNew, CreateScriptEpisodeRequest } from "@/api/script_episode";
|
||||
import { getScriptEpisodeListNew } from "@/api/script_episode";
|
||||
import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation2';
|
||||
import { ChatInputBox } from '@/components/ChatInputBox/ChatInputBox';
|
||||
import cover_image1 from '@/public/assets/cover_image1.jpg';
|
||||
@ -119,20 +119,7 @@ export default function CreateToVideo2() {
|
||||
transition={{ repeat: Infinity, duration: 1.5 }}
|
||||
/>
|
||||
{/* 状态文字 */}
|
||||
<span className="text-xs tracking-widest text-yellow-300 font-medium drop-shadow-[0_0_6px_rgba(255,220,100,0.6)]">
|
||||
PROCESSING
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{/* 已完成 */}
|
||||
{status === 'completed' && (
|
||||
<>
|
||||
<motion.span
|
||||
className="w-2 h-2 rounded-full bg-green-400 shadow-[0_0_8px_rgba(0,255,120,0.9)]"
|
||||
/>
|
||||
<span className="text-xs tracking-widest text-green-300 font-medium drop-shadow-[0_0_6px_rgba(0,255,120,0.6)]">
|
||||
COMPLETED
|
||||
</span>
|
||||
<span className="text-xs tracking-widest text-yellow-300 font-medium drop-shadow-[0_0_6px_rgba(255,220,100,0.6)]"></span>
|
||||
</>
|
||||
)}
|
||||
{/* 失败 */}
|
||||
@ -183,45 +170,44 @@ export default function CreateToVideo2() {
|
||||
return (
|
||||
<div
|
||||
key={project.project_id}
|
||||
className="group relative aspect-video bg-black/20 rounded-lg overflow-hidden cursor-pointer"
|
||||
className="group flex flex-col bg-black/20 rounded-lg overflow-hidden cursor-pointer"
|
||||
onClick={() => router.push(`/create/work-flow?episodeId=${project.project_id}`)}
|
||||
onMouseEnter={() => handleMouseEnter(project.project_id)}
|
||||
onMouseLeave={() => handleMouseLeave(project.project_id)}
|
||||
data-alt="project-card"
|
||||
>
|
||||
{/* 视频/图片区域 */}
|
||||
{project.final_video_url ? (
|
||||
<video
|
||||
ref={(el) => setVideoRef(project.project_id, el)}
|
||||
src={project.final_video_url}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
preload="none"
|
||||
poster={`${project.final_video_url}?vframe/jpg/offset/1`}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center group-hover:scale-105 transition-transform duration-500"
|
||||
style={{
|
||||
backgroundImage: `url(${cover_image1.src})`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 渐变遮罩 */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent" />
|
||||
<div className="relative aspect-video">
|
||||
{project.final_video_url ? (
|
||||
<video
|
||||
ref={(el) => setVideoRef(project.project_id, el)}
|
||||
src={project.final_video_url}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
preload="none"
|
||||
poster={`${project.final_video_url}?vframe/jpg/offset/1`}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center group-hover:scale-105 transition-transform duration-500"
|
||||
style={{
|
||||
backgroundImage: `url(${cover_image1.src})`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 状态标签 - 左上角 */}
|
||||
<div className="absolute top-3 left-3">
|
||||
{StatusBadge(project.status === 'COMPLETED' ? 'completed' : project.status === 'FAILED' ? 'failed' : 'pending')}
|
||||
{/* 状态标签 - 左上角 */}
|
||||
<div className="absolute top-3 left-3">
|
||||
{StatusBadge(project.status === 'COMPLETED' ? 'completed' : project.status === 'FAILED' ? 'failed' : 'pending')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部信息 */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-medium text-white line-clamp-1">
|
||||
<h2 className="text-sm font-medium text-white line-clamp-1">
|
||||
{project.name || "Unnamed"}
|
||||
</h2>
|
||||
</div>
|
||||
@ -230,96 +216,6 @@ export default function CreateToVideo2() {
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染剧集卡片
|
||||
const renderEpisodeCard = (episode: any) => {
|
||||
return (
|
||||
<div
|
||||
key={episode.project_id}
|
||||
className="group relative bg-white/[0.08] backdrop-blur-[20px] border border-white/[0.12] rounded-[12px] overflow-hidden hover:bg-white/[0.12] transition-all duration-300 hover:shadow-[0_8px_32px_rgba(0,0,0,0.4)] cursor-pointer"
|
||||
onClick={() => router.push(`/create/work-flow?episodeId=${episode.project_id}`)}
|
||||
>
|
||||
{/* 视频缩略图 */}
|
||||
<div className="relative h-[180px] bg-gradient-to-br from-purple-500/20 to-blue-500/20 overflow-hidden">
|
||||
{episode.final_video_url ? (
|
||||
<video
|
||||
src={episode.final_video_url}
|
||||
className="w-full h-full object-cover"
|
||||
muted
|
||||
loop
|
||||
preload="none"
|
||||
poster={`${episode.final_video_url}?vframe/jpg/offset/1`}
|
||||
onMouseEnter={(e) => (e.target as HTMLVideoElement).play()}
|
||||
onMouseLeave={(e) => (e.target as HTMLVideoElement).pause()}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Video className="w-12 h-12 text-white/30" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 播放按钮覆盖 */}
|
||||
<div className="absolute inset-0 bg-black/20 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div className="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center">
|
||||
<Play className="w-6 h-6 text-white ml-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 状态标签 */}
|
||||
<div className="absolute top-3 left-3">
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
episode.status === 'COMPLETED' ? 'bg-green-500/80 text-white' :
|
||||
episode.status !== 'COMPLETED' ? 'bg-yellow-500/80 text-white' :
|
||||
'bg-gray-500/80 text-white'
|
||||
}`}>
|
||||
{episode.status === 'COMPLETED' ? 'finished' : 'processing'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 时长标签 */}
|
||||
{episode.duration && (
|
||||
<div className="absolute bottom-3 right-3">
|
||||
<span className="px-2 py-1 bg-black/60 backdrop-blur-sm rounded text-xs text-white">
|
||||
{episode.duration}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="p-4">
|
||||
<h3 className="text-white font-medium text-sm mb-2 line-clamp-2 group-hover:text-blue-300 transition-colors">
|
||||
{episode.name || episode.title || 'Unnamed episode'}
|
||||
</h3>
|
||||
|
||||
{/* 元数据 */}
|
||||
<div className="flex items-center justify-between text-xs text-white/40">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-3 h-3" />
|
||||
<span>{new Date(episode.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{new Date(episode.updated_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div className="flex gap-2">
|
||||
<button className="w-8 h-8 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white/30 transition-colors">
|
||||
<Share2 className="w-4 h-4 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col absolute top-[5rem] left-0 right-0 bottom-[1rem] px-6">
|
||||
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 656 KiB |
Loading…
x
Reference in New Issue
Block a user