package httpapi import ( "encoding/json" "errors" "fmt" "io" "mime" "net/http" "os" "sort" "strings" "time" ) type UIInstance struct { Name string `json:"name"` Template string `json:"template"` Params map[string]any `json:"params,omitempty"` Override map[string]any `json:"override,omitempty"` } type uiRootConfig struct { Global map[string]any `json:"global,omitempty"` Queue map[string]any `json:"queue,omitempty"` Templates map[string]any `json:"templates,omitempty"` Instances []UIInstance `json:"instances,omitempty"` Graphs []map[string]any `json:"graphs,omitempty"` } type uiPlanRequest struct { Global map[string]any `json:"global,omitempty"` Queue map[string]any `json:"queue,omitempty"` Instances []UIInstance `json:"instances"` } type uiDiff struct { Added []string `json:"added"` Removed []string `json:"removed"` Changed []string `json:"changed"` } type uiPlanResponse struct { Ok bool `json:"ok"` GeneratedConfig map[string]any `json:"generated_config"` Diff uiDiff `json:"diff"` Warnings []string `json:"warnings,omitempty"` } func (s *Server) handleConfigUISchema(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { errorJSON(w, http.StatusMethodNotAllowed, "method not allowed") return } if !s.authorize(r, false) { errorJSON(w, http.StatusUnauthorized, "unauthorized") return } writeJSON(w, http.StatusOK, uiSchema()) } func (s *Server) handleConfigUIState(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { errorJSON(w, http.StatusMethodNotAllowed, "method not allowed") return } if !s.authorize(r, false) { errorJSON(w, http.StatusUnauthorized, "unauthorized") return } root, _ := readUIRootConfig(s.agentCfg.ConfigPath) mode := "instances" warnings := []string{} if st, err := os.Stat(s.agentCfg.ConfigPath); err != nil { if os.IsNotExist(err) { warnings = append(warnings, "config file not found: "+s.agentCfg.ConfigPath) } else { warnings = append(warnings, "config stat failed: "+err.Error()) } } else if st.IsDir() { warnings = append(warnings, "config path is a directory: "+s.agentCfg.ConfigPath) } instances := root.Instances // If current config is graphs-only, do a best-effort import so GUI can edit. if len(instances) == 0 && len(root.Graphs) > 0 { mode = "graphs_imported" inst, warn := inferInstancesFromGraphs(root.Graphs) if len(inst) > 0 { instances = inst } warnings = append(warnings, warn...) } // Force templates/instances-only view. // templates = union(current templates, built-in templates) tplKeys := map[string]bool{} for k := range root.Templates { tplKeys[k] = true } for k := range uiTemplates() { tplKeys[k] = true } mergedTpls := make([]string, 0, len(tplKeys)) for k := range tplKeys { mergedTpls = append(mergedTpls, k) } sort.Strings(mergedTpls) resp := map[string]any{ "ok": true, "global": root.Global, "queue": root.Queue, "instances": instances, "templates": mergedTpls, "mode": mode, "warnings": warnings, } writeJSON(w, http.StatusOK, resp) } func inferInstancesFromGraphs(graphs []map[string]any) ([]UIInstance, []string) { instances := []UIInstance{} warnings := []string{} used := map[string]int{} for _, g := range graphs { gname := asString(g["name"]) nodes := asSlice(g["nodes"]) if len(nodes) == 0 { continue } // Try templates in priority order. if inst, ok := inferFaceDetRecog(gname, nodes); ok { inst.Name = uniquify(inst.Name, used) instances = append(instances, inst) continue } if inst, ok := inferFaceDet(gname, nodes); ok { inst.Name = uniquify(inst.Name, used) instances = append(instances, inst) continue } if inst, ok := inferYoloAlarmMinio(gname, nodes); ok { inst.Name = uniquify(inst.Name, used) instances = append(instances, inst) continue } if inst, ok := inferYolo(gname, nodes); ok { inst.Name = uniquify(inst.Name, used) instances = append(instances, inst) continue } if inst, ok := inferTranscode(gname, nodes); ok { inst.Name = uniquify(inst.Name, used) instances = append(instances, inst) continue } if gname == "" { gname = "noname" } warnings = append(warnings, "graph not recognized for templates/instances import: "+gname) } return instances, warnings } func inferTranscode(graphName string, nodes []any) (UIInstance, bool) { in := findNodeByType(nodes, "input_rtsp") pub := findNodeByType(nodes, "publish") if in == nil || pub == nil { return UIInstance{}, false } if findNodeByType(nodes, "ai_yolo") != nil || findNodeByType(nodes, "ai_face_det") != nil || findNodeByType(nodes, "ai_face_recog") != nil { return UIInstance{}, false } name := deriveNameFromPublish(pub, graphName) params := map[string]any{} params["name"] = name if url := asString(in["url"]); url != "" { params["url"] = url } else { return UIInstance{}, false } params["fps"] = asIntOr(in["fps"], asIntOr(pub["fps"], 25)) params["src_w"] = asIntOr(in["width"], 1920) params["src_h"] = asIntOr(in["height"], 1080) params["gop"] = asIntOr(pub["gop"], 50) params["bitrate_kbps"] = asIntOr(pub["bitrate_kbps"], 2000) applyPublishOutputsToParams(pub, params) return UIInstance{Name: name, Template: "transcode_rtsp_hls", Params: params}, true } func inferYolo(graphName string, nodes []any) (UIInstance, bool) { in := findNodeByType(nodes, "input_rtsp") ai := findNodeByType(nodes, "ai_yolo") pub := findNodeByType(nodes, "publish") if in == nil || ai == nil || pub == nil { return UIInstance{}, false } if findNodeByType(nodes, "alarm") != nil { return UIInstance{}, false } name := deriveNameFromPublish(pub, graphName) params := map[string]any{} params["name"] = name url := asString(in["url"]) model := asString(ai["model_path"]) if url == "" || model == "" { return UIInstance{}, false } params["url"] = url params["model_path"] = model params["fps"] = asIntOr(in["fps"], asIntOr(pub["fps"], 25)) params["src_w"] = asIntOr(in["width"], 1920) params["src_h"] = asIntOr(in["height"], 1080) params["gop"] = asIntOr(pub["gop"], 50) params["bitrate_kbps"] = asIntOr(pub["bitrate_kbps"], 2000) applyPublishOutputsToParams(pub, params) return UIInstance{Name: name, Template: "yolo_rtsp_hls", Params: params}, true } func inferYoloAlarmMinio(graphName string, nodes []any) (UIInstance, bool) { in := findNodeByType(nodes, "input_rtsp") ai := findNodeByType(nodes, "ai_yolo") alarm := findNodeByType(nodes, "alarm") pub := findNodeByType(nodes, "publish") if in == nil || ai == nil || alarm == nil || pub == nil { return UIInstance{}, false } name := deriveNameFromPublish(pub, graphName) params := map[string]any{} params["name"] = name url := asString(in["url"]) model := asString(ai["model_path"]) if url == "" || model == "" { return UIInstance{}, false } params["url"] = url params["model_path"] = model params["fps"] = asIntOr(in["fps"], asIntOr(pub["fps"], 25)) params["src_w"] = asIntOr(in["width"], 1920) params["src_h"] = asIntOr(in["height"], 1080) params["gop"] = asIntOr(pub["gop"], 50) params["bitrate_kbps"] = asIntOr(pub["bitrate_kbps"], 2000) applyPublishOutputsToParams(pub, params) // cooldown if rules := asSlice(alarm["rules"]); len(rules) > 0 { if r0 := asMap(rules[0]); r0 != nil { params["cooldown_ms"] = asIntOr(r0["cooldown_ms"], 3000) } } // minio from snapshot/clip upload actions := asMap(alarm["actions"]) upl := firstMinioUpload(actions) if upl == nil { return UIInstance{}, false } ep := asString(upl["endpoint"]) bucket := asString(upl["bucket"]) ak := asString(upl["access_key"]) sk := asString(upl["secret_key"]) if ep == "" || bucket == "" || ak == "" || sk == "" { return UIInstance{}, false } params["minio_endpoint"] = ep params["minio_bucket"] = bucket params["minio_ak"] = ak params["minio_sk"] = sk if region := asString(upl["region"]); region != "" { params["minio_region"] = region } else { params["minio_region"] = "us-east-1" } return UIInstance{Name: name, Template: "yolo_alarm_minio", Params: params}, true } func inferFaceDet(graphName string, nodes []any) (UIInstance, bool) { in := findNodeByType(nodes, "input_rtsp") det := findNodeByType(nodes, "ai_face_det") pub := findNodeByType(nodes, "publish") if in == nil || det == nil || pub == nil { return UIInstance{}, false } if findNodeByType(nodes, "ai_face_recog") != nil { return UIInstance{}, false } name := deriveNameFromPublish(pub, graphName) params := map[string]any{} params["name"] = name url := asString(in["url"]) model := asString(det["model_path"]) if url == "" || model == "" { return UIInstance{}, false } params["url"] = url params["det_model_path"] = model params["fps"] = asIntOr(in["fps"], asIntOr(pub["fps"], 25)) params["src_w"] = asIntOr(in["width"], 1920) params["src_h"] = asIntOr(in["height"], 1080) params["gop"] = asIntOr(pub["gop"], 50) params["bitrate_kbps"] = asIntOr(pub["bitrate_kbps"], 2000) applyPublishOutputsToParams(pub, params) return UIInstance{Name: name, Template: "face_det_rtsp_hls", Params: params}, true } func inferFaceDetRecog(graphName string, nodes []any) (UIInstance, bool) { in := findNodeByType(nodes, "input_rtsp") det := findNodeByType(nodes, "ai_face_det") rec := findNodeByType(nodes, "ai_face_recog") pub := findNodeByType(nodes, "publish") if in == nil || det == nil || rec == nil || pub == nil { return UIInstance{}, false } name := deriveNameFromPublish(pub, graphName) params := map[string]any{} params["name"] = name url := asString(in["url"]) detModel := asString(det["model_path"]) recModel := asString(rec["model_path"]) if url == "" || detModel == "" || recModel == "" { return UIInstance{}, false } params["url"] = url params["det_model_path"] = detModel params["recog_model_path"] = recModel params["fps"] = asIntOr(in["fps"], asIntOr(pub["fps"], 25)) params["src_w"] = asIntOr(in["width"], 1920) params["src_h"] = asIntOr(in["height"], 1080) params["gop"] = asIntOr(pub["gop"], 50) params["bitrate_kbps"] = asIntOr(pub["bitrate_kbps"], 2000) applyPublishOutputsToParams(pub, params) if th := asMap(rec["threshold"]); th != nil { params["thr_accept"] = asFloatOr(th["accept"], 0.45) params["thr_margin"] = asFloatOr(th["margin"], 0.05) } else { params["thr_accept"] = 0.45 params["thr_margin"] = 0.05 } if gal := asMap(rec["gallery"]); gal != nil { if p := asString(gal["path"]); p != "" { params["gallery_path"] = p } else { params["gallery_path"] = "./models/face_gallery.db" } } else { params["gallery_path"] = "./models/face_gallery.db" } return UIInstance{Name: name, Template: "face_det_recog_rtsp_hls", Params: params}, true } func findNodeByType(nodes []any, typ string) map[string]any { for _, n := range nodes { m := asMap(n) if m == nil { continue } if asString(m["type"]) == typ { return m } } return nil } func deriveNameFromPublish(pub map[string]any, fallback string) string { outs := asSlice(pub["outputs"]) for _, o := range outs { om := asMap(o) if om == nil { continue } if asString(om["proto"]) != "rtsp_server" { continue } p := asString(om["path"]) if strings.HasPrefix(p, "/live/") { n := strings.TrimPrefix(p, "/live/") n = strings.Trim(n, "/") if n != "" { return n } } } if strings.TrimSpace(fallback) != "" { return fallback } return "cam" } func applyPublishOutputsToParams(pub map[string]any, params map[string]any) { outs := asSlice(pub["outputs"]) for _, o := range outs { om := asMap(o) if om == nil { continue } proto := asString(om["proto"]) if proto == "rtsp_server" { params["rtsp_port"] = asIntOr(om["port"], asIntOr(params["rtsp_port"], 8555)) } if proto == "hls" { if p := asString(om["path"]); p != "" { params["hls_path"] = p } } } } func firstMinioUpload(actions map[string]any) map[string]any { if actions == nil { return nil } // Prefer snapshot.upload. if snap := asMap(actions["snapshot"]); snap != nil { if up := asMap(snap["upload"]); up != nil { if asString(up["type"]) == "minio" { return up } } } if clip := asMap(actions["clip"]); clip != nil { if up := asMap(clip["upload"]); up != nil { if asString(up["type"]) == "minio" { return up } } } return nil } func uniquify(name string, used map[string]int) string { base := name if base == "" { base = "cam" } if used[base] == 0 { used[base] = 1 return base } used[base]++ return fmt.Sprintf("%s_%d", base, used[base]) } func asMap(v any) map[string]any { if v == nil { return nil } if m, ok := v.(map[string]any); ok { return m } return nil } func asSlice(v any) []any { if v == nil { return nil } if s, ok := v.([]any); ok { return s } return nil } func asString(v any) string { if v == nil { return "" } if s, ok := v.(string); ok { return strings.TrimSpace(s) } return "" } func asIntOr(v any, def int) int { if v == nil { return def } switch x := v.(type) { case float64: return int(x) case int: return x case int32: return int(x) case int64: return int(x) case string: // ignore parse errors return def default: return def } } func asFloatOr(v any, def float64) float64 { if v == nil { return def } switch x := v.(type) { case float64: return x case int: return float64(x) case int32: return float64(x) case int64: return float64(x) default: return def } } func (s *Server) handleConfigUIPlan(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { errorJSON(w, http.StatusMethodNotAllowed, "method not allowed") return } if !s.authorize(r, true) { errorJSON(w, http.StatusUnauthorized, "unauthorized") return } req, err := readRequiredJSON[uiPlanRequest](w, r, 1<<20) if err != nil { errorJSON(w, http.StatusBadRequest, err.Error()) return } resp, err := s.planUIConfig(req) if err != nil { errorJSON(w, http.StatusBadRequest, "validation failed: "+err.Error()) return } writeJSON(w, http.StatusOK, resp) } func (s *Server) handleConfigUIApply(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { errorJSON(w, http.StatusMethodNotAllowed, "method not allowed") return } if !s.authorize(r, true) { errorJSON(w, http.StatusUnauthorized, "unauthorized") return } req, err := readRequiredJSON[uiPlanRequest](w, r, 1<<20) if err != nil { errorJSON(w, http.StatusBadRequest, err.Error()) return } resp, err := s.planUIConfig(req) if err != nil { errorJSON(w, http.StatusBadRequest, "validation failed: "+err.Error()) return } b, err := json.MarshalIndent(resp.GeneratedConfig, "", " ") if err != nil { errorJSON(w, http.StatusInternalServerError, "internal error: marshal config failed: "+err.Error()) return } if err := s.applyRootConfigBytes(r.Context(), b); err != nil { errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error()) return } writeJSON(w, http.StatusOK, resp) } func (s *Server) planUIConfig(req uiPlanRequest) (uiPlanResponse, error) { cur, _ := readUIRootConfig(s.agentCfg.ConfigPath) curInst := map[string]UIInstance{} for _, it := range cur.Instances { if strings.TrimSpace(it.Name) == "" { continue } curInst[it.Name] = it } warnings := []string{} // Allow both built-in templates and templates already present in the current config. // Merge rule: keep existing templates as-is; add built-in templates if missing. tpls := map[string]any{} for k, v := range cur.Templates { tpls[k] = v } for k, v := range uiTemplates() { if _, ok := tpls[k]; !ok { tpls[k] = v } } seen := map[string]bool{} for i := range req.Instances { it := &req.Instances[i] it.Name = strings.TrimSpace(it.Name) it.Template = strings.TrimSpace(it.Template) if it.Params == nil { it.Params = map[string]any{} } if it.Name == "" { return uiPlanResponse{}, errors.New("instances[].name is required") } if seen[it.Name] { return uiPlanResponse{}, errors.New("instance name not unique: " + it.Name) } seen[it.Name] = true if _, ok := tpls[it.Template]; !ok { return uiPlanResponse{}, errors.New("unknown template: " + it.Template) } // Apply per-template defaults. applyUIDefaults(it) // Basic required params validation. missing := validateUIRequiredParams(*it) if len(missing) > 0 { return uiPlanResponse{}, errors.New("instance " + it.Name + " missing params: " + strings.Join(missing, ", ")) } } // Build root config. root := uiRootConfig{} if req.Global != nil { root.Global = req.Global } else { root.Global = cur.Global } if req.Queue != nil { root.Queue = req.Queue } else { root.Queue = cur.Queue } root.Templates = tpls root.Instances = req.Instances gen := map[string]any{} b, _ := json.Marshal(root) _ = json.Unmarshal(b, &gen) // Diff summary (instances only). added := []string{} removed := []string{} changed := []string{} for name := range seen { if _, ok := curInst[name]; !ok { added = append(added, name) continue } prev := curInst[name] now := seenInstance(req.Instances, name) if prev.Template != now.Template { changed = append(changed, name) continue } // Rough compare params by json. pb, _ := json.Marshal(prev.Params) nb, _ := json.Marshal(now.Params) if string(pb) != string(nb) { changed = append(changed, name) } } for name := range curInst { if !seen[name] { removed = append(removed, name) } } sort.Strings(added) sort.Strings(removed) sort.Strings(changed) return uiPlanResponse{ Ok: true, GeneratedConfig: gen, Diff: uiDiff{Added: added, Removed: removed, Changed: changed}, Warnings: warnings, }, nil } func readUIRootConfig(path string) (uiRootConfig, error) { var out uiRootConfig b, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { // Return a well-formed empty state. out.Instances = []UIInstance{} out.Global = map[string]any{} out.Queue = map[string]any{} out.Templates = map[string]any{} return out, nil } return out, err } _ = json.Unmarshal(b, &out) if out.Instances == nil { out.Instances = []UIInstance{} } if out.Global == nil { out.Global = map[string]any{} } if out.Queue == nil { out.Queue = map[string]any{} } if out.Templates == nil { out.Templates = map[string]any{} } return out, nil } func readRequiredJSON[T any](w http.ResponseWriter, r *http.Request, maxBytes int64) (T, error) { var zero T if r.Body == nil { return zero, errors.New("validation failed: empty body") } if mt, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")); err != nil || mt != "application/json" { return zero, errors.New("validation failed: Content-Type must be application/json") } r.Body = http.MaxBytesReader(w, r.Body, maxBytes) body, err := io.ReadAll(r.Body) if err != nil { if strings.Contains(err.Error(), "request body too large") { return zero, errors.New("payload too large") } return zero, fmt.Errorf("invalid json: %v", err) } if len(strings.TrimSpace(string(body))) == 0 { return zero, errors.New("validation failed: empty body") } var v T if err := json.Unmarshal(body, &v); err != nil { return zero, fmt.Errorf("invalid json: %v", err) } return v, nil } func seenInstance(insts []UIInstance, name string) UIInstance { for _, it := range insts { if it.Name == name { return it } } return UIInstance{} } func sortedKeys(m map[string]any) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) return keys } func applyUIDefaults(it *UIInstance) { if it.Params == nil { it.Params = map[string]any{} } // Common defaults. setDefault(it.Params, "name", it.Name) setDefault(it.Params, "fps", 25) setDefault(it.Params, "src_w", 1920) setDefault(it.Params, "src_h", 1080) setDefault(it.Params, "gop", 50) setDefault(it.Params, "bitrate_kbps", 2000) setDefault(it.Params, "rtsp_port", 8555) setDefault(it.Params, "hls_path", "./web/hls/"+it.Name+"/index.m3u8") // Face defaults. if it.Template == "face_det_recog_rtsp_hls" { setDefault(it.Params, "gallery_path", "./models/face_gallery.db") setDefault(it.Params, "thr_accept", 0.45) setDefault(it.Params, "thr_margin", 0.05) } // Alarm defaults. if it.Template == "yolo_alarm_minio" { setDefault(it.Params, "minio_region", "us-east-1") setDefault(it.Params, "cooldown_ms", 3000) } } func validateUIRequiredParams(it UIInstance) []string { req := []string{"url"} switch it.Template { case "transcode_rtsp_hls": // url is enough case "yolo_rtsp_hls", "yolo_alarm_minio": req = append(req, "model_path") case "face_det_rtsp_hls": req = append(req, "det_model_path") case "face_det_recog_rtsp_hls": req = append(req, "det_model_path", "recog_model_path") } missing := []string{} for _, k := range req { v, ok := it.Params[k] if !ok { missing = append(missing, k) continue } if s, ok := v.(string); ok { if strings.TrimSpace(s) == "" { missing = append(missing, k) } } } return missing } func setDefault(m map[string]any, key string, val any) { if _, ok := m[key]; ok { return } m[key] = val } func uiSchema() map[string]any { return map[string]any{ "version": 1, "templates": sortedKeys(uiTemplates()), "fields": map[string]any{ "common": []map[string]any{ {"key": "name", "type": "string", "required": true}, {"key": "url", "type": "string", "required": true}, {"key": "fps", "type": "int", "default": 25}, {"key": "src_w", "type": "int", "default": 1920}, {"key": "src_h", "type": "int", "default": 1080}, {"key": "gop", "type": "int", "default": 50}, {"key": "bitrate_kbps", "type": "int", "default": 2000}, }, "yolo": []map[string]any{ {"key": "model_path", "type": "string", "required": true}, }, "face": []map[string]any{ {"key": "det_model_path", "type": "string", "required": true}, {"key": "recog_model_path", "type": "string", "required": false}, {"key": "gallery_path", "type": "string", "default": "./models/face_gallery.db"}, {"key": "thr_accept", "type": "float", "default": 0.45}, {"key": "thr_margin", "type": "float", "default": 0.05}, }, "alarm_minio": []map[string]any{ {"key": "minio_endpoint", "type": "string", "required": true}, {"key": "minio_bucket", "type": "string", "required": true}, {"key": "minio_ak", "type": "string", "required": true}, {"key": "minio_sk", "type": "string", "required": true}, {"key": "cooldown_ms", "type": "int", "default": 3000}, }, }, "generated_at_ms": time.Now().UnixMilli(), } } func uiTemplates() map[string]any { // Templates are expressed in media-server native format with placeholders used by config_expand. return map[string]any{ "transcode_rtsp_hls": map[string]any{ "nodes": []any{ map[string]any{"id": "in", "type": "input_rtsp", "role": "source", "enable": true, "url": "${url}", "fps": "${fps}", "width": "${src_w}", "height": "${src_h}", "use_mpp": true, "use_ffmpeg": false, "force_tcp": true, "reconnect_sec": 5, "reconnect_backoff_max_sec": 30}, map[string]any{"id": "pre", "type": "preprocess", "role": "filter", "enable": true, "dst_w": "${src_w}", "dst_h": "${src_h}", "dst_format": "nv12", "keep_ratio": false, "use_rga": true}, map[string]any{"id": "pub", "type": "publish", "role": "filter", "enable": true, "codec": "h264", "fps": "${fps}", "gop": "${gop}", "bitrate_kbps": "${bitrate_kbps}", "use_mpp": true, "use_ffmpeg_mux": true, "outputs": []any{ map[string]any{"proto": "rtsp_server", "port": "${rtsp_port}", "path": "/live/${name}"}, map[string]any{"proto": "hls", "path": "${hls_path}", "segment_sec": 2}, }}, }, "edges": []any{ []any{"in", "pre"}, []any{"pre", "pub"}, }, }, "yolo_rtsp_hls": map[string]any{ "nodes": []any{ map[string]any{"id": "in", "type": "input_rtsp", "role": "source", "enable": true, "url": "${url}", "fps": "${fps}", "width": "${src_w}", "height": "${src_h}", "use_mpp": true, "use_ffmpeg": false, "force_tcp": true, "reconnect_sec": 5, "reconnect_backoff_max_sec": 30}, map[string]any{"id": "pre", "type": "preprocess", "role": "filter", "enable": true, "dst_w": 640, "dst_h": 640, "dst_format": "rgb", "keep_ratio": false, "use_rga": true}, map[string]any{"id": "ai", "type": "ai_yolo", "role": "filter", "enable": true, "infer_fps": 10, "model_path": "${model_path}", "model_version": "auto", "num_classes": 80, "conf": 0.25, "nms": 0.45, "class_filter": []any{}}, map[string]any{"id": "osd", "type": "osd", "role": "filter", "enable": true, "draw_bbox": true, "draw_text": true, "line_width": 2, "font_scale": 1, "labels": []any{}}, map[string]any{"id": "post", "type": "preprocess", "role": "filter", "enable": true, "dst_w": "${src_w}", "dst_h": "${src_h}", "dst_format": "nv12", "keep_ratio": false, "use_rga": true}, map[string]any{"id": "pub", "type": "publish", "role": "filter", "enable": true, "codec": "h264", "fps": "${fps}", "gop": "${gop}", "bitrate_kbps": "${bitrate_kbps}", "use_mpp": true, "use_ffmpeg_mux": true, "outputs": []any{ map[string]any{"proto": "rtsp_server", "port": "${rtsp_port}", "path": "/live/${name}"}, map[string]any{"proto": "hls", "path": "${hls_path}", "segment_sec": 2}, }}, }, "edges": []any{ []any{"in", "pre"}, []any{"pre", "ai"}, []any{"ai", "osd"}, []any{"osd", "post"}, []any{"post", "pub"}, }, }, "yolo_alarm_minio": map[string]any{ "nodes": []any{ map[string]any{"id": "in", "type": "input_rtsp", "role": "source", "enable": true, "url": "${url}", "fps": "${fps}", "width": "${src_w}", "height": "${src_h}", "use_mpp": true, "use_ffmpeg": false, "force_tcp": true, "reconnect_sec": 5, "reconnect_backoff_max_sec": 30}, map[string]any{"id": "pre", "type": "preprocess", "role": "filter", "enable": true, "dst_w": 640, "dst_h": 640, "dst_format": "rgb", "keep_ratio": false, "use_rga": true}, map[string]any{"id": "ai", "type": "ai_yolo", "role": "filter", "enable": true, "infer_fps": 10, "model_path": "${model_path}", "model_version": "auto", "num_classes": 80, "conf": 0.25, "nms": 0.45, "class_filter": []any{}}, map[string]any{"id": "osd", "type": "osd", "role": "filter", "enable": true, "draw_bbox": true, "draw_text": true, "line_width": 2, "font_scale": 1, "labels": []any{}}, map[string]any{"id": "post", "type": "preprocess", "role": "filter", "enable": true, "dst_w": "${src_w}", "dst_h": "${src_h}", "dst_format": "nv12", "keep_ratio": false, "use_rga": true}, map[string]any{"id": "pub", "type": "publish", "role": "filter", "enable": true, "codec": "h264", "fps": "${fps}", "gop": "${gop}", "bitrate_kbps": "${bitrate_kbps}", "use_mpp": true, "use_ffmpeg_mux": true, "outputs": []any{map[string]any{"proto": "rtsp_server", "port": "${rtsp_port}", "path": "/live/${name}"}}}, map[string]any{"id": "alarm", "type": "alarm", "role": "sink", "enable": true, "eval_fps": 10, "labels": []any{}, "rules": []any{ map[string]any{"name": "person_in_view", "class_ids": []any{0}, "roi": map[string]any{"x": 0.0, "y": 0.0, "w": 1.0, "h": 1.0}, "min_duration_ms": 0, "cooldown_ms": "${cooldown_ms}"}, }, "actions": map[string]any{ "log": map[string]any{"enable": true, "level": "info"}, "snapshot": map[string]any{"enable": true, "format": "jpg", "quality": 85, "upload": map[string]any{"type": "minio", "endpoint": "${minio_endpoint}", "bucket": "${minio_bucket}", "region": "${minio_region}", "access_key": "${minio_ak}", "secret_key": "${minio_sk}"}}, "clip": map[string]any{"enable": true, "pre_sec": 5, "post_sec": 10, "format": "mp4", "fps": "${fps}", "upload": map[string]any{"type": "minio", "endpoint": "${minio_endpoint}", "bucket": "${minio_bucket}", "region": "${minio_region}", "access_key": "${minio_ak}", "secret_key": "${minio_sk}"}}, "http": map[string]any{"enable": false, "url": "http://127.0.0.1:8080/api/alarm", "timeout_ms": 3000, "include_media_url": true, "method": "POST"}, }, }, }, "edges": []any{ []any{"in", "pre"}, []any{"pre", "ai"}, []any{"ai", "osd"}, []any{"osd", "post"}, []any{"post", "pub"}, []any{"pub", "alarm"}, }, }, "face_det_rtsp_hls": map[string]any{ "nodes": []any{ map[string]any{"id": "in", "type": "input_rtsp", "role": "source", "enable": true, "url": "${url}", "fps": "${fps}", "width": "${src_w}", "height": "${src_h}", "use_mpp": true, "use_ffmpeg": false, "force_tcp": true, "reconnect_sec": 5, "reconnect_backoff_max_sec": 30}, map[string]any{"id": "pre", "type": "preprocess", "role": "filter", "enable": true, "dst_w": 320, "dst_h": 320, "dst_format": "rgb", "keep_ratio": false, "use_rga": true}, map[string]any{"id": "face_det", "type": "ai_face_det", "role": "filter", "enable": true, "model_path": "${det_model_path}", "conf": 0.6, "nms": 0.4, "max_faces": 10, "output_landmarks": true, "input_format": "rgb"}, map[string]any{"id": "osd", "type": "osd", "role": "filter", "enable": true, "draw_bbox": true, "draw_text": true, "draw_face_det": true, "line_width": 2, "font_scale": 1, "labels": []any{}}, map[string]any{"id": "post", "type": "preprocess", "role": "filter", "enable": true, "dst_w": "${src_w}", "dst_h": "${src_h}", "dst_format": "nv12", "keep_ratio": false, "use_rga": true}, map[string]any{"id": "pub", "type": "publish", "role": "filter", "enable": true, "codec": "h264", "fps": "${fps}", "gop": "${gop}", "bitrate_kbps": "${bitrate_kbps}", "use_mpp": true, "use_ffmpeg_mux": true, "outputs": []any{map[string]any{"proto": "rtsp_server", "port": "${rtsp_port}", "path": "/live/${name}"}}}, }, "edges": []any{ []any{"in", "pre"}, []any{"pre", "face_det"}, []any{"face_det", "osd"}, []any{"osd", "post"}, []any{"post", "pub"}, }, }, "face_det_recog_rtsp_hls": map[string]any{ "nodes": []any{ map[string]any{"id": "in", "type": "input_rtsp", "role": "source", "enable": true, "url": "${url}", "fps": "${fps}", "width": "${src_w}", "height": "${src_h}", "use_mpp": true, "use_ffmpeg": false, "force_tcp": true, "reconnect_sec": 5, "reconnect_backoff_max_sec": 30}, map[string]any{"id": "pre", "type": "preprocess", "role": "filter", "enable": true, "dst_w": "${src_w}", "dst_h": "${src_h}", "dst_format": "rgb", "keep_ratio": false, "use_rga": true}, map[string]any{"id": "face_det", "type": "ai_face_det", "role": "filter", "enable": true, "model_path": "${det_model_path}", "conf": 0.6, "nms": 0.4, "max_faces": 10, "output_landmarks": true, "input_format": "rgb"}, map[string]any{"id": "face_recog", "type": "ai_face_recog", "role": "filter", "enable": true, "model_path": "${recog_model_path}", "align": true, "emit_embedding": false, "max_faces": 10, "threshold": map[string]any{"accept": "${thr_accept}", "margin": "${thr_margin}"}, "gallery": map[string]any{"backend": "sqlite", "path": "${gallery_path}", "load_on_start": true, "expected_dim": 512, "dtype": "auto"}}, map[string]any{"id": "osd", "type": "osd", "role": "filter", "enable": true, "draw_bbox": true, "draw_text": true, "draw_face_det": false, "line_width": 2, "font_scale": 1, "labels": []any{}}, map[string]any{"id": "post", "type": "preprocess", "role": "filter", "enable": true, "dst_w": "${src_w}", "dst_h": "${src_h}", "dst_format": "nv12", "keep_ratio": false, "use_rga": true}, map[string]any{"id": "pub", "type": "publish", "role": "filter", "enable": true, "codec": "h264", "fps": "${fps}", "gop": "${gop}", "bitrate_kbps": "${bitrate_kbps}", "use_mpp": true, "use_ffmpeg_mux": true, "outputs": []any{map[string]any{"proto": "rtsp_server", "port": "${rtsp_port}", "path": "/live/${name}"}}}, }, "edges": []any{ []any{"in", "pre"}, []any{"pre", "face_det"}, []any{"face_det", "face_recog"}, []any{"face_recog", "osd"}, []any{"osd", "post"}, []any{"post", "pub"}, }, }, } }