Compare commits

...

16 Commits

Author SHA1 Message Date
Gitea Actions
d263be2ac1 chore: bump sidecar version to 1.0.15 [skip ci] 2026-04-12 17:44:51 +00:00
Developer
1c8c6ad7e8 Fix display user not updating locally until app restart
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 3m12s
Engines now read user.name from the config object at transcription time
instead of caching it at init, so name changes take effect immediately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:40:46 -07:00
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
Gitea Actions
b4c0589b04 chore: bump sidecar version to 1.0.12 [skip ci] 2026-04-11 02:44:12 +00:00
Developer
66c441b17f Revert macOS workflow to pre-signing state
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 1m59s
Remove all signing env vars and setup steps. The local act runner's
keychain interferes with Tauri's auto-detection. Will re-add signing
once Apple Developer verification is complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:41:46 -07:00
Developer
94bc704950 Fix settings save blocking event loop and overwriting config keys
Some checks failed
Tests / Python Backend Tests (push) Successful in 5s
Tests / Frontend Tests (push) Successful in 8s
Tests / Rust Sidecar Tests (push) Has been cancelled
- Run apply_settings in thread pool executor to prevent engine reload
  from blocking the HTTP response (caused "TypeError: Failed to fetch")
- Flatten nested config objects into dot-notation keys before saving
  so partial updates don't wipe out unincluded keys like auth_token

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:40:51 -07:00
Developer
7900d2d9f2 Detect cloud-only sidecar from compute devices (no sidecar rebuild needed)
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 1m57s
Use the existing /api/compute-devices response to determine if only cloud
is available, instead of relying on the backend's is_cloud_only status field.
Hides Local (Whisper) option when the sidecar only supports cloud.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:36:13 -07:00
Developer
e0396df7b0 Use ad-hoc signing when no Apple certificate is configured
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 2m4s
Prevents Tauri from auto-detecting local keychain certificates on the
build machine, which causes SecKeychainItemImport failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:30:44 -07:00
16 changed files with 132 additions and 86 deletions

View File

@@ -39,27 +39,7 @@ jobs:
- name: Install npm dependencies - name: Install npm dependencies
run: npm ci run: npm ci
- name: Setup code signing
env:
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }}
run: |
if [ -n "${APPLE_API_KEY_CONTENT}" ]; then
echo "Setting up notarization API key..."
mkdir -p ~/private_keys
echo "${APPLE_API_KEY_CONTENT}" > ~/private_keys/AuthKey_${APPLE_API_KEY}.p8
else
echo "No signing secrets configured, skipping code signing setup"
fi
- name: Build Tauri app - name: Build Tauri app
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY_PATH: ~/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8
run: npm run tauri build run: npm run tauri build
- name: Upload to release - name: Upload to release
@@ -111,6 +91,3 @@ jobs:
"${REPO_API}/releases/${RELEASE_ID}/assets?name=${encoded_name}") "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encoded_name}")
echo "Upload response: HTTP ${HTTP_CODE}" echo "Upload response: HTTP ${HTTP_CODE}"
done done
- name: Cleanup signing artifacts
run: rm -rf ~/private_keys

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:
try:
original_state_cb(state, message) 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
@@ -212,7 +219,11 @@ class APIServer:
@app.put("/api/config") @app.put("/api/config")
async def update_config(update: ConfigUpdate): async def update_config(update: ConfigUpdate):
engine_reloaded, message = ctrl.apply_settings(update.settings) import asyncio
loop = asyncio.get_event_loop()
engine_reloaded, message = await loop.run_in_executor(
None, ctrl.apply_settings, update.settings
)
return { return {
"status": "ok", "status": "ok",
"message": message, "message": message,
@@ -269,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

@@ -276,7 +276,6 @@ class AppController:
self.current_model_size = model self.current_model_size = model
self.current_device_config = device_config self.current_device_config = device_config
user_name = self.config.get('user.name', 'User')
continuous_mode = self.config.get('transcription.continuous_mode', False) continuous_mode = self.config.get('transcription.continuous_mode', False)
if continuous_mode: if continuous_mode:
@@ -293,7 +292,6 @@ class AppController:
if remote_mode in ('managed', 'byok'): if remote_mode in ('managed', 'byok'):
self.transcription_engine = DeepgramTranscriptionEngine( self.transcription_engine = DeepgramTranscriptionEngine(
config=self.config, config=self.config,
user_name=user_name,
input_device_index=audio_device, input_device_index=audio_device,
) )
self.transcription_engine.set_callbacks( self.transcription_engine.set_callbacks(
@@ -343,7 +341,7 @@ class AppController:
initial_prompt=self.config.get('transcription.initial_prompt', ''), initial_prompt=self.config.get('transcription.initial_prompt', ''),
no_log_file=self.config.get('transcription.no_log_file', True), no_log_file=self.config.get('transcription.no_log_file', True),
input_device_index=audio_device, input_device_index=audio_device,
user_name=user_name, app_config=self.config,
) )
self.transcription_engine.set_callbacks( self.transcription_engine.set_callbacks(
realtime_callback=self._on_realtime_transcription, realtime_callback=self._on_realtime_transcription,
@@ -608,8 +606,17 @@ class AppController:
Returns (engine_reload_needed, message). Returns (engine_reload_needed, message).
""" """
if new_config: if new_config:
for key, value in new_config.items(): # Flatten nested dicts into dot-notation keys so we merge
self.config.set(key, value) # individual values instead of replacing entire sections
# (e.g. remote.mode instead of overwriting all of remote)
def _flatten(d, prefix=""):
for k, v in d.items():
full_key = f"{prefix}{k}" if not prefix else f"{prefix}.{k}"
if isinstance(v, dict):
_flatten(v, full_key)
else:
self.config.set(full_key, v)
_flatten(new_config)
# Update web server display settings # Update web server display settings
if self.web_server: if self.web_server:

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}
try:
print(json.dumps(event), flush=True) 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

@@ -36,18 +36,16 @@ class DeepgramTranscriptionEngine:
# Construction / configuration # Construction / configuration
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
def __init__(self, config, user_name: str = "User", input_device_index: Optional[int] = None): def __init__(self, config, input_device_index: Optional[int] = None):
""" """
Initialise the engine from a :class:`client.config.Config` object. Initialise the engine from a :class:`client.config.Config` object.
Args: Args:
config: Application ``Config`` instance. config: Application ``Config`` instance.
user_name: Display name attached to transcriptions.
input_device_index: Index of the audio input device to use input_device_index: Index of the audio input device to use
(``None`` for the system default). (``None`` for the system default).
""" """
self.config = config self.config = config
self.user_name = user_name
self.input_device_index = input_device_index self.input_device_index = input_device_index
# Mode: 'managed' (proxy) or 'byok' (direct Deepgram) # Mode: 'managed' (proxy) or 'byok' (direct Deepgram)
@@ -320,9 +318,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, {}
@@ -450,7 +452,7 @@ class DeepgramTranscriptionEngine:
text=text, text=text,
is_final=is_final, is_final=is_final,
timestamp=datetime.now(), timestamp=datetime.now(),
user_name=self.user_name, user_name=self.config.get('user.name', 'User'),
) )
if is_final: if is_final:
if self.final_callback: if self.final_callback:
@@ -501,7 +503,7 @@ class DeepgramTranscriptionEngine:
text=transcript, text=transcript,
is_final=is_final, is_final=is_final,
timestamp=datetime.now(), timestamp=datetime.now(),
user_name=self.user_name, user_name=self.config.get('user.name', 'User'),
) )
if is_final: if is_final:
if self.final_callback: if self.final_callback:
@@ -532,10 +534,6 @@ class DeepgramTranscriptionEngine:
pass pass
self._ws = None self._ws = None
def set_user_name(self, user_name: str):
"""Update the user name attached to future transcriptions."""
self.user_name = user_name
def is_recording_active(self) -> bool: def is_recording_active(self) -> bool:
"""Return ``True`` if audio is currently being captured.""" """Return ``True`` if audio is currently being captured."""
return self._is_recording return self._is_recording

View File

@@ -58,8 +58,8 @@ class RealtimeTranscriptionEngine:
no_log_file: bool = True, no_log_file: bool = True,
# Audio device # Audio device
input_device_index: Optional[int] = None, input_device_index: Optional[int] = None,
# User name # App config (for reading user.name at transcription time)
user_name: str = "" app_config=None
): ):
""" """
Initialize RealtimeSTT transcription engine. Initialize RealtimeSTT transcription engine.
@@ -82,7 +82,7 @@ class RealtimeTranscriptionEngine:
initial_prompt: Optional prompt to guide transcription initial_prompt: Optional prompt to guide transcription
no_log_file: Disable RealtimeSTT logging no_log_file: Disable RealtimeSTT logging
input_device_index: Audio input device index input_device_index: Audio input device index
user_name: User name for transcriptions app_config: App Config object for reading user.name dynamically
""" """
self.model = model self.model = model
self.language = language self.language = language
@@ -100,7 +100,7 @@ class RealtimeTranscriptionEngine:
self.enable_realtime = enable_realtime_transcription self.enable_realtime = enable_realtime_transcription
self.realtime_model = realtime_model self.realtime_model = realtime_model
self.realtime_processing_pause = realtime_processing_pause self.realtime_processing_pause = realtime_processing_pause
self.user_name = user_name self.app_config = app_config
# Callbacks # Callbacks
self.realtime_callback: Optional[Callable[[TranscriptionResult], None]] = None self.realtime_callback: Optional[Callable[[TranscriptionResult], None]] = None
@@ -162,6 +162,11 @@ class RealtimeTranscriptionEngine:
self.realtime_callback = realtime_callback self.realtime_callback = realtime_callback
self.final_callback = final_callback self.final_callback = final_callback
def _get_user_name(self) -> str:
if self.app_config:
return self.app_config.get('user.name', '')
return ''
def _on_realtime_transcription(self, text: str): def _on_realtime_transcription(self, text: str):
"""Internal callback for realtime transcriptions.""" """Internal callback for realtime transcriptions."""
if self.realtime_callback and text.strip(): if self.realtime_callback and text.strip():
@@ -169,7 +174,7 @@ class RealtimeTranscriptionEngine:
text=text, text=text,
is_final=False, is_final=False,
timestamp=datetime.now(), timestamp=datetime.now(),
user_name=self.user_name user_name=self._get_user_name()
) )
self.realtime_callback(result) self.realtime_callback(result)
@@ -180,7 +185,7 @@ class RealtimeTranscriptionEngine:
text=text, text=text,
is_final=True, is_final=True,
timestamp=datetime.now(), timestamp=datetime.now(),
user_name=self.user_name user_name=self._get_user_name()
) )
self.final_callback(result) self.final_callback(result)
@@ -406,10 +411,6 @@ class RealtimeTranscriptionEngine:
if self.is_recording: if self.is_recording:
print("VAD settings updated. Restart transcription to apply changes.") print("VAD settings updated. Restart transcription to apply changes.")
def set_user_name(self, user_name: str):
"""Set the user name for transcriptions."""
self.user_name = user_name
def __repr__(self) -> str: def __repr__(self) -> str:
return f"RealtimeTranscriptionEngine(model={self.model}, device={self.device}, running={self.is_recording})" return f"RealtimeTranscriptionEngine(model={self.model}, device={self.device}, running={self.is_recording})"

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

@@ -401,7 +401,6 @@ class MainWindow(QMainWindow):
# Use Deepgram-based remote transcription # Use Deepgram-based remote transcription
self.transcription_engine = DeepgramTranscriptionEngine( self.transcription_engine = DeepgramTranscriptionEngine(
config=self.config, config=self.config,
user_name=user_name,
input_device_index=audio_device input_device_index=audio_device
) )
self.transcription_engine.set_callbacks( self.transcription_engine.set_callbacks(
@@ -431,7 +430,7 @@ class MainWindow(QMainWindow):
initial_prompt=self.config.get('transcription.initial_prompt', ''), initial_prompt=self.config.get('transcription.initial_prompt', ''),
no_log_file=self.config.get('transcription.no_log_file', True), no_log_file=self.config.get('transcription.no_log_file', True),
input_device_index=audio_device, input_device_index=audio_device,
user_name=user_name app_config=self.config
) )
# Set up callbacks for transcription results # Set up callbacks for transcription results

View File

@@ -90,7 +90,7 @@ class TranscriptionCLI:
initial_prompt=self.config.get('transcription.initial_prompt', ''), initial_prompt=self.config.get('transcription.initial_prompt', ''),
no_log_file=True, no_log_file=True,
input_device_index=audio_device, input_device_index=audio_device,
user_name=user_name app_config=self.config
) )
# Set up callbacks # Set up callbacks

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.11" version = "1.0.15"
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,9 +44,13 @@
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(
computeDevices.some(d => d.id === "cloud")
);
// Room creation / join state // Room creation / join state
let shareCode = $state(""); let shareCode = $state("");
@@ -128,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;
}); });
@@ -254,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}`;
} }
} }
@@ -453,7 +481,7 @@
/> />
Managed Service Managed Service
</label> </label>
{#if !backendStore.isCloudOnly} {#if !isCloudOnly}
<label> <label>
<input <input
type="radio" type="radio"
@@ -481,6 +509,15 @@
{/if} {/if}
{#if remoteMode === "managed"} {#if remoteMode === "managed"}
<div class="managed-auth"> <div class="managed-auth">
{#if managedLoggedIn}
<p style="font-size: 13px; margin: 0 0 8px;">
<span style="color: var(--accent-green, #4CAF50);">✓ Logged in</span>
as <strong>{managedEmail}</strong>
</p>
<div class="auth-buttons">
<button onclick={handleManagedLogout}>Log out</button>
</div>
{:else}
<div class="field"> <div class="field">
<label for="managed-email">Email</label> <label for="managed-email">Email</label>
<input <input
@@ -504,6 +541,12 @@
<p style="font-size: 11px; color: var(--text-muted); margin-top: 8px;"> <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> 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> </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: