Compare commits

..

15 Commits

Author SHA1 Message Date
Gitea Actions
d220158dd7 chore: bump version to 2.0.15 [skip ci] 2026-04-10 19:38:00 +00:00
Developer
8670e19acc Add "Share Current Room" button to copy existing room config as share code
All checks were successful
Tests / Python Backend Tests (push) Successful in 5s
Tests / Frontend Tests (push) Successful in 7s
Tests / Rust Sidecar Tests (push) Successful in 1m58s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 12:26:29 -07:00
Gitea Actions
812cc4ac5e chore: bump version to 2.0.14 [skip ci] 2026-04-10 19:15:02 +00:00
Developer
4aa19eee86 Fix test: align remote.mode in no-reload settings test
All checks were successful
Tests / Python Backend Tests (push) Successful in 5s
Tests / Frontend Tests (push) Successful in 8s
Tests / Rust Sidecar Tests (push) Successful in 1m59s
The default remote.mode changed from 'local' to 'byok', causing
the apply_settings test to detect a mode mismatch and trigger an
unexpected engine reload. Pin remote.mode to 'local' in the test
to match the controller's assumed current mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 12:01:11 -07:00
Developer
b8dfe0f1ba Cloud-first UX: default to Deepgram, gate start button, add room sharing
Some checks failed
Tests / Python Backend Tests (push) Failing after 6s
Tests / Frontend Tests (push) Successful in 9s
Tests / Rust Sidecar Tests (push) Successful in 2m1s
- Change default transcription mode from local to byok (cloud/Deepgram)
- Move Transcription Mode selector to top of settings for visibility
- Hide local-only settings (model, VAD, timing) when cloud mode selected
- Disable Start button until API key (byok) or login (managed) is configured
- Add room creation and share code flow to Shared Captions section
- Add POST /api/create-room endpoint to Node.js sync server
- Update default sync URL placeholder to caption.shadowdao.com

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:58:49 -07:00
Gitea Actions
5837b97a20 chore: bump sidecar version to 1.0.11 [skip ci] 2026-04-08 21:15:05 +00:00
Gitea Actions
ab09a3e9da chore: bump version to 2.0.13 [skip ci] 2026-04-08 21:09:40 +00:00
Developer
5343a28a08 Bundle sounddevice PortAudio library in sidecar builds
All checks were successful
Tests / Python Backend Tests (push) Successful in 5s
Tests / Frontend Tests (push) Successful in 8s
Tests / Rust Sidecar Tests (push) Successful in 2m4s
On macOS, sounddevice ships its own PortAudio dylib in the
_sounddevice_data directory. PyInstaller wasn't collecting it,
causing "Error querying device -1" when the sidecar tried to
open an audio stream.

Added data collection for _sounddevice_data in both cloud and
headless PyInstaller specs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:07:04 -07:00
Developer
f0bf026133 Handle ExitRequested to stop sidecar on macOS Cmd+Q
All checks were successful
Tests / Python Backend Tests (push) Successful in 4s
Tests / Frontend Tests (push) Successful in 7s
Tests / Rust Sidecar Tests (push) Successful in 1m56s
On macOS, Cmd+Q triggers ExitRequested before Exit. If the app is
force-quit or closed via Cmd+Q, the Exit event may not fire,
leaving the sidecar process orphaned with ports 8080/8081 in use.

Now handles both ExitRequested and Exit to ensure the sidecar is
always stopped when the app closes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:02:25 -07:00
Developer
37a029d1c6 Show app version from Tauri instead of sidecar
All checks were successful
Tests / Python Backend Tests (push) Successful in 5s
Tests / Frontend Tests (push) Successful in 8s
Tests / Rust Sidecar Tests (push) Successful in 1m59s
The version label was reading from backendStore.version which comes
from the sidecar's version.py (hardcoded at build time). Now uses
Tauri's getVersion() API which reads from tauri.conf.json -- the
actual app version that gets bumped by the release workflow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:45:53 -07:00
Gitea Actions
5ec030387f chore: bump sidecar version to 1.0.10 [skip ci] 2026-04-08 20:27:00 +00:00
Gitea Actions
4d9bdba903 chore: bump version to 2.0.12 [skip ci] 2026-04-08 20:22:08 +00:00
Gitea Actions
a7a3bcd102 chore: bump version to 2.0.11 [skip ci] 2026-04-08 20:10:12 +00:00
Developer
115d93482a Always poll status after start/stop, even on API error
All checks were successful
Tests / Python Backend Tests (push) Successful in 4s
Tests / Frontend Tests (push) Successful in 7s
Tests / Rust Sidecar Tests (push) Successful in 1m55s
When apiPost throws (e.g. 400 "Already transcribing"), pollStatus
never ran because it was in the try block. The button stayed stuck
on "Start" even though transcription was running.

Moved pollStatus to the finally block so it always syncs the UI
with actual backend state. Also suppresses the error message for
400 responses since they just mean the state is already correct.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:03:34 -07:00
Developer
fb672cbaef Update Cargo.lock
Some checks failed
Tests / Python Backend Tests (push) Successful in 4s
Tests / Frontend Tests (push) Successful in 7s
Tests / Rust Sidecar Tests (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:02:03 -07:00
16 changed files with 464 additions and 113 deletions

View File

@@ -125,6 +125,8 @@ def test_apply_settings_no_reload_when_same(controller):
# Ensure config returns the same values
controller.config.set("transcription.model", "base.en")
controller.config.set("transcription.device", "auto")
# Remote mode must also match (no engine means current mode is 'local')
controller.config.set("remote.mode", "local")
controller.reload_engine = MagicMock(return_value=(True, "reloaded"))

View File

@@ -42,7 +42,7 @@ transcription:
server_sync:
enabled: false
url: "http://localhost:3000/api/send"
url: ""
room: "default"
passphrase: ""
# Font settings are now in the display section (shared for local and server sync)
@@ -69,7 +69,7 @@ web_server:
host: "127.0.0.1"
remote:
mode: local # local | managed | byok
mode: byok # local | managed | byok
server_url: "" # Proxy server URL for managed mode (e.g., wss://your-proxy.com)
auth_token: "" # JWT stored after login (managed mode)
byok_api_key: "" # Deepgram API key for BYOK mode

View File

@@ -19,9 +19,26 @@ datas = [
('config/default_config.yaml', 'config'),
]
# Collect sounddevice's bundled PortAudio library (_sounddevice_data)
try:
import sounddevice
sd_path = os.path.dirname(sounddevice.__file__)
sd_data = os.path.join(sd_path, '_sounddevice_data')
if os.path.exists(sd_data):
datas.append((sd_data, '_sounddevice_data'))
print(f" + Collected sounddevice PortAudio data from {sd_data}")
# Also collect the package itself
sd_datas = collect_data_files('sounddevice')
if sd_datas:
datas += sd_datas
print(f" + Collected {len(sd_datas)} sounddevice data files")
except ImportError:
print(" - Warning: sounddevice not found")
# Hidden imports -- only lightweight deps needed for Deepgram streaming
hiddenimports = [
'sounddevice',
'_sounddevice_data',
'numpy',
# FastAPI and dependencies
'fastapi',

View File

@@ -38,6 +38,21 @@ datas = [
(vad_assets_path, 'faster_whisper/assets'),
] + pvporcupine_data_files
# Collect sounddevice's bundled PortAudio library (_sounddevice_data)
try:
import sounddevice
sd_path = os.path.dirname(sounddevice.__file__)
sd_data = os.path.join(sd_path, '_sounddevice_data')
if os.path.exists(sd_data):
datas.append((sd_data, '_sounddevice_data'))
print(f" + Collected sounddevice PortAudio data from {sd_data}")
sd_datas = collect_data_files('sounddevice')
if sd_datas:
datas += sd_datas
print(f" + Collected {len(sd_datas)} sounddevice data files")
except ImportError:
print(" - Warning: sounddevice not found")
# Hidden imports -- NO PySide6/Qt needed for headless backend
hiddenimports = [
# Transcription engine
@@ -46,6 +61,7 @@ hiddenimports = [
'faster_whisper.vad',
'ctranslate2',
'sounddevice',
'_sounddevice_data',
'scipy',
'scipy.signal',
'numpy',

View File

@@ -1,7 +1,7 @@
{
"name": "local-transcription",
"private": true,
"version": "2.0.10",
"version": "2.0.15",
"type": "module",
"scripts": {
"dev": "vite dev",

View File

@@ -1,6 +1,6 @@
[project]
name = "local-transcription"
version = "1.0.9"
version = "1.0.11"
description = "A standalone desktop application for real-time speech-to-text transcription using Whisper models"
readme = "README.md"
requires-python = ">=3.9"

View File

@@ -703,6 +703,36 @@ app.post('/api/send', async (req, res) => {
}
});
// Create room explicitly (no transcription needed)
app.post('/api/create-room', async (req, res) => {
try {
const { room, passphrase } = req.body;
if (!room || !passphrase) {
return res.status(400).json({ error: 'Missing room or passphrase' });
}
// Check if room already exists
const existing = await loadRoom(room);
if (existing) {
const valid = await verifyPassphrase(room, passphrase);
if (!valid) {
return res.status(401).json({ error: 'Room exists with different passphrase' });
}
return res.json({ status: 'ok', room, created: false, message: 'Room already exists' });
}
// Create the room (verifyPassphrase creates it if it doesn't exist)
await verifyPassphrase(room, passphrase);
console.log(`[Room] Created room "${room}"`);
res.json({ status: 'ok', room, created: true });
} catch (err) {
console.error('Error in /api/create-room:', err);
res.status(500).json({ error: err.message });
}
});
// List transcriptions
app.get('/api/list', async (req, res) => {
try {

2
src-tauri/Cargo.lock generated
View File

@@ -1881,7 +1881,7 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
name = "local-transcription"
version = "2.0.3"
version = "2.0.12"
dependencies = [
"bytes",
"chrono",

View File

@@ -1,6 +1,6 @@
[package]
name = "local-transcription"
version = "2.0.10"
version = "2.0.15"
description = "Real-time speech-to-text transcription for streamers"
authors = ["Local Transcription Contributors"]
edition = "2021"

View File

@@ -74,14 +74,25 @@ pub fn run() {
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|app, event| {
if let tauri::RunEvent::Exit = event {
// Stop the sidecar when the app exits
if let Some(state) = app.try_state::<sidecar::ManagedSidecar>() {
if let Ok(mut mgr) = state.0.lock() {
eprintln!("[app] Stopping sidecar on exit...");
mgr.stop();
match event {
tauri::RunEvent::Exit => {
if let Some(state) = app.try_state::<sidecar::ManagedSidecar>() {
if let Ok(mut mgr) = state.0.lock() {
eprintln!("[app] Stopping sidecar on exit...");
mgr.stop();
}
}
}
tauri::RunEvent::ExitRequested { .. } => {
// Also stop sidecar on exit request (Cmd+Q on macOS)
if let Some(state) = app.try_state::<sidecar::ManagedSidecar>() {
if let Ok(mut mgr) = state.0.lock() {
eprintln!("[app] Stopping sidecar on exit request...");
mgr.stop();
}
}
}
_ => {}
}
});
}

View File

@@ -1,6 +1,6 @@
{
"productName": "Local Transcription",
"version": "2.0.10",
"version": "2.0.15",
"identifier": "net.anhonesthost.local-transcription",
"build": {
"frontendDist": "../dist",

View File

@@ -15,6 +15,7 @@
let sidecarState = $state<SidecarState>("checking");
let debugLog = $state("");
let availableUpdate = $state("");
let appVersion = $state("");
let obsDisplayUrl = $derived(backendStore.obsUrl);
let syncDisplayUrl = $derived(backendStore.syncUrl);
@@ -108,6 +109,14 @@
}
onMount(() => {
// Get app version from Tauri
import("@tauri-apps/api/app").then(({ getVersion }) =>
getVersion().then((v) => { appVersion = v; })
).catch(() => {
// Browser dev mode -- read from package.json or use fallback
appVersion = "dev";
});
checkAndLaunchSidecar();
return () => {
@@ -201,7 +210,7 @@
<TranscriptionDisplay />
<Controls />
<div class="version-label">v{backendStore.version}</div>
<div class="version-label">v{appVersion || backendStore.version}</div>
</div>
{#if showSettings}

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { backendStore } from "$lib/stores/backend";
import { configStore } from "$lib/stores/config";
import { transcriptionStore } from "$lib/stores/transcriptions";
let isTranscribing = $derived(backendStore.appState === "transcribing");
@@ -8,6 +9,16 @@
);
let isLoading = $state(false);
let remoteMode = $derived(configStore.config.remote.mode);
let byokApiKey = $derived(configStore.config.remote.byok_api_key);
let authToken = $derived(configStore.config.remote.auth_token);
let cloudConfigured = $derived(
remoteMode === "local" ||
(remoteMode === "byok" && byokApiKey.trim() !== "") ||
(remoteMode === "managed" && authToken.trim() !== "")
);
let errorMessage = $state("");
async function toggleTranscription() {
@@ -20,14 +31,17 @@
} else {
await backendStore.apiPost("/api/start");
}
// Poll status to update UI immediately instead of waiting
// for WebSocket broadcast (which can be delayed or missed)
await backendStore.pollStatus();
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
console.error("Failed to toggle transcription:", msg);
errorMessage = msg;
// Ignore "Already transcribing/not transcribing" -- just sync the state
if (!msg.includes("400")) {
console.error("Failed to toggle transcription:", msg);
errorMessage = msg;
}
} finally {
// Always poll status to sync UI with actual backend state,
// even if the API call failed (e.g. "Already transcribing")
await backendStore.pollStatus();
isLoading = false;
}
}
@@ -91,7 +105,7 @@
<button
class={isTranscribing ? "danger" : "primary"}
onclick={toggleTranscription}
disabled={!isReady || isLoading}
disabled={!isReady || isLoading || !cloudConfigured}
>
{#if isLoading}
...
@@ -113,6 +127,18 @@
{#if errorMessage}
<span class="error-msg">{errorMessage}</span>
{/if}
{#if !cloudConfigured && isReady}
<div class="cloud-warning">
{#if remoteMode === "byok"}
<span>API key required. Get one at
<a href="https://console.deepgram.com" target="_blank" rel="noopener">console.deepgram.com</a>,
then enter it in Settings.</span>
{:else if remoteMode === "managed"}
<span>Login required. Open Settings to log in.</span>
{/if}
</div>
{/if}
</div>
<style>
@@ -122,6 +148,18 @@
margin-left: 8px;
}
.cloud-warning {
font-size: 12px;
color: #ff9800;
margin-left: 8px;
flex: 1;
}
.cloud-warning a {
color: #4fc3f7;
text-decoration: underline;
}
.controls {
display: flex;
align-items: center;

View File

@@ -46,6 +46,14 @@
let managedPassword = $state("");
let autoCheckUpdates = $state(true);
let isCloudMode = $derived(remoteMode === "managed" || remoteMode === "byok");
// Room creation / join state
let shareCode = $state("");
let joinCode = $state("");
let roomCreating = $state(false);
let roomCreateMessage = $state("");
let saving = $state(false);
let saveMessage = $state("");
@@ -266,6 +274,110 @@
}
}
const CAPTION_SERVER = "https://caption.shadowdao.com";
function generateRandomName(): string {
const adjectives = ['swift', 'bright', 'cosmic', 'electric', 'turbo', 'mega', 'ultra', 'super', 'hyper', 'alpha'];
const nouns = ['phoenix', 'dragon', 'tiger', 'falcon', 'comet', 'storm', 'blaze', 'thunder', 'frost', 'nebula'];
const num = Math.floor(Math.random() * 10000);
return `${adjectives[Math.floor(Math.random() * adjectives.length)]}-${nouns[Math.floor(Math.random() * nouns.length)]}-${num}`;
}
function generateRandomPassphrase(): string {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < 16; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
function encodeShareCode(url: string, room: string, passphrase: string): string {
return btoa(JSON.stringify({ url, room, passphrase }));
}
function decodeShareCode(code: string): { url: string; room: string; passphrase: string } | null {
try {
const json = JSON.parse(atob(code.trim()));
if (json.url && json.room && json.passphrase) {
return json;
}
return null;
} catch {
return null;
}
}
async function handleCreateRoom() {
roomCreating = true;
roomCreateMessage = "";
shareCode = "";
const room = generateRandomName();
const passphrase = generateRandomPassphrase();
const serverSendUrl = `${CAPTION_SERVER}/api/send`;
try {
const resp = await fetch(`${CAPTION_SERVER}/api/create-room`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ room, passphrase }),
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({ error: "Request failed" }));
roomCreateMessage = `Error: ${err.error || resp.statusText}`;
return;
}
syncUrl = serverSendUrl;
syncRoom = room;
syncPassphrase = passphrase;
syncEnabled = true;
shareCode = encodeShareCode(serverSendUrl, room, passphrase);
roomCreateMessage = "Room created! Share the code below with others.";
} catch (err) {
roomCreateMessage = `Error: ${err instanceof Error ? err.message : String(err)}`;
} finally {
roomCreating = false;
}
}
function handleJoinRoom() {
const decoded = decodeShareCode(joinCode);
if (!decoded) {
roomCreateMessage = "Invalid share code. Please check and try again.";
return;
}
syncUrl = decoded.url;
syncRoom = decoded.room;
syncPassphrase = decoded.passphrase;
syncEnabled = true;
joinCode = "";
roomCreateMessage = "Room joined! Fields have been auto-filled.";
}
async function handleShareCurrentRoom() {
const code = encodeShareCode(syncUrl, syncRoom, syncPassphrase);
shareCode = code;
try {
await navigator.clipboard.writeText(code);
roomCreateMessage = "Share code copied to clipboard!";
} catch {
roomCreateMessage = "Share code generated. Copy it from the field below.";
}
}
async function copyShareCode() {
try {
await navigator.clipboard.writeText(shareCode);
roomCreateMessage = "Share code copied to clipboard!";
} catch {
roomCreateMessage = "Failed to copy. Please select and copy manually.";
}
}
function handleOverlayClick(e: MouseEvent) {
if ((e.target as HTMLElement).classList.contains("settings-overlay")) {
handleCancel();
@@ -327,7 +439,90 @@
</div>
</section>
<!-- Transcription Settings -->
<!-- Remote Transcription (moved up for cloud-first UX) -->
<section class="settings-section">
<h3>Transcription Mode</h3>
<div class="radio-group">
<label>
<input
type="radio"
name="remote-mode"
value="byok"
bind:group={remoteMode}
/>
Cloud (Deepgram)
</label>
<label>
<input
type="radio"
name="remote-mode"
value="managed"
bind:group={remoteMode}
/>
Managed Service
</label>
<label>
<input
type="radio"
name="remote-mode"
value="local"
bind:group={remoteMode}
/>
Local (Whisper)
</label>
</div>
{#if remoteMode === "byok"}
<div class="field">
<label for="byok-key">Deepgram API Key</label>
<input
id="byok-key"
type="password"
bind:value={byokApiKey}
placeholder="Enter your Deepgram API key"
/>
<p style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">
Get a key at <a href="https://console.deepgram.com" target="_blank" rel="noopener" style="color: var(--accent-blue);">console.deepgram.com</a>
</p>
</div>
{/if}
{#if remoteMode === "managed"}
<div class="field">
<label for="remote-url">Server URL</label>
<input
id="remote-url"
type="url"
bind:value={remoteServerUrl}
placeholder="wss://your-proxy.com"
/>
</div>
<div class="managed-auth">
<div class="field">
<label for="managed-email">Email</label>
<input
id="managed-email"
type="email"
bind:value={managedEmail}
placeholder="email@example.com"
/>
</div>
<div class="field">
<label for="managed-password">Password</label>
<input
id="managed-password"
type="password"
bind:value={managedPassword}
/>
</div>
<div class="auth-buttons">
<button onclick={handleManagedLogin}>Login</button>
<button onclick={handleManagedRegister}>Register</button>
</div>
</div>
{/if}
</section>
{#if !isCloudMode}
<!-- Transcription Settings (local Whisper only) -->
<section class="settings-section">
<h3>Transcription Settings</h3>
<div class="field">
@@ -473,6 +668,7 @@
/>
</div>
</section>
{/if}
<!-- Display Settings -->
<section class="settings-section">
@@ -628,11 +824,11 @@
</div>
</section>
<!-- Server Sync -->
<!-- Server Sync (Shared Captions) -->
<section class="settings-section">
<h3>Server Sync</h3>
<h3>Shared Captions</h3>
<div class="field-row">
<label for="sync-enabled">Enable Server Sync</label>
<label for="sync-enabled">Enable Shared Captions</label>
<input
id="sync-enabled"
type="checkbox"
@@ -640,13 +836,57 @@
/>
</div>
{#if syncEnabled}
<div class="room-actions">
<div class="room-buttons-row">
<button
onclick={handleCreateRoom}
disabled={roomCreating}
class="secondary"
>
{roomCreating ? "Creating..." : "Create Room"}
</button>
<button
onclick={handleShareCurrentRoom}
disabled={!syncUrl.trim() || !syncRoom.trim() || !syncPassphrase.trim()}
class="secondary"
>
Share Current Room
</button>
</div>
<div class="join-row">
<input
type="text"
bind:value={joinCode}
placeholder="Paste share code to join"
class="join-input"
/>
<button onclick={handleJoinRoom} disabled={!joinCode.trim()} class="secondary">
Join
</button>
</div>
</div>
{#if roomCreateMessage}
<p class="room-message" class:error={roomCreateMessage.startsWith("Error")}>{roomCreateMessage}</p>
{/if}
{#if shareCode}
<div class="share-code-box">
<label>Share Code</label>
<div class="share-code-row">
<input type="text" value={shareCode} readonly class="share-code-input" />
<button onclick={copyShareCode} class="secondary">Copy</button>
</div>
</div>
{/if}
<div class="field">
<label for="sync-url">Server URL</label>
<input
id="sync-url"
type="url"
bind:value={syncUrl}
placeholder="http://localhost:3000/api/send"
placeholder="https://caption.shadowdao.com/api/send"
/>
</div>
<div class="field">
@@ -664,90 +904,6 @@
{/if}
</section>
<!-- Remote Transcription -->
<section class="settings-section">
<h3>Remote Transcription</h3>
<div class="radio-group">
<label>
<input
type="radio"
name="remote-mode"
value="local"
bind:group={remoteMode}
/>
Local
</label>
<label>
<input
type="radio"
name="remote-mode"
value="managed"
bind:group={remoteMode}
/>
Managed
</label>
<label>
<input
type="radio"
name="remote-mode"
value="byok"
bind:group={remoteMode}
/>
BYOK (Bring Your Own Key)
</label>
</div>
{#if remoteMode === "managed"}
<div class="field">
<label for="remote-url">Server URL</label>
<input
id="remote-url"
type="url"
bind:value={remoteServerUrl}
placeholder="wss://your-proxy.com"
/>
</div>
{/if}
{#if remoteMode === "byok"}
<div class="field">
<label for="byok-key">Deepgram API Key</label>
<input
id="byok-key"
type="password"
bind:value={byokApiKey}
placeholder="Enter your Deepgram API key"
/>
<p style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">
Get a key at <a href="https://console.deepgram.com" target="_blank" rel="noopener" style="color: var(--accent-blue);">console.deepgram.com</a>
</p>
</div>
{/if}
{#if remoteMode === "managed"}
<div class="managed-auth">
<div class="field">
<label for="managed-email">Email</label>
<input
id="managed-email"
type="email"
bind:value={managedEmail}
placeholder="email@example.com"
/>
</div>
<div class="field">
<label for="managed-password">Password</label>
<input
id="managed-password"
type="password"
bind:value={managedPassword}
/>
</div>
<div class="auth-buttons">
<button onclick={handleManagedLogin}>Login</button>
<button onclick={handleManagedRegister}>Register</button>
</div>
</div>
{/if}
</section>
<!-- Updates -->
<section class="settings-section">
<h3>Updates</h3>
@@ -943,6 +1099,78 @@
color: #f44336;
}
.room-actions {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.room-buttons-row {
display: flex;
gap: 8px;
}
.join-row {
display: flex;
gap: 8px;
}
.join-input {
flex: 1;
}
.room-message {
font-size: 12px;
color: #4CAF50;
margin-bottom: 8px;
}
.room-message.error {
color: #f44336;
}
.share-code-box {
margin-bottom: 12px;
}
.share-code-box label {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: var(--text-secondary);
}
.share-code-row {
display: flex;
gap: 8px;
}
.share-code-input {
flex: 1;
font-size: 11px;
font-family: monospace;
}
.secondary {
background: transparent;
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
}
.secondary:hover {
background: var(--bg-tertiary);
}
.secondary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.danger-btn {
background: transparent;
border: 1px solid var(--accent-red, #f44336);

View File

@@ -107,7 +107,7 @@ function getDefaultConfig(): AppConfig {
},
server_sync: {
enabled: false,
url: "http://localhost:3000/api/send",
url: "",
room: "default",
passphrase: "",
},
@@ -128,7 +128,7 @@ function getDefaultConfig(): AppConfig {
},
web_server: { port: 8080, host: "127.0.0.1" },
remote: {
mode: "local",
mode: "byok",
server_url: "",
auth_token: "",
byok_api_key: "",

View File

@@ -1,7 +1,7 @@
"""Version information for Local Transcription."""
__version__ = "2.0.10"
__version_info__ = (2, 0, 10)
__version__ = "2.0.15"
__version_info__ = (2, 0, 15)
# Version history:
# 1.4.0 - Auto-update feature: