Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1210acd07f | ||
|
|
352615c15c | ||
|
|
a3bcc5bee5 | ||
|
|
b91fe876f9 | ||
|
|
7e04d6b4af | ||
|
|
15c4e262b9 | ||
|
|
2246723220 |
@@ -41,10 +41,11 @@ jobs:
|
||||
sudo apt-get install -y portaudio19-dev
|
||||
|
||||
- name: Build sidecar (CPU)
|
||||
env:
|
||||
UV_NO_SOURCES: "1"
|
||||
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
|
||||
|
||||
- name: Package sidecar (CPU)
|
||||
|
||||
@@ -56,10 +56,11 @@ jobs:
|
||||
|
||||
- name: Build sidecar (CPU)
|
||||
shell: powershell
|
||||
env:
|
||||
UV_NO_SOURCES: "1"
|
||||
run: |
|
||||
$env:UV_NO_SOURCES = "1"
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
### 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
|
||||
- Models download automatically on first use to `~/.cache/huggingface/`
|
||||
- First run requires internet connection
|
||||
|
||||
@@ -396,7 +396,14 @@ class AppController:
|
||||
try:
|
||||
success = self.transcription_engine.start_recording()
|
||||
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
|
||||
if self.config.get('server_sync.enabled', False):
|
||||
|
||||
@@ -156,17 +156,30 @@ class DeepgramTranscriptionEngine:
|
||||
return True
|
||||
|
||||
self._stop_event.clear()
|
||||
self._ws_connected = threading.Event()
|
||||
self._is_recording = True
|
||||
|
||||
# Start the asyncio event-loop thread (handles WS send/receive)
|
||||
self._thread = threading.Thread(target=self._run_event_loop, daemon=True)
|
||||
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
|
||||
try:
|
||||
self._start_audio_stream()
|
||||
except Exception as 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._stop_event.set()
|
||||
return False
|
||||
@@ -283,6 +296,11 @@ class DeepgramTranscriptionEngine:
|
||||
if not await self._managed_handshake():
|
||||
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
|
||||
await asyncio.gather(
|
||||
self._send_loop(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "local-transcription",
|
||||
"private": true,
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "local-transcription"
|
||||
version = "1.0.6"
|
||||
version = "1.0.7"
|
||||
description = "A standalone desktop application for real-time speech-to-text transcription using Whisper models"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "local-transcription"
|
||||
version = "2.0.8"
|
||||
version = "2.0.9"
|
||||
description = "Real-time speech-to-text transcription for streamers"
|
||||
authors = ["Local Transcription Contributors"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -71,6 +71,17 @@ pub fn run() {
|
||||
sidecar::reset_sidecar,
|
||||
write_log,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"productName": "Local Transcription",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.9",
|
||||
"identifier": "net.anhonesthost.local-transcription",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
);
|
||||
let isLoading = $state(false);
|
||||
|
||||
let errorMessage = $state("");
|
||||
|
||||
async function toggleTranscription() {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
errorMessage = "";
|
||||
try {
|
||||
if (isTranscribing) {
|
||||
await backendStore.apiPost("/api/stop");
|
||||
@@ -20,8 +23,10 @@
|
||||
// Poll status to update UI immediately instead of waiting
|
||||
// for WebSocket broadcast (which can be delayed or missed)
|
||||
await backendStore.pollStatus();
|
||||
} catch (err) {
|
||||
console.error("Failed to toggle transcription:", err);
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error("Failed to toggle transcription:", msg);
|
||||
errorMessage = msg;
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
@@ -104,9 +109,19 @@
|
||||
<button onclick={saveTranscriptions} disabled={!backendStore.connected}>
|
||||
Save
|
||||
</button>
|
||||
|
||||
{#if errorMessage}
|
||||
<span class="error-msg">{errorMessage}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.error-msg {
|
||||
color: #f44336;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Version information for Local Transcription."""
|
||||
|
||||
__version__ = "2.0.8"
|
||||
__version_info__ = (2, 0, 8)
|
||||
__version__ = "2.0.9"
|
||||
__version_info__ = (2, 0, 9)
|
||||
|
||||
# Version history:
|
||||
# 1.4.0 - Auto-update feature:
|
||||
|
||||
Reference in New Issue
Block a user