import { CameraOutlined, ColumnWidthOutlined, CustomerServiceOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, FileImageOutlined, FolderAddOutlined, FontSizeOutlined, LinkOutlined, PictureOutlined, ScissorOutlined, SwapOutlined, ThunderboltOutlined, VideoCameraOutlined, } from "@ant-design/icons"; import { useCallback, useEffect, useRef, useState, type DragEvent } from "react"; import "../../styles/pages/more-tools.css"; import "../../styles/pages/image-workbench.css"; import { aiGenerationClient } from "../../api/aiGenerationClient"; import { waitForTask } from "../../api/taskSubscription"; import { saveToolTaskState, loadToolTaskState, clearToolTaskState } from "../workbench/toolKeepalive"; import { translateTaskError } from "../../utils/translateTaskError"; import { getServerBaseUrl, isServerRequestError } from "../../api/serverConnection"; import { summarizeUrl, formatFileSize, fileToDataUrl, wait } from "../../utils/toolPageUtils"; import TaskStatusBar from "../../components/TaskStatusBar"; import BeforeAfterCompare from "../../components/BeforeAfterCompare"; import { addToolResultToAssetLibrary, saveToolResultToLocal } from "../workbench/toolResultActions"; import type { WebImageWorkbenchTool, WebViewKey } from "../../types"; type UpscaleMode = "image" | "video"; type ImageScale = "2x" | "4x"; type VideoMinLen = 540 | 720; const videoStyles = [ { value: 0, label: "日式漫画", color: "#f9a8d4" }, { value: 1, label: "美式漫画", color: "#fbbf24" }, { value: 2, label: "清新漫画", color: "#86efac" }, { value: 3, label: "3D卡通", color: "#93c5fd" }, { value: 4, label: "国风卡通", color: "#fca5a5" }, { value: 5, label: "纸艺风格", color: "#d6d3d1" }, { value: 6, label: "简易插画", color: "#a5b4fc" }, { value: 7, label: "国风水墨", color: "#94a3b8" }, ]; function formatCreateTaskError(error: unknown, taskMode: UpscaleMode): string { if (isServerRequestError(error) && error.status === 404) { const baseUrl = getServerBaseUrl() || window.location.origin; const route = taskMode === "image" ? "/api/ai/image/super-resolve" : "/api/ai/video/super-resolve"; return `当前连接的服务端 ${baseUrl} 还没有部署 ${route} 接口,请更新并重启服务端后再试`; } return error instanceof Error ? error.message : "超分任务创建失败,请稍后重试"; } interface ResolutionUpscalePageProps { isAuthenticated?: boolean; onOpenMore?: () => void; onOpenImageTool?: (tool: WebImageWorkbenchTool) => void; onSelectView?: (view: WebViewKey) => void; } function ResolutionUpscalePage({ isAuthenticated = false, onOpenMore, onOpenImageTool, onSelectView, }: ResolutionUpscalePageProps) { const fileInputRef = useRef(null); const pollRunRef = useRef(0); const cancelRef = useRef(false); const [mode, setMode] = useState("image"); const [imageScale, setImageScale] = useState("2x"); const [keepOriginalStyle, setKeepOriginalStyle] = useState(true); const [videoStyle, setVideoStyle] = useState(0); const [videoMinLen, setVideoMinLen] = useState(540); const [videoFps, setVideoFps] = useState(15); const [useSR, setUseSR] = useState(true); const [sourceName, setSourceName] = useState(""); const [sourceFile, setSourceFile] = useState(null); const [sourceUrl, setSourceUrl] = useState(""); const [sourcePreview, setSourcePreview] = useState(""); const [resultPreview, setResultPreview] = useState(""); const [sourceDimensions, setSourceDimensions] = useState<{ width: number; height: number } | null>(null); const [videoViewMode, setVideoViewMode] = useState<"source" | "result">("source"); const [status, setStatus] = useState("上传素材并选择参数后点击开始超分"); const [activeTaskId, setActiveTaskId] = useState(""); const [taskProgress, setTaskProgress] = useState(0); const [isProcessing, setIsProcessing] = useState(false); const [isDownloading, setIsDownloading] = useState(false); const [isSavingAsset, setIsSavingAsset] = useState(false); const [isDragging, setIsDragging] = useState(false); const activeTaskIdRef = useRef(activeTaskId); activeTaskIdRef.current = activeTaskId; const keepaliveRestoredRef = useRef(false); useEffect(() => { return () => { if (sourcePreview.startsWith("blob:")) URL.revokeObjectURL(sourcePreview); }; }, [sourcePreview]); useEffect(() => { return () => { if (resultPreview.startsWith("blob:")) URL.revokeObjectURL(resultPreview); }; }, [resultPreview]); // Keep-alive: restore saved task on mount useEffect(() => { if (keepaliveRestoredRef.current) return; keepaliveRestoredRef.current = true; const saved = loadToolTaskState("upscale"); if (!saved || saved.resultUrl) return; setSourceName(saved.sourceName || ""); setSourceUrl(saved.sourceUrl || ""); setIsProcessing(true); cancelRef.current = false; pollRunRef.current += 1; void waitForTaskResult(saved.taskId, mode).catch(() => {}); }, []); useEffect(() => { return () => { // Stop polling but keep server task alive — keep-alive will resume on remount pollRunRef.current += 1; cancelRef.current = true; }; }, []); const clearSource = () => { if (sourcePreview.startsWith("blob:")) URL.revokeObjectURL(sourcePreview); setSourceName(""); setSourceFile(null); setSourceUrl(""); setSourcePreview(""); setResultPreview(""); setSourceDimensions(null); setVideoViewMode("source"); setActiveTaskId(""); setTaskProgress(0); setStatus("已清空素材"); }; const handleModeChange = (nextMode: UpscaleMode) => { if (nextMode === mode) return; clearSource(); setMode(nextMode); setStatus(nextMode === "image" ? "已切换为图片超分,请上传图片素材" : "已切换为视频超分,请上传视频素材"); }; const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; if (sourcePreview.startsWith("blob:")) URL.revokeObjectURL(sourcePreview); setSourceName(file.name); setSourceFile(file); setSourceUrl(""); setSourcePreview(URL.createObjectURL(file)); setResultPreview(""); setSourceDimensions(null); setVideoViewMode("source"); setActiveTaskId(""); setTaskProgress(0); setStatus(`已导入 ${file.name}`); event.currentTarget.value = ""; }; const processDroppedFile = (file: File) => { if (sourcePreview.startsWith("blob:")) URL.revokeObjectURL(sourcePreview); setSourceName(file.name); setSourceFile(file); setSourceUrl(""); setSourcePreview(URL.createObjectURL(file)); setResultPreview(""); setSourceDimensions(null); setVideoViewMode("source"); setActiveTaskId(""); setTaskProgress(0); setStatus(`已导入 ${file.name}`); }; const handleDragOver = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer.types.includes("Files")) setIsDragging(true); }; const handleDragLeave = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.currentTarget === e.target || !e.currentTarget.contains(e.relatedTarget as Node)) setIsDragging(false); }; const handleDrop = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const file = e.dataTransfer.files[0]; if (file) processDroppedFile(file); }; const handleImportUrl = () => { const normalizedUrl = sourceUrl.trim(); if (!/^https?:\/\//i.test(normalizedUrl)) { setStatus("请输入可访问的 HTTP/HTTPS 素材 URL"); return; } if (sourcePreview.startsWith("blob:")) URL.revokeObjectURL(sourcePreview); setSourceFile(null); setSourceName(summarizeUrl(normalizedUrl)); setSourcePreview(normalizedUrl); setResultPreview(""); setVideoViewMode("source"); setActiveTaskId(""); setTaskProgress(0); setStatus(`已导入 URL:${summarizeUrl(normalizedUrl)}`); }; const getSourceGenerationUrl = async () => { if (!sourceFile) return sourcePreview; setStatus(`正在上传 ${sourceFile.name},${formatFileSize(sourceFile.size)}`); const uploaded = await aiGenerationClient.uploadAsset({ dataUrl: await fileToDataUrl(sourceFile), name: sourceFile.name, mimeType: sourceFile.type || (mode === "image" ? "image/png" : "video/mp4"), }); return uploaded.signedUrl || uploaded.url; }; const waitForTaskResult = async (taskId: string, taskMode: UpscaleMode) => { const runId = ++pollRunRef.current; setActiveTaskId(taskId); setTaskProgress(5); saveToolTaskState("upscale", { taskId, sourceName, sourceUrl, status: `任务 ${taskId}`, progress: 5 }); await waitForTask(taskId, { abortRef: cancelRef, onProgress: (e) => { if (pollRunRef.current !== runId) return; const progress = Math.max(0, Math.min(100, Math.trunc(e.progress || 0))); setTaskProgress(progress); setStatus(`任务 ${taskId} ${e.status},进度 ${progress}%`); if (e.status === "completed" && e.resultUrl) { setResultPreview(e.resultUrl); setVideoViewMode("result"); setStatus(`${taskMode === "image" ? "图片" : "视频"}超分完成:${summarizeUrl(e.resultUrl)}`); setTaskProgress(100); clearToolTaskState("upscale"); saveToolTaskState("upscale", { taskId, resultUrl: e.resultUrl, resultPreview: e.resultUrl, sourceName, sourceUrl, status: "完成", progress: 100 }); } }, }); if (cancelRef.current) throw new Error("已取消"); if (pollRunRef.current !== runId) throw new Error("当前任务已被新的超分任务替换"); }; const handleCancel = useCallback(() => { cancelRef.current = true; if (activeTaskId) { aiGenerationClient.cancelTask(activeTaskId).catch(() => {}); } setIsProcessing(false); setStatus("已取消"); clearToolTaskState("upscale"); }, [activeTaskId]); const handleDownload = async () => { if (!resultPreview || isDownloading) return; setIsDownloading(true); try { const isVideo = mode === "video"; const filenameBase = isVideo ? `upscaled-video-${Date.now()}` : `upscaled-image-${Date.now()}`; await saveToolResultToLocal({ url: resultPreview, name: filenameBase, type: isVideo ? "video" : "image", isVideo, taskId: activeTaskId || undefined, tags: ["工具盒", "超分辨率", isVideo ? "生成视频" : "生成图片"], }); setStatus("下载完成"); } catch (error) { setStatus(error instanceof Error ? error.message : "下载失败"); } finally { setIsDownloading(false); } }; const handleSaveToAssets = async () => { if (!resultPreview || isSavingAsset) return; setIsSavingAsset(true); try { const isVideo = mode === "video"; const status = await addToolResultToAssetLibrary({ url: resultPreview, name: `upscaled-${isVideo ? "video" : "image"}-${Date.now()}.${isVideo ? "mp4" : "png"}`, description: `工具盒${isVideo ? "视频" : "图片"}超分生成结果`, type: isVideo ? "video" : "image", isVideo, taskId: activeTaskId || undefined, tags: ["工具盒", "超分辨率", isVideo ? "生成视频" : "生成图片"], }); setStatus(status === "server" ? "已加入资产库" : "已加入本地资产库"); } catch (error) { setStatus(error instanceof Error ? error.message : "加入资产库失败"); } finally { setIsSavingAsset(false); } }; const handleStart = async () => { if (!isAuthenticated) { setStatus("请先登录后再创建超分任务"); return; } if (!sourcePreview) { setStatus(mode === "image" ? "请先上传需要提升分辨率的图片" : "请先上传需要提升分辨率的视频"); return; } if (isProcessing) return; setIsProcessing(true); setResultPreview(""); setTaskProgress(0); cancelRef.current = false; try { const generationUrl = await getSourceGenerationUrl(); setStatus(`素材已就绪:${summarizeUrl(generationUrl)},正在提交任务`); if (mode === "image") { const result = await aiGenerationClient.createImageSuperResolveTask({ imageUrl: generationUrl, scale: imageScale, }); await waitForTaskResult(result.taskId, "image"); } else if (keepOriginalStyle) { const result = await aiGenerationClient.createVideoSuperResolveTask({ videoUrl: generationUrl, }); await waitForTaskResult(result.taskId, "video"); } else { const result = await aiGenerationClient.createVideoSuperResolveTask({ videoUrl: generationUrl, provider: "dashscope-style-transform", style: videoStyle, videoFps, minLen: videoMinLen, useSR, animateEmotion: true, }); await waitForTaskResult(result.taskId, "video"); } } catch (error) { setStatus(formatCreateTaskError(error, mode)); } finally { setIsProcessing(false); } }; const currentVideoStyleLabel = keepOriginalStyle ? "纯超分" : videoStyles.find((style) => style.value === videoStyle)?.label || "日式漫画"; const scaleFactor = Number.parseInt(imageScale, 10); const sourceSizeText = sourceDimensions ? `原图 ${sourceDimensions.width}*${sourceDimensions.height}` : "原图"; const resultSizeText = sourceDimensions ? `结果图 ${sourceDimensions.width * scaleFactor}*${sourceDimensions.height * scaleFactor}` : "结果图"; const modeLabel = mode === "image" ? `图片超分 ${imageScale}` : keepOriginalStyle ? "纯超分(保持原风格)" : `${currentVideoStyleLabel} ${videoMinLen}P${useSR ? " + SR" : ""}`; return (
分辨率提升
{sourcePreview ? ( mode === "image" ? (
setSourceDimensions({ width, height })} /> {resultPreview && (
)}
) : (
{resultPreview && (
)}
) ) : (
fileInputRef.current?.click()} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") fileInputRef.current?.click(); }} >
{mode === "image" ? "点击或拖拽上传图片" : "点击或拖拽上传视频"}
{mode === "image" ? "支持 PNG / JPG / WebP" : "支持 MP4 / MOV / WebM"}
)}
); } export default ResolutionUpscalePage;