omnius 1.0.627 → 1.0.629
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.js +10160 -9484
- package/dist/scripts/audio-yamnet-tensorrt-worker.py +335 -0
- package/dist/update-worker.js +224 -182
- package/docs/DISCOVERY.json +311 -9
- package/docs/DISCOVERY.md +5 -2
- package/docs/reference/rest-api.md +4 -1
- package/docs/rest/INDEX.md +1 -1
- package/docs/rest/endpoints/voice-vision.md +61 -1
- package/npm-shrinkwrap.json +17 -17
- package/package.json +2 -2
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Persistent CUDA/TensorRT YAMNet worker for Omnius audio classification.
|
|
3
|
+
|
|
4
|
+
The process deliberately accepts only already-captured WAV paths. It never
|
|
5
|
+
opens an ALSA device and never downloads a model. Omnius bootstrap stages and
|
|
6
|
+
checksums the ONNX model and TensorRT plan before this worker is launched.
|
|
7
|
+
|
|
8
|
+
Protocol: JSON Lines on stdin/stdout. stdout is protocol-only; diagnostics go
|
|
9
|
+
to stderr so callers cannot confuse logs with inference results.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import csv
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
import traceback
|
|
21
|
+
import wave
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def emit(payload: dict) -> None:
|
|
25
|
+
sys.stdout.write(json.dumps(payload, separators=(",", ":")) + "\n")
|
|
26
|
+
sys.stdout.flush()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def fail(message: str) -> None:
|
|
30
|
+
emit({"type": "fatal", "error": message})
|
|
31
|
+
raise RuntimeError(message)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def cuda_check(result, label: str):
|
|
35
|
+
"""cuda-python returns either an error enum or (error enum, value)."""
|
|
36
|
+
if isinstance(result, tuple):
|
|
37
|
+
status, value = result[0], result[1:]
|
|
38
|
+
else:
|
|
39
|
+
status, value = result, ()
|
|
40
|
+
if int(status) != 0:
|
|
41
|
+
raise RuntimeError(f"CUDA {label} failed: {status}")
|
|
42
|
+
if len(value) == 0:
|
|
43
|
+
return None
|
|
44
|
+
return value[0] if len(value) == 1 else value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CudaPythonRuntime:
|
|
48
|
+
"""Thin adapter over JetPack's cuda-python bindings."""
|
|
49
|
+
def __init__(self, cudart):
|
|
50
|
+
self.cudart = cudart
|
|
51
|
+
|
|
52
|
+
def stream_create(self):
|
|
53
|
+
return cuda_check(self.cudart.cudaStreamCreate(), "stream create")
|
|
54
|
+
|
|
55
|
+
def alloc(self, size, label):
|
|
56
|
+
return cuda_check(self.cudart.cudaMalloc(size), f"{label} allocation")
|
|
57
|
+
|
|
58
|
+
def h2d(self, device, host, stream, label):
|
|
59
|
+
cuda_check(
|
|
60
|
+
self.cudart.cudaMemcpyAsync(
|
|
61
|
+
device,
|
|
62
|
+
host.ctypes.data,
|
|
63
|
+
host.nbytes,
|
|
64
|
+
self.cudart.cudaMemcpyKind.cudaMemcpyHostToDevice,
|
|
65
|
+
stream,
|
|
66
|
+
),
|
|
67
|
+
f"{label} copy",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def d2h(self, host, device, stream, label):
|
|
71
|
+
cuda_check(
|
|
72
|
+
self.cudart.cudaMemcpyAsync(
|
|
73
|
+
host.ctypes.data,
|
|
74
|
+
device,
|
|
75
|
+
host.nbytes,
|
|
76
|
+
self.cudart.cudaMemcpyKind.cudaMemcpyDeviceToHost,
|
|
77
|
+
stream,
|
|
78
|
+
),
|
|
79
|
+
f"{label} copy",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def synchronize(self, stream):
|
|
83
|
+
cuda_check(self.cudart.cudaStreamSynchronize(stream), "stream synchronize")
|
|
84
|
+
|
|
85
|
+
def free(self, allocation):
|
|
86
|
+
self.cudart.cudaFree(allocation)
|
|
87
|
+
|
|
88
|
+
def device_metadata(self):
|
|
89
|
+
device = cuda_check(self.cudart.cudaGetDevice(), "get device")
|
|
90
|
+
props = cuda_check(self.cudart.cudaGetDeviceProperties(device), "get device properties")
|
|
91
|
+
name = getattr(props, "name", "CUDA device")
|
|
92
|
+
if isinstance(name, bytes):
|
|
93
|
+
name = name.split(b"\0", 1)[0].decode("utf8", "replace")
|
|
94
|
+
return str(name), f"{getattr(props, 'major', 0)}.{getattr(props, 'minor', 0)}"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class PyCudaRuntime:
|
|
98
|
+
"""JetPack also ships python3-pycuda on supported L4T images.
|
|
99
|
+
|
|
100
|
+
Keeping this fallback avoids a generic PyPI CUDA runtime wheel: both
|
|
101
|
+
backends call the CUDA libraries supplied by the installed JetPack image.
|
|
102
|
+
"""
|
|
103
|
+
def __init__(self, cuda):
|
|
104
|
+
cuda.init()
|
|
105
|
+
self.cuda = cuda
|
|
106
|
+
self.device = cuda.Device(0)
|
|
107
|
+
self.context = self.device.make_context()
|
|
108
|
+
|
|
109
|
+
def stream_create(self):
|
|
110
|
+
return self.cuda.Stream()
|
|
111
|
+
|
|
112
|
+
def alloc(self, size, _label):
|
|
113
|
+
return self.cuda.mem_alloc(size)
|
|
114
|
+
|
|
115
|
+
def h2d(self, device, host, stream, _label):
|
|
116
|
+
self.cuda.memcpy_htod_async(device, host, stream)
|
|
117
|
+
|
|
118
|
+
def d2h(self, host, device, stream, _label):
|
|
119
|
+
self.cuda.memcpy_dtoh_async(host, device, stream)
|
|
120
|
+
|
|
121
|
+
def synchronize(self, stream):
|
|
122
|
+
stream.synchronize()
|
|
123
|
+
|
|
124
|
+
def free(self, allocation):
|
|
125
|
+
allocation.free()
|
|
126
|
+
|
|
127
|
+
def stream_handle(self, stream):
|
|
128
|
+
return stream.handle
|
|
129
|
+
|
|
130
|
+
def device_metadata(self):
|
|
131
|
+
major, minor = self.device.compute_capability()
|
|
132
|
+
return self.device.name(), f"{major}.{minor}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def read_wav(path: str, np):
|
|
136
|
+
started = time.perf_counter()
|
|
137
|
+
with wave.open(path, "rb") as reader:
|
|
138
|
+
channels = reader.getnchannels()
|
|
139
|
+
sample_width = reader.getsampwidth()
|
|
140
|
+
sample_rate = reader.getframerate()
|
|
141
|
+
frames = reader.getnframes()
|
|
142
|
+
compression = reader.getcomptype()
|
|
143
|
+
raw = reader.readframes(frames)
|
|
144
|
+
if compression != "NONE":
|
|
145
|
+
raise RuntimeError("Only uncompressed RIFF/WAV input is supported")
|
|
146
|
+
if channels != 1:
|
|
147
|
+
raise RuntimeError(f"Expected mono WAV from the caller, received {channels} channels")
|
|
148
|
+
if sample_width != 2:
|
|
149
|
+
raise RuntimeError(f"Expected PCM16 WAV from the caller, received {sample_width * 8}-bit samples")
|
|
150
|
+
if sample_rate != 16000:
|
|
151
|
+
raise RuntimeError(f"Expected 16 kHz WAV from the caller, received {sample_rate} Hz")
|
|
152
|
+
waveform = np.frombuffer(raw, dtype="<i2").astype(np.float32) / 32768.0
|
|
153
|
+
return waveform, sample_rate, (time.perf_counter() - started) * 1000
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class YAMNetTensorRT:
|
|
157
|
+
def __init__(self, engine_path: str, class_map_path: str):
|
|
158
|
+
started = time.perf_counter()
|
|
159
|
+
import numpy as np
|
|
160
|
+
import tensorrt as trt
|
|
161
|
+
try:
|
|
162
|
+
from cuda import cudart
|
|
163
|
+
cuda = CudaPythonRuntime(cudart)
|
|
164
|
+
except ImportError:
|
|
165
|
+
import pycuda.driver as pycuda
|
|
166
|
+
cuda = PyCudaRuntime(pycuda)
|
|
167
|
+
|
|
168
|
+
self.np = np
|
|
169
|
+
self.trt = trt
|
|
170
|
+
self.cuda = cuda
|
|
171
|
+
self.logger = trt.Logger(trt.Logger.ERROR)
|
|
172
|
+
with open(engine_path, "rb") as handle:
|
|
173
|
+
serialized = handle.read()
|
|
174
|
+
self.runtime = trt.Runtime(self.logger)
|
|
175
|
+
self.engine = self.runtime.deserialize_cuda_engine(serialized)
|
|
176
|
+
if self.engine is None:
|
|
177
|
+
raise RuntimeError("TensorRT could not deserialize the YAMNet engine")
|
|
178
|
+
self.context = self.engine.create_execution_context()
|
|
179
|
+
if self.context is None:
|
|
180
|
+
raise RuntimeError("TensorRT could not create a YAMNet execution context")
|
|
181
|
+
self.stream = cuda.stream_create()
|
|
182
|
+
self.input_name = None
|
|
183
|
+
self.output_names = []
|
|
184
|
+
for index in range(self.engine.num_io_tensors):
|
|
185
|
+
name = self.engine.get_tensor_name(index)
|
|
186
|
+
if self.engine.get_tensor_mode(name) == trt.TensorIOMode.INPUT:
|
|
187
|
+
self.input_name = name
|
|
188
|
+
else:
|
|
189
|
+
self.output_names.append(name)
|
|
190
|
+
if self.input_name is None or not self.output_names:
|
|
191
|
+
raise RuntimeError("YAMNet TensorRT engine did not expose input and output tensors")
|
|
192
|
+
with open(class_map_path, newline="", encoding="utf8") as handle:
|
|
193
|
+
self.classes = [row["display_name"] for row in csv.DictReader(handle)]
|
|
194
|
+
if len(self.classes) != 521:
|
|
195
|
+
raise RuntimeError(f"YAMNet class map must contain 521 classes; found {len(self.classes)}")
|
|
196
|
+
self.device_name, self.compute_capability = self._device_metadata()
|
|
197
|
+
self.model_load_ms = (time.perf_counter() - started) * 1000
|
|
198
|
+
|
|
199
|
+
def _device_metadata(self):
|
|
200
|
+
# TensorRT uses the CUDA-visible ordinal. Omnius preflight limits that
|
|
201
|
+
# namespace to exactly the approved Jetson GPU before process launch.
|
|
202
|
+
return self.cuda.device_metadata()
|
|
203
|
+
|
|
204
|
+
def warm(self):
|
|
205
|
+
# YAMNet needs >=0.975 seconds. This is a model-load probe, not a
|
|
206
|
+
# captured-audio classification, and it verifies the TensorRT path is
|
|
207
|
+
# actually executable before readiness is reported.
|
|
208
|
+
self.infer(self.np.zeros(15600, dtype=self.np.float32), 1)
|
|
209
|
+
|
|
210
|
+
def infer(self, waveform, top_k: int):
|
|
211
|
+
started = time.perf_counter()
|
|
212
|
+
np = self.np
|
|
213
|
+
if waveform.ndim != 1 or waveform.size < 15600:
|
|
214
|
+
# YAMNet's framing kernel requires 0.975 s; pad silence only to
|
|
215
|
+
# satisfy the model shape, without altering any supplied samples.
|
|
216
|
+
waveform = np.pad(waveform.reshape(-1), (0, max(0, 15600 - waveform.size)))
|
|
217
|
+
waveform = np.ascontiguousarray(waveform.astype(np.float32, copy=False))
|
|
218
|
+
if not self.context.set_input_shape(self.input_name, tuple(waveform.shape)):
|
|
219
|
+
raise RuntimeError(f"TensorRT rejected YAMNet input shape {tuple(waveform.shape)}")
|
|
220
|
+
|
|
221
|
+
allocations = []
|
|
222
|
+
host_outputs = {}
|
|
223
|
+
try:
|
|
224
|
+
input_bytes = waveform.nbytes
|
|
225
|
+
input_device = self.cuda.alloc(input_bytes, "input")
|
|
226
|
+
allocations.append(input_device)
|
|
227
|
+
self.context.set_tensor_address(self.input_name, int(input_device))
|
|
228
|
+
for name in self.output_names:
|
|
229
|
+
shape = tuple(self.context.get_tensor_shape(name))
|
|
230
|
+
if any(int(dim) < 0 for dim in shape):
|
|
231
|
+
raise RuntimeError(f"TensorRT did not resolve output shape for {name}: {shape}")
|
|
232
|
+
dtype = self.trt.nptype(self.engine.get_tensor_dtype(name))
|
|
233
|
+
host = np.empty(shape, dtype=dtype)
|
|
234
|
+
device = self.cuda.alloc(host.nbytes, name)
|
|
235
|
+
allocations.append(device)
|
|
236
|
+
host_outputs[name] = (host, device)
|
|
237
|
+
self.context.set_tensor_address(name, int(device))
|
|
238
|
+
self.cuda.h2d(input_device, waveform, self.stream, "input")
|
|
239
|
+
stream_handle = self.cuda.stream_handle(self.stream) if hasattr(self.cuda, "stream_handle") else self.stream
|
|
240
|
+
if not self.context.execute_async_v3(stream_handle):
|
|
241
|
+
raise RuntimeError("TensorRT YAMNet execute_async_v3 returned false")
|
|
242
|
+
for name, (host, device) in host_outputs.items():
|
|
243
|
+
self.cuda.d2h(host, device, self.stream, name)
|
|
244
|
+
self.cuda.synchronize(self.stream)
|
|
245
|
+
# The converted Google model exposes output_0 with AudioSet scores.
|
|
246
|
+
scores = next((value[0] for name, value in host_outputs.items() if value[0].shape[-1] == 521), None)
|
|
247
|
+
if scores is None:
|
|
248
|
+
raise RuntimeError("TensorRT YAMNet engine did not produce a 521-class score tensor")
|
|
249
|
+
mean_scores = np.mean(scores, axis=0)
|
|
250
|
+
indices = np.argsort(mean_scores)[-top_k:][::-1]
|
|
251
|
+
return [
|
|
252
|
+
{"label": self.classes[int(index)], "confidence": float(mean_scores[int(index)])}
|
|
253
|
+
for index in indices
|
|
254
|
+
], (time.perf_counter() - started) * 1000
|
|
255
|
+
finally:
|
|
256
|
+
for allocation in allocations:
|
|
257
|
+
try:
|
|
258
|
+
self.cuda.free(allocation)
|
|
259
|
+
except Exception:
|
|
260
|
+
pass
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def file_digest(path: str) -> str:
|
|
264
|
+
digest = hashlib.sha256()
|
|
265
|
+
with open(path, "rb") as handle:
|
|
266
|
+
while True:
|
|
267
|
+
chunk = handle.read(1024 * 1024)
|
|
268
|
+
if not chunk:
|
|
269
|
+
break
|
|
270
|
+
digest.update(chunk)
|
|
271
|
+
return "sha256:" + digest.hexdigest()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def main() -> int:
|
|
275
|
+
parser = argparse.ArgumentParser()
|
|
276
|
+
parser.add_argument("--engine", required=True)
|
|
277
|
+
parser.add_argument("--class-map", required=True)
|
|
278
|
+
parser.add_argument("--model-digest", required=True)
|
|
279
|
+
args = parser.parse_args()
|
|
280
|
+
# CUDA_VISIBLE_DEVICES must be a single approved accelerator before any
|
|
281
|
+
# CUDA/TensorRT import. Jetson's integrated GPU is logical device 0.
|
|
282
|
+
visible = os.environ.get("CUDA_VISIBLE_DEVICES", "").strip()
|
|
283
|
+
if visible != "0":
|
|
284
|
+
fail("Audio TensorRT worker requires exactly CUDA_VISIBLE_DEVICES=0 on Jetson")
|
|
285
|
+
worker = YAMNetTensorRT(args.engine, args.class_map)
|
|
286
|
+
worker.warm()
|
|
287
|
+
emit({
|
|
288
|
+
"type": "ready",
|
|
289
|
+
"pid": os.getpid(),
|
|
290
|
+
"backend": "tensorrt-fp16",
|
|
291
|
+
"device": worker.device_name,
|
|
292
|
+
"compute_capability": worker.compute_capability,
|
|
293
|
+
"cuda_visible_devices": visible,
|
|
294
|
+
"model": "yamnet",
|
|
295
|
+
"model_digest": args.model_digest,
|
|
296
|
+
"taxonomy": "AudioSet-521",
|
|
297
|
+
"model_load_ms": round(worker.model_load_ms, 3),
|
|
298
|
+
"warmed": True,
|
|
299
|
+
})
|
|
300
|
+
for line in sys.stdin:
|
|
301
|
+
try:
|
|
302
|
+
request = json.loads(line)
|
|
303
|
+
if request.get("type") == "shutdown":
|
|
304
|
+
emit({"type": "stopped"})
|
|
305
|
+
return 0
|
|
306
|
+
if request.get("type") != "classify":
|
|
307
|
+
raise RuntimeError("unknown request type")
|
|
308
|
+
request_id = str(request.get("id") or "")
|
|
309
|
+
file_path = str(request.get("file") or "")
|
|
310
|
+
if not request_id or not file_path:
|
|
311
|
+
raise RuntimeError("classify requires id and file")
|
|
312
|
+
waveform, sample_rate, decode_ms = read_wav(file_path, worker.np)
|
|
313
|
+
classifications, inference_ms = worker.infer(waveform, max(1, min(int(request.get("top_k", 5)), 25)))
|
|
314
|
+
emit({
|
|
315
|
+
"type": "result",
|
|
316
|
+
"id": request_id,
|
|
317
|
+
"success": True,
|
|
318
|
+
"sample_rate_hz": sample_rate,
|
|
319
|
+
"duration_seconds": round(float(waveform.size) / sample_rate, 6),
|
|
320
|
+
"classifications": classifications,
|
|
321
|
+
"timings_ms": {"decode": round(decode_ms, 3), "inference": round(inference_ms, 3)},
|
|
322
|
+
})
|
|
323
|
+
except Exception as exc: # keep the persistent worker alive per request
|
|
324
|
+
emit({
|
|
325
|
+
"type": "result",
|
|
326
|
+
"id": str(locals().get("request", {}).get("id", "")),
|
|
327
|
+
"success": False,
|
|
328
|
+
"error": str(exc),
|
|
329
|
+
})
|
|
330
|
+
print(traceback.format_exc(), file=sys.stderr, flush=True)
|
|
331
|
+
return 0
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
if __name__ == "__main__":
|
|
335
|
+
raise SystemExit(main())
|