forked from 77media/video-flow
根据接口 再次调整相关交互逻辑
This commit is contained in:
parent
6c32afbe73
commit
d7fba252d0
@ -23,6 +23,8 @@ interface UseImageStoryService {
|
||||
hasAnalyzed: boolean;
|
||||
/** 计算后的角色头像数据 */
|
||||
avatarComputed: Array<{ name: string; url: string }>;
|
||||
/** 原始用户描述 */
|
||||
originalUserDescription: string;
|
||||
/** 上传图片并分析 */
|
||||
uploadAndAnalyzeImage: () => Promise<void>;
|
||||
/** 触发文件选择 */
|
||||
@ -40,13 +42,14 @@ interface UseImageStoryService {
|
||||
/** 重置图片故事数据 */
|
||||
resetImageStory: (showAnalysisState?: boolean) => void;
|
||||
setCharactersAnalysis: Dispatch<SetStateAction<CharacterAnalysis[]>>
|
||||
setOriginalUserDescription: Dispatch<SetStateAction<string>>
|
||||
}
|
||||
|
||||
export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
// 基础状态
|
||||
const [imageStory, setImageStory] = useState<Partial<ImageStoryEntity>>({
|
||||
imageUrl: "",
|
||||
storyType: "auto",
|
||||
storyType: "",
|
||||
});
|
||||
|
||||
// 图片相关状态
|
||||
@ -54,6 +57,8 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
|
||||
// 故事内容状态(统一管理用户输入和AI分析结果)
|
||||
const [storyContent, setStoryContent] = useState<string>("");
|
||||
// 原始用户描述
|
||||
const [originalUserDescription, setOriginalUserDescription] = useState<string>("");
|
||||
|
||||
// 分析结果状态
|
||||
/** 角色头像及名称 */
|
||||
@ -64,7 +69,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
const [potentialGenres, setPotentialGenres] = useState<string[]>([]);
|
||||
|
||||
// 分类状态
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>("Auto");
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>("");
|
||||
|
||||
// 流程状态
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@ -216,18 +221,17 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
const uploadAndAnalyzeImage = useCallback(
|
||||
async (): Promise<void> => {
|
||||
try {
|
||||
console.log('123123123', 123123123)
|
||||
setIsLoading(true);
|
||||
|
||||
// 调用用例处理图片上传和分析
|
||||
await imageStoryUseCase.handleImageUpload(activeImageUrl);
|
||||
|
||||
const newImageStory = await imageStoryUseCase.handleImageUpload(activeImageUrl);
|
||||
setOriginalUserDescription(storyContent)
|
||||
// 获取更新后的数据
|
||||
const updatedStory = imageStoryUseCase.storyLogline;
|
||||
const updatedCharacters = imageStoryUseCase.charactersAnalysis;
|
||||
const updatedGenres = imageStoryUseCase.potentialGenres;
|
||||
const updatedImageStory = imageStoryUseCase.imageStory;
|
||||
|
||||
setSelectedCategory(imageStoryUseCase.potentialGenres[0]);
|
||||
// 更新所有响应式状态
|
||||
setCharactersAnalysis(updatedCharacters);
|
||||
setPotentialGenres(updatedGenres);
|
||||
@ -235,18 +239,20 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
|
||||
// 将AI分析的故事内容直接更新到统一的故事内容字段
|
||||
updateStoryContent(updatedStory || "");
|
||||
setSelectedCategory("Auto");
|
||||
|
||||
|
||||
// 标记已分析
|
||||
setHasAnalyzed(true);
|
||||
} catch (error) {
|
||||
console.error("图片上传分析失败:", error);
|
||||
setHasAnalyzed(false);
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[activeImageUrl, imageStoryUseCase]
|
||||
[activeImageUrl, imageStoryUseCase,storyContent,setOriginalUserDescription]
|
||||
);
|
||||
|
||||
/**
|
||||
@ -314,8 +320,9 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
(oldName: string, newName: string) => {
|
||||
// 更新故事内容中的角色标签
|
||||
setStoryContent((prev) => {
|
||||
const regex = new RegExp(`<role_name>${oldName}<\/role_name>`, "g");
|
||||
const content = prev.replace(regex, `<role_name>${newName}</role_name>`);
|
||||
// 匹配新的角色标签格式 <role id="C1">Dezhong Huang</role>
|
||||
const regex = new RegExp(`<role[^>]*>${oldName}<\/role>`, "g");
|
||||
const content = prev.replace(regex, `<role >${newName}</role>`);
|
||||
imageStoryUseCase.updateStoryContent(content);
|
||||
return content;
|
||||
});
|
||||
@ -363,14 +370,15 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
// 重置所有状态
|
||||
setImageStory({
|
||||
imageUrl: "",
|
||||
storyType: "auto",
|
||||
storyType: "",
|
||||
});
|
||||
setActiveImageUrl("");
|
||||
updateStoryContent("");
|
||||
setPotentialGenres([]);
|
||||
setSelectedCategory("auto");
|
||||
setSelectedCategory("");
|
||||
setHasAnalyzed(false);
|
||||
setIsLoading(false);
|
||||
setOriginalUserDescription("");
|
||||
}, [imageStoryUseCase]);
|
||||
|
||||
/**
|
||||
@ -437,6 +445,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
isLoading,
|
||||
hasAnalyzed,
|
||||
avatarComputed,
|
||||
originalUserDescription,
|
||||
setCharactersAnalysis,
|
||||
uploadAndAnalyzeImage,
|
||||
triggerFileSelection,
|
||||
@ -446,5 +455,6 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
updateCharacterName,
|
||||
syncRoleNameToContent,
|
||||
resetImageStory,
|
||||
setOriginalUserDescription
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
|
||||
import { getUploadToken, uploadToQiniu } from "@/api/common";
|
||||
import { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { ScriptEditKey } from "../usecase/ScriptEditUseCase";
|
||||
/**
|
||||
* 渲染数据转换器
|
||||
@ -13,7 +12,7 @@ export function parseScriptBlock(
|
||||
key: ScriptEditKey,
|
||||
headerName: string,
|
||||
scriptText: string,
|
||||
contentType?: 'paragraph' | 'bold' | 'italic' | 'heading' | 'tag',
|
||||
contentType?: "paragraph" | "bold" | "italic" | "heading" | "tag"
|
||||
) {
|
||||
return {
|
||||
id: key,
|
||||
@ -27,8 +26,6 @@ export function parseScriptBlock(
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 用于上传文件到七牛云的自定义 Hook
|
||||
* @returns {object} - 包含上传函数和加载状态
|
||||
@ -45,14 +42,17 @@ export function useUploadFile() {
|
||||
* @throws {Error} - 上传失败时抛出异常
|
||||
*/
|
||||
const uploadFile = useCallback(
|
||||
async (file: File, onProgress?: (progress: number) => void): Promise<string> => {
|
||||
async (
|
||||
file: File,
|
||||
onProgress?: (progress: number) => void
|
||||
): Promise<string> => {
|
||||
try {
|
||||
setIsUploading(true);
|
||||
const { token } = await getUploadToken();
|
||||
const fileUrl = await uploadToQiniu(file, token, onProgress);
|
||||
return fileUrl;
|
||||
} catch (err) {
|
||||
console.error('文件上传失败:', err);
|
||||
console.error("文件上传失败:", err);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
@ -63,3 +63,65 @@ export function useUploadFile() {
|
||||
|
||||
return { uploadFile, isUploading };
|
||||
}
|
||||
|
||||
/**加载文案定时变 */
|
||||
export function useLoadScriptText(loading: boolean): { loadingText: string } {
|
||||
// 如果loading 为true 则每五秒切换一次文本,如果变false 则停止切换,且重置文本位置
|
||||
const tests = [
|
||||
"loading...",
|
||||
"Brainstorming initial story concepts and themes.",
|
||||
"Drafting the screenplay's first outline.",
|
||||
"Refining character arcs and plot points.",
|
||||
"Finalizing the script with dialogue polish.",
|
||||
"Creating detailed storyboards for key scenes.",
|
||||
"Scouting potential filming locations.",
|
||||
"Designing mood boards for visual aesthetics.",
|
||||
"Casting actors to bring characters to life.",
|
||||
"Scheduling production timelines and shoots.",
|
||||
"Securing permits for on-location filming.",
|
||||
"Building sets to match the story’s vision.",
|
||||
"Designing costumes for character authenticity.",
|
||||
"Planning lighting setups for each scene.",
|
||||
"Renting equipment for high-quality production.",
|
||||
"Rehearsing actors for seamless performances.",
|
||||
"Setting up cameras for the first shot.",
|
||||
"Filming establishing shots for scene context.",
|
||||
"Capturing key dialogue scenes with precision.",
|
||||
"Recording action sequences with dynamic angles.",
|
||||
"Filming close-ups to capture emotions.",
|
||||
"Wrapping principal photography on set.",
|
||||
"Reviewing dailies for quality assurance.",
|
||||
"Organizing raw footage for editing.",
|
||||
"Editing scenes for narrative flow.",
|
||||
"Adding sound effects to enhance immersion.",
|
||||
"Composing the film’s musical score.",
|
||||
"Mixing audio for balanced sound design.",
|
||||
"Applying color grading for visual consistency.",
|
||||
"Rendering visual effects for final polish.",
|
||||
"Exporting the final cut for distribution.",
|
||||
];
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null);
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentIndex((prev) => (prev + 1) % tests.length);
|
||||
}, 5000);
|
||||
setIntervalId(interval);
|
||||
} else {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
setIntervalId(null);
|
||||
setCurrentIndex(0);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
setIntervalId(null);
|
||||
setCurrentIndex(0);
|
||||
}
|
||||
};
|
||||
}, [loading, tests.length]);
|
||||
return { loadingText: tests[currentIndex] };
|
||||
}
|
||||
|
||||
@ -8,10 +8,13 @@ import { MovieStartDTO, CharacterAnalysis } from "@/api/DTO/movie_start_dto";
|
||||
*/
|
||||
export class ImageStoryUseCase {
|
||||
/** 当前图片故事数据 */
|
||||
imageStory: Partial<ImageStoryEntity> = {
|
||||
imageStory: ImageStoryEntity = {
|
||||
id: "",
|
||||
imageAnalysis: "",
|
||||
roleImage: [],
|
||||
imageUrl: "",
|
||||
imageStory: "",
|
||||
storyType: "Auto",
|
||||
storyType: "",
|
||||
};
|
||||
|
||||
/** 故事梗概 */
|
||||
@ -28,7 +31,6 @@ export class ImageStoryUseCase {
|
||||
|
||||
/** 是否正在上传 */
|
||||
isUploading: boolean = false;
|
||||
|
||||
constructor() {}
|
||||
|
||||
|
||||
@ -46,9 +48,12 @@ export class ImageStoryUseCase {
|
||||
*/
|
||||
resetImageStory(): void {
|
||||
this.imageStory = {
|
||||
id: "",
|
||||
imageAnalysis: "",
|
||||
roleImage: [],
|
||||
imageUrl: "",
|
||||
imageStory: "",
|
||||
storyType: "Auto",
|
||||
storyType: "",
|
||||
};
|
||||
this.storyLogline = "";
|
||||
this.charactersAnalysis = [];
|
||||
@ -62,7 +67,7 @@ export class ImageStoryUseCase {
|
||||
* @param {string} imageUrl - 已上传的图片URL
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handleImageUpload(imageUrl: string): Promise<void> {
|
||||
async handleImageUpload(imageUrl: string) {
|
||||
try {
|
||||
this.isUploading = false; // 图片已上传,设置上传状态为false
|
||||
this.isAnalyzing = true;
|
||||
@ -71,7 +76,7 @@ export class ImageStoryUseCase {
|
||||
this.setImageStory({ imageUrl });
|
||||
|
||||
// 调用AI分析接口
|
||||
await this.analyzeImageWithAI();
|
||||
return await this.analyzeImageWithAI();
|
||||
|
||||
} catch (error) {
|
||||
console.error("图片分析失败:", error);
|
||||
@ -87,7 +92,7 @@ export class ImageStoryUseCase {
|
||||
* 使用AI分析图片
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async analyzeImageWithAI(): Promise<void> {
|
||||
async analyzeImageWithAI() {
|
||||
console.log('this.imageStory.imageUrl', this.imageStory.imageUrl)
|
||||
try {
|
||||
// 调用AI分析接口
|
||||
@ -102,6 +107,7 @@ export class ImageStoryUseCase {
|
||||
|
||||
// 组合成ImageStoryEntity
|
||||
this.composeImageStoryEntity(response.data);
|
||||
return this.imageStory;
|
||||
} else {
|
||||
throw new Error("AI分析失败");
|
||||
}
|
||||
@ -140,13 +146,15 @@ export class ImageStoryUseCase {
|
||||
y: character.region?.y || 0,
|
||||
width: character.region?.width || 0,
|
||||
height: character.region?.height || 0,
|
||||
}
|
||||
},
|
||||
|
||||
})) || [];
|
||||
|
||||
// 更新ImageStoryEntity
|
||||
this.setImageStory({
|
||||
...this.imageStory,
|
||||
imageAnalysis: data.story_logline || "",
|
||||
storyType: "Auto", // 使用第一个分类作为故事类型
|
||||
storyType: data.potential_genres[0] || "", // 使用第一个分类作为故事类型
|
||||
roleImage,
|
||||
});
|
||||
}
|
||||
|
||||
@ -35,6 +35,7 @@ import StarterKit from "@tiptap/starter-kit";
|
||||
import { HighlightTextExtension } from "@/components/ui/main-editor/HighlightText";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import { createMovieProjectV1 } from "@/api/video_flow";
|
||||
import { useLoadScriptText } from "@/app/service/domain/service";
|
||||
|
||||
// 自定义音频播放器样式
|
||||
const customAudioPlayerStyles = `
|
||||
@ -914,10 +915,11 @@ const RoleHighlightEditor = ({
|
||||
to < doc.content.size
|
||||
? doc.textBetween(to, Math.min(doc.content.size, to + 50))
|
||||
: "";
|
||||
// TODO role id 的结构
|
||||
const beforeMatch = textBefore.match(/<role_name>[^<]*$/);
|
||||
const afterMatch = textAfter.match(/^[^>]*<\/role_name>/);
|
||||
// 匹配新的角色标签格式 <role id="C1">Dezhong Huang</role>
|
||||
const beforeMatch = textBefore.match(/<role[^>]*>[^<]*$/);
|
||||
const afterMatch = textAfter.match(/^[^>]*<\/role>/);
|
||||
|
||||
// 如果光标在角色标签内,阻止输入(只允许删除操作)
|
||||
if (beforeMatch || afterMatch) {
|
||||
if (event.key !== "Backspace" && event.key !== "Delete") {
|
||||
event.preventDefault();
|
||||
@ -938,9 +940,9 @@ const RoleHighlightEditor = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// 将带标签的内容转换为高亮显示
|
||||
// 将带标签的内容转换为高亮显示(支持新的角色标签格式)
|
||||
const htmlContent = content.replace(
|
||||
/<role_name>([^<]+)<\/role_name>/g,
|
||||
/<role[^>]*>([^<]+)<\/role>/g,
|
||||
'<highlight-text type="role" text="$1" color="blue">$1</highlight-text>'
|
||||
);
|
||||
editor.commands.setContent(htmlContent, { emitUpdate: false });
|
||||
@ -1028,8 +1030,9 @@ const PhotoStoryModal = ({
|
||||
avatarComputed,
|
||||
uploadAndAnalyzeImage,
|
||||
setCharactersAnalysis,
|
||||
originalUserDescription
|
||||
} = useImageStoryServiceHook();
|
||||
|
||||
const { loadingText } = useLoadScriptText(isLoading);
|
||||
// 重置状态
|
||||
const handleClose = () => {
|
||||
resetImageStory();
|
||||
@ -1068,7 +1071,7 @@ const PhotoStoryModal = ({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Spin spinning={isLoading} tip="Processing...">
|
||||
<Spin spinning={isLoading} tip={loadingText}>
|
||||
<div className="rounded-2xl">
|
||||
{/* 弹窗头部 */}
|
||||
<div className="flex items-center gap-3 p-2 border-b border-white/[0.1]">
|
||||
@ -1083,7 +1086,7 @@ const PhotoStoryModal = ({
|
||||
<div className="flex-shrink-0">
|
||||
<div
|
||||
data-alt="image-upload-area"
|
||||
className={`w-20 h-20 rounded-lg flex flex-col items-center justify-center transition-all duration-300 cursor-pointer ${
|
||||
className={`w-24 h-24 rounded-lg flex flex-col items-center justify-center transition-all duration-300 cursor-pointer ${
|
||||
activeImageUrl
|
||||
? "border-2 border-white/20 bg-white/[0.05]"
|
||||
: "border-2 border-dashed border-white/20 bg-white/[0.02] hover:border-white/40 hover:bg-white/[0.05] hover:scale-105"
|
||||
@ -1154,8 +1157,12 @@ const PhotoStoryModal = ({
|
||||
);
|
||||
return updatedCharacters;
|
||||
});
|
||||
// 从故事内容中删除该角色名称
|
||||
// 从故事内容中删除该角色的所有标签和引用
|
||||
const updatedStory = storyContent
|
||||
.replace(
|
||||
new RegExp(`<role[^>]*>${avatar.name}<\/role>`, "g"),
|
||||
""
|
||||
)
|
||||
.replace(
|
||||
new RegExp(`\\b${avatar.name}\\b`, "g"),
|
||||
""
|
||||
@ -1164,7 +1171,6 @@ const PhotoStoryModal = ({
|
||||
.trim();
|
||||
// 更新状态
|
||||
updateStoryContent(updatedStory);
|
||||
// 注意:这里需要直接更新 charactersAnalysis,但 hook 中没有提供 setter
|
||||
}}
|
||||
className="absolute top-1 right-1 w-4 h-4 bg-black/[0.05] border border-black/[0.1] text-white rounded-full flex items-center justify-center transition-colors opacity-0 group-hover:opacity-100 z-10"
|
||||
>
|
||||
@ -1196,7 +1202,7 @@ const PhotoStoryModal = ({
|
||||
{hasAnalyzed && potentialGenres.length > 0 && (
|
||||
<div className="flex-shrink-0 animate-in fade-in-0 slide-in-from-right-4 duration-300">
|
||||
<div className="flex gap-2">
|
||||
{["Auto", ...potentialGenres].map((genre) => (
|
||||
{[ ...potentialGenres].map((genre) => (
|
||||
<button
|
||||
key={genre}
|
||||
onClick={() => updateStoryType(genre)}
|
||||
@ -1213,6 +1219,11 @@ const PhotoStoryModal = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* 原始用户描述的展示 */}
|
||||
{originalUserDescription && (
|
||||
<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">
|
||||
{/* 文本输入框 */}
|
||||
<RoleHighlightEditor
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user