hailo-apps / rules
Cursor rule506 starsChanged 7 months agoMITView on GitHub
Build a complete GStreamer pipeline app for real-time video processing on Hailo-8/8L/10H.
We split this hl-build-pipeline-app.mdc into the rules it gives an agent
13 rules. About 0 are common, 1 is distinctive, and 12 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
12
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 23
Read this SKILL.md (full file, single read) + common_pitfalls.md.
- Distinctive: line 29
Create the app directory:
- Very specific: line 40
Create app.yaml with type: pipeline and run.sh wrapper.
- Very specific: line 41
Do NOT register in defines.py or resources_config.yaml.
- Very specific: line 135
USB camera input: Always use --input usb for USB cameras — the framework auto-detects the correct device.
- Very specific: line 137
Pipeline composition: Use helper functions — SOURCE_PIPELINE, INFERENCE_PIPELINE, DISPLAY_PIPELINE
- Very specific: line 139
Resolution preservation: Use INFERENCE_PIPELINE_WRAPPER for full-res display
- Very specific: line 142
VAAPI: Add QUEUE("vaapi_queue") + vaapi_convert_pipeline for HW decode
- Very specific: line 159
When you need to draw on frames (overlays, game graphics, custom visualizations), use the use_frame pattern:
- Very specific: line 213
Must convert to BGR with cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) before set_frame()
- Very specific: line 214
Always check user_data.use_frame and that frame is not None
- Very specific: line 219
When the user wants a custom background image (not the live camera feed), use background.copy() — never blend the camera feed with the …
- Very specific: line 323
When subclassing, you get the full pipeline for free — just provide your custom app_callback and app_callback_class.
≈ “Common” is an estimate: at least 1 in 5 of the 6,350 Cursor rule files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
---
description: "Build a complete GStreamer pipeline app for real-time video processing on Hailo-8/8L/10H."
alwaysApply: false
---
# Skill: Build GStreamer Pipeline Application
Build a complete GStreamer pipeline app for real-time video processing on Hailo-8/8L/10H.
## When This Skill Is Loaded
- User wants **real-time video processing** (detection, pose, segmentation)
- User mentions: GStreamer, pipeline, stream, FPS, real-time video, tracking
- User needs a video app with **high throughput** rather than AI understanding
## Reference Implementations
The canonical pipeline app is `detection/`. Other examples: `pose_estimation/`, `instance_segmentation/`, `face_recognition/`.
**Do NOT read these source files.** This SKILL.md contains all patterns needed to build any pipeline app. The sections below cover: basic pipelines, frame overlays, custom backgrounds, pose extraction, detection data, and subclassing existing pipeline classes.
### Minimum Context for Any Pipeline App
Read this SKILL.md (full file, single read) + `common_pitfalls.md`. That's it. Build immediately.
## Build Process
### Step 1: Create App Directory
Create the app directory:
```
hailo_apps/python/<type>/<app_name>/
├── app.yaml # App manifest (required)
├── run.sh # Launch wrapper
├── __init__.py
├── <app_name>.py # Main app
└── README.md # Usage documentation (REQUIRED — never skip)
```
Create `app.yaml` with `type: pipeline` and `run.sh` wrapper.
Do NOT register in `defines.py` or `resources_config.yaml`.
### Step 2: Build Main App
```python
import gi
gi.require_version('Gst', '1.0')
from gi.repository import Gst
import hailo # Required for detection/landmark extraction in callbacks
from hailo_apps.python.core.common.hailo_logger import get_logger
from hailo_apps.python.core.common.core import resolve_hef_path, handle_list_models_flag
from hailo_apps.python.core.common.parser import get_pipeline_parser
# If your app uses resolve_hef_path with an app name, register it in defines.py.
# Otherwise use a local string constant:
# APP_NAME = "my_pipeline_app"
from hailo_apps.python.core.gstreamer.gstreamer_app import GStreamerApp, app_callback_class
from hailo_apps.python.core.gstreamer.gstreamer_helper_pipelines import (
SOURCE_PIPELINE,
INFERENCE_PIPELINE,
INFERENCE_PIPELINE_WRAPPER,
DISPLAY_PIPELINE,
TRACKER_PIPELINE,
USER_CALLBACK_PIPELINE,
QUEUE,
)
from hailo_apps.python.core.common.buffer_utils import (
get_caps_from_pad,
get_numpy_from_buffer,
)
logger = get_logger(__name__)
APP_NAME = "my_pipeline_app"
class UserAppCallback(app_callback_class):
"""Custom callback class for per-frame state."""
def __init__(self):
super().__init__()
self.detection_count = 0
def app_callback(element, buffer, user_data):
"""Per-frame callback — runs on every GStreamer buffer."""
# Access detections from buffer
# user_data.detection_count += len(detections)
return Gst.FlowReturn.OK
class MyPipelineApp(GStreamerApp):
def __init__(self, app_callback, user_data, parser=None):
parser = parser or get_pipeline_parser()
handle_list_models_flag(parser, APP_NAME)
args = parser.parse_args()
super().__init__(args, user_data)
self.hef_path = resolve_hef_path(args.hef_path, APP_NAME, self.arch)
logger.info("HEF: %s", self.hef_path)
def get_pipeline_string(self):
return (
SOURCE_PIPELINE(self.video_source, self.arch)
+ " ! "
+ INFERENCE_PIPELINE(
hef_path=self.hef_path,
batch_size=self.batch_size,
)
+ " ! "
+ USER_CALLBACK_PIPELINE()
+ " ! "
+ DISPLAY_PIPELINE(video_sink=self.video_sink, sync=self.sync)
)
def main():
user_data = UserAppCallback()
app = MyPipelineApp(app_callback, user_data)
app.run()
if __name__ == "__main__":
main()
```
### Step 4: Validate
```bash
python3 .hailo/scripts/validate_app.py hailo_apps/python/pipeline_apps/my_pipeline_app --smoke-test
```
## Critical Conventions
0. **USB camera input**: Always use `--input usb` for USB cameras — the framework auto-detects the correct device. **NEVER** hardcode `/dev/video0` — that is often the integrated webcam, not the USB camera. If you need a specific device, run `v4l2-ctl --list-devices` first.
1. **CLI parser**: `get_pipeline_parser()` (NOT `get_standalone_parser()`)
2. **Pipeline composition**: Use helper functions — `SOURCE_PIPELINE`, `INFERENCE_PIPELINE`, `DISPLAY_PIPELINE`
3. **Callback**: `app_callback(element, buffer, user_data)` — never call `user_data.increment()`
4. **Resolution preservation**: Use `INFERENCE_PIPELINE_WRAPPER` for full-res display
5. **Tracking**: `TRACKER_PIPELINE()` for ByteTrack
6. **Cascaded inference**: `CROPPER_PIPELINE()` for crop → second model
7. **VAAPI**: Add `QUEUE("vaapi_queue") + vaapi_convert_pipeline` for HW decode
## Common Patterns
| Pattern | Helper | Use Case |
|---|---|---|
| Basic inference | `INFERENCE_PIPELINE(hef_path=...)` | Single model |
| With tracking | `+ TRACKER_PIPELINE()` | Object tracking |
| With user callback | `+ USER_CALLBACK_PIPELINE()` | Per-frame processing |
| Cascaded | `CROPPER_PIPELINE(...)` | Face detection → recognition |
| Multi-source | Multiple `SOURCE_PIPELINE` + compositor | Dashboard view |
| Tiling | Custom tiling pipeline | Small object detection |
---
## Frame Overlay Pattern (use_frame + OpenCV Drawing)
When you need to draw on frames (overlays, game graphics, custom visualizations), use the `use_frame` pattern:
### 1. Enable use_frame in your callback class
```python
class UserAppCallback(app_callback_class):
def __init__(self):
super().__init__()
self.use_frame = True # Enables frame access in callback
```
**CRITICAL**: Setting `use_frame = True` in the callback class alone is NOT enough when subclassing a pipeline class (e.g., `GStreamerPoseEstimationApp`). `GStreamerApp.__init__()` overwrites `user_data.use_frame` from the CLI default (`False`). You MUST also force it in the app class:
```python
class MyApp(GStreamerPoseEstimationApp):
def __init__(self, app_callback, user_data, parser=None):
super().__init__(app_callback, user_data, parser)
self.options_menu.use_frame = True # starts display process
user_data.use_frame = True # enables frame extraction
```
Without this, `set_frame()` calls are silently ignored and only the raw camera feed is shown.
### 2. Get the frame in the callback
```python
import cv2
import hailo
from hailo_apps.python.core.common.buffer_utils import get_caps_from_pad, get_numpy_from_buffer
def app_callback(element, buffer, user_data):
pad = element.get_static_pad("src")
format, width, height = get_caps_from_pad(pad)
frame = None
if user_data.use_frame and format and width and height:
# Signature: get_numpy_from_buffer(buffer, format, width, height)
# Returns RGB numpy array (H, W, 3)
frame = get_numpy_from_buffer(buffer, format, width, height)
```
### 3. Draw with OpenCV, convert RGB→BGR, then set_frame()
```python
if user_data.use_frame and frame is not None:
# Draw on the frame (frame is RGB from GStreamer)
cv2.circle(frame, (x, y), 10, (0, 255, 0), -1)
cv2.putText(frame, "Hello", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255), 2)
# CRITICAL: Convert RGB → BGR before set_frame (OpenCV expects BGR)
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
user_data.set_frame(frame)
return Gst.FlowReturn.OK
```
**Key rules:**
- `get_numpy_from_buffer(buffer, format, width, height)` — NOT `(buffer, pad, format)`
- Frame comes in **RGB** from GStreamer
- Must convert to **BGR** with `cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)` before `set_frame()`
- Always check `user_data.use_frame` and that `frame is not None`
- Pass `--use-frame` on CLI to enable (or set `self.use_frame = True` in callback class)
### Custom Background Pattern (games, virtual scenes)
When the user wants a **custom background image** (not the live camera feed), use `background.copy()` — **never blend** the camera feed with the background via `addWeighted` or similar.
```python
# ✅ CORRECT — background only, no camera feed visible
if self.background is not None:
output = self.background.copy()
else:
output = np.zeros_like(frame)
# Draw game elements on output...
# Draw hand/body markers from pose data on output...
output = cv2.cvtColor(output, cv2.COLOR_RGB2BGR)
user_data.set_frame(output)
```
```python
# ❌ WRONG — blends camera feed, user sees themselves + background = confusing
output = cv2.addWeighted(self.background, 0.4, frame, 0.6, 0)
```
**Rule**: If the user provides a background image or asks for a virtual scene, the camera feed is used **only for pose/detection data extraction** — it must NOT appear in the rendered output. The `frame` from `get_numpy_from_buffer()` is still needed to extract detections, but the display output should be `background.copy()` with game elements drawn on top.
---
## Detection Data Extraction in Callbacks
### Getting ROI and Detections
```python
import hailo
def app_callback(element, buffer, user_data):
roi = hailo.get_roi_from_buffer(buffer)
detections = roi.get_objects_typed(hailo.HAILO_DETECTION)
for detection in detections:
label = detection.get_label() # e.g., "person", "car"
confidence = detection.get_confidence() # 0.0 - 1.0
bbox = detection.get_bbox() # Normalized bounding box
# bbox.xmin(), bbox.ymin(), bbox.width(), bbox.height() — all normalized [0,1]
```
### Getting Track IDs (requires TRACKER_PIPELINE in pipeline)
```python
track_id = 0
track = detection.get_objects_typed(hailo.HAILO_UNIQUE_ID)
if len(track) == 1:
track_id = track[0].get_id()
```
### Getting Pose Landmarks (pose estimation models)
```python
landmarks = detection.get_objects_typed(hailo.HAILO_LANDMARKS)
if landmarks:
points = landmarks[0].get_points()
# Each point has .x() and .y() — normalized to bounding box
# Convert to pixel coordinates:
for point in points:
pixel_x = int((point.x() * bbox.width() + bbox.xmin()) * frame_width)
pixel_y = int((point.y() * bbox.height() + bbox.ymin()) * frame_height)
```
### Hailo Detection Types Reference
| Type | Constant | Method | Returns |
|---|---|---|---|
| Detection boxes | `hailo.HAILO_DETECTION` | `roi.get_objects_typed()` | List of detections |
| Track IDs | `hailo.HAILO_UNIQUE_ID` | `detection.get_objects_typed()` | List with single ID object |
| Pose landmarks | `hailo.HAILO_LANDMARKS` | `detection.get_objects_typed()` | List of landmark sets |
| Classification | `hailo.HAILO_CLASSIFICATION` | `detection.get_objects_typed()` | List of classifications |
| Masks | `hailo.HAILO_CONF_CLASS_MASK` | `detection.get_objects_typed()` | Segmentation masks |
---
## Reusing Existing Pipeline Classes
For apps that extend existing pipelines (e.g., a pose estimation game), **subclass the domain-specific pipeline class** instead of the base `GStreamerApp`:
```python
from hailo_apps.python.pipeline_apps.pose_estimation.pose_estimation_pipeline import (
GStreamerPoseEstimationApp,
)
class MyPoseGame(GStreamerPoseEstimationApp):
"""Inherits full pose pipeline: SOURCE → INFERENCE → TRACKER → USER_CALLBACK → DISPLAY"""
pass # Pipeline is already configured — just write your callback
def app_callback(element, buffer, user_data):
# All pose detection data is available here
...
def main():
user_data = UserAppCallback()
app = MyPoseGame(app_callback, user_data) # No pipeline config needed
app.run()
```
**Available pipeline classes to subclass:**
| Class | Module | Pipeline includes |
|---|---|---|
| `GStreamerPoseEstimationApp` | `pose_estimation.pose_estimation_pipeline` | Inference + Tracker + User Callback |
| `GStreamerDetectionApp` | `detection.detection_pipeline` | Inference + User Callback |
| `GStreamerInstanceSegmentationApp` | `instance_segmentation.instance_segmentation_pipeline` | Inference + User Callback |
| `GStreamerFaceRecognitionApp` | `face_recognition.face_recognition_pipeline` | Cascaded inference + Tracker |
When subclassing, you get the full pipeline for free — just provide your custom `app_callback` and `app_callback_class`.
Where files are headingNew Cursor rule files have more rules about planning and tracking tasks, and fewer about code style.
Of the Cursor rule files we read, the 3,978 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository65 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- .cursor/rules/core-framework.mdcCursor rule
- .cursor/rules/gen-ai-apps.mdcCursor rule
- .cursor/rules/hailo-global.mdcCursor rule
- .cursor/rules/hl-build-agent-app.mdcCursor rule
- .cursor/rules/hl-build-llm-app.mdcCursor rule
- .cursor/rules/hl-build-standalone-app.mdcCursor rule
- .cursor/rules/hl-build-vlm-app.mdcCursor rule
- .cursor/rules/hl-build-voice-app.mdcCursor rule
- .cursor/rules/hl-camera.mdcCursor rule
- .cursor/rules/hl-event-detection.mdcCursor rule
- .cursor/rules/hl-model-management.mdcCursor rule
- .cursor/rules/hl-monitoring.mdcCursor rule
- .cursor/rules/hl-plan-and-execute.mdcCursor rule
- .cursor/rules/hl-validate.mdcCursor rule
- .cursor/rules/inst-agent-protocols.mdcCursor rule
- .cursor/rules/inst-architecture.mdcCursor rule
- .cursor/rules/inst-coding-standards.mdcCursor rule
- .cursor/rules/inst-gen-ai-development.mdcCursor rule
- .cursor/rules/inst-gstreamer-pipelines.mdcCursor rule
- .cursor/rules/inst-orchestration.mdcCursor rule
- .cursor/rules/inst-testing-patterns.mdcCursor rule
- .cursor/rules/memory-camera_and_display.mdcCursor rule
- .cursor/rules/memory-common_pitfalls.mdcCursor rule
- .cursor/rules/memory-gen_ai_patterns.mdcCursor rule
- .cursor/rules/memory-hailo_platform_api.mdcCursor rule
- .cursor/rules/memory-MEMORY.mdcCursor rule
- .cursor/rules/memory-pipeline_optimization.mdcCursor rule
- .cursor/rules/pipeline-apps.mdcCursor rule
- .cursor/rules/prompt-new-agent-tool.mdcCursor rule
- .cursor/rules/prompt-new-llm-app.mdcCursor rule
- .cursor/rules/prompt-new-pipeline-app.mdcCursor rule
- .cursor/rules/prompt-new-pose-game.mdcCursor rule
- .cursor/rules/prompt-new-standalone-app.mdcCursor rule
- .cursor/rules/prompt-new-vlm-variant.mdcCursor rule
- .cursor/rules/prompt-new-voice-app.mdcCursor rule
- .cursor/rules/prompt-orchestrated-build.mdcCursor rule
- .cursor/rules/standalone-apps.mdcCursor rule
- .cursor/rules/tests.mdcCursor rule
- .cursor/rules/toolset-core-framework-api.mdcCursor rule
- .cursor/rules/toolset-gen-ai-utilities.mdcCursor rule
- .cursor/rules/toolset-gstreamer-elements.mdcCursor rule
- .cursor/rules/toolset-hailort-api.mdcCursor rule
- .cursor/rules/toolset-pose-keypoints.mdcCursor rule
- .cursor/rules/toolset-vlm-backend-api.mdcCursor rule
- .cursor/rules/toolset-yolo-coco-classes.mdcCursor rule
- hl-build-agent-appSkill
- hl-build-llm-appSkill
- hl-build-pipeline-appSkill
- hl-build-standalone-appSkill
- hl-build-vlm-appSkill
- hl-build-voice-appSkill
- hl-cameraSkill
- hl-event-detectionSkill
- hl-model-managementSkill
- hl-monitoringSkill
- hl-plan-and-executeSkill
- hl-validateSkill
- hl-build-agent-appSkill
- The first 60 shown.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

