jim800121chen 9ab1a11ea4 feat(models): 補 7 個預設模型(B8)+ 詳細頁下載按鈕(B5)
# B8 預設模型(簡單版:寫死常數 + 打包 + 不簽 token)
模型庫「預設模型」原本實際是空的(舊 seed 是 demo 假資料、production 不啟用)。
補 7 個公用預設模型(kl520×4 + kl720×3,來源 local-tool models.json):
- metadata 寫死成 Go 常數(presets.go),不進 DB → 公用、不可刪、無 seed 重複
- 7 個 .nef 打包進 image(assets/preset-models/,~61MB;Dockerfile COPY)
- download 走 visionA 自己(簡單版不簽 token,preset 公用本不需授權):
  download handler 三分支 preset→visionA URL / converted→FAA / uploaded→501
- 新 GET /preset-models/*filepath 靜態 serve(無 auth、Content-Disposition
  attachment 用 mime.FormatMediaType、path-traversal 防禦)
- list/get 含 preset(對所有人可見)

# B5 詳細頁下載按鈕 + preset 可下載
- model-detail-client.tsx 補下載按鈕(行為對齊列表卡片)
- isModelDownloadable 加 preset(source==="preset" → true)
- normalizeModelSummary 對 target_chip toLowerCase(修 preset 大寫 KL520
  在小寫晶片篩選下被隱藏的 bug;收斂所有來源大小寫)

# 測試
backend:presets/preset 下載三分支/靜態 serve/path-traversal/條件對稱 全綠
frontend:詳細頁下載鈕 + preset 可下載 + target_chip 大小寫篩選回歸 38 PASS
Reviewer 兩輪通過(Major-1 + Minor-1/2 修畢、複審 0 問題)

# backlog(未做的優化)
preset 下載改 HMAC presigned / preset .nef 改放 FAA 減 image / metadata 後台可管理

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:38:25 +08:00

190 lines
7.9 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.

// presets.go — 7 個系統預設模型B8
//
// 設計決策(使用者拍板「簡單版」):
// - metadata 寫死成 Go 常數,**不進 DB**。理由preset 是固定公用資料,不該有 owner
// 隔離、不該能被刪、也無 seed 重複問題(多實例 / 重啟結果一致)。
// - 對應的 .nef 檔打包進 visionA-backend imageassets/preset-models/{id}.nef
// download 走 visionA 自己的 storage handler簡單版不簽 tokenpreset 公用、本不需授權)。
//
// 資料來源local-tool/server/data/models.json7 筆 metadata + nef 檔)。對映到 Model
// struct 既有欄位models.json 有但 Model 無對應欄位的 metadataquantization / license /
// version / author / taskType / categories / thumbnail暫不落地見 B8 回報)。
//
// StorageKey 格式 `models/preset/{id}.nef`:對 download handler 而言只是「preset 識別 +
// 對外 URL 檔名」用途,實際檔案由 PresetAssetsDirapi 層)提供,不經 LocalFS storage root。
package model
import "time"
// presetCreatedAt 是 7 個 preset 的固定建立時間(對齊 models.json 的 createdAt
// 寫死成常數讓 List/Get 回應在重啟 / 多實例間穩定(不用 time.Now(),避免每次不同)。
var presetCreatedAt = time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
// presetStorageKey 依 preset id 組出對外用的 storage key檔名穩定、URL 穩定)。
func presetStorageKey(id string) string {
return "models/preset/" + id + ".nef"
}
// presetModels 是寫死的 7 個系統預設模型(公用、無 owner、不可刪
//
// 欄位對映models.json → Model
// - name/description/framework 直接對映
// - InputShapeinputSize{width,height} → []int{1, 3, height, width}NCHW對齊
// conversion 端與 PG INT[] 既有慣例 [1,3,H,W]。preset 皆 RGB 影像模型batch=1、channel=3。
// - Classeslabels 直接對映
// - TargetChipsupportedHardware[0](每筆只支援單一晶片)
// - FileSizemodelSize
// - Source = SourcePreset、OwnerUserID = ""(公用,非任何 user
// - StorageKey = models/preset/{id}.nef不設 FAAObjectKeydownload 走 visionA 自己)
var presetModels = []*Model{
{
ID: "kl520-yolov5-detection",
Name: "YOLOv5 Detection (KL520)",
Description: "YOLOv5 object detection model compiled for Kneron KL520. No upsample variant optimized for NPU inference at 640x640 resolution.",
StorageKey: presetStorageKey("kl520-yolov5-detection"),
FileSize: 7200000,
TargetChip: "KL520",
InputShape: []int{1, 3, 640, 640},
Classes: []string{"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light"},
Framework: "NEF",
Source: SourcePreset,
CreatedAt: presetCreatedAt,
UpdatedAt: presetCreatedAt,
UploadedAt: &presetCreatedAt,
},
{
ID: "kl520-fcos-detection",
Name: "FCOS Detection (KL520)",
Description: "FCOS (Fully Convolutional One-Stage) object detection with DarkNet53s backbone, compiled for KL520. Anchor-free detection at 512x512.",
StorageKey: presetStorageKey("kl520-fcos-detection"),
FileSize: 8900000,
TargetChip: "KL520",
InputShape: []int{1, 3, 512, 512},
Classes: []string{"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light"},
Framework: "NEF",
Source: SourcePreset,
CreatedAt: presetCreatedAt,
UpdatedAt: presetCreatedAt,
UploadedAt: &presetCreatedAt,
},
{
ID: "kl520-ssd-face-detection",
Name: "SSD Face Detection (KL520)",
Description: "SSD-based face detection with landmark localization, compiled for KL520. Lightweight model suitable for face detection and alignment tasks.",
StorageKey: presetStorageKey("kl520-ssd-face-detection"),
FileSize: 1000000,
TargetChip: "KL520",
InputShape: []int{1, 3, 240, 320},
Classes: []string{"face"},
Framework: "NEF",
Source: SourcePreset,
CreatedAt: presetCreatedAt,
UpdatedAt: presetCreatedAt,
UploadedAt: &presetCreatedAt,
},
{
ID: "kl520-tiny-yolov3",
Name: "Tiny YOLOv3 (KL520)",
Description: "Tiny YOLOv3 object detection model compiled for KL520. Compact and fast model for general-purpose multi-object detection on edge devices.",
StorageKey: presetStorageKey("kl520-tiny-yolov3"),
FileSize: 9400000,
TargetChip: "KL520",
InputShape: []int{1, 3, 416, 416},
Classes: []string{"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light"},
Framework: "NEF",
Source: SourcePreset,
CreatedAt: presetCreatedAt,
UpdatedAt: presetCreatedAt,
UploadedAt: &presetCreatedAt,
},
{
ID: "kl720-yolov5-detection",
Name: "YOLOv5 Detection (KL720)",
Description: "YOLOv5 object detection model compiled for Kneron KL720. No upsample variant optimized for KL720 NPU inference at 640x640 resolution with USB 3.0 throughput.",
StorageKey: presetStorageKey("kl720-yolov5-detection"),
FileSize: 10168348,
TargetChip: "KL720",
InputShape: []int{1, 3, 640, 640},
Classes: []string{"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light"},
Framework: "NEF",
Source: SourcePreset,
CreatedAt: presetCreatedAt,
UpdatedAt: presetCreatedAt,
UploadedAt: &presetCreatedAt,
},
{
ID: "kl720-resnet18-classification",
Name: "ImageNet Classification ResNet18 (KL720)",
Description: "ResNet18-based image classification compiled for KL720. Supports 1000 ImageNet categories with fast inference via USB 3.0.",
StorageKey: presetStorageKey("kl720-resnet18-classification"),
FileSize: 12826804,
TargetChip: "KL720",
InputShape: []int{1, 3, 224, 224},
Classes: []string{"airplane", "automobile", "bird", "cat", "deer", "dog", "frog", "horse", "ship", "truck"},
Framework: "NEF",
Source: SourcePreset,
CreatedAt: presetCreatedAt,
UpdatedAt: presetCreatedAt,
UploadedAt: &presetCreatedAt,
},
{
ID: "kl720-fcos-detection",
Name: "FCOS Detection (KL720)",
Description: "FCOS (Fully Convolutional One-Stage) object detection with DarkNet53s backbone, compiled for KL720. Anchor-free detection at 512x512.",
StorageKey: presetStorageKey("kl720-fcos-detection"),
FileSize: 13004640,
TargetChip: "KL720",
InputShape: []int{1, 3, 512, 512},
Classes: []string{"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light"},
Framework: "NEF",
Source: SourcePreset,
CreatedAt: presetCreatedAt,
UpdatedAt: presetCreatedAt,
UploadedAt: &presetCreatedAt,
},
}
// PresetModels 回傳所有系統預設模型的深拷貝(避免呼叫端意外改到共用常數)。
//
// 回傳順序固定(與宣告順序一致),讓 List 回應穩定、測試可確定性斷言。
func PresetModels() []*Model {
out := make([]*Model, 0, len(presetModels))
for _, m := range presetModels {
out = append(out, clonePreset(m))
}
return out
}
// PresetByID 依 id 取單一 preset 的深拷貝;非 preset id 回 (nil, false)。
func PresetByID(id string) (*Model, bool) {
for _, m := range presetModels {
if m.ID == id {
return clonePreset(m), true
}
}
return nil, false
}
// IsPresetID 判斷 id 是否為系統預設模型 id。
func IsPresetID(id string) bool {
_, ok := PresetByID(id)
return ok
}
// clonePreset 深拷貝一筆 preset含 slice 欄位),避免外部修改污染共用常數。
func clonePreset(m *Model) *Model {
cp := *m
if m.InputShape != nil {
cp.InputShape = append([]int(nil), m.InputShape...)
}
if m.Classes != nil {
cp.Classes = append([]string(nil), m.Classes...)
}
// UploadedAt 指向共用的 presetCreatedAt複製一份新指標避免別名共享。
if m.UploadedAt != nil {
t := *m.UploadedAt
cp.UploadedAt = &t
}
return &cp
}