Compare commits
6 Commits
sidecar-v1
...
v2.0.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1210acd07f | ||
|
|
352615c15c | ||
|
|
a3bcc5bee5 | ||
|
|
b91fe876f9 | ||
|
|
7e04d6b4af | ||
|
|
15c4e262b9 |
@@ -41,10 +41,11 @@ jobs:
|
|||||||
sudo apt-get install -y portaudio19-dev
|
sudo apt-get install -y portaudio19-dev
|
||||||
|
|
||||||
- name: Build sidecar (CPU)
|
- name: Build sidecar (CPU)
|
||||||
env:
|
|
||||||
UV_NO_SOURCES: "1"
|
|
||||||
run: |
|
run: |
|
||||||
uv sync
|
uv sync --no-sources
|
||||||
|
# PyPI's default torch on Linux includes CUDA (~800MB).
|
||||||
|
# Replace with CPU-only torch from the dedicated index.
|
||||||
|
uv pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu --force-reinstall
|
||||||
.venv/bin/pyinstaller local-transcription-headless.spec
|
.venv/bin/pyinstaller local-transcription-headless.spec
|
||||||
|
|
||||||
- name: Package sidecar (CPU)
|
- name: Package sidecar (CPU)
|
||||||
|
|||||||
@@ -56,10 +56,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Build sidecar (CPU)
|
- name: Build sidecar (CPU)
|
||||||
shell: powershell
|
shell: powershell
|
||||||
env:
|
|
||||||
UV_NO_SOURCES: "1"
|
|
||||||
run: |
|
run: |
|
||||||
|
$env:UV_NO_SOURCES = "1"
|
||||||
uv sync
|
uv sync
|
||||||
|
# PyPI's default torch includes CUDA. Replace with CPU-only.
|
||||||
|
uv pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu --force-reinstall
|
||||||
.venv\Scripts\pyinstaller.exe local-transcription-headless.spec
|
.venv\Scripts\pyinstaller.exe local-transcription-headless.spec
|
||||||
|
|
||||||
- name: Package sidecar (CPU)
|
- name: Package sidecar (CPU)
|
||||||
|
|||||||
102
.gitea/workflows/cleanup-releases.yml
Normal file
102
.gitea/workflows/cleanup-releases.yml
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
name: Cleanup Old Releases
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
keep_app_releases:
|
||||||
|
description: 'Number of app releases to keep'
|
||||||
|
required: false
|
||||||
|
default: '3'
|
||||||
|
keep_sidecar_releases:
|
||||||
|
description: 'Number of sidecar releases to keep'
|
||||||
|
required: false
|
||||||
|
default: '2'
|
||||||
|
dry_run:
|
||||||
|
description: 'Dry run (show what would be deleted without deleting)'
|
||||||
|
required: false
|
||||||
|
default: 'true'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cleanup:
|
||||||
|
name: Cleanup Old Releases
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Cleanup releases
|
||||||
|
env:
|
||||||
|
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||||
|
run: |
|
||||||
|
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
|
KEEP_APP="${{ inputs.keep_app_releases }}"
|
||||||
|
KEEP_SIDECAR="${{ inputs.keep_sidecar_releases }}"
|
||||||
|
DRY_RUN="${{ inputs.dry_run }}"
|
||||||
|
|
||||||
|
echo "=== Cleanup Configuration ==="
|
||||||
|
echo "Keep app releases: ${KEEP_APP}"
|
||||||
|
echo "Keep sidecar releases: ${KEEP_SIDECAR}"
|
||||||
|
echo "Dry run: ${DRY_RUN}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Fetch all releases
|
||||||
|
ALL_RELEASES=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
|
||||||
|
"${REPO_API}/releases?limit=50")
|
||||||
|
|
||||||
|
# ── App releases (v* tags, not sidecar-v*) ──
|
||||||
|
echo "=== App Releases ==="
|
||||||
|
APP_RELEASES=$(echo "$ALL_RELEASES" | jq -c '[.[] | select(.tag_name | startswith("v")) | select(.tag_name | startswith("sidecar") | not)]')
|
||||||
|
APP_TOTAL=$(echo "$APP_RELEASES" | jq 'length')
|
||||||
|
echo "Found ${APP_TOTAL} app releases, keeping ${KEEP_APP}"
|
||||||
|
|
||||||
|
if [ "$APP_TOTAL" -gt "$KEEP_APP" ]; then
|
||||||
|
echo "$APP_RELEASES" | jq -c ".[$KEEP_APP:][]" | while read -r release; do
|
||||||
|
ID=$(echo "$release" | jq -r '.id')
|
||||||
|
TAG=$(echo "$release" | jq -r '.tag_name')
|
||||||
|
SIZE=$(echo "$release" | jq '[.assets[]?.size // 0] | add // 0')
|
||||||
|
SIZE_MB=$(echo "scale=1; $SIZE / 1048576" | bc 2>/dev/null || echo "?")
|
||||||
|
|
||||||
|
# Protect v1.4.0 (last pre-Tauri release)
|
||||||
|
if [ "$TAG" = "v1.4.0" ]; then
|
||||||
|
echo " PROTECT ${TAG} (${SIZE_MB} MB)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" = "true" ]; then
|
||||||
|
echo " WOULD DELETE ${TAG} (ID: ${ID}, ${SIZE_MB} MB)"
|
||||||
|
else
|
||||||
|
echo " DELETING ${TAG} (ID: ${ID}, ${SIZE_MB} MB)..."
|
||||||
|
curl -s -X DELETE -H "Authorization: token ${BUILD_TOKEN}" \
|
||||||
|
"${REPO_API}/releases/${ID}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo " Nothing to clean up"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Sidecar releases (sidecar-v* tags) ──
|
||||||
|
echo "=== Sidecar Releases ==="
|
||||||
|
SIDECAR_RELEASES=$(echo "$ALL_RELEASES" | jq -c '[.[] | select(.tag_name | startswith("sidecar-v"))]')
|
||||||
|
SIDECAR_TOTAL=$(echo "$SIDECAR_RELEASES" | jq 'length')
|
||||||
|
echo "Found ${SIDECAR_TOTAL} sidecar releases, keeping ${KEEP_SIDECAR}"
|
||||||
|
|
||||||
|
if [ "$SIDECAR_TOTAL" -gt "$KEEP_SIDECAR" ]; then
|
||||||
|
echo "$SIDECAR_RELEASES" | jq -c ".[$KEEP_SIDECAR:][]" | while read -r release; do
|
||||||
|
ID=$(echo "$release" | jq -r '.id')
|
||||||
|
TAG=$(echo "$release" | jq -r '.tag_name')
|
||||||
|
SIZE=$(echo "$release" | jq '[.assets[]?.size // 0] | add // 0')
|
||||||
|
SIZE_MB=$(echo "scale=1; $SIZE / 1048576" | bc 2>/dev/null || echo "?")
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" = "true" ]; then
|
||||||
|
echo " WOULD DELETE ${TAG} (ID: ${ID}, ${SIZE_MB} MB)"
|
||||||
|
else
|
||||||
|
echo " DELETING ${TAG} (ID: ${ID}, ${SIZE_MB} MB)..."
|
||||||
|
curl -s -X DELETE -H "Authorization: token ${BUILD_TOKEN}" \
|
||||||
|
"${REPO_API}/releases/${ID}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo " Nothing to clean up"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Done ==="
|
||||||
@@ -267,6 +267,15 @@ Both workflows require a `BUILD_TOKEN` secret in the repo settings (Gitea API to
|
|||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
### macOS: "App is damaged and can't be opened"
|
||||||
|
macOS Gatekeeper blocks unsigned applications. Since the app is not yet signed with an Apple Developer certificate, you need to remove the quarantine flag before opening:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xattr -cr "/Applications/Local Transcription.app"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open the app normally. You only need to do this once after downloading.
|
||||||
|
|
||||||
### Model Loading Issues
|
### Model Loading Issues
|
||||||
- Models download automatically on first use to `~/.cache/huggingface/`
|
- Models download automatically on first use to `~/.cache/huggingface/`
|
||||||
- First run requires internet connection
|
- First run requires internet connection
|
||||||
|
|||||||
@@ -396,7 +396,14 @@ class AppController:
|
|||||||
try:
|
try:
|
||||||
success = self.transcription_engine.start_recording()
|
success = self.transcription_engine.start_recording()
|
||||||
if not success:
|
if not success:
|
||||||
return False, "Failed to start recording"
|
import logging
|
||||||
|
# Check if there's a recent error in the logger
|
||||||
|
err_detail = getattr(self.transcription_engine, '_last_error', '')
|
||||||
|
msg = f"Failed to start recording"
|
||||||
|
if err_detail:
|
||||||
|
msg += f": {err_detail}"
|
||||||
|
print(f"ERROR: {msg}")
|
||||||
|
return False, msg
|
||||||
|
|
||||||
# Start server sync if enabled
|
# Start server sync if enabled
|
||||||
if self.config.get('server_sync.enabled', False):
|
if self.config.get('server_sync.enabled', False):
|
||||||
|
|||||||
@@ -156,17 +156,30 @@ class DeepgramTranscriptionEngine:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
self._stop_event.clear()
|
self._stop_event.clear()
|
||||||
|
self._ws_connected = threading.Event()
|
||||||
self._is_recording = True
|
self._is_recording = True
|
||||||
|
|
||||||
# Start the asyncio event-loop thread (handles WS send/receive)
|
# Start the asyncio event-loop thread (handles WS send/receive)
|
||||||
self._thread = threading.Thread(target=self._run_event_loop, daemon=True)
|
self._thread = threading.Thread(target=self._run_event_loop, daemon=True)
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
|
|
||||||
|
# Wait for the WebSocket to connect before starting audio capture.
|
||||||
|
# Without this, audio chunks arrive before the WS is open -> broken pipe.
|
||||||
|
if not self._ws_connected.wait(timeout=15):
|
||||||
|
logger.error("Timed out waiting for Deepgram WebSocket connection")
|
||||||
|
print("ERROR: Timed out waiting for Deepgram WebSocket connection")
|
||||||
|
self._last_error = "Timed out connecting to Deepgram"
|
||||||
|
self._is_recording = False
|
||||||
|
self._stop_event.set()
|
||||||
|
return False
|
||||||
|
|
||||||
# Start the audio capture stream
|
# Start the audio capture stream
|
||||||
try:
|
try:
|
||||||
self._start_audio_stream()
|
self._start_audio_stream()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("Failed to open audio stream: %s", exc)
|
logger.error("Failed to open audio stream: %s", exc)
|
||||||
|
print(f"ERROR: Failed to open audio stream: {exc}")
|
||||||
|
self._last_error = f"Audio stream error: {exc}"
|
||||||
self._is_recording = False
|
self._is_recording = False
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
return False
|
return False
|
||||||
@@ -283,6 +296,11 @@ class DeepgramTranscriptionEngine:
|
|||||||
if not await self._managed_handshake():
|
if not await self._managed_handshake():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Signal that the WebSocket is connected and ready
|
||||||
|
logger.info("WebSocket connected to Deepgram")
|
||||||
|
if hasattr(self, '_ws_connected'):
|
||||||
|
self._ws_connected.set()
|
||||||
|
|
||||||
# Run send and receive concurrently
|
# Run send and receive concurrently
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
self._send_loop(),
|
self._send_loop(),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "local-transcription",
|
"name": "local-transcription",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.8",
|
"version": "2.0.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "local-transcription"
|
name = "local-transcription"
|
||||||
version = "2.0.8"
|
version = "2.0.9"
|
||||||
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"
|
||||||
|
|||||||
@@ -71,6 +71,17 @@ pub fn run() {
|
|||||||
sidecar::reset_sidecar,
|
sidecar::reset_sidecar,
|
||||||
write_log,
|
write_log,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"productName": "Local Transcription",
|
"productName": "Local Transcription",
|
||||||
"version": "2.0.8",
|
"version": "2.0.9",
|
||||||
"identifier": "net.anhonesthost.local-transcription",
|
"identifier": "net.anhonesthost.local-transcription",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
@@ -8,9 +8,12 @@
|
|||||||
);
|
);
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
|
|
||||||
|
let errorMessage = $state("");
|
||||||
|
|
||||||
async function toggleTranscription() {
|
async function toggleTranscription() {
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
errorMessage = "";
|
||||||
try {
|
try {
|
||||||
if (isTranscribing) {
|
if (isTranscribing) {
|
||||||
await backendStore.apiPost("/api/stop");
|
await backendStore.apiPost("/api/stop");
|
||||||
@@ -20,8 +23,10 @@
|
|||||||
// Poll status to update UI immediately instead of waiting
|
// Poll status to update UI immediately instead of waiting
|
||||||
// for WebSocket broadcast (which can be delayed or missed)
|
// for WebSocket broadcast (which can be delayed or missed)
|
||||||
await backendStore.pollStatus();
|
await backendStore.pollStatus();
|
||||||
} catch (err) {
|
} catch (err: unknown) {
|
||||||
console.error("Failed to toggle transcription:", err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error("Failed to toggle transcription:", msg);
|
||||||
|
errorMessage = msg;
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
}
|
}
|
||||||
@@ -104,9 +109,19 @@
|
|||||||
<button onclick={saveTranscriptions} disabled={!backendStore.connected}>
|
<button onclick={saveTranscriptions} disabled={!backendStore.connected}>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{#if errorMessage}
|
||||||
|
<span class="error-msg">{errorMessage}</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.error-msg {
|
||||||
|
color: #f44336;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Version information for Local Transcription."""
|
"""Version information for Local Transcription."""
|
||||||
|
|
||||||
__version__ = "2.0.8"
|
__version__ = "2.0.9"
|
||||||
__version_info__ = (2, 0, 8)
|
__version_info__ = (2, 0, 9)
|
||||||
|
|
||||||
# Version history:
|
# Version history:
|
||||||
# 1.4.0 - Auto-update feature:
|
# 1.4.0 - Auto-update feature:
|
||||||
|
|||||||
Reference in New Issue
Block a user