Commit Graph
1556 Commits
Author SHA1 Message Date
Codex 5818e19137 fix(worker): secure inbound node connections 2026-08-11 23:51:29 +00:00
Codex 80eca8cc6f fix(workers): harden inbound panel boundaries 2026-08-11 23:50:30 +00:00
debpalash 037a5689de fix(worker): address legacy transport review findings 2026-08-11 23:49:46 +00:00
debpalash 5b806124c2 Merge remote-tracking branch 'origin/main' into fix/pr1495-final
# Conflicts:
#	CHANGELOG.md
2026-08-11 22:29:56 +00:00
Palash Debnath 50dd851bf8 Merge pull request #1502 from debpalash/feat/ui-scale-onboarding
feat: add first-run interface scaling
2026-08-11 22:13:26 +00:00
velixio 2165339a36 docs(changelog): record the cuBLAS workspace fix and the reserved-VRAM readout 2026-08-12 03:32:34 +05:30
velixio b0a6fdfdc2 Merge remote-tracking branch 'upstream/main'
# Conflicts:
#	CHANGELOG.md
#	frontend/src/components/settings/ModelStoreTab.jsx
#	frontend/src/i18n/locales/ar.json
#	frontend/src/i18n/locales/de.json
#	frontend/src/i18n/locales/en.json
#	frontend/src/i18n/locales/es.json
#	frontend/src/i18n/locales/fr.json
#	frontend/src/i18n/locales/hi.json
#	frontend/src/i18n/locales/id.json
#	frontend/src/i18n/locales/it.json
#	frontend/src/i18n/locales/ja.json
#	frontend/src/i18n/locales/ko.json
#	frontend/src/i18n/locales/nl.json
#	frontend/src/i18n/locales/pl.json
#	frontend/src/i18n/locales/pt.json
#	frontend/src/i18n/locales/ru.json
#	frontend/src/i18n/locales/sv.json
#	frontend/src/i18n/locales/th.json
#	frontend/src/i18n/locales/tr.json
#	frontend/src/i18n/locales/uk.json
#	frontend/src/i18n/locales/vi.json
#	frontend/src/i18n/locales/zh-CN.json
#	frontend/src/i18n/locales/zh-TW.json
2026-08-12 03:27:08 +05:30
debpalash ffc6fade50 fix(ui): preview first-run scale choices 2026-08-11 21:45:52 +00:00
velixio 5bb50f7832 fix(memory): clear cuBLAS workspaces so an unloaded model's segment can go
After the ordering fix, unloading the model on a 4090 still left the GPU at
1238 MiB with torch reporting 8.5 MB allocated and 803 MB reserved -- and no
number of Flush Memory presses moved it. A segment dump said why: ONE 803 MB
segment, 794.7 MB of it inactive-but-split, pinned by a single live block of
8,519,680 bytes.

That is cuBLAS's default workspace. It is taken from the caching allocator on
first use, so it lands inside whatever segment the model load had just grown,
and it is held for the life of the cuBLAS handle. empty_cache() can only
return segments that are entirely free, so one 8.5 MB block kept three
quarters of a gigabyte from ever reaching the driver again. On a machine
lending its GPU that is the difference between an idle node costing 470 MiB
and costing 1.2 GB.

free_vram() now clears the workspaces before emptying the cache, on the
unload paths only -- the next cuBLAS call re-takes one, which is cheap but
not something to pay per generate. The binding is private
(torch._C._cuda_clearCublasWorkspaces), so it is optional by construction: a
build without it keeps today's behaviour rather than failing an unload.

Found by adding reserved-vs-allocated to /system/flush-memory in 642513d2.
Allocated alone reads near zero after an unload, which is exactly why this
hid for so long -- every diagnostic we had agreed the memory was free.
2026-08-12 03:08:46 +05:30
velixio 642513d205 fix(system): report reserved VRAM alongside allocated in flush-memory
memory_allocated counts live tensors only, so after an unload it reads
near zero while nvidia-smi still shows gigabytes. That gap is the whole
substance of every "flush says it worked, the GPU says it didn't" report,
and the endpoint was reporting only the half that looks good.

memory_reserved is what the caching allocator holds from the driver; the
remainder between that and the driver's own figure is the CUDA context and
kernel workspaces, which nothing in-process can hand back.
2026-08-12 02:58:42 +05:30
velixio 090cc37144 fix(memory): release the model before emptying the cache, not after
The shared voice model's unload emptied the allocator caches and *then*
dropped the reference. That frees nothing: the weights are still reachable
when gc.collect() runs, empty_cache() only returns blocks the allocator
already considered free, and the reference drops a moment later into a cache
nothing will flush again. The unload logs success, the engine leaves the
registry, and nvidia-smi does not move.

Six modules open-coded the same two lines. Exactly one had them inverted --
OmniVoiceBackend.unload, which is the path the engine-registry idle sweep
reaches, which is the sweep a headless worker node runs. So every unload a
user could trigger from the UI worked, and the one that runs unattended on a
machine lending its GPU held 3.6 GB indefinitely. Found on hardware: the
sweep fired on schedule, logged "Released 1 idle engine(s)", and VRAM stayed
flat at 3656 MiB for the next two minutes.

Replace all six with model_manager.unload_shared_model(), which clears the
reference, drops the clone-prompt side cache, then frees -- in that order,
in one place. Two callers gain the side-cache drop they were missing
(/system/flush-memory and the shutdown path), which is the same defect one
step down: an unload that kept the encoded reference tensors belonging to the
model it had just released.

A source guard asserts nothing outside model_manager assigns the shared
reference, so the next caller cannot reintroduce the ordering. It caught the
sixth site while being written.

Also give the AudioSeal watermark models the bargain every other model in the
app already makes: they loaded on the first embed and stayed resident for the
life of the process. CPU-resident, so this is system RAM rather than VRAM,
and the machines that notice are the ones running batches.

The error text on a failing unload changes with the ordering. "Could not be
unloaded, retry after the current generation finishes" was accurate when the
cache flush ran first and aborted before the release; now the release has
already happened and only the flush can fail, so it says that instead of
sending the user to repeat work that is done.
2026-08-12 02:33:04 +05:30
debpalash 0f69b4d3ca docs: record interface scale setup 2026-08-11 21:02:08 +00:00
debpalash 232784cb5a Merge remote-tracking branch 'origin/main' into feat/ui-scale-onboarding 2026-08-11 21:01:41 +00:00
Palash Debnath 32bd5cfedb Merge pull request #1491 from debpalash/feat/workspace-design-refresh
feat(ui): refresh core workspaces and settings
2026-08-11 20:39:56 +00:00
debpalash 6ba2e2a914 feat(ui): add first-run interface scaling 2026-08-11 20:35:11 +00:00
debpalash d5a496a8ad fix(ui): complete workspace review follow-ups 2026-08-11 20:24:49 +00:00
velixio 6a6f3fbc29 fix(models): do not preload a model on a machine with no local user
The startup preload exists so the first generate feels instant for the person
sitting in front of the app. A machine lending its GPU has nobody sitting
there, so it was several GB of VRAM held from boot against a request that may
never arrive — and the idle sweep could not reclaim it, because the sweep owns
the worker executor's engines while this is the default local model.

Measured on gpu2: a node that had run nothing still sat at 2.4 GB, and an idle
unload after a real job returned it to exactly that floor rather than below it.

Worker-mode processes now load on first request and release when idle, which is
what a node should do. A machine that is both a desktop app and a worker keeps
the warm-up — there is a real user there and the point stands.
2026-08-12 01:40:22 +05:30
debpalash 8a8059b9c6 fix(ui): address workspace review findings 2026-08-11 20:03:15 +00:00
velixio 5ebf21166d Merge branch 'main' of github.com:velixio/VoiceStudio 2026-08-12 01:31:18 +05:30
velixio b8fb5a14c2 feat(workers): let the idle-unload timings be shortened for testing
Watching a ten-minute rule take effect means waiting ten minutes, so it tends
not to get watched. Both numbers are now env-tunable:
OMNIVOICE_ENGINE_IDLE_UNLOAD_SECONDS and OMNIVOICE_IDLE_SWEEP_SECONDS.

They are documented as a pair, because shortening only the threshold still
means waiting a full sweep interval to see it fire — which reads as a broken
sweep and sends you looking for a bug that is not there.

Unparseable values and anything below the floor are ignored with a warning
rather than honoured. A zero threshold would hand back a model the instant it
went idle and reload it for the very next request, which is worse than the
behaviour being tuned.
2026-08-12 01:30:53 +05:30
velixio 17a364c476 Merge branch 'debpalash:main' into main 2026-08-12 01:25:10 +05:30
debpalash ce5f051252 fix(ui): remove framed structural borders 2026-08-11 19:49:10 +00:00
velixio 1ac3dcf3fe fix(workers): unload idle models on an inbound-only node
The ten-minute idle sweep lived inside the dial-out agent. A node that only
accepts inbound connections never starts that agent — on gpu2 it fails outright
with 'Set OMNIVOICE_WORKER_ENDPOINT' — so a machine lending its GPU to panels
that dial IN held several GB of weights forever. That is precisely the cost the
sweep exists to avoid, and it was silently missing in the mode most likely to
be a shared box.

The loop moves to module scope and both transports use it. Inbound starts it
when the listener starts and cancels it when the listener stops, and passes a
callback that re-advertises capabilities to every attached panel, so a control
plane's view of what is resident does not go stale the moment it becomes
useful. Local behaviour is unchanged: nothing sweeps unless a worker role runs.
2026-08-12 01:14:16 +05:30
debpalash 6535ed3284 Merge commit '99e865600b2ce730ee7ac9860f97deea75ff57a1' into feat/workspace-design-refresh-consolidated 2026-08-11 19:38:59 +00:00
debpalash 5ef5d9b73a style: format consolidated workspace changes 2026-08-11 19:35:41 +00:00
debpalash 735cdd6b6f docs: record workspace design refresh 2026-08-11 19:34:17 +00:00
debpalash 6c679bfd75 feat(launchpad): simplify creative entry screen 2026-08-11 19:34:08 +00:00
debpalash 16a1ee63ee feat(profile): simplify the voice inspector 2026-08-11 19:30:52 +00:00
debpalash 2b0cd04599 docs: align workspace design specifications 2026-08-11 19:28:52 +00:00
debpalash f0382e0290 feat(workspaces): refine voice story and audiobook flows 2026-08-11 19:28:43 +00:00
Palash Debnath 99e865600b Merge pull request #1501 from debpalash/fix/remote-backend-recovery-1496
fix: recover from unreachable remote backends
2026-08-11 19:22:57 +00:00
debpalash 680fa8fdef feat(settings): refresh responsive preferences workspace 2026-08-11 19:16:21 +00:00
velixio 40569d0657 Merge remote-tracking branch 'upstream/main'
# Conflicts:
#	CHANGELOG.md
2026-08-12 00:34:31 +05:30
debpalash cf316b18bc test(remote): exercise streamed health responses 2026-08-11 19:04:05 +00:00
debpalash 5832a81bb6 feat(onboarding): refresh the bundled demo voice 2026-08-11 19:02:38 +00:00
velixio 9ef0f4a61b Merge pull request #2 from velixio/feat/inbound-node-mode
Share one GPU machine between several people
2026-08-12 00:31:34 +05:30
debpalash 8e5a023058 fix(remote): bound startup health responses 2026-08-11 18:52:17 +00:00
velixio b718b2be46 fix(workers): accept the nested input ids that staging actually produces
Found by clicking Synthesize in the desktop UI — the one path nothing had
exercised.

task_store.stage_input mints inputs/<digest><ext>, a path rather than a bare
name. The node ran safe_filename over it, which rejects anything nested, so
every real clone input was refused, the dispatch failed, and the scheduler
retried about eighteen times a second while the 4090 sat idle and the user
watched a spinner.

The wire id is now hashed into a directory name rather than used as one. That
accepts any id the protocol allows while leaving placement entirely ours to
decide, which is the property the check was really buying. The declared
filename is still required to be a bare name, and a hostile one is still
refused outright — covered by its own test so the containment cannot be traded
away later to fix some future rejection.

Every earlier test used a flat id like 'ref-1' and so never met the shape
production emits.
2026-08-12 00:14:08 +05:30
debpalash 8d8765315f docs: link remote recovery changelog 2026-08-11 18:04:36 +00:00
debpalash dc5c9cf43e fix(remote): recover from unreachable backends
Closes #1496
2026-08-11 18:03:51 +00:00
velixio fd7f06d62e fix(workers): give each attach a fresh outbox
Found on hardware. The queue was built once per connection and reused across
reconnects, so a frame a dying session left behind became the FIRST frame of
the next attach. The node requires a registration there, aborted the call, and
the two span at full speed — session epoch 2445 inside one second, the node
logging 'Locally aborted' on repeat, and the panel reporting the machine
offline while the connection list showed it connected.
2026-08-11 23:28:27 +05:30
velixio 33714b2fe0 fix(workers): make Disconnect hold, and stop a bad paste from replacing a good key
Two more found on hardware.

Disconnect ended the session and the panel redialled two seconds later, so the
log read disconnected and connected in the same breath and the button appeared
to do nothing. A kicked key now sits out for a minute — long enough that the
disconnect is real and the person notices, short enough that it is plainly not
a revocation, which stays a separate and permanent action. The docs now say
which of the two buttons does which.

Re-pasting a connection string for an already-connected machine saved the new
string and then short-circuited on the existing session, so a wrong key
reported success, kept running on the old connection, and only failed after a
restart — by which point nothing pointed back at the paste that caused it. The
live session is now torn down before the new one is dialled.
2026-08-11 23:22:47 +05:30
Palash Debnath 008c8a70a6 Merge pull request #1500 from debpalash/fix/wavesurfer-abort-report-1498
fix(ui): ignore expected aborted audio streams
2026-08-11 17:47:10 +00:00
velixio 4f2dea97b8 fix(workers): read a result ref's size as a size, not an offset
Found on hardware. FetchResult seeked to request.size_bytes as though it were
a resume point, but that field is the artifact's total size — so every fetch
started at end-of-file, yielded no chunks, and failed with 'the result ended
before its final chunk' while the finished render sat on the node's disk.

ArtifactRef carries no resume field, so resumption is a protocol addition
rather than a reinterpreted one, and the fetch now always starts at zero.

Every earlier test drove publish and stage directly and never called
FetchResult with a populated ref, which is exactly why this survived them.
2026-08-11 23:13:29 +05:30
velixio 3dbae35feb fix(workers): actually move artifacts on an inbound session
Found on hardware. The job ran on the GPU machine and the audio never arrived:
'gpu2 finished the job but its audio did not arrive.'

Both artifact directions were built and neither was wired. A result reported by
a dialled node is only staged on that node's disk — nothing pushes it, because
the node cannot call us — so the commit recorded an artifact path that had
never been written. Inputs had the mirror problem: nothing sent them, so a
clone would have failed on a reference file that was never delivered.

Results are now pulled when the frame naming them arrives, and inputs are
pushed before the assignment rather than alongside it, because the executor
asks for them as soon as it starts and an assignment that overtakes its own
reference audio fails on a file that is merely late.

A fetch that fails is not a silent loss: no artifact is recorded, the task
fails naming the machine, and the node keeps its copy because nothing
acknowledges a result we could not fetch.
2026-08-11 23:11:08 +05:30
velixio 569517e5d8 fix(workers): send heartbeats on an inbound session
Found on hardware. The Attach handler started the read pump and the outbound
loop but never the heartbeat loop that the outbound path starts inside
_connect_once. So a node registered, went silent, was declared dead about
ninety seconds later, reconnected, and flapped forever — and in between, work
aimed at it fell back to the local machine with 'gpu2 is offline', while the
panel had shown it ready at 3.4 ms moments earlier.

Every end-to-end test in this file finished inside three seconds, comfortably
within the grace window that hid it. The regression test therefore asserts on
the emitted heartbeat frames themselves rather than on liveness, and shortens
the advertised interval so it does that in two seconds instead of twenty.
2026-08-11 23:05:20 +05:30
debpalash eaad1017df Merge commit '9930a0b41ab4a97560ddc30afdadc9728293d796' into fix/wavesurfer-abort-report-1498 2026-08-11 17:28:14 +00:00
velixio e121e69d0f fix(workers): put a dialable address in the connection string
Found on hardware. With the listener bound to 0.0.0.0 — which is what sharing
a GPU across a network requires — the issued string came out as
ovnode://...@0.0.0.0:7444. That is a legal bind and a meaningless destination,
so it would have failed on the far end with a connection error naming nothing,
and the person who pasted it had no way to tell a bad string from a firewall.

The string is now built from an advertised address rather than the bind: for a
wildcard bind, the source address the routing table would use to leave this
machine, found with a connected UDP socket that sends no packets and needs no
DNS. An explicitly typed bind is advertised verbatim, because someone who
entered a specific address meant it.
2026-08-11 22:57:44 +05:30
velixio 0988a48caa feat(workers): Settings UI for sharing a GPU, in all 21 languages
Adds the panel that makes inbound mode usable: a toggle to accept connections,
a bind field that says which side of "only this machine" you are on, per-person
connection strings with a copy button, the live list of who is connected with a
disconnect button, and a paste box for joining someone else's GPU.

Placed behind the existing Remote workers toggle rather than beside it. "Off
means off" is this feature's stated contract, and a second switch that stayed
live underneath would be exactly the surprise that promise exists to prevent.
Headless machines that only lend a GPU set OMNIVOICE_INBOUND_NODE and never see
this panel.

The unencrypted warning appears where it becomes true, not buried in a doc:
next to the bind field once it points beyond this machine, naming the address,
and again under every freshly issued connection string. The remove-access
confirm says the others stay connected, since that is the only place a user
learns keys are per person rather than one switch for everybody.

All 36 strings are translated into all 20 non-English locales in this change,
with the {{address}}, {{label}}, {{count}} and {{when}} placeholders verified
programmatically against en.json before writing — a dropped token is the exact
bug the parity test was built for, and en-only keys would have passed CI
silently while every other language read English.

Two existing WorkersPanel rename tests queried the only textbox on the page.
That was incidental, not intentional; they now name the field they mean.
2026-08-11 22:46:06 +05:30
Palash Debnath 9930a0b41a Merge pull request #1499 from Marc-oss-hub/feat/orcarouter-provider
feat(llm-providers): add OrcaRouter as a named LLM provider
2026-08-11 17:11:12 +00:00