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.
Files changed (137) hide show
  1. marimba_audio-0.1.0/.dockerignore +11 -0
  2. marimba_audio-0.1.0/.editorconfig +14 -0
  3. marimba_audio-0.1.0/.github/CODEOWNERS +1 -0
  4. marimba_audio-0.1.0/.github/dependabot.yml +10 -0
  5. marimba_audio-0.1.0/.github/pull_request_template.md +6 -0
  6. marimba_audio-0.1.0/.github/workflows/ci.yml +38 -0
  7. marimba_audio-0.1.0/.github/workflows/publish.yml +40 -0
  8. marimba_audio-0.1.0/.github/workflows/security.yml +62 -0
  9. marimba_audio-0.1.0/.gitignore +29 -0
  10. marimba_audio-0.1.0/.pre-commit-config.yaml +23 -0
  11. marimba_audio-0.1.0/.runpod/Dockerfile +16 -0
  12. marimba_audio-0.1.0/.runpod/README.md +192 -0
  13. marimba_audio-0.1.0/.runpod/handler.py +108 -0
  14. marimba_audio-0.1.0/.runpod/hub.json +26 -0
  15. marimba_audio-0.1.0/.runpod/tests.json +35 -0
  16. marimba_audio-0.1.0/AGENTS.md +20 -0
  17. marimba_audio-0.1.0/CLAUDE.md +1 -0
  18. marimba_audio-0.1.0/CODE_OF_CONDUCT.md +73 -0
  19. marimba_audio-0.1.0/CONTRIBUTING.md +34 -0
  20. marimba_audio-0.1.0/LICENSE +201 -0
  21. marimba_audio-0.1.0/PKG-INFO +183 -0
  22. marimba_audio-0.1.0/README.md +153 -0
  23. marimba_audio-0.1.0/SECURITY.md +3 -0
  24. marimba_audio-0.1.0/THIRD_PARTY_NOTICES.md +20 -0
  25. marimba_audio-0.1.0/benchmarks/l4_e2e_clip_ids.json +3922 -0
  26. marimba_audio-0.1.0/benchmarks/l4_e2e_latency.json +1263 -0
  27. marimba_audio-0.1.0/demo/.gitattributes +1 -0
  28. marimba_audio-0.1.0/demo/.gitignore +6 -0
  29. marimba_audio-0.1.0/demo/README.md +182 -0
  30. marimba_audio-0.1.0/demo/frontend/eslint.config.js +14 -0
  31. marimba_audio-0.1.0/demo/frontend/index.html +14 -0
  32. marimba_audio-0.1.0/demo/frontend/package-lock.json +3022 -0
  33. marimba_audio-0.1.0/demo/frontend/package.json +22 -0
  34. marimba_audio-0.1.0/demo/frontend/public/IBMPlexMono-Regular.ttf +0 -0
  35. marimba_audio-0.1.0/demo/frontend/public/IBMPlexMono-Thin.ttf +0 -0
  36. marimba_audio-0.1.0/demo/frontend/public/IBMPlexSans-Regular.ttf +0 -0
  37. marimba_audio-0.1.0/demo/frontend/public/OFL.txt +93 -0
  38. marimba_audio-0.1.0/demo/frontend/public/favicon.svg +1 -0
  39. marimba_audio-0.1.0/demo/frontend/public/marimba.svg +1 -0
  40. marimba_audio-0.1.0/demo/frontend/src/audio-worklet.js +38 -0
  41. marimba_audio-0.1.0/demo/frontend/src/audio-worklet.test.ts +65 -0
  42. marimba_audio-0.1.0/demo/frontend/src/instruments.ts +43 -0
  43. marimba_audio-0.1.0/demo/frontend/src/main.ts +424 -0
  44. marimba_audio-0.1.0/demo/frontend/src/presentation.test.ts +88 -0
  45. marimba_audio-0.1.0/demo/frontend/src/presentation.ts +45 -0
  46. marimba_audio-0.1.0/demo/frontend/src/production.test.ts +70 -0
  47. marimba_audio-0.1.0/demo/frontend/src/socket.test.ts +45 -0
  48. marimba_audio-0.1.0/demo/frontend/src/socket.ts +25 -0
  49. marimba_audio-0.1.0/demo/frontend/src/state.test.ts +162 -0
  50. marimba_audio-0.1.0/demo/frontend/src/state.ts +112 -0
  51. marimba_audio-0.1.0/demo/frontend/src/stt.ts +15 -0
  52. marimba_audio-0.1.0/demo/frontend/src/style.css +568 -0
  53. marimba_audio-0.1.0/demo/frontend/src/vite-env.d.ts +1 -0
  54. marimba_audio-0.1.0/demo/frontend/tsconfig.json +12 -0
  55. marimba_audio-0.1.0/demo/frontend/vite.config.ts +4 -0
  56. marimba_audio-0.1.0/demo/marimba_demo/__init__.py +1 -0
  57. marimba_audio-0.1.0/demo/marimba_demo/cards.py +90 -0
  58. marimba_audio-0.1.0/demo/marimba_demo/cli.py +78 -0
  59. marimba_audio-0.1.0/demo/marimba_demo/protocol.py +30 -0
  60. marimba_audio-0.1.0/demo/marimba_demo/scheduler.py +367 -0
  61. marimba_audio-0.1.0/demo/marimba_demo/server.py +192 -0
  62. marimba_audio-0.1.0/demo/marimba_demo/speech.py +64 -0
  63. marimba_audio-0.1.0/demo/pyproject.toml +40 -0
  64. marimba_audio-0.1.0/demo/tests/test_protocol.py +529 -0
  65. marimba_audio-0.1.0/demo/tests/test_scheduler.py +611 -0
  66. marimba_audio-0.1.0/docs/audio.md +27 -0
  67. marimba_audio-0.1.0/docs/batch.md +38 -0
  68. marimba_audio-0.1.0/docs/benchmarks.md +125 -0
  69. marimba_audio-0.1.0/docs/calibration.md +55 -0
  70. marimba_audio-0.1.0/docs/capabilities.md +93 -0
  71. marimba_audio-0.1.0/docs/index.md +17 -0
  72. marimba_audio-0.1.0/docs/quickstart.md +41 -0
  73. marimba_audio-0.1.0/docs/runtime.md +51 -0
  74. marimba_audio-0.1.0/docs/scheduling.md +91 -0
  75. marimba_audio-0.1.0/docs/streaming.md +66 -0
  76. marimba_audio-0.1.0/hf/README.md +174 -0
  77. marimba_audio-0.1.0/notebooks/01_quickstart.ipynb +113 -0
  78. marimba_audio-0.1.0/notebooks/02_datasets_map.ipynb +107 -0
  79. marimba_audio-0.1.0/notebooks/03_streaming.ipynb +124 -0
  80. marimba_audio-0.1.0/notebooks/README.md +23 -0
  81. marimba_audio-0.1.0/pyproject.toml +67 -0
  82. marimba_audio-0.1.0/scripts/check_public.py +108 -0
  83. marimba_audio-0.1.0/scripts/l4_e2e_latency.py +313 -0
  84. marimba_audio-0.1.0/scripts/latency_matrix.py +356 -0
  85. marimba_audio-0.1.0/scripts/tests/test_l4_e2e_latency.py +151 -0
  86. marimba_audio-0.1.0/space/README.md +43 -0
  87. marimba_audio-0.1.0/space/app.py +72 -0
  88. marimba_audio-0.1.0/space/requirements.txt +2 -0
  89. marimba_audio-0.1.0/src/marimba/__init__.py +28 -0
  90. marimba_audio-0.1.0/src/marimba/_diarization.py +122 -0
  91. marimba_audio-0.1.0/src/marimba/_head.py +205 -0
  92. marimba_audio-0.1.0/src/marimba/_qwen.py +270 -0
  93. marimba_audio-0.1.0/src/marimba/_vendor/__init__.py +1 -0
  94. marimba_audio-0.1.0/src/marimba/_vendor/qwen_asr/LICENSE +201 -0
  95. marimba_audio-0.1.0/src/marimba/_vendor/qwen_asr/__init__.py +1 -0
  96. marimba_audio-0.1.0/src/marimba/_vendor/qwen_asr/configuration_qwen3_asr.py +425 -0
  97. marimba_audio-0.1.0/src/marimba/_vendor/qwen_asr/modeling_qwen3_asr.py +1365 -0
  98. marimba_audio-0.1.0/src/marimba/_vendor/qwen_asr/processing_qwen3_asr.py +209 -0
  99. marimba_audio-0.1.0/src/marimba/_vendor/qwen_asr/utils.py +211 -0
  100. marimba_audio-0.1.0/src/marimba/audio.py +69 -0
  101. marimba_audio-0.1.0/src/marimba/calibration.py +106 -0
  102. marimba_audio-0.1.0/src/marimba/capabilities.py +65 -0
  103. marimba_audio-0.1.0/src/marimba/cli.py +253 -0
  104. marimba_audio-0.1.0/src/marimba/decision.py +47 -0
  105. marimba_audio-0.1.0/src/marimba/integrations/__init__.py +1 -0
  106. marimba_audio-0.1.0/src/marimba/integrations/pipecat.py +58 -0
  107. marimba_audio-0.1.0/src/marimba/language.py +75 -0
  108. marimba_audio-0.1.0/src/marimba/lexical.py +18 -0
  109. marimba_audio-0.1.0/src/marimba/model.py +231 -0
  110. marimba_audio-0.1.0/src/marimba/pii.py +5 -0
  111. marimba_audio-0.1.0/src/marimba/pretrained.py +334 -0
  112. marimba_audio-0.1.0/src/marimba/py.typed +0 -0
  113. marimba_audio-0.1.0/src/marimba/questions.py +123 -0
  114. marimba_audio-0.1.0/src/marimba/readout.py +39 -0
  115. marimba_audio-0.1.0/src/marimba/runtime.py +111 -0
  116. marimba_audio-0.1.0/src/marimba/stream.py +222 -0
  117. marimba_audio-0.1.0/src/marimba/testing.py +149 -0
  118. marimba_audio-0.1.0/src/marimba/weights.py +42 -0
  119. marimba_audio-0.1.0/src/marimba/windows.py +88 -0
  120. marimba_audio-0.1.0/tests/test_calibration.py +94 -0
  121. marimba_audio-0.1.0/tests/test_cli_bench.py +150 -0
  122. marimba_audio-0.1.0/tests/test_diarization.py +219 -0
  123. marimba_audio-0.1.0/tests/test_diarization_reference.py +31 -0
  124. marimba_audio-0.1.0/tests/test_head.py +51 -0
  125. marimba_audio-0.1.0/tests/test_language_options.py +22 -0
  126. marimba_audio-0.1.0/tests/test_latency_matrix.py +56 -0
  127. marimba_audio-0.1.0/tests/test_package.py +341 -0
  128. marimba_audio-0.1.0/tests/test_pipecat.py +113 -0
  129. marimba_audio-0.1.0/tests/test_pretrained.py +729 -0
  130. marimba_audio-0.1.0/tests/test_qwen_inputs.py +177 -0
  131. marimba_audio-0.1.0/tests/test_release_scaffold.py +92 -0
  132. marimba_audio-0.1.0/tests/test_runpod_worker.py +168 -0
  133. marimba_audio-0.1.0/tests/test_runtime.py +114 -0
  134. marimba_audio-0.1.0/tests/test_scheduling.py +148 -0
  135. marimba_audio-0.1.0/tests/test_stream.py +123 -0
  136. marimba_audio-0.1.0/tests/test_vendored_qwen.py +34 -0
  137. marimba_audio-0.1.0/uv.lock +5659 -0
@@ -0,0 +1,11 @@
1
+ **
2
+ !pyproject.toml
3
+ !README.md
4
+ !LICENSE
5
+ !THIRD_PARTY_NOTICES.md
6
+ !src/
7
+ !src/**
8
+ !.runpod/
9
+ !.runpod/handler.py
10
+ **/__pycache__/
11
+ **/*.pyc
@@ -0,0 +1,14 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+
8
+ [*.py]
9
+ indent_style = space
10
+ indent_size = 4
11
+
12
+ [*.{yml,json,md}]
13
+ indent_style = space
14
+ indent_size = 2
@@ -0,0 +1 @@
1
+ * @solanovisitor
@@ -0,0 +1,10 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ groups:
8
+ github-actions:
9
+ patterns:
10
+ - "*"
@@ -0,0 +1,6 @@
1
+ ## Checklist
2
+
3
+ - [ ] Tests and checks have been run.
4
+ - [ ] Documentation has been updated.
5
+ - [ ] Only public or synthetic data is included.
6
+ - [ ] No internal names, non-public identifiers, or secrets are included.
@@ -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
+ }