This commit is contained in:
海龙 2025-08-19 05:13:03 +08:00
parent 87efe72eeb
commit f55724a545
3 changed files with 110 additions and 50 deletions

View File

@ -221,21 +221,21 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
return charactersAnalysis.map((character) => { return charactersAnalysis.map((character) => {
console.log('character', character) console.log('character', character)
// 如果已经有头像URL直接返回 // 如果已经有头像URL直接返回
if (character.avatarUrl) { if (character.crop_url) {
return { return {
name: character.role_name, name: character.role_name,
url: character.avatarUrl, url: character.crop_url,
}; };
} }
// 异步生成头像URL // // 异步生成头像URL
generateAvatarFromRegion(character, activeImageUrl); // generateAvatarFromRegion(character, activeImageUrl);
return { // return {
name: character.role_name, // name: character.role_name,
url: "", // 初始为空,异步生成完成后会更新 // url: "", // 初始为空,异步生成完成后会更新
}; // };
}); }).filter(Boolean) as { name: string; url: string }[];
}, [charactersAnalysis, activeImageUrl, generateAvatarFromRegion]); }, [charactersAnalysis, activeImageUrl, generateAvatarFromRegion]);
/** /**
* *

View File

@ -35,7 +35,7 @@ import StarterKit from "@tiptap/starter-kit";
import { HighlightTextExtension } from "@/components/ui/main-editor/HighlightText"; import { HighlightTextExtension } from "@/components/ui/main-editor/HighlightText";
import Placeholder from "@tiptap/extension-placeholder"; import Placeholder from "@tiptap/extension-placeholder";
import { createMovieProjectV1 } from "@/api/video_flow"; import { createMovieProjectV1 } from "@/api/video_flow";
import { useLoadScriptText } from "@/app/service/domain/service"; import { useLoadScriptText, useUploadFile } from "@/app/service/domain/service";
// 自定义音频播放器样式 // 自定义音频播放器样式
const customAudioPlayerStyles = ` const customAudioPlayerStyles = `
@ -556,7 +556,7 @@ export function ChatInputBox() {
// 调用创建剧集API // 调用创建剧集API
const episodeResponse = await createMovieProjectV1(episodeData); const episodeResponse = await createMovieProjectV1(episodeData);
console.log('episodeResponse', episodeResponse); console.log("episodeResponse", episodeResponse);
if (episodeResponse.code !== 0) { if (episodeResponse.code !== 0) {
console.error(`创建剧集失败: ${episodeResponse.message}`); console.error(`创建剧集失败: ${episodeResponse.message}`);
alert(`创建剧集失败: ${episodeResponse.message}`); alert(`创建剧集失败: ${episodeResponse.message}`);
@ -704,11 +704,11 @@ export function ChatInputBox() {
</Tooltip> </Tooltip>
{/* 图片故事弹窗 */} {/* 图片故事弹窗 */}
<PhotoStoryModal <PhotoStoryModal
isOpen={isPhotoStoryModalOpen} isOpen={isPhotoStoryModalOpen}
onClose={() => setIsPhotoStoryModalOpen(false)} onClose={() => setIsPhotoStoryModalOpen(false)}
configOptions={configOptions} configOptions={configOptions}
/> />
</div> </div>
{/* 右侧Action按钮 */} {/* 右侧Action按钮 */}
@ -762,7 +762,6 @@ const ActionButton = ({
<button <button
name="text" name="text"
className="w-full h-full opacity-90 rounded-xl bg-black flex items-center justify-center" className="w-full h-full opacity-90 rounded-xl bg-black flex items-center justify-center"
> >
{isCreating ? <Loader2 className="w-5 h-5 animate-spin" /> : icon} {isCreating ? <Loader2 className="w-5 h-5 animate-spin" /> : icon}
</button> </button>
@ -1042,9 +1041,10 @@ const PhotoStoryModal = ({
uploadAndAnalyzeImage, uploadAndAnalyzeImage,
setCharactersAnalysis, setCharactersAnalysis,
originalUserDescription, originalUserDescription,
actionMovie actionMovie,
} = useImageStoryServiceHook(); } = useImageStoryServiceHook();
const { loadingText } = useLoadScriptText(isLoading); const { loadingText } = useLoadScriptText(isLoading);
const { uploadFile } = useUploadFile();
// 重置状态 // 重置状态
const handleClose = () => { const handleClose = () => {
resetImageStory(); resetImageStory();
@ -1072,13 +1072,13 @@ const PhotoStoryModal = ({
} }
// 调用actionMovie接口 // 调用actionMovie接口
const episodeResponse = await actionMovie( const episodeResponse = await actionMovie(
String(User.id), String(User.id),
configOptions.mode as "auto" | "manual", configOptions.mode as "auto" | "manual",
configOptions.resolution as "720p" | "1080p" | "4k", configOptions.resolution as "720p" | "1080p" | "4k",
configOptions.language configOptions.language
); );
if(!episodeResponse) return if (!episodeResponse) return;
let episodeId = episodeResponse.project_id; let episodeId = episodeResponse.project_id;
// let episodeId = '9c34fcc4-c8d8-44fc-879e-9bd56f608c76'; // let episodeId = '9c34fcc4-c8d8-44fc-879e-9bd56f608c76';
router.push(`/create/work-flow?episodeId=${episodeId}`); router.push(`/create/work-flow?episodeId=${episodeId}`);
@ -1096,7 +1096,7 @@ const PhotoStoryModal = ({
footer={null} footer={null}
width="80%" width="80%"
style={{ maxWidth: "1000px", marginTop: "10vh" }} style={{ maxWidth: "1000px", marginTop: "10vh" }}
className="photo-story-modal" className="photo-story-modal bg-white/[0.08] backdrop-blur-[20px] [&_.ant-modal-content]:bg-white/[0.00]"
closeIcon={ closeIcon={
<div className="w-6 h-6 bg-white/10 rounded-full flex items-center justify-center hover:bg-white/20 transition-colors"> <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"> <span className="text-white/70 text-lg leading-none flex items-center justify-center">
@ -1114,7 +1114,7 @@ const PhotoStoryModal = ({
Movie Generation from Image Movie Generation from Image
</h2> </h2>
</div> </div>
<div className="w-full bg-white/[0.05] border border-white/[0.1] rounded-xl p-4 mt-2"> <div className="w-full bg-white/[0.04] border border-white/[0.1] rounded-xl p-4 mt-2">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
{/* 左侧:图片上传 */} {/* 左侧:图片上传 */}
<div className="flex-shrink-0"> <div className="flex-shrink-0">
@ -1164,7 +1164,7 @@ const PhotoStoryModal = ({
key={`${avatar.name}-${index}`} key={`${avatar.name}-${index}`}
className="flex flex-col items-center" className="flex flex-col items-center"
> >
<div className="relative w-14 h-14 rounded-sm overflow-hidden bg-white/[0.05] border border-white/[0.1] mb-2 group"> <div className="relative w-14 h-14 rounded-sm overflow-hidden bg-white/[0.05] border border-white/[0.1] mb-2 group cursor-pointer">
<img <img
src={avatar.url} src={avatar.url}
alt={avatar.name} alt={avatar.name}
@ -1194,7 +1194,10 @@ const PhotoStoryModal = ({
// 从故事内容中删除该角色的所有标签和引用 // 从故事内容中删除该角色的所有标签和引用
const updatedStory = storyContent const updatedStory = storyContent
.replace( .replace(
new RegExp(`<role[^>]*>${avatar.name}<\/role>`, "g"), new RegExp(
`<role[^>]*>${avatar.name}<\/role>`,
"g"
),
"" ""
) )
.replace( .replace(
@ -1211,6 +1214,61 @@ const PhotoStoryModal = ({
<Trash2 className="w-2.5 h-2.5" /> <Trash2 className="w-2.5 h-2.5" />
</button> </button>
</Tooltip> </Tooltip>
{/* 上传新图片按钮 - 悬停时显示 */}
<Tooltip
title="Click to upload new image for this character"
placement="top"
>
<button
onClick={(e) => {
e.stopPropagation();
// 创建隐藏的文件输入框
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.style.display = "none";
input.onchange = async (event) => {
const target =
event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
try {
// 使用七牛云上传
const newImageUrl = await uploadFile(
file
);
// 更新角色分析中的图片URL
setCharactersAnalysis((prev) =>
prev.map((char) =>
char.role_name === avatar.name
? { ...char, crop_url: newImageUrl }
: char
)
);
// 清理临时元素
document.body.removeChild(input);
} catch (error) {
console.error("上传图片失败:", error);
// 清理临时元素
if (document.body.contains(input)) {
document.body.removeChild(input);
}
}
}
};
// 添加到DOM并触发点击
document.body.appendChild(input);
input.click();
}}
className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center"
>
<Upload className="w-4 h-4 text-white" />
</button>
</Tooltip>
</div> </div>
<div className="relative group"> <div className="relative group">
<input <input
@ -1236,7 +1294,7 @@ const PhotoStoryModal = ({
{hasAnalyzed && potentialGenres.length > 0 && ( {hasAnalyzed && potentialGenres.length > 0 && (
<div className="flex-shrink-0 animate-in fade-in-0 slide-in-from-right-4 duration-300"> <div className="flex-shrink-0 animate-in fade-in-0 slide-in-from-right-4 duration-300">
<div className="flex gap-2"> <div className="flex gap-2">
{[ ...potentialGenres].map((genre) => ( {[...potentialGenres].map((genre) => (
<button <button
key={genre} key={genre}
onClick={() => updateStoryType(genre)} onClick={() => updateStoryType(genre)}
@ -1255,7 +1313,9 @@ const PhotoStoryModal = ({
</div> </div>
{/* 原始用户描述的展示 */} {/* 原始用户描述的展示 */}
{originalUserDescription && ( {originalUserDescription && (
<div className="mt-2 text-sm text-white/30 italic">Your Provided Text:{originalUserDescription}</div> <div className="mt-2 text-sm text-white/30 italic">
Your Provided Text:{originalUserDescription}
</div>
)} )}
<div className="flex items-start gap-4 mt-2 relative"> <div className="flex items-start gap-4 mt-2 relative">

View File

@ -46,7 +46,7 @@ export function ThumbnailGrid({
const container = thumbnailsRef.current; const container = thumbnailsRef.current;
const thumbnailWidth = container.offsetWidth / 4; // 每个缩略图宽度(包含间距) const thumbnailWidth = container.offsetWidth / 4; // 每个缩略图宽度(包含间距)
const scrollPosition = currentSketchIndex * thumbnailWidth; const scrollPosition = currentSketchIndex * thumbnailWidth;
container.scrollTo({ container.scrollTo({
left: scrollPosition, left: scrollPosition,
behavior: 'smooth' behavior: 'smooth'
@ -68,13 +68,13 @@ export function ThumbnailGrid({
// 使用 useRef 存储前一次的数据,避免触发重渲染 // 使用 useRef 存储前一次的数据,避免触发重渲染
const prevDataRef = useRef<any[]>([]); const prevDataRef = useRef<any[]>([]);
useEffect(() => { useEffect(() => {
const currentData = getCurrentData(); const currentData = getCurrentData();
if (currentData && currentData.length > 0) { if (currentData && currentData.length > 0) {
const currentDataStr = JSON.stringify(currentData); const currentDataStr = JSON.stringify(currentData);
const prevDataStr = JSON.stringify(prevDataRef.current); const prevDataStr = JSON.stringify(prevDataRef.current);
// 只有当数据真正发生变化时才进行处理 // 只有当数据真正发生变化时才进行处理
if (currentDataStr !== prevDataStr) { if (currentDataStr !== prevDataStr) {
// 找到最新更新的数据项的索引 // 找到最新更新的数据项的索引
@ -84,14 +84,14 @@ export function ThumbnailGrid({
// 检查数据是否发生变化(包括状态变化) // 检查数据是否发生变化(包括状态变化)
return JSON.stringify(item) !== JSON.stringify(prevDataRef.current[index]); return JSON.stringify(item) !== JSON.stringify(prevDataRef.current[index]);
}); });
console.log('changedIndex_thumbnail-grid', changedIndex, 'currentData:', currentData, 'prevData:', prevDataRef.current); console.log('changedIndex_thumbnail-grid', changedIndex, 'currentData:', currentData, 'prevData:', prevDataRef.current);
// 如果找到变化的项,自动选择该项 // 如果找到变化的项,自动选择该项
if (changedIndex !== -1) { if (changedIndex !== -1) {
onSketchSelect(changedIndex); onSketchSelect(changedIndex);
} }
// 更新前一次的数据快照 // 更新前一次的数据快照
prevDataRef.current = JSON.parse(JSON.stringify(currentData)); prevDataRef.current = JSON.parse(JSON.stringify(currentData));
} }
@ -103,10 +103,10 @@ export function ThumbnailGrid({
const currentData = getCurrentData(); const currentData = getCurrentData();
const maxIndex = currentData.length - 1; const maxIndex = currentData.length - 1;
console.log('handleKeyDown', maxIndex, 'isFocused:', isFocused); console.log('handleKeyDown', maxIndex, 'isFocused:', isFocused);
if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && maxIndex >= 0) { if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && maxIndex >= 0) {
e.preventDefault(); e.preventDefault();
let newIndex = currentSketchIndex; let newIndex = currentSketchIndex;
if (e.key === 'ArrowLeft') { if (e.key === 'ArrowLeft') {
// 向左循环 // 向左循环
@ -115,7 +115,7 @@ export function ThumbnailGrid({
// 向右循环 // 向右循环
newIndex = currentSketchIndex === maxIndex ? 0 : currentSketchIndex + 1; newIndex = currentSketchIndex === maxIndex ? 0 : currentSketchIndex + 1;
} }
console.log('切换索引:', currentSketchIndex, '->', newIndex, '最大索引:', maxIndex); console.log('切换索引:', currentSketchIndex, '->', newIndex, '最大索引:', maxIndex);
onSketchSelect(newIndex); onSketchSelect(newIndex);
} }
@ -188,16 +188,16 @@ export function ThumbnailGrid({
const currentSketch = taskSketch[currentSketchIndex]; const currentSketch = taskSketch[currentSketchIndex];
const defaultBgColors = ['RGB(45, 50, 70)', 'RGB(75, 80, 100)', 'RGB(105, 110, 130)']; const defaultBgColors = ['RGB(45, 50, 70)', 'RGB(75, 80, 100)', 'RGB(105, 110, 130)'];
const bgColors = currentSketch?.bg_rgb || defaultBgColors; const bgColors = currentSketch?.bg_rgb || defaultBgColors;
return ( return (
<motion.div <motion.div
className="relative aspect-video rounded-lg overflow-hidden" className="relative aspect-video rounded-lg overflow-hidden"
initial={{ opacity: 0, scale: 0.8 }} initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
> >
{/* 动态渐变背景 */} {/* 动态渐变背景 */}
<motion.div <motion.div
className={`absolute inset-0 bg-gradient-to-r from-[${bgColors[0]}] via-[${bgColors[1]}] to-[${bgColors[2]}]`} className={`absolute inset-0 bg-gradient-to-r from-[${bgColors[0]}] via-[${bgColors[1]}] to-[${bgColors[2]}]`}
animate={{ animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"], backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
@ -212,7 +212,7 @@ export function ThumbnailGrid({
}} }}
/> />
{/* 动态光效 */} {/* 动态光效 */}
<motion.div <motion.div
className="absolute inset-0 opacity-50" className="absolute inset-0 opacity-50"
style={{ style={{
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)", background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
@ -230,11 +230,11 @@ export function ThumbnailGrid({
<div className="relative"> <div className="relative">
<motion.div <motion.div
className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl" className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl"
animate={{ animate={{
scale: [1, 1.2, 1], scale: [1, 1.2, 1],
rotate: [0, 180, 360], rotate: [0, 180, 360],
}} }}
transition={{ transition={{
duration: 4, duration: 4,
repeat: Infinity, repeat: Infinity,
ease: "linear" ease: "linear"
@ -252,7 +252,7 @@ export function ThumbnailGrid({
// 渲染视频阶段的缩略图 // 渲染视频阶段的缩略图
const renderVideoThumbnails = () => ( const renderVideoThumbnails = () => (
taskObject.videos.data.map((video, index) => { taskObject.videos.data.map((video, index) => {
return ( return (
<div <div
key={`video-${index}`} key={`video-${index}`}
@ -279,7 +279,7 @@ export function ThumbnailGrid({
)} )}
{taskObject.videos.data[index].urls ? ( {taskObject.videos.data[index].urls ? (
<video <video
className="w-full h-full object-cover" className="w-full h-full object-cover"
src={taskObject.videos.data[index].urls[0]} src={taskObject.videos.data[index].urls[0]}
playsInline playsInline
@ -288,17 +288,17 @@ export function ThumbnailGrid({
/> />
) : ( ) : (
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500"> <div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
<img <img
className={`w-full h-full object-cover transition-all duration-300 select-none ${ className={`w-full h-full object-cover transition-all duration-300 select-none ${
(!taskObject.shot_sketch.data[index]) ? 'filter blur-sm opacity-60' : '' (!taskObject.shot_sketch.data[index]) ? 'filter blur-sm opacity-60' : ''
}`} }`}
src={taskObject.shot_sketch.data[index] ? taskObject.shot_sketch.data[index].url : video.urls[0]} src={taskObject.shot_sketch.data[index] ? taskObject.shot_sketch.data[index].url : video.urls?.[0] || ''}
alt={`Thumbnail ${index + 1}`} alt={`Thumbnail ${index + 1}`}
draggable="false" draggable="false"
/> />
</div> </div>
)} )}
</div> </div>
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent z-10"> <div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent z-10">
@ -313,7 +313,7 @@ export function ThumbnailGrid({
const renderSketchThumbnails = (sketchData: any[]) => ( const renderSketchThumbnails = (sketchData: any[]) => (
<> <>
{sketchData.map((sketch, index) => { {sketchData.map((sketch, index) => {
return ( return (
<div <div
key={`sketch-${index}`} key={`sketch-${index}`}
@ -340,7 +340,7 @@ export function ThumbnailGrid({
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */} {/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
{(sketch.status === 1) && ( {(sketch.status === 1) && (
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500"> <div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
<img <img
className="w-full h-full object-cover select-none" className="w-full h-full object-cover select-none"
src={sketch.url} src={sketch.url}
alt={`NG ${index + 1}`} alt={`NG ${index + 1}`}
@ -359,7 +359,7 @@ export function ThumbnailGrid({
); );
return ( return (
<div <div
ref={thumbnailsRef} ref={thumbnailsRef}
tabIndex={0} tabIndex={0}
className="w-full h-full grid grid-flow-col auto-cols-[20%] gap-4 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing focus:outline-none select-none" className="w-full h-full grid grid-flow-col auto-cols-[20%] gap-4 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing focus:outline-none select-none"
@ -377,4 +377,4 @@ export function ThumbnailGrid({
{taskObject.currentStage === 'character' && renderSketchThumbnails(taskObject.scenes.data)} {taskObject.currentStage === 'character' && renderSketchThumbnails(taskObject.scenes.data)}
</div> </div>
); );
} }