diff --git a/backend/utils/airdrama_client.py b/backend/utils/airdrama_client.py index 63e05a9..4bcef4c 100644 --- a/backend/utils/airdrama_client.py +++ b/backend/utils/airdrama_client.py @@ -9,6 +9,7 @@ ERROR_MESSAGES = { # Input content moderation — 人脸/敏感内容 'InputImageSensitiveContentDetected.PrivacyInformation': '参考图片中检测到真实人脸,请使用虚拟人像素材替代真人照片', 'InputImageSensitiveContentDetected': '参考图片包含敏感内容,请更换图片后重试', + 'InputVideoSensitiveContentDetected.PrivacyInformation': '参考视频中检测到真实人脸,请使用虚拟人像素材替代真人视频', 'InputVideoSensitiveContentDetected': '参考视频包含敏感内容,请更换视频后重试', 'InputTextSensitiveContentDetected': '提示词包含敏感内容,请修改后重试', 'InputAudioSensitiveContentDetected': '参考音频包含敏感内容,请更换音频后重试', diff --git a/web/src/components/AssetLibraryModal.module.css b/web/src/components/AssetLibraryModal.module.css index 41177d9..c7e85b8 100644 --- a/web/src/components/AssetLibraryModal.module.css +++ b/web/src/components/AssetLibraryModal.module.css @@ -11,12 +11,11 @@ .modal { width: 90vw; max-width: 1400px; - min-height: 85vh; - max-height: 92vh; + height: 85vh; background: #16161e; border: 1px solid var(--color-border-card); border-radius: 12px; - overflow-y: auto; + overflow: hidden; display: flex; flex-direction: column; } diff --git a/web/src/components/AssetLibraryModal.tsx b/web/src/components/AssetLibraryModal.tsx index e1f10ef..ce9c28e 100644 --- a/web/src/components/AssetLibraryModal.tsx +++ b/web/src/components/AssetLibraryModal.tsx @@ -1,7 +1,8 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { useAssetLibraryStore } from '../store/assetLibrary'; -import { assetsApi } from '../lib/api'; +import { assetsApi, tosThumb } from '../lib/api'; import { showToast } from './Toast'; +import { ImageLightbox } from './ImageLightbox'; import type { AssetGroup, AssetItem } from '../types'; import styles from './AssetLibraryModal.module.css'; @@ -20,6 +21,7 @@ export function AssetLibraryModal({ open, onClose }: Props) { const [uploadFile, setUploadFile] = useState(null); const [uploadPreview, setUploadPreview] = useState(null); const [dragOver, setDragOver] = useState(false); + const [lightboxSrc, setLightboxSrc] = useState(null); const fileInputRef = useRef(null); const addFileInputRef = useRef(null); @@ -209,7 +211,7 @@ export function AssetLibraryModal({ open, onClose }: Props) {
{groups.map((group) => (
handleGroupClick(group)}> - {group.name} + {group.name}
{editingName && editingName.id === group.id ? (
e.stopPropagation()}> @@ -342,7 +344,13 @@ export function AssetLibraryModal({ open, onClose }: Props) {
{groupAssets.map((asset) => (
- {asset.name} + {asset.name} setLightboxSrc(asset.url)} + />
{asset.name}
+ setLightboxSrc(null)} />
); } diff --git a/web/src/components/GenerationCard.tsx b/web/src/components/GenerationCard.tsx index 4dc743e..65da072 100644 --- a/web/src/components/GenerationCard.tsx +++ b/web/src/components/GenerationCard.tsx @@ -4,6 +4,7 @@ import type { GenerationTask } from '../types'; import { useGenerationStore } from '../store/generation'; import { showToast } from './Toast'; import { ConfirmModal } from './ConfirmModal'; +import { tosThumb } from '../lib/api'; import styles from './GenerationCard.module.css'; const EditIcon = () => ( @@ -57,7 +58,7 @@ function MentionTag({ label, thumbUrl }: { label: string; thumbUrl?: string }) { > {thumbUrl && ( @@ -66,7 +67,7 @@ function MentionTag({ label, thumbUrl }: { label: string; thumbUrl?: string }) { {hover && thumbUrl && createPortal(
- {label} + {label}
{label}
, document.body @@ -137,6 +138,7 @@ export function GenerationCard({ task, onOpenDetail }: Props) { const [detailPos, setDetailPos] = useState({ top: 0, right: 0 }); const detailLinkRef = useRef(null); const detailLeaveTimer = useRef | null>(null); + const [refPreview, setRefPreview] = useState<{ url: string; label: string; type: string; top: number; left: number } | null>(null); const startDetailLeave = useCallback(() => { if (detailLeaveTimer.current) clearTimeout(detailLeaveTimer.current); @@ -272,7 +274,16 @@ export function GenerationCard({ task, onOpenDetail }: Props) { {task.references.length > 0 && (
{task.references.map((ref) => ( -
+
{ + if (ref.type === 'audio') return; + const rect = e.currentTarget.getBoundingClientRect(); + setRefPreview({ url: ref.previewUrl, label: ref.label, type: ref.type, top: rect.top - 8, left: rect.left + rect.width / 2 }); + }} + onMouseLeave={() => setRefPreview(null)} + > {ref.type === 'video' ? (
) : ( - {ref.label} + {ref.label} )}
))} @@ -387,6 +398,19 @@ export function GenerationCard({ task, onOpenDetail }: Props) { )}
+ {/* Reference thumbnail hover preview */} + {refPreview && createPortal( +
+ {refPreview.type === 'video' ? ( +
, + document.body + )} + {/* Video / result area */}
{isGenerating ? ( diff --git a/web/src/components/PromptInput.tsx b/web/src/components/PromptInput.tsx index b387b8b..4f15211 100644 --- a/web/src/components/PromptInput.tsx +++ b/web/src/components/PromptInput.tsx @@ -1,7 +1,7 @@ import { useRef, useEffect, useCallback, useState } from 'react'; import DOMPurify from 'dompurify'; import { useInputBarStore } from '../store/inputBar'; -import { assetsApi } from '../lib/api'; +import { assetsApi, tosThumb } from '../lib/api'; import type { UploadedFile, AssetGroup } from '../types'; import styles from './PromptInput.module.css'; @@ -77,7 +77,7 @@ export function PromptInput() { if (opts.thumbUrl) { const img = document.createElement('img'); - img.src = opts.thumbUrl; + img.src = tosThumb(opts.thumbUrl, 32); img.className = styles.mentionImg; // 显式设置尺寸,防止 CSS class 未生效时图片为 0x0 img.setAttribute('width', '16'); @@ -253,6 +253,12 @@ export function PromptInput() { const textBeforeCursor = text.substring(0, offset); const lastAtIdx = textBeforeCursor.lastIndexOf('@'); + if (lastAtIdx < 0) { + // No @ before cursor, close popup + setShowMentionPopup(false); + return; + } + if (lastAtIdx >= 0) { const textAfterAt = textBeforeCursor.substring(lastAtIdx + 1); @@ -604,7 +610,7 @@ export function PromptInput() { {ref.type === 'video' ? (
{ref.label} @@ -628,7 +634,7 @@ export function PromptInput() { }} >
- +
{group.name} 人像 @@ -656,7 +662,7 @@ export function PromptInput() { /> ) : ( {hoverRef.label} diff --git a/web/src/components/UniversalUpload.tsx b/web/src/components/UniversalUpload.tsx index f598f34..2804955 100644 --- a/web/src/components/UniversalUpload.tsx +++ b/web/src/components/UniversalUpload.tsx @@ -2,6 +2,7 @@ import { useRef, useState } from 'react'; import { useInputBarStore } from '../store/inputBar'; import { showToast } from './Toast'; import { ImageLightbox } from './ImageLightbox'; +import { tosThumb } from '../lib/api'; import styles from './UniversalUpload.module.css'; const MAX_IMAGE_SIZE = 30 * 1024 * 1024; // 30MB per API doc @@ -124,7 +125,7 @@ export function UniversalUpload() {
) : ( - {ref.label} { e.stopPropagation(); setLightboxSrc(ref.previewUrl); }} /> + {ref.label} { e.stopPropagation(); setLightboxSrc(ref.previewUrl); }} /> )}
) : ( - {ref.label} setLightboxSrc(ref.previewUrl)} /> + {ref.label} setLightboxSrc(ref.previewUrl)} /> )} {ref.label}
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index c9d4e6b..abc7f04 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -387,4 +387,17 @@ export const assetsApi = { api.get<{ id: number; status: string; url: string; error_message: string }>(`/assets/${id}/status`), }; +/** + * Append TOS image resize parameter to reduce loading size. + * Only applies to TOS image URLs (volces.com with image extensions). + */ +export function tosThumb(url: string | undefined, height: number): string { + if (!url) return ''; + // 只对我们自己的 TOS 桶生效(airdrama-media),不处理火山内部桶(ark-media-asset 等) + if (!url.includes('airdrama-media')) return url; + if (!/\.(png|jpg|jpeg|webp|gif)/i.test(url)) return url; + const sep = url.includes('?') ? '&' : '?'; + return `${url}${sep}x-tos-process=image/resize,h_${height}`; +} + export default api;