# 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>
190 lines
7.9 KiB
Go
190 lines
7.9 KiB
Go
// presets.go — 7 個系統預設模型(B8)。
|
||
//
|
||
// 設計決策(使用者拍板「簡單版」):
|
||
// - metadata 寫死成 Go 常數,**不進 DB**。理由:preset 是固定公用資料,不該有 owner
|
||
// 隔離、不該能被刪、也無 seed 重複問題(多實例 / 重啟結果一致)。
|
||
// - 對應的 .nef 檔打包進 visionA-backend image(assets/preset-models/{id}.nef),
|
||
// download 走 visionA 自己的 storage handler(簡單版不簽 token,preset 公用、本不需授權)。
|
||
//
|
||
// 資料來源:local-tool/server/data/models.json(7 筆 metadata + nef 檔)。對映到 Model
|
||
// struct 既有欄位;models.json 有但 Model 無對應欄位的 metadata(quantization / license /
|
||
// version / author / taskType / categories / thumbnail)暫不落地(見 B8 回報)。
|
||
//
|
||
// StorageKey 格式 `models/preset/{id}.nef`:對 download handler 而言只是「preset 識別 +
|
||
// 對外 URL 檔名」用途,實際檔案由 PresetAssetsDir(api 層)提供,不經 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 直接對映
|
||
// - InputShape:inputSize{width,height} → []int{1, 3, height, width}(NCHW,對齊
|
||
// conversion 端與 PG INT[] 既有慣例 [1,3,H,W])。preset 皆 RGB 影像模型,batch=1、channel=3。
|
||
// - Classes:labels 直接對映
|
||
// - TargetChip:supportedHardware[0](每筆只支援單一晶片)
|
||
// - FileSize:modelSize
|
||
// - Source = SourcePreset、OwnerUserID = ""(公用,非任何 user)
|
||
// - StorageKey = models/preset/{id}.nef(不設 FAAObjectKey;download 走 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
|
||
}
|