1. 09 Sep, 2026 2 commits
    • Stefy Lanza (nextime / spora )'s avatar
      video/audio: fix camera_motion 500, dialog voice cloning, and the BigVGAN hub break · cc18ca3e
      Stefy Lanza (nextime / spora ) authored
      Three defects found while auditing the 'person from a video -> scene with their
      voice' path.
      
      1. camera_motion returned 500 on every request that used it.
         _apply_camera_motion() set kw['camera_motion'] unconditionally and nothing
         ever popped it, so pipe(**kw) raised TypeError — no diffusers video pipeline
         accepts that kwarg (the 'CogVideoX supports it natively' comment was wrong).
         tools/videogen.py sends the field, so that path was dead. Now the kwarg is
         passed only when the pipeline signature really takes it, otherwise the motion
         is folded into the prompt, which is the only conditioning these models have.
      
      2. dialogs[].voice never cloned the voice it named.
         _generate_tts_for_line() loaded the profile's reference audio and then used it
         purely as a flag to force a generic voice (en-US-JennyNeural / af_sky) —
         F5-TTS was never invoked, so the selected identity was silently discarded.
         It now clones with F5-TTS when the profile has reference audio AND a
         transcript, and says why in the log when it falls back.
      
      3. seed-vc and F5-TTS could not load their BigVGAN vocoder.
         huggingface_hub 1.x stopped forwarding proxies/resume_download to a subclass
         _from_pretrained; BigVGAN still declares them as required keyword-only args,
         so loading died with 'missing 2 required keyword-only arguments'. Pinning hub
         below 1.0 is not possible (transformers 5.x needs the new one), so
         codai/api/hub_compat.py supplies the dropped arguments for exactly the
         subclasses whose signature still requires them. This also un-breaks
         /v1/audio/convert.
      
      Also fixed on the way:
      - edge-tts never ran: get_event_loop().run_until_complete() inside an executor
        thread raises RuntimeError. In _generate_tts_for_line it was swallowed; in
        _generate_tts only ImportError was caught, so it propagated out of
        /v1/video/dub. Both now use asyncio.run() and verify the output file.
      - music dub adopted a failed Seed-VC result: converted_path was assigned before
        the write succeeded, so a near-empty conversion got remixed instead of falling
        back to the F5-TTS vocal. The result is now validated before adoption.
      
      Verified offline (live container untouched): camera_motion passes through only
      to a supporting pipeline and otherwise reaches the prompt; a dialog line clones
      with the profile's reference audio and transcript and refuses to clone without
      one; seed-vc loads BigVGAN and converts; the music dub falls back to the full
      vocal and reports the reason.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      cc18ca3e
    • Stefy Lanza (nextime / spora )'s avatar
      audio: implement music dub, demucs separation and ML restoration for real · 091fedd2
      Stefy Lanza (nextime / spora ) authored
      Three related placeholders were shipping as working features:
      
      - audio_stems.separate_with_provider() and audio_clean.restore_with_provider()
        were one-line stubs that unconditionally raised 501, so /v1/audio/stems and
        /v1/audio/cleanup only ever worked in ffmpeg fallback_mode despite demucs and
        deepfilternet being declared dependencies and probed by detect_audio_backends.
      - run_full_music_dub() ran only the STT step and returned hardcoded paths
        (vocals.wav, final_mix.wav) plus 'status': 'placeholder' for the other four,
        while the endpoint reported 'status': 'available'.
      
      Now:
      
      - separate_with_provider drives demucs (demucs.api, CLI fallback), keeps the
        vocals stem and sums drums+bass+other into an instrumental bed for
        vocals-instrumental; 4-stem passes the four through.
      - restore_with_provider runs DeepFilterNet (VoiceFixer fallback) and then applies
        the hum/click/loudness DSP with ffmpeg, so those options are no longer lost.
      - audio-music-dub is a real six-stage chain: separate -> transcribe the ISOLATED
        vocal -> adapt lyrics (LLM prompted for singable output with matching syllable
        count, argostranslate fallback) -> re-sing with F5-TTS cloned from the original
        vocal -> optional Seed-VC f0-conditioned singing pass -> ffmpeg remix + limiter.
        New request fields: text_model, voice_name/ref_text, sing_convert,
        diffusion_steps, pitch_shift, speed, seed, fallback_mode.
      
      Missing optional deps now degrade to a reported 'skipped'/'failed' step with a
      reason and a top-level status of 'complete' vs 'degraded' plus warnings[], rather
      than silently producing placeholders. 501s name the package to install.
      
      Verified offline with a fake-model harness: the chain produces a real RIFF wav
      mix end to end, feeds the vocal stem (not the mix) to STT, and correctly reports
      degraded when seed-vc is unavailable.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      091fedd2
  2. 07 Sep, 2026 1 commit
    • Stefy Lanza (nextime / spora )'s avatar
      docs: bring README + docs area up to date (engines, RunPod, OCR, speech, embeddings) · 1b357ab6
      Stefy Lanza (nextime / spora ) authored
      The README had not been touched since the OCR/multi-engine work in August and
      described roughly half of what the server does. Add the missing subsystems and
      correct the ones that overstated the code:
      
      - Inference engines: the per-model backend matrix (transformers/gguf/ds4/
        colibri/k3/kt/vllm/runpod) and how one is selected.
      - RunPod: feature summary plus a full docs/runpod.md — pods vs serverless,
        per-model config keys, cold start, GPU fallback, stuck-boot retry, budgets
        and the ledger, the stale-pod reaper, spillover (with its real scope),
        observability and RunPod-only deployments.
      - STT backends, per-model languages/translation, word timestamps, diarization
        and enrolled speaker recognition; embeddings families (BGE-M3 multi-vector,
        GME-Qwen2-VL, GeoCLIP, VPR) and /v1/rerank; the OCR subsystem and its
        schema store; LoRA training.
      - API tables for /v1/ocr*, /v1/rerank, /v1/loras*, the audio/speaker routes
        and the two missing pipelines.
      - models.json reference incl. multi-config (config_id/alias), the front/engine
        split, optional-subsystem requirements files, and a docs/ index.
      
      Honesty fixes: /v1/pipelines/audio-music-dub is flagged as incomplete (only
      the transcription step runs); spillover is documented as serverless-target /
      direct-path only.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      1b357ab6
  3. 06 Sep, 2026 7 commits
    • Stefy Lanza (nextime / spora )'s avatar
      runpod: guarantee a host node on GPU-less / unknown-backend deployments · 97d16a0a
      Stefy Lanza (nextime / spora ) authored
      For a coderai instance that only orchestrates RunPod (no local models), the node
      that hosts the proxy must advertise the runpod capability. The no-GPU path already
      builds its engine with backend "auto" (which has it), but an explicit engine_specs
      backend:"cpu" and the unknown-backend fallback did not — those would 503.
      
      - _DEFAULT_CAPS gains "cpu": {transformers, gguf, k3, runpod}.
      - Unknown-backend fallback now {transformers, gguf, runpod} (runpod needs no local
        hardware, so every node can host it).
      
      Verified by simulating pick_engine for a runpod-pinned model on auto/cpu/nvidia
      nodes — all route instead of 503.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      97d16a0a
    • Stefy Lanza (nextime / spora )'s avatar
      runpod: per-model boot/load timeouts (big models need longer) · 256900b7
      Stefy Lanza (nextime / spora ) authored
      The 300s boot cap was a hard-coded constant; large models need much more time for
      the image pull + weight download. Both budgets are now per-model config:
      
      - boot_timeout_s (default 300) — until the pod exposes its port; a stuck machine
        is still dropped fast and the next candidate tried.
      - load_timeout_s (default 600) — until vLLM answers /v1/models; raise for big models.
      
      Parsed in RunpodModelConfig (min 30s), used by _provision_one in place of the class
      constants (which remain the fallback), whitelisted in api_model_configure, and
      exposed on the model page with hints.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      256900b7
    • Stefy Lanza (nextime / spora )'s avatar
      runpod: try multiple log routes (the tail= param returned HTTP 400) · d9e49200
      Stefy Lanza (nextime / spora ) authored
      Live run showed get_pod_logs hitting HTTP 400 on {rest}/pods/{id}/logs?tail=N, so a
      failed boot fell back to the console link with no output. Now tries the plausible
      routes in order (no params, ?limit=, containerLogs) and keeps the first that
      answers, reporting the collected errors otherwise. The console deep-link remains
      the always-available path.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      d9e49200
    • Stefy Lanza (nextime / spora )'s avatar
      runpod: retry stuck-boot pods on another machine + self-diagnosing vLLM logs · 0c8417fa
      Stefy Lanza (nextime / spora ) authored
      Live testing showed RunPod sometimes ACCEPTS a pod create but the machine never
      starts (port never exposed) — 2 of 4 pods hung this way, burning the full 900s
      wait. Provisioning is now resilient and diagnosable:
      
      - Boot failure retries on the NEXT ranked GPU candidate (MAX_PROVISION_ATTEMPTS=3)
        instead of failing the request; _create_with_fallback returns the next index so
        each attempt uses a different machine/type.
      - Timeouts split + tightened: port-exposed 300s (was 900 — a pod that hasn't
        exposed its port in 5min never will), vLLM /v1/models health 600s (model load).
        Cuts worst-case billing on a bad machine from ~15min to ~5min per attempt.
      - Self-diagnosing: on any boot failure the pod's container/vLLM log is fetched and
        printed to coderai's log before teardown (the user's #1 cause is a vLLM OOM /
        launch error, only visible in that log), plus the console link.
      - Pod id + console URL are now logged immediately after create (previously only at
        "ready", so a stuck pod was untraceable).
      - Booting pods appear in pods_status/stats page as state="provisioning" with their
        console link, so a slow boot is visible live.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      0c8417fa
    • Stefy Lanza (nextime / spora )'s avatar
      runpod: fix pods /v1 path; add stale-pod reaper, multi-GPU fallback, pod logs · b911310a
      Stefy Lanza (nextime / spora ) authored
      Live-tested against a real RunPod account (Secure RTX 4090, Qwen2.5-0.5B):
      provision → boot → vLLM /v1/models health → idle auto-teardown + ledger ($0.021)
      all confirmed. Serverless + spillover proxy paths confirmed (reach RunPod, correct
      URL/auth). Fixes/additions from that run:
      
      - FIX: pods mode posted to <proxy>/chat/completions (missing /v1) → 404. The pod's
        vLLM serves under /v1 (health check hit /v1/models=200), so RunpodBackend now
        appends /v1 to the pod proxy URL (serverless base already ends in /openai/v1).
      
      - Stale-pod REAPER (safety): runpod_worker.reap_orphans() lists account pods tagged
        coderai-<deployment_id>-* and terminates any NOT tracked by a live pool (orphans
        from a crash/restart) — never touches in-flight provisions or another deployment's
        pods. Runs every ~30s in the scaler loop AND once at startup; started on the
        primary engine (where pods are hosted) via start_runpod_maintenance(). New
        config.runpod.deployment_id (default "default"); pods named with the tag.
      
      - Multi-GPU capacity FALLBACK: _rank_gpus() returns ranked candidates and
        _create_with_fallback() tries the next on a RunPod "no instances available"
        capacity error instead of failing (hit a real A5000 community capacity miss).
      
      - Pod LOGS: RunpodClient.get_pod_logs() (best-effort REST) + pod_console_url deep
        link; admin GET /admin/api/runpod/pod-logs; RunPod stats page gets a per-pod
        "view logs" modal + console link. PodHandle/pods_status carry console_url.
      
      - Settings: deployment_id field.
      
      Built coderai:base=5cffe23d1d20. Relaunch to activate (running image had the /v1
      bug + no reaper). After relaunch, a pods chat returns a real completion.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      b911310a
    • Stefy Lanza (nextime / spora )'s avatar
      runpod(phase3-5): pod pool + scaler + cost ledger, budgets, global-gate bypass, spillover, stats UI · 7649c9c2
      Stefy Lanza (nextime / spora ) authored
      Completes the RunPod backend for testing.
      
      Pods pool (runpod_worker.py):
      - RunpodPodPool: provision via _select_gpu (cheaper=lowest $/hr, faster=SECURE+
        throughput, across allowed pools, spot-aware) → create_pod → wait_ready →
        vLLM health-check; least-loaded per-request acquire, provision-on-demand up to
        max_pods; background scaler thread tears down idle pods to min after
        idle_timeout_s, keeps min warm, reaps. get_pod_pool registry + pods_status().
      - Budget enforcement at provision: global $/hr rate cap + per-model & global
        cumulative spend caps (rolling period) via the ledger; refuses over budget.
      
      Cost ledger (runpod_ledger.py): persistent per-model + global spend, trailing
      rolling windows (hour/day/week/month), totals() for stats. Final pod cost booked
      on teardown; live pod cost added on top for enforcement/stats (no double count).
      
      Backend (backends/runpod.py): pods mode wired to the pool (per-request acquire/
      release; async stream acquires off the event loop); serverless unchanged. Config
      adds global_cost_limit_usd + global_cost_period.
      
      Global-gate bypass (app.py _model_capacity): runpod models get an effectively
      unlimited LOCAL queue cap — remote GPUs aren't bound by the local concurrency
      gate; RunPod's max_pods/workers are the real limit.
      
      Spillover (app.py): a local model can burst to a RunPod SERVERLESS target when
      local can't serve it — front reverse-proxies to the serverless OpenAI URL, guarded
      to fall back to local on any error. Wired at the direct path for on_no_gpu
      (no engine) + on_concurrency_full (queue full). NOTE partial: broker path,
      streaming-concurrency, pods-target and on_local_error triggers are follow-ups.
      
      UI: dedicated /admin/runpod stats page (pods, live $/hr vs caps, spend by period +
      model) + nav link; Tasks page RunPod box; Settings split into category tabs
      (JS-only, IDs/save untouched) + global spend cap fields.
      
      Verified: imports in-image (8b16914ee24b), ledger + GPU selection (cheaper/faster)
      unit-tested, all templates Jinja-parse. Live RunPod calls (provision/serverless)
      still need an end-to-end test with the user's account.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      7649c9c2
    • Stefy Lanza (nextime / spora )'s avatar
      runpod(phase2): serverless request path + per-model config + no-local-download · 17604ab4
      Stefy Lanza (nextime / spora ) authored
      End-to-end serverless: a model tagged backend:runpod (mode:serverless) proxies
      OpenAI chat/completions to a RunPod serverless endpoint. Runs on a remote GPU,
      holds NO local weights.
      
      - codai/api/runpod_worker.py: RunpodModelConfig + parse_model_runpod (mode/pools/
        criteria/gpu/price/scaling/cost/serverless), serverless_base_url + auth_headers.
      - codai/backends/runpod.py: RunpodBackend — thin OpenAI proxy (chat/stream, Bearer
        auth for serverless), uses_local_vram=False; pods/auto raise a clear "next phase"
        error for now.
      - manager.py: runpod in _ENGINE_BACKENDS + _runpod_name_claims (pin-only) +
        runpod_should_handle + load_model dispatch branch. _model_is_runpod() gate:
        set_default_model skips local download/cache for runpod-served models.
      - router.py + registry.py: runpod is an explicit-pin capability; added to every
        engine's default caps (no local VRAM → any node hosts the proxy; prefers primary).
      - admin routes: whitelist per-model `runpod` + `runpod_spillover` blocks; skip
        local-VRAM estimate for runpod models.
      - models.html: RunPod backend option + full per-model sub-form (mode w/ explainer,
        serverless endpoint, served model, pools/criteria/gpu/price/spot, min/max pods,
        idle timeout, image/disk/port/env, spend limit+period) + prefill/collect.
      
      Serverless verified wired in-image (ef1fd7a3802c). Pods pool/scaler, cost ledger,
      spillover routing, and stats UI are later phases.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      17604ab4
  4. 05 Sep, 2026 1 commit
    • Stefy Lanza (nextime / spora )'s avatar
      runpod(phase1): account config + GraphQL client + admin key/GPU-catalog plumbing · 7c057af2
      Stefy Lanza (nextime / spora ) authored
      First slice of the RunPod remote-GPU backend (rent cloud GPUs, proxy inference).
      Dormant unless enabled — no request path yet.
      
      - config.py: RunpodConfig (enabled/api_key/cloud_type/api_base/rest_base/
        serverless_base/default_gpu_type/data_center/global_max_hourly_usd); registered,
        loaded, serialized. get_active_runpod_config() in manager.py.
      - codai/api/runpod_client.py: GraphQL client — ping, list_gpu_types (+prices),
        pick_gpu (cheapest under a $/hr ceiling, VRAM filter, spot-aware), create_pod
        (on-demand + interruptible/spot), get_pod, stop/terminate, list_pods, wait_ready.
        API key via ?api_key=, masked in all error strings.
      - admin: settings GET/save runpod block (key masked, overwritten only on a real
        new value); GET /admin/api/runpod/gpu-types (key check + catalog) and
        /admin/api/runpod/status (pods + live $/hr).
      - settings.html: RunPod card (key/cloud/default-gpu/global $/hr cap/DC/advanced
        bases) + "Test key & list GPUs" button.
      
      Decisions locked: host in primary engine (Layer-2, zero local VRAM); default
      vLLM-OpenAI pod image; per-model $/hr + global budget; per-model allow_spot;
      mode pods|serverless per model. TODO (request-path phase): RunPod requests must
      bypass the GLOBAL concurrency gate only (QueueManager.max_parallel_requests).
      
      Client GraphQL field names written from docs — pending live verification with a key.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      7c057af2
  5. 02 Sep, 2026 1 commit
    • Stefy Lanza (nextime / spora )'s avatar
      audio(whisper-server): convert mp4/any input with our ffmpeg (fix stuck-queue hang) · 717ab1ad
      Stefy Lanza (nextime / spora ) authored
      whisper-server models (whisper0/1/2/3) relied on whisper.cpp's --convert,
      whose ffmpeg call deadlocks on mp4 (undrained pipe) and hangs forever,
      head-of-line blocking the single-threaded server so every later request
      queues (app reported jobs "stuck in queue"; found a --convert ffmpeg stuck 4h+).
      
      - WhisperServerManager.start(): drop the --convert flag.
      - add _to_wav_16k_mono(): ffmpeg via subprocess.run (drains stdout/stderr,
        no pipe deadlock) + 120s timeout; transcribe() converts before POST so
        whisper.cpp only ever receives clean 16k mono wav. Bad file fails fast.
      - keep_resident honored for whisper-server models (vram group membership).
      
      Verified on f595eb2a6ece after relaunch: whisper3 + mp4 -> 200 (was infinite
      hang); wav control + repeat mp4 return sub-second (no head-of-line stall).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      717ab1ad
  6. 01 Sep, 2026 6 commits
    • Stefy Lanza (nextime / spora )'s avatar
      audio: CrisperWhisper long-form overlap windowing (no seam drops) · b6f6ca9e
      Stefy Lanza (nextime / spora ) authored
      Contiguous windows dropped words at each ~28s seam (Whisper's weak edge context +
      words straddling the boundary). Now use overlapping windows (29s wide, 24s hop =
      5s overlap), advancing by the hop and transcribing each full window, then
      de-duplicate the overlap by keeping only words that start at/after the last kept
      word's end, and rebuild the transcript from the de-duped words. Verified on an
      ~11min clip: 0 gaps >2s across all seams, continuous word timestamps to 673s,
      recovered the previously-dropped seam words.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      b6f6ca9e
    • Stefy Lanza (nextime / spora )'s avatar
      audio: CrisperWhisper long-form windowing + worker self-heal · b0483e8c
      Stefy Lanza (nextime / spora ) authored
      - Long-form fix: the HF pipeline's internal word-timestamp stitching crashes at
        the ~30s chunk boundary for CrisperWhisper (IndexError, dim-2 index==size). The
        service now windows manually — pipeline built without chunk_length_s; audio read
        via soundfile, split into <=28s windows (each a single Whisper pass), per-window
        word/segment timestamps offset by window start and concatenated. Verified on an
        ~11min clip: 1877 words, continuous timestamps to 673s, HTTP 200.
      - Self-heal: _RemoteCrisperWhisperBackend + _RemoteNemoBackend re-ensure the
        worker and retry once on a ConnectionError, so a restarted/killed worker no
        longer leaves a stale cached service_url in acquire_stt_backend.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      b0483e8c
    • Stefy Lanza (nextime / spora )'s avatar
      audio: CrisperWhisper support (isolated venv, verbatim + word timestamps) · b069137b
      Stefy Lanza (nextime / spora ) authored
      CrisperWhisper does verbatim ASR with precise word timestamps, but its custom
      generation config crashes on the server's transformers 5.x. Run it in its own
      venv worker (pinned torch 2.5.1+cu124 + transformers 4.46.3 — last pre-5.x with
      working Whisper word timestamps and a py3.13 tokenizers wheel):
      - codai/api/crisperwhisper_worker.py + tools/crisperwhisper_service.py
        (return_timestamps="word" + CrisperWhisper pause redistribution).
      - requirements-crisperwhisper.txt.
      - stt_backends.py: `crisperwhisper` is its own family -> _RemoteCrisperWhisperBackend
        (HTTP to the worker); generic `whisper-hf` still runs in the shared venv
        (text-only default + graceful timestamp fallback); HF-ASR path now converts any
        container (mp4/m4a/webm) to 16k mono wav first and uses model-correct
        return_timestamps (Whisper True/"word", CTC "chunk"/"word").
      - transcriptions.py: crisperwhisper dispatched as a GPU worker.
      - manager skip-download + admin backend option for whisper-hf/crisperwhisper.
      
      Verified: mp4 -> verbatim text + per-word timestamps, HTTP 200, on the 3090.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      b069137b
    • Stefy Lanza (nextime / spora )'s avatar
      audio: FP16 wav2vec2 on GPU (halves VRAM) · d51f46d2
      Stefy Lanza (nextime / spora ) authored
      Load the wav2vec2/HF-ASR pipeline in FP16 on CUDA (~3GB -> ~1.5GB), FP32 on CPU,
      with a fallback to FP32 if a checkpoint rejects half precision; overridable via a
      `dtype` config field. Lower the wav2vec2 VRAM reservation default 3->2GB to match.
      Enables co-residency of the full transcription/diarization/speaker stack with a
      14B LLM on one 24GB 3090 (measured ~14.5GB used with a gemma-4-14b @8k sibling).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      d51f46d2
    • Stefy Lanza (nextime / spora )'s avatar
      audio: sliding-window speaker embeddings (window/step) · ed8c014d
      Stefy Lanza (nextime / spora ) authored
      /v1/audio/speaker-embeddings gains window (seconds) + step (default = window):
      returns one embedding per sliding window, each tagged start/end, instead of a
      single whole-file embedding. Implemented in the pyannote worker's /embed via
      _crop_wav over the cached embedder; client + endpoint pass the params through;
      whole-file mode and enroll/identify/verify are unchanged.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      ed8c014d
    • Stefy Lanza (nextime / spora )'s avatar
      audio: STT backends, diarization, speaker embeddings/ID, word timestamps · 7e5a925c
      Stefy Lanza (nextime / spora ) authored
      Transcription (/v1/audio/transcriptions):
      - Route multipart uploads by peeked model (front _peek_model) so they land on
        the model's assigned engine instead of scattering by capability.
      - New pluggable STT backends (codai/api/stt_backends.py): wav2vec2 (HF), vosk
        (CPU dirs), NVIDIA NeMo-Canary (isolated venv worker) + faster-whisper/whisper.
      - Per-model language selection + validation; Canary source->target translation.
      - OpenAI timestamp_granularities[]=word: top-level words[] in verbose_json
        (vosk/wav2vec2/faster-whisper/canary).
      - VRAM-eviction tracking (acquire_stt_backend) + keep_resident co-residency for
        the small set; /v1/models surfaces languages + backends.
      
      Diarization (/v1/audio/diarization + diarize=true):
      - pyannote.audio isolated venv worker (codai/api/pyannote_worker.py,
        tools/pyannote_service.py) on the 3090; speaker-labeled segments; graceful
        degradation. Pinned torch/pyannote/hf_hub/matplotlib in requirements-pyannote.txt.
      
      Speaker embeddings + recognition:
      - /v1/audio/speaker-embeddings (ecapa via speechbrain, pyannote/wespeaker).
      - Enroll/list/delete + identify/verify (codai/api/speaker_registry.py); persistent
        voiceprint store; diarization identify=true relabels turns with enrolled names.
      
      New isolated-venv requirements: requirements-nemo.txt, requirements-pyannote.txt
      (+ vosk in requirements.txt).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      7e5a925c
  7. 27 Aug, 2026 7 commits
    • Stefy Lanza (nextime / spora )'s avatar
      vram: don't ratchet down a model's learned GPU-layer split under contention · f54428cf
      Stefy Lanza (nextime / spora ) authored
      record_vram_delta persisted measured_n_gpu_layers from every load, including
      VRAM-CONTENDED ones (another model resident → llama.cpp fit fewer layers). Since
      _learned_n_gpu_layers reuses that as a hard cap for auto (-1) configs, one unlucky
      contended load pinned a model half on CPU permanently — it never recovered even
      when the card was later free.
      
      Observed on the gemma-4-26B-A4B 32k config (alias lisa-32k): a contended load
      ratcheted 31→20 GPU layers and stuck, spilling ~5.8 GB of weights + KV to CPU on
      a 3090 with 11 GB free.
      
      Fix: in record_vram_delta, never overwrite a HIGHER known-good measured_n_gpu_layers
      with a LOWER value from an offloaded (CPU-spilled) load. A genuinely-too-big model
      still settles at its free-card maximum on an uncontended load, so keeping the higher
      value is safe; worst case is one offload-retry on a later truly-contended load.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      f54428cf
    • Stefy Lanza (nextime / spora )'s avatar
      logging: durable request-debug sink + non-silent engine log pump · ced55385
      Stefy Lanza (nextime / spora ) authored
      Fixes "embedding request debug on radeon disappeared" — a real regression
      where the radeon engine stopped emitting ALL log output the instant it
      finished loading its GGUF embedders (~13s after launch), while still serving,
      so its FULL REQUEST DEBUG + thermal debug vanished with no trace.
      
      Two changes:
      
      1) codai/api/log.py — the log_requests middleware now ALSO appends each
         FULL REQUEST DEBUG block, from the engine's own process, straight to
         <dir(CODERAI_LOG_FILE)>/requests.log via a single atomic O_APPEND write
         (tagged [ts][engine] from CODERAI_ENGINE_NAME). This survives the engine's
         stdout stream dying, so request bodies are captured regardless. Concurrent
         engines never interleave mid-block.
      
      2) codai/frontproxy/engine_supervisor.py — _pump_logs no longer dies silently:
         a bad line can't kill the pump, and on stream end it prints
         "[pump] log stream ended (<stdout EOF|pump error>); engine still ALIVE/exited"
         so a vanishing engine log stream is diagnosable instead of looking like
         "debug turned off".
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      ced55385
    • Stefy Lanza (nextime / spora )'s avatar
      config(multi-config): fix GGUF dispatch + per-config-id measured persistence · f9f2a373
      Stefy Lanza (nextime / spora ) authored
      Follow-up to the same-path multi-config runtime fix. Two more path-keyed spots
      broke a same-file sibling addressed by its alias (e.g. lisa-32k):
      
      1) GGUF dispatch: _resolve_local_gguf matched entries by path/basename only, so
         a sibling addressed by alias wasn't recognized as GGUF and fell to the HF
         loader ("lisa-32k is not a valid model identifier"). Now also match the
         entry's alias.
      
      2) Measured footprint cross-contamination: persist_model_field matched entries
         by path, so one config's measured_vram_gb/n_gpu_layers overwrote the sibling's
         — the small-ctx variant inherited the large-ctx footprint (34 GB) and still
         offloaded to CPU. persist_model_field now targets by config_id when given;
         record_vram_delta passes cfg['config_id']; _model_cfg carries config_id/
         config_name so the config knows its own entry identity.
      
      Net: lisa (178K) and lisa-32k (32K) are now fully independent — distinct config,
      GGUF load from the shared file, and separate measured footprints.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      f9f2a373
    • Stefy Lanza (nextime / spora )'s avatar
      config: honor same-path multi-config at runtime (key siblings by alias) · 6ec4af1c
      Stefy Lanza (nextime / spora ) authored
      The web UI stores sibling configs for one model file (distinct config_id, e.g. a
      smaller-context variant), and the front already routes by alias (_route_key:
      "two configs of the same model with distinct aliases can be assigned to
      different engines"). But engine startup keyed every text-model config by path
      (_model_id -> path), so a second same-path entry OVERWROTE the first —
      BOTH aliases then resolved to the last-registered config (verified: lisa and
      lisa-32k both got n_ctx=32768, silently changing the primary too).
      
      Fix: in main.py text-model registration, key the PRIMARY config by path (stays
      path-addressable) and each SIBLING (same path, already seen) by its ALIAS, so
      the two configs are distinct and each alias resolves to its own n_ctx. A sibling
      without an alias is skipped (can't be addressed). The GGUF loader already reads
      the file from cfg['path'], so an alias-keyed sibling still loads the right file.
      Verified: lisa->178000, lisa-32k->32768, same underlying .gguf.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      6ec4af1c
    • Stefy Lanza (nextime / spora )'s avatar
      rerank: resolve model via embedding category to pass manager type-validation · b72896e5
      Stefy Lanza (nextime / spora ) authored
      The reranker is registered in embedding_models, so request_model(model,"rerank")
      was rejected ("is a embedding model and cannot be used for rerank generation").
      Resolve with model_type="embedding" (its actual category) so validation passes;
      rerank.py still loads it as a cross-encoder (AutoModelForSequenceClassification),
      re-loading if a cached embedder object is found under the same key.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      b72896e5
    • Stefy Lanza (nextime / spora )'s avatar
      deps: bump bitsandbytes to >=0.50.2 (fix 4-bit load on transformers 5.12) · 28d7711d
      Stefy Lanza (nextime / spora ) authored
      bitsandbytes 0.49.2's Params4bit.__new__ has no **kwargs and rejects the
      _is_hf_initialized kwarg that transformers 5.12 passes during 4-bit weight
      init, so every load_in_4bit model crashed with:
        Params4bit.__new__() got an unexpected keyword argument '_is_hf_initialized'
      In production this turned a single default-model chat request (Qwen3.5-9B,
      load_in_4bit) into a ~2h internal 60-retry storm. bnb 0.50.2 adds **kwargs to
      Params4bit.__new__ (absorbs _is_hf_initialized); verified end-to-end: a
      transformers load_in_4bit load now succeeds on cuda:0 (torch 2.11/cu130), and a
      GPU 4-bit quantize/dequantize round-trips. Image rebuilt with bnb 0.50.2.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      28d7711d
    • Stefy Lanza (nextime / spora )'s avatar
      rerank: add /v1/rerank cross-encoder endpoint + surface OCR in /v1/models · e44d41ac
      Stefy Lanza (nextime / spora ) authored
      Reranker (e.g. BAAI/bge-reranker-v2-m3):
      - new codai/api/rerank.py: /v1/rerank scores query↔document pairs with a native
        AutoModelForSequenceClassification cross-encoder (no new dep), cached +
        VRAM/thermal-managed via request_model like embeddings. Returns
        {index, relevance_score(sigmoid), document?} sorted desc, top_n honored.
      - router: /v1/rerank added to _INFERENCE_PATHS so the front routes it by model
        capability (transformers → nvidia engine), like /v1/embeddings.
      - app: include rerank_router.
      - capabilities: add `reranking` + `ocr` fields; detect 'rerank'/'cross-encoder'
        names as reranking.
      
      /v1/models discoverability:
      - list_models() now appends enabled OCR engines (paddle/doctr/surya) as
        type='ocr' entries (a node subsystem invoked at /v1/ocr?engine=<id>), so
        clients can discover them.
      - a reranker registered in models.json (embedding_models, capabilities
        ['reranking']) surfaces automatically via list_models, like bge-m3.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      e44d41ac
  8. 26 Aug, 2026 4 commits
    • Stefy Lanza (nextime / spora )'s avatar
      embeddings(bge-m3): vectorize sparse/colbert aggregation on GPU · 59f6ebe0
      Stefy Lanza (nextime / spora ) authored
      The per-token Python loops that built the sparse {token_id: weight} dict and
      the colbert vector lists held the GIL between forward passes, starving the GPU
      (steady ~10-36% util with brief bursts). Replace them with GPU tensor ops:
      
      - sparse: relu(sparse_linear·h) with invalid positions (specials/padding)
        zeroed, then max-pooled per token id via a single scatter_reduce(amax) into a
        (B, vocab) matrix; per-row nonzero extraction is O(nnz), not O(L).
      - colbert: one C-level tolist per row over its valid tokens (vs per-token).
      - valid mask computed on-device via torch.isin.
      
      Output byte-matches the previous per-token logic (dense/sparse/colbert within
      1e-4). Benefit grows with batch size, so larger client batches translate to GPU
      work instead of Python-loop time.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      59f6ebe0
    • Stefy Lanza (nextime / spora )'s avatar
      embeddings(bge-m3): reliable sparse_head resolution via cache glob · 8fb4f85a
      Stefy Lanza (nextime / spora ) authored
      hf_hub_download(local_files_only=True) intermittently failed to locate the
      cached sparse_linear.pt inside the engine process, so _is_bge_m3 returned
      False and bge-m3 silently degraded to the dense-only sentence-transformers
      path. _resolve_repo_file now falls back through try_to_load_from_cache and a
      direct filesystem glob of the HF cache (models--org--name/snapshots/*/file),
      which resolves reliably regardless of process context. Detection keys off the
      sparse head file rather than the model_type string. (Debug scaffolding removed.)
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      8fb4f85a
    • Stefy Lanza (nextime / spora )'s avatar
      embeddings(bge-m3): fix loader crash + head dtype/grad · cc06016d
      Stefy Lanza (nextime / spora ) authored
      - import build_from_pretrained_kwargs inside _load_bge_m3 (it's a local import
        of _load_embedding_model, not module-level) — the NameError made every bge-m3
        load fall over so the model silently degraded to the dense-only path.
      - cast the sparse/colbert heads to the base model's dtype (bf16) so the head
        matmul over the last hidden state doesn't dtype-mismatch.
      - requires_grad_(False) on the heads (inference only) — no autograd graph per
        request.
      - log the bge-m3 backend selection at load.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      cc06016d
    • Stefy Lanza (nextime / spora )'s avatar
      embeddings: native bge-m3 dense+sparse(+colbert) multi-vector support · 66f5540b
      Stefy Lanza (nextime / spora ) authored
      Add BAAI/bge-m3 hybrid embedding (dense + sparse lexical, optional colbert)
      without the FlagEmbedding dependency (which pins older transformers/peft and
      would conflict with the transformers 5.x stack): the sparse/colbert heads are
      plain Linear layers over XLM-RoBERTa's last hidden state, reproduced natively
      with transformers + torch already in the venv.
      
      - new 'bge-m3' embedding backend: detected by xlm-roberta model_type + a
        sparse_linear.pt head (so plain bge-large etc. stay on the dense ST path);
        loads the base model + sparse_linear.pt (+ colbert_linear.pt if present).
      - _bge_m3_encode: dense = L2-normalized CLS; sparse = max-pooled
        relu(sparse_linear·h) per token id (specials dropped) as {token_id: weight};
        colbert = L2-normalized colbert_linear·h per content token.
      - /v1/embeddings extended: request.embedding_types selects any of
        dense|sparse|colbert (overrides the model-config default); response items
        gain sparse_embedding {indices, values} and colbert_embedding. Dense stays
        in the standard `embedding` field (base64 honored). Configurable default via
        the model's embedding_types config.
      - cleanup()/eviction branch moves the base model + heads off GPU.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      66f5540b
  9. 22 Aug, 2026 1 commit
    • Stefy Lanza (nextime / spora )'s avatar
      ocr: make managed Surya-2 vLLM reclaimable by VRAM eviction · 2c419247
      Stefy Lanza (nextime / spora ) authored
      The Surya-2 engine serves its VLM via a managed vLLM subprocess
      (vllm_worker.ensure_service). That subprocess is not a manager-tracked
      model, and the OCR VRAM releaser only tore down the lightweight worker
      pools (killing the HTTP workers) — it never stopped the vLLM holding the
      bulk VRAM (gpu_memory_utilization × card). So on-request eviction saw the
      VRAM as used, found nothing it could free, and (with no CPU fallback) a
      docTR/Paddle load on the same card could OOM.
      
      - vllm_worker: add stop_service_for(cfg, model_path, served_name) (stop ONE
        service, return estimated GB freed) + is_running() helper.
      - OcrManager._release_vram now also stops the managed Surya-2 vLLM
        (_stop_surya_vllm) when surya_serve == "vllm", so the registered external
        releaser actually reclaims its VRAM. Next OCR request re-boots it via
        ensure_service. Targeted by (model_path, served_name) so an LLM vLLM
        instance is left alone.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      2c419247
  10. 21 Aug, 2026 10 commits