<% const displayCompanionVersion = (value) => { const match = /^(\d+\.\d+\.\d+)-experimental\.(\d+)$/i.exec(String(value || '')); return match ? `${match[1]} · Experimental release ${match[2]}` : String(value || 'Unknown version'); }; %>
<%- include("../../../src/web/views/partials/page-header", { eyebrow: "Experimental companion", pageTitle: "Lumi Transcription", description: "Server-hosted speech recognition for selected OBS sources, managed through one Lumi Companion application." }) %> <%= providerHealth.healthy ? 'Inference ready' : 'Setup required' %>
Live Twitch delivery is not accepted yet

The native OBS 31+ caption path still needs target-machine validation. Lumi will not present a local simulated caption as live Twitch success.

First usable path

Setup progress

Complete one clear step at a time. Existing server administration remains in Lumi; streaming-computer controls remain in Companion.

  1. Download and pair CompanionCreate a short-lived, single-use package for the streaming computer.
  2. Install the OBS integration<%= devices.some((device) => device.metadata?.bridge_installed) ? 'A paired Companion has verified its managed OBS bridge installation.' : 'Companion owns installation and repair; the bridge has no separate settings.' %>
  3. Load and benchmark a modelSmall English is the recommended starting point.
  4. Select and test a microphone<%= devices.some((device) => device.metadata?.path_test_valid) ? 'The saved voice-free path check is still valid for the current configuration.' : 'Run the short voice-free path check once after setup or a relevant configuration change.' %>
Review speech models

Lumi host

Inference health

Speech recognition runs on the Lumi server, never on the streaming computer.

Providerwhisper.cpp <%= runtimeManifest.tested_version %>
Worker<%= providerHealth.state %>
Selected model<%= providerHealth.model ? providerHealth.model + (providerHealth.model_ready ? '' : ' · not ready') : 'Not loaded' %>
Active sessions<%= providerHealth.sessions || 0 %>
<% if (!providerHealth.healthy) { const workerReason = providerHealth.last_error?.message || (providerHealth.last_exit ? `Last exit code: ${providerHealth.last_exit.code ?? 'unknown'}` : null) || providerHealth.last_diagnostic?.message || 'No worker failure detail has been reported yet.'; %>
Speech recognition needs attention

<%= workerReason %> Reload the verified model below, then rerun the Companion test.

<% } %>
<% runtimeManifest.artifacts.forEach((artifact) => { %> <% }) %>

The low-latency package targets <%= runtimeManifest.artifacts.find((artifact) => artifact.backend === 'cuda')?.minimum_gpu || 'a supported NVIDIA GPU' %> and newer. Runtime changes activate immediately when no transcription session is active.

Shared GPU awareness

Lumi warns and benchmarks when Lumi AI already occupies GPU memory. It never unloads AI models automatically.

Access

Paired devices

Active credentials are shown first. Switch to revoked history when needed; revoked records are permanently removed after 30 days.

<%= devices.length %> active
<% if (!devices.length) { %>
No active Companion is paired

Use Download Companion to create a package that expires after 15 minutes and works once.

<% } %> <% if (devices.length) { %>
    <% devices.forEach((device) => { %>
  • <%= device.name %>Last connected <%= new Date(device.last_connected_at).toLocaleString() %><% if (device.metadata?.companion_version) { %> · Companion <%= displayCompanionVersion(device.metadata.companion_version) %><% } %>
    Allowed
  • <% }) %>
<% } %>
Last hour

Transcription test analysis

Inspect each dedicated Companion test by latency, finalized confidence, or both. Test transcripts and measurements are permanently deleted after one hour.

<%= benchmarks.length %> retained
<% if (!benchmarks.length) { %>
No dedicated tests in the last hour

Start Accuracy & latency test from the Companion Test page. Full-path checks are intentionally not retained here.

<% } %> <% benchmarks.forEach((test) => { const metricRows = [ ['Minimum', 'min'], ['Low 1% avg', 'low_1_average'], ['Median', 'median'], ['Average', 'average'], ['P99', 'p99'], ['High 1% avg', 'high_1_average'], ['Maximum', 'max'] ]; %>
<%= new Date(test.started_at).toLocaleString() %><%= test.source_name %> · <%= test.model_id || 'model pending' %> / <%= test.backend || 'backend pending' %> · <%= Math.round(test.duration_ms / 1000) %>s <%= test.status.replaceAll('_', ' ') %>
LatencyGreen <750 msBlue <1,250 msYellow <2,000 msOrange <3,000 msRed ≥3,000 ms
ConfidenceGreen ≥92%Blue ≥80%Yellow ≥65%Orange ≥45%Red <45%
<% if (!test.words.length) { %>

<%= test.transcript || 'No finalized words were measured.' %>

<% } %> <% test.words.forEach((word) => { const confidenceLabel = Number.isFinite(word.confidence) ? `${(word.confidence * 100).toFixed(1)}%` : 'not finalized'; %> <%= word.text %> <% }) %>
<% [['Latency', test.stats.latency, (value) => `${Math.round(value)} ms`], ['Final confidence', test.stats.confidence, (value) => `${(value * 100).toFixed(1)}%`]].forEach(([label, stats, format]) => { %>

<%= label %>

<% metricRows.forEach(([metricLabel, key]) => { %>
<%= metricLabel %><%= Number.isFinite(stats[key]) ? format(stats[key]) : '—' %>
<% }) %>

<%= stats.count %> measured values

<% }) %>
<% }) %>
Curated choices

Speech models

Downloads require confirmation and checksum verification before a model can load.

<% models.forEach((model) => { %>

<%= model.label %>

<%= model.recommended ? 'Recommended starting point' : 'Fallback option' %> · <%= Math.round(model.bytes / 1048576) %> MiB

<%= providerHealth.model === model.id && providerHealth.model_ready ? 'Loaded' : providerHealth.model === model.id && model.status.valid ? 'Reload required' : model.status.valid ? 'Verified' : model.status.installed ? 'Checksum failed' : 'Not installed' %> <% if (!model.status.valid) { %><% } %> <% if (model.status.valid && (providerHealth.model !== model.id || !providerHealth.model_ready)) { %><% } %>
<% }) %>
Audio privacy

Raw audio is held only in bounded memory and is never written to disk by default. Diagnostic caption text is retained for seven days unless disabled.