Compare commits

...

9 Commits

Author SHA1 Message Date
Gitea Actions
023bc0218b chore: bump version to 2.0.20 [skip ci] 2026-04-12 02:12:38 +00:00
Gitea Actions
634506f902 chore: bump sidecar version to 1.0.14 [skip ci] 2026-04-12 01:59:52 +00:00
Developer
8c7f4e8008 Fix sidecar pipe crash on state changes and show logged-in state in settings
All checks were successful
Tests / Python Backend Tests (push) Successful in 14s
Tests / Frontend Tests (push) Successful in 8s
Tests / Rust Sidecar Tests (push) Successful in 2m0s
The state callback in main_headless.py wrote events to stdout
synchronously, so an EINVAL on the Tauri sidecar pipe (Windows) bubbled
up through _set_state and tore down engine init and reload_engine. That
turned PUT /api/config into a "Failed to fetch" for the user. The print
is now pipe-safe and api_server isolates the chained callback so a
future misbehaving listener cannot break the engine state machine.

Settings also now persists remote.email on login and shows a "Logged in
as <email>" indicator with a Log out button when an auth_token is
present, instead of leaving the email/password fields blank on reload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:55:43 -07:00
Developer
b8d718caa6 Add login success/failure feedback message
All checks were successful
Tests / Python Backend Tests (push) Successful in 6s
Tests / Frontend Tests (push) Successful in 7s
Tests / Rust Sidecar Tests (push) Successful in 2m6s
Shows "Logged in successfully!" or "Login failed" after clicking
the managed mode Login button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 20:30:11 -07:00
Gitea Actions
d92005bf95 chore: bump version to 2.0.19 [skip ci] 2026-04-11 03:09:49 +00:00
Gitea Actions
e90d154b83 chore: bump sidecar version to 1.0.13 [skip ci] 2026-04-11 03:09:45 +00:00
Developer
fa749b571d Fix cloud-only detection: check for cloud device presence, not exclusivity
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 2m3s
The compute-devices endpoint always includes "auto" alongside "cloud",
so checking every() never matched. Use some() to detect cloud sidecar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 20:06:50 -07:00
Developer
ef188e1f67 Fix managed mode WebSocket URL when server_url uses https://
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 2m21s
The URL builder was prepending wss:// to the full https:// URL, producing
an invalid wss://https://... URL. Now properly converts https→wss and
http→ws before appending the /ws/transcribe path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 20:03:58 -07:00
Gitea Actions
f7b9695418 chore: bump version to 2.0.18 [skip ci] 2026-04-11 02:49:54 +00:00
11 changed files with 96 additions and 35 deletions

View File

@@ -73,8 +73,15 @@ class APIServer:
original_state_cb = self.controller.on_state_changed original_state_cb = self.controller.on_state_changed
def on_state_changed(state: str, message: str): def on_state_changed(state: str, message: str):
# Isolate the upstream callback so a failure there (e.g. a
# broken stdout pipe in main_headless) cannot propagate into
# _set_state and tear down engine init / reload_engine /
# apply_settings request handling.
if original_state_cb: if original_state_cb:
original_state_cb(state, message) try:
original_state_cb(state, message)
except Exception:
pass
self._broadcast_control({"type": "state_changed", "state": state, "message": message}) self._broadcast_control({"type": "state_changed", "state": state, "message": message})
self.controller.on_state_changed = on_state_changed self.controller.on_state_changed = on_state_changed
@@ -273,6 +280,7 @@ class APIServer:
data = resp.json() data = resp.json()
ctrl.config.set('remote.auth_token', data.get('token', '')) ctrl.config.set('remote.auth_token', data.get('token', ''))
ctrl.config.set('remote.server_url', req.server_url) ctrl.config.set('remote.server_url', req.server_url)
ctrl.config.set('remote.email', req.email)
return {"status": "ok", "token": data.get('token', '')} return {"status": "ok", "token": data.get('token', '')}
else: else:
raise HTTPException(status_code=resp.status_code, detail=resp.text) raise HTTPException(status_code=resp.status_code, detail=resp.text)

View File

@@ -75,10 +75,16 @@ def main():
# Create controller and initialize # Create controller and initialize
controller = AppController(config=config) controller = AppController(config=config)
# Wire a state callback that prints the ready event # Wire a state callback that prints state events for the parent
# process to read. Stdout writes can fail with EINVAL on Windows
# when the parent stops reading the sidecar pipe; swallow those
# so the engine state machine isn't taken down by a logging path.
def on_state_changed(state, message): def on_state_changed(state, message):
event = {"event": "state", "state": state, "message": message} event = {"event": "state", "state": state, "message": message}
print(json.dumps(event), flush=True) try:
print(json.dumps(event), flush=True)
except (OSError, ValueError):
pass
controller.on_state_changed = on_state_changed controller.on_state_changed = on_state_changed

View File

@@ -320,9 +320,13 @@ class DeepgramTranscriptionEngine:
def _build_ws_url_and_headers(self): def _build_ws_url_and_headers(self):
"""Return ``(url, headers)`` depending on the current mode.""" """Return ``(url, headers)`` depending on the current mode."""
if self.mode == "managed": if self.mode == "managed":
# Ensure the server URL uses wss:// and append the path # Convert HTTP(S) URLs to WS(S) for WebSocket connection
url = self.server_url.rstrip("/") url = self.server_url.rstrip("/")
if not url.startswith("ws://") and not url.startswith("wss://"): if url.startswith("https://"):
url = "wss://" + url[len("https://"):]
elif url.startswith("http://"):
url = "ws://" + url[len("http://"):]
elif not url.startswith("ws://") and not url.startswith("wss://"):
url = f"wss://{url}" url = f"wss://{url}"
url = f"{url}/ws/transcribe" url = f"{url}/ws/transcribe"
return url, {} return url, {}

View File

@@ -72,6 +72,7 @@ remote:
mode: byok # local | managed | byok mode: byok # local | managed | byok
server_url: "https://transcribe.shadowdao.com" # Proxy server URL for managed mode server_url: "https://transcribe.shadowdao.com" # Proxy server URL for managed mode
auth_token: "" # JWT stored after login (managed mode) auth_token: "" # JWT stored after login (managed mode)
email: "" # Email of the logged-in managed-mode account (for UI display)
byok_api_key: "" # Deepgram API key for BYOK mode byok_api_key: "" # Deepgram API key for BYOK mode
deepgram_model: nova-2 # Deepgram model to use deepgram_model: nova-2 # Deepgram model to use
language: en-US # Language code language: en-US # Language code

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,11 +44,12 @@
let byokApiKey = $state(""); let byokApiKey = $state("");
let managedEmail = $state(""); let managedEmail = $state("");
let managedPassword = $state(""); let managedPassword = $state("");
let managedLoggedIn = $state(false);
let autoCheckUpdates = $state(true); let autoCheckUpdates = $state(true);
let isCloudMode = $derived(remoteMode === "managed" || remoteMode === "byok"); let isCloudMode = $derived(remoteMode === "managed" || remoteMode === "byok");
let isCloudOnly = $derived( let isCloudOnly = $derived(
computeDevices.length > 0 && computeDevices.every(d => d.id === "cloud") computeDevices.some(d => d.id === "cloud")
); );
// Room creation / join state // Room creation / join state
@@ -131,6 +132,8 @@
remoteMode = cfg.remote.mode; remoteMode = cfg.remote.mode;
remoteServerUrl = cfg.remote.server_url; remoteServerUrl = cfg.remote.server_url;
byokApiKey = cfg.remote.byok_api_key ?? ""; byokApiKey = cfg.remote.byok_api_key ?? "";
managedEmail = cfg.remote.email ?? "";
managedLoggedIn = !!(cfg.remote.auth_token && cfg.remote.email);
autoCheckUpdates = cfg.updates.auto_check; autoCheckUpdates = cfg.updates.auto_check;
}); });
@@ -257,15 +260,37 @@
const MANAGED_SERVER_URL = "https://transcribe.shadowdao.com"; const MANAGED_SERVER_URL = "https://transcribe.shadowdao.com";
let loginMessage = $state("");
async function handleManagedLogin() { async function handleManagedLogin() {
loginMessage = "";
try { try {
await backendStore.apiPost("/api/login", { await backendStore.apiPost("/api/login", {
email: managedEmail, email: managedEmail,
password: managedPassword, password: managedPassword,
server_url: remoteServerUrl || MANAGED_SERVER_URL, server_url: remoteServerUrl || MANAGED_SERVER_URL,
}); });
loginMessage = "Logged in successfully!";
managedPassword = "";
managedLoggedIn = true;
await configStore.fetchConfig();
} catch (err) { } catch (err) {
console.error("Login failed:", err); console.error("Login failed:", err);
loginMessage = "Login failed. Check your email and password.";
}
}
async function handleManagedLogout() {
try {
await configStore.updateConfig({
remote: { auth_token: "", email: "" },
});
managedLoggedIn = false;
managedPassword = "";
loginMessage = "";
} catch (err) {
console.error("Logout failed:", err);
loginMessage = `Error: ${err}`;
} }
} }
@@ -484,29 +509,44 @@
{/if} {/if}
{#if remoteMode === "managed"} {#if remoteMode === "managed"}
<div class="managed-auth"> <div class="managed-auth">
<div class="field"> {#if managedLoggedIn}
<label for="managed-email">Email</label> <p style="font-size: 13px; margin: 0 0 8px;">
<input <span style="color: var(--accent-green, #4CAF50);">✓ Logged in</span>
id="managed-email" as <strong>{managedEmail}</strong>
type="email" </p>
bind:value={managedEmail} <div class="auth-buttons">
placeholder="email@example.com" <button onclick={handleManagedLogout}>Log out</button>
/> </div>
</div> {:else}
<div class="field"> <div class="field">
<label for="managed-password">Password</label> <label for="managed-email">Email</label>
<input <input
id="managed-password" id="managed-email"
type="password" type="email"
bind:value={managedPassword} bind:value={managedEmail}
/> placeholder="email@example.com"
</div> />
<div class="auth-buttons"> </div>
<button onclick={handleManagedLogin}>Login</button> <div class="field">
</div> <label for="managed-password">Password</label>
<p style="font-size: 11px; color: var(--text-muted); margin-top: 8px;"> <input
Don't have an account? <a href="https://transcribe.shadowdao.com/register.html" target="_blank" rel="noopener" style="color: var(--accent-blue);">Sign up here</a> id="managed-password"
</p> type="password"
bind:value={managedPassword}
/>
</div>
<div class="auth-buttons">
<button onclick={handleManagedLogin}>Login</button>
</div>
<p style="font-size: 11px; color: var(--text-muted); margin-top: 8px;">
Don't have an account? <a href="https://transcribe.shadowdao.com/register.html" target="_blank" rel="noopener" style="color: var(--accent-blue);">Sign up here</a>
</p>
{/if}
{#if loginMessage}
<p style="font-size: 12px; margin-top: 6px; color: {loginMessage.startsWith('Logged') ? 'var(--accent-green, #4CAF50)' : 'var(--accent-red, #f44336)'};">
{loginMessage}
</p>
{/if}
</div> </div>
{/if} {/if}
</section> </section>

View File

@@ -65,6 +65,7 @@ export interface AppConfig {
mode: string; mode: string;
server_url: string; server_url: string;
auth_token: string; auth_token: string;
email: string;
byok_api_key: string; byok_api_key: string;
deepgram_model: string; deepgram_model: string;
language: string; language: string;
@@ -131,6 +132,7 @@ function getDefaultConfig(): AppConfig {
mode: "byok", mode: "byok",
server_url: "", server_url: "",
auth_token: "", auth_token: "",
email: "",
byok_api_key: "", byok_api_key: "",
deepgram_model: "nova-2", deepgram_model: "nova-2",
language: "en-US", language: "en-US",

View File

@@ -1,7 +1,7 @@
"""Version information for Local Transcription.""" """Version information for Local Transcription."""
__version__ = "2.0.17" __version__ = "2.0.20"
__version_info__ = (2, 0, 17) __version_info__ = (2, 0, 20)
# Version history: # Version history:
# 1.4.0 - Auto-update feature: # 1.4.0 - Auto-update feature: