安裝包從 8 個 .nef 縮減為 2 個,models.json 保留完整定義以便日後 加回。 - Makefile 加 BUNDLED_NEFS 白名單,三平台共用 copy_bundled_data helper;用 POSIX find/cp 而非 rsync(Windows CI 的 Git Bash 沒有 rsync) - 白名單檔案不存在時 build 直接失敗,並在複製後驗證 models.json 存在且 .nef 數量相符 —— 避免產出「安裝後 0 個模型」卻回報成功 - FCOS Detection (KL520) 改名為「物件辨識」 - Tiny YOLOv3 (KL520) 改名為「人型監測」 (只改 name/description,id 不動以免影響既有設定與紀錄) - Repository 啟動時過濾 .nef 不存在的模型,否則使用者會看到未打包 的模型並在選取後拿到莫名錯誤 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
156 lines
4.2 KiB
Go
156 lines
4.2 KiB
Go
package model
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
)
|
||
|
||
type Repository struct {
|
||
models []Model
|
||
mu sync.RWMutex
|
||
}
|
||
|
||
// NewRepository 載入 models.json 的內建模型目錄。
|
||
//
|
||
// models.json 會列出所有「產品支援」的模型定義,但安裝包不一定會帶上每個
|
||
// 對應的 .nef(見 Makefile 的 BUNDLED_NEFS 白名單)。因此載入後會過濾掉
|
||
// .nef 檔案實際不存在的 model —— 否則使用者會在 UI 看到選不了的模型,選下去
|
||
// 才在 flash 階段拿到 "model file not found" 這種沒頭沒尾的錯誤。
|
||
//
|
||
// 過濾只作用在 models.json 的內建模型。使用者上傳的自訂模型走 Add(),
|
||
// 路徑是絕對路徑且必定存在,不受影響。
|
||
func NewRepository(dataPath string) *Repository {
|
||
r := &Repository{}
|
||
data, err := os.ReadFile(dataPath)
|
||
if err != nil {
|
||
fmt.Printf("Warning: could not load models from %s: %v\n", dataPath, err)
|
||
return r
|
||
}
|
||
var declared []Model
|
||
if err := json.Unmarshal(data, &declared); err != nil {
|
||
fmt.Printf("Warning: could not parse models JSON: %v\n", err)
|
||
return r
|
||
}
|
||
r.models = filterAvailableModels(declared, filepath.Dir(dataPath))
|
||
return r
|
||
}
|
||
|
||
// filterAvailableModels 只保留 .nef 檔案實際存在的 model。
|
||
//
|
||
// dataDir 是 models.json 所在的目錄(即 bundle 內的 data/),models.json 的
|
||
// filePath 以它為基準解析。
|
||
func filterAvailableModels(models []Model, dataDir string) []Model {
|
||
available := make([]Model, 0, len(models))
|
||
for _, m := range models {
|
||
path := resolveBuiltInModelPath(m.FilePath, dataDir)
|
||
// 沒宣告 filePath 的 model 不做檔案檢查(沒有東西可以檢查),保留原行為。
|
||
if path == "" {
|
||
available = append(available, m)
|
||
continue
|
||
}
|
||
if info, err := os.Stat(path); err != nil || info.IsDir() {
|
||
fmt.Printf("[INFO] Skipping model %q (%s): .nef not bundled at %s\n", m.ID, m.Name, path)
|
||
continue
|
||
}
|
||
available = append(available, m)
|
||
}
|
||
return available
|
||
}
|
||
|
||
// resolveBuiltInModelPath 把 models.json 的 filePath 解析成實際的檔案路徑。
|
||
//
|
||
// 規則與 flash.Service.StartFlash 一致:
|
||
// - 絕對路徑 → 原樣使用
|
||
// - "data/nef/..." → 去掉 "data/" 前綴後接在 dataDir 之下
|
||
// (因為 dataDir 本身就是那個 data/ 目錄,不去掉會變成 data/data/nef/...)
|
||
// - 其他相對路徑 → 直接接在 dataDir 之下
|
||
func resolveBuiltInModelPath(filePath, dataDir string) string {
|
||
if filePath == "" {
|
||
return ""
|
||
}
|
||
if filepath.IsAbs(filePath) {
|
||
return filePath
|
||
}
|
||
if strings.HasPrefix(filePath, "data/") || strings.HasPrefix(filePath, "data\\") {
|
||
return filepath.Join(dataDir, filePath[len("data/"):])
|
||
}
|
||
return filepath.Join(dataDir, filePath)
|
||
}
|
||
|
||
func (r *Repository) List(filter ModelFilter) ([]ModelSummary, int) {
|
||
r.mu.RLock()
|
||
defer r.mu.RUnlock()
|
||
|
||
var results []ModelSummary
|
||
for _, m := range r.models {
|
||
if filter.TaskType != "" && m.TaskType != filter.TaskType {
|
||
continue
|
||
}
|
||
if filter.Hardware != "" {
|
||
found := false
|
||
for _, hw := range m.SupportedHardware {
|
||
if hw == filter.Hardware {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
continue
|
||
}
|
||
}
|
||
if filter.Query != "" {
|
||
q := strings.ToLower(filter.Query)
|
||
if !strings.Contains(strings.ToLower(m.Name), q) &&
|
||
!strings.Contains(strings.ToLower(m.Description), q) {
|
||
continue
|
||
}
|
||
}
|
||
results = append(results, m.ToSummary())
|
||
}
|
||
return results, len(results)
|
||
}
|
||
|
||
func (r *Repository) GetByID(id string) (*Model, error) {
|
||
r.mu.RLock()
|
||
defer r.mu.RUnlock()
|
||
|
||
for i := range r.models {
|
||
if r.models[i].ID == id {
|
||
return &r.models[i], nil
|
||
}
|
||
}
|
||
return nil, fmt.Errorf("model not found: %s", id)
|
||
}
|
||
|
||
func (r *Repository) Count() int {
|
||
r.mu.RLock()
|
||
defer r.mu.RUnlock()
|
||
return len(r.models)
|
||
}
|
||
|
||
func (r *Repository) Add(m Model) {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
r.models = append(r.models, m)
|
||
}
|
||
|
||
func (r *Repository) Remove(id string) error {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
|
||
for i := range r.models {
|
||
if r.models[i].ID == id {
|
||
if !r.models[i].IsCustom {
|
||
return fmt.Errorf("cannot delete built-in model: %s", id)
|
||
}
|
||
r.models = append(r.models[:i], r.models[i+1:]...)
|
||
return nil
|
||
}
|
||
}
|
||
return fmt.Errorf("model not found: %s", id)
|
||
}
|