Compare commits

..

11 Commits

Author SHA1 Message Date
Gitea Actions
36b4f7dad5 chore: bump version to 2.0.1 [skip ci] 2026-04-07 15:58:01 +00:00
Developer
1ecb23b83f Bump to v2.0.0 — cross-platform Tauri rewrite
All checks were successful
Release / Run Tests (push) Successful in 9s
Tests / Python Backend Tests (push) Successful in 5s
Tests / Frontend Tests (push) Successful in 6s
Tests / Rust Sidecar Tests (push) Successful in 2m7s
Release / Bump version and tag (push) Successful in 4s
Major version bump reflecting the architecture change from PySide6/Qt
to Tauri v2 + Svelte 5 with cross-platform support for Windows,
macOS, and Linux.

Key changes since v1.4.0:
- Tauri v2 native desktop shell replacing PySide6/Qt
- Svelte 5 reactive frontend
- Headless Python backend as a downloadable sidecar
- Deepgram cloud transcription (managed + BYOK)
- Gitea CI/CD with per-OS builds and automated releases
- Sidecar auto-update checking on startup
- 63-test suite (Python + Svelte + Rust)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:55:25 -07:00
Developer
4b88871a9b Add sidecar update check on startup
Some checks failed
Release / Run Tests (push) Successful in 11s
Tests / Python Backend Tests (push) Successful in 5s
Tests / Frontend Tests (push) Successful in 7s
Release / Bump version and tag (push) Has been cancelled
Tests / Rust Sidecar Tests (push) Has been cancelled
On launch, after confirming the sidecar is installed, the app now
checks for a newer sidecar version via the Gitea API. If an update
is available, shows a prompt with "Update Now" or "Skip":

- Update Now: shows the SidecarSetup download screen
- Skip: launches the existing sidecar version

The update check is non-blocking -- if it fails (no internet, API
error), the app silently proceeds with the current version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:54:18 -07:00
Gitea Actions
0ae48a67d5 chore: bump version to 1.4.21 [skip ci] 2026-04-07 15:51:32 +00:00
Developer
924cae6c75 Fix double-prefix sidecar dir and stale PID lock on startup
All checks were successful
Release / Run Tests (push) Successful in 29s
Tests / Python Backend Tests (push) Successful in 8s
Tests / Frontend Tests (push) Successful in 10s
Tests / Rust Sidecar Tests (push) Successful in 4m7s
Release / Bump version and tag (push) Successful in 4s
Two bugs preventing sidecar from starting:

1. Directory was "sidecar-sidecar-v1.0.3" (double prefix) because
   sidecar_dir_for_version() prepended "sidecar-" to a version that
   already contained it. Now uses the tag directly as the dir name.

2. After a crash, the Python InstanceLock PID file at
   ~/.local-transcription/app.lock remained, blocking the next launch
   with "Another instance is already running". Now clears the stale
   lock file before spawning the sidecar.

Also fixed cleanup_old_versions() and tests to match the corrected
directory naming.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:46:31 -07:00
Gitea Actions
5139936e18 chore: bump version to 1.4.20 [skip ci] 2026-04-07 15:44:54 +00:00
Developer
47724f1ac0 Capture sidecar stderr to sidecar.log for crash debugging
All checks were successful
Release / Run Tests (push) Successful in 15s
Tests / Python Backend Tests (push) Successful in 7s
Tests / Frontend Tests (push) Successful in 10s
Tests / Rust Sidecar Tests (push) Successful in 2m30s
Release / Bump version and tag (push) Successful in 12s
When the sidecar process exits before sending the ready event, the
error message now includes the last 10 lines of stderr. Stderr is
captured in a background thread and written to sidecar.log in the
app data directory.

This helps diagnose why the PyInstaller sidecar fails to start
(missing DLLs, import errors, permission issues, etc.).

Log location: %APPDATA%\net.anhonesthost.local-transcription\sidecar.log

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:41:40 -07:00
Developer
3b204be37e Add automatic cleanup of old releases to save storage
All checks were successful
Tests / Python Backend Tests (push) Successful in 13s
Tests / Frontend Tests (push) Successful in 23s
Tests / Rust Sidecar Tests (push) Successful in 2m8s
- App releases: keeps latest 3 + v1.4.0 (last pre-Tauri version),
  deletes older releases and their tags
- Sidecar releases: keeps latest 2, deletes older releases and tags
  (sidecars are large, ~500MB-2GB each)

Cleanup runs after creating new releases, before triggering builds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:10:55 -07:00
Developer
4c02a48135 Fix CI: use uv for test venv, gate builds on tests, reduce build triggers
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 2m7s
- test.yml: use uv venv instead of pip --break-system-packages
- release.yml: inline test job that must pass before version bump;
  only triggers on source file changes (src/, src-tauri/, package.json)
- sidecar-release.yml: inline Python test job that must pass before
  sidecar version bump
- Both coordinators use `needs: test` so builds never start if tests fail

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:05:49 -07:00
Gitea Actions
997e97c19a chore: bump version to 1.4.19 [skip ci] 2026-04-07 15:00:21 +00:00
Developer
6ca8fc41b2 Fix sidecar release conflict: stop modifying version.py
Some checks failed
Release / Bump version and tag (push) Successful in 4s
Tests / Python Backend Tests (push) Failing after 3s
Tests / Frontend Tests (push) Successful in 7s
Tests / Rust Sidecar Tests (push) Successful in 2m21s
Both release.yml and sidecar-release.yml were updating version.py,
causing merge conflicts when both ran on the same push. Now:
- release.yml (app) owns: package.json, tauri.conf.json, Cargo.toml, version.py
- sidecar-release.yml owns: pyproject.toml only

Also deleted the stale sidecar-v1.0.4 tag that failed to push.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:00:09 -07:00
9 changed files with 265 additions and 25 deletions

View File

@@ -3,11 +3,45 @@ name: Release
on:
push:
branches: [main]
paths:
- 'src/**'
- 'src-tauri/**'
- 'package.json'
- 'vite.config.ts'
- 'index.html'
jobs:
test:
name: Run Tests
if: "!contains(github.event.head_commit.message, '[skip ci]')"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install npm deps
run: npm ci
- name: Frontend tests
run: npx vitest run
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Python tests
run: |
uv venv .testvenv
VIRTUAL_ENV=.testvenv uv pip install pytest httpx pytest-asyncio anyio fastapi pydantic pyyaml uvicorn requests
.testvenv/bin/python -m pytest backend/tests/ client/tests/ -v --tb=short
bump-version:
name: Bump version and tag
if: "!contains(github.event.head_commit.message, '[skip ci]')"
needs: test
runs-on: ubuntu-latest
outputs:
new_version: ${{ steps.bump.outputs.new_version }}
@@ -89,3 +123,45 @@ jobs:
"${REPO_API}/actions/workflows/${workflow}/dispatches")
echo " -> HTTP ${HTTP_CODE}"
done
- name: Clean up old app releases
env:
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
run: |
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
KEEP=3
PROTECT_TAG="v1.4.0"
echo "Cleaning up old app releases (keeping latest ${KEEP} + ${PROTECT_TAG})..."
# Get all app releases (v* tags, not sidecar-v*)
RELEASES=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
"${REPO_API}/releases?limit=50" | jq -c '[.[] | select(.tag_name | startswith("v")) | select(.tag_name | startswith("sidecar") | not)]')
TOTAL=$(echo "$RELEASES" | jq 'length')
echo "Found ${TOTAL} app releases"
if [ "$TOTAL" -le "$KEEP" ]; then
echo "Nothing to clean up"
exit 0
fi
# Skip the newest KEEP releases, delete the rest (except protected)
echo "$RELEASES" | jq -c ".[$KEEP:][]" | while read -r release; do
ID=$(echo "$release" | jq -r '.id')
TAG=$(echo "$release" | jq -r '.tag_name')
if [ "$TAG" = "$PROTECT_TAG" ]; then
echo " Protecting ${TAG}"
continue
fi
echo " Deleting release ${TAG} (ID: ${ID})..."
curl -s -X DELETE -H "Authorization: token ${BUILD_TOKEN}" \
"${REPO_API}/releases/${ID}"
# Also delete the tag
curl -s -X DELETE -H "Authorization: token ${BUILD_TOKEN}" \
"${REPO_API}/tags/${TAG}"
done
echo "Cleanup complete"

View File

@@ -12,8 +12,27 @@ on:
workflow_dispatch:
jobs:
test:
name: Run Tests
if: "!contains(github.event.head_commit.message, '[skip ci]')"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Python tests
run: |
uv venv .testvenv
VIRTUAL_ENV=.testvenv uv pip install pytest httpx pytest-asyncio anyio fastapi pydantic pyyaml uvicorn requests
.testvenv/bin/python -m pytest backend/tests/ client/tests/ -v --tb=short
bump-sidecar-version:
name: Bump sidecar version and tag
needs: test
if: "!contains(github.event.head_commit.message, '[skip ci]')"
runs-on: ubuntu-latest
outputs:
@@ -62,8 +81,6 @@ jobs:
echo "New sidecar version: ${NEW_VERSION}"
sed -i "s/^version = \"${CURRENT}\"/version = \"${NEW_VERSION}\"/" pyproject.toml
sed -i "s/__version__ = \"${CURRENT}\"/__version__ = \"${NEW_VERSION}\"/" version.py
sed -i "s/__version_info__ = .*/__version_info__ = (${MAJOR}, ${MINOR}, ${NEW_PATCH})/" version.py
echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT
echo "tag=sidecar-v${NEW_VERSION}" >> $GITHUB_OUTPUT
@@ -75,7 +92,7 @@ jobs:
run: |
NEW_VERSION="${{ steps.bump.outputs.version }}"
TAG="${{ steps.bump.outputs.tag }}"
git add pyproject.toml version.py
git add pyproject.toml
git commit -m "chore: bump sidecar version to ${NEW_VERSION} [skip ci]"
git tag "${TAG}"
@@ -118,3 +135,40 @@ jobs:
"${REPO_API}/actions/workflows/${workflow}/dispatches")
echo " -> HTTP ${HTTP_CODE}"
done
- name: Clean up old sidecar releases
if: steps.check_changes.outputs.has_changes == 'true'
env:
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
run: |
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
KEEP=2
echo "Cleaning up old sidecar releases (keeping latest ${KEEP})..."
# Get all sidecar releases (sidecar-v* tags)
RELEASES=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
"${REPO_API}/releases?limit=50" | jq -c '[.[] | select(.tag_name | startswith("sidecar-v"))]')
TOTAL=$(echo "$RELEASES" | jq 'length')
echo "Found ${TOTAL} sidecar releases"
if [ "$TOTAL" -le "$KEEP" ]; then
echo "Nothing to clean up"
exit 0
fi
# Skip the newest KEEP releases, delete the rest
echo "$RELEASES" | jq -c ".[$KEEP:][]" | while read -r release; do
ID=$(echo "$release" | jq -r '.id')
TAG=$(echo "$release" | jq -r '.tag_name')
echo " Deleting sidecar release ${TAG} (ID: ${ID})..."
curl -s -X DELETE -H "Authorization: token ${BUILD_TOKEN}" \
"${REPO_API}/releases/${ID}"
# Also delete the tag
curl -s -X DELETE -H "Authorization: token ${BUILD_TOKEN}" \
"${REPO_API}/tags/${TAG}"
done
echo "Cleanup complete"

View File

@@ -5,6 +5,7 @@ on:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
python-tests:
@@ -13,12 +14,20 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install test dependencies
- name: Install uv
run: |
pip install --break-system-packages pytest httpx pytest-asyncio anyio fastapi pydantic pyyaml uvicorn requests
if command -v uv &> /dev/null; then
echo "uv already installed: $(uv --version)"
else
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
fi
- name: Run pytest
run: python3 -m pytest backend/tests/ client/tests/ -v --tb=short
run: |
uv venv .testvenv
VIRTUAL_ENV=.testvenv uv pip install pytest httpx pytest-asyncio anyio fastapi pydantic pyyaml uvicorn requests
.testvenv/bin/python -m pytest backend/tests/ client/tests/ -v --tb=short
frontend-tests:
name: Frontend Tests

View File

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

View File

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

View File

@@ -54,7 +54,8 @@ fn read_installed_version() -> Option<String> {
}
fn sidecar_dir_for_version(version: &str) -> PathBuf {
data_dir().join(format!("sidecar-{version}"))
// version is the full tag name, e.g. "sidecar-v1.0.3" -- use it directly
data_dir().join(version)
}
fn binary_path_for_version(version: &str) -> PathBuf {
@@ -371,12 +372,12 @@ fn extract_zip(zip_path: &std::path::Path, dest: &std::path::Path) -> Result<(),
fn cleanup_old_versions(current_version: &str) {
let data = data_dir();
let current_dir_name = format!("sidecar-{current_version}");
// current_version is already the full tag, e.g. "sidecar-v1.0.3"
if let Ok(entries) = std::fs::read_dir(data) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("sidecar-v") // e.g. sidecar-v1.0.1
&& name != current_dir_name
if name.starts_with("sidecar-")
&& name != current_version
&& entry.path().is_dir()
{
let _ = std::fs::remove_dir_all(entry.path());
@@ -433,6 +434,20 @@ impl SidecarManager {
.ok_or_else(|| "Sidecar running but port unknown".into());
}
// Clear stale PID lock from a previous crash so the sidecar can start.
// The Python InstanceLock writes to ~/.local-transcription/app.lock
if let Ok(home) = std::env::var("USERPROFILE")
.or_else(|_| std::env::var("HOME"))
{
let lock_file = PathBuf::from(home)
.join(".local-transcription")
.join("app.lock");
if lock_file.exists() {
eprintln!("[sidecar] Removing stale lock file: {}", lock_file.display());
let _ = std::fs::remove_file(&lock_file);
}
}
let is_dev = cfg!(debug_assertions)
|| std::env::var("LOCAL_TRANSCRIPTION_DEV")
.map(|v| v == "1")
@@ -463,11 +478,63 @@ impl SidecarManager {
.take()
.ok_or("Failed to capture sidecar stdout")?;
let port = Self::wait_for_ready(stdout)?;
// Capture stderr in a background thread so we can log it
let stderr = child
.stderr
.take()
.ok_or("Failed to capture sidecar stderr")?;
self.child = Some(child);
self.port = Some(port);
Ok(port)
let log_dir = DIRS.get().map(|d| d.data_dir.clone());
std::thread::spawn(move || {
use std::io::BufRead;
let reader = std::io::BufReader::new(stderr);
let mut log_file = log_dir.and_then(|d| {
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(d.join("sidecar.log"))
.ok()
});
for line in reader.lines() {
if let Ok(line) = line {
eprintln!("[sidecar-stderr] {}", line);
if let Some(ref mut f) = log_file {
use std::io::Write;
let _ = writeln!(f, "{}", line);
}
}
}
});
match Self::wait_for_ready(stdout) {
Ok(port) => {
self.child = Some(child);
self.port = Some(port);
Ok(port)
}
Err(e) => {
// Kill the child if ready failed
let _ = child.kill();
let _ = child.wait();
// Read the sidecar.log for context
let log_hint = DIRS
.get()
.and_then(|d| std::fs::read_to_string(d.data_dir.join("sidecar.log")).ok())
.and_then(|s| {
let lines: Vec<&str> = s.lines().collect();
let tail: Vec<&str> = lines.iter().rev().take(10).rev().cloned().collect();
if tail.is_empty() { None } else { Some(tail.join("\n")) }
})
.unwrap_or_default();
if log_hint.is_empty() {
Err(e)
} else {
Err(format!("{e}\n\nSidecar stderr (last 10 lines):\n{log_hint}"))
}
}
}
}
/// Stop the sidecar process if running.
@@ -739,7 +806,7 @@ mod tests {
fn sidecar_dir_for_version_contains_version() {
let data = ensure_dirs_initialised();
let dir = sidecar_dir_for_version("sidecar-v1.2.3");
assert_eq!(dir, data.join("sidecar-sidecar-v1.2.3"));
assert_eq!(dir, data.join("sidecar-v1.2.3"));
}
#[test]
@@ -784,9 +851,8 @@ mod tests {
std::fs::create_dir_all(data.join(d)).unwrap();
}
// cleanup_old_versions builds `current_dir_name = "sidecar-{version}"`.
// Passing "v1.0.2" produces "sidecar-v1.0.2" which matches our dir name.
cleanup_old_versions("v1.0.2");
// current_version is the full tag, e.g. "sidecar-v1.0.2"
cleanup_old_versions("sidecar-v1.0.2");
assert!(
!data.join("sidecar-v1.0.0").exists(),

View File

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

View File

@@ -9,11 +9,12 @@
import { backendStore } from "$lib/stores/backend";
import { configStore } from "$lib/stores/config";
type SidecarState = "checking" | "needs_setup" | "starting" | "connected";
type SidecarState = "checking" | "needs_setup" | "update_available" | "starting" | "connected";
let showSettings = $state(false);
let sidecarState = $state<SidecarState>("checking");
let debugLog = $state("");
let availableUpdate = $state("");
let obsDisplayUrl = $derived(backendStore.obsUrl);
let syncDisplayUrl = $derived(backendStore.syncUrl);
@@ -53,6 +54,20 @@
return;
}
// Check for sidecar updates before launching
try {
log("Checking for sidecar updates...");
const update = await invoke<string | null>("check_sidecar_update");
if (update) {
log(`Sidecar update available: ${update}`);
availableUpdate = update;
sidecarState = "update_available";
return;
}
} catch (err) {
log(`Update check failed (non-fatal): ${err}`);
}
await launchSidecar();
} catch (err) {
// Not running in Tauri (browser dev mode) - skip sidecar check
@@ -118,6 +133,26 @@
{:else if sidecarState === "needs_setup"}
<SidecarSetup onComplete={onSidecarReady} />
{:else if sidecarState === "update_available"}
<div class="connecting-overlay" style="background:#1e1e1e;color:#e0e0e0;display:flex;align-items:center;justify-content:center;height:100%;width:100%;">
<div class="connecting-content" style="text-align:center;max-width:400px;">
<h2 style="font-size:20px;margin:0 0 12px;">Sidecar Update Available</h2>
<p style="color:#a0a0a0;font-size:14px;margin:0 0 20px;">
A new version of the transcription engine is available ({availableUpdate}).
</p>
<div style="display:flex;gap:10px;justify-content:center;">
<button
style="padding:8px 20px;border:1px solid #555;border-radius:6px;background:transparent;color:#e0e0e0;cursor:pointer;"
onclick={() => launchSidecar()}
>Skip</button>
<button
style="padding:8px 20px;border:none;border-radius:6px;background:#4CAF50;color:white;cursor:pointer;font-weight:500;"
onclick={() => { sidecarState = "needs_setup"; }}
>Update Now</button>
</div>
</div>
</div>
{:else if !isConnected}
<div class="connecting-overlay" style="background:#1e1e1e;color:#e0e0e0;display:flex;align-items:center;justify-content:center;height:100%;width:100%;">
<div class="connecting-content" style="text-align:center;">

View File

@@ -1,7 +1,7 @@
"""Version information for Local Transcription."""
__version__ = "1.4.18"
__version_info__ = (1, 4, 18)
__version__ = "2.0.1"
__version_info__ = (2, 0, 1)
# Version history:
# 1.4.0 - Auto-update feature: