docs: karaoke pipeline architecture with dot diagrams
Add docs/karaoke-pipeline.md covering the full streaming pipeline from MPS through unsandbox API to zerotrust container and back. Includes two Graphviz dot diagrams (rendered to SVG): - karaoke-pipeline.dot: full system flow across MPS, API, pool, container - karaoke-ondemand.dot: watch mode on-demand user flow Update architecture.md feature toggle matrix and related docs table. Update CLAUDE.md karaoke section with streaming path and on-demand info.
This commit is contained in:
parent
6fd6bfd756
commit
8649e6aaae
7 changed files with 887 additions and 5 deletions
15
CLAUDE.md
15
CLAUDE.md
|
|
@ -29,15 +29,20 @@ Files are NEVER streamed through uwsgi. The server only generates presigned URLs
|
|||
|
||||
### Karaoke Pipeline (lib/karaoke.py)
|
||||
|
||||
Disk-backed vocal isolation pipeline. Downloads media from S3, builds a JSON
|
||||
request on disk with streaming base64 encoding, POSTs to unsandbox API, streams
|
||||
response to disk, decodes artifacts one at a time and uploads to S3.
|
||||
Disk-backed vocal isolation pipeline using spectral mid-side Wiener masking
|
||||
(`voxsplit.c`, zero ML deps). Streams media to unsandbox via `POST /upload`
|
||||
(64KB chunks, constant memory), executes in zerotrust container, streams
|
||||
response back, uploads instrumentals + vocals to S3.
|
||||
|
||||
Full architecture doc: `docs/karaoke-pipeline.md` (with dot diagrams).
|
||||
|
||||
- **Concurrency**: `ThreadPoolExecutor` sized to account's unsandbox concurrency limit
|
||||
- **Memory**: ~64KB per worker on the request side (disk-backed, not in-memory)
|
||||
- **Memory**: ~64KB per worker at every stage (disk-backed, not in-memory)
|
||||
- **Upstream limit**: 3.698GB / 3,698,742,051 bytes per file (unsandbox `@max_upload_bytes`)
|
||||
- **Retries**: 3 attempts with exponential backoff (5s, 10s)
|
||||
- **Callers**: `views/product.py`, `views/shop.py` (backfill), `scripts/backfill_karaoke.py`
|
||||
- **Callers**: `views/product.py` (upload), `views/watch.py` (on-demand), `views/shop.py` (backfill), `scripts/backfill_karaoke.py`
|
||||
- **On-demand**: `POST /karaoke/{product_id}` — forks detached child, watch.js 10s refresh detects completion, auto-switches to instrumentals
|
||||
- **Streaming path**: MPS → `POST /upload` → API encrypts to disk → pool pulls via `GET /internal/upload/{id}` → pipes into container `/root/input/` — zero bytes cross Erlang distribution
|
||||
|
||||
## Project Setup
|
||||
|
||||
|
|
|
|||
|
|
@ -206,6 +206,7 @@ mps_page_session (raw rows)
|
|||
| Trial | `shop.trial_started_timestamp` | Auto on creation | 21 days |
|
||||
| Discovery ring | `shop.discovery_ring` | Automatic | Auto-computed |
|
||||
| Subscriptions | `shop.subscription_*` | `ribbon-settings` | Off |
|
||||
| Karaoke (vocal isolation) | `shop.unsandbox_*_key` | `unsandbox-settings` | Off |
|
||||
|
||||
## Ticket Index
|
||||
|
||||
|
|
@ -235,5 +236,6 @@ mps_page_session (raw rows)
|
|||
|-----|---------|
|
||||
| [Design System](design-system.md) | Design tokens, CSS architecture, component library |
|
||||
| [JavaScript](JAVASCRIPT.md) | Client-side JS architecture |
|
||||
| [Karaoke Pipeline](karaoke-pipeline.md) | Vocal isolation: pipeline, on-demand, streaming architecture |
|
||||
| [Sandbox Mode](sandbox-mode.md) | Creative filter system |
|
||||
| [Testing Performance](testing-performance.md) | Test suite optimization |
|
||||
|
|
|
|||
30
docs/karaoke-ondemand.dot
Normal file
30
docs/karaoke-ondemand.dot
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// On-Demand Karaoke — Watch Mode User Flow
|
||||
// Render: dot -Tsvg docs/karaoke-ondemand.dot -o docs/karaoke-ondemand.dot.svg
|
||||
digraph karaoke_ondemand {
|
||||
rankdir=TB;
|
||||
node [shape=box, style="rounded,filled", fontname="monospace", fontsize=10];
|
||||
edge [fontname="monospace", fontsize=9];
|
||||
|
||||
click [label="User clicks 🎤\n(watch.js)", fillcolor="#e0f2f1"];
|
||||
check [label="Has tracks?", shape=diamond, fillcolor="#fff8e1"];
|
||||
cycle [label="cycleKaraoke()\nOriginal → Instrumentals → Vocals", fillcolor="#e8f5e9"];
|
||||
post [label="POST /karaoke/{id}\n(fetch, non-blocking)", fillcolor="#e8eaf6"];
|
||||
fork [label="Server forks\ndetached grandchild", fillcolor="#e8eaf6"];
|
||||
process [label="process_karaoke()\n(~30-120s)", fillcolor="#fce4ec"];
|
||||
hourglass [label="Button shows ⌛\nkaraokeProcessing=true", fillcolor="#e0f2f1"];
|
||||
refresh [label="10s URL refresh\nfetchWatchData()", fillcolor="#e0f2f1"];
|
||||
detect [label="instrumentals_url\nappears in JSON?", shape=diamond, fillcolor="#fff8e1"];
|
||||
autoswitch [label="Auto-switch to\ninstrumentals 🎤", fillcolor="#e8f5e9"];
|
||||
|
||||
click -> check;
|
||||
check -> cycle [label="yes"];
|
||||
check -> post [label="no (eligible)"];
|
||||
post -> fork [label="200 {status: processing}"];
|
||||
post -> hourglass;
|
||||
fork -> process;
|
||||
hourglass -> refresh [label="every 10s"];
|
||||
refresh -> detect;
|
||||
detect -> refresh [label="not yet"];
|
||||
detect -> autoswitch [label="tracks ready"];
|
||||
process -> detect [label="DB updated", style=dashed];
|
||||
}
|
||||
155
docs/karaoke-ondemand.dot.svg
Normal file
155
docs/karaoke-ondemand.dot.svg
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.43.0 (0)
|
||||
-->
|
||||
<!-- Title: karaoke_ondemand Pages: 1 -->
|
||||
<svg width="458pt" height="577pt"
|
||||
viewBox="0.00 0.00 458.00 577.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 573)">
|
||||
<title>karaoke_ondemand</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-573 454,-573 454,4 -4,4"/>
|
||||
<!-- click -->
|
||||
<g id="node1" class="node">
|
||||
<title>click</title>
|
||||
<path fill="#e0f2f1" stroke="black" d="M165,-569C165,-569 88,-569 88,-569 82,-569 76,-563 76,-557 76,-557 76,-545 76,-545 76,-539 82,-533 88,-533 88,-533 165,-533 165,-533 171,-533 177,-539 177,-545 177,-545 177,-557 177,-557 177,-563 171,-569 165,-569"/>
|
||||
<text text-anchor="middle" x="126.5" y="-554" font-family="monospace" font-size="10.00">User clicks 🎤</text>
|
||||
<text text-anchor="middle" x="126.5" y="-543" font-family="monospace" font-size="10.00">(watch.js)</text>
|
||||
</g>
|
||||
<!-- check -->
|
||||
<g id="node2" class="node">
|
||||
<title>check</title>
|
||||
<path fill="#fff8e1" stroke="black" d="M114.89,-492.98C114.89,-492.98 68.92,-481.02 68.92,-481.02 63.11,-479.51 63.11,-476.49 68.92,-474.98 68.92,-474.98 114.89,-463.02 114.89,-463.02 120.69,-461.51 132.31,-461.51 138.11,-463.02 138.11,-463.02 184.08,-474.98 184.08,-474.98 189.89,-476.49 189.89,-479.51 184.08,-481.02 184.08,-481.02 138.11,-492.98 138.11,-492.98 132.31,-494.49 120.69,-494.49 114.89,-492.98"/>
|
||||
<text text-anchor="middle" x="126.5" y="-475.5" font-family="monospace" font-size="10.00">Has tracks?</text>
|
||||
</g>
|
||||
<!-- click->check -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>click->check</title>
|
||||
<path fill="none" stroke="black" d="M126.5,-532.81C126.5,-524.79 126.5,-515.05 126.5,-506.07"/>
|
||||
<polygon fill="black" stroke="black" points="130,-506.03 126.5,-496.03 123,-506.03 130,-506.03"/>
|
||||
</g>
|
||||
<!-- cycle -->
|
||||
<g id="node3" class="node">
|
||||
<title>cycle</title>
|
||||
<path fill="#e8f5e9" stroke="black" d="M203,-414C203,-414 12,-414 12,-414 6,-414 0,-408 0,-402 0,-402 0,-390 0,-390 0,-384 6,-378 12,-378 12,-378 203,-378 203,-378 209,-378 215,-384 215,-390 215,-390 215,-402 215,-402 215,-408 209,-414 203,-414"/>
|
||||
<text text-anchor="middle" x="107.5" y="-399" font-family="monospace" font-size="10.00">cycleKaraoke()</text>
|
||||
<text text-anchor="middle" x="107.5" y="-388" font-family="monospace" font-size="10.00">Original → Instrumentals → Vocals</text>
|
||||
</g>
|
||||
<!-- check->cycle -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>check->cycle</title>
|
||||
<path fill="none" stroke="black" d="M122.66,-460.81C120.17,-450.36 116.88,-436.5 114,-424.36"/>
|
||||
<polygon fill="black" stroke="black" points="117.32,-423.19 111.6,-414.27 110.51,-424.81 117.32,-423.19"/>
|
||||
<text text-anchor="middle" x="126.5" y="-434.8" font-family="monospace" font-size="9.00">yes</text>
|
||||
</g>
|
||||
<!-- post -->
|
||||
<g id="node4" class="node">
|
||||
<title>post</title>
|
||||
<path fill="#e8eaf6" stroke="black" d="M364,-414C364,-414 245,-414 245,-414 239,-414 233,-408 233,-402 233,-402 233,-390 233,-390 233,-384 239,-378 245,-378 245,-378 364,-378 364,-378 370,-378 376,-384 376,-390 376,-390 376,-402 376,-402 376,-408 370,-414 364,-414"/>
|
||||
<text text-anchor="middle" x="304.5" y="-399" font-family="monospace" font-size="10.00">POST /karaoke/{id}</text>
|
||||
<text text-anchor="middle" x="304.5" y="-388" font-family="monospace" font-size="10.00">(fetch, non-blocking)</text>
|
||||
</g>
|
||||
<!-- check->post -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>check->post</title>
|
||||
<path fill="none" stroke="black" d="M150.51,-466.21C177.74,-453.97 222.83,-433.7 257.2,-418.26"/>
|
||||
<polygon fill="black" stroke="black" points="258.9,-421.33 266.59,-414.04 256.03,-414.94 258.9,-421.33"/>
|
||||
<text text-anchor="middle" x="261" y="-434.8" font-family="monospace" font-size="9.00">no (eligible)</text>
|
||||
</g>
|
||||
<!-- fork -->
|
||||
<g id="node5" class="node">
|
||||
<title>fork</title>
|
||||
<path fill="#e8eaf6" stroke="black" d="M287,-332C287,-332 180,-332 180,-332 174,-332 168,-326 168,-320 168,-320 168,-308 168,-308 168,-302 174,-296 180,-296 180,-296 287,-296 287,-296 293,-296 299,-302 299,-308 299,-308 299,-320 299,-320 299,-326 293,-332 287,-332"/>
|
||||
<text text-anchor="middle" x="233.5" y="-317" font-family="monospace" font-size="10.00">Server forks</text>
|
||||
<text text-anchor="middle" x="233.5" y="-306" font-family="monospace" font-size="10.00">detached grandchild</text>
|
||||
</g>
|
||||
<!-- post->fork -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>post->fork</title>
|
||||
<path fill="none" stroke="black" d="M253.29,-377.86C245.86,-373.21 239.15,-367.35 234.5,-360 231.18,-354.76 229.82,-348.43 229.53,-342.21"/>
|
||||
<polygon fill="black" stroke="black" points="233.03,-342.14 229.89,-332.03 226.04,-341.9 233.03,-342.14"/>
|
||||
<text text-anchor="middle" x="298" y="-352.8" font-family="monospace" font-size="9.00">200 {status: processing}</text>
|
||||
</g>
|
||||
<!-- hourglass -->
|
||||
<g id="node7" class="node">
|
||||
<title>hourglass</title>
|
||||
<path fill="#e0f2f1" stroke="black" d="M438,-278C438,-278 313,-278 313,-278 307,-278 301,-272 301,-266 301,-266 301,-254 301,-254 301,-248 307,-242 313,-242 313,-242 438,-242 438,-242 444,-242 450,-248 450,-254 450,-254 450,-266 450,-266 450,-272 444,-278 438,-278"/>
|
||||
<text text-anchor="middle" x="375.5" y="-263" font-family="monospace" font-size="10.00">Button shows ⌛</text>
|
||||
<text text-anchor="middle" x="375.5" y="-252" font-family="monospace" font-size="10.00">karaokeProcessing=true</text>
|
||||
</g>
|
||||
<!-- post->hourglass -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>post->hourglass</title>
|
||||
<path fill="none" stroke="black" d="M342.82,-377.98C350.04,-373.14 356.79,-367.19 361.5,-360 375.36,-338.86 377.98,-309.74 377.67,-288.55"/>
|
||||
<polygon fill="black" stroke="black" points="381.15,-288.15 377.28,-278.28 374.16,-288.41 381.15,-288.15"/>
|
||||
</g>
|
||||
<!-- process -->
|
||||
<g id="node6" class="node">
|
||||
<title>process</title>
|
||||
<path fill="#fce4ec" stroke="black" d="M279,-224C279,-224 184,-224 184,-224 178,-224 172,-218 172,-212 172,-212 172,-200 172,-200 172,-194 178,-188 184,-188 184,-188 279,-188 279,-188 285,-188 291,-194 291,-200 291,-200 291,-212 291,-212 291,-218 285,-224 279,-224"/>
|
||||
<text text-anchor="middle" x="231.5" y="-209" font-family="monospace" font-size="10.00">process_karaoke()</text>
|
||||
<text text-anchor="middle" x="231.5" y="-198" font-family="monospace" font-size="10.00">(~30-120s)</text>
|
||||
</g>
|
||||
<!-- fork->process -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>fork->process</title>
|
||||
<path fill="none" stroke="black" d="M233.18,-295.97C232.87,-279.38 232.38,-253.88 232.02,-234.43"/>
|
||||
<polygon fill="black" stroke="black" points="235.52,-234.27 231.83,-224.34 228.52,-234.4 235.52,-234.27"/>
|
||||
</g>
|
||||
<!-- detect -->
|
||||
<g id="node9" class="node">
|
||||
<title>detect</title>
|
||||
<path fill="#fff8e1" stroke="black" d="M216.86,-139.07C216.86,-139.07 121.14,-114.93 121.14,-114.93 115.32,-113.47 115.32,-110.53 121.14,-109.07 121.14,-109.07 216.86,-84.93 216.86,-84.93 222.68,-83.47 234.32,-83.47 240.14,-84.93 240.14,-84.93 335.86,-109.07 335.86,-109.07 341.68,-110.53 341.68,-113.47 335.86,-114.93 335.86,-114.93 240.14,-139.07 240.14,-139.07 234.32,-140.53 222.68,-140.53 216.86,-139.07"/>
|
||||
<text text-anchor="middle" x="228.5" y="-115" font-family="monospace" font-size="10.00">instrumentals_url</text>
|
||||
<text text-anchor="middle" x="228.5" y="-104" font-family="monospace" font-size="10.00">appears in JSON?</text>
|
||||
</g>
|
||||
<!-- process->detect -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>process->detect</title>
|
||||
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M230.94,-187.7C230.61,-177.61 230.18,-164.46 229.77,-152.01"/>
|
||||
<polygon fill="black" stroke="black" points="233.26,-151.68 229.44,-141.8 226.27,-151.91 233.26,-151.68"/>
|
||||
<text text-anchor="middle" x="257" y="-162.8" font-family="monospace" font-size="9.00">DB updated</text>
|
||||
</g>
|
||||
<!-- refresh -->
|
||||
<g id="node8" class="node">
|
||||
<title>refresh</title>
|
||||
<path fill="#e0f2f1" stroke="black" d="M345,-36C345,-36 256,-36 256,-36 250,-36 244,-30 244,-24 244,-24 244,-12 244,-12 244,-6 250,0 256,0 256,0 345,0 345,0 351,0 357,-6 357,-12 357,-12 357,-24 357,-24 357,-30 351,-36 345,-36"/>
|
||||
<text text-anchor="middle" x="300.5" y="-21" font-family="monospace" font-size="10.00">10s URL refresh</text>
|
||||
<text text-anchor="middle" x="300.5" y="-10" font-family="monospace" font-size="10.00">fetchWatchData()</text>
|
||||
</g>
|
||||
<!-- hourglass->refresh -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>hourglass->refresh</title>
|
||||
<path fill="none" stroke="black" d="M377.02,-241.77C379.19,-209.07 380.58,-136.4 356.5,-82 350.05,-67.43 338.96,-53.86 328.34,-43.07"/>
|
||||
<polygon fill="black" stroke="black" points="330.74,-40.52 321.12,-36.07 325.86,-45.55 330.74,-40.52"/>
|
||||
<text text-anchor="middle" x="400.5" y="-162.8" font-family="monospace" font-size="9.00">every 10s</text>
|
||||
</g>
|
||||
<!-- refresh->detect -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>refresh->detect</title>
|
||||
<path fill="none" stroke="black" d="M287.08,-36.15C277.84,-47.96 265.27,-64.02 254.14,-78.23"/>
|
||||
<polygon fill="black" stroke="black" points="251.2,-76.31 247.79,-86.35 256.71,-80.63 251.2,-76.31"/>
|
||||
</g>
|
||||
<!-- detect->refresh -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>detect->refresh</title>
|
||||
<path fill="none" stroke="black" d="M252.48,-88.02C259.61,-80.65 267.19,-72.26 273.5,-64 277.96,-58.16 282.29,-51.54 286.12,-45.2"/>
|
||||
<polygon fill="black" stroke="black" points="289.33,-46.65 291.35,-36.25 283.29,-43.12 289.33,-46.65"/>
|
||||
<text text-anchor="middle" x="298" y="-56.8" font-family="monospace" font-size="9.00">not yet</text>
|
||||
</g>
|
||||
<!-- autoswitch -->
|
||||
<g id="node10" class="node">
|
||||
<title>autoswitch</title>
|
||||
<path fill="#e8f5e9" stroke="black" d="M214,-36C214,-36 125,-36 125,-36 119,-36 113,-30 113,-24 113,-24 113,-12 113,-12 113,-6 119,0 125,0 125,0 214,0 214,0 220,0 226,-6 226,-12 226,-12 226,-24 226,-24 226,-30 220,-36 214,-36"/>
|
||||
<text text-anchor="middle" x="169.5" y="-21" font-family="monospace" font-size="10.00">Auto-switch to</text>
|
||||
<text text-anchor="middle" x="169.5" y="-10" font-family="monospace" font-size="10.00">instrumentals 🎤</text>
|
||||
</g>
|
||||
<!-- detect->autoswitch -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>detect->autoswitch</title>
|
||||
<path fill="none" stroke="black" d="M199.97,-89.01C192.3,-81.8 184.7,-73.25 179.5,-64 176.43,-58.55 174.31,-52.23 172.83,-46.09"/>
|
||||
<polygon fill="black" stroke="black" points="176.24,-45.25 170.92,-36.09 169.36,-46.57 176.24,-45.25"/>
|
||||
<text text-anchor="middle" x="211.5" y="-56.8" font-family="monospace" font-size="9.00">tracks ready</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
77
docs/karaoke-pipeline.dot
Normal file
77
docs/karaoke-pipeline.dot
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Karaoke Pipeline — MPS → Unsandbox → Container → S3
|
||||
// Render: dot -Tsvg docs/karaoke-pipeline.dot -o docs/karaoke-pipeline.dot.svg
|
||||
digraph karaoke_pipeline {
|
||||
rankdir=LR;
|
||||
node [shape=box, style="rounded,filled", fontname="monospace", fontsize=10];
|
||||
edge [fontname="monospace", fontsize=9];
|
||||
|
||||
subgraph cluster_mps {
|
||||
label="MPS (uWSGI)";
|
||||
style=dashed;
|
||||
color="#5871ad";
|
||||
|
||||
trigger [label="Trigger\n(upload / on-demand / backfill)", fillcolor="#e8eaf6"];
|
||||
s3_down [label="S3 Download\n→ /tmp/karaoke_*/media.bin\n64KB chunks", fillcolor="#e8eaf6"];
|
||||
upload_media [label="POST /upload\n(streaming file body)\n→ upload_id", fillcolor="#e8eaf6"];
|
||||
upload_vox [label="POST /upload\nvoxsplit.c\n→ upload_id", fillcolor="#e8eaf6"];
|
||||
execute [label="POST /execute\n{upload_ids, script}\ntiny JSON, no file bytes", fillcolor="#e8eaf6"];
|
||||
stream_resp [label="Stream response\n→ /tmp/karaoke_*/response.json\n64KB chunks", fillcolor="#e8eaf6"];
|
||||
decode [label="Decode artifacts\n(base64 → tmpfile)\nupload to S3", fillcolor="#e8eaf6"];
|
||||
db_update [label="Update product\nextensions + file_bytes\nset S3 ACLs", fillcolor="#e8eaf6"];
|
||||
}
|
||||
|
||||
subgraph cluster_api {
|
||||
label="api.unsandbox.com";
|
||||
style=dashed;
|
||||
color="#ad5871";
|
||||
|
||||
recv_upload [label="Receive upload\nAES-256-CTR encrypt\n→ /tmp/uploads/{id}.enc\nkey in ETS only", fillcolor="#fce4ec"];
|
||||
dispatch [label="Dispatch execute\nErlang RPC\n~200B metadata only", fillcolor="#fce4ec"];
|
||||
}
|
||||
|
||||
subgraph cluster_pool {
|
||||
label="Pool Node";
|
||||
style=dashed;
|
||||
color="#58ad71";
|
||||
|
||||
pull [label="GET /internal/upload/{id}\nX-Upload-Key auth\nstreaming decrypt", fillcolor="#e8f5e9"];
|
||||
inject [label="lxc exec ... cat >\n/root/input/{filename}\n64KB streaming pipe", fillcolor="#e8f5e9"];
|
||||
}
|
||||
|
||||
subgraph cluster_container {
|
||||
label="Zerotrust Container";
|
||||
style=dashed;
|
||||
color="#ad8f58";
|
||||
|
||||
compile [label="gcc -O2 voxsplit.c -lm", fillcolor="#fff8e1"];
|
||||
extract [label="ffmpeg -i media\n-vn -acodec pcm_s16le\n-ar 44100 -ac 2\naudio.wav", fillcolor="#fff8e1"];
|
||||
split [label="voxsplit audio.wav\n→ split-instrumental.wav\n→ split-vocal.wav", fillcolor="#fff8e1"];
|
||||
remux [label="ffmpeg remux\n(video: copy video +\nisolated audio)\n(audio: copy wav)", fillcolor="#fff8e1"];
|
||||
artifacts [label="/tmp/artifacts/\ninstrumentals.{ext}\nvocals.{ext}", fillcolor="#fff8e1"];
|
||||
}
|
||||
|
||||
s3 [label="S3 / CDN\n(DO Spaces or BYOB)", shape=cylinder, fillcolor="#f3e5f5"];
|
||||
browser [label="Browser\n(watch.js)", shape=ellipse, fillcolor="#e0f2f1"];
|
||||
|
||||
trigger -> s3_down;
|
||||
s3_down -> upload_media [label="file on disk"];
|
||||
s3_down -> upload_vox;
|
||||
upload_media -> recv_upload [label="streaming\noctet-stream"];
|
||||
upload_vox -> recv_upload;
|
||||
recv_upload -> execute [label="upload_id", style=dashed, dir=back];
|
||||
execute -> dispatch [label="JSON\n{upload_ids}"];
|
||||
dispatch -> pull [label="RPC\nmetadata only"];
|
||||
pull -> recv_upload [label="HTTPS GET\nstreaming decrypt", style=dotted];
|
||||
pull -> inject;
|
||||
inject -> compile;
|
||||
compile -> extract;
|
||||
extract -> split;
|
||||
split -> remux;
|
||||
remux -> artifacts;
|
||||
artifacts -> stream_resp [label="response JSON\nbase64 artifacts", style=dotted];
|
||||
stream_resp -> decode;
|
||||
decode -> s3 [label="PUT instrumentals\nPUT vocals"];
|
||||
db_update -> s3 [label="ACL update", style=dashed];
|
||||
decode -> db_update;
|
||||
s3 -> browser [label="presigned URL\n15 min TTL"];
|
||||
}
|
||||
327
docs/karaoke-pipeline.dot.svg
Normal file
327
docs/karaoke-pipeline.dot.svg
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.43.0 (0)
|
||||
-->
|
||||
<!-- Title: karaoke_pipeline Pages: 1 -->
|
||||
<svg width="1637pt" height="501pt"
|
||||
viewBox="0.00 0.00 1637.00 501.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 497)">
|
||||
<title>karaoke_pipeline</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-497 1633,-497 1633,4 -4,4"/>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_mps</title>
|
||||
<polygon fill="none" stroke="#5871ad" stroke-dasharray="5,2" points="8,-286 8,-485 781,-485 781,-286 8,-286"/>
|
||||
<text text-anchor="middle" x="394.5" y="-469.8" font-family="Times,serif" font-size="14.00">MPS (uWSGI)</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_api</title>
|
||||
<polygon fill="none" stroke="#ad5871" stroke-dasharray="5,2" points="346,-127 346,-278 517,-278 517,-127 346,-127"/>
|
||||
<text text-anchor="middle" x="431.5" y="-262.8" font-family="Times,serif" font-size="14.00">api.unsandbox.com</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_pool</title>
|
||||
<polygon fill="none" stroke="#58ad71" stroke-dasharray="5,2" points="26,-8 26,-89 514,-89 514,-8 26,-8"/>
|
||||
<text text-anchor="middle" x="270" y="-73.8" font-family="Times,serif" font-size="14.00">Pool Node</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_container</title>
|
||||
<polygon fill="none" stroke="#ad8f58" stroke-dasharray="5,2" points="613,-10 613,-101 1621,-101 1621,-10 613,-10"/>
|
||||
<text text-anchor="middle" x="1117" y="-85.8" font-family="Times,serif" font-size="14.00">Zerotrust Container</text>
|
||||
</g>
|
||||
<!-- trigger -->
|
||||
<g id="node1" class="node">
|
||||
<title>trigger</title>
|
||||
<path fill="#e8eaf6" stroke="black" d="M207,-449C207,-449 28,-449 28,-449 22,-449 16,-443 16,-437 16,-437 16,-425 16,-425 16,-419 22,-413 28,-413 28,-413 207,-413 207,-413 213,-413 219,-419 219,-425 219,-425 219,-437 219,-437 219,-443 213,-449 207,-449"/>
|
||||
<text text-anchor="middle" x="117.5" y="-434" font-family="monospace" font-size="10.00">Trigger</text>
|
||||
<text text-anchor="middle" x="117.5" y="-423" font-family="monospace" font-size="10.00">(upload / on-demand / backfill)</text>
|
||||
</g>
|
||||
<!-- s3_down -->
|
||||
<g id="node2" class="node">
|
||||
<title>s3_down</title>
|
||||
<path fill="#e8eaf6" stroke="black" d="M506,-394.5C506,-394.5 357,-394.5 357,-394.5 351,-394.5 345,-388.5 345,-382.5 345,-382.5 345,-365.5 345,-365.5 345,-359.5 351,-353.5 357,-353.5 357,-353.5 506,-353.5 506,-353.5 512,-353.5 518,-359.5 518,-365.5 518,-365.5 518,-382.5 518,-382.5 518,-388.5 512,-394.5 506,-394.5"/>
|
||||
<text text-anchor="middle" x="431.5" y="-382.5" font-family="monospace" font-size="10.00">S3 Download</text>
|
||||
<text text-anchor="middle" x="431.5" y="-371.5" font-family="monospace" font-size="10.00">→ /tmp/karaoke_*/media.bin</text>
|
||||
<text text-anchor="middle" x="431.5" y="-360.5" font-family="monospace" font-size="10.00">64KB chunks</text>
|
||||
</g>
|
||||
<!-- trigger->s3_down -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>trigger->s3_down</title>
|
||||
<path fill="none" stroke="black" d="M217.22,-412.96C254.83,-406.09 297.6,-398.28 334.91,-391.46"/>
|
||||
<polygon fill="black" stroke="black" points="335.57,-394.9 344.78,-389.66 334.31,-388.01 335.57,-394.9"/>
|
||||
</g>
|
||||
<!-- upload_media -->
|
||||
<g id="node3" class="node">
|
||||
<title>upload_media</title>
|
||||
<path fill="#e8eaf6" stroke="black" d="M755,-394.5C755,-394.5 636,-394.5 636,-394.5 630,-394.5 624,-388.5 624,-382.5 624,-382.5 624,-365.5 624,-365.5 624,-359.5 630,-353.5 636,-353.5 636,-353.5 755,-353.5 755,-353.5 761,-353.5 767,-359.5 767,-365.5 767,-365.5 767,-382.5 767,-382.5 767,-388.5 761,-394.5 755,-394.5"/>
|
||||
<text text-anchor="middle" x="695.5" y="-382.5" font-family="monospace" font-size="10.00">POST /upload</text>
|
||||
<text text-anchor="middle" x="695.5" y="-371.5" font-family="monospace" font-size="10.00">(streaming file body)</text>
|
||||
<text text-anchor="middle" x="695.5" y="-360.5" font-family="monospace" font-size="10.00">→ upload_id</text>
|
||||
</g>
|
||||
<!-- s3_down->upload_media -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>s3_down->upload_media</title>
|
||||
<path fill="none" stroke="black" d="M518.29,-374C548.99,-374 583.48,-374 613.74,-374"/>
|
||||
<polygon fill="black" stroke="black" points="613.83,-377.5 623.83,-374 613.83,-370.5 613.83,-377.5"/>
|
||||
<text text-anchor="middle" x="568" y="-376.8" font-family="monospace" font-size="9.00">file on disk</text>
|
||||
</g>
|
||||
<!-- upload_vox -->
|
||||
<g id="node4" class="node">
|
||||
<title>upload_vox</title>
|
||||
<path fill="#e8eaf6" stroke="black" d="M728,-335.5C728,-335.5 663,-335.5 663,-335.5 657,-335.5 651,-329.5 651,-323.5 651,-323.5 651,-306.5 651,-306.5 651,-300.5 657,-294.5 663,-294.5 663,-294.5 728,-294.5 728,-294.5 734,-294.5 740,-300.5 740,-306.5 740,-306.5 740,-323.5 740,-323.5 740,-329.5 734,-335.5 728,-335.5"/>
|
||||
<text text-anchor="middle" x="695.5" y="-323.5" font-family="monospace" font-size="10.00">POST /upload</text>
|
||||
<text text-anchor="middle" x="695.5" y="-312.5" font-family="monospace" font-size="10.00">voxsplit.c</text>
|
||||
<text text-anchor="middle" x="695.5" y="-301.5" font-family="monospace" font-size="10.00">→ upload_id</text>
|
||||
</g>
|
||||
<!-- s3_down->upload_vox -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>s3_down->upload_vox</title>
|
||||
<path fill="none" stroke="black" d="M518.29,-354.68C558.63,-345.6 605.49,-335.04 640.74,-327.11"/>
|
||||
<polygon fill="black" stroke="black" points="641.84,-330.45 650.83,-324.83 640.3,-323.62 641.84,-330.45"/>
|
||||
</g>
|
||||
<!-- recv_upload -->
|
||||
<g id="node9" class="node">
|
||||
<title>recv_upload</title>
|
||||
<path fill="#fce4ec" stroke="black" d="M497,-247C497,-247 366,-247 366,-247 360,-247 354,-241 354,-235 354,-235 354,-207 354,-207 354,-201 360,-195 366,-195 366,-195 497,-195 497,-195 503,-195 509,-201 509,-207 509,-207 509,-235 509,-235 509,-241 503,-247 497,-247"/>
|
||||
<text text-anchor="middle" x="431.5" y="-235" font-family="monospace" font-size="10.00">Receive upload</text>
|
||||
<text text-anchor="middle" x="431.5" y="-224" font-family="monospace" font-size="10.00">AES-256-CTR encrypt</text>
|
||||
<text text-anchor="middle" x="431.5" y="-213" font-family="monospace" font-size="10.00">→ /tmp/uploads/{id}.enc</text>
|
||||
<text text-anchor="middle" x="431.5" y="-202" font-family="monospace" font-size="10.00">key in ETS only</text>
|
||||
</g>
|
||||
<!-- upload_media->recv_upload -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>upload_media->recv_upload</title>
|
||||
<path fill="none" stroke="black" d="M631.5,-353.46C625.16,-350.83 618.89,-348 613,-345 598.87,-337.79 526.49,-287.31 477.79,-253.02"/>
|
||||
<polygon fill="black" stroke="black" points="479.78,-250.14 469.59,-247.24 475.75,-255.86 479.78,-250.14"/>
|
||||
<text text-anchor="middle" x="568" y="-348.8" font-family="monospace" font-size="9.00">streaming</text>
|
||||
<text text-anchor="middle" x="568" y="-338.8" font-family="monospace" font-size="9.00">octet-stream</text>
|
||||
</g>
|
||||
<!-- upload_vox->recv_upload -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>upload_vox->recv_upload</title>
|
||||
<path fill="none" stroke="black" d="M665.99,-294.44C647.88,-282.24 623.49,-267.38 600,-258 574.5,-247.81 545.63,-240.16 519.14,-234.54"/>
|
||||
<polygon fill="black" stroke="black" points="519.61,-231.07 509.11,-232.49 518.2,-237.93 519.61,-231.07"/>
|
||||
</g>
|
||||
<!-- execute -->
|
||||
<g id="node5" class="node">
|
||||
<title>execute</title>
|
||||
<path fill="#e8eaf6" stroke="black" d="M186,-394.5C186,-394.5 49,-394.5 49,-394.5 43,-394.5 37,-388.5 37,-382.5 37,-382.5 37,-365.5 37,-365.5 37,-359.5 43,-353.5 49,-353.5 49,-353.5 186,-353.5 186,-353.5 192,-353.5 198,-359.5 198,-365.5 198,-365.5 198,-382.5 198,-382.5 198,-388.5 192,-394.5 186,-394.5"/>
|
||||
<text text-anchor="middle" x="117.5" y="-382.5" font-family="monospace" font-size="10.00">POST /execute</text>
|
||||
<text text-anchor="middle" x="117.5" y="-371.5" font-family="monospace" font-size="10.00">{upload_ids, script}</text>
|
||||
<text text-anchor="middle" x="117.5" y="-360.5" font-family="monospace" font-size="10.00">tiny JSON, no file bytes</text>
|
||||
</g>
|
||||
<!-- dispatch -->
|
||||
<g id="node10" class="node">
|
||||
<title>dispatch</title>
|
||||
<path fill="#fce4ec" stroke="black" d="M485,-176.5C485,-176.5 378,-176.5 378,-176.5 372,-176.5 366,-170.5 366,-164.5 366,-164.5 366,-147.5 366,-147.5 366,-141.5 372,-135.5 378,-135.5 378,-135.5 485,-135.5 485,-135.5 491,-135.5 497,-141.5 497,-147.5 497,-147.5 497,-164.5 497,-164.5 497,-170.5 491,-176.5 485,-176.5"/>
|
||||
<text text-anchor="middle" x="431.5" y="-164.5" font-family="monospace" font-size="10.00">Dispatch execute</text>
|
||||
<text text-anchor="middle" x="431.5" y="-153.5" font-family="monospace" font-size="10.00">Erlang RPC</text>
|
||||
<text text-anchor="middle" x="431.5" y="-142.5" font-family="monospace" font-size="10.00">~200B metadata only</text>
|
||||
</g>
|
||||
<!-- execute->dispatch -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>execute->dispatch</title>
|
||||
<path fill="none" stroke="black" d="M198.38,-360C206.06,-356.12 213.15,-351.21 219,-345 243.99,-318.47 216.7,-295.27 237,-265 264.19,-224.45 288.69,-232.27 327,-202 335.4,-195.36 335.78,-191.43 345,-186 348.66,-183.85 352.5,-181.81 356.44,-179.9"/>
|
||||
<polygon fill="black" stroke="black" points="358.13,-182.97 365.78,-175.65 355.23,-176.6 358.13,-182.97"/>
|
||||
<text text-anchor="middle" x="282" y="-277.8" font-family="monospace" font-size="9.00">JSON</text>
|
||||
<text text-anchor="middle" x="282" y="-267.8" font-family="monospace" font-size="9.00">{upload_ids}</text>
|
||||
</g>
|
||||
<!-- stream_resp -->
|
||||
<g id="node6" class="node">
|
||||
<title>stream_resp</title>
|
||||
<path fill="#e8eaf6" stroke="black" d="M204,-335.5C204,-335.5 31,-335.5 31,-335.5 25,-335.5 19,-329.5 19,-323.5 19,-323.5 19,-306.5 19,-306.5 19,-300.5 25,-294.5 31,-294.5 31,-294.5 204,-294.5 204,-294.5 210,-294.5 216,-300.5 216,-306.5 216,-306.5 216,-323.5 216,-323.5 216,-329.5 210,-335.5 204,-335.5"/>
|
||||
<text text-anchor="middle" x="117.5" y="-323.5" font-family="monospace" font-size="10.00">Stream response</text>
|
||||
<text text-anchor="middle" x="117.5" y="-312.5" font-family="monospace" font-size="10.00">→ /tmp/karaoke_*/response.json</text>
|
||||
<text text-anchor="middle" x="117.5" y="-301.5" font-family="monospace" font-size="10.00">64KB chunks</text>
|
||||
</g>
|
||||
<!-- decode -->
|
||||
<g id="node7" class="node">
|
||||
<title>decode</title>
|
||||
<path fill="#e8eaf6" stroke="black" d="M482,-453.5C482,-453.5 381,-453.5 381,-453.5 375,-453.5 369,-447.5 369,-441.5 369,-441.5 369,-424.5 369,-424.5 369,-418.5 375,-412.5 381,-412.5 381,-412.5 482,-412.5 482,-412.5 488,-412.5 494,-418.5 494,-424.5 494,-424.5 494,-441.5 494,-441.5 494,-447.5 488,-453.5 482,-453.5"/>
|
||||
<text text-anchor="middle" x="431.5" y="-441.5" font-family="monospace" font-size="10.00">Decode artifacts</text>
|
||||
<text text-anchor="middle" x="431.5" y="-430.5" font-family="monospace" font-size="10.00">(base64 → tmpfile)</text>
|
||||
<text text-anchor="middle" x="431.5" y="-419.5" font-family="monospace" font-size="10.00">upload to S3</text>
|
||||
</g>
|
||||
<!-- stream_resp->decode -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>stream_resp->decode</title>
|
||||
<path fill="none" stroke="black" d="M191.93,-335.54C201.11,-338.51 210.32,-341.69 219,-345 276.78,-367.01 287.63,-380.92 345,-404 349.49,-405.81 354.14,-407.6 358.85,-409.35"/>
|
||||
<polygon fill="black" stroke="black" points="357.96,-412.76 368.56,-412.9 360.36,-406.18 357.96,-412.76"/>
|
||||
</g>
|
||||
<!-- db_update -->
|
||||
<g id="node8" class="node">
|
||||
<title>db_update</title>
|
||||
<path fill="#e8eaf6" stroke="black" d="M761,-453.5C761,-453.5 630,-453.5 630,-453.5 624,-453.5 618,-447.5 618,-441.5 618,-441.5 618,-424.5 618,-424.5 618,-418.5 624,-412.5 630,-412.5 630,-412.5 761,-412.5 761,-412.5 767,-412.5 773,-418.5 773,-424.5 773,-424.5 773,-441.5 773,-441.5 773,-447.5 767,-453.5 761,-453.5"/>
|
||||
<text text-anchor="middle" x="695.5" y="-441.5" font-family="monospace" font-size="10.00">Update product</text>
|
||||
<text text-anchor="middle" x="695.5" y="-430.5" font-family="monospace" font-size="10.00">extensions + file_bytes</text>
|
||||
<text text-anchor="middle" x="695.5" y="-419.5" font-family="monospace" font-size="10.00">set S3 ACLs</text>
|
||||
</g>
|
||||
<!-- decode->db_update -->
|
||||
<g id="edge20" class="edge">
|
||||
<title>decode->db_update</title>
|
||||
<path fill="none" stroke="black" d="M494.26,-433C528,-433 570.34,-433 607.39,-433"/>
|
||||
<polygon fill="black" stroke="black" points="607.69,-436.5 617.69,-433 607.69,-429.5 607.69,-436.5"/>
|
||||
</g>
|
||||
<!-- s3 -->
|
||||
<g id="node18" class="node">
|
||||
<title>s3</title>
|
||||
<path fill="#f3e5f5" stroke="black" d="M999,-485.98C999,-488.06 969.64,-489.75 933.5,-489.75 897.36,-489.75 868,-488.06 868,-485.98 868,-485.98 868,-452.02 868,-452.02 868,-449.94 897.36,-448.25 933.5,-448.25 969.64,-448.25 999,-449.94 999,-452.02 999,-452.02 999,-485.98 999,-485.98"/>
|
||||
<path fill="none" stroke="black" d="M999,-485.98C999,-483.9 969.64,-482.21 933.5,-482.21 897.36,-482.21 868,-483.9 868,-485.98"/>
|
||||
<text text-anchor="middle" x="933.5" y="-472" font-family="monospace" font-size="10.00">S3 / CDN</text>
|
||||
<text text-anchor="middle" x="933.5" y="-461" font-family="monospace" font-size="10.00">(DO Spaces or BYOB)</text>
|
||||
</g>
|
||||
<!-- decode->s3 -->
|
||||
<g id="edge18" class="edge">
|
||||
<title>decode->s3</title>
|
||||
<path fill="none" stroke="black" d="M494.05,-445.28C528.82,-451.71 573.17,-459.04 613,-463 691.77,-470.84 711.86,-467.67 791,-469 812.63,-469.36 836.12,-469.48 857.63,-469.47"/>
|
||||
<polygon fill="black" stroke="black" points="857.69,-472.97 867.69,-469.46 857.68,-465.97 857.69,-472.97"/>
|
||||
<text text-anchor="middle" x="695.5" y="-480.8" font-family="monospace" font-size="9.00">PUT instrumentals</text>
|
||||
<text text-anchor="middle" x="695.5" y="-470.8" font-family="monospace" font-size="9.00">PUT vocals</text>
|
||||
</g>
|
||||
<!-- db_update->s3 -->
|
||||
<g id="edge19" class="edge">
|
||||
<title>db_update->s3</title>
|
||||
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M773.12,-444.69C800.21,-448.82 830.64,-453.46 857.58,-457.57"/>
|
||||
<polygon fill="black" stroke="black" points="857.31,-461.07 867.73,-459.12 858.37,-454.15 857.31,-461.07"/>
|
||||
<text text-anchor="middle" x="817.5" y="-457.8" font-family="monospace" font-size="9.00">ACL update</text>
|
||||
</g>
|
||||
<!-- recv_upload->execute -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>recv_upload->execute</title>
|
||||
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M384.11,-252.78C342.4,-280.21 278.7,-319.36 219,-345 211.91,-348.05 204.4,-350.88 196.81,-353.49"/>
|
||||
<polygon fill="black" stroke="black" points="386.26,-255.55 392.66,-247.11 382.39,-249.72 386.26,-255.55"/>
|
||||
<text text-anchor="middle" x="282" y="-338.8" font-family="monospace" font-size="9.00">upload_id</text>
|
||||
</g>
|
||||
<!-- pull -->
|
||||
<g id="node11" class="node">
|
||||
<title>pull</title>
|
||||
<path fill="#e8f5e9" stroke="black" d="M189,-57.5C189,-57.5 46,-57.5 46,-57.5 40,-57.5 34,-51.5 34,-45.5 34,-45.5 34,-28.5 34,-28.5 34,-22.5 40,-16.5 46,-16.5 46,-16.5 189,-16.5 189,-16.5 195,-16.5 201,-22.5 201,-28.5 201,-28.5 201,-45.5 201,-45.5 201,-51.5 195,-57.5 189,-57.5"/>
|
||||
<text text-anchor="middle" x="117.5" y="-45.5" font-family="monospace" font-size="10.00">GET /internal/upload/{id}</text>
|
||||
<text text-anchor="middle" x="117.5" y="-34.5" font-family="monospace" font-size="10.00">X-Upload-Key auth</text>
|
||||
<text text-anchor="middle" x="117.5" y="-23.5" font-family="monospace" font-size="10.00">streaming decrypt</text>
|
||||
</g>
|
||||
<!-- dispatch->pull -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>dispatch->pull</title>
|
||||
<path fill="none" stroke="black" d="M376.65,-135.46C322.62,-114.85 239.42,-83.12 182.16,-61.28"/>
|
||||
<polygon fill="black" stroke="black" points="183.08,-57.89 172.49,-57.59 180.59,-64.43 183.08,-57.89"/>
|
||||
<text text-anchor="middle" x="282" y="-127.8" font-family="monospace" font-size="9.00">RPC</text>
|
||||
<text text-anchor="middle" x="282" y="-117.8" font-family="monospace" font-size="9.00">metadata only</text>
|
||||
</g>
|
||||
<!-- pull->recv_upload -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>pull->recv_upload</title>
|
||||
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M138.8,-57.88C161.27,-80.13 199.3,-115.33 237,-139 281.34,-166.83 296.77,-165.64 345,-186 348.91,-187.65 352.92,-189.33 356.97,-191.02"/>
|
||||
<polygon fill="black" stroke="black" points="355.9,-194.37 366.47,-194.97 358.58,-187.9 355.9,-194.37"/>
|
||||
<text text-anchor="middle" x="282" y="-190.8" font-family="monospace" font-size="9.00">HTTPS GET</text>
|
||||
<text text-anchor="middle" x="282" y="-180.8" font-family="monospace" font-size="9.00">streaming decrypt</text>
|
||||
</g>
|
||||
<!-- inject -->
|
||||
<g id="node12" class="node">
|
||||
<title>inject</title>
|
||||
<path fill="#e8f5e9" stroke="black" d="M494,-57.5C494,-57.5 369,-57.5 369,-57.5 363,-57.5 357,-51.5 357,-45.5 357,-45.5 357,-28.5 357,-28.5 357,-22.5 363,-16.5 369,-16.5 369,-16.5 494,-16.5 494,-16.5 500,-16.5 506,-22.5 506,-28.5 506,-28.5 506,-45.5 506,-45.5 506,-51.5 500,-57.5 494,-57.5"/>
|
||||
<text text-anchor="middle" x="431.5" y="-45.5" font-family="monospace" font-size="10.00">lxc exec ... cat ></text>
|
||||
<text text-anchor="middle" x="431.5" y="-34.5" font-family="monospace" font-size="10.00">/root/input/{filename}</text>
|
||||
<text text-anchor="middle" x="431.5" y="-23.5" font-family="monospace" font-size="10.00">64KB streaming pipe</text>
|
||||
</g>
|
||||
<!-- pull->inject -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>pull->inject</title>
|
||||
<path fill="none" stroke="black" d="M201.29,-37C246.27,-37 301.75,-37 346.86,-37"/>
|
||||
<polygon fill="black" stroke="black" points="346.91,-40.5 356.91,-37 346.91,-33.5 346.91,-40.5"/>
|
||||
</g>
|
||||
<!-- compile -->
|
||||
<g id="node13" class="node">
|
||||
<title>compile</title>
|
||||
<path fill="#fff8e1" stroke="black" d="M758,-57C758,-57 633,-57 633,-57 627,-57 621,-51 621,-45 621,-45 621,-33 621,-33 621,-27 627,-21 633,-21 633,-21 758,-21 758,-21 764,-21 770,-27 770,-33 770,-33 770,-45 770,-45 770,-51 764,-57 758,-57"/>
|
||||
<text text-anchor="middle" x="695.5" y="-36.5" font-family="monospace" font-size="10.00">gcc -O2 voxsplit.c -lm</text>
|
||||
</g>
|
||||
<!-- inject->compile -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>inject->compile</title>
|
||||
<path fill="none" stroke="black" d="M506.18,-37.56C538.69,-37.81 577.06,-38.1 610.66,-38.36"/>
|
||||
<polygon fill="black" stroke="black" points="610.89,-41.86 620.92,-38.44 610.95,-34.86 610.89,-41.86"/>
|
||||
</g>
|
||||
<!-- extract -->
|
||||
<g id="node14" class="node">
|
||||
<title>extract</title>
|
||||
<path fill="#fff8e1" stroke="black" d="M993,-70C993,-70 874,-70 874,-70 868,-70 862,-64 862,-58 862,-58 862,-30 862,-30 862,-24 868,-18 874,-18 874,-18 993,-18 993,-18 999,-18 1005,-24 1005,-30 1005,-30 1005,-58 1005,-58 1005,-64 999,-70 993,-70"/>
|
||||
<text text-anchor="middle" x="933.5" y="-58" font-family="monospace" font-size="10.00">ffmpeg -i media</text>
|
||||
<text text-anchor="middle" x="933.5" y="-47" font-family="monospace" font-size="10.00">-vn -acodec pcm_s16le</text>
|
||||
<text text-anchor="middle" x="933.5" y="-36" font-family="monospace" font-size="10.00">-ar 44100 -ac 2</text>
|
||||
<text text-anchor="middle" x="933.5" y="-25" font-family="monospace" font-size="10.00">audio.wav</text>
|
||||
</g>
|
||||
<!-- compile->extract -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>compile->extract</title>
|
||||
<path fill="none" stroke="black" d="M770.2,-40.56C795.99,-41.11 825.08,-41.72 851.44,-42.28"/>
|
||||
<polygon fill="black" stroke="black" points="851.71,-45.79 861.78,-42.5 851.85,-38.79 851.71,-45.79"/>
|
||||
</g>
|
||||
<!-- split -->
|
||||
<g id="node15" class="node">
|
||||
<title>split</title>
|
||||
<path fill="#fff8e1" stroke="black" d="M1259,-64.5C1259,-64.5 1122,-64.5 1122,-64.5 1116,-64.5 1110,-58.5 1110,-52.5 1110,-52.5 1110,-35.5 1110,-35.5 1110,-29.5 1116,-23.5 1122,-23.5 1122,-23.5 1259,-23.5 1259,-23.5 1265,-23.5 1271,-29.5 1271,-35.5 1271,-35.5 1271,-52.5 1271,-52.5 1271,-58.5 1265,-64.5 1259,-64.5"/>
|
||||
<text text-anchor="middle" x="1190.5" y="-52.5" font-family="monospace" font-size="10.00">voxsplit audio.wav</text>
|
||||
<text text-anchor="middle" x="1190.5" y="-41.5" font-family="monospace" font-size="10.00">→ split-instrumental.wav</text>
|
||||
<text text-anchor="middle" x="1190.5" y="-30.5" font-family="monospace" font-size="10.00">→ split-vocal.wav</text>
|
||||
</g>
|
||||
<!-- extract->split -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>extract->split</title>
|
||||
<path fill="none" stroke="black" d="M1005.19,-44C1034.33,-44 1068.43,-44 1099.33,-44"/>
|
||||
<polygon fill="black" stroke="black" points="1099.68,-47.5 1109.68,-44 1099.68,-40.5 1099.68,-47.5"/>
|
||||
</g>
|
||||
<!-- remux -->
|
||||
<g id="node16" class="node">
|
||||
<title>remux</title>
|
||||
<path fill="#fff8e1" stroke="black" d="M1433,-70C1433,-70 1320,-70 1320,-70 1314,-70 1308,-64 1308,-58 1308,-58 1308,-30 1308,-30 1308,-24 1314,-18 1320,-18 1320,-18 1433,-18 1433,-18 1439,-18 1445,-24 1445,-30 1445,-30 1445,-58 1445,-58 1445,-64 1439,-70 1433,-70"/>
|
||||
<text text-anchor="middle" x="1376.5" y="-58" font-family="monospace" font-size="10.00">ffmpeg remux</text>
|
||||
<text text-anchor="middle" x="1376.5" y="-47" font-family="monospace" font-size="10.00">(video: copy video +</text>
|
||||
<text text-anchor="middle" x="1376.5" y="-36" font-family="monospace" font-size="10.00">isolated audio)</text>
|
||||
<text text-anchor="middle" x="1376.5" y="-25" font-family="monospace" font-size="10.00">(audio: copy wav)</text>
|
||||
</g>
|
||||
<!-- split->remux -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>split->remux</title>
|
||||
<path fill="none" stroke="black" d="M1271.13,-44C1279.93,-44 1288.88,-44 1297.65,-44"/>
|
||||
<polygon fill="black" stroke="black" points="1297.85,-47.5 1307.85,-44 1297.85,-40.5 1297.85,-47.5"/>
|
||||
</g>
|
||||
<!-- artifacts -->
|
||||
<g id="node17" class="node">
|
||||
<title>artifacts</title>
|
||||
<path fill="#fff8e1" stroke="black" d="M1601,-67.5C1601,-67.5 1494,-67.5 1494,-67.5 1488,-67.5 1482,-61.5 1482,-55.5 1482,-55.5 1482,-38.5 1482,-38.5 1482,-32.5 1488,-26.5 1494,-26.5 1494,-26.5 1601,-26.5 1601,-26.5 1607,-26.5 1613,-32.5 1613,-38.5 1613,-38.5 1613,-55.5 1613,-55.5 1613,-61.5 1607,-67.5 1601,-67.5"/>
|
||||
<text text-anchor="middle" x="1547.5" y="-55.5" font-family="monospace" font-size="10.00">/tmp/artifacts/</text>
|
||||
<text text-anchor="middle" x="1547.5" y="-44.5" font-family="monospace" font-size="10.00">instrumentals.{ext}</text>
|
||||
<text text-anchor="middle" x="1547.5" y="-33.5" font-family="monospace" font-size="10.00">vocals.{ext}</text>
|
||||
</g>
|
||||
<!-- remux->artifacts -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>remux->artifacts</title>
|
||||
<path fill="none" stroke="black" d="M1445.26,-45.2C1453.91,-45.36 1462.8,-45.51 1471.55,-45.67"/>
|
||||
<polygon fill="black" stroke="black" points="1471.68,-49.17 1481.74,-45.85 1471.8,-42.17 1471.68,-49.17"/>
|
||||
</g>
|
||||
<!-- artifacts->stream_resp -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>artifacts->stream_resp</title>
|
||||
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M1510.87,-67.65C1477.53,-85 1425.76,-107 1377.5,-107 430.5,-107 430.5,-107 430.5,-107 343.6,-107 310.34,-88.39 237,-135 180.85,-170.69 145.52,-243.66 129.19,-284.89"/>
|
||||
<polygon fill="black" stroke="black" points="125.85,-283.81 125.54,-294.4 132.39,-286.32 125.85,-283.81"/>
|
||||
<text text-anchor="middle" x="933.5" y="-119.8" font-family="monospace" font-size="9.00">response JSON</text>
|
||||
<text text-anchor="middle" x="933.5" y="-109.8" font-family="monospace" font-size="9.00">base64 artifacts</text>
|
||||
</g>
|
||||
<!-- browser -->
|
||||
<g id="node19" class="node">
|
||||
<title>browser</title>
|
||||
<ellipse fill="#e0f2f1" stroke="black" cx="1190.5" cy="-469" rx="54.39" ry="21.43"/>
|
||||
<text text-anchor="middle" x="1190.5" y="-472" font-family="monospace" font-size="10.00">Browser</text>
|
||||
<text text-anchor="middle" x="1190.5" y="-461" font-family="monospace" font-size="10.00">(watch.js)</text>
|
||||
</g>
|
||||
<!-- s3->browser -->
|
||||
<g id="edge21" class="edge">
|
||||
<title>s3->browser</title>
|
||||
<path fill="none" stroke="black" d="M999.18,-469C1037.91,-469 1086.97,-469 1125.57,-469"/>
|
||||
<polygon fill="black" stroke="black" points="1125.66,-472.5 1135.66,-469 1125.66,-465.5 1125.66,-472.5"/>
|
||||
<text text-anchor="middle" x="1057.5" y="-481.8" font-family="monospace" font-size="9.00">presigned URL</text>
|
||||
<text text-anchor="middle" x="1057.5" y="-471.8" font-family="monospace" font-size="9.00">15 min TTL</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 23 KiB |
286
docs/karaoke-pipeline.md
Normal file
286
docs/karaoke-pipeline.md
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
# Karaoke Pipeline — Vocal Isolation via Unsandbox
|
||||
|
||||
## Overview
|
||||
|
||||
MPS separates instrumentals and vocals from audio/video uploads using spectral
|
||||
mid-side Wiener masking (`voxsplit.c`, zero ML dependencies). Processing runs
|
||||
inside unsandbox zerotrust containers. The pipeline is disk-backed with ~64KB
|
||||
memory per worker at every stage.
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```dot
|
||||
// Render: dot -Tsvg docs/karaoke-pipeline.dot -o docs/karaoke-pipeline.dot.svg
|
||||
digraph karaoke_pipeline {
|
||||
rankdir=LR;
|
||||
node [shape=box, style="rounded,filled", fontname="monospace", fontsize=10];
|
||||
edge [fontname="monospace", fontsize=9];
|
||||
|
||||
subgraph cluster_mps {
|
||||
label="MPS (uWSGI)";
|
||||
style=dashed;
|
||||
color="#5871ad";
|
||||
|
||||
trigger [label="Trigger\n(upload / on-demand / backfill)", fillcolor="#e8eaf6"];
|
||||
s3_down [label="S3 Download\n→ /tmp/karaoke_*/media.bin\n64KB chunks", fillcolor="#e8eaf6"];
|
||||
upload_media [label="POST /upload\n(streaming file body)\n→ upload_id", fillcolor="#e8eaf6"];
|
||||
upload_vox [label="POST /upload\nvoxsplit.c\n→ upload_id", fillcolor="#e8eaf6"];
|
||||
execute [label="POST /execute\n{upload_ids, script}\ntiny JSON, no file bytes", fillcolor="#e8eaf6"];
|
||||
stream_resp [label="Stream response\n→ /tmp/karaoke_*/response.json\n64KB chunks", fillcolor="#e8eaf6"];
|
||||
decode [label="Decode artifacts\n(base64 → tmpfile)\nupload to S3", fillcolor="#e8eaf6"];
|
||||
db_update [label="Update product\nextensions + file_bytes\nset S3 ACLs", fillcolor="#e8eaf6"];
|
||||
}
|
||||
|
||||
subgraph cluster_api {
|
||||
label="api.unsandbox.com";
|
||||
style=dashed;
|
||||
color="#ad5871";
|
||||
|
||||
recv_upload [label="Receive upload\nAES-256-CTR encrypt\n→ /tmp/uploads/{id}.enc\nkey in ETS only", fillcolor="#fce4ec"];
|
||||
dispatch [label="Dispatch execute\nErlang RPC\n~200B metadata only", fillcolor="#fce4ec"];
|
||||
}
|
||||
|
||||
subgraph cluster_pool {
|
||||
label="Pool Node";
|
||||
style=dashed;
|
||||
color="#58ad71";
|
||||
|
||||
pull [label="GET /internal/upload/{id}\nX-Upload-Key auth\nstreaming decrypt", fillcolor="#e8f5e9"];
|
||||
inject [label="lxc exec ... cat >\n/root/input/{filename}\n64KB streaming pipe", fillcolor="#e8f5e9"];
|
||||
}
|
||||
|
||||
subgraph cluster_container {
|
||||
label="Zerotrust Container";
|
||||
style=dashed;
|
||||
color="#ad8f58";
|
||||
|
||||
compile [label="gcc -O2 voxsplit.c -lm", fillcolor="#fff8e1"];
|
||||
extract [label="ffmpeg -i media\n-vn -acodec pcm_s16le\n-ar 44100 -ac 2\naudio.wav", fillcolor="#fff8e1"];
|
||||
split [label="voxsplit audio.wav\n→ split-instrumental.wav\n→ split-vocal.wav", fillcolor="#fff8e1"];
|
||||
remux [label="ffmpeg remux\n(video: copy video +\nisolated audio)\n(audio: copy wav)", fillcolor="#fff8e1"];
|
||||
artifacts [label="/tmp/artifacts/\ninstrumentals.{ext}\nvocals.{ext}", fillcolor="#fff8e1"];
|
||||
}
|
||||
|
||||
s3 [label="S3 / CDN\n(DO Spaces or BYOB)", shape=cylinder, fillcolor="#f3e5f5"];
|
||||
browser [label="Browser\n(watch.js)", shape=ellipse, fillcolor="#e0f2f1"];
|
||||
|
||||
trigger -> s3_down;
|
||||
s3_down -> upload_media [label="file on disk"];
|
||||
s3_down -> upload_vox;
|
||||
upload_media -> recv_upload [label="streaming\noctet-stream"];
|
||||
upload_vox -> recv_upload;
|
||||
recv_upload -> execute [label="upload_id", style=dashed, dir=back];
|
||||
execute -> dispatch [label="JSON\n{upload_ids}"];
|
||||
dispatch -> pull [label="RPC\nmetadata only"];
|
||||
pull -> recv_upload [label="HTTPS GET\nstreaming decrypt", style=dotted];
|
||||
pull -> inject;
|
||||
inject -> compile;
|
||||
compile -> extract;
|
||||
extract -> split;
|
||||
split -> remux;
|
||||
remux -> artifacts;
|
||||
artifacts -> stream_resp [label="response JSON\nbase64 artifacts", style=dotted];
|
||||
stream_resp -> decode;
|
||||
decode -> s3 [label="PUT instrumentals\nPUT vocals"];
|
||||
db_update -> s3 [label="ACL update", style=dashed];
|
||||
decode -> db_update;
|
||||
s3 -> browser [label="presigned URL\n15 min TTL"];
|
||||
}
|
||||
```
|
||||
|
||||
## Pipeline Stages
|
||||
|
||||
### Stage 1: Trigger
|
||||
|
||||
Three entry points, all converge on `process_karaoke()`:
|
||||
|
||||
| Entry Point | File | When |
|
||||
|-------------|------|------|
|
||||
| Upload | `views/product.py:382` | User uploads audio/video to product |
|
||||
| On-demand | `views/watch.py:karaoke_process` | User clicks 🎤 button in watch mode |
|
||||
| Backfill | `lib/karaoke.py:backfill_karaoke_async` | Shop owner triggers from settings |
|
||||
|
||||
On-demand and backfill fork a detached grandchild process (double-fork +
|
||||
`os.setsid()`) that survives uWSGI worker recycling. Per-product lockfile
|
||||
(`/tmp/karaoke_{product_id}.lock`) prevents duplicate processing.
|
||||
|
||||
### Stage 2: Download from S3
|
||||
|
||||
```python
|
||||
# karaoke.py:217-226
|
||||
resp = s3_client.get_object(Bucket=bucket, Key=s3_key)
|
||||
with open(media_path, "wb") as f:
|
||||
while True:
|
||||
chunk = body.read(65536) # 64KB
|
||||
if not chunk: break
|
||||
f.write(chunk)
|
||||
```
|
||||
|
||||
### Stage 3: Upload to Unsandbox API
|
||||
|
||||
```python
|
||||
# karaoke.py:45-77
|
||||
# HMAC signs empty body — API skips body parsing for /upload
|
||||
headers = {"X-Filename": filename, "Content-Type": "application/octet-stream"}
|
||||
with open(file_path, "rb") as body:
|
||||
response = requests.post(url, data=body, headers=headers)
|
||||
# Returns: {"upload_id": "uuid"}
|
||||
```
|
||||
|
||||
Two uploads: the media file + `voxsplit.c` source. Both stream from disk,
|
||||
constant memory.
|
||||
|
||||
### Stage 4: API Receives + Encrypts
|
||||
|
||||
`upload_store.ex` writes incoming bytes to `/tmp/uploads/{id}.enc` with
|
||||
AES-256-CTR encryption. Unique key+IV per upload. Keys live only in ETS
|
||||
(BEAM memory, never written to disk). Files auto-expire after 15 minutes.
|
||||
|
||||
### Stage 5: Execute (Metadata Only)
|
||||
|
||||
```python
|
||||
# karaoke.py:80-125
|
||||
payload = {
|
||||
"language": "bash",
|
||||
"code": script,
|
||||
"network_mode": "zerotrust",
|
||||
"input_files": [
|
||||
{"upload_id": media_id, "filename": "media"},
|
||||
{"upload_id": voxsplit_id, "filename": "voxsplit.c"},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Tiny JSON body. Zero file content crosses Erlang distribution.
|
||||
|
||||
### Stage 6: Pool Pulls Files
|
||||
|
||||
Pool node calls `GET /internal/upload/{id}` back to the API. API decrypts
|
||||
on-the-fly, streams plaintext. Pool pipes into container:
|
||||
|
||||
```
|
||||
lxc exec {container} -- sh -c 'cat > /root/input/media'
|
||||
```
|
||||
|
||||
`/tmp/input` symlinks to `/root/input/` (canonical location).
|
||||
|
||||
### Stage 7: Container Processing
|
||||
|
||||
```bash
|
||||
# Compile voxsplit (spectral mid-side Wiener masking, libc + libm only)
|
||||
gcc -O2 -o /tmp/voxsplit /tmp/input/voxsplit.c -lm
|
||||
|
||||
# Extract audio from media
|
||||
ffmpeg -y -i /tmp/input/media -vn -acodec pcm_s16le -ar 44100 -ac 2 /tmp/audio.wav
|
||||
|
||||
# Separate instrumentals and vocals
|
||||
/tmp/voxsplit /tmp/audio.wav -o /tmp/split
|
||||
|
||||
# For video: remux original video with each isolated audio
|
||||
ffmpeg -y -i /tmp/input/media -i /tmp/split-instrumental.wav \
|
||||
-c:v copy -map 0:v -map 1:a -shortest /tmp/artifacts/instrumentals.mp4
|
||||
ffmpeg -y -i /tmp/input/media -i /tmp/split-vocal.wav \
|
||||
-c:v copy -map 0:v -map 1:a -shortest /tmp/artifacts/vocals.mp4
|
||||
```
|
||||
|
||||
### Stage 8: Response + S3 Upload
|
||||
|
||||
Response JSON streams back to MPS temp file. Artifacts are base64-encoded in
|
||||
the response. MPS decodes each one to a temp file, uploads to S3, deletes
|
||||
temp file. Updates product metadata and S3 ACLs.
|
||||
|
||||
## On-Demand Flow (Watch Mode)
|
||||
|
||||
```dot
|
||||
// Render: dot -Tsvg docs/karaoke-ondemand.dot -o docs/karaoke-ondemand.dot.svg
|
||||
digraph karaoke_ondemand {
|
||||
rankdir=TB;
|
||||
node [shape=box, style="rounded,filled", fontname="monospace", fontsize=10];
|
||||
edge [fontname="monospace", fontsize=9];
|
||||
|
||||
click [label="User clicks 🎤\n(watch.js)", fillcolor="#e0f2f1"];
|
||||
check [label="Has tracks?", shape=diamond, fillcolor="#fff8e1"];
|
||||
cycle [label="cycleKaraoke()\nOriginal → Instrumentals → Vocals", fillcolor="#e8f5e9"];
|
||||
post [label="POST /karaoke/{id}\n(fetch, non-blocking)", fillcolor="#e8eaf6"];
|
||||
fork [label="Server forks\ndetached grandchild", fillcolor="#e8eaf6"];
|
||||
process [label="process_karaoke()\n(~30-120s)", fillcolor="#fce4ec"];
|
||||
hourglass [label="Button shows ⌛\nkaraokeProcessing=true", fillcolor="#e0f2f1"];
|
||||
refresh [label="10s URL refresh\nfetchWatchData()", fillcolor="#e0f2f1"];
|
||||
detect [label="instrumentals_url\nappears in JSON", shape=diamond, fillcolor="#fff8e1"];
|
||||
autoswitch [label="Auto-switch to\ninstrumentals 🎤", fillcolor="#e8f5e9"];
|
||||
|
||||
click -> check;
|
||||
check -> cycle [label="yes"];
|
||||
check -> post [label="no (eligible)"];
|
||||
post -> fork [label="200 {status: processing}"];
|
||||
post -> hourglass;
|
||||
fork -> process;
|
||||
hourglass -> refresh [label="every 10s"];
|
||||
refresh -> detect;
|
||||
detect -> refresh [label="not yet"];
|
||||
detect -> autoswitch [label="tracks ready"];
|
||||
process -> detect [label="DB updated", style=dashed];
|
||||
}
|
||||
```
|
||||
|
||||
## Security Properties
|
||||
|
||||
- **Encryption at rest**: AES-256-CTR, unique key+IV per upload, key in ETS only
|
||||
- **Single-use**: Upload deleted immediately after pool pulls it
|
||||
- **TTL**: 15-minute expiry, swept every 60 seconds
|
||||
- **Key loss on restart**: BEAM restart = all encryption keys gone = files undecryptable
|
||||
- **Auth**: `X-Upload-Key` header + 128-bit random UUID per upload
|
||||
- **Zerotrust network**: Container has no outbound network access
|
||||
|
||||
## Limits
|
||||
|
||||
| Limit | Value | Source |
|
||||
|-------|-------|--------|
|
||||
| Max file size | 3.698 GB (3,698,742,051 bytes) | `upload_store.ex @max_upload_bytes` |
|
||||
| Memory per worker | ~64 KB | Disk-backed streaming at every stage |
|
||||
| Concurrency | Per-key limit from `validate_keys()` | Unsandbox account tier |
|
||||
| Retries | 3 attempts, exponential backoff (5s, 10s, 20s) | `karaoke.py` backfill |
|
||||
| URL TTL | 15 minutes | Presigned URL expiry |
|
||||
| Upload TTL | 15 minutes | API auto-cleanup |
|
||||
| Container TTL | 300 seconds | `/execute` payload `ttl` field |
|
||||
|
||||
## S3 Key Structure
|
||||
|
||||
```
|
||||
{shop_id}/products/{product_id}/
|
||||
product # original media file
|
||||
preview # preview (for sellable products)
|
||||
thumbnail1-4 # thumbnails
|
||||
instrumentals # karaoke: isolated instrumentals
|
||||
vocals # karaoke: isolated vocals
|
||||
```
|
||||
|
||||
## Product Metadata
|
||||
|
||||
```python
|
||||
product.extensions = {
|
||||
"product": "mp4", # original
|
||||
"instrumentals": "mp4", # same ext for video, "wav" for audio
|
||||
"vocals": "mp4",
|
||||
}
|
||||
product.file_bytes = {
|
||||
"product": 15000000,
|
||||
"instrumentals": 12000000,
|
||||
"vocals": 8000000,
|
||||
}
|
||||
```
|
||||
|
||||
## Related Files
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `lib/karaoke.py` | Core pipeline: download, upload, execute, process response |
|
||||
| `lib/voxsplit.c` | Spectral mid-side vocal isolation (C, libc+libm only) |
|
||||
| `lib/un.py` | Unsandbox API client, HMAC signing, key validation |
|
||||
| `views/watch.py` | On-demand `POST /karaoke/{id}` endpoint + watch JSON |
|
||||
| `views/product.py` | Upload-triggered karaoke processing |
|
||||
| `views/content.py` | Content page karaoke URL generation |
|
||||
| `static/js/watch.js` | Client: button states, on-demand trigger, auto-switch |
|
||||
| `templates/snippets/related_content.j2` | Karaoke button HTML |
|
||||
| `templates/content.j2` | `data-karaoke-eligible` attribute |
|
||||
| `scripts/backfill_karaoke.py` | Standalone backfill script |
|
||||
Loading…
Add table
Add a link
Reference in a new issue