Compare commits
23 Commits
sidecar-v1
...
sidecar-v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8c79be094 | ||
|
|
2811f5bb9c | ||
|
|
30127d68e7 | ||
|
|
ae61c8c75a | ||
|
|
2654200fe9 | ||
|
|
cae0c0b265 | ||
|
|
91b27ac22e | ||
|
|
1210acd07f | ||
|
|
352615c15c | ||
|
|
a3bcc5bee5 | ||
|
|
b91fe876f9 | ||
|
|
7e04d6b4af | ||
|
|
15c4e262b9 | ||
|
|
2246723220 | ||
|
|
1c586738f3 | ||
|
|
fb02a24334 | ||
|
|
ce64cacc5e | ||
|
|
14a7ca3b30 | ||
|
|
5b7387f9c6 | ||
|
|
293362baa1 | ||
|
|
41f50dedec | ||
|
|
d8b7811153 | ||
|
|
ec8922672c |
@@ -40,26 +40,17 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y portaudio19-dev
|
||||
|
||||
- name: Build sidecar (CUDA)
|
||||
run: |
|
||||
uv sync --frozen || uv sync
|
||||
uv run pyinstaller local-transcription-headless.spec
|
||||
|
||||
- name: Package sidecar (CUDA)
|
||||
run: |
|
||||
cd dist/local-transcription-backend && zip -r ../../sidecar-linux-x86_64-cuda.zip .
|
||||
|
||||
- name: Build sidecar (CPU)
|
||||
run: |
|
||||
rm -rf dist/local-transcription-backend build/
|
||||
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
|
||||
# Run pyinstaller directly from venv to prevent uv run from
|
||||
# re-resolving torch back to the CUDA version via pyproject.toml sources
|
||||
.venv/bin/pyinstaller local-transcription-headless.spec
|
||||
|
||||
- name: Package sidecar (CPU)
|
||||
run: |
|
||||
cd dist/local-transcription-backend && zip -r ../../sidecar-linux-x86_64-cpu.zip .
|
||||
cd dist/local-transcription-backend && zip -9 -r ../../sidecar-linux-x86_64-cpu.zip .
|
||||
|
||||
- name: Upload to sidecar release
|
||||
env:
|
||||
|
||||
@@ -54,29 +54,19 @@ jobs:
|
||||
choco install 7zip -y
|
||||
}
|
||||
|
||||
- name: Build sidecar (CUDA)
|
||||
shell: powershell
|
||||
run: |
|
||||
uv sync --frozen
|
||||
if ($LASTEXITCODE -ne 0) { uv sync }
|
||||
uv run pyinstaller local-transcription-headless.spec
|
||||
|
||||
- name: Package sidecar (CUDA)
|
||||
shell: powershell
|
||||
run: |
|
||||
7z a -tzip -mx=5 sidecar-windows-x86_64-cuda.zip .\dist\local-transcription-backend\*
|
||||
|
||||
- name: Build sidecar (CPU)
|
||||
shell: powershell
|
||||
run: |
|
||||
Remove-Item -Recurse -Force dist\local-transcription-backend, build -ErrorAction SilentlyContinue
|
||||
$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)
|
||||
shell: powershell
|
||||
run: |
|
||||
7z a -tzip -mx=5 sidecar-windows-x86_64-cpu.zip .\dist\local-transcription-backend\*
|
||||
7z a -tzip -mx=9 sidecar-windows-x86_64-cpu.zip .\dist\local-transcription-backend\*
|
||||
|
||||
- name: Upload to sidecar release
|
||||
shell: powershell
|
||||
|
||||
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 ==="
|
||||
@@ -36,9 +36,6 @@ jobs:
|
||||
name: Bump version and tag
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
new_version: ${{ steps.bump.outputs.new_version }}
|
||||
tag: ${{ steps.bump.outputs.tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -50,7 +47,6 @@ jobs:
|
||||
git config user.email "actions@gitea.local"
|
||||
|
||||
- name: Bump patch version
|
||||
id: bump
|
||||
run: |
|
||||
CURRENT=$(grep '"version"' package.json | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/')
|
||||
echo "Current version: ${CURRENT}"
|
||||
@@ -68,35 +64,34 @@ jobs:
|
||||
sed -i "s/__version__ = \"${CURRENT}\"/__version__ = \"${NEW_VERSION}\"/" version.py
|
||||
sed -i "s/__version_info__ = .*/__version_info__ = (${MAJOR}, ${MINOR}, ${NEW_PATCH})/" version.py
|
||||
|
||||
echo "new_version=${NEW_VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "tag=v${NEW_VERSION}" >> $GITHUB_OUTPUT
|
||||
# Write to env file instead of step outputs (avoids act runner bug)
|
||||
echo "NEW_VERSION=${NEW_VERSION}" >> $GITHUB_ENV
|
||||
echo "RELEASE_TAG=v${NEW_VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Commit and tag
|
||||
env:
|
||||
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||
run: |
|
||||
NEW_VERSION="${{ steps.bump.outputs.new_version }}"
|
||||
git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml version.py
|
||||
git commit -m "chore: bump version to ${NEW_VERSION} [skip ci]"
|
||||
git tag "v${NEW_VERSION}"
|
||||
git tag "${RELEASE_TAG}"
|
||||
|
||||
REMOTE_URL=$(git remote get-url origin | sed "s|://|://gitea-actions:${BUILD_TOKEN}@|")
|
||||
git pull --rebase "${REMOTE_URL}" main || true
|
||||
git push "${REMOTE_URL}" HEAD:main
|
||||
git push "${REMOTE_URL}" "v${NEW_VERSION}"
|
||||
git push "${REMOTE_URL}" "${RELEASE_TAG}"
|
||||
|
||||
- name: Create Gitea release
|
||||
env:
|
||||
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||
run: |
|
||||
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||
TAG="${{ steps.bump.outputs.tag }}"
|
||||
RELEASE_NAME="Local Transcription ${TAG}"
|
||||
RELEASE_NAME="Local Transcription ${RELEASE_TAG}"
|
||||
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${BUILD_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"${TAG}\", \"name\": \"${RELEASE_NAME}\", \"body\": \"Automated build.\", \"draft\": false, \"prerelease\": false}" \
|
||||
-d "{\"tag_name\": \"${RELEASE_TAG}\", \"name\": \"${RELEASE_NAME}\", \"body\": \"Automated build.\", \"draft\": false, \"prerelease\": false}" \
|
||||
"${REPO_API}/releases"
|
||||
echo "Created release: ${RELEASE_NAME}"
|
||||
|
||||
@@ -105,18 +100,14 @@ jobs:
|
||||
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||
run: |
|
||||
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||
TAG="${{ steps.bump.outputs.tag }}"
|
||||
|
||||
for workflow in build-app-linux.yml build-app-windows.yml build-app-macos.yml; do
|
||||
echo "Dispatching ${workflow} for ${TAG}..."
|
||||
echo "Dispatching ${workflow} for ${RELEASE_TAG}..."
|
||||
HTTP_CODE=$(curl -s -w "%{http_code}" -o /tmp/dispatch_resp.txt -X POST \
|
||||
-H "Authorization: token ${BUILD_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"ref\": \"main\", \"inputs\": {\"tag\": \"${TAG}\"}}" \
|
||||
-d "{\"ref\": \"main\", \"inputs\": {\"tag\": \"${RELEASE_TAG}\"}}" \
|
||||
"${REPO_API}/actions/workflows/${workflow}/dispatches")
|
||||
echo " -> HTTP ${HTTP_CODE}"
|
||||
[ "$HTTP_CODE" != "204" ] && cat /tmp/dispatch_resp.txt && echo ""
|
||||
if [ "$HTTP_CODE" != "204" ]; then cat /tmp/dispatch_resp.txt; echo ""; fi
|
||||
done
|
||||
|
||||
# NOTE: Automatic cleanup disabled -- it races with async builds.
|
||||
# Clean up old releases manually from the Gitea UI when needed.
|
||||
|
||||
@@ -27,40 +27,17 @@ jobs:
|
||||
needs: test
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.bump.outputs.version }}
|
||||
tag: ${{ steps.bump.outputs.tag }}
|
||||
has_changes: ${{ steps.check_changes.outputs.has_changes }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check for backend changes
|
||||
id: check_changes
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
CHANGED=$(git diff --name-only HEAD~1 HEAD -- client/ server/ backend/ pyproject.toml local-transcription-headless.spec 2>/dev/null || echo "")
|
||||
if [ -n "$CHANGED" ]; then
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "Backend changes detected: $CHANGED"
|
||||
else
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "No backend changes detected, skipping sidecar build"
|
||||
fi
|
||||
|
||||
- name: Configure git
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
run: |
|
||||
git config user.name "Gitea Actions"
|
||||
git config user.email "actions@gitea.local"
|
||||
|
||||
- name: Bump sidecar patch version
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
id: bump
|
||||
run: |
|
||||
CURRENT=$(grep '^version = ' pyproject.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
|
||||
echo "Current sidecar version: ${CURRENT}"
|
||||
@@ -74,56 +51,49 @@ jobs:
|
||||
|
||||
sed -i "s/^version = \"${CURRENT}\"/version = \"${NEW_VERSION}\"/" pyproject.toml
|
||||
|
||||
echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "tag=sidecar-v${NEW_VERSION}" >> $GITHUB_OUTPUT
|
||||
# Write to env file instead of step outputs (avoids act runner bug)
|
||||
echo "NEW_VERSION=${NEW_VERSION}" >> $GITHUB_ENV
|
||||
echo "RELEASE_TAG=sidecar-v${NEW_VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Commit and tag
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
env:
|
||||
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||
run: |
|
||||
NEW_VERSION="${{ steps.bump.outputs.version }}"
|
||||
TAG="${{ steps.bump.outputs.tag }}"
|
||||
git add pyproject.toml
|
||||
git commit -m "chore: bump sidecar version to ${NEW_VERSION} [skip ci]"
|
||||
git tag "${TAG}"
|
||||
git tag "${RELEASE_TAG}"
|
||||
|
||||
REMOTE_URL=$(git remote get-url origin | sed "s|://|://gitea-actions:${BUILD_TOKEN}@|")
|
||||
git pull --rebase "${REMOTE_URL}" main || true
|
||||
git push "${REMOTE_URL}" HEAD:main
|
||||
git push "${REMOTE_URL}" "${TAG}"
|
||||
git push "${REMOTE_URL}" "${RELEASE_TAG}"
|
||||
|
||||
- name: Create Gitea release
|
||||
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}"
|
||||
TAG="${{ steps.bump.outputs.tag }}"
|
||||
VERSION="${{ steps.bump.outputs.version }}"
|
||||
RELEASE_NAME="Sidecar v${VERSION}"
|
||||
RELEASE_NAME="Sidecar v${NEW_VERSION}"
|
||||
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${BUILD_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"${TAG}\", \"name\": \"${RELEASE_NAME}\", \"body\": \"Automated sidecar build.\", \"draft\": false, \"prerelease\": false}" \
|
||||
-d "{\"tag_name\": \"${RELEASE_TAG}\", \"name\": \"${RELEASE_NAME}\", \"body\": \"Automated sidecar build.\", \"draft\": false, \"prerelease\": false}" \
|
||||
"${REPO_API}/releases"
|
||||
echo "Created release: ${RELEASE_NAME}"
|
||||
|
||||
- name: Trigger per-OS sidecar builds
|
||||
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}"
|
||||
TAG="${{ steps.bump.outputs.tag }}"
|
||||
|
||||
for workflow in build-sidecar-linux.yml build-sidecar-windows.yml build-sidecar-macos.yml build-sidecar-cloud.yml; do
|
||||
echo "Dispatching ${workflow} for ${TAG}..."
|
||||
echo "Dispatching ${workflow} for ${RELEASE_TAG}..."
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token ${BUILD_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"ref\": \"main\", \"inputs\": {\"tag\": \"${TAG}\"}}" \
|
||||
-d "{\"ref\": \"main\", \"inputs\": {\"tag\": \"${RELEASE_TAG}\"}}" \
|
||||
"${REPO_API}/actions/workflows/${workflow}/dispatches")
|
||||
echo " -> HTTP ${HTTP_CODE}"
|
||||
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
|
||||
|
||||
@@ -151,14 +151,24 @@ class APIServer:
|
||||
|
||||
@app.post("/api/start")
|
||||
async def start_transcription():
|
||||
success, message = ctrl.start_transcription()
|
||||
import asyncio
|
||||
# Run in thread pool to avoid blocking the event loop
|
||||
# (start_recording can block up to 15s waiting for Deepgram WS)
|
||||
loop = asyncio.get_event_loop()
|
||||
success, message = await loop.run_in_executor(
|
||||
None, ctrl.start_transcription
|
||||
)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail=message)
|
||||
return {"status": "ok", "message": message}
|
||||
|
||||
@app.post("/api/stop")
|
||||
async def stop_transcription():
|
||||
success, message = ctrl.stop_transcription()
|
||||
import asyncio
|
||||
loop = asyncio.get_event_loop()
|
||||
success, message = await loop.run_in_executor(
|
||||
None, ctrl.stop_transcription
|
||||
)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail=message)
|
||||
return {"status": "ok", "message": message}
|
||||
@@ -223,7 +233,11 @@ class APIServer:
|
||||
|
||||
@app.post("/api/reload-engine")
|
||||
async def reload_engine():
|
||||
success, message = ctrl.reload_engine()
|
||||
import asyncio
|
||||
loop = asyncio.get_event_loop()
|
||||
success, message = await loop.run_in_executor(
|
||||
None, ctrl.reload_engine
|
||||
)
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail=message)
|
||||
return {"status": "ok", "message": message}
|
||||
|
||||
@@ -106,6 +106,12 @@ class AppController:
|
||||
DeviceManager = None
|
||||
|
||||
self.device_manager = DeviceManager() if DeviceManager else None
|
||||
self.is_cloud_only = DeviceManager is None
|
||||
|
||||
# If this is the cloud-only sidecar and mode is still "local",
|
||||
# auto-switch to "byok" so the engine doesn't try to load Whisper.
|
||||
if self.is_cloud_only and self.config.get('remote.mode', 'local') == 'local':
|
||||
self.config.set('remote.mode', 'byok')
|
||||
|
||||
# State
|
||||
self._state = AppState.INITIALIZING
|
||||
@@ -300,8 +306,17 @@ class AppController:
|
||||
# Lazy-import heavy local transcription dependencies
|
||||
global RealtimeTranscriptionEngine
|
||||
if RealtimeTranscriptionEngine is None:
|
||||
from client.transcription_engine_realtime import RealtimeTranscriptionEngine as _RTE
|
||||
RealtimeTranscriptionEngine = _RTE
|
||||
try:
|
||||
from client.transcription_engine_realtime import RealtimeTranscriptionEngine as _RTE
|
||||
RealtimeTranscriptionEngine = _RTE
|
||||
except ImportError:
|
||||
# Cloud-only sidecar -- local engine not available
|
||||
self._set_state(
|
||||
AppState.ERROR,
|
||||
"Local transcription not available in this build. "
|
||||
"Please switch to Cloud (Deepgram) mode in Settings."
|
||||
)
|
||||
return
|
||||
|
||||
if self.device_manager:
|
||||
self.device_manager.set_device(device_config)
|
||||
@@ -358,7 +373,15 @@ class AppController:
|
||||
|
||||
self._set_state(AppState.READY, f"Ready | Device: {device_display}")
|
||||
else:
|
||||
self._set_state(AppState.ERROR, message)
|
||||
# Cloud sidecar with no API key -- show helpful setup message
|
||||
# instead of a scary error. The user needs to enter their key.
|
||||
if self.is_cloud_only:
|
||||
self._set_state(
|
||||
AppState.READY,
|
||||
"Setup needed: Open Settings > Remote Transcription > enter your Deepgram API key"
|
||||
)
|
||||
else:
|
||||
self._set_state(AppState.ERROR, message)
|
||||
|
||||
# ── Transcription Control ──────────────────────────────────────
|
||||
|
||||
@@ -373,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.6",
|
||||
"version": "2.0.10",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "local-transcription"
|
||||
version = "1.0.5"
|
||||
version = "1.0.9"
|
||||
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.6"
|
||||
version = "2.0.10"
|
||||
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.6",
|
||||
"version": "2.0.10",
|
||||
"identifier": "net.anhonesthost.local-transcription",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
@@ -8,17 +8,25 @@
|
||||
);
|
||||
let isLoading = $state(false);
|
||||
|
||||
let errorMessage = $state("");
|
||||
|
||||
async function toggleTranscription() {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
errorMessage = "";
|
||||
try {
|
||||
if (isTranscribing) {
|
||||
await backendStore.apiPost("/api/stop");
|
||||
} else {
|
||||
await backendStore.apiPost("/api/start");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to toggle transcription:", err);
|
||||
// Poll status to update UI immediately instead of waiting
|
||||
// for WebSocket broadcast (which can be delayed or missed)
|
||||
await backendStore.pollStatus();
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error("Failed to toggle transcription:", msg);
|
||||
errorMessage = msg;
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
@@ -101,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;
|
||||
|
||||
@@ -36,11 +36,12 @@
|
||||
|
||||
try {
|
||||
// Listen for progress events from the Tauri backend
|
||||
unlisten = await listen<{ progress: number; message: string }>(
|
||||
unlisten = await listen<{ downloaded: number; total: number; phase: string; message: string }>(
|
||||
"sidecar-download-progress",
|
||||
(event) => {
|
||||
progress = event.payload.progress;
|
||||
progressMessage = event.payload.message;
|
||||
const { downloaded, total, message } = event.payload;
|
||||
progress = total > 0 ? (downloaded / total) * 100 : 0;
|
||||
progressMessage = message;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -125,23 +126,6 @@
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="variant-option" class:selected={variant === "cuda"}>
|
||||
<input
|
||||
type="radio"
|
||||
name="variant"
|
||||
value="cuda"
|
||||
bind:group={variant}
|
||||
/>
|
||||
<div class="variant-info">
|
||||
<span class="variant-name">Local - GPU (NVIDIA CUDA)</span>
|
||||
<span class="variant-desc">~2 GB download</span>
|
||||
<span class="variant-detail">
|
||||
Runs Whisper AI models locally using your NVIDIA GPU for fast
|
||||
transcription. No internet needed after download. Requires an
|
||||
NVIDIA GPU with CUDA support.
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="download-btn" onclick={startDownload}>
|
||||
|
||||
@@ -302,6 +302,7 @@ export const backendStore = {
|
||||
setPort,
|
||||
connect: connectWebSocket,
|
||||
disconnect,
|
||||
pollStatus,
|
||||
apiUrl,
|
||||
apiFetch,
|
||||
apiGet,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Version information for Local Transcription."""
|
||||
|
||||
__version__ = "2.0.6"
|
||||
__version_info__ = (2, 0, 6)
|
||||
__version__ = "2.0.10"
|
||||
__version_info__ = (2, 0, 10)
|
||||
|
||||
# Version history:
|
||||
# 1.4.0 - Auto-update feature:
|
||||
|
||||
Reference in New Issue
Block a user