jim800121chen ddd1aae5d1 feat(server): 把 model metadata 傳進推論鏈路
models.json 宣告的 taskType/labels/inputSize 原本在 flash 時被丟棄
(只傳 modelPath),導致 bridge 只能靠檔名猜測模型類型與尺寸。

- FlashOptions 帶 TaskType/Labels/InputWidth/InputHeight
- 抽出 buildLoadModelCommand,四處 load_model 呼叫點(初次 + 三條
  retry 路徑)統一走它,並加測試釘住呼叫點數量與「不得有手寫 payload」
  —— 讓漏改 retry 路徑在結構上不可能發生
- ClassResult 加 ClassIndex(不加 omitempty,index 0 是合法值)

用 FlashOptions struct 而非裸參數,未來擴充欄位不需再動 interface
與所有 test fake。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 19:27:02 +08:00

141 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package driver
import "time"
type DeviceDriver interface {
Info() DeviceInfo
Connect() error
Disconnect() error
IsConnected() bool
Flash(modelPath string, opts FlashOptions, progressCh chan<- FlashProgress) error
StartInference() error
StopInference() error
ReadInference() (*InferenceResult, error)
RunInference(imageData []byte) (*InferenceResult, error)
GetModelInfo() (*ModelInfo, error)
}
type DeviceInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Port string `json:"port"`
VendorID uint16 `json:"vendorId,omitempty"`
ProductID uint16 `json:"productId,omitempty"`
Status DeviceStatus `json:"status"`
FirmwareVer string `json:"firmwareVersion,omitempty"`
FlashedModel string `json:"flashedModel,omitempty"`
}
type DeviceStatus string
const (
StatusDetected DeviceStatus = "detected"
StatusConnecting DeviceStatus = "connecting"
StatusConnected DeviceStatus = "connected"
StatusFlashing DeviceStatus = "flashing"
StatusInferencing DeviceStatus = "inferencing"
StatusError DeviceStatus = "error"
StatusDisconnected DeviceStatus = "disconnected"
)
// FlashOptions 帶入 model metadata供 driver 在 load model 時傳給硬體 bridge。
//
// 為什麼用 struct 而不是多帶兩個參數:載入模型需要的 metadata 之後還會長
// 如前處理色彩格式、top-K用 struct 之後新增欄位不必再改 interface 簽章
// 與所有 test fake。
//
// 兩個欄位都是 optional —— 空值代表「未指定」bridge 端會 fallback 到既有的
// model id / 檔名 heuristics維持既有 detection 行為不變)。
type FlashOptions struct {
// TaskType 為 models.json 宣告的推論類型("classification" /
// "object_detection"。bridge 端有指定就不再用檔名猜測。
TaskType string
// Labels 是 class index → 顯示名稱的對應表,純顯示層用途、非推論必要輸入。
// 沒帶時 classification 輸出原始 enumclass_N、detection 沿用 COCO。
Labels []string
// InputWidth / InputHeight 是 models.json / metadata.json 宣告的模型輸入
// 尺寸。
//
// ⚠️ 這是**最後手段**不是可信來源bridge 端會優先向 SDK 問模型自己
// 宣告的 input tensor shape只有 SDK 沒回報時才用這組值。原因是這裡的
// 數字是人在上傳表單填的,實際案例是使用者填了 640x640 但模型根本不是
// 那個尺寸 —— 尺寸錯了 NPU 不會報錯,只會安靜地給出錯的推論結果。
//
// 零值 = 未宣告bridge 端會忽略並往下 fallback。
InputWidth int
InputHeight int
}
// InferenceOptions 是推論期可即時調整的解析設定。
//
// 與 FlashOptions 的分工FlashOptions 在「把 model 載進裝置」時一次性帶入;
// InferenceOptions 則是在**同一個已載入的 model 上**改變輸出的解讀方式,
// 不需要重燒KL520 重燒要數十秒)。
//
// 兩個欄位的零值語意刻意不同,因為要能表達「不動」與「清空」兩種意圖:
//
// TaskType == "" → 不改變當前解析方式
// Labels == nil → 不改變當前 label 表
// Labels == []string{} → 清空 label 表,回到原始 enumclass_N
//
// ⚠️ 因此 Labels 的判斷必須用 `!= nil` 而非 `len() > 0` —— 用長度判斷會讓
// 「清空」這個合法意圖永遠送不出去。
type InferenceOptions struct {
TaskType string
Labels []string
}
type FlashProgress struct {
Percent int `json:"percent"`
Stage string `json:"stage"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
}
type InferenceResult struct {
DeviceID string `json:"deviceId,omitempty"`
ModelID string `json:"modelId,omitempty"`
TaskType string `json:"taskType"`
Timestamp int64 `json:"timestamp"`
LatencyMs float64 `json:"latencyMs"`
Classifications []ClassResult `json:"classifications,omitempty"`
Detections []DetectionResult `json:"detections,omitempty"`
// Batch image fields (omitted for single-image/camera/video modes)
ImageIndex int `json:"imageIndex,omitempty"`
TotalImages int `json:"totalImages,omitempty"`
Filename string `json:"filename,omitempty"`
// Video progress fields (omitted for non-video modes)
FrameIndex int `json:"frameIndex,omitempty"`
TotalFrames int `json:"totalFrames,omitempty"`
}
type ClassResult struct {
Label string `json:"label"`
Confidence float64 `json:"confidence"`
// ClassIndex 是模型輸出的原始類別索引,供前端在 label 缺漏時 fallback 顯示。
// 刻意不加 omitempty —— index 0 是合法類別omitempty 會把它吃掉。
ClassIndex int `json:"classIndex"`
}
type DetectionResult struct {
Label string `json:"label"`
Confidence float64 `json:"confidence"`
BBox BBox `json:"bbox"`
}
type BBox struct {
X float64 `json:"x"`
Y float64 `json:"y"`
Width float64 `json:"width"`
Height float64 `json:"height"`
}
type ModelInfo struct {
ID string `json:"id"`
Name string `json:"name"`
LoadedAt time.Time `json:"loadedAt"`
}