qwen3.5 优化
@@ -144,7 +144,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="font-bold text-lg leading-tight tracking-tight">SAM3 Admin</h2>
|
||||
<p class="text-[10px] text-slate-400 font-bold tracking-widest uppercase mt-0.5">Quant Speed AI</p>
|
||||
<p class="text-[10px] text-slate-400 font-bold tracking-widest uppercase mt-0.5">Quantum Track AI</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -155,6 +155,11 @@
|
||||
<i class="fas fa-chart-pie w-5 text-center transition-colors group-hover:text-blue-600" :class="currentTab === 'dashboard' ? 'text-blue-600' : 'text-slate-400'"></i>
|
||||
<span class="font-medium">数据看板</span>
|
||||
</a>
|
||||
<a href="#" @click.prevent="switchTab('tarot')" :class="{ 'active': currentTab === 'tarot' }"
|
||||
class="nav-link flex items-center gap-3 px-4 py-3 text-slate-600 hover:bg-slate-50 hover:text-blue-600 rounded-xl transition-all duration-200 group">
|
||||
<i class="fas fa-star w-5 text-center transition-colors group-hover:text-blue-600" :class="currentTab === 'tarot' ? 'text-blue-600' : 'text-slate-400'"></i>
|
||||
<span class="font-medium">塔罗牌识别</span>
|
||||
</a>
|
||||
<a href="#" @click.prevent="switchTab('gpu')" :class="{ 'active': currentTab === 'gpu' }"
|
||||
class="nav-link flex items-center gap-3 px-4 py-3 text-slate-600 hover:bg-slate-50 hover:text-blue-600 rounded-xl transition-all duration-200 group">
|
||||
<i class="fas fa-microchip w-5 text-center transition-colors group-hover:text-blue-600" :class="currentTab === 'gpu' ? 'text-blue-600' : 'text-slate-400'"></i>
|
||||
@@ -372,6 +377,158 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Tarot Tab -->
|
||||
<div v-if="currentTab === 'tarot'" key="tarot" class="space-y-6">
|
||||
<!-- Input Section -->
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6">
|
||||
<h3 class="font-bold text-slate-800 mb-6 flex items-center gap-2">
|
||||
<span class="w-1 h-5 bg-purple-500 rounded-full"></span>
|
||||
塔罗牌识别任务
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Image Upload -->
|
||||
<div class="space-y-4">
|
||||
<label class="block text-sm font-medium text-slate-700">上传图片</label>
|
||||
<div class="flex items-center justify-center w-full">
|
||||
<label for="dropzone-file" class="flex flex-col items-center justify-center w-full h-64 border-2 border-slate-300 border-dashed rounded-xl cursor-pointer bg-slate-50 hover:bg-slate-100 transition-colors relative overflow-hidden">
|
||||
<div v-if="!tarotFile && !tarotImageUrl" class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
<i class="fas fa-cloud-upload-alt text-4xl text-slate-400 mb-3"></i>
|
||||
<p class="mb-2 text-sm text-slate-500"><span class="font-semibold">点击上传</span> 或拖拽文件到此处</p>
|
||||
<p class="text-xs text-slate-400">支持 JPG, PNG (MAX. 10MB)</p>
|
||||
</div>
|
||||
<div v-else class="absolute inset-0 flex items-center justify-center bg-slate-100">
|
||||
<img v-if="tarotPreview" :src="tarotPreview" class="max-h-full max-w-full object-contain">
|
||||
<div v-else class="text-slate-500 flex flex-col items-center">
|
||||
<i class="fas fa-link text-2xl mb-2"></i>
|
||||
<span class="text-xs truncate max-w-[200px]">{{ tarotImageUrl }}</span>
|
||||
</div>
|
||||
<button @click.prevent="clearTarotInput" class="absolute top-2 right-2 bg-white/80 p-1.5 rounded-full hover:bg-white text-slate-600 shadow-sm">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<input id="dropzone-file" type="file" class="hidden" accept="image/*" @change="handleTarotFileChange" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<i class="fas fa-link text-slate-400"></i>
|
||||
</div>
|
||||
<input type="text" v-model="tarotImageUrl" @input="handleUrlInput"
|
||||
class="bg-slate-50 border border-slate-200 text-slate-900 text-sm rounded-xl focus:ring-purple-500 focus:border-purple-500 block w-full pl-10 p-2.5 outline-none"
|
||||
placeholder="或者输入图片 URL...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">预期卡牌数量</label>
|
||||
<div class="flex items-center gap-4">
|
||||
<input type="number" v-model.number="tarotExpectedCount" min="1" max="10"
|
||||
class="bg-slate-50 border border-slate-200 text-slate-900 text-sm rounded-xl focus:ring-purple-500 focus:border-purple-500 block w-full p-2.5 outline-none font-mono">
|
||||
<span class="text-sm text-slate-500 whitespace-nowrap">张</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mt-2">系统将尝试检测并分割指定数量的卡牌。如果检测数量不符,将返回错误提示。</p>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<button @click="recognizeTarot" :disabled="isRecognizing || (!tarotFile && !tarotImageUrl)"
|
||||
class="w-full bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-bold py-3 px-4 rounded-xl shadow-lg shadow-purple-500/30 transition-all transform active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2">
|
||||
<i class="fas fa-magic" :class="{'fa-spin': isRecognizing}"></i>
|
||||
{{ isRecognizing ? '正在识别中...' : '开始识别 (Recognize)' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Section -->
|
||||
<div v-if="tarotResult" class="space-y-6 animate-fade-in">
|
||||
<!-- Status Banner -->
|
||||
<div :class="tarotResult.status === 'success' ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-red-700'"
|
||||
class="p-4 rounded-xl border flex items-center gap-3 shadow-sm">
|
||||
<i :class="tarotResult.status === 'success' ? 'fas fa-check-circle' : 'fas fa-exclamation-circle'" class="text-xl"></i>
|
||||
<div>
|
||||
<h4 class="font-bold">{{ tarotResult.status === 'success' ? '识别成功' : '识别失败' }}</h4>
|
||||
<p class="text-sm opacity-90">{{ tarotResult.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spread Info -->
|
||||
<div v-if="tarotResult.spread_info" class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6">
|
||||
<h3 class="font-bold text-slate-800 mb-4 flex items-center gap-2">
|
||||
<i class="fas fa-layer-group text-purple-500"></i>
|
||||
牌阵信息
|
||||
</h3>
|
||||
<div class="bg-purple-50 rounded-xl p-4 border border-purple-100">
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<div class="md:w-1/3">
|
||||
<span class="text-xs font-bold text-purple-400 uppercase tracking-wider">牌阵名称</span>
|
||||
<div class="text-xl font-bold text-slate-800 mt-1">{{ tarotResult.spread_info.spread_name }}</div>
|
||||
</div>
|
||||
<div class="md:w-2/3 border-t md:border-t-0 md:border-l border-purple-200 pt-4 md:pt-0 md:pl-4">
|
||||
<span class="text-xs font-bold text-purple-400 uppercase tracking-wider">描述 / 寓意</span>
|
||||
<div class="text-sm text-slate-700 mt-1 leading-relaxed">{{ tarotResult.spread_info.description || '暂无描述' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tarotResult.spread_info.model_used" class="mt-3 pt-3 border-t border-purple-200 flex items-center gap-2">
|
||||
<span class="text-xs bg-white text-purple-600 px-2 py-0.5 rounded border border-purple-200 font-mono">
|
||||
<i class="fas fa-robot mr-1"></i>{{ tarotResult.spread_info.model_used }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cards Grid -->
|
||||
<div v-if="tarotResult.tarot_cards && tarotResult.tarot_cards.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div v-for="(card, index) in tarotResult.tarot_cards" :key="index" class="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden hover:shadow-md transition-shadow group">
|
||||
<div class="relative aspect-[2/3] bg-slate-100 overflow-hidden cursor-pointer" @click="previewImage(card.url)">
|
||||
<img :src="card.url" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500">
|
||||
<div class="absolute top-2 right-2 bg-black/60 backdrop-blur-sm text-white text-xs font-bold px-2 py-1 rounded">
|
||||
#{{ index + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 space-y-3">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h4 class="font-bold text-lg text-slate-800">{{ card.recognition?.name || '未知' }}</h4>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span :class="card.recognition?.position === '正位' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
|
||||
class="text-xs font-bold px-2 py-0.5 rounded">
|
||||
{{ card.recognition?.position || '未知' }}
|
||||
</span>
|
||||
<span v-if="card.is_rotated" class="text-[10px] text-slate-400 bg-slate-100 px-1.5 rounded" title="已自动矫正方向">
|
||||
<i class="fas fa-sync-alt"></i> Auto-Rotated
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="card.recognition?.model_used" class="pt-3 border-t border-slate-100 flex items-center justify-between text-xs">
|
||||
<span class="text-slate-400">Model Used:</span>
|
||||
<span class="font-mono text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded border border-purple-100">
|
||||
{{ card.recognition.model_used }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full Visualization -->
|
||||
<div v-if="tarotResult.full_visualization" class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6">
|
||||
<h3 class="font-bold text-slate-800 mb-4 flex items-center gap-2">
|
||||
<i class="fas fa-image text-blue-500"></i>
|
||||
整体可视化结果
|
||||
</h3>
|
||||
<div class="rounded-xl overflow-hidden border border-slate-200 cursor-pointer" @click="previewImage(tarotResult.full_visualization)">
|
||||
<img :src="tarotResult.full_visualization" class="w-full h-auto">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History Tab -->
|
||||
<div v-if="currentTab === 'history'" key="history" class="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
@@ -383,7 +540,7 @@
|
||||
<th class="px-6 py-4">Prompt / 详情</th>
|
||||
<th class="px-6 py-4 text-center">耗时</th>
|
||||
<th class="px-6 py-4 text-center">状态</th>
|
||||
<th class="px-6 py-4 text-center">查看</th>
|
||||
<th class="px-6 py-4 text-center">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@@ -787,6 +944,14 @@
|
||||
const gpuStatus = ref({});
|
||||
const gpuHistory = ref([]);
|
||||
let gpuInterval = null;
|
||||
|
||||
// Tarot State
|
||||
const tarotFile = ref(null);
|
||||
const tarotImageUrl = ref('');
|
||||
const tarotExpectedCount = ref(3);
|
||||
const tarotPreview = ref(null);
|
||||
const tarotResult = ref(null);
|
||||
const isRecognizing = ref(false);
|
||||
|
||||
// Filters
|
||||
const selectedTimeRange = ref('all');
|
||||
@@ -1147,6 +1312,90 @@
|
||||
} catch (e) { alert('删除失败: ' + e.message); }
|
||||
};
|
||||
|
||||
// --- Tarot Actions ---
|
||||
const handleTarotFileChange = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
tarotFile.value = file;
|
||||
tarotImageUrl.value = ''; // Clear URL if file is selected
|
||||
tarotPreview.value = URL.createObjectURL(file);
|
||||
tarotResult.value = null; // Clear previous result
|
||||
}
|
||||
};
|
||||
|
||||
const handleUrlInput = () => {
|
||||
if (tarotImageUrl.value) {
|
||||
tarotFile.value = null; // Clear file if URL is entered
|
||||
tarotPreview.value = null; // Can't preview external URL easily without loading it, or just use the URL
|
||||
// Simple preview for URL
|
||||
tarotPreview.value = tarotImageUrl.value;
|
||||
tarotResult.value = null;
|
||||
} else {
|
||||
tarotPreview.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const clearTarotInput = () => {
|
||||
tarotFile.value = null;
|
||||
tarotImageUrl.value = '';
|
||||
tarotPreview.value = null;
|
||||
tarotResult.value = null;
|
||||
// Reset file input value
|
||||
const fileInput = document.getElementById('dropzone-file');
|
||||
if (fileInput) fileInput.value = '';
|
||||
};
|
||||
|
||||
const recognizeTarot = async () => {
|
||||
if (!tarotFile.value && !tarotImageUrl.value) return;
|
||||
|
||||
isRecognizing.value = true;
|
||||
tarotResult.value = null;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
if (tarotFile.value) {
|
||||
formData.append('file', tarotFile.value);
|
||||
} else {
|
||||
formData.append('image_url', tarotImageUrl.value);
|
||||
}
|
||||
formData.append('expected_count', tarotExpectedCount.value);
|
||||
|
||||
// Use axios directly or a helper. Need to handle API Key if required by backend,
|
||||
// but admin usually has session. Wait, the backend endpoints like /recognize_tarot
|
||||
// require X-API-Key header.
|
||||
// The admin page uses cookie for /admin/api/* but /recognize_tarot is a public API protected by Key.
|
||||
// We should add the key to the header.
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
'X-API-Key': '123quant-speed' // Hardcoded as per fastAPI_tarot.py VALID_API_KEY
|
||||
},
|
||||
timeout: 120000 // 2分钟超时,大模型响应较慢
|
||||
};
|
||||
|
||||
const res = await axios.post('/recognize_tarot', formData, config);
|
||||
tarotResult.value = res.data;
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
let msg = e.response?.data?.detail || e.message || '识别请求失败';
|
||||
|
||||
// 针对 504 Gateway Timeout 或 请求超时做特殊提示
|
||||
if (e.response && e.response.status === 504) {
|
||||
msg = '请求超时 (504):大模型处理时间较长。后台可能仍在运行,请稍后在“识别记录”中刷新查看结果。';
|
||||
} else if (e.code === 'ECONNABORTED') {
|
||||
msg = '请求超时:网络连接中断或服务器响应过慢。请稍后重试。';
|
||||
}
|
||||
|
||||
tarotResult.value = {
|
||||
status: 'failed',
|
||||
message: msg
|
||||
};
|
||||
} finally {
|
||||
isRecognizing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Navigation & Helpers ---
|
||||
const switchTab = (tab) => {
|
||||
const prevTab = currentTab.value;
|
||||
@@ -1236,6 +1485,7 @@
|
||||
const getPageTitle = (tab) => {
|
||||
const map = {
|
||||
'dashboard': '数据看板',
|
||||
'tarot': '塔罗牌识别',
|
||||
'history': '识别记录',
|
||||
'files': '文件资源管理',
|
||||
'prompts': '提示词工程',
|
||||
@@ -1248,6 +1498,7 @@
|
||||
const getPageSubtitle = (tab) => {
|
||||
const map = {
|
||||
'dashboard': '系统运行状态与核心指标概览',
|
||||
'tarot': 'SAM3 + Qwen-VL 联合识别与分割',
|
||||
'history': '所有视觉识别任务的历史流水',
|
||||
'files': '查看和管理生成的图像及JSON结果',
|
||||
'prompts': '调整各个识别场景的 System Prompt',
|
||||
@@ -1460,7 +1711,10 @@
|
||||
selectedTimeRange, selectedType,
|
||||
barChartRef, pieChartRef, promptPieChartRef, promptBarChartRef, wordCloudRef,
|
||||
formatBytes, gpuStatus,
|
||||
gpuUtilChartRef, gpuTempChartRef
|
||||
gpuUtilChartRef, gpuTempChartRef,
|
||||
// Tarot
|
||||
tarotFile, tarotImageUrl, tarotExpectedCount, tarotPreview, tarotResult, isRecognizing,
|
||||
handleTarotFileChange, handleUrlInput, clearTarotInput, recognizeTarot
|
||||
};
|
||||
}
|
||||
}).mount('#app');
|
||||
|
||||
BIN
static/results/1771396461_4ac00fc4/raw_for_spread.jpg
Normal file
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 344 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 343 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 266 KiB |
BIN
static/results/1771396501_cd6d8769/raw_for_spread.jpg
Normal file
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 344 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 343 KiB |
BIN
static/results/1771396577_a0d559ee/raw_for_spread.jpg
Normal file
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 344 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 343 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 266 KiB |