Compare commits
15 Commits
v1.4.19
...
sidecar-v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e42a922507 | ||
|
|
8fc2d11c5f | ||
|
|
11832e911b | ||
|
|
18e6b974c0 | ||
|
|
08e464daaf | ||
|
|
5d22adcaa4 | ||
|
|
36b4f7dad5 | ||
|
|
1ecb23b83f | ||
|
|
4b88871a9b | ||
|
|
0ae48a67d5 | ||
|
|
924cae6c75 | ||
|
|
5139936e18 | ||
|
|
47724f1ac0 | ||
|
|
3b204be37e | ||
|
|
4c02a48135 |
@@ -13,23 +13,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NODE_VERSION: "20"
|
NODE_VERSION: "20"
|
||||||
|
RELEASE_TAG: ${{ inputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Determine tag
|
- name: Show tag
|
||||||
id: tag
|
run: echo "Building for tag: ${RELEASE_TAG}"
|
||||||
run: |
|
|
||||||
TAG="${{ inputs.tag }}"
|
|
||||||
if [ -z "$TAG" ]; then
|
|
||||||
TAG="${{ github.event.inputs.tag }}"
|
|
||||||
fi
|
|
||||||
if [ -z "$TAG" ]; then
|
|
||||||
TAG=$(git ls-remote --tags --sort=-v:refname origin 'refs/tags/v*' | head -1 | sed 's|.*refs/tags/||')
|
|
||||||
fi
|
|
||||||
echo "Building for tag: ${TAG}"
|
|
||||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ steps.tag.outputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -58,7 +49,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y jq
|
sudo apt-get install -y jq
|
||||||
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
TAG="${{ steps.tag.outputs.tag }}"
|
TAG="${RELEASE_TAG}"
|
||||||
echo "Release tag: ${TAG}"
|
echo "Release tag: ${TAG}"
|
||||||
|
|
||||||
echo "Waiting for release ${TAG} to be available..."
|
echo "Waiting for release ${TAG} to be available..."
|
||||||
|
|||||||
@@ -13,23 +13,14 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
env:
|
env:
|
||||||
NODE_VERSION: "20"
|
NODE_VERSION: "20"
|
||||||
|
RELEASE_TAG: ${{ inputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Determine tag
|
- name: Show tag
|
||||||
id: tag
|
run: echo "Building for tag: ${RELEASE_TAG}"
|
||||||
run: |
|
|
||||||
TAG="${{ inputs.tag }}"
|
|
||||||
if [ -z "$TAG" ]; then
|
|
||||||
TAG="${{ github.event.inputs.tag }}"
|
|
||||||
fi
|
|
||||||
if [ -z "$TAG" ]; then
|
|
||||||
TAG=$(git ls-remote --tags --sort=-v:refname origin 'refs/tags/v*' | head -1 | sed 's|.*refs/tags/||')
|
|
||||||
fi
|
|
||||||
echo "Building for tag: ${TAG}"
|
|
||||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ steps.tag.outputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -56,7 +47,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
which jq || brew install jq
|
which jq || brew install jq
|
||||||
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
TAG="${{ steps.tag.outputs.tag }}"
|
TAG="${RELEASE_TAG}"
|
||||||
echo "Release tag: ${TAG}"
|
echo "Release tag: ${TAG}"
|
||||||
|
|
||||||
echo "Waiting for release ${TAG} to be available..."
|
echo "Waiting for release ${TAG} to be available..."
|
||||||
|
|||||||
@@ -13,23 +13,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
PYTHON_VERSION: "3.11"
|
PYTHON_VERSION: "3.11"
|
||||||
|
RELEASE_TAG: ${{ inputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Determine tag
|
- name: Show tag
|
||||||
id: tag
|
run: echo "Building for tag: ${RELEASE_TAG}"
|
||||||
run: |
|
|
||||||
TAG="${{ inputs.tag }}"
|
|
||||||
if [ -z "$TAG" ]; then
|
|
||||||
TAG="${{ github.event.inputs.tag }}"
|
|
||||||
fi
|
|
||||||
if [ -z "$TAG" ]; then
|
|
||||||
TAG=$(git ls-remote --tags --sort=-v:refname origin 'refs/tags/sidecar-v*' | head -1 | sed 's|.*refs/tags/||')
|
|
||||||
fi
|
|
||||||
echo "Building for tag: ${TAG}"
|
|
||||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ steps.tag.outputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
run: |
|
run: |
|
||||||
@@ -75,7 +66,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y jq
|
sudo apt-get install -y jq
|
||||||
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
TAG="${{ steps.tag.outputs.tag }}"
|
TAG="${RELEASE_TAG}"
|
||||||
|
|
||||||
echo "Waiting for sidecar release ${TAG} to be available..."
|
echo "Waiting for sidecar release ${TAG} to be available..."
|
||||||
for i in $(seq 1 30); do
|
for i in $(seq 1 30); do
|
||||||
|
|||||||
@@ -13,23 +13,14 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
env:
|
env:
|
||||||
PYTHON_VERSION: "3.11"
|
PYTHON_VERSION: "3.11"
|
||||||
|
RELEASE_TAG: ${{ inputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Determine tag
|
- name: Show tag
|
||||||
id: tag
|
run: echo "Building for tag: ${RELEASE_TAG}"
|
||||||
run: |
|
|
||||||
TAG="${{ inputs.tag }}"
|
|
||||||
if [ -z "$TAG" ]; then
|
|
||||||
TAG="${{ github.event.inputs.tag }}"
|
|
||||||
fi
|
|
||||||
if [ -z "$TAG" ]; then
|
|
||||||
TAG=$(git ls-remote --tags --sort=-v:refname origin 'refs/tags/sidecar-v*' | head -1 | sed 's|.*refs/tags/||')
|
|
||||||
fi
|
|
||||||
echo "Building for tag: ${TAG}"
|
|
||||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ steps.tag.outputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
run: |
|
run: |
|
||||||
@@ -66,7 +57,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
which jq || brew install jq
|
which jq || brew install jq
|
||||||
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
TAG="${{ steps.tag.outputs.tag }}"
|
TAG="${RELEASE_TAG}"
|
||||||
|
|
||||||
echo "Waiting for sidecar release ${TAG} to be available..."
|
echo "Waiting for sidecar release ${TAG} to be available..."
|
||||||
for i in $(seq 1 30); do
|
for i in $(seq 1 30); do
|
||||||
|
|||||||
@@ -3,11 +3,45 @@ name: Release
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'src-tauri/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'vite.config.ts'
|
||||||
|
- 'index.html'
|
||||||
|
|
||||||
jobs:
|
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:
|
bump-version:
|
||||||
name: Bump version and tag
|
name: Bump version and tag
|
||||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
needs: test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
new_version: ${{ steps.bump.outputs.new_version }}
|
new_version: ${{ steps.bump.outputs.new_version }}
|
||||||
@@ -89,3 +123,43 @@ jobs:
|
|||||||
"${REPO_API}/actions/workflows/${workflow}/dispatches")
|
"${REPO_API}/actions/workflows/${workflow}/dispatches")
|
||||||
echo " -> HTTP ${HTTP_CODE}"
|
echo " -> HTTP ${HTTP_CODE}"
|
||||||
done
|
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}"
|
||||||
|
# Keep the git tag -- only delete the release (assets).
|
||||||
|
# Deleting tags breaks builds that haven't checked out yet.
|
||||||
|
done
|
||||||
|
echo "Cleanup complete"
|
||||||
|
|||||||
@@ -12,8 +12,27 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
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:
|
bump-sidecar-version:
|
||||||
name: Bump sidecar version and tag
|
name: Bump sidecar version and tag
|
||||||
|
needs: test
|
||||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
@@ -61,7 +80,6 @@ jobs:
|
|||||||
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
|
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
|
||||||
echo "New sidecar version: ${NEW_VERSION}"
|
echo "New sidecar version: ${NEW_VERSION}"
|
||||||
|
|
||||||
# Only update pyproject.toml -- version.py is owned by the app release workflow
|
|
||||||
sed -i "s/^version = \"${CURRENT}\"/version = \"${NEW_VERSION}\"/" pyproject.toml
|
sed -i "s/^version = \"${CURRENT}\"/version = \"${NEW_VERSION}\"/" pyproject.toml
|
||||||
|
|
||||||
echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT
|
echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT
|
||||||
@@ -117,3 +135,38 @@ jobs:
|
|||||||
"${REPO_API}/actions/workflows/${workflow}/dispatches")
|
"${REPO_API}/actions/workflows/${workflow}/dispatches")
|
||||||
echo " -> HTTP ${HTTP_CODE}"
|
echo " -> HTTP ${HTTP_CODE}"
|
||||||
done
|
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}"
|
||||||
|
# Keep the git tag -- only delete the release (assets).
|
||||||
|
# Deleting tags breaks builds that haven't checked out yet.
|
||||||
|
done
|
||||||
|
echo "Cleanup complete"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
python-tests:
|
python-tests:
|
||||||
@@ -13,12 +14,20 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install test dependencies
|
- name: Install uv
|
||||||
run: |
|
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
|
- 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:
|
frontend-tests:
|
||||||
name: Frontend Tests
|
name: Frontend Tests
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "local-transcription",
|
"name": "local-transcription",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.4.19",
|
"version": "2.0.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "local-transcription"
|
name = "local-transcription"
|
||||||
version = "1.0.3"
|
version = "1.0.4"
|
||||||
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"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "local-transcription"
|
name = "local-transcription"
|
||||||
version = "1.4.19"
|
version = "2.0.3"
|
||||||
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"
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.manage(sidecar::ManagedSidecar(Mutex::new(
|
.manage(sidecar::ManagedSidecar(std::sync::Arc::new(Mutex::new(
|
||||||
sidecar::SidecarManager::new(),
|
sidecar::SidecarManager::new(),
|
||||||
)))
|
))))
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let resource_dir = app
|
let resource_dir = app
|
||||||
.path()
|
.path()
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ fn read_installed_version() -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn sidecar_dir_for_version(version: &str) -> PathBuf {
|
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 {
|
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) {
|
fn cleanup_old_versions(current_version: &str) {
|
||||||
let data = data_dir();
|
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) {
|
if let Ok(entries) = std::fs::read_dir(data) {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let name = entry.file_name().to_string_lossy().to_string();
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
if name.starts_with("sidecar-v") // e.g. sidecar-v1.0.1
|
if name.starts_with("sidecar-")
|
||||||
&& name != current_dir_name
|
&& name != current_version
|
||||||
&& entry.path().is_dir()
|
&& entry.path().is_dir()
|
||||||
{
|
{
|
||||||
let _ = std::fs::remove_dir_all(entry.path());
|
let _ = std::fs::remove_dir_all(entry.path());
|
||||||
@@ -433,6 +434,20 @@ impl SidecarManager {
|
|||||||
.ok_or_else(|| "Sidecar running but port unknown".into());
|
.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)
|
let is_dev = cfg!(debug_assertions)
|
||||||
|| std::env::var("LOCAL_TRANSCRIPTION_DEV")
|
|| std::env::var("LOCAL_TRANSCRIPTION_DEV")
|
||||||
.map(|v| v == "1")
|
.map(|v| v == "1")
|
||||||
@@ -463,11 +478,63 @@ impl SidecarManager {
|
|||||||
.take()
|
.take()
|
||||||
.ok_or("Failed to capture sidecar stdout")?;
|
.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);
|
let log_dir = DIRS.get().map(|d| d.data_dir.clone());
|
||||||
self.port = Some(port);
|
std::thread::spawn(move || {
|
||||||
Ok(port)
|
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.
|
/// Stop the sidecar process if running.
|
||||||
@@ -488,7 +555,7 @@ impl SidecarManager {
|
|||||||
|
|
||||||
fn build_dev_command(&self) -> Result<std::process::Command, String> {
|
fn build_dev_command(&self) -> Result<std::process::Command, String> {
|
||||||
let mut cmd = std::process::Command::new("python");
|
let mut cmd = std::process::Command::new("python");
|
||||||
cmd.args(["-m", "backend.main_headless"]);
|
cmd.args(["-u", "-m", "backend.main_headless"]); // -u = unbuffered
|
||||||
|
|
||||||
// Try to find the project root (parent of src-tauri)
|
// Try to find the project root (parent of src-tauri)
|
||||||
if let Some(dirs) = DIRS.get() {
|
if let Some(dirs) = DIRS.get() {
|
||||||
@@ -501,6 +568,7 @@ impl SidecarManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.env("PYTHONUNBUFFERED", "1");
|
||||||
Ok(cmd)
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,27 +584,51 @@ impl SidecarManager {
|
|||||||
bin.parent()
|
bin.parent()
|
||||||
.ok_or("Cannot determine sidecar parent dir")?,
|
.ok_or("Cannot determine sidecar parent dir")?,
|
||||||
);
|
);
|
||||||
|
// Force unbuffered stdout so the ready event is sent immediately.
|
||||||
|
// PyInstaller frozen executables buffer stdout when piped.
|
||||||
|
cmd.env("PYTHONUNBUFFERED", "1");
|
||||||
Ok(cmd)
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_for_ready(stdout: std::process::ChildStdout) -> Result<u16, String> {
|
fn wait_for_ready(stdout: std::process::ChildStdout) -> Result<u16, String> {
|
||||||
let reader = std::io::BufReader::new(stdout);
|
use std::sync::mpsc;
|
||||||
let timeout = std::time::Duration::from_secs(120);
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
|
|
||||||
for line in reader.lines() {
|
let timeout = std::time::Duration::from_secs(120);
|
||||||
if start.elapsed() > timeout {
|
|
||||||
return Err("Timed out waiting for sidecar ready event".into());
|
// Read stdout in a background thread so we can enforce a real timeout.
|
||||||
}
|
// BufReader::lines() blocks indefinitely if no data arrives.
|
||||||
let line = line.map_err(|e| format!("IO error reading stdout: {e}"))?;
|
let (tx, rx) = mpsc::channel();
|
||||||
if let Ok(evt) = serde_json::from_str::<ReadyEvent>(&line) {
|
|
||||||
if evt.event == "ready" {
|
std::thread::spawn(move || {
|
||||||
return Ok(evt.port);
|
let reader = std::io::BufReader::new(stdout);
|
||||||
|
for line in reader.lines() {
|
||||||
|
match line {
|
||||||
|
Ok(line) => {
|
||||||
|
eprintln!("[sidecar-stdout] {}", line);
|
||||||
|
if let Ok(evt) = serde_json::from_str::<ReadyEvent>(&line) {
|
||||||
|
if evt.event == "ready" {
|
||||||
|
let _ = tx.send(Ok(evt.port));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let _ = tx.send(Err(format!("IO error reading stdout: {e}")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ignore other lines (e.g. log output)
|
let _ = tx.send(Err(
|
||||||
}
|
"Sidecar process exited before sending ready event".into(),
|
||||||
Err("Sidecar process exited before sending ready event".into())
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
rx.recv_timeout(timeout).unwrap_or_else(|_| {
|
||||||
|
Err(format!(
|
||||||
|
"Timed out after {}s waiting for sidecar ready event",
|
||||||
|
timeout.as_secs()
|
||||||
|
))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,7 +637,8 @@ impl SidecarManager {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Wrapper so we can store `SidecarManager` in Tauri's managed state.
|
/// Wrapper so we can store `SidecarManager` in Tauri's managed state.
|
||||||
pub struct ManagedSidecar(pub Mutex<SidecarManager>);
|
/// Uses Arc so it can be cloned into background threads for async commands.
|
||||||
|
pub struct ManagedSidecar(pub std::sync::Arc<Mutex<SidecarManager>>);
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_sidecar_port(state: tauri::State<'_, ManagedSidecar>) -> Result<Option<u16>, String> {
|
pub fn get_sidecar_port(state: tauri::State<'_, ManagedSidecar>) -> Result<Option<u16>, String> {
|
||||||
@@ -561,12 +654,16 @@ pub fn get_sidecar_port(state: tauri::State<'_, ManagedSidecar>) -> Result<Optio
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn start_sidecar(state: tauri::State<'_, ManagedSidecar>) -> Result<u16, String> {
|
pub async fn start_sidecar(state: tauri::State<'_, ManagedSidecar>) -> Result<u16, String> {
|
||||||
let mut mgr = state
|
let mgr = state.0.clone();
|
||||||
.0
|
// Run blocking sidecar launch in a background thread so it doesn't
|
||||||
.lock()
|
// freeze the Tauri UI while waiting for the ready event (up to 120s).
|
||||||
.map_err(|e| format!("Lock error: {e}"))?;
|
tokio::task::spawn_blocking(move || {
|
||||||
mgr.ensure_running()
|
let mut mgr = mgr.lock().map_err(|e| format!("Lock error: {e}"))?;
|
||||||
|
mgr.ensure_running()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Task join error: {e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -739,7 +836,7 @@ mod tests {
|
|||||||
fn sidecar_dir_for_version_contains_version() {
|
fn sidecar_dir_for_version_contains_version() {
|
||||||
let data = ensure_dirs_initialised();
|
let data = ensure_dirs_initialised();
|
||||||
let dir = sidecar_dir_for_version("sidecar-v1.2.3");
|
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]
|
#[test]
|
||||||
@@ -784,9 +881,8 @@ mod tests {
|
|||||||
std::fs::create_dir_all(data.join(d)).unwrap();
|
std::fs::create_dir_all(data.join(d)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanup_old_versions builds `current_dir_name = "sidecar-{version}"`.
|
// current_version is the full tag, e.g. "sidecar-v1.0.2"
|
||||||
// Passing "v1.0.2" produces "sidecar-v1.0.2" which matches our dir name.
|
cleanup_old_versions("sidecar-v1.0.2");
|
||||||
cleanup_old_versions("v1.0.2");
|
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
!data.join("sidecar-v1.0.0").exists(),
|
!data.join("sidecar-v1.0.0").exists(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"productName": "Local Transcription",
|
"productName": "Local Transcription",
|
||||||
"version": "1.4.19",
|
"version": "2.0.3",
|
||||||
"identifier": "net.anhonesthost.local-transcription",
|
"identifier": "net.anhonesthost.local-transcription",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
@@ -9,11 +9,12 @@
|
|||||||
import { backendStore } from "$lib/stores/backend";
|
import { backendStore } from "$lib/stores/backend";
|
||||||
import { configStore } from "$lib/stores/config";
|
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 showSettings = $state(false);
|
||||||
let sidecarState = $state<SidecarState>("checking");
|
let sidecarState = $state<SidecarState>("checking");
|
||||||
let debugLog = $state("");
|
let debugLog = $state("");
|
||||||
|
let availableUpdate = $state("");
|
||||||
|
|
||||||
let obsDisplayUrl = $derived(backendStore.obsUrl);
|
let obsDisplayUrl = $derived(backendStore.obsUrl);
|
||||||
let syncDisplayUrl = $derived(backendStore.syncUrl);
|
let syncDisplayUrl = $derived(backendStore.syncUrl);
|
||||||
@@ -53,6 +54,20 @@
|
|||||||
return;
|
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();
|
await launchSidecar();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Not running in Tauri (browser dev mode) - skip sidecar check
|
// Not running in Tauri (browser dev mode) - skip sidecar check
|
||||||
@@ -118,6 +133,26 @@
|
|||||||
{:else if sidecarState === "needs_setup"}
|
{:else if sidecarState === "needs_setup"}
|
||||||
<SidecarSetup onComplete={onSidecarReady} />
|
<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}
|
{: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-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;">
|
<div class="connecting-content" style="text-align:center;">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Version information for Local Transcription."""
|
"""Version information for Local Transcription."""
|
||||||
|
|
||||||
__version__ = "1.4.19"
|
__version__ = "2.0.3"
|
||||||
__version_info__ = (1, 4, 19)
|
__version_info__ = (2, 0, 3)
|
||||||
|
|
||||||
# Version history:
|
# Version history:
|
||||||
# 1.4.0 - Auto-update feature:
|
# 1.4.0 - Auto-update feature:
|
||||||
|
|||||||
Reference in New Issue
Block a user