marimba-audio 0.1.0__tar.gz
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.
- marimba_audio-0.1.0/.dockerignore +11 -0
- marimba_audio-0.1.0/.editorconfig +14 -0
- marimba_audio-0.1.0/.github/CODEOWNERS +1 -0
- marimba_audio-0.1.0/.github/dependabot.yml +10 -0
- marimba_audio-0.1.0/.github/pull_request_template.md +6 -0
- marimba_audio-0.1.0/.github/workflows/ci.yml +38 -0
- marimba_audio-0.1.0/.github/workflows/publish.yml +40 -0
- marimba_audio-0.1.0/.github/workflows/security.yml +62 -0
- marimba_audio-0.1.0/.gitignore +29 -0
- marimba_audio-0.1.0/.pre-commit-config.yaml +23 -0
- marimba_audio-0.1.0/.runpod/Dockerfile +16 -0
- marimba_audio-0.1.0/.runpod/README.md +192 -0
- marimba_audio-0.1.0/.runpod/handler.py +108 -0
- marimba_audio-0.1.0/.runpod/hub.json +26 -0
- marimba_audio-0.1.0/.runpod/tests.json +35 -0
- marimba_audio-0.1.0/AGENTS.md +20 -0
- marimba_audio-0.1.0/CLAUDE.md +1 -0
- marimba_audio-0.1.0/CODE_OF_CONDUCT.md +73 -0
- marimba_audio-0.1.0/CONTRIBUTING.md +34 -0
- marimba_audio-0.1.0/LICENSE +201 -0
- marimba_audio-0.1.0/PKG-INFO +183 -0
- marimba_audio-0.1.0/README.md +153 -0
- marimba_audio-0.1.0/SECURITY.md +3 -0
- marimba_audio-0.1.0/THIRD_PARTY_NOTICES.md +20 -0
- marimba_audio-0.1.0/benchmarks/l4_e2e_clip_ids.json +3922 -0
- marimba_audio-0.1.0/benchmarks/l4_e2e_latency.json +1263 -0
- marimba_audio-0.1.0/demo/.gitattributes +1 -0
- marimba_audio-0.1.0/demo/.gitignore +6 -0
- marimba_audio-0.1.0/demo/README.md +182 -0
- marimba_audio-0.1.0/demo/frontend/eslint.config.js +14 -0
- marimba_audio-0.1.0/demo/frontend/index.html +14 -0
- marimba_audio-0.1.0/demo/frontend/package-lock.json +3022 -0
- marimba_audio-0.1.0/demo/frontend/package.json +22 -0
- marimba_audio-0.1.0/demo/frontend/public/IBMPlexMono-Regular.ttf +0 -0
- marimba_audio-0.1.0/demo/frontend/public/IBMPlexMono-Thin.ttf +0 -0
- marimba_audio-0.1.0/demo/frontend/public/IBMPlexSans-Regular.ttf +0 -0
- marimba_audio-0.1.0/demo/frontend/public/OFL.txt +93 -0
- marimba_audio-0.1.0/demo/frontend/public/favicon.svg +1 -0
- marimba_audio-0.1.0/demo/frontend/public/marimba.svg +1 -0
- marimba_audio-0.1.0/demo/frontend/src/audio-worklet.js +38 -0
- marimba_audio-0.1.0/demo/frontend/src/audio-worklet.test.ts +65 -0
- marimba_audio-0.1.0/demo/frontend/src/instruments.ts +43 -0
- marimba_audio-0.1.0/demo/frontend/src/main.ts +424 -0
- marimba_audio-0.1.0/demo/frontend/src/presentation.test.ts +88 -0
- marimba_audio-0.1.0/demo/frontend/src/presentation.ts +45 -0
- marimba_audio-0.1.0/demo/frontend/src/production.test.ts +70 -0
- marimba_audio-0.1.0/demo/frontend/src/socket.test.ts +45 -0
- marimba_audio-0.1.0/demo/frontend/src/socket.ts +25 -0
- marimba_audio-0.1.0/demo/frontend/src/state.test.ts +162 -0
- marimba_audio-0.1.0/demo/frontend/src/state.ts +112 -0
- marimba_audio-0.1.0/demo/frontend/src/stt.ts +15 -0
- marimba_audio-0.1.0/demo/frontend/src/style.css +568 -0
- marimba_audio-0.1.0/demo/frontend/src/vite-env.d.ts +1 -0
- marimba_audio-0.1.0/demo/frontend/tsconfig.json +12 -0
- marimba_audio-0.1.0/demo/frontend/vite.config.ts +4 -0
- marimba_audio-0.1.0/demo/marimba_demo/__init__.py +1 -0
- marimba_audio-0.1.0/demo/marimba_demo/cards.py +90 -0
- marimba_audio-0.1.0/demo/marimba_demo/cli.py +78 -0
- marimba_audio-0.1.0/demo/marimba_demo/protocol.py +30 -0
- marimba_audio-0.1.0/demo/marimba_demo/scheduler.py +367 -0
- marimba_audio-0.1.0/demo/marimba_demo/server.py +192 -0
- marimba_audio-0.1.0/demo/marimba_demo/speech.py +64 -0
- marimba_audio-0.1.0/demo/pyproject.toml +40 -0
- marimba_audio-0.1.0/demo/tests/test_protocol.py +529 -0
- marimba_audio-0.1.0/demo/tests/test_scheduler.py +611 -0
- marimba_audio-0.1.0/docs/audio.md +27 -0
- marimba_audio-0.1.0/docs/batch.md +38 -0
- marimba_audio-0.1.0/docs/benchmarks.md +125 -0
- marimba_audio-0.1.0/docs/calibration.md +55 -0
- marimba_audio-0.1.0/docs/capabilities.md +93 -0
- marimba_audio-0.1.0/docs/index.md +17 -0
- marimba_audio-0.1.0/docs/quickstart.md +41 -0
- marimba_audio-0.1.0/docs/runtime.md +51 -0
- marimba_audio-0.1.0/docs/scheduling.md +91 -0
- marimba_audio-0.1.0/docs/streaming.md +66 -0
- marimba_audio-0.1.0/hf/README.md +174 -0
- marimba_audio-0.1.0/notebooks/01_quickstart.ipynb +113 -0
- marimba_audio-0.1.0/notebooks/02_datasets_map.ipynb +107 -0
- marimba_audio-0.1.0/notebooks/03_streaming.ipynb +124 -0
- marimba_audio-0.1.0/notebooks/README.md +23 -0
- marimba_audio-0.1.0/pyproject.toml +67 -0
- marimba_audio-0.1.0/scripts/check_public.py +108 -0
- marimba_audio-0.1.0/scripts/l4_e2e_latency.py +313 -0
- marimba_audio-0.1.0/scripts/latency_matrix.py +356 -0
- marimba_audio-0.1.0/scripts/tests/test_l4_e2e_latency.py +151 -0
- marimba_audio-0.1.0/space/README.md +43 -0
- marimba_audio-0.1.0/space/app.py +72 -0
- marimba_audio-0.1.0/space/requirements.txt +2 -0
- marimba_audio-0.1.0/src/marimba/__init__.py +28 -0
- marimba_audio-0.1.0/src/marimba/_diarization.py +122 -0
- marimba_audio-0.1.0/src/marimba/_head.py +205 -0
- marimba_audio-0.1.0/src/marimba/_qwen.py +270 -0
- marimba_audio-0.1.0/src/marimba/_vendor/__init__.py +1 -0
- marimba_audio-0.1.0/src/marimba/_vendor/qwen_asr/LICENSE +201 -0
- marimba_audio-0.1.0/src/marimba/_vendor/qwen_asr/__init__.py +1 -0
- marimba_audio-0.1.0/src/marimba/_vendor/qwen_asr/configuration_qwen3_asr.py +425 -0
- marimba_audio-0.1.0/src/marimba/_vendor/qwen_asr/modeling_qwen3_asr.py +1365 -0
- marimba_audio-0.1.0/src/marimba/_vendor/qwen_asr/processing_qwen3_asr.py +209 -0
- marimba_audio-0.1.0/src/marimba/_vendor/qwen_asr/utils.py +211 -0
- marimba_audio-0.1.0/src/marimba/audio.py +69 -0
- marimba_audio-0.1.0/src/marimba/calibration.py +106 -0
- marimba_audio-0.1.0/src/marimba/capabilities.py +65 -0
- marimba_audio-0.1.0/src/marimba/cli.py +253 -0
- marimba_audio-0.1.0/src/marimba/decision.py +47 -0
- marimba_audio-0.1.0/src/marimba/integrations/__init__.py +1 -0
- marimba_audio-0.1.0/src/marimba/integrations/pipecat.py +58 -0
- marimba_audio-0.1.0/src/marimba/language.py +75 -0
- marimba_audio-0.1.0/src/marimba/lexical.py +18 -0
- marimba_audio-0.1.0/src/marimba/model.py +231 -0
- marimba_audio-0.1.0/src/marimba/pii.py +5 -0
- marimba_audio-0.1.0/src/marimba/pretrained.py +334 -0
- marimba_audio-0.1.0/src/marimba/py.typed +0 -0
- marimba_audio-0.1.0/src/marimba/questions.py +123 -0
- marimba_audio-0.1.0/src/marimba/readout.py +39 -0
- marimba_audio-0.1.0/src/marimba/runtime.py +111 -0
- marimba_audio-0.1.0/src/marimba/stream.py +222 -0
- marimba_audio-0.1.0/src/marimba/testing.py +149 -0
- marimba_audio-0.1.0/src/marimba/weights.py +42 -0
- marimba_audio-0.1.0/src/marimba/windows.py +88 -0
- marimba_audio-0.1.0/tests/test_calibration.py +94 -0
- marimba_audio-0.1.0/tests/test_cli_bench.py +150 -0
- marimba_audio-0.1.0/tests/test_diarization.py +219 -0
- marimba_audio-0.1.0/tests/test_diarization_reference.py +31 -0
- marimba_audio-0.1.0/tests/test_head.py +51 -0
- marimba_audio-0.1.0/tests/test_language_options.py +22 -0
- marimba_audio-0.1.0/tests/test_latency_matrix.py +56 -0
- marimba_audio-0.1.0/tests/test_package.py +341 -0
- marimba_audio-0.1.0/tests/test_pipecat.py +113 -0
- marimba_audio-0.1.0/tests/test_pretrained.py +729 -0
- marimba_audio-0.1.0/tests/test_qwen_inputs.py +177 -0
- marimba_audio-0.1.0/tests/test_release_scaffold.py +92 -0
- marimba_audio-0.1.0/tests/test_runpod_worker.py +168 -0
- marimba_audio-0.1.0/tests/test_runtime.py +114 -0
- marimba_audio-0.1.0/tests/test_scheduling.py +148 -0
- marimba_audio-0.1.0/tests/test_stream.py +123 -0
- marimba_audio-0.1.0/tests/test_vendored_qwen.py +34 -0
- marimba_audio-0.1.0/uv.lock +5659 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* @solanovisitor
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions: {}
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
check:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
23
|
+
env:
|
|
24
|
+
UV_PYTHON: ${{ matrix.python-version }}
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
27
|
+
with:
|
|
28
|
+
persist-credentials: false
|
|
29
|
+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
30
|
+
with:
|
|
31
|
+
python-version: ${{ matrix.python-version }}
|
|
32
|
+
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
|
33
|
+
- run: uv sync --locked --extra datasets --extra pipecat --group dev
|
|
34
|
+
- run: uv run ruff check .
|
|
35
|
+
- run: uv run ruff format --check .
|
|
36
|
+
- run: uv run mypy
|
|
37
|
+
- run: uv run pytest -q
|
|
38
|
+
- run: uv build
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions: {}
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
16
|
+
with:
|
|
17
|
+
persist-credentials: false
|
|
18
|
+
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
|
19
|
+
with:
|
|
20
|
+
enable-cache: false
|
|
21
|
+
- run: uv build
|
|
22
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
23
|
+
with:
|
|
24
|
+
name: dist
|
|
25
|
+
path: dist/
|
|
26
|
+
|
|
27
|
+
pypi:
|
|
28
|
+
needs: build
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
environment:
|
|
31
|
+
name: pypi
|
|
32
|
+
url: https://pypi.org/p/marimba-audio
|
|
33
|
+
permissions:
|
|
34
|
+
id-token: write
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
37
|
+
with:
|
|
38
|
+
name: dist
|
|
39
|
+
path: dist/
|
|
40
|
+
- uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: Security
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions: {}
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
zizmor:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
21
|
+
with:
|
|
22
|
+
persist-credentials: false
|
|
23
|
+
- uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0
|
|
24
|
+
with:
|
|
25
|
+
advanced-security: false
|
|
26
|
+
annotations: true
|
|
27
|
+
min-severity: high
|
|
28
|
+
version: 1.29.0
|
|
29
|
+
|
|
30
|
+
gitleaks:
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
permissions:
|
|
33
|
+
contents: read
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
36
|
+
with:
|
|
37
|
+
fetch-depth: 0
|
|
38
|
+
persist-credentials: false
|
|
39
|
+
- name: Run Gitleaks
|
|
40
|
+
env:
|
|
41
|
+
GITLEAKS_VERSION: 8.21.2
|
|
42
|
+
GITLEAKS_SHA256: 5bc41815076e6ed6ef8fbecc9d9b75bcae31f39029ceb55da08086315316e3ba
|
|
43
|
+
run: |
|
|
44
|
+
curl --fail --silent --show-error --location \
|
|
45
|
+
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
|
46
|
+
--output gitleaks.tar.gz
|
|
47
|
+
printf '%s gitleaks.tar.gz\n' "$GITLEAKS_SHA256" | sha256sum --check
|
|
48
|
+
tar -xzf gitleaks.tar.gz gitleaks
|
|
49
|
+
./gitleaks git . --log-opts=HEAD --redact --no-banner --exit-code 1
|
|
50
|
+
|
|
51
|
+
public-text:
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
permissions:
|
|
54
|
+
contents: read
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
57
|
+
with:
|
|
58
|
+
persist-credentials: false
|
|
59
|
+
- name: Check public text
|
|
60
|
+
env:
|
|
61
|
+
PUBLIC_CHECK_TERMS: ${{ secrets.PUBLIC_CHECK_TERMS }}
|
|
62
|
+
run: python3 scripts/check_public.py
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
.venv/
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.mypy_cache/
|
|
7
|
+
.ruff_cache/
|
|
8
|
+
build/
|
|
9
|
+
dist/
|
|
10
|
+
*.egg-info/
|
|
11
|
+
|
|
12
|
+
# Node
|
|
13
|
+
node_modules/
|
|
14
|
+
.next/
|
|
15
|
+
|
|
16
|
+
# Environment and local checks
|
|
17
|
+
.env*
|
|
18
|
+
.public-check-terms
|
|
19
|
+
|
|
20
|
+
# Audio and model artifacts
|
|
21
|
+
*.wav
|
|
22
|
+
*.flac
|
|
23
|
+
*.mp3
|
|
24
|
+
*.pt
|
|
25
|
+
*.safetensors
|
|
26
|
+
*.onnx
|
|
27
|
+
|
|
28
|
+
# OS
|
|
29
|
+
.DS_Store
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: local
|
|
3
|
+
hooks:
|
|
4
|
+
- id: ruff-check
|
|
5
|
+
name: Ruff check
|
|
6
|
+
entry: uv run --with ruff ruff check
|
|
7
|
+
language: system
|
|
8
|
+
types: [python]
|
|
9
|
+
- id: ruff-format
|
|
10
|
+
name: Ruff format
|
|
11
|
+
entry: uv run --with ruff ruff format --check
|
|
12
|
+
language: system
|
|
13
|
+
types: [python]
|
|
14
|
+
- id: public-text-check
|
|
15
|
+
name: Public text check
|
|
16
|
+
entry: python3 scripts/check_public.py
|
|
17
|
+
language: system
|
|
18
|
+
pass_filenames: false
|
|
19
|
+
always_run: true
|
|
20
|
+
- repo: https://github.com/gitleaks/gitleaks
|
|
21
|
+
rev: v8.21.2
|
|
22
|
+
hooks:
|
|
23
|
+
- id: gitleaks
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
FROM runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404
|
|
2
|
+
|
|
3
|
+
ENV HF_HUB_ENABLE_HF_TRANSFER=0 \
|
|
4
|
+
PYTHONUNBUFFERED=1
|
|
5
|
+
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
|
|
8
|
+
# The base image's Debian cryptography installation has no pip RECORD.
|
|
9
|
+
RUN python -m pip install --no-cache-dir --ignore-installed cryptography
|
|
10
|
+
|
|
11
|
+
COPY pyproject.toml README.md LICENSE THIRD_PARTY_NOTICES.md ./
|
|
12
|
+
COPY src/ ./src/
|
|
13
|
+
RUN python -m pip install --no-cache-dir '.[torch]' 'runpod==1.8.1'
|
|
14
|
+
|
|
15
|
+
COPY .runpod/handler.py ./handler.py
|
|
16
|
+
CMD ["python", "-u", "/app/handler.py"]
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Marimba decoder worker
|
|
2
|
+
|
|
3
|
+
This Runpod Serverless worker answers Language and Term questions on a CUDA GPU.
|
|
4
|
+
It loads Marimba once with the Torch backend and calls `decide_snapshot` without
|
|
5
|
+
changing the package's inference path. Term detection retains greedy decoding
|
|
6
|
+
plus four beam hypotheses. Encoder questions stay on the client.
|
|
7
|
+
|
|
8
|
+
## Container and environment
|
|
9
|
+
|
|
10
|
+
Build from the repository root:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
docker build -f .runpod/Dockerfile -t marimba-decoder:local .
|
|
14
|
+
docker run --rm --gpus all --env HF_TOKEN marimba-decoder:local
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`HF_TOKEN` is an optional Hugging Face read token, required only while model
|
|
18
|
+
access requires authentication. Pass it at runtime; never bake it into an image
|
|
19
|
+
or put its value in Hub metadata. `HF_HUB_ENABLE_HF_TRANSFER=0` is set in the image
|
|
20
|
+
because the base image enables an optional transfer package it does not install.
|
|
21
|
+
The worker uses the package's default model and pinned revision.
|
|
22
|
+
|
|
23
|
+
The Hub configuration selects one GPU from the 24 GB or larger `ADA_24` and
|
|
24
|
+
`AMPERE_48` pools, a 30 GB container disk, and CUDA 12.8. Model files download at
|
|
25
|
+
startup into the Hugging Face cache; startup requires network and model access.
|
|
26
|
+
The Hub smoke test uses one second of synthetic silence and a Language question
|
|
27
|
+
on an RTX 4090. It checks execution, not language accuracy on silence.
|
|
28
|
+
|
|
29
|
+
The files follow the [Runpod Hub publishing guide](https://docs.runpod.io/hub/publishing-guide).
|
|
30
|
+
Hub publication is a separate step: it requires accessible source and model
|
|
31
|
+
artifacts and a GitHub release. Adding these files does not publish a repository,
|
|
32
|
+
model, image, release, or Hub listing.
|
|
33
|
+
|
|
34
|
+
## Request
|
|
35
|
+
|
|
36
|
+
Send a Runpod job with an `input` object:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"input": {
|
|
41
|
+
"lane": "decoder",
|
|
42
|
+
"sample_rate": 16000,
|
|
43
|
+
"pcm16_b64": "BASE64_PCM16_LE",
|
|
44
|
+
"mask_b64": "BASE64_UINT8_MASK",
|
|
45
|
+
"window_start_s": 0.0,
|
|
46
|
+
"window_end_s": 1.0,
|
|
47
|
+
"questions": [
|
|
48
|
+
{
|
|
49
|
+
"text": "What language is being spoken?",
|
|
50
|
+
"kind": "choice",
|
|
51
|
+
"options": ["English", "Portuguese"],
|
|
52
|
+
"task": "language",
|
|
53
|
+
"threshold": 0.5
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`pcm16_b64` encodes mono, signed, little-endian PCM16 at 16 kHz. `mask_b64`
|
|
61
|
+
encodes one byte per sample: zero for padding, one for valid audio (including
|
|
62
|
+
silence). Both arrays must have the same nonzero sample count, at most 30 seconds,
|
|
63
|
+
and at least one sample must be valid. Timestamps are finite seconds with
|
|
64
|
+
`0 <= window_start_s <= window_end_s`. Audio may include left padding, so the
|
|
65
|
+
valid interval can be shorter than the array duration.
|
|
66
|
+
|
|
67
|
+
Questions are serialized with `dataclasses.asdict(Question(...))`; JSON options
|
|
68
|
+
are restored to tuples. Only `language` and `term` tasks are supported. A Term
|
|
69
|
+
example is `Question.noul('Did someone say "hello"?')`. Encoder questions and
|
|
70
|
+
other tasks are rejected before loading the GPU model. Invalid inputs fail the
|
|
71
|
+
Runpod job rather than producing decisions.
|
|
72
|
+
|
|
73
|
+
## Response
|
|
74
|
+
|
|
75
|
+
On success, Runpod's response has `status: "COMPLETED"` and an `output` object:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"decisions": [
|
|
80
|
+
{
|
|
81
|
+
"question": {
|
|
82
|
+
"text": "What language is being spoken?",
|
|
83
|
+
"kind": "choice",
|
|
84
|
+
"options": ["English", "Portuguese"],
|
|
85
|
+
"task": "language",
|
|
86
|
+
"threshold": 0.5
|
|
87
|
+
},
|
|
88
|
+
"task": "language",
|
|
89
|
+
"kind": "choice",
|
|
90
|
+
"value": "English",
|
|
91
|
+
"probability": null,
|
|
92
|
+
"probabilities": {"English": 0.8, "Portuguese": 0.2},
|
|
93
|
+
"score": null,
|
|
94
|
+
"threshold": 0.5,
|
|
95
|
+
"window_start_s": 0.0,
|
|
96
|
+
"window_end_s": 1.0,
|
|
97
|
+
"latency_ms": 0.0,
|
|
98
|
+
"status": "ok",
|
|
99
|
+
"calibrated": false
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
"gpu_compute_ms": 0.0
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The values above illustrate the schema, not a prediction for the synthetic test.
|
|
107
|
+
Decisions are returned in question order using `Decision.to_dict()`.
|
|
108
|
+
`gpu_compute_ms` times the synchronized `decide_snapshot` call, excluding model
|
|
109
|
+
loading, payload decoding, and network time.
|
|
110
|
+
|
|
111
|
+
## Client usage
|
|
112
|
+
|
|
113
|
+
Install the repository package with its `torch` extra on the client. Set
|
|
114
|
+
`RUNPOD_ENDPOINT_ID` and `RUNPOD_API_KEY` in the client's environment. The API key
|
|
115
|
+
authenticates calls to Runpod; it is separate from the worker's `HF_TOKEN`.
|
|
116
|
+
The following example creates synthetic silence, keeps voice activity on CPU,
|
|
117
|
+
and sends only Language and Term questions to the decoder endpoint:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import base64
|
|
121
|
+
import json
|
|
122
|
+
import os
|
|
123
|
+
from dataclasses import asdict
|
|
124
|
+
from urllib.request import Request, urlopen
|
|
125
|
+
|
|
126
|
+
import numpy as np
|
|
127
|
+
from marimba import Marimba, Question, WindowSnapshot
|
|
128
|
+
|
|
129
|
+
local = Marimba.from_pretrained(device="cpu", backend="torch")
|
|
130
|
+
snapshot = WindowSnapshot(
|
|
131
|
+
np.zeros(8 * 16000, dtype=np.float32),
|
|
132
|
+
np.ones(8 * 16000, dtype=np.bool_),
|
|
133
|
+
0.0,
|
|
134
|
+
8.0,
|
|
135
|
+
)
|
|
136
|
+
questions = [
|
|
137
|
+
Question.score("Is someone speaking?"),
|
|
138
|
+
Question.choice("What language is being spoken?", ["English", "Portuguese"]),
|
|
139
|
+
Question.noul('Did someone say "hello"?'),
|
|
140
|
+
]
|
|
141
|
+
encoder_questions = [q for q in questions if local.lane(q) == "encoder"]
|
|
142
|
+
decoder_questions = [q for q in questions if local.lane(q) == "decoder"]
|
|
143
|
+
if any(q.task not in ("language", "term") for q in decoder_questions):
|
|
144
|
+
raise ValueError("The remote worker supports Language and Term only")
|
|
145
|
+
local_decisions = local.decide_encoded(local.encode(snapshot), encoder_questions)
|
|
146
|
+
|
|
147
|
+
pcm16 = np.clip(np.rint(snapshot.audio.astype(np.float64) * 32768), -32768, 32767)
|
|
148
|
+
payload = {
|
|
149
|
+
"input": {
|
|
150
|
+
"lane": "decoder",
|
|
151
|
+
"sample_rate": 16000,
|
|
152
|
+
"pcm16_b64": base64.b64encode(pcm16.astype("<i2").tobytes()).decode("ascii"),
|
|
153
|
+
"mask_b64": base64.b64encode(snapshot.mask.astype(np.uint8).tobytes()).decode("ascii"),
|
|
154
|
+
"window_start_s": snapshot.window_start_s,
|
|
155
|
+
"window_end_s": snapshot.window_end_s,
|
|
156
|
+
"questions": [asdict(q) for q in decoder_questions],
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
endpoint = os.environ["RUNPOD_ENDPOINT_ID"]
|
|
160
|
+
request = Request(
|
|
161
|
+
f"https://api.runpod.ai/v2/{endpoint}/runsync",
|
|
162
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
163
|
+
headers={
|
|
164
|
+
"Authorization": "Bearer " + os.environ["RUNPOD_API_KEY"],
|
|
165
|
+
"Content-Type": "application/json",
|
|
166
|
+
"User-Agent": "marimba-decoder-client/0.1",
|
|
167
|
+
},
|
|
168
|
+
method="POST",
|
|
169
|
+
)
|
|
170
|
+
with urlopen(request, timeout=300) as response:
|
|
171
|
+
result = json.load(response)
|
|
172
|
+
if result.get("status") != "COMPLETED":
|
|
173
|
+
raise RuntimeError(f"Runpod job {result.get('id')} status: {result.get('status')}")
|
|
174
|
+
output = result["output"]
|
|
175
|
+
print([d.to_dict() for d in local_decisions])
|
|
176
|
+
print(output["decisions"], output["gpu_compute_ms"])
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
For an existing stream, serialize its `WindowSnapshot` in the same way; keep
|
|
180
|
+
encoder inference local. A synchronous request may return `IN_QUEUE` or
|
|
181
|
+
`IN_PROGRESS`. In that case, retain the job ID and use the
|
|
182
|
+
[Runpod status endpoint](https://docs.runpod.io/serverless/endpoints/send-requests)
|
|
183
|
+
to retrieve that job instead of submitting duplicate work. Never print API keys
|
|
184
|
+
or authorization headers.
|
|
185
|
+
|
|
186
|
+
## Prior validation
|
|
187
|
+
|
|
188
|
+
CPU and GPU parity matched for Language and Term in prior validation. On an
|
|
189
|
+
RTX 4090, measured median round-trip latency was 663 ms for Language and 1639 ms
|
|
190
|
+
for Term; worker GPU compute was 115 ms and 1108 ms, respectively. These are
|
|
191
|
+
observations from that validation, not latency guarantees or results of the
|
|
192
|
+
synthetic Hub smoke test.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Runpod Serverless worker for Language and Term decoder questions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from functools import cache
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from marimba import Marimba, Question, WindowSnapshot
|
|
14
|
+
from marimba.windows import MAX_DECODER_SECONDS
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("marimba-runpod")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@cache
|
|
20
|
+
def _load_model() -> Marimba:
|
|
21
|
+
import torch
|
|
22
|
+
|
|
23
|
+
if not torch.cuda.is_available():
|
|
24
|
+
raise RuntimeError("CUDA is not available in the worker container")
|
|
25
|
+
torch.set_float32_matmul_precision("highest")
|
|
26
|
+
torch.backends.cuda.matmul.allow_tf32 = False
|
|
27
|
+
torch.backends.cudnn.allow_tf32 = False
|
|
28
|
+
logger.info("Loading Marimba decoder model on CUDA")
|
|
29
|
+
model = Marimba.from_pretrained(device="cuda", backend="torch")
|
|
30
|
+
logger.info("Marimba decoder model is ready")
|
|
31
|
+
return model
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _snapshot(data: dict[str, Any]) -> WindowSnapshot:
|
|
35
|
+
if data.get("sample_rate") != 16000:
|
|
36
|
+
raise ValueError("Expected 16000 Hz PCM")
|
|
37
|
+
audio_bytes = base64.b64decode(data["pcm16_b64"], validate=True)
|
|
38
|
+
mask_bytes = base64.b64decode(data["mask_b64"], validate=True)
|
|
39
|
+
if len(audio_bytes) % 2:
|
|
40
|
+
raise ValueError("PCM16 payload has an odd byte count")
|
|
41
|
+
|
|
42
|
+
pcm = np.frombuffer(audio_bytes, dtype="<i2")
|
|
43
|
+
mask = np.frombuffer(mask_bytes, dtype=np.uint8)
|
|
44
|
+
if len(pcm) != len(mask):
|
|
45
|
+
raise ValueError("PCM and mask lengths differ")
|
|
46
|
+
if not 0 < len(pcm) <= 16000 * MAX_DECODER_SECONDS or not mask.any():
|
|
47
|
+
raise ValueError("Decoder snapshots require valid audio and at most 30 seconds")
|
|
48
|
+
if not np.isin(mask, [0, 1]).all():
|
|
49
|
+
raise ValueError("Mask bytes must be zero or one")
|
|
50
|
+
|
|
51
|
+
return WindowSnapshot(
|
|
52
|
+
pcm.astype(np.float32) / 32768.0,
|
|
53
|
+
mask.astype(np.bool_),
|
|
54
|
+
float(data["window_start_s"]),
|
|
55
|
+
float(data["window_end_s"]),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _questions(values: list[dict[str, Any]]) -> list[Question]:
|
|
60
|
+
result = []
|
|
61
|
+
for value in values:
|
|
62
|
+
question = dict(value)
|
|
63
|
+
if question.get("options") is not None:
|
|
64
|
+
question["options"] = tuple(question["options"])
|
|
65
|
+
parsed = Question(**question)
|
|
66
|
+
if parsed.task not in ("language", "term"):
|
|
67
|
+
raise ValueError("The remote worker accepts Language and Term questions only")
|
|
68
|
+
result.append(parsed)
|
|
69
|
+
if not result:
|
|
70
|
+
raise ValueError("At least one decoder question is required")
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def handler(job: dict[str, Any]) -> dict[str, Any]:
|
|
75
|
+
data = job["input"]
|
|
76
|
+
if data.get("lane") != "decoder":
|
|
77
|
+
raise ValueError("The remote worker accepts decoder-lane requests only")
|
|
78
|
+
|
|
79
|
+
snapshot = _snapshot(data)
|
|
80
|
+
questions = _questions(data["questions"])
|
|
81
|
+
model = _load_model()
|
|
82
|
+
if any(model.lane(question) != "decoder" for question in questions):
|
|
83
|
+
raise ValueError("The remote worker accepts decoder questions only")
|
|
84
|
+
|
|
85
|
+
import torch
|
|
86
|
+
|
|
87
|
+
torch.cuda.synchronize()
|
|
88
|
+
started = time.perf_counter()
|
|
89
|
+
decisions = model.decide_snapshot(snapshot, questions)
|
|
90
|
+
torch.cuda.synchronize()
|
|
91
|
+
compute_ms = (time.perf_counter() - started) * 1000
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
"decisions": [decision.to_dict() for decision in decisions],
|
|
95
|
+
"gpu_compute_ms": compute_ms,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def main() -> None:
|
|
100
|
+
import runpod
|
|
101
|
+
|
|
102
|
+
logging.basicConfig(level=logging.INFO)
|
|
103
|
+
_load_model()
|
|
104
|
+
runpod.serverless.start({"handler": handler})
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
main()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Marimba Decoder",
|
|
3
|
+
"description": "Language identification and term detection from audio with Marimba.",
|
|
4
|
+
"type": "serverless",
|
|
5
|
+
"category": "audio",
|
|
6
|
+
"config": {
|
|
7
|
+
"runsOn": "GPU",
|
|
8
|
+
"gpuCount": 1,
|
|
9
|
+
"gpuIds": "ADA_24,AMPERE_48",
|
|
10
|
+
"containerDiskInGb": 30,
|
|
11
|
+
"allowedCudaVersions": ["12.8"],
|
|
12
|
+
"env": [
|
|
13
|
+
{
|
|
14
|
+
"key": "HF_TOKEN",
|
|
15
|
+
"input": {
|
|
16
|
+
"name": "Hugging Face token",
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Optional read token, needed only when model access requires authentication.",
|
|
19
|
+
"default": "",
|
|
20
|
+
"required": false,
|
|
21
|
+
"advanced": true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|