feat: 标准调试参数 & 克隆不立即保存
1. 新增标准调试参数
- templates/standard_overlays/ 目录下导入 std_face_debug、
std_test_sensitive、std_production_quiet 三个标准调试参数
- 启动时通过 ImportStandardOverlaysFromDir 自动导入 SQLite
- 支持 std_ 前缀只读保护,不可直接编辑/删除
2. 复制后不立即保存
- 调试参数克隆:重定向到新建页预填数据,点击保存才入库
- 识别模板克隆:重定向到模板编辑页预填,点击创建/进入可视化编辑才入库
- 可视化编辑也支持 clone_source 参数,编辑保存时创建新模板
3. 简化调试参数 JSON 结构
- 去掉 instance_overrides 层级,改为顶层 override 键
- 运行时渲染兼容新旧两种格式
- UI 去掉目标/目标数量字段
4. 清理死代码
- 移除从零新建空白调试参数/模板的代码路径
This commit is contained in:
parent
7386969c27
commit
ce18c85eba
@ -2,10 +2,12 @@
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build & Test Commands
|
||||
## Build, Test & Restart Commands
|
||||
|
||||
- Build: `go build -o managerd ./cmd/managerd`
|
||||
- Build: `go build -o managerd ./cmd/managerd` 或 `pwsh -File scripts/managerd.ps1 build`
|
||||
- Run: `./managerd` (uses `managerd.json` config) or `./managerd <config-path>`
|
||||
- 重启服务(编译+启停+健康检查一站式):`pwsh -File scripts/managerd.ps1 restart`
|
||||
- 查看服务状态:`pwsh -File scripts/managerd.ps1 status`
|
||||
- Test all: `go test ./...`
|
||||
- Test single package: `go test ./internal/service/...`
|
||||
- Test single test: `go test ./internal/storage/... -run TestAssetsRepo_SaveTemplate`
|
||||
@ -92,3 +94,4 @@ The config preview rendering pipeline: merge template + profile + overlays → r
|
||||
- **No `fetch()` in templates** for page data — all API calls must go through Go server-side handlers. JS is only for UI interactivity (modals, carousels, confirm dialogs).
|
||||
- **No inline `onclick` with complex quoting** — use `data-*` attributes + event delegation instead.
|
||||
- **Use `pwsh`** for PowerShell commands (v7.6.1 in PATH, not `powershell.exe`).
|
||||
- **重启服务**:统一用 `pwsh -File scripts/managerd.ps1 restart`,它会先编译、停旧进程、再启动新进程并做健康检查。不要手动 kill + build + start。
|
||||
|
||||
@ -47,6 +47,11 @@ func main() {
|
||||
} else if imported > 0 {
|
||||
log.Printf("imported %d standard templates", imported)
|
||||
}
|
||||
if imported, err := service.ImportStandardOverlaysFromDir(assetsRepo, filepath.Join("templates", "standard_overlays")); err != nil {
|
||||
log.Fatalf("import standard overlays: %v", err)
|
||||
} else if imported > 0 {
|
||||
log.Printf("imported %d standard overlays", imported)
|
||||
}
|
||||
standardModelsDir := filepath.Join("models", "standard_models")
|
||||
modelSvc := service.NewModelManagementService(modelsRepo)
|
||||
if err := modelSvc.SyncStandardModelsFromDirectory(standardModelsDir); err != nil {
|
||||
|
||||
@ -65,6 +65,7 @@ type ConfigOverlayAsset struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Description string `json:"description"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
OverrideTargets []string `json:"override_targets"`
|
||||
OverrideTargetNum int `json:"override_target_num"`
|
||||
Raw map[string]any `json:"raw"`
|
||||
@ -677,6 +678,12 @@ func (s *ConfigPreviewService) ListOverlayAssets() ([]ConfigOverlayAsset, error)
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
if items[i].ReadOnly != items[j].ReadOnly {
|
||||
return items[i].ReadOnly
|
||||
}
|
||||
return items[i].Name < items[j].Name
|
||||
})
|
||||
return items, nil
|
||||
}
|
||||
|
||||
@ -696,6 +703,7 @@ func (s *ConfigPreviewService) GetOverlayAsset(name string) (*ConfigOverlayAsset
|
||||
Name: name,
|
||||
Path: path,
|
||||
Description: stringValue(raw["description"]),
|
||||
ReadOnly: isStandardTemplateName(name),
|
||||
OverrideTargets: targets,
|
||||
OverrideTargetNum: len(targets),
|
||||
Raw: raw,
|
||||
@ -710,6 +718,9 @@ func (s *ConfigPreviewService) SaveOverlayAsset(asset ConfigOverlayAsset, raw ma
|
||||
if err := validateConfigName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
if isStandardTemplateName(name) {
|
||||
return fmt.Errorf("标准调试参数 %q 为只读,请先复制后再编辑", name)
|
||||
}
|
||||
if raw == nil {
|
||||
raw = map[string]any{}
|
||||
}
|
||||
@ -729,6 +740,9 @@ func (s *ConfigPreviewService) DeleteOverlayAsset(name string) error {
|
||||
if err := validateConfigName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
if isStandardTemplateName(name) {
|
||||
return fmt.Errorf("标准调试参数 %q 为只读,无法删除", name)
|
||||
}
|
||||
refs, err := s.profileNamesReferencingOverlay(name)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -292,6 +292,19 @@ func applyRuntimeOverlay(root map[string]any, overlay map[string]any) (map[strin
|
||||
out[key] = deepMergeAny(out[key], value)
|
||||
}
|
||||
}
|
||||
if rawOverride, ok := overlay["override"]; ok {
|
||||
if override, _ := rawOverride.(map[string]any); override != nil {
|
||||
instances, _ := out["instances"].([]any)
|
||||
mergedInstances := make([]any, 0, len(instances))
|
||||
for _, item := range instances {
|
||||
instance, _ := item.(map[string]any)
|
||||
merged := deepCopyMap(instance)
|
||||
merged = mergeRuntimeInstancePatch(merged, override)
|
||||
mergedInstances = append(mergedInstances, merged)
|
||||
}
|
||||
out["instances"] = mergedInstances
|
||||
}
|
||||
}
|
||||
if rawPatches, ok := overlay["instance_overrides"]; ok {
|
||||
patches, _ := rawPatches.(map[string]any)
|
||||
if patches == nil {
|
||||
|
||||
@ -75,3 +75,69 @@ func ImportStandardTemplatesFromDir(repo *storage.AssetsRepo, dir string) (int,
|
||||
}
|
||||
return imported, nil
|
||||
}
|
||||
|
||||
func ImportStandardOverlaysFromDir(repo *storage.AssetsRepo, dir string) (int, error) {
|
||||
if repo == nil {
|
||||
return 0, fmt.Errorf("asset repository is not configured")
|
||||
}
|
||||
dir = filepath.Clean(strings.TrimSpace(dir))
|
||||
if dir == "" {
|
||||
return 0, fmt.Errorf("standard overlay dir is empty")
|
||||
}
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
imported := 0
|
||||
for _, file := range files {
|
||||
if file.IsDir() || strings.ToLower(filepath.Ext(file.Name())) != ".json" {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(dir, file.Name())
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return imported, err
|
||||
}
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal(body, &raw); err != nil {
|
||||
return imported, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
if raw == nil {
|
||||
raw = map[string]any{}
|
||||
}
|
||||
name := strings.TrimSpace(firstString(raw["name"], strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))))
|
||||
if name == "" {
|
||||
return imported, fmt.Errorf("%s: overlay name is empty", path)
|
||||
}
|
||||
if err := validateConfigName(name); err != nil {
|
||||
return imported, fmt.Errorf("%s: invalid overlay name %q: %w", path, name, err)
|
||||
}
|
||||
if !isStandardTemplateName(name) {
|
||||
return imported, fmt.Errorf("%s: standard overlay name must start with std_", path)
|
||||
}
|
||||
description := strings.TrimSpace(stringValue(raw["description"]))
|
||||
delete(raw, "name")
|
||||
delete(raw, "description")
|
||||
body, err = marshalConfigJSON(raw)
|
||||
if err != nil {
|
||||
return imported, err
|
||||
}
|
||||
existing, err := repo.GetOverlay(name)
|
||||
if err != nil {
|
||||
return imported, err
|
||||
}
|
||||
if existing != nil &&
|
||||
strings.TrimSpace(existing.Description) == description &&
|
||||
strings.TrimSpace(existing.BodyJSON) == strings.TrimSpace(string(body)) {
|
||||
continue
|
||||
}
|
||||
if err := repo.SaveOverlay(name, description, string(body)); err != nil {
|
||||
return imported, err
|
||||
}
|
||||
imported++
|
||||
}
|
||||
return imported, nil
|
||||
}
|
||||
|
||||
@ -127,6 +127,8 @@ type PageData struct {
|
||||
TemplateDraftName string
|
||||
TemplateDraftDescription string
|
||||
TemplateCloneSource string
|
||||
TemplateCloneName string
|
||||
TemplateCloneDesc string
|
||||
TemplateCreateMode string
|
||||
OverlayDraftJSON string
|
||||
AuditEntries []storage.AuditLogRecord
|
||||
@ -617,6 +619,7 @@ func (u *UI) Routes() (chi.Router, error) {
|
||||
r.Post("/assets/integrations/{name}/delete", u.actionAssetIntegrationDelete)
|
||||
r.Get("/assets/overlays", u.pageAssetOverlays)
|
||||
r.Post("/assets/overlays", u.actionAssetOverlaySave)
|
||||
r.Post("/assets/overlays/{name}/clone", u.actionAssetOverlayClone)
|
||||
r.Post("/assets/overlays/{name}/delete", u.actionAssetOverlayDelete)
|
||||
r.Get("/assets/overlays/{name}", u.pageAssetOverlay)
|
||||
r.Get("/assets/overlays/{name}/export", u.pageAssetOverlayExport)
|
||||
@ -1586,6 +1589,9 @@ func (u *UI) pageAssetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
data.Error = strings.TrimSpace(r.URL.Query().Get("error"))
|
||||
}
|
||||
editMode := strings.TrimSpace(r.URL.Query().Get("edit")) == "1"
|
||||
cloneSource := strings.TrimSpace(r.URL.Query().Get("clone_source"))
|
||||
cloneName := strings.TrimSpace(r.URL.Query().Get("clone_name"))
|
||||
cloneDesc := strings.TrimSpace(r.URL.Query().Get("clone_desc"))
|
||||
if name := strings.TrimSpace(r.URL.Query().Get("name")); name != "" {
|
||||
if item, err := u.preview.GetTemplateAsset(name); err == nil {
|
||||
data.AssetTemplate = item
|
||||
@ -1599,7 +1605,30 @@ func (u *UI) pageAssetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
data.Error = err.Error()
|
||||
}
|
||||
}
|
||||
data.AssetTemplateEditing = editMode && data.AssetTemplate != nil && !data.AssetTemplate.ReadOnly
|
||||
if cloneSource != "" && cloneName != "" {
|
||||
data.TemplateCloneSource = cloneSource
|
||||
data.TemplateCloneName = cloneName
|
||||
data.TemplateCloneDesc = cloneDesc
|
||||
if source, err := u.preview.GetTemplateAsset(cloneSource); err == nil {
|
||||
body, _ := json.Marshal(source.Raw)
|
||||
doc := map[string]any{}
|
||||
if json.Unmarshal(body, &doc) == nil {
|
||||
desc := cloneDesc
|
||||
if desc == "" {
|
||||
desc = source.Description
|
||||
}
|
||||
doc["name"] = cloneName
|
||||
doc["description"] = desc
|
||||
data.AssetTemplate = &service.ConfigTemplateAsset{
|
||||
Name: cloneName,
|
||||
Description: desc,
|
||||
Raw: doc,
|
||||
ReadOnly: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
data.AssetTemplateEditing = (editMode || cloneSource != "") && data.AssetTemplate != nil && !data.AssetTemplate.ReadOnly
|
||||
u.render(w, r, "asset_templates", data)
|
||||
}
|
||||
|
||||
@ -1617,37 +1646,29 @@ func (u *UI) actionAssetTemplateCreate(w http.ResponseWriter, r *http.Request) {
|
||||
description := strings.TrimSpace(r.FormValue("description"))
|
||||
cloneSource := strings.TrimSpace(r.FormValue("clone_source"))
|
||||
|
||||
if cloneSource == "" {
|
||||
http.Error(w, "create template requires clone_source", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var doc map[string]any
|
||||
if cloneSource != "" {
|
||||
item, err := u.preview.GetTemplateAsset(cloneSource)
|
||||
if err != nil || item == nil {
|
||||
http.Error(w, "clone source template not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body, err := json.Marshal(item.Raw)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(body, &doc); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
doc = map[string]any{
|
||||
"name": name,
|
||||
"description": description,
|
||||
"params": map[string]any{},
|
||||
"template": map[string]any{
|
||||
"nodes": []any{},
|
||||
"edges": []any{},
|
||||
},
|
||||
}
|
||||
item, err := u.preview.GetTemplateAsset(cloneSource)
|
||||
if err != nil || item == nil {
|
||||
http.Error(w, "clone source template not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body, err := json.Marshal(item.Raw)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(body, &doc); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
doc["name"] = name
|
||||
doc["description"] = description
|
||||
body, err := json.MarshalIndent(doc, "", " ")
|
||||
body, err = json.MarshalIndent(doc, "", " ")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@ -1660,7 +1681,7 @@ func (u *UI) actionAssetTemplateCreate(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), status)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/ui/assets/templates/"+url.PathEscape(name)+"/graph?msg="+urlQueryEscape("模板已创建"), http.StatusFound)
|
||||
http.Redirect(w, r, "/ui/assets/templates?name="+url.PathEscape(name)+"&edit=1&msg="+urlQueryEscape("模板已创建"), http.StatusFound)
|
||||
}
|
||||
|
||||
func (u *UI) actionAssetTemplateClone(w http.ResponseWriter, r *http.Request) {
|
||||
@ -1683,28 +1704,10 @@ func (u *UI) actionAssetTemplateClone(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/ui/assets/templates?name="+url.PathEscape(sourceName)+"&error="+urlQueryEscape(err.Error()), http.StatusFound)
|
||||
return
|
||||
}
|
||||
body, err := json.Marshal(item.Raw)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
doc := map[string]any{}
|
||||
if err := json.Unmarshal(body, &doc); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
doc["name"] = targetName
|
||||
doc["description"] = item.Description
|
||||
pretty, err := json.MarshalIndent(doc, "", " ")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := u.preview.SaveTemplateAsset(targetName, item.Description, string(pretty)+"\n"); err != nil {
|
||||
http.Redirect(w, r, "/ui/assets/templates?name="+url.PathEscape(sourceName)+"&error="+urlQueryEscape(err.Error()), http.StatusFound)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/ui/assets/templates?name="+url.PathEscape(targetName)+"&edit=1&msg="+urlQueryEscape("标准模板已复制,请继续编辑"), http.StatusFound)
|
||||
cloneSource := url.QueryEscape(sourceName)
|
||||
cloneName := url.QueryEscape(targetName)
|
||||
cloneDesc := url.QueryEscape(item.Description)
|
||||
http.Redirect(w, r, "/ui/assets/templates?clone_source="+cloneSource+"&clone_name="+cloneName+"&clone_desc="+cloneDesc, http.StatusFound)
|
||||
}
|
||||
|
||||
func (u *UI) nextTemplateCloneName(sourceName string) (string, error) {
|
||||
@ -1747,6 +1750,7 @@ func (u *UI) pageAssetTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (u *UI) pageAssetTemplateGraph(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
cloneSource := strings.TrimSpace(r.URL.Query().Get("clone_source"))
|
||||
data := u.assetPageData("templates")
|
||||
data.Message = strings.TrimSpace(r.URL.Query().Get("msg"))
|
||||
if data.Error == "" {
|
||||
@ -1757,17 +1761,41 @@ func (u *UI) pageAssetTemplateGraph(w http.ResponseWriter, r *http.Request) {
|
||||
u.render(w, r, "asset_templates", data)
|
||||
return
|
||||
}
|
||||
item, err := u.preview.GetTemplateAsset(name)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
if cloneSource != "" {
|
||||
source, err := u.preview.GetTemplateAsset(cloneSource)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
body, _ := json.Marshal(source.Raw)
|
||||
doc := map[string]any{}
|
||||
if json.Unmarshal(body, &doc) != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
doc["name"] = name
|
||||
doc["description"] = source.Description
|
||||
data.Title = "模板可视化编辑(从 " + cloneSource + " 克隆)"
|
||||
data.AssetTemplate = &service.ConfigTemplateAsset{
|
||||
Name: name,
|
||||
Description: source.Description,
|
||||
Raw: doc,
|
||||
ReadOnly: false,
|
||||
}
|
||||
data.TemplateCloneSource = cloneSource
|
||||
} else {
|
||||
item, err := u.preview.GetTemplateAsset(name)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
data.Title = "模板可视化编辑"
|
||||
if item.ReadOnly {
|
||||
data.Title = "标准模板可视化预览"
|
||||
}
|
||||
data.AssetTemplate = item
|
||||
}
|
||||
data.Title = "模板可视化编辑"
|
||||
if item.ReadOnly {
|
||||
data.Title = "标准模板可视化预览"
|
||||
}
|
||||
data.AssetTemplate = item
|
||||
raw, err := compactJSON(item.Raw)
|
||||
raw, err := compactJSON(data.AssetTemplate.Raw)
|
||||
if err != nil {
|
||||
data.Error = err.Error()
|
||||
u.render(w, r, "asset_templates", data)
|
||||
@ -1779,6 +1807,7 @@ func (u *UI) pageAssetTemplateGraph(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (u *UI) actionAssetTemplateGraphSave(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
cloneSource := strings.TrimSpace(r.FormValue("clone_source"))
|
||||
if u.preview == nil {
|
||||
http.Error(w, "config preview service is not configured", http.StatusInternalServerError)
|
||||
return
|
||||
@ -1817,8 +1846,12 @@ func (u *UI) actionAssetTemplateGraphSave(w http.ResponseWriter, r *http.Request
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if targetName != name {
|
||||
err = u.preview.RenameTemplateAsset(name, targetName, description, string(body)+"\n")
|
||||
if cloneSource != "" || targetName != name {
|
||||
if cloneSource != "" {
|
||||
err = u.preview.SaveTemplateAsset(targetName, description, string(body)+"\n")
|
||||
} else {
|
||||
err = u.preview.RenameTemplateAsset(name, targetName, description, string(body)+"\n")
|
||||
}
|
||||
} else {
|
||||
err = u.preview.SaveTemplateAsset(name, description, string(body)+"\n")
|
||||
}
|
||||
@ -2497,7 +2530,9 @@ func (u *UI) pageAssetOverlays(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
newMode := strings.TrimSpace(r.URL.Query().Get("new")) == "1"
|
||||
editMode := strings.TrimSpace(r.URL.Query().Get("edit")) == "1"
|
||||
if name := strings.TrimSpace(r.URL.Query().Get("name")); name != "" {
|
||||
cloneSource := strings.TrimSpace(r.URL.Query().Get("clone_source"))
|
||||
cloneName := strings.TrimSpace(r.URL.Query().Get("clone_name"))
|
||||
if name := strings.TrimSpace(r.URL.Query().Get("name")); name != "" && !newMode {
|
||||
if item, err := u.preview.GetOverlayAsset(name); err == nil {
|
||||
data.AssetOverlay = item
|
||||
} else if data.Error == "" {
|
||||
@ -2510,19 +2545,31 @@ func (u *UI) pageAssetOverlays(w http.ResponseWriter, r *http.Request) {
|
||||
data.Error = err.Error()
|
||||
}
|
||||
}
|
||||
if newMode {
|
||||
data.AssetOverlay = &service.ConfigOverlayAsset{
|
||||
Name: "",
|
||||
Description: "",
|
||||
Raw: map[string]any{
|
||||
"name": "",
|
||||
"description": "",
|
||||
"instance_overrides": map[string]any{"*": map[string]any{"override": map[string]any{}}},
|
||||
},
|
||||
if newMode && cloneSource != "" {
|
||||
if item, err := u.preview.GetOverlayAsset(cloneSource); err == nil {
|
||||
rawJSON, err := json.Marshal(item.Raw)
|
||||
if err == nil {
|
||||
doc := map[string]any{}
|
||||
if err := json.Unmarshal(rawJSON, &doc); err == nil {
|
||||
doc["name"] = cloneName
|
||||
doc["description"] = item.Description
|
||||
data.AssetOverlay = &service.ConfigOverlayAsset{
|
||||
Name: cloneName,
|
||||
Description: item.Description,
|
||||
Raw: doc,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
data.AssetOverlayEditing = true
|
||||
if data.AssetOverlay == nil {
|
||||
newMode = false
|
||||
data.Error = "无法加载源调试参数"
|
||||
}
|
||||
}
|
||||
if newMode {
|
||||
data.AssetOverlayEditing = data.AssetOverlay != nil
|
||||
} else {
|
||||
data.AssetOverlayEditing = editMode && data.AssetOverlay != nil
|
||||
data.AssetOverlayEditing = editMode && data.AssetOverlay != nil && !data.AssetOverlay.ReadOnly
|
||||
}
|
||||
if data.AssetOverlay != nil {
|
||||
rawJSON, err := compactJSON(data.AssetOverlay.Raw)
|
||||
@ -2586,6 +2633,53 @@ func (u *UI) actionAssetOverlaySave(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/ui/assets/overlays?msg="+urlQueryEscape("调试参数已保存")+"&name="+url.PathEscape(name), http.StatusFound)
|
||||
}
|
||||
|
||||
func (u *UI) actionAssetOverlayClone(w http.ResponseWriter, r *http.Request) {
|
||||
if u.preview == nil {
|
||||
http.Error(w, "config preview service is not configured", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
sourceName := chi.URLParam(r, "name")
|
||||
item, err := u.preview.GetOverlayAsset(sourceName)
|
||||
if err != nil || item == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if !item.ReadOnly {
|
||||
http.Redirect(w, r, "/ui/assets/overlays?name="+url.PathEscape(sourceName)+"&error="+urlQueryEscape("只允许从标准调试参数复制创建"), http.StatusFound)
|
||||
return
|
||||
}
|
||||
targetName, err := u.nextOverlayCloneName(item.Name)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/ui/assets/overlays?name="+url.PathEscape(sourceName)+"&error="+urlQueryEscape(err.Error()), http.StatusFound)
|
||||
return
|
||||
}
|
||||
cloneSource := url.QueryEscape(sourceName)
|
||||
cloneName := url.QueryEscape(targetName)
|
||||
http.Redirect(w, r, "/ui/assets/overlays?new=1&clone_source="+cloneSource+"&clone_name="+cloneName, http.StatusFound)
|
||||
}
|
||||
|
||||
func (u *UI) nextOverlayCloneName(sourceName string) (string, error) {
|
||||
base := strings.TrimSpace(sourceName)
|
||||
if base == "" {
|
||||
return "", fmt.Errorf("调试参数名称不能为空")
|
||||
}
|
||||
if strings.HasPrefix(base, "std_") {
|
||||
base = strings.TrimPrefix(base, "std_")
|
||||
}
|
||||
candidate := base + "_copy"
|
||||
if item, err := u.preview.GetOverlayAsset(candidate); err == nil && item != nil {
|
||||
for i := 2; i < 1000; i++ {
|
||||
name := fmt.Sprintf("%s_copy_%d", base, i)
|
||||
item, err := u.preview.GetOverlayAsset(name)
|
||||
if err != nil || item == nil {
|
||||
return name, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("无法生成可用的调试参数副本名称")
|
||||
}
|
||||
return candidate, nil
|
||||
}
|
||||
|
||||
func (u *UI) actionAssetOverlayDelete(w http.ResponseWriter, r *http.Request) {
|
||||
if u.preview == nil {
|
||||
http.Error(w, "config preview service is not configured", http.StatusInternalServerError)
|
||||
|
||||
@ -4,14 +4,20 @@
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2 class="title-with-icon">{{icon "overlay"}}<span>调试参数列表</span></h2>
|
||||
<div class="form-hint">标准调试参数应作为基线,复制后再做定制化修改。</div>
|
||||
</div>
|
||||
<div class="actions compact">
|
||||
<a class="btn secondary" href="/ui/assets/overlays?new=1">{{icon "apply"}}<span>新增调试参数</span></a>
|
||||
{{if .AssetOverlay}}
|
||||
<a class="btn secondary" href="/ui/assets/overlays?name={{.AssetOverlay.Name}}&edit=1">{{icon "edit"}}<span>编辑</span></a>
|
||||
<form method="post" action="/ui/assets/overlays/{{.AssetOverlay.Name}}/delete">
|
||||
<button class="btn secondary" type="submit">删除</button>
|
||||
</form>
|
||||
{{if .AssetOverlay.ReadOnly}}
|
||||
<form method="post" action="/ui/assets/overlays/{{.AssetOverlay.Name}}/clone">
|
||||
<button type="submit" class="btn secondary">{{icon "add"}}<span>复制标准调试参数</span></button>
|
||||
</form>
|
||||
{{else}}
|
||||
<a class="btn secondary" href="/ui/assets/overlays?name={{.AssetOverlay.Name}}&edit=1">{{icon "edit"}}<span>编辑</span></a>
|
||||
<form method="post" action="/ui/assets/overlays/{{.AssetOverlay.Name}}/delete" onsubmit="return confirm('确认删除这个调试参数吗?');">
|
||||
<button type="submit" class="btn secondary">删除</button>
|
||||
</form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
@ -21,7 +27,7 @@
|
||||
<tr>
|
||||
<th>调试参数</th>
|
||||
<th>描述</th>
|
||||
<th>目标</th>
|
||||
<th>来源</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -29,7 +35,7 @@
|
||||
<tr data-nav-row data-nav-href="/ui/assets/overlays?name={{.Name}}"{{if and $.AssetOverlay (eq $.AssetOverlay.Name .Name)}} class="selected"{{end}}>
|
||||
<td><a class="mono" href="/ui/assets/overlays?name={{.Name}}">{{.Name}}</a></td>
|
||||
<td>{{if .Description}}{{.Description}}{{else}}-{{end}}</td>
|
||||
<td>{{if .OverrideTargets}}{{range $i, $item := .OverrideTargets}}{{if $i}}, {{end}}{{$item}}{{end}}{{else}}-{{end}}</td>
|
||||
<td>{{if .ReadOnly}}标准参数{{else}}用户参数{{end}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="3"><div class="empty-state compact"><div class="empty-title">还没有调试参数</div></div></td></tr>
|
||||
@ -40,7 +46,6 @@
|
||||
</div>
|
||||
|
||||
{{if .AssetOverlay}}
|
||||
<form method="post" action="/ui/assets/overlays">
|
||||
<div class="card editor-state {{if .AssetOverlayEditing}}editing{{else}}readonly{{end}}">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
@ -65,34 +70,39 @@
|
||||
</div>
|
||||
</div>
|
||||
{{if .AssetOverlayEditing}}
|
||||
<div class="field-grid">
|
||||
<label><span>调试参数名称<span class="required-mark">*</span></span><input name="name" value="{{.AssetOverlay.Name}}" autofocus /></label>
|
||||
<label><span>目标数量</span><input value="{{.AssetOverlay.OverrideTargetNum}}" readonly /></label>
|
||||
<label class="full"><span>描述</span><input name="description" value="{{.AssetOverlay.Description}}" /></label>
|
||||
<label class="full"><span>调试参数 JSON<span class="required-mark">*</span></span><textarea class="code-input" name="json">{{.OverlayDraftJSON}}</textarea></label>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn secondary">{{icon "apply"}}<span>保存</span></button>
|
||||
<a class="btn secondary" href="/ui/assets/overlays{{if .AssetOverlay.Name}}?name={{.AssetOverlay.Name}}{{end}}">{{icon "close"}}<span>取消</span></a>
|
||||
</div>
|
||||
<form method="post" action="/ui/assets/overlays">
|
||||
<div class="field-grid">
|
||||
<label><span>调试参数名称<span class="required-mark">*</span></span><input name="name" value="{{.AssetOverlay.Name}}" autofocus /></label>
|
||||
<label class="full"><span>描述</span><input name="description" value="{{.AssetOverlay.Description}}" /></label>
|
||||
<label class="full"><span>调试参数 JSON<span class="required-mark">*</span></span><textarea class="code-input" name="json">{{.OverlayDraftJSON}}</textarea></label>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn secondary">{{icon "apply"}}<span>保存</span></button>
|
||||
<a class="btn secondary" href="/ui/assets/overlays">{{icon "close"}}<span>取消</span></a>
|
||||
</div>
|
||||
</form>
|
||||
{{else}}
|
||||
<div class="detail-sheet">
|
||||
<div class="detail-item"><span>调试参数</span><strong class="mono">{{.AssetOverlay.Name}}</strong></div>
|
||||
<div class="detail-item"><span>目标数量</span><strong>{{.AssetOverlay.OverrideTargetNum}}</strong></div>
|
||||
<div class="detail-item"><span>参数类型</span><strong>{{if .AssetOverlay.ReadOnly}}标准参数(只读){{else}}用户参数{{end}}</strong></div>
|
||||
<div class="detail-item full"><span>描述</span><strong>{{if .AssetOverlay.Description}}{{.AssetOverlay.Description}}{{else}}-{{end}}</strong></div>
|
||||
<div class="detail-item full"><span>作用目标</span><strong>{{if .AssetOverlay.OverrideTargets}}{{range $i, $item := .AssetOverlay.OverrideTargets}}{{if $i}}, {{end}}{{$item}}{{end}}{{else}}-{{end}}</strong></div>
|
||||
<div class="detail-item full"><span>路径</span><strong class="mono">{{.AssetOverlay.Path}}</strong></div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if not .AssetOverlayEditing}}
|
||||
|
||||
{{if and (not .AssetOverlayEditing) .AssetOverlay.ReadOnly}}
|
||||
<div class="card">
|
||||
<div class="form-hint">标准调试参数保留在模板目录中,作为只读基线使用。需要调整参数时,请先复制为用户参数,再进行编辑保存。</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if not .AssetOverlayEditing}}
|
||||
<details class="card collapsible">
|
||||
<summary class="title-with-icon">{{icon "tech"}}<span>原始 JSON</span></summary>
|
||||
<pre>{{json .AssetOverlay.Raw}}</pre>
|
||||
</details>
|
||||
{{end}}
|
||||
</form>
|
||||
{{end}}
|
||||
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
|
||||
{{template "asset_tabs_end" .}}
|
||||
|
||||
@ -10,13 +10,14 @@
|
||||
{{if not .AssetTemplate.ReadOnly}}
|
||||
<form class="graph-save-form" method="post" action="/ui/assets/templates/{{.AssetTemplate.Name}}/graph">
|
||||
<input type="hidden" name="json" />
|
||||
{{if .TemplateCloneSource}}<input type="hidden" name="clone_source" value="{{.TemplateCloneSource}}" />{{end}}
|
||||
<button type="submit" class="primary">{{icon "apply"}}<span>保存模板</span></button>
|
||||
</form>
|
||||
{{end}}
|
||||
{{if .AssetTemplate.ReadOnly}}
|
||||
<a class="btn secondary" href="/ui/assets/templates?clone={{.AssetTemplate.Name}}">{{icon "add"}}<span>复制后编辑</span></a>
|
||||
{{end}}
|
||||
<a class="btn ghost" href="/ui/assets/templates?name={{.AssetTemplate.Name}}">返回模板</a>
|
||||
<a class="btn ghost" href="/ui/assets/templates{{if .TemplateCloneSource}}?clone_source={{.TemplateCloneSource}}&clone_name={{.AssetTemplate.Name}}{{else}}?name={{.AssetTemplate.Name}}{{end}}">返回模板</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -8,7 +8,8 @@
|
||||
</div>
|
||||
<div class="actions compact">
|
||||
{{if .AssetTemplate}}
|
||||
{{if .AssetTemplate.ReadOnly}}
|
||||
{{if .TemplateCloneSource}}
|
||||
{{else if .AssetTemplate.ReadOnly}}
|
||||
<form method="post" action="/ui/assets/templates/{{.AssetTemplate.Name}}/clone">
|
||||
<button type="submit" class="btn secondary">{{icon "add"}}<span>复制标准模板</span></button>
|
||||
</form>
|
||||
@ -51,11 +52,11 @@
|
||||
<div class="card editor-state {{if .AssetTemplateEditing}}editing{{else}}readonly{{end}}">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2 class="title-with-icon">{{icon "template"}}<span>{{if .AssetTemplateEditing}}模板编辑{{else}}模板详情{{end}}{{if .AssetTemplate.Name}} · {{.AssetTemplate.Name}}{{end}}</span></h2>
|
||||
<h2 class="title-with-icon">{{icon "template"}}<span>{{if .TemplateCloneSource}}从标准模板克隆{{else if .AssetTemplateEditing}}模板编辑{{else}}模板详情{{end}}{{if .AssetTemplate.Name}} · {{.AssetTemplate.Name}}{{end}}</span></h2>
|
||||
<div class="form-hint form-state-hint">
|
||||
{{if .AssetTemplateEditing}}
|
||||
<span class="pill run">编辑模式</span>
|
||||
<span>当前模板基础信息正在编辑,结构修改请进入可视化编辑。</span>
|
||||
<span>{{if .TemplateCloneSource}}当前为标准模板克隆预览,保存后将创建为用户模板。{{else}}当前模板基础信息正在编辑,结构修改请进入可视化编辑。{{end}}</span>
|
||||
{{else}}
|
||||
<span class="pill">查看模式</span>
|
||||
<span>当前模板为只读详情展示。</span>
|
||||
@ -63,22 +64,28 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions compact">
|
||||
<a class="btn secondary" href="/ui/assets/templates/{{.AssetTemplate.Name}}/graph">{{icon "assets"}}<span>{{if .AssetTemplate.ReadOnly}}可视化预览{{else}}可视化编辑{{end}}</span></a>
|
||||
<a class="btn secondary" href="/ui/assets/templates/{{.AssetTemplate.Name}}/graph{{if .TemplateCloneSource}}?clone_source={{.TemplateCloneSource}}{{end}}">{{icon "assets"}}<span>{{if .AssetTemplate.ReadOnly}}可视化预览{{else if .TemplateCloneSource}}进入可视化编辑{{else}}可视化编辑{{end}}</span></a>
|
||||
{{if not .TemplateCloneSource}}
|
||||
<button
|
||||
type="button"
|
||||
class="btn secondary js-export-json"
|
||||
data-export-url="/ui/assets/templates/{{.AssetTemplate.Name}}/export"
|
||||
data-default-filename="{{.AssetTemplate.Name}}.json"
|
||||
>{{icon "apply"}}<span>导出为 JSON</span></button>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{if .AssetTemplateEditing}}
|
||||
<form method="post" action="/ui/assets/templates/{{.AssetTemplate.Name}}/rename">
|
||||
<form method="post" action="{{if .TemplateCloneSource}}/ui/assets/templates/create{{else}}/ui/assets/templates/{{.AssetTemplate.Name}}/rename{{end}}">
|
||||
<div class="field-grid">
|
||||
<label><span>模板名称<span class="required-mark">*</span></span><input name="name" value="{{.AssetTemplate.Name}}" class="mono" autofocus /></label>
|
||||
<label><span>模板类型</span><input value="{{if .AssetTemplate.ReadOnly}}标准模板(只读){{else}}用户模板{{end}}" readonly /></label>
|
||||
<label class="full"><span>模板描述</span><input name="description" value="{{.AssetTemplate.Description}}" /></label>
|
||||
</div>
|
||||
{{if .TemplateCloneSource}}
|
||||
<input type="hidden" name="clone_source" value="{{.TemplateCloneSource}}" />
|
||||
{{end}}
|
||||
{{if not .TemplateCloneSource}}
|
||||
<div class="detail-sheet">
|
||||
<div class="detail-item"><span>来源文件</span><strong class="mono">{{if .AssetTemplate.Source}}{{.AssetTemplate.Source}}{{else}}-{{end}}</strong></div>
|
||||
<div class="detail-item"><span>节点数</span><strong>{{.AssetTemplate.NodeCount}}</strong></div>
|
||||
@ -88,9 +95,10 @@
|
||||
<div class="detail-item"><span>输出槽位</span><strong>{{if .AssetTemplate.Slots.Outputs}}{{len .AssetTemplate.Slots.Outputs}}{{else}}0{{end}}</strong></div>
|
||||
<div class="detail-item full"><span>路径</span><strong class="mono">{{.AssetTemplate.Path}}</strong></div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn secondary">{{icon "apply"}}<span>保存</span></button>
|
||||
<a class="btn secondary" href="/ui/assets/templates?name={{.AssetTemplate.Name}}">{{icon "close"}}<span>取消</span></a>
|
||||
<button type="submit" class="btn secondary">{{icon "apply"}}<span>{{if .TemplateCloneSource}}创建模板{{else}}保存{{end}}</span></button>
|
||||
<a class="btn secondary" href="/ui/assets/templates">{{icon "close"}}<span>取消</span></a>
|
||||
</div>
|
||||
</form>
|
||||
{{else}}
|
||||
@ -115,7 +123,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if or .AssetTemplate.Slots.Inputs .AssetTemplate.Slots.Services .AssetTemplate.Slots.Outputs}}
|
||||
{{if and (not .TemplateCloneSource) (or .AssetTemplate.Slots.Inputs .AssetTemplate.Slots.Services .AssetTemplate.Slots.Outputs)}}
|
||||
<div class="card">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
@ -136,13 +144,13 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .AssetTemplate.ReadOnly}}
|
||||
{{if and (not .TemplateCloneSource) .AssetTemplate.ReadOnly}}
|
||||
<div class="card">
|
||||
<div class="form-hint">标准模板保留在模板目录中,作为只读基线使用。需要调整流程时,请先复制为用户模板,再进入可视化编辑保存。</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .AssetTemplate.AdvancedParams}}
|
||||
{{if and (not .TemplateCloneSource) .AssetTemplate.AdvancedParams}}
|
||||
<details class="card collapsible">
|
||||
<summary class="title-with-icon">{{icon "tech"}}<span>高级设置</span></summary>
|
||||
<pre>{{json .AssetTemplate.AdvancedParams}}</pre>
|
||||
|
||||
@ -4044,7 +4044,7 @@ func TestUI_AssetTemplatePageShowsEditAndDeleteForUserTemplate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUI_ActionAssetTemplateCloneCreatesDefaultCopyAndRedirectsToEdit(t *testing.T) {
|
||||
func TestUI_ActionAssetTemplateCloneRedirectsWithCloneSource(t *testing.T) {
|
||||
ui := newTestUI(t)
|
||||
store, err := storage.OpenSQLite(filepath.Join(t.TempDir(), "app.db"))
|
||||
if err != nil {
|
||||
@ -4068,17 +4068,16 @@ func TestUI_ActionAssetTemplateCloneCreatesDefaultCopyAndRedirectsToEdit(t *test
|
||||
if rr.Code != http.StatusFound {
|
||||
t.Fatalf("expected redirect, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
if got := rr.Header().Get("Location"); !strings.Contains(got, "/ui/assets/templates?name=face_recognition_stream_copy&edit=1") {
|
||||
t.Fatalf("expected edit redirect, got %q", got)
|
||||
wantLocation := "/ui/assets/templates?clone_source=std_face_recognition_stream&clone_name=face_recognition_stream_copy&clone_desc=helmet+template"
|
||||
if got := rr.Header().Get("Location"); got != wantLocation {
|
||||
t.Fatalf("expected redirect to %q, got %q", wantLocation, got)
|
||||
}
|
||||
saved, err := repo.GetTemplate("face_recognition_stream_copy")
|
||||
if err != nil {
|
||||
t.Fatalf("GetTemplate: %v", err)
|
||||
}
|
||||
for _, want := range []string{`"name": "face_recognition_stream_copy"`, `"description": "helmet template"`, `"type": "input_rtsp"`, `"mode": "std"`} {
|
||||
if saved == nil || !strings.Contains(saved.BodyJSON, want) {
|
||||
t.Fatalf("expected cloned template to contain %q, got %#v", want, saved)
|
||||
}
|
||||
if saved != nil {
|
||||
t.Fatal("expected clone template NOT to be saved until form submission")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
20
templates/standard_overlays/std_face_debug.json
Normal file
20
templates/standard_overlays/std_face_debug.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"description": "开启人脸识别详细调试日志和未匹配候选输出,用于联调和测试。",
|
||||
"override": {
|
||||
"nodes": {
|
||||
"recognize_face": {
|
||||
"debug": {
|
||||
"enabled": true,
|
||||
"log_matches": true,
|
||||
"min_log_interval_ms": 0
|
||||
}
|
||||
},
|
||||
"alarm_violation": {
|
||||
"face_debug": {
|
||||
"log_unknown_candidates": true,
|
||||
"unknown_candidate_interval_ms": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
templates/standard_overlays/std_production_quiet.json
Normal file
35
templates/standard_overlays/std_production_quiet.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"description": "关闭所有调试日志和统计输出,适用于正式生产运行。",
|
||||
"override": {
|
||||
"nodes": {
|
||||
"recognize_face": {
|
||||
"debug": {
|
||||
"enabled": false,
|
||||
"log_matches": false
|
||||
}
|
||||
},
|
||||
"alarm_violation": {
|
||||
"face_debug": {
|
||||
"log_unknown_candidates": false
|
||||
}
|
||||
},
|
||||
"detect_person": {
|
||||
"debug": {
|
||||
"stats": false,
|
||||
"detections": false
|
||||
}
|
||||
},
|
||||
"detect_face": {
|
||||
"debug": {
|
||||
"stats": false
|
||||
}
|
||||
},
|
||||
"rule_shoe_association": {
|
||||
"debug": false
|
||||
},
|
||||
"rule_shoe_color": {
|
||||
"debug": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
templates/standard_overlays/std_test_sensitive.json
Normal file
43
templates/standard_overlays/std_test_sensitive.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"description": "降低告警阈值用于测试:缩短冷却时间、降低最小持续时间和命中数,适用于短时验证视频。",
|
||||
"override": {
|
||||
"nodes": {
|
||||
"alarm_violation": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "non_compliant_workshoe",
|
||||
"class_ids": [2],
|
||||
"min_score": 0.1,
|
||||
"min_duration_ms": 100,
|
||||
"min_hits": 1,
|
||||
"hit_window_ms": 5000,
|
||||
"cooldown_ms": 2000
|
||||
}
|
||||
],
|
||||
"face_rules": [
|
||||
{
|
||||
"name": "unknown_face",
|
||||
"type": "unknown",
|
||||
"cooldown_ms": 2000,
|
||||
"min_hits": 1,
|
||||
"hit_window_ms": 5000,
|
||||
"min_face_area_ratio": 0.0001,
|
||||
"min_face_aspect": 0.3,
|
||||
"max_face_aspect": 3.0
|
||||
},
|
||||
{
|
||||
"name": "known_person",
|
||||
"type": "person",
|
||||
"cooldown_ms": 2000,
|
||||
"min_sim": 0.3,
|
||||
"min_hits": 1,
|
||||
"hit_window_ms": 5000,
|
||||
"min_face_area_ratio": 0.0001,
|
||||
"min_face_aspect": 0.3,
|
||||
"max_face_aspect": 3.0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user