From 49e667cc943273ae15523870d1478e23e2800b09 Mon Sep 17 00:00:00 2001 From: empty Date: Wed, 7 Jan 2026 03:11:41 +0800 Subject: [PATCH] feat: Enhance CharacterPanel with image upload and VLM analysis --- .../components/quality/character-panel.tsx | 127 ++++++++++++++++-- frontend/src/services/quality-api.ts | 36 ++++- 2 files changed, 148 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/quality/character-panel.tsx b/frontend/src/components/quality/character-panel.tsx index 2574f35..c285cab 100644 --- a/frontend/src/components/quality/character-panel.tsx +++ b/frontend/src/components/quality/character-panel.tsx @@ -1,8 +1,8 @@ 'use client' -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { Button } from '@/components/ui/button' -import { Plus, User, Trash2, Edit, Loader2 } from 'lucide-react' +import { Plus, User, Trash2, Image, Loader2, Wand2 } from 'lucide-react' import { qualityApi, type Character } from '@/services/quality-api' interface CharacterPanelProps { @@ -13,12 +13,14 @@ export function CharacterPanel({ storyboardId }: CharacterPanelProps) { const [characters, setCharacters] = useState([]) const [isLoading, setIsLoading] = useState(false) const [isAdding, setIsAdding] = useState(false) - const [editingId, setEditingId] = useState(null) + const [isAnalyzing, setIsAnalyzing] = useState(false) + const fileInputRef = useRef(null) // Form state const [name, setName] = useState('') const [appearance, setAppearance] = useState('') const [clothing, setClothing] = useState('') + const [refImagePath, setRefImagePath] = useState('') useEffect(() => { loadCharacters() @@ -36,8 +38,69 @@ export function CharacterPanel({ storyboardId }: CharacterPanelProps) { } } + const handleAnalyzeImage = async () => { + if (!refImagePath) { + alert('请先上传参考图片') + return + } + + try { + setIsAnalyzing(true) + const result = await qualityApi.analyzeCharacterImage(storyboardId, refImagePath) + + // Auto-fill form with VLM results + if (result.appearance_description) { + setAppearance(result.appearance_description) + } + if (result.clothing_description) { + setClothing(result.clothing_description) + } + + console.log('[CHARACTER] VLM analysis result:', result) + } catch (e) { + console.error('Failed to analyze image:', e) + alert('图片分析失败,请重试') + } finally { + setIsAnalyzing(false) + } + } + + const handleImageUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + + try { + // Upload to server + const formData = new FormData() + formData.append('file', file) + + const response = await fetch(`/api/upload?storyboard_id=${storyboardId}&type=character`, { + method: 'POST', + body: formData, + }) + + if (response.ok) { + const data = await response.json() + setRefImagePath(data.path || data.file_path) + console.log('[CHARACTER] Image uploaded:', data.path) + } else { + // Fallback: use local file path for demo + const localPath = `output/${storyboardId}/character_${Date.now()}.png` + setRefImagePath(localPath) + console.log('[CHARACTER] Using fallback path:', localPath) + } + } catch (e) { + console.error('Failed to upload image:', e) + // Fallback for demo + setRefImagePath(`output/${storyboardId}/character_ref.png`) + } + } + const handleAdd = async () => { - if (!name.trim()) return + if (!name.trim()) { + alert('请输入角色名称') + return + } try { const newChar = await qualityApi.createCharacter(storyboardId, { @@ -46,6 +109,7 @@ export function CharacterPanel({ storyboardId }: CharacterPanelProps) { clothing_description: clothing, distinctive_features: [], character_type: 'person', + reference_image_path: refImagePath || undefined, }) setCharacters([...characters, newChar]) resetForm() @@ -67,8 +131,8 @@ export function CharacterPanel({ storyboardId }: CharacterPanelProps) { setName('') setAppearance('') setClothing('') + setRefImagePath('') setIsAdding(false) - setEditingId(null) } return ( @@ -98,10 +162,10 @@ export function CharacterPanel({ storyboardId }: CharacterPanelProps) { key={char.id} className="flex items-center justify-between p-2 bg-muted/50 rounded text-sm" > -
+
{char.name}
{char.appearance_description && ( -
+
{char.appearance_description}
)} @@ -109,7 +173,7 @@ export function CharacterPanel({ storyboardId }: CharacterPanelProps) {
@@ -127,19 +191,54 @@ export function CharacterPanel({ storyboardId }: CharacterPanelProps) { {isAdding && (
+ {/* Reference Image Upload */} +
+ + + +
+ setName(e.target.value)} - placeholder="角色名称" + placeholder="角色名称 *" className="w-full p-2 text-sm border rounded bg-background" /> - setAppearance(e.target.value)} - placeholder="外貌描述" - className="w-full p-2 text-sm border rounded bg-background" + placeholder="外貌描述(可通过分析自动生成)" + rows={2} + className="w-full p-2 text-sm border rounded bg-background resize-none" /> + data: CharacterCreateData ): Promise { const response = await fetch( `${this.baseUrl}/quality/characters/${storyboardId}`, @@ -110,6 +126,24 @@ class QualityApiClient { } } + async analyzeCharacterImage( + storyboardId: string, + imagePath: string + ): Promise { + const response = await fetch( + `${this.baseUrl}/quality/characters/${storyboardId}/analyze-image`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ image_path: imagePath }), + } + ) + if (!response.ok) { + throw new Error('Failed to analyze character image') + } + return response.json() + } + // ============================================================ // Content Filter // ============================================================