播放器啊~

This commit is contained in:
海龙 2025-08-23 01:30:24 +08:00
parent 5473662fbb
commit b0e15c16c0
11 changed files with 580 additions and 402 deletions

View File

@ -200,8 +200,6 @@ export interface CreateMovieProjectV3Request {
mode: "auto" | "manual"; mode: "auto" | "manual";
/** 分辨率720p | 1080p | 4k */ /** 分辨率720p | 1080p | 4k */
resolution: "720p" | "1080p" | "4k"; resolution: "720p" | "1080p" | "4k";
/** 类型 */
genre: string;
/** 语言 */ /** 语言 */
language: string; language: string;
/**模板id */ /**模板id */
@ -210,6 +208,8 @@ export interface CreateMovieProjectV3Request {
storyRole: { storyRole: {
/**角色名 */ /**角色名 */
role_name: string; role_name: string;
/**角色描述 */
role_description: string;
/**照片URL */ /**照片URL */
photo_url: string; photo_url: string;
/**声音URL */ /**声音URL */

View File

@ -5,28 +5,26 @@ import {
MovieStartDTO, MovieStartDTO,
StoryAnalysisTask, StoryAnalysisTask,
MovieStoryTaskDetail, MovieStoryTaskDetail,
CreateMovieProjectV3Request,
} from "./DTO/movie_start_dto"; } from "./DTO/movie_start_dto";
import { get, post } from "./request"; import { get, post } from "./request";
import { import {
StoryTemplateEntity, StoryTemplateEntity,
ImageStoryEntity,
} from "@/app/service/domain/Entities"; } from "@/app/service/domain/Entities";
/** /**
* *
*/ */
export const getTemplateStoryList = async () => { export const getTemplateStoryList = async (page?: number, per_page?: number) => {
return await get<ApiResponse<StoryTemplateEntity[]>>("/template-story/list"); return await post<ApiResponse<{
}; total: number;
items: StoryTemplateEntity[];
/** page: number;
* per_page: number;
*/ }>>("/movie_template/story-template-list",{
export const actionTemplateStory = async (template: StoryTemplateEntity) => { page,
return await post<ApiResponse<{ projectId: string }>>( per_page,
"/template-story/action", });
template
);
}; };
/** /**
@ -62,7 +60,7 @@ export const createMovieProjectV2 = async (
* @returns Promise<ApiResponse<CreateMovieProjectResponse>> * @returns Promise<ApiResponse<CreateMovieProjectResponse>>
*/ */
export const createMovieProjectV3 = async ( export const createMovieProjectV3 = async (
request: CreateMovieProjectV2Request request: CreateMovieProjectV3Request
) => { ) => {
return post<ApiResponse<CreateMovieProjectResponse>>( return post<ApiResponse<CreateMovieProjectResponse>>(
"/movie/create_movie_project_v3", "/movie/create_movie_project_v3",

View File

@ -1,5 +1,5 @@
// import { redirect } from 'next/navigation'; // import { redirect } from 'next/navigation';
import { CreateToVideo2 } from '@/components/pages/create-to-video2'; import CreateToVideo2 from '@/components/pages/create-to-video2';
export default function CreatePage() { export default function CreatePage() {
// redirect('/create/video-to-video'); // redirect('/create/video-to-video');

View File

@ -175,3 +175,6 @@ body {
height: 0; height: 0;
pointer-events: none; pointer-events: none;
} }
.ant-spin-nested-loading .ant-spin {
max-height: none !important;
}

View File

@ -1,4 +1,6 @@
import { message } from "antd";
import { StoryTemplateEntity } from "../domain/Entities"; import { StoryTemplateEntity } from "../domain/Entities";
import { useUploadFile } from "../domain/service";
/** 模板角色接口 */ /** 模板角色接口 */
interface TemplateRole { interface TemplateRole {
@ -12,6 +14,8 @@ interface TemplateRole {
import { TemplateStoryUseCase } from "../usecase/templateStoryUseCase"; import { TemplateStoryUseCase } from "../usecase/templateStoryUseCase";
import { getUploadToken, uploadToQiniu } from "@/api/common"; import { getUploadToken, uploadToQiniu } from "@/api/common";
import { useState, useCallback, useMemo } from "react"; import { useState, useCallback, useMemo } from "react";
import { createMovieProjectV3 } from "@/api/movie_start";
import { CreateMovieProjectV3Request } from "@/api/DTO/movie_start_dto";
interface UseTemplateStoryService { interface UseTemplateStoryService {
/** 模板列表 */ /** 模板列表 */
@ -25,33 +29,56 @@ interface UseTemplateStoryService {
/** 加载状态 */ /** 加载状态 */
isLoading: boolean; isLoading: boolean;
/** 获取模板列表函数 */ /** 获取模板列表函数 */
/** 获取模板列表函数 */
getTemplateStoryList: () => Promise<void>; getTemplateStoryList: () => Promise<void>;
/** action 生成电影函数 */ /**
actionStory: () => Promise<string>; * action
* @param {string} user_id - ID
* @param {"auto" | "manual"} mode -
* @param {"720p" | "1080p" | "4k"} resolution -
* @param {string} language -
* @returns {Promise<string>} - ID
*/
actionStory: (
user_id: string,
mode: "auto" | "manual",
resolution: "720p" | "1080p" | "4k" ,
language: string
) => Promise<string|undefined>;
/** 设置选中的模板 */ /** 设置选中的模板 */
setSelectedTemplate: (template: StoryTemplateEntity | null) => void; setSelectedTemplate: (template: StoryTemplateEntity | null) => void;
/** 设置活跃角色索引 */ /** 设置活跃角色索引 */
setActiveRoleIndex: (index: number) => void; setActiveRoleIndex: (index: number) => void;
/** 设置当前活跃角色的图片URL */
setActiveRoleImage: (imageUrl: string) => void;
/** 设置当前活跃角色的音频URL */ /** 设置当前活跃角色的音频URL */
setActiveRoleAudio: (audioUrl: string) => void; setActiveRoleAudio: (audioUrl: string) => void;
/**清空数据 */ /**清空数据 */
clearData: () => void; clearData: () => void;
/** 上传人物头像并分析 */
AvatarAndAnalyzeFeatures: (imageUrl: string) => Promise<void>;
} }
export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
const [templateStoryList, setTemplateStoryList] = useState<StoryTemplateEntity[]>([]); const [templateStoryList, setTemplateStoryList] = useState<
const [selectedTemplate, setSelectedTemplate] = useState<StoryTemplateEntity | null>(null); StoryTemplateEntity[]
>([]);
const [selectedTemplate, setSelectedTemplate] =
useState<StoryTemplateEntity | null>(null);
const [activeRoleIndex, setActiveRoleIndex] = useState<number>(0); const [activeRoleIndex, setActiveRoleIndex] = useState<number>(0);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
// 使用上传文件Hook
const { uploadFile } = useUploadFile();
/** 模板故事用例实例 */ /** 模板故事用例实例 */
const templateStoryUseCase = useMemo(() => new TemplateStoryUseCase(), []); const templateStoryUseCase = useMemo(() => new TemplateStoryUseCase(), []);
/** 计算属性:当前活跃角色信息 */ /** 计算属性:当前活跃角色信息 */
const activeRole = useMemo(() => { const activeRole = useMemo(() => {
if (!selectedTemplate || activeRoleIndex < 0 || activeRoleIndex >= selectedTemplate.storyRole.length) { if (
!selectedTemplate ||
activeRoleIndex < 0 ||
activeRoleIndex >= selectedTemplate.storyRole.length
) {
return null; return null;
} }
return selectedTemplate.storyRole[activeRoleIndex]; return selectedTemplate.storyRole[activeRoleIndex];
@ -64,117 +91,19 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
try { try {
setIsLoading(true); setIsLoading(true);
// const templates = await templateStoryUseCase.getTemplateStoryList(); const templates = await templateStoryUseCase.getTemplateStoryList();
const templates = await new Promise<StoryTemplateEntity[]>((resolve) => {
setTimeout(() => {
resolve([
{
id: '1',
name: '奇幻冒险故事',
image_url: ['/assets/3dr_howlcastle.png', '/assets/3dr_spirited.jpg'],
generateText: '一个关于勇气与友谊的奇幻冒险故事,主角在神秘世界中寻找失落的宝藏,结识了各种奇特的生物和伙伴。',
storyRole: [
{
role_name: '艾莉娅',
role_description: '一个勇敢的女孩,拥有强大的魔法力量,她的冒险旅程充满了危险和挑战。',
photo_url: '/assets/3dr_chihiro.png',
voice_url:""
},
{
role_name: '魔法师梅林',
role_description: '一个智慧的魔法师,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
photo_url: '/assets/3dr_mono.png',
voice_url:""
},
{
role_name: '守护者龙',
role_description: '一个强大的守护者,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
photo_url: '/assets/3dr_howlbg.jpg',
voice_url:""
}
]
},
{
id: '2',
name: '科幻探索之旅',
image_url: ['/assets/3dr_monobg.jpg'],
generateText: '未来世界的太空探索故事,人类在浩瀚宇宙中寻找新的家园,面对未知的挑战和外星文明的接触。',
storyRole: [
{
role_name: '船长凯特',
role_description: '一个勇敢的船长,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
photo_url: '/assets/3dr_chihiro.png',
voice_url:""
},
{
role_name: 'AI助手诺娃',
role_description: '一个强大的AI助手拥有强大的魔法力量他的冒险旅程充满了危险和挑战。',
photo_url: '/assets/3dr_mono.png',
voice_url: ''
}
]
},
{
id: '3',
name: '温馨家庭喜剧',
image_url: ['/assets/3dr_spirited.jpg'],
generateText: '一个充满欢笑和温情的家庭故事,讲述家庭成员之间的日常趣事,以及如何一起面对生活中的小挑战。',
storyRole: [
{
role_name: '妈妈莉莉',
role_description: '一个温柔的妈妈,拥有强大的魔法力量,她的冒险旅程充满了危险和挑战。',
photo_url: '/assets/3dr_chihiro.png',
voice_url:""
},
{
role_name: '爸爸汤姆',
role_description: '一个勇敢的爸爸,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
photo_url: '/assets/3dr_mono.png',
voice_url:""
},
{
role_name: '孩子小杰',
role_description: '一个聪明的孩子,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
photo_url: '/assets/3dr_howlbg.jpg',
voice_url:""
}
]
}
]);
}, 3000);
});
setTemplateStoryList(templates); setTemplateStoryList(templates);
setSelectedTemplate(templates[0]); setSelectedTemplate(templates[0]);
setActiveRoleIndex(0);
console.log(selectedTemplate, activeRoleIndex)
} catch (err) { } catch (err) {
console.error('获取模板列表失败:', err); console.error("获取模板列表失败:", err);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [templateStoryUseCase]); }, [templateStoryUseCase]);
/**
* action
*/
const actionStory = useCallback(async (): Promise<string> => {
if (!selectedTemplate) {
throw new Error('请先选择一个故事模板');
}
try {
setIsLoading(true);
console.log('selectedTemplate', selectedTemplate)
// const projectId = await templateStoryUseCase.actionStory(selectedTemplate);
return 'projectId';
} catch (err) {
console.error('生成电影失败:', err);
throw err;
} finally {
setIsLoading(false);
}
}, [selectedTemplate, templateStoryUseCase]);
/** /**
* *
*/ */
@ -185,25 +114,56 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
/** /**
* URL * URL
*/ */
const setActiveRoleImage = useCallback((imageUrl: string): void => { const setActiveRoleData = useCallback(
if (!selectedTemplate || activeRoleIndex < 0 || activeRoleIndex >= selectedTemplate.storyRole.length) { (imageUrl: string, desc: string): void => {
if (
!selectedTemplate ||
activeRoleIndex < 0 ||
activeRoleIndex >= selectedTemplate.storyRole.length
) {
console.log(selectedTemplate, activeRoleIndex);
return; return;
} }
try {
const character_briefs = {
name: selectedTemplate.storyRole[activeRoleIndex].role_name,
image_url: imageUrl,
character_analysis: JSON.parse(desc).character_analysis,
};
console.log("character_briefs", character_briefs);
const updatedTemplate = { const updatedTemplate = {
...selectedTemplate, ...selectedTemplate,
storyRole: selectedTemplate.storyRole.map((role, index) => storyRole: selectedTemplate.storyRole.map((role, index) =>
index === activeRoleIndex ? { ...role, photo_url: imageUrl } : role index === activeRoleIndex
? {
...role,
photo_url: imageUrl,
role_description: JSON.stringify(character_briefs),
}
: role
), ),
}; };
setSelectedTemplate(updatedTemplate); setSelectedTemplate(updatedTemplate);
}, [selectedTemplate, activeRoleIndex]); } catch (error) {
message.error("Image analysis failed");
console.log("error", error);
}
},
[selectedTemplate, activeRoleIndex]
);
/** /**
* URL * URL
*/ */
const setActiveRoleAudio = useCallback((audioUrl: string): void => { const setActiveRoleAudio = useCallback(
if (!selectedTemplate || activeRoleIndex < 0 || activeRoleIndex >= selectedTemplate.storyRole.length) { (audioUrl: string): void => {
if (
!selectedTemplate ||
activeRoleIndex < 0 ||
activeRoleIndex >= selectedTemplate.storyRole.length
) {
return; return;
} }
@ -214,8 +174,60 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
), ),
}; };
setSelectedTemplate(updatedTemplate); setSelectedTemplate(updatedTemplate);
}, [selectedTemplate, activeRoleIndex]); },
[selectedTemplate, activeRoleIndex]
);
/**
*
* @param {string} characterName -
*/
const AvatarAndAnalyzeFeatures = useCallback(
async (imageUrl: string): Promise<void> => {
try {
setIsLoading(true);
// 调用用例处理人物头像上传和特征分析
const result = await templateStoryUseCase.AvatarAndAnalyzeFeatures(
imageUrl
);
setActiveRoleData(result.crop_url, result.whisk_caption);
console.log("人物头像和特征描述更新成功:", result);
} catch (error) {
console.error("人物头像上传和特征分析失败:", error);
throw error;
} finally {
setIsLoading(false);
}
},
[templateStoryUseCase]
);
const actionStory = useCallback(
async (
user_id: string,
mode: "auto" | "manual" = "auto",
resolution: "720p" | "1080p" | "4k" = "720p",
language: string = "English"
) => {
try {
const params: CreateMovieProjectV3Request = {
user_id,
mode,
resolution,
storyRole: selectedTemplate?.storyRole || [],
language,
template_id: selectedTemplate?.template_id || "",
};
const result = await createMovieProjectV3(params);
return result.data.project_id as string;
} catch (error) {
console.error("创建电影项目失败:", error);
}
},
[selectedTemplate]
);
return { return {
templateStoryList, templateStoryList,
selectedTemplate, selectedTemplate,
@ -226,12 +238,12 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
actionStory, actionStory,
setSelectedTemplate, setSelectedTemplate,
setActiveRoleIndex: handleSetActiveRoleIndex, setActiveRoleIndex: handleSetActiveRoleIndex,
setActiveRoleImage,
setActiveRoleAudio, setActiveRoleAudio,
AvatarAndAnalyzeFeatures,
clearData: () => { clearData: () => {
setTemplateStoryList([]); setTemplateStoryList([]);
setSelectedTemplate(null); setSelectedTemplate(null);
setActiveRoleIndex(0); setActiveRoleIndex(0);
} },
}; };
}; };

View File

@ -140,6 +140,8 @@ export interface StoryTemplateEntity {
image_url: string[]; image_url: string[];
/** 故事模板概览*/ /** 故事模板概览*/
generateText: string; generateText: string;
/** 故事模板ID */
template_id: string;
/**故事角色 */ /**故事角色 */
storyRole: { storyRole: {
/**角色名 */ /**角色名 */

View File

@ -1,5 +1,6 @@
import { generateCharacterBrief } from "@/api/video_flow";
import { StoryTemplateEntity } from "../domain/Entities"; import { StoryTemplateEntity } from "../domain/Entities";
import { getTemplateStoryList, actionTemplateStory } from "@/api/movie_start"; import { getTemplateStoryList } from "@/api/movie_start";
/** /**
* *
@ -21,35 +22,44 @@ export class TemplateStoryUseCase {
try { try {
const response = await getTemplateStoryList(); const response = await getTemplateStoryList();
if (response.successful && response.data) { if (response.successful && response.data) {
this.templateStoryList = response.data; this.templateStoryList = response.data.items;
return response.data; return response.data.items;
} }
throw new Error(response.message || '获取故事模板列表失败'); throw new Error(response.message || "获取故事模板列表失败");
} catch (error) { } catch (error) {
console.error('获取故事模板列表失败:', error); console.error("获取故事模板列表失败:", error);
throw error; throw error;
} }
} }
/** /**
* *
* @param {StoryTemplateEntity} template - * @returns {Promise<{crop_url: string, whisk_caption: string}>} URL和特征描述
* @returns {Promise<string>} - id
*/ */
async actionStory(template: StoryTemplateEntity): Promise<string> { async AvatarAndAnalyzeFeatures(
imageUrl: string
): Promise<{ crop_url: string; whisk_caption: string }> {
// 直接在这里处理上传和分析逻辑
try { try {
if (!template) { // 2. 调用AI分析接口获取人物特征描述
throw new Error('故事模板不能为空'); const analysisResult = await generateCharacterBrief({
image_url: imageUrl,
});
if (!analysisResult.successful || !analysisResult.data) {
throw new Error("人物特征分析失败");
} }
const response = await actionTemplateStory(template); // 3. 返回新的头像URL和特征描述用于替换旧数据
if (response.successful && response.data) { const result = {
this.selectedTemplate = template; crop_url: imageUrl,
return response.data.projectId; whisk_caption: JSON.stringify(analysisResult.data.character_brief),
} };
throw new Error(response.message || '执行故事模板操作失败');
return result;
} catch (error) { } catch (error) {
console.error('执行故事模板操作失败:', error);
throw error; throw error;
} }
} }

View File

@ -1,12 +1,24 @@
"use client"; "use client";
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect } from "react";
import { Mic, MicOff, Upload, Play, Pause, Trash2, X } from "lucide-react"; import {
Mic,
MicOff,
Upload,
Play,
Pause,
Trash2,
X,
Volume2,
} from "lucide-react";
import { Tooltip, Upload as AntdUpload, message } from "antd"; import { Tooltip, Upload as AntdUpload, message } from "antd";
import { InboxOutlined } from "@ant-design/icons"; import { InboxOutlined } from "@ant-design/icons";
import WaveSurfer from "wavesurfer.js"; import WaveSurfer from "wavesurfer.js";
import { getAudioDuration, useUploadFile } from "../../app/service/domain/service"; import {
getAudioDuration,
useUploadFile,
} from "../../app/service/domain/service";
// 自定义样式 // 自定义样式
const audioRecorderStyles = ` const audioRecorderStyles = `
@ -92,16 +104,35 @@ export function AudioRecorder({
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [volume, setVolume] = useState(1); const [volume, setVolume] = useState(1);
const [isMuted, setIsMuted] = useState(false); const [isMuted, setIsMuted] = useState(false);
const [recordingTime, setRecordingTime] = useState(0); // 录制时长(秒)
const [isUploadingRecording, setIsUploadingRecording] = useState(false); // 录制音频上传状态
const { uploadFile, isUploading } = useUploadFile(); const { uploadFile, isUploading } = useUploadFile();
const mediaRecorderRef = useRef<MediaRecorder | null>(null); const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const chunksRef = useRef<Blob[]>([]); const chunksRef = useRef<Blob[]>([]);
const recordingTimerRef = useRef<NodeJS.Timeout | null>(null); // 录制计时器引用
const actualRecordingTimeRef = useRef<number>(0); // 实际录制时长引用,用于准确获取时长
// 清理计时器
useEffect(() => {
return () => {
if (recordingTimerRef.current) {
clearInterval(recordingTimerRef.current);
}
};
}, []);
// 开始录制 // 开始录制
const startRecording = async () => { const startRecording = async () => {
try { try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mediaRecorder = new MediaRecorder(stream);
// 检查支持的音频格式,优先使用 webm
const mimeType = MediaRecorder.isTypeSupported("audio/webm")
? "audio/webm"
: "audio/mp4";
const mediaRecorder = new MediaRecorder(stream, { mimeType });
mediaRecorderRef.current = mediaRecorder; mediaRecorderRef.current = mediaRecorder;
chunksRef.current = []; chunksRef.current = [];
@ -112,23 +143,70 @@ export function AudioRecorder({
}; };
mediaRecorder.onstop = async () => { mediaRecorder.onstop = async () => {
const audioBlob = new Blob(chunksRef.current, { type: "audio/wav" }); const audioBlob = new Blob(chunksRef.current, { type: mimeType });
const audioUrl = URL.createObjectURL(audioBlob);
const duration = await getAudioDuration(audioBlob); // 先停止计时器,然后获取最终时长
if (recordingTimerRef.current) {
clearInterval(recordingTimerRef.current);
recordingTimerRef.current = null;
}
// 使用引用值获取准确的录制时长
const duration = actualRecordingTimeRef.current;
console.log("录制时长:", duration, "秒");
// 无论时长是否符合要求,都要释放麦克风权限 // 无论时长是否符合要求,都要释放麦克风权限
stream.getTracks().forEach((track) => track.stop()); stream.getTracks().forEach((track) => track.stop());
if(duration>20||duration<10){ if (duration > 20 || duration < 10) {
message.warning("Please keep your audio between 10 and 20 seconds to help us better learn your voice.") message.warning(
"Please keep your audio between 10 and 20 seconds to help us better learn your voice."
);
return; return;
} }
onAudioRecorded(audioBlob, audioUrl); try {
// 将 Blob 转换为 File 对象,以便上传
const audioFile = new File(
[audioBlob],
`recording_${Date.now()}.webm`,
{ type: mimeType }
);
// 设置上传状态
setIsUploadingRecording(true);
// 使用 uploadFile 上传到服务器
console.log("开始上传录制的音频文件...");
const uploadedUrl = await uploadFile(audioFile);
console.log("音频上传成功服务器URL:", uploadedUrl);
// 使用服务器返回的真实URL地址
onAudioRecorded(audioBlob, uploadedUrl);
} catch (error) {
console.error("音频上传失败:", error);
message.error("音频上传失败,请重试");
return;
} finally {
setIsUploadingRecording(false);
}
// 重置计时器状态
setRecordingTime(0);
actualRecordingTimeRef.current = 0; // 重置引用值
setIsRecording(false);
}; };
mediaRecorder.start(); mediaRecorder.start();
setIsRecording(true); setIsRecording(true);
setRecordingTime(0); // 重置计时器
actualRecordingTimeRef.current = 0; // 重置引用值
// 开始计时
recordingTimerRef.current = setInterval(() => {
setRecordingTime((prev) => prev + 1);
actualRecordingTimeRef.current += 1; // 同时更新引用值
}, 1000);
} catch (error) { } catch (error) {
console.error("录制失败:", error); console.error("录制失败:", error);
} }
@ -139,6 +217,7 @@ export function AudioRecorder({
if (mediaRecorderRef.current && isRecording) { if (mediaRecorderRef.current && isRecording) {
mediaRecorderRef.current.stop(); mediaRecorderRef.current.stop();
setIsRecording(false); setIsRecording(false);
// 注意:计时器清理现在在 onstop 回调中处理
} }
}; };
@ -184,7 +263,7 @@ export function AudioRecorder({
<div className="flex items-center justify-center min-h-[60px]"> <div className="flex items-center justify-center min-h-[60px]">
{mode === "upload" ? ( {mode === "upload" ? (
// 上传模式 // 上传模式
<div className="text-center w-full"> <div className="text-center w-full flex flex-col justify-center h-32 ">
<Tooltip <Tooltip
title="Please clearly read the story description above and record a 15-second audio file for upload" title="Please clearly read the story description above and record a 15-second audio file for upload"
placement="top" placement="top"
@ -193,12 +272,15 @@ export function AudioRecorder({
<div> <div>
<AntdUpload.Dragger <AntdUpload.Dragger
accept="audio/*" accept="audio/*"
beforeUpload={async(file) => { beforeUpload={async (file) => {
console.log("beforeUpload 被调用:", file); console.log("beforeUpload 被调用:", file);
// 移除 return false让文件能够正常进入 customRequest // 移除 return false让文件能够正常进入 customRequest
const duration = await getAudioDuration(file); const duration = await getAudioDuration(file);
if(duration>20||duration<10){ console.log("duration", duration);
message.warning("Please keep your audio between 10 and 20 seconds to help us better learn your voice.") 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 false;
} }
return true; return true;
@ -258,7 +340,7 @@ export function AudioRecorder({
</div> </div>
) : ( ) : (
// 录制模式 // 录制模式
<div className="text-center w-full"> <div className="text-center w-full flex flex-col justify-center h-32">
{isRecording ? ( {isRecording ? (
// 录制中状态 // 录制中状态
<div className="space-y-3"> <div className="space-y-3">
@ -273,18 +355,33 @@ export function AudioRecorder({
<div className="text-xs text-white/60">Recording...</div> <div className="text-xs text-white/60">Recording...</div>
</div> </div>
{/* 录制状态指示器 */} {/* 录制状态指示器和计时器 */}
<div className="w-full h-12 bg-white/[0.05] rounded-lg flex items-center justify-center"> <div className="w-full h-12 bg-white/[0.05] rounded-lg flex items-center justify-center">
<div className="flex items-center gap-2 text-white/60"> <div className="flex items-center gap-3 text-white/60">
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div> <div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>
<span className="text-xs">Recording...</span> <span className="text-xs">Recording...</span>
<span className="text-sm font-mono text-red-400">
{Math.floor(recordingTime / 60)
.toString()
.padStart(2, "0")}
:{(recordingTime % 60).toString().padStart(2, "0")}
</span>
</div> </div>
</div> </div>
</div> </div>
) : isUploadingRecording ? (
// 录制音频上传中状态
<div className="text-center">
<div className="text-xs text-white/60 mb-3">
Uploading recorded audio...
</div>
<div className="flex items-center justify-center">
<div className="w-8 h-8 border-2 border-white/20 border-t-white/60 rounded-full animate-spin"></div>
</div>
</div>
) : ( ) : (
// 录制准备状态 // 录制准备状态
<div className="text-center"> <div className="text-center">
<div className="text-2xl text-white/40 mb-2">🎙</div>
<div className="text-xs text-white/60 mb-3"> <div className="text-xs text-white/60 mb-3">
Click to start recording Click to start recording
</div> </div>
@ -314,8 +411,8 @@ export function AudioRecorder({
onClick={() => setMode("upload")} onClick={() => setMode("upload")}
className={`transition-colors ${ className={`transition-colors ${
mode === "upload" mode === "upload"
? "text-blue-300" ? "text-[rgb(106,244,249)]"
: "text-white/40 hover:text-white/60" : "text-white/40 hover:text-[rgb(106,244,249)]/60"
}`} }`}
> >
<Upload className="w-5 h-5" /> <Upload className="w-5 h-5" />
@ -327,8 +424,8 @@ export function AudioRecorder({
onClick={() => setMode("record")} onClick={() => setMode("record")}
className={`transition-colors ${ className={`transition-colors ${
mode === "record" mode === "record"
? "text-green-300" ? "text-[rgb(199,59,255)]"
: "text-white/40 hover:text-white/60" : "text-white/40 hover:text-[rgb(199,59,255)]/60"
}`} }`}
> >
<Mic className="w-5 h-5" /> <Mic className="w-5 h-5" />
@ -344,13 +441,99 @@ export function AudioRecorder({
return ( return (
<> <>
<style>{audioRecorderStyles}</style> <style>{audioRecorderStyles}</style>
<div className="relative bg-white/[0.05] border border-white/[0.1] rounded-lg p-4"> <div
className="relative bg-white/[0.05] border border-white/[0.1] rounded-lg p-4 h-52 group"
data-alt="audio-player-container"
>
{/* 大旋转的播放圆盘 */}
<div className=" absolute z-1 right-6 top-[50%] translate-y-[-50%] scale-[1.4]">
<div className="relative">
{/* 外层边框 - 唱片边缘 */}
<div
className={`w-28 h-28 rounded-full border-4 border-gray-400/30 ${
isPlaying ? "animate-spin" : ""
}`}
style={{
animationDuration: "6s",
}}
></div>
{/* 唱片主体 - 纯黑色唱片面 */}
<div
className={`absolute inset-2 w-24 h-24 rounded-full bg-black shadow-[inset_0_2px_8px_rgba(0,0,0,0.8)] ${
isPlaying ? "animate-spin" : ""
}`}
style={{ animationDuration: "6s" }}
>
{/* 唱片纹理 - 细密的同心圆 */}
<div className="absolute inset-0 rounded-full bg-[radial-gradient(circle_at_center,transparent_0%,rgba(255,255,255,0.02)_1px,transparent_2px)] bg-[length:4px_4px]"></div>
{/* 中心孔洞 - 透明圆环,显示背景色 */}
<div className="absolute top-1/2 left-1/2 w-8 h-8 rounded-full -translate-x-1/2 -translate-y-1/2 bg-transparent border-2 border-white/20"></div>
{/* MovieFlow 字母 - 环形文本效果 */}
<div className="absolute inset-0">
<svg
viewBox="0 0 96 96"
className="w-full h-full"
style={{
filter: "drop-shadow(0 0 8px rgba(106, 244, 249, 0.3))",
}}
>
{/* 定义渐变 */}
<defs>
<linearGradient
id="movieFlowGradient"
x1="0%"
y1="0%"
x2="100%"
y2="0%"
>
<stop offset="30%" stopColor="rgb(106, 244, 249)" />
<stop offset="70%" stopColor="rgb(199, 59, 255)" />
</linearGradient>
</defs>
{/* 圆形路径 - 减小半径让文字更靠近中心 */}
<path
id="movieFlowCircle"
d="M 48,16 a 32,32 0 1,1 0,64 a 32,32 0 1,1 0,-64"
fill="none"
stroke="none"
/>
{/* 环形文本 - 调整起始位置让文字更居中 */}
<text fontSize="12" fontWeight="bold" letterSpacing="1">
<textPath
href="#movieFlowCircle"
startOffset="25%"
textAnchor="middle"
fill="url(#movieFlowGradient)"
>
MovieFlow
</textPath>
</text>
</svg>
</div>
</div>
{/* 唱片光泽效果 */}
<div
className={`absolute inset-2 w-24 h-24 rounded-full bg-gradient-to-br from-transparent via-white/5 to-transparent ${
isPlaying ? "animate-spin" : ""
}`}
style={{ animationDuration: "10s" }}
></div>
</div>
</div>
{/* 头部 - 只显示操作按钮 */} {/* 头部 - 只显示操作按钮 */}
<div className="flex justify-end gap-2 mb-3"> <div className="relative z-10 flex justify-end gap-2 mb-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<button <button
onClick={handleDelete} onClick={handleDelete}
className="text-white/60 hover:text-red-400 transition-colors" className="text-white/60 hover:text-red-400 transition-colors"
title="Delete audio" title="Delete audio"
data-alt="delete-audio-button"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
@ -358,15 +541,26 @@ export function AudioRecorder({
<button <button
onClick={onClose} onClick={onClose}
className="text-white/60 hover:text-white/80 transition-colors" className="text-white/60 hover:text-white/80 transition-colors"
data-alt="close-audio-button"
> >
<X className="w-4 h-4" /> <X className="w-4 h-4" />
</button> </button>
)} )}
</div> </div>
{/* 播放控制 */}
<div className="relative z-10 flex items-center justify-center gap-4 mb-4 mt-12">
<button
onClick={togglePlay}
className="flex items-center justify-center w-12 h-12 bg-gradient-to-br from-[rgb(106,244,249)] to-[rgb(199,59,255)] text-white rounded-xl shadow-[inset_0_1px_0_rgba(255,255,255,0.3),0_4px_12px_rgba(0,0,0,0.3)] hover:shadow-[inset_0_1px_0_rgba(255,255,255,0.4),0_6px_20px_rgba(106,244,249,0.4)] transition-all duration-300 hover:scale-105 active:scale-95"
>
{isPlaying ? (
<Pause className="w-5 h-5 drop-shadow-sm" />
) : (
<Play className="w-5 h-5 drop-shadow-sm ml-0.5" />
)}
</button>
{/* WaveSurfer 波形图区域 */} {/* WaveSurfer 波形图区域 */}
<div className="mb-4"> <div className="h-16 flex-1 bg-white/[0.05] rounded-lg overflow-hidden">
<div className="h-16 bg-white/[0.05] rounded-lg overflow-hidden">
<WaveformPlayer <WaveformPlayer
audioUrl={audioUrl} audioUrl={audioUrl}
isPlaying={isPlaying} isPlaying={isPlaying}
@ -377,37 +571,38 @@ export function AudioRecorder({
</div> </div>
</div> </div>
{/* 播放控制 */}
<div className="flex items-center justify-center gap-4 mb-4">
<button
onClick={togglePlay}
className="flex items-center justify-center w-12 h-12 bg-blue-500 text-white rounded-full hover:bg-blue-600 transition-colors"
>
{isPlaying ? (
<Pause className="w-5 h-5" />
) : (
<Play className="w-5 h-5" />
)}
</button>
</div>
{/* 音频设置 */} {/* 音频设置 */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between px-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-6">
{/* 音量控制 */}
<div className="flex items-center gap-3">
<div className="w-24 h-3 bg-white/[0.08] rounded-full overflow-hidden shadow-inner border border-white/[0.1] relative">
<div
className="h-full bg-gradient-to-r from-[rgb(106,244,249)] to-[rgb(199,59,255)] rounded-full transition-all duration-300"
style={{ width: `${volume * 100}%` }}
/>
{/* 可拖拽的音量滑块 */}
<input <input
type="range" type="range"
min="0" min="0"
max="1" max="1"
step="0.1" step="0.01"
value={volume} value={volume}
onChange={handleVolumeChange} onChange={handleVolumeChange}
className="w-16 h-1 !bg-white/20 rounded-lg appearance-none cursor-pointer slider" className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
{/* 视觉滑块指示器 */}
<div
className="absolute top-1/2 -translate-y-1/2 w-4 h-4 bg-white rounded-full shadow-lg border-2 border-[rgb(199,59,255)] pointer-events-none transition-all duration-200"
style={{ left: `calc(${volume * 100}% - 8px)` }}
/> />
<span className="text-xs text-white/60 w-8">
{Math.round(volume * 100)}%
</span>
</div> </div>
<div className="text-xs text-white/40">1x</div> {/* 音频图标 */}
<div className="text-white/70 hover:text-white/90 transition-colors">
<Volume2 className="w-5 h-5" />
</div>
</div>
</div>
</div> </div>
</div> </div>
</> </>
@ -463,12 +658,22 @@ function WaveformPlayer({
useEffect(() => { useEffect(() => {
if (!containerRef.current || !audioUrl) return; if (!containerRef.current || !audioUrl) return;
// 创建Canvas渐变
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) return;
const gradient = ctx.createLinearGradient(0, 0, 0, 64); // 64是波形高度
gradient.addColorStop(0, "rgb(106, 244, 249)"); // 顶部:青色
gradient.addColorStop(0.5, "rgb(152, 151, 254)"); // 中间:蓝紫色
gradient.addColorStop(1, "rgb(199, 59, 255)"); // 底部:紫色
// 创建WaveSurfer实例并配置样式 // 创建WaveSurfer实例并配置样式
const ws = WaveSurfer.create({ const ws = WaveSurfer.create({
container: containerRef.current, container: containerRef.current,
waveColor: "#3b82f6", // 波形颜色 waveColor: gradient, // 使用渐变作为波形颜色
progressColor: "#1d4ed8", // 播放进度颜色 progressColor: "rgb(152, 151, 254)", // 播放进度颜色 - 半透明白色,更优雅
cursorColor: "#1e40af", // 播放光标颜色 cursorColor: "rgb(152, 151, 254)", // 播放光标颜色 - 紫
height: 64, // 波形高度 height: 64, // 波形高度
barWidth: 2, // 波形条宽度 barWidth: 2, // 波形条宽度
barGap: 1, // 波形条间距 barGap: 1, // 波形条间距

View File

@ -23,7 +23,6 @@ import {
Modal, Modal,
Tooltip, Tooltip,
Upload, Upload,
Spin,
Popconfirm, Popconfirm,
Image, Image,
} from "antd"; } from "antd";
@ -44,9 +43,21 @@ import GlobalLoad from "../common/GlobalLoad";
const RenderTemplateStoryMode = ({ const RenderTemplateStoryMode = ({
isOpen, isOpen,
onClose, onClose,
configOptions = {
mode: "auto" as "auto" | "manual",
resolution: "720p" as "720p" | "1080p" | "4k",
language: "english",
videoDuration: "1min",
},
}: { }: {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
configOptions: {
mode: "auto" | "manual";
resolution: "720p" | "1080p" | "4k";
language: string;
videoDuration: string;
};
}) => { }) => {
// 使用 hook 管理状态 // 使用 hook 管理状态
const { const {
@ -59,17 +70,16 @@ const RenderTemplateStoryMode = ({
actionStory, actionStory,
setSelectedTemplate, setSelectedTemplate,
setActiveRoleIndex, setActiveRoleIndex,
setActiveRoleImage, AvatarAndAnalyzeFeatures,
setActiveRoleAudio, setActiveRoleAudio,
clearData, clearData,
} = useTemplateStoryServiceHook(); } = useTemplateStoryServiceHook();
// 使用上传文件hook // 使用上传文件hook
const { uploadFile, isUploading } = useUploadFile(); const { uploadFile, isUploading } = useUploadFile();
// 本地加载状态,用于 UI 反馈 // 本地加载状态,用于 UI 反馈
const [localLoading, setLocalLoading] = useState(0); const [localLoading, setLocalLoading] = useState(0);
const router = useRouter();
// 组件挂载时获取模板列表 // 组件挂载时获取模板列表
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
@ -97,9 +107,18 @@ const RenderTemplateStoryMode = ({
}, 100); }, 100);
try { try {
setLocalLoading(1); setLocalLoading(1);
// 假性的增加进度条 // 获取当前用户信息
const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
const projectId = await actionStory(); if (!User.id) {
console.error("用户未登录");
return;
}
const projectId = await actionStory(String(User.id), configOptions.mode, configOptions.resolution, configOptions.language);
if (projectId) {
// 跳转到电影详情页
router.push(`/create/work-flow?episodeId=${projectId}`);
}
console.log("Story action created:", projectId); console.log("Story action created:", projectId);
onClose(); onClose();
// 重置状态 // 重置状态
@ -113,31 +132,6 @@ const RenderTemplateStoryMode = ({
clearInterval(timer); clearInterval(timer);
} }
}; };
// 处理角色图片上传 - 已移至customRequest中处理
// const handleRoleImageUpload = async (roleIndex: number, file: File) => {
// if (!file || !selectedTemplate) return;
//
// try {
// // 使用真正的文件上传功能
// const imageUrl = await uploadFile(file, (progress) => {
// console.log(`上传进度: ${progress}%`);
// });
//
// // 上传成功后,更新角色图片
// setActiveRoleImage(imageUrl);
// } catch (error) {
// console.error("图片上传失败:", error);
// // 这里可以添加错误提示
// }
// };
// 删除角色图片
const handleDeleteRoleImage = (roleIndex: number) => {
if (selectedTemplate) {
setActiveRoleImage("");
}
};
// 模板列表渲染 // 模板列表渲染
const templateListRender = () => { const templateListRender = () => {
return ( return (
@ -172,7 +166,7 @@ const RenderTemplateStoryMode = ({
{/* 模板信息头部 - 增加顶部空间 */} {/* 模板信息头部 - 增加顶部空间 */}
<div className="flex gap-3 py-4 border-b border-white/[0.1] h-[300px]"> <div className="flex gap-3 py-4 border-b border-white/[0.1] h-[300px]">
{/* 左侧图片 */} {/* 左侧图片 */}
<div className="w-1/3"> <div className="w-1/4">
<div <div
data-alt="template-preview-image" data-alt="template-preview-image"
className="relative w-full aspect-square rounded-xl overflow-hidden group cursor-pointer" className="relative w-full aspect-square rounded-xl overflow-hidden group cursor-pointer"
@ -180,7 +174,7 @@ const RenderTemplateStoryMode = ({
<img <img
src={selectedTemplate.image_url[0]} src={selectedTemplate.image_url[0]}
alt={selectedTemplate.name} alt={selectedTemplate.name}
className="w-full h-full object-cover transition-all duration-500 group-hover:scale-105 group-hover:rotate-1" className="w-full h-full object-contain transition-all duration-500 group-hover:scale-105 group-hover:rotate-1"
/> />
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" /> <div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
</div> </div>
@ -194,7 +188,13 @@ const RenderTemplateStoryMode = ({
> >
{selectedTemplate.name} {selectedTemplate.name}
</h2> </h2>
<div className="flex-1 overflow-y-auto max-h-96 pr-3"> <div
className="flex-1 overflow-y-auto max-h-96 pr-3"
style={{
scrollbarWidth: "thin",
scrollbarColor: "rgba(156,163,175,0.2) rgba(0,0,0,0)",
}}
>
<p <p
data-alt="template-description" data-alt="template-description"
className="text-gray-300 text-sm leading-relaxed" className="text-gray-300 text-sm leading-relaxed"
@ -216,121 +216,10 @@ const RenderTemplateStoryMode = ({
{/* 紧凑布局 */} {/* 紧凑布局 */}
<div className="mb-6 flex gap-4"> <div className="mb-6 flex gap-4">
{/* 左侧:当前选中角色的音频与照片更改 - 精简版本 */} {/* 左侧:音频部分 */}
<div className="flex-1 space-y-4"> <div className="flex-1 space-y-4">
{/* 图片上传部分 - 精简 */}
<div className="space-y-2 mb-8 mt-4">
<div className="flex justify-center">
<Tooltip
title="Upload a portrait photo to replace this character's appearance in the movie."
placement="top"
>
<Upload
name="avatar"
listType="picture-card"
className="avatar-uploader [&_.ant-upload-select]:!w-32 [&_.ant-upload-select]:!h-32"
showUploadList={false}
beforeUpload={(file) => {
// 验证文件类型
const isImage = file.type.startsWith("image/");
if (!isImage) {
console.error("只能上传图片文件");
return false;
}
// 验证文件大小 (5MB)
const isLt5M = file.size / 1024 / 1024 < 10;
if (!isLt5M) {
console.error("图片大小不能超过10MB");
return false;
}
return true; // 允许文件通过验证
}}
customRequest={async ({ file, onSuccess, onError }) => {
try {
const fileObj = file as File;
console.log(
"开始上传图片文件:",
fileObj.name,
fileObj.type,
fileObj.size
);
// 使用 hook 上传文件到七牛云
const uploadedUrl = await uploadFile(
fileObj,
(progress) => {
console.log(`上传进度: ${progress}%`);
}
);
console.log("图片上传成功URL:", uploadedUrl);
// 上传成功后,更新角色图片
setActiveRoleImage(uploadedUrl);
// 调用成功回调
onSuccess?.(uploadedUrl);
} catch (error) {
console.error("图片上传失败:", error);
onError?.(error as Error);
}
}}
>
{activeRole?.photo_url ? (
<div className="relative w-32 h-32 rounded-lg overflow-hidden">
<Image
src={activeRole.photo_url}
alt="Character Portrait"
className="w-full h-full object-cover"
preview={{
mask: null,
maskClassName: "hidden",
}}
fallback="/assets/empty_video.png"
/>
{/* <button
onClick={(e) => {
e.stopPropagation();
handleDeleteRoleImage(activeRoleIndex);
}}
className="absolute top-1 right-1 w-5 h-5 bg-red-500/80 hover:bg-red-500 text-white rounded-full flex items-center justify-center transition-colors"
title="Delete Photo"
>
<Trash2 className="w-2.5 h-2.5" />
</button> */}
<div className="absolute inset-0 bg-black/50 opacity-0 hover:opacity-100 transition-opacity duration-200 flex items-center justify-center">
<div className="text-white text-center">
<UploadOutlined className="w-4 h-4 mb-1" />
<div className="text-xs">Change Photo</div>
</div>
</div>
</div>
) : (
<div className="w-32 h-32 flex flex-col items-center justify-center text-white/50 bg-white/[0.05] border border-white/[0.1] rounded-lg hover:bg-white/[0.08] transition-colors">
{isUploading ? (
<>
<Loader2 className="w-6 h-6 mb-1 animate-spin" />
<span className="text-xs">...</span>
</>
) : (
<>
<UploadOutlined className="w-6 h-6 mb-1" />
<span className="text-xs">Upload Photo</span>
</>
)}
</div>
)}
</Upload>
</Tooltip>
</div>
</div>
{/* 音频部分 - 精简版本 */}
<div className="space-y-2">
{/* 音频操作区域 - 使用新的 AudioRecorder 组件 */} {/* 音频操作区域 - 使用新的 AudioRecorder 组件 */}
<div className="space-y-2"> <div className="">
<AudioRecorder <AudioRecorder
audioUrl={activeRole?.voice_url || ""} audioUrl={activeRole?.voice_url || ""}
onAudioRecorded={(audioBlob, audioUrl) => { onAudioRecorded={(audioBlob, audioUrl) => {
@ -342,15 +231,12 @@ const RenderTemplateStoryMode = ({
/> />
</div> </div>
</div> </div>
</div>
{/* 右侧:角色图片缩略图列表 - 精简 */} {/* 右侧:角色图片缩略图列表 - 精简 */}
<div className="w-24 space-y-2"> <div className="w-24 flex flex-col gap-y-[.6rem] ">
<h4 className="text-white/70 text-xs font-medium mb-2">
Characters
</h4>
{selectedTemplate.storyRole.map((role, index: number) => ( {selectedTemplate.storyRole.map((role, index: number) => (
<Tooltip key={index} title={role.role_name} placement="left"> <div key={index} className="relative group">
<Tooltip title={role.role_name} placement="left">
<button <button
data-alt={`character-thumbnail-${index}`} data-alt={`character-thumbnail-${index}`}
className={`w-full aspect-square rounded-lg overflow-hidden border-2 transition-all duration-200 hover:scale-105 ${ className={`w-full aspect-square rounded-lg overflow-hidden border-2 transition-all duration-200 hover:scale-105 ${
@ -372,6 +258,67 @@ const RenderTemplateStoryMode = ({
/> />
</button> </button>
</Tooltip> </Tooltip>
{/* 上传按钮 - 右上角 */}
<Tooltip title="更换角色头像" placement="top">
<Upload
name="avatar"
showUploadList={false}
beforeUpload={(file) => {
// 验证文件类型
const isImage = file.type.startsWith("image/");
if (!isImage) {
console.error("只能上传图片文件");
return false;
}
// 验证文件大小 (10MB)
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
console.error("图片大小不能超过10MB");
return false;
}
return true;
}}
customRequest={async ({ file, onSuccess, onError }) => {
try {
const fileObj = file as File;
console.log(
"开始上传图片文件:",
fileObj.name,
fileObj.type,
fileObj.size
);
// 使用 hook 上传文件到七牛云
const uploadedUrl = await uploadFile(
fileObj,
(progress) => {
console.log(`上传进度: ${progress}%`);
}
);
console.log("图片上传成功URL:", uploadedUrl);
// 上传成功后,更新角色图片
await AvatarAndAnalyzeFeatures(uploadedUrl);
onSuccess?.(uploadedUrl);
} catch (error) {
console.error("图片上传失败:", error);
onError?.(error as Error);
}
}}
>
<button
data-alt={`upload-button-${index}`}
className="absolute -top-1 -right-1 w-6 h-6 bg-blue-500 hover:bg-blue-600 text-white rounded-full flex items-center justify-center transition-all duration-200 opacity-0 group-hover:opacity-100 hover:scale-110 shadow-lg"
title="更换角色头像"
>
<UploadOutlined className="w-3 h-3" />
</button>
</Upload>
</Tooltip>
</div>
))} ))}
</div> </div>
</div> </div>
@ -415,6 +362,7 @@ const RenderTemplateStoryMode = ({
</div> </div>
} }
> >
<GlobalLoad show={isLoading} progress={0}>
<div className="rounded-2xl min-h-min transition-all duration-700 ease-out"> <div className="rounded-2xl min-h-min transition-all duration-700 ease-out">
{/* 弹窗头部 */} {/* 弹窗头部 */}
<div className="flex gap-4 p-4 border-b border-white/[0.1]"> <div className="flex gap-4 p-4 border-b border-white/[0.1]">
@ -422,13 +370,13 @@ const RenderTemplateStoryMode = ({
Template Story Selection Template Story Selection
</h2> </h2>
</div> </div>
<GlobalLoad show={isLoading} progress={0}>
<div className="flex gap-4 pb-4 "> <div className="flex gap-4 pb-4 ">
{templateListRender()} {templateListRender()}
<div className="flex-1">{storyEditorRender()}</div> <div className="flex-1">{storyEditorRender()}</div>
</div> </div>
</GlobalLoad>
</div> </div>
</GlobalLoad>
</Modal> </Modal>
</> </>
); );
@ -681,6 +629,7 @@ export function ChatInputBox() {
{/* 模板故事弹窗 */} {/* 模板故事弹窗 */}
<RenderTemplateStoryMode <RenderTemplateStoryMode
configOptions={configOptions}
isOpen={isTemplateModalOpen} isOpen={isTemplateModalOpen}
onClose={() => setIsTemplateModalOpen(false)} onClose={() => setIsTemplateModalOpen(false)}
/> />
@ -954,7 +903,7 @@ const PhotoStoryModal = ({
<img <img
src={activeImageUrl} src={activeImageUrl}
alt="Story inspiration" alt="Story inspiration"
className="w-full h-full object-cover rounded-lg bg-white/[0.05]" className="w-full h-full object-contain rounded-lg bg-white/[0.05]"
/> />
<Popconfirm <Popconfirm
title="Clear all content" title="Clear all content"
@ -1005,7 +954,7 @@ const PhotoStoryModal = ({
<img <img
src={avatar.url} src={avatar.url}
alt={avatar.name} alt={avatar.name}
className="w-full h-full object-cover bg-white/[0.05]" className="w-full h-full object-contain bg-white/[0.05]"
onError={(e) => { onError={(e) => {
// 如果裁剪的头像加载失败,回退到原图 // 如果裁剪的头像加载失败,回退到原图
const target = e.target as HTMLImageElement; const target = e.target as HTMLImageElement;

View File

@ -43,7 +43,6 @@ export default function GlobalLoad({
{Boolean(progress) && <TailwindLinearLoader progress={progress as number} width={progressWidth} />} {Boolean(progress) && <TailwindLinearLoader progress={progress as number} width={progressWidth} />}
</div> </div>
); );
return ( return (
<div data-alt="loading-container" className="relative"> <div data-alt="loading-container" className="relative">
<Spin spinning={true} tip="" indicator={customIndicator}> <Spin spinning={true} tip="" indicator={customIndicator}>

View File

@ -12,7 +12,7 @@ import { ChatInputBox } from '@/components/ChatInputBox/ChatInputBox';
// ideaText已迁移到ChatInputBox组件中 // ideaText已迁移到ChatInputBox组件中
export function CreateToVideo2() { export default function CreateToVideo2() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const projectId = searchParams.get('projectId') ? parseInt(searchParams.get('projectId')!) : 0; const projectId = searchParams.get('projectId') ? parseInt(searchParams.get('projectId')!) : 0;