jim800121chen 47a1d4d0ef feat(backend): 設備註冊 + 模型共享 backend(B 設備管理 + C 模型共享)
B 設備管理(feature-device-mgmt-tdd):
- POST /api/devices/:id/register + /unregister(owner 檢查 + representative 擋
  + 已註冊擋 + SetRegistered 單欄翻轉,不碰 unpair 軟刪)
- error codes ALREADY_REGISTERED / REPRESENTATIVE_DEVICE(409)
- 不需 migration(registered_at 欄/index/讀寫已在 0005)

C 模型共享(feature-model-sharing-tdd,security 深審 APPROVE):
- migration 0006:models.visibility enum DEFAULT 'private'(零行為改變)+ model_shares 表
- canAccessModel single source(owner ∪ share ∪ public ∪ tenant):profile + download 共用
- GET /library(cursor keyset)/ GET /:id/profile(404 防列舉、GetWithOwner join name 不洩 email)
  / PATCH /:id/visibility(owner-only)/ shares CRUD / download 放寬
- tenant 因 OIDC 無 org claim 留 stub(恆空、安全預設;補 org claim 需重送 security 深審)

reviewer 通過(B 三條紅線 / C security APPROVE 無 C/M)。130 dbtest 全綠、gosec 新檔 0。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-02 16:29:50 +08:00

193 lines
8.1 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
}
// preset 是公用模型語意等同全平台可見visibility=public
// 在此統一標記preset 宣告區不必逐筆設 Visibility。
cp.Visibility = VisibilityPublic
return &cp
}