[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M752XW3BPNVCM0823YAEP6YA":3,"skill-same:01M752XW3BPNVCM0823YAEP6YA":46},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":24,"commitSha":25,"skillDirectory":26,"forksCount":14,"repositoryHtmlUrl":27,"repositoryHomepageUrl":6,"ownerHtmlUrl":28,"ownerBlogUrl":29,"ownerDescription":30,"updatedAt":31,"install":32,"resources":45},"01M752XW3BPNVCM0823YAEP6YA","hl-build-vlm-app",null,"Build a complete Vision-Language Model application that uses the Hailo-10H VLM for image understanding.","TejasLamba2006\u002Fhailo-apps","TejasLamba2006","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F73745640?v=4","01M752WBF8KHM6KJTEQXTJZR9K","Tejas Lamba","01M752WBGE2NS66C4ENRHWY36S",0,"MIT","https:\u002F\u002Fgithub.com\u002FTejasLamba2006\u002Fhailo-apps\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fhl-build-vlm-app\u002FSKILL.md",[],[],"2026-11-12 13:55:43",false,"---\nname: hl-build-vlm-app\ndescription: Build a complete Vision-Language Model application that uses the Hailo-10H VLM for image understanding.\n---\n\n# Skill: Build VLM Application\n\nBuild a complete Vision-Language Model application that uses the Hailo-10H VLM for image understanding.\n\n## When This Skill Is Loaded\n\n- User wants to build an app that **looks at camera images and answers questions**\n- User needs **visual scene understanding** (describe, count, detect, analyze)\n- User wants a variant of the VLM Chat app with different behavior\n- User mentions: VLM, vision, image understanding, camera monitoring, scene analysis\n\n## Reference Implementation\n\nStudy `hailo_apps\u002Fpython\u002Fgen_ai_apps\u002Fvlm_chat\u002F` — the canonical VLM app:\n- `vlm_chat.py` — State machine app with camera loop\n- `backend.py` — Multiprocessing VLM inference backend (REUSE this, don't copy)\n\n## Build Process\n\n### Step 1: Create App Directory\n\nCreate the app directory:\n\n```\nhailo_apps\u002Fpython\u002F\u003Ctype>\u002F\u003Capp_name>\u002F\n├── app.yaml              # App manifest (required)\n├── run.sh                # Launch wrapper (sets PYTHONPATH)\n├── __init__.py           # Empty\n├── \u003Capp_name>.py         # Main app class + entry point\n├── event_tracker.py      # Optional: event classification (for monitoring apps)\n└── README.md             # Usage documentation (REQUIRED — never skip)\n```\n\n**app.yaml** — required manifest:\n```yaml\nname: \u003Capp_name>\ntitle: My VLM App\ndescription: One-line description\nauthor: AI Agent (auto-generated)\ndate: \"YYYY-MM-DD\"\ntype: gen_ai\nhailo_arch: hailo10h\nmodel: Qwen2-VL-2B-Instruct\ntags: [vlm, monitoring]\nstatus: draft\n```\n\n**run.sh** — launch wrapper:\n```bash\n#!\u002Fbin\u002Fbash\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"$SCRIPT_DIR\u002F..\u002F..\u002F..\" && pwd)\"\nPYTHONPATH=\"$REPO_ROOT\" python3 \"$SCRIPT_DIR\u002F\u003Capp_name>.py\" \"$@\"\n```\n\n**NOTE**: Do NOT register in `defines.py` or `resources_config.yaml`.\nCommunity apps are run via `run.sh`. Registration happens during promotion\n\n### Step 2: Build the App\n\nThe main app file follows this structure:\n```python\nimport os\nimport sys\nimport cv2\nimport signal\nimport time\nfrom typing import Optional\n\nos.environ[\"QT_QPA_PLATFORM\"] = 'xcb'\n\nfrom hailo_apps.python.gen_ai_apps.vlm_chat.backend import Backend\nfrom hailo_apps.python.core.common.core import (\n    get_standalone_parser, resolve_hef_path, handle_list_models_flag\n)\nfrom hailo_apps.python.core.common.defines import (\n    HAILO10H_ARCH, USB_CAMERA\n)\nfrom hailo_apps.python.core.common.camera_utils import get_usb_video_devices\nfrom hailo_apps.python.core.common.hailo_logger import get_logger\n\nlogger = get_logger(__name__)\n\nAPP_NAME = \"my_vlm_app\"\n\nSYSTEM_PROMPT = \"Your system prompt here...\"\nMONITOR_PROMPT = \"Your per-frame VLM question here...\"\n\nclass MyVLMApp:\n    def __init__(self, camera, camera_type, args):\n        self.camera = camera\n        self.camera_type = camera_type\n        self.running = True\n        self.backend = None\n        signal.signal(signal.SIGINT, self.signal_handler)\n        # Initialize Backend, EventTracker, etc.\n\n    def signal_handler(self, sig, frame):\n        self.running = False\n\n    def run(self):\n        # Main loop: capture frame, display, analyze periodically\n        pass\n\n    def cleanup(self):\n        if self.backend:\n            self.backend.close()\n        cv2.destroyAllWindows()\n\ndef main():\n    parser = get_standalone_parser()\n    # IMPORTANT: Add ALL custom args BEFORE handle_list_models_flag\n    # so they appear in --help output\n    parser.add_argument(\"--interval\", type=int, default=15, help=\"Seconds between analyses\")\n    handle_list_models_flag(parser, APP_NAME)\n    args = parser.parse_args()\n    hef_path = resolve_hef_path(args.hef_path, app_name=APP_NAME, arch=HAILO10H_ARCH)\n    # Camera setup, app.run()\n\nif __name__ == \"__main__\":\n    main()\n```\n\n### Step 4: Validate\n\nRun the automated validation script (includes static checks + runtime smoke tests):\n```bash\npython3 .github\u002Fscripts\u002Fvalidate_app.py hailo_apps\u002Fpython\u002Fgen_ai_apps\u002F\u003Capp_name> --smoke-test\n```\n\n### Step 5: Write README\n\nInclude: description, requirements, usage CLI, architecture, customization notes.\n\n## Key Customization Points\n\n| What to Change | Where |\n|---|---|\n| System prompt | `SYSTEM_PROMPT` constant |\n| Per-frame VLM question | `MONITOR_PROMPT` constant |\n| Image preprocessing | `Backend.convert_resize_image()` |\n| Inference parameters | `MAX_TOKENS`, `TEMPERATURE` |\n| Event classification | `EventTracker.classify_response()` |\n| Display overlay | OpenCV `cv2.putText()` in main loop |\n\n## Display & Output Best Practices\n\n### Window Size\nThe VLM crops images to 336×336 but this is too small for a display window.\nAlways resize to at least 640×640 for readability:\n```python\nDISPLAY_SIZE = (640, 640)\ndisplay = cv2.resize(frame, DISPLAY_SIZE, interpolation=cv2.INTER_LINEAR)\n```\n\n### Text Wrapping\nVLM responses can be long (100+ chars). Always wrap overlay text to fit the window:\n```python\n@staticmethod\ndef _wrap_text(text: str, max_chars: int = 70) -> list[str]:\n    words = text.split()\n    lines, current = [], \"\"\n    for word in words:\n        if current and len(current) + 1 + len(word) > max_chars:\n            lines.append(current)\n            current = word\n        else:\n            current = f\"{current} {word}\".strip() if current else word\n    if current:\n        lines.append(current)\n    return lines or [\"\"]\n```\nMake the banner height dynamic: `banner_h = 35 + 22 * len(desc_lines)`.\n\n### Print Activity to Terminal\n`logger.info()` may not be visible at default log level. Always `print()` event\nclassifications so the user sees them:\n```python\nprint(f\"\\n[{event.time_str}] Activity: {event.event_type.value}\")\nprint(f\"  {answer}\")\n```\n\n## Video Playback During Inference (CRITICAL)\n\n**NEVER freeze video playback during VLM inference.** VLM inference takes 10-30 seconds.\nFreezing the display makes the app feel broken and wastes most of the video.\n\nThe correct pattern for continuous monitoring apps:\n- Video keeps playing normally at all times\n- Inference runs in a background thread via `ThreadPoolExecutor`\n- Track `_inference_pending` flag to avoid submitting overlapping requests\n- When inference completes, update the overlay with the result\n- The overlay shows the *latest* result while live video continues\n\nFreezing is ONLY appropriate for interactive capture-and-ask apps (like `vlm_chat`)\nwhere the user explicitly presses a key to capture a frame and ask a question.\n\n```python\n# ── Main loop: NEVER freeze ───────────────────────────────────\n while self.running:\n    raw_frame = get_frame()\n    if raw_frame is None:\n        # End of video — wait for pending inference, show result 5s\n        if vlm_future and not vlm_future.done():\n            vlm_future.result(timeout=INFERENCE_TIMEOUT)\n        # hold last overlay 5 seconds\n        ...\n        break\n\n    frame = preprocess(raw_frame)\n\n    # Submit inference on timer (non-blocking)\n    if not self._inference_pending and time_elapsed >= self.interval:\n        self._inference_pending = True\n        vlm_future = self.executor.submit(self._analyze_frame, frame.copy())\n\n    # Check completion (non-blocking)\n    if vlm_future and vlm_future.done():\n        vlm_future.result()\n        vlm_future = None\n\n    # ALWAYS display live frame + overlay\n    display = self._draw_overlay(frame)\n    cv2.imshow(WINDOW_NAME, display)\n    cv2.waitKey(25)\n```\n\n## End-of-Video Handling\n\nShort videos (or any file input) will end before inference completes.\nThe app MUST:\n1. Wait for any pending `vlm_future` to finish when `get_frame()` returns `None`\n2. Redraw the overlay with the final result AFTER inference completes\n3. Hold the final frame on screen for a few seconds so the user can read it\n\n```python\nif raw_frame is None:\n    if vlm_future and not vlm_future.done():\n        logger.info(\"Video ended. Waiting for pending inference...\")\n        vlm_future.result(timeout=INFERENCE_TIMEOUT)\n    # Show final result for 5 seconds\n    if last_good_frame is not None:\n        end_time = time.time() + 5\n        while time.time() \u003C end_time:\n            display = self._draw_overlay(last_good_frame)\n            cv2.imshow(WINDOW_NAME, display)\n            if cv2.waitKey(100) & 0xFF == ord(\"q\"):\n                break\n    break\n```\n\n## Event Tracker Pattern (for monitoring apps)\n\nWhen building a monitoring-style app, create an `event_tracker.py` with:\n- `EventType` enum with activity categories\n- `Event` dataclass with timestamp, type, description\n- `EventTracker` class with `classify_response()`, `add_event()`, `get_summary()`\n\nKeyword matching in `classify_response()`:\n```python\ndef classify_response(self, vlm_response: str) -> EventType:\n    response_lower = vlm_response.lower()\n    for event_type, keywords in self.keyword_map.items():\n        if any(kw in response_lower for kw in keywords):\n            return event_type\n    return EventType.IDLE\n```\n\n## Registration Checklist\n\nCommunity apps do NOT need registration. These steps are only needed after promotion:\n1. `hailo_apps\u002Fpython\u002Fcore\u002Fcommon\u002Fdefines.py` — app name constant (e.g. `MY_VLM_APP = \"my_vlm_app\"`)\n2. `hailo_apps\u002Fconfig\u002Fresources_config.yaml` — model mapping (e.g. `my_vlm_app: *vlm_chat_app`)\n\n\n## Lessons Learned (from real builds)\n\n### 1. YAML Alias Placement Breaks Config\nWhen adding `new_app: *vlm_chat_app` to `resources_config.yaml`, inserting it between\nan existing key and its `models:` block breaks YAML parsing with a confusing error\nreferencing a distant line. **Always insert after the full preceding block** and\nvalidate with `yaml.safe_load()`.\n\n### 2. Custom CLI Args Must Come Before handle_list_models_flag()\nIf you add `parser.add_argument(\"--interval\", ...)` after `handle_list_models_flag()`,\nthe argument won't appear in `--help` output. The flag handler does `parse_known_args()`\nwhich triggers argparse's help rendering before your arg is registered. Solution:\nadd all custom arguments **before** calling `handle_list_models_flag()`.\n\n### 3. MAX_TOKENS Too High → Repetitive VLM Output\nQwen2-VL with `MAX_TOKENS=300` on short prompts produces verbose, looping text\n(the same sentences repeated). For monitoring apps, use `MAX_TOKENS=100`–`150` and\nreinforce brevity in the prompt: `\"Be concise — one or two sentences maximum.\"`\n\n### 4. Event Keyword Classification — First Match Wins\nThe keyword classifier matches the first `EventType` whose keywords appear in the\nresponse. Generic words (\"food\", \"floor\") can trigger the wrong category. Fix by:\n- Ordering categories from most-specific to least-specific\n- Using specific action verbs (\"sniffing\", \"chewing\") instead of nouns\n- Or: instruct the VLM to output a single label from a fixed list\n\n### 5. Check Video Duration Before Launch\nA 60s video at `--interval 15` with 5-45s inference time yields only 1-2 observations.\nAlways check the video duration first and set interval accordingly:\n```bash\nffprobe -v quiet -show_entries format=duration -of default=noprint_wrappers=1 video.mp4\n```\n\n### 6. python3 Not python — Ubuntu Has No `python` Binary\nOn Ubuntu\u002FDebian, `python` doesn't exist by default. Always use `python3` in\nterminal commands and documentation. The `setup_env.sh` activates the venv but\ndoes NOT create a `python` → `python3` alias.\n\n### 7. YAML Edits Fail on Whitespace Mismatches\nWhen editing `resources_config.yaml` with `replace_string_in_file`, the match\nmust be byte-exact including trailing spaces. If the first attempt fails, re-read\nthe target lines with `read_file` and copy the exact whitespace. Include 3-5\ncontext lines from the actual file output, not from memory.\n\n### 8. Validation Script Is the Single Gate Check\nThe `validate_app.py` script runs 11 static checks (file existence, syntax, imports,\nconventions, unused imports, unreachable code, README quality). With `--smoke-test`, it also runs 2 runtime checks: CLI `--help` and module\nimport tests (gracefully skipping on non-Hailo systems). If all checks pass, the app is\nconvention-compliant. Run it once at the end instead of manual grep checks — it catches everything.\n\n### 9. Auto-Approve Eliminates 46+ Clicks Per Build\nAdd `\"chat.tools.autoApprove\": true` to `.vscode\u002Fsettings.json` for fully\nautonomous agentic workflows. Without it, every tool call requires manual approval.\n\n## Common VLM Prompts\n\n| Variant | System Prompt | User Prompt |\n|---|---|---|\n| Scene monitor | \"Monitor the scene. Report activities.\" | \"What is happening in the scene right now?\" |\n| Safety | \"You are a safety inspector.\" | \"List any safety hazards visible\" |\n| Scene | \"Describe what you see concisely.\" | \"Describe the image\" |\n| Counter | \"Count objects precisely. Reply JSON.\" | \"Count all {objects}\" |\n| Traffic | \"Analyze traffic patterns.\" | \"Describe traffic and vehicles\" |\n\n","# Skill: Build VLM Application\n\nBuild a complete Vision-Language Model application that uses the Hailo-10H VLM for image understanding.\n\n## When This Skill Is Loaded\n\n- User wants to build an app that **looks at camera images and answers questions**\n- User needs **visual scene understanding** (describe, count, detect, analyze)\n- User wants a variant of the VLM Chat app with different behavior\n- User mentions: VLM, vision, image understanding, camera monitoring, scene analysis\n\n## Reference Implementation\n\nStudy `hailo_apps\u002Fpython\u002Fgen_ai_apps\u002Fvlm_chat\u002F` — the canonical VLM app:\n- `vlm_chat.py` — State machine app with camera loop\n- `backend.py` — Multiprocessing VLM inference backend (REUSE this, don't copy)\n\n## Build Process\n\n### Step 1: Create App Directory\n\nCreate the app directory:\n\n```\nhailo_apps\u002Fpython\u002F\u003Ctype>\u002F\u003Capp_name>\u002F\n├── app.yaml              # App manifest (required)\n├── run.sh                # Launch wrapper (sets PYTHONPATH)\n├── __init__.py           # Empty\n├── \u003Capp_name>.py         # Main app class + entry point\n├── event_tracker.py      # Optional: event classification (for monitoring apps)\n└── README.md             # Usage documentation (REQUIRED — never skip)\n```\n\n**app.yaml** — required manifest:\n```yaml\nname: \u003Capp_name>\ntitle: My VLM App\ndescription: One-line description\nauthor: AI Agent (auto-generated)\ndate: \"YYYY-MM-DD\"\ntype: gen_ai\nhailo_arch: hailo10h\nmodel: Qwen2-VL-2B-Instruct\ntags: [vlm, monitoring]\nstatus: draft\n```\n\n**run.sh** — launch wrapper:\n```bash\n#!\u002Fbin\u002Fbash\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"$SCRIPT_DIR\u002F..\u002F..\u002F..\" && pwd)\"\nPYTHONPATH=\"$REPO_ROOT\" python3 \"$SCRIPT_DIR\u002F\u003Capp_name>.py\" \"$@\"\n```\n\n**NOTE**: Do NOT register in `defines.py` or `resources_config.yaml`.\nCommunity apps are run via `run.sh`. Registration happens during promotion\n\n### Step 2: Build the App\n\nThe main app file follows this structure:\n```python\nimport os\nimport sys\nimport cv2\nimport signal\nimport time\nfrom typing import Optional\n\nos.environ[\"QT_QPA_PLATFORM\"] = 'xcb'\n\nfrom hailo_apps.python.gen_ai_apps.vlm_chat.backend import Backend\nfrom hailo_apps.python.core.common.core import (\n    get_standalone_parser, resolve_hef_path, handle_list_models_flag\n)\nfrom hailo_apps.python.core.common.defines import (\n    HAILO10H_ARCH, USB_CAMERA\n)\nfrom hailo_apps.python.core.common.camera_utils import get_usb_video_devices\nfrom hailo_apps.python.core.common.hailo_logger import get_logger\n\nlogger = get_logger(__name__)\n\nAPP_NAME = \"my_vlm_app\"\n\nSYSTEM_PROMPT = \"Your system prompt here...\"\nMONITOR_PROMPT = \"Your per-frame VLM question here...\"\n\nclass MyVLMApp:\n    def __init__(self, camera, camera_type, args):\n        self.camera = camera\n        self.camera_type = camera_type\n        self.running = True\n        self.backend = None\n        signal.signal(signal.SIGINT, self.signal_handler)\n        # Initialize Backend, EventTracker, etc.\n\n    def signal_handler(self, sig, frame):\n        self.running = False\n\n    def run(self):\n        # Main loop: capture frame, display, analyze periodically\n        pass\n\n    def cleanup(self):\n        if self.backend:\n            self.backend.close()\n        cv2.destroyAllWindows()\n\ndef main():\n    parser = get_standalone_parser()\n    # IMPORTANT: Add ALL custom args BEFORE handle_list_models_flag\n    # so they appear in --help output\n    parser.add_argument(\"--interval\", type=int, default=15, help=\"Seconds between analyses\")\n    handle_list_models_flag(parser, APP_NAME)\n    args = parser.parse_args()\n    hef_path = resolve_hef_path(args.hef_path, app_name=APP_NAME, arch=HAILO10H_ARCH)\n    # Camera setup, app.run()\n\nif __name__ == \"__main__\":\n    main()\n```\n\n### Step 4: Validate\n\nRun the automated validation script (includes static checks + runtime smoke tests):\n```bash\npython3 .github\u002Fscripts\u002Fvalidate_app.py hailo_apps\u002Fpython\u002Fgen_ai_apps\u002F\u003Capp_name> --smoke-test\n```\n\n### Step 5: Write README\n\nInclude: description, requirements, usage CLI, architecture, customization notes.\n\n## Key Customization Points\n\n| What to Change | Where |\n|---|---|\n| System prompt | `SYSTEM_PROMPT` constant |\n| Per-frame VLM question | `MONITOR_PROMPT` constant |\n| Image preprocessing | `Backend.convert_resize_image()` |\n| Inference parameters | `MAX_TOKENS`, `TEMPERATURE` |\n| Event classification | `EventTracker.classify_response()` |\n| Display overlay | OpenCV `cv2.putText()` in main loop |\n\n## Display & Output Best Practices\n\n### Window Size\nThe VLM crops images to 336×336 but this is too small for a display window.\nAlways resize to at least 640×640 for readability:\n```python\nDISPLAY_SIZE = (640, 640)\ndisplay = cv2.resize(frame, DISPLAY_SIZE, interpolation=cv2.INTER_LINEAR)\n```\n\n### Text Wrapping\nVLM responses can be long (100+ chars). Always wrap overlay text to fit the window:\n```python\n@staticmethod\ndef _wrap_text(text: str, max_chars: int = 70) -> list[str]:\n    words = text.split()\n    lines, current = [], \"\"\n    for word in words:\n        if current and len(current) + 1 + len(word) > max_chars:\n            lines.append(current)\n            current = word\n        else:\n            current = f\"{current} {word}\".strip() if current else word\n    if current:\n        lines.append(current)\n    return lines or [\"\"]\n```\nMake the banner height dynamic: `banner_h = 35 + 22 * len(desc_lines)`.\n\n### Print Activity to Terminal\n`logger.info()` may not be visible at default log level. Always `print()` event\nclassifications so the user sees them:\n```python\nprint(f\"\\n[{event.time_str}] Activity: {event.event_type.value}\")\nprint(f\"  {answer}\")\n```\n\n## Video Playback During Inference (CRITICAL)\n\n**NEVER freeze video playback during VLM inference.** VLM inference takes 10-30 seconds.\nFreezing the display makes the app feel broken and wastes most of the video.\n\nThe correct pattern for continuous monitoring apps:\n- Video keeps playing normally at all times\n- Inference runs in a background thread via `ThreadPoolExecutor`\n- Track `_inference_pending` flag to avoid submitting overlapping requests\n- When inference completes, update the overlay with the result\n- The overlay shows the *latest* result while live video continues\n\nFreezing is ONLY appropriate for interactive capture-and-ask apps (like `vlm_chat`)\nwhere the user explicitly presses a key to capture a frame and ask a question.\n\n```python\n# ── Main loop: NEVER freeze ───────────────────────────────────\n while self.running:\n    raw_frame = get_frame()\n    if raw_frame is None:\n        # End of video — wait for pending inference, show result 5s\n        if vlm_future and not vlm_future.done():\n            vlm_future.result(timeout=INFERENCE_TIMEOUT)\n        # hold last overlay 5 seconds\n        ...\n        break\n\n    frame = preprocess(raw_frame)\n\n    # Submit inference on timer (non-blocking)\n    if not self._inference_pending and time_elapsed >= self.interval:\n        self._inference_pending = True\n        vlm_future = self.executor.submit(self._analyze_frame, frame.copy())\n\n    # Check completion (non-blocking)\n    if vlm_future and vlm_future.done():\n        vlm_future.result()\n        vlm_future = None\n\n    # ALWAYS display live frame + overlay\n    display = self._draw_overlay(frame)\n    cv2.imshow(WINDOW_NAME, display)\n    cv2.waitKey(25)\n```\n\n## End-of-Video Handling\n\nShort videos (or any file input) will end before inference completes.\nThe app MUST:\n1. Wait for any pending `vlm_future` to finish when `get_frame()` returns `None`\n2. Redraw the overlay with the final result AFTER inference completes\n3. Hold the final frame on screen for a few seconds so the user can read it\n\n```python\nif raw_frame is None:\n    if vlm_future and not vlm_future.done():\n        logger.info(\"Video ended. Waiting for pending inference...\")\n        vlm_future.result(timeout=INFERENCE_TIMEOUT)\n    # Show final result for 5 seconds\n    if last_good_frame is not None:\n        end_time = time.time() + 5\n        while time.time() \u003C end_time:\n            display = self._draw_overlay(last_good_frame)\n            cv2.imshow(WINDOW_NAME, display)\n            if cv2.waitKey(100) & 0xFF == ord(\"q\"):\n                break\n    break\n```\n\n## Event Tracker Pattern (for monitoring apps)\n\nWhen building a monitoring-style app, create an `event_tracker.py` with:\n- `EventType` enum with activity categories\n- `Event` dataclass with timestamp, type, description\n- `EventTracker` class with `classify_response()`, `add_event()`, `get_summary()`\n\nKeyword matching in `classify_response()`:\n```python\ndef classify_response(self, vlm_response: str) -> EventType:\n    response_lower = vlm_response.lower()\n    for event_type, keywords in self.keyword_map.items():\n        if any(kw in response_lower for kw in keywords):\n            return event_type\n    return EventType.IDLE\n```\n\n## Registration Checklist\n\nCommunity apps do NOT need registration. These steps are only needed after promotion:\n1. `hailo_apps\u002Fpython\u002Fcore\u002Fcommon\u002Fdefines.py` — app name constant (e.g. `MY_VLM_APP = \"my_vlm_app\"`)\n2. `hailo_apps\u002Fconfig\u002Fresources_config.yaml` — model mapping (e.g. `my_vlm_app: *vlm_chat_app`)\n\n\n## Lessons Learned (from real builds)\n\n### 1. YAML Alias Placement Breaks Config\nWhen adding `new_app: *vlm_chat_app` to `resources_config.yaml`, inserting it between\nan existing key and its `models:` block breaks YAML parsing with a confusing error\nreferencing a distant line. **Always insert after the full preceding block** and\nvalidate with `yaml.safe_load()`.\n\n### 2. Custom CLI Args Must Come Before handle_list_models_flag()\nIf you add `parser.add_argument(\"--interval\", ...)` after `handle_list_models_flag()`,\nthe argument won't appear in `--help` output. The flag handler does `parse_known_args()`\nwhich triggers argparse's help rendering before your arg is registered. Solution:\nadd all custom arguments **before** calling `handle_list_models_flag()`.\n\n### 3. MAX_TOKENS Too High → Repetitive VLM Output\nQwen2-VL with `MAX_TOKENS=300` on short prompts produces verbose, looping text\n(the same sentences repeated). For monitoring apps, use `MAX_TOKENS=100`–`150` and\nreinforce brevity in the prompt: `\"Be concise — one or two sentences maximum.\"`\n\n### 4. Event Keyword Classification — First Match Wins\nThe keyword classifier matches the first `EventType` whose keywords appear in the\nresponse. Generic words (\"food\", \"floor\") can trigger the wrong category. Fix by:\n- Ordering categories from most-specific to least-specific\n- Using specific action verbs (\"sniffing\", \"chewing\") instead of nouns\n- Or: instruct the VLM to output a single label from a fixed list\n\n### 5. Check Video Duration Before Launch\nA 60s video at `--interval 15` with 5-45s inference time yields only 1-2 observations.\nAlways check the video duration first and set interval accordingly:\n```bash\nffprobe -v quiet -show_entries format=duration -of default=noprint_wrappers=1 video.mp4\n```\n\n### 6. python3 Not python — Ubuntu Has No `python` Binary\nOn Ubuntu\u002FDebian, `python` doesn't exist by default. Always use `python3` in\nterminal commands and documentation. The `setup_env.sh` activates the venv but\ndoes NOT create a `python` → `python3` alias.\n\n### 7. YAML Edits Fail on Whitespace Mismatches\nWhen editing `resources_config.yaml` with `replace_string_in_file`, the match\nmust be byte-exact including trailing spaces. If the first attempt fails, re-read\nthe target lines with `read_file` and copy the exact whitespace. Include 3-5\ncontext lines from the actual file output, not from memory.\n\n### 8. Validation Script Is the Single Gate Check\nThe `validate_app.py` script runs 11 static checks (file existence, syntax, imports,\nconventions, unused imports, unreachable code, README quality). With `--smoke-test`, it also runs 2 runtime checks: CLI `--help` and module\nimport tests (gracefully skipping on non-Hailo systems). If all checks pass, the app is\nconvention-compliant. Run it once at the end instead of manual grep checks — it catches everything.\n\n### 9. Auto-Approve Eliminates 46+ Clicks Per Build\nAdd `\"chat.tools.autoApprove\": true` to `.vscode\u002Fsettings.json` for fully\nautonomous agentic workflows. Without it, every tool call requires manual approval.\n\n## Common VLM Prompts\n\n| Variant | System Prompt | User Prompt |\n|---|---|---|\n| Scene monitor | \"Monitor the scene. Report activities.\" | \"What is happening in the scene right now?\" |\n| Safety | \"You are a safety inspector.\" | \"List any safety hazards visible\" |\n| Scene | \"Describe what you see concisely.\" | \"Describe the image\" |\n| Counter | \"Count objects precisely. Reply JSON.\" | \"Count all {objects}\" |\n| Traffic | \"Analyze traffic patterns.\" | \"Describe traffic and vehicles\" |\n\n",{"name":5,"description":7},"main","9beb824677028b4974e90b0cc558b6fd6de3d935",".github\u002Fskills\u002Fhl-build-vlm-app","https:\u002F\u002Fgithub.com\u002FTejasLamba2006\u002Fhailo-apps","https:\u002F\u002Fgithub.com\u002FTejasLamba2006","https:\u002F\u002Ftejas.is-a.dev\u002F","Rust | Java |  TypeScript ","2026-11-12 16:44:30",{"source":8,"skillName":5,"commands":33,"prompt":43,"zipAvailable":44},[34,37,40],{"tool":35,"command":36},"npx","npx skills add TejasLamba2006\u002Fhailo-apps --skill hl-build-vlm-app",{"tool":38,"command":39},"bunx","bunx skills add TejasLamba2006\u002Fhailo-apps --skill hl-build-vlm-app",{"tool":41,"command":42},"pnpm","pnpm dlx skills add TejasLamba2006\u002Fhailo-apps --skill hl-build-vlm-app","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FTejasLamba2006\u002Fhailo-apps\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fhl-build-vlm-app\u002FSKILL.md",true,[],[47,53,60,67,74,81],{"skillNo":48,"name":49,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":50,"categories":51,"keywords":52,"publishedAt":19},"01M752Y17N6R7ME2TRA2NNS5X3","hl-build-voice-app","https:\u002F\u002Fgithub.com\u002FTejasLamba2006\u002Fhailo-apps\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fhl-build-voice-app\u002FSKILL.md",[],[],{"skillNo":54,"name":55,"nameZh":6,"description":56,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":57,"categories":58,"keywords":59,"publishedAt":19},"01M752XT54VT5E95QASFX7F8JR","hl-build-standalone-app","Build a complete standalone app using HailoInfer + OpenCV on Hailo-8\u002F8L\u002F10H.","https:\u002F\u002Fgithub.com\u002FTejasLamba2006\u002Fhailo-apps\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fhl-build-standalone-app\u002FSKILL.md",[],[],{"skillNo":61,"name":62,"nameZh":6,"description":63,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":64,"categories":65,"keywords":66,"publishedAt":19},"01M752XNJFF5KT07AZAZ6N07BP","hl-build-pipeline-app","Build a complete GStreamer pipeline app for real-time video processing on Hailo-8\u002F8L\u002F10H.","https:\u002F\u002Fgithub.com\u002FTejasLamba2006\u002Fhailo-apps\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fhl-build-pipeline-app\u002FSKILL.md",[],[],{"skillNo":68,"name":69,"nameZh":6,"description":70,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":71,"categories":72,"keywords":73,"publishedAt":19},"01M752XK1JS2XC5BF31EVA06XP","hl-build-llm-app","Build a complete LLM text generation app running on Hailo-10H.","https:\u002F\u002Fgithub.com\u002FTejasLamba2006\u002Fhailo-apps\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fhl-build-llm-app\u002FSKILL.md",[],[],{"skillNo":75,"name":76,"nameZh":6,"description":77,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":78,"categories":79,"keywords":80,"publishedAt":19},"01M752XFTEGGYMFEWZVECY31HM","hl-build-agent-app","Build a complete agent app with LLM reasoning + tool execution on Hailo-10H.","https:\u002F\u002Fgithub.com\u002FTejasLamba2006\u002Fhailo-apps\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fhl-build-agent-app\u002FSKILL.md",[],[],{"skillNo":82,"name":83,"nameZh":6,"description":84,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":85,"categories":86,"keywords":87,"publishedAt":19},"01M752XBWXEB5MRRHGAQBK6JHC","hl-validate","Validation at every phase gate.","https:\u002F\u002Fgithub.com\u002FTejasLamba2006\u002Fhailo-apps\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fhl-validate\u002FSKILL.md",[],[]]