Skip to content

Commit a9de8b8

Browse files
authored
feat: merge initial VENTUNO Q support (#160)
1 parent c267459 commit a9de8b8

130 files changed

Lines changed: 9101 additions & 113 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Cleanup - Delete branch buildcache
2+
3+
on:
4+
delete:
5+
6+
permissions:
7+
packages: write
8+
9+
jobs:
10+
cleanup-buildcache:
11+
if: github.event.ref_type == 'branch'
12+
runs-on: ubuntu-latest
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
image:
17+
# Base images
18+
- ghcr.io/${{ github.repository_owner }}/app-bricks/base
19+
- ghcr.io/${{ github.repository_owner }}/app-bricks/python-base
20+
- ghcr.io/${{ github.repository_owner }}/app-bricks/python-apps-base
21+
# AI Hub models
22+
- ghcr.io/${{ github.repository_owner }}/app-bricks/aihub-models-runner
23+
- ghcr.io/${{ github.repository_owner }}/app-bricks/ei-models-runner
24+
- ghcr.io/${{ github.repository_owner }}/app-bricks/llamacpp-models-runner
25+
- ghcr.io/${{ github.repository_owner }}/app-bricks/ollama-models-runner
26+
- ghcr.io/${{ github.repository_owner }}/app-bricks/gesture-recognition
27+
28+
steps:
29+
- name: Delete buildcache for branch ${{ github.event.ref }}
30+
run: |
31+
# Extract branch name (remove refs/heads/ prefix if present)
32+
BRANCH_NAME="${{ github.event.ref }}"
33+
BRANCH_NAME="${BRANCH_NAME#refs/heads/}"
34+
35+
# Convert branch name to safe tag format (replace / with -)
36+
CACHE_TAG="${BRANCH_NAME//\//-}-buildcache"
37+
38+
echo "Attempting to delete cache tag: $CACHE_TAG for image: ${{ matrix.image }}"
39+
40+
# Use GitHub API to delete the package version
41+
PACKAGE_NAME=$(echo "${{ matrix.image }}" | cut -d'/' -f3-)
42+
43+
# Get the package version ID for the cache tag
44+
VERSION_ID=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
45+
-H "Accept: application/vnd.github.v3+json" \
46+
"https://api.github.com/orgs/${{ github.repository_owner }}/packages/container/${PACKAGE_NAME}/versions" \
47+
| jq -r ".[] | select(.metadata.container.tags[] == \"${CACHE_TAG}\") | .id")
48+
49+
if [ -n "$VERSION_ID" ] && [ "$VERSION_ID" != "null" ]; then
50+
echo "Found cache version ID: $VERSION_ID. Deleting..."
51+
curl -X DELETE \
52+
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
53+
-H "Accept: application/vnd.github.v3+json" \
54+
"https://api.github.com/orgs/${{ github.repository_owner }}/packages/container/${PACKAGE_NAME}/versions/${VERSION_ID}"
55+
echo "Deleted cache tag: $CACHE_TAG"
56+
else
57+
echo "No cache found for tag: $CACHE_TAG (this is normal if the branch never built)"
58+
fi
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc)
2+
#
3+
# SPDX-License-Identifier: MPL-2.0
4+
5+
ARG DEBIAN_FRONTEND=noninteractive
6+
7+
FROM ubuntu:24.04
8+
9+
ENV PYTHONUNBUFFERED=1
10+
COPY requirements-base.txt .
11+
12+
# Install system dependencies and Python dependencies, then clean up apt caches and temporary files to reduce image size
13+
RUN apt-get update \
14+
&& apt-get install -y --no-install-recommends --no-install-suggests software-properties-common \
15+
&& apt-add-repository -y ppa:ubuntu-qcom-iot/qcom-ppa \
16+
&& apt-get update \
17+
&& apt-get install -y --no-install-recommends --no-install-suggests \
18+
netcat-openbsd \
19+
python3 \
20+
python3-pip \
21+
python3-venv \
22+
python-is-python3 \
23+
libqnn1 \
24+
qcom-fastrpc1 \
25+
&& apt-get remove -y python3-blinker \
26+
&& python -m pip install --no-cache-dir --break-system-packages -r requirements-base.txt \
27+
&& python -m compileall /usr/local/bin \
28+
&& python -m compileall /usr/local/lib \
29+
&& rm requirements-base.txt \
30+
&& apt-get remove -y software-properties-common \
31+
&& apt-get autoremove -y \
32+
&& apt-get dist-clean \
33+
&& rm -rf /var/lib/apt/lists/*
34+
35+
# Prepare the user
36+
# RUN groupadd -g 1000 arduino && useradd -u 1000 -g arduino -ms /bin/bash arduino
37+
# USER arduino
38+
39+
# Make the venv the default for child images
40+
RUN python -m venv --system-site-packages /opt/venv
41+
ENV PATH="/opt/venv/bin:${PATH}"
42+
ENV VIRTUAL_ENV="/opt/venv"
43+
44+
ENV PYTHONPATH="/opt"
45+
COPY aihub/ /opt/aihub/
46+
47+
# Prepare the app directory for child images
48+
RUN mkdir -p /app
49+
WORKDIR /app
50+
COPY main.py inference.py requirements.txt ./
51+
52+
ENTRYPOINT [ "python", "main.py", "--verbose", "--input", "websocket", "--output", "mjpeg", "websocket" ]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc)
2+
#
3+
# SPDX-License-Identifier: MPL-2.0
4+
5+
from aihub.app import AIHubApp
6+
from aihub.cli import parse_args, create_parser
7+
from aihub.base import InputSource, OutputSink
8+
9+
__all__ = [
10+
"AIHubApp",
11+
"parse_args",
12+
"create_parser",
13+
"InputSource",
14+
"OutputSink",
15+
]
16+
17+
__version__ = "0.1.0"
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc)
2+
#
3+
# SPDX-License-Identifier: MPL-2.0
4+
5+
import time
6+
from typing import Callable, List
7+
8+
import numpy as np
9+
10+
from aihub.logging import setup_logging, logger
11+
12+
13+
class AIHubApp:
14+
"""
15+
Main application class that orchestrates input sources and output sinks.
16+
17+
Example usage:
18+
def my_inference(frame: np.ndarray) -> tuple[np.ndarray, dict]:
19+
# ... your inference code here ...
20+
return annotated_frame, {}
21+
22+
app = AIHubApp(
23+
inference_cb=my_inference,
24+
input_type="gstreamer",
25+
output_types=["mjpeg", "websocket"],
26+
gst_source="v4l2src device=/dev/video0",
27+
gst_width=1024,
28+
gst_height=768,
29+
ws_output_port=5001,
30+
mjpeg_port=5002,
31+
)
32+
app.run()
33+
"""
34+
35+
def __init__(
36+
self,
37+
inference_cb: Callable[[np.ndarray], tuple[np.ndarray, dict]],
38+
input_type: str = "gstreamer",
39+
output_types: List[str] = ["mjpeg"],
40+
**kwargs,
41+
):
42+
"""
43+
Initialize AIHubApp.
44+
45+
Args:
46+
inference_cb: User callback for frame processing. Receives RGB frame,
47+
returns processed RGB frame and metadata.
48+
input_type: Input source type ("gstreamer" or "websocket").
49+
output_types: List of output sink types (accepted values are "mjpeg",
50+
"websocket"). Defaults to ["mjpeg"].
51+
**kwargs: Forwarded to input/output constructors based on prefix:
52+
- gst_* -> GStreamer input
53+
- ws_input_* -> WebSocket input
54+
- mjpeg_* -> MJPEG output
55+
- ws_output_* -> WebSocket output
56+
"""
57+
self._inference_cb = inference_cb
58+
self._input_type = input_type
59+
self._output_types = output_types
60+
self._kwargs = kwargs
61+
62+
self._input_source = None
63+
self._output_sinks: List = []
64+
self._is_running = False
65+
66+
# Setup logging
67+
self._verbose = kwargs.get("verbose", False)
68+
setup_logging(self._verbose)
69+
70+
# FPS tracking state
71+
self._fps_start_time = time.perf_counter()
72+
self._fps_frame_count = 0
73+
74+
def run(self) -> None:
75+
"""
76+
Start the application and block until stopped by Ctrl+C.
77+
"""
78+
try:
79+
self._setup()
80+
self._is_running = True
81+
82+
for sink in self._output_sinks:
83+
sink.start()
84+
85+
if self._input_source:
86+
self._input_source.start() # This call blocks until stop() is called
87+
88+
except KeyboardInterrupt:
89+
pass
90+
except Exception as e:
91+
logger.error(e)
92+
finally:
93+
logger.info("Shutting down...")
94+
self._cleanup()
95+
96+
def stop(self) -> None:
97+
"""Signal the application to stop."""
98+
self._is_running = False
99+
if self._input_source:
100+
self._input_source.stop()
101+
102+
def _setup(self) -> None:
103+
"""Initialize input source and output sinks based on configuration."""
104+
# Setup input source
105+
if self._input_type == "gstreamer":
106+
input_kwargs = self._extract_kwargs("gst_")
107+
elif self._input_type == "websocket":
108+
input_kwargs = self._extract_kwargs("ws_input_")
109+
else:
110+
input_kwargs = {}
111+
112+
InputClass = self._get_input_class(self._input_type)
113+
self._input_source = InputClass(
114+
on_frame_cb=self._frame_callback,
115+
**input_kwargs,
116+
)
117+
118+
# Setup output sinks
119+
for output_type in self._output_types:
120+
if output_type == "mjpeg":
121+
output_kwargs = self._extract_kwargs("mjpeg_")
122+
elif output_type == "websocket":
123+
output_kwargs = self._extract_kwargs("ws_output_")
124+
else:
125+
output_kwargs = {}
126+
127+
OutputClass = self._get_output_class(output_type)
128+
sink = OutputClass(**output_kwargs)
129+
self._output_sinks.append(sink)
130+
131+
def _extract_kwargs(self, prefix: str) -> dict:
132+
"""Extract kwargs with given prefix, stripping the prefix from keys."""
133+
result = {}
134+
for key, value in self._kwargs.items():
135+
if key.startswith(prefix):
136+
result[key.removeprefix(prefix)] = value
137+
return result
138+
139+
def _get_input_class(self, input_type: str):
140+
"""Lazily load and return the input class for the given type."""
141+
if input_type == "gstreamer":
142+
from aihub.gstreamer.input import GStreamerInput
143+
144+
return GStreamerInput
145+
elif input_type == "websocket":
146+
from aihub.websocket.input import WebSocketInput
147+
148+
return WebSocketInput
149+
else:
150+
raise ValueError(f"Unknown input type: {input_type}")
151+
152+
def _get_output_class(self, output_type: str):
153+
"""Lazily load and return the output class for the given type."""
154+
if output_type == "mjpeg":
155+
from aihub.mjpeg.output import MJPEGOutput
156+
157+
return MJPEGOutput
158+
elif output_type == "websocket":
159+
from aihub.websocket.output import WebSocketOutput
160+
161+
return WebSocketOutput
162+
else:
163+
raise ValueError(f"Unknown output type: {output_type}")
164+
165+
def _frame_callback(self, frame: np.ndarray):
166+
"""
167+
Internal callback that wraps user callback and distributes to outputs.
168+
"""
169+
processed_frame, metadata = self._inference_cb(frame)
170+
171+
for sink in self._output_sinks:
172+
try:
173+
sink.send_frame(processed_frame, metadata)
174+
except Exception as e:
175+
logger.warning(f"Output sink error: {e}")
176+
177+
# FPS tracking
178+
self._fps_frame_count += 1
179+
cur_time = time.perf_counter()
180+
elapsed = cur_time - self._fps_start_time
181+
if elapsed >= 1.0:
182+
fps = self._fps_frame_count / elapsed
183+
logger.debug(f"FPS: {fps:.1f}")
184+
self._fps_start_time = cur_time
185+
self._fps_frame_count = 0
186+
187+
def _cleanup(self) -> None:
188+
"""Clean up resources."""
189+
self._is_running = False
190+
191+
if self._input_source:
192+
try:
193+
self._input_source.stop()
194+
except Exception as e:
195+
logger.warning(f"Error stopping input: {e}")
196+
197+
for sink in self._output_sinks:
198+
try:
199+
sink.stop()
200+
except Exception as e:
201+
logger.warning(f"Error stopping output: {e}")

0 commit comments

Comments
 (0)