detection 畫框、classification 改在影像右上角疊標籤(不畫框), 並在推論面板提供即時切換解析方式與上傳 label 檔。 - 新增 InferenceOverlay 依 taskType 分派;ClassificationOverlay 用 DOM 而非 canvas,讓 CJK 排版與 aria-live 交給瀏覽器處理 - 標籤防閃爍:挑戰者需連續 3 幀居冠才切換,或信心度領先 15% 直接切; 低於門檻立即清空(顯示過期標籤比空白更糟) - 身分比對優先用 classIndex,避免換 label 檔時被誤判為換類別 - 推論設定卡片:即時切 detection/classification、上傳 .txt label、 清除 label,皆不需重燒 - classification-result 原本無條件渲染,導致 detection 模式下側欄 永遠顯示「分類結果」與空圖表,改為依 taskType 切換 - 清掉 camera-overlay 每幀執行的 debug console.log - 補 ResizeObserver stub,原本任何渲染 InferencePanel 的測試都會 在 jsdom 直接拋錯 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
130 lines
4.7 KiB
TypeScript
130 lines
4.7 KiB
TypeScript
'use client';
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { ClassificationResult } from './classification-result';
|
|
import { DetectionResultList } from './detection-result';
|
|
import { PerformanceMetrics } from './performance-metrics';
|
|
import { ConfidenceSlider } from './confidence-slider';
|
|
import { VideoProgress } from './video-progress';
|
|
import { InferenceOptions } from './inference-options';
|
|
import { useInferenceStore } from '@/stores/inference-store';
|
|
import { useCameraStore } from '@/stores/camera-store';
|
|
import { isClassificationTask } from '@/lib/classification';
|
|
import { useTranslation } from '@/lib/i18n';
|
|
|
|
interface InferencePanelProps {
|
|
deviceId: string;
|
|
}
|
|
|
|
export function InferencePanel({ deviceId }: InferencePanelProps) {
|
|
const { t } = useTranslation();
|
|
const { result, fps, avgLatency, isRunning, confidenceThreshold, batchResults } =
|
|
useInferenceStore();
|
|
const { sourceType, batchSelectedIndex, batchImages, batchProcessedCount } =
|
|
useCameraStore();
|
|
|
|
const isVideoMode = sourceType === 'video';
|
|
const isBatchMode = sourceType === 'batch_image';
|
|
const displayResult = isBatchMode
|
|
? batchResults[batchSelectedIndex]
|
|
: result;
|
|
const classifications = displayResult?.classifications || [];
|
|
const detections = displayResult?.detections || [];
|
|
// Branch on "is classification?" so detection stays the safe fall-through
|
|
// under either taskType spelling (see lib/classification.ts).
|
|
const isClassification = isClassificationTask(displayResult?.taskType);
|
|
|
|
return (
|
|
<div className="w-80 space-y-4">
|
|
<PerformanceMetrics fps={fps} avgLatency={avgLatency} isRunning={isRunning} />
|
|
|
|
{isVideoMode && isRunning && <VideoProgress />}
|
|
|
|
{isBatchMode && batchImages.length > 0 && (
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm">{t('inference.batchProgress')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-1 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">{t('inference.processed')}</span>
|
|
<span>{batchProcessedCount} / {batchImages.length}</span>
|
|
</div>
|
|
{batchImages[batchSelectedIndex] && (
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">{t('inference.currentImage')}</span>
|
|
<span className="truncate max-w-32 text-right">
|
|
{batchImages[batchSelectedIndex].filename}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm">{t('inference.confidenceFilter')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ConfidenceSlider />
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm">{t('inference.options.title')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<InferenceOptions deviceId={deviceId} resultTaskType={displayResult?.taskType} />
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm">
|
|
{isClassification
|
|
? t('inference.classificationResults')
|
|
: t('inference.detectionResults')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isClassification ? (
|
|
<ClassificationResult
|
|
results={classifications}
|
|
confidenceThreshold={confidenceThreshold}
|
|
/>
|
|
) : (
|
|
<DetectionResultList
|
|
results={detections}
|
|
confidenceThreshold={confidenceThreshold}
|
|
/>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{displayResult && (
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm">{t('inference.details')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-1 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">{t('inference.model')}</span>
|
|
<span>{displayResult.modelId}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">{t('inference.task')}</span>
|
|
<span>{displayResult.taskType}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">{t('inference.latency')}</span>
|
|
<span>{displayResult.latencyMs.toFixed(1)} ms</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|