13 Commits

Author SHA1 Message Date
Gitea Actions
9033881274 chore: bump version to 0.2.1 [skip ci] 2026-03-21 18:53:23 +00:00
Claude
1ed34e0bbb Add auto-increment version and release workflow
All checks were successful
Release / Bump version and tag (push) Successful in 3s
- New release.yml: bumps patch version, commits with skip-ci marker, tags, creates Gitea release
- Build workflows now trigger on v* tags only (not branch push)
- Simplified upload steps: use tag directly, retry loop for release lookup
- Fix macOS: install jq if missing
- Sync python/pyproject.toml version to 0.2.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 11:53:13 -07:00
Claude
b7a00af2e0 Fix duplicate CI runs: remove tags trigger, detect tags on commit
Some checks failed
Build macOS / Build (macOS) (push) Failing after 3m31s
Build Linux / Build (Linux) (push) Failing after 7m22s
Build Windows / Build (Windows) (push) Successful in 16m5s
Pushing to main + a tag triggered 6 workflows (3 per trigger).
Now only main pushes trigger builds. The upload step detects version
tags on the current commit via git tag --points-at HEAD.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 11:30:57 -07:00
Claude
2d0d4cfc50 Skip AppImage and RPM builds to avoid slow 360MB+ compression
Some checks failed
Build Windows / Build (Windows) (push) Has been cancelled
Build Linux / Build (Linux) (push) Has been cancelled
Build macOS / Build (macOS) (push) Has been cancelled
AppImage bundler compresses the entire sidecar.zip into squashfs,
causing builds to hang/timeout. Limit targets to deb (Linux),
nsis+msi (Windows), and dmg (macOS).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 11:28:55 -07:00
Claude
b99613f452 Fix Windows release upload: use curl for streaming large file uploads
Some checks failed
Build Linux / Build (Linux) (push) Has been cancelled
Build macOS / Build (macOS) (push) Has been cancelled
Build Windows / Build (Windows) (push) Has been cancelled
Invoke-RestMethod loads entire files into memory, causing connection
failures on 360MB+ installer files. Switch to curl which streams
the upload.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 11:27:33 -07:00
Claude
ec7e364165 Fix CI release upload: support versioned releases on tag pushes
Some checks failed
Build macOS / Build (macOS) (push) Successful in 4m43s
Build Windows / Build (Windows) (push) Successful in 18m20s
Build Linux / Build (Linux) (push) Has been cancelled
- Upload step now runs on both main pushes and v* tag pushes
- Tag pushes create a versioned release (e.g., "Voice to Notes v0.2.0")
- Main pushes update the "latest" prerelease as before
- Windows: filter for *-setup.exe to avoid uploading non-installer binaries

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 11:03:00 -07:00
Claude
a0cb034ab5 Bump version to 0.2.0
Some checks failed
Build Windows / Build (Windows) (push) Successful in 11m37s
Build macOS / Build (macOS) (push) Successful in 3m30s
Build Linux / Build (Linux) (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 07:34:37 -07:00
Claude
52d7d06d84 Fix sidecar.zip not bundled: move resources config into tauri.conf.json
Some checks failed
Build Linux / Build (Linux) (push) Has been cancelled
Build Windows / Build (Windows) (push) Has started running
Build macOS / Build (macOS) (push) Has been cancelled
The TAURI_CONFIG env var approach for resources wasn't being applied
by the NSIS bundler, so sidecar.zip was never included in the installer.

- Add resources: ["sidecar.zip"] directly to tauri.conf.json
- build.rs creates a minimal placeholder zip for dev builds so
  compilation succeeds even without the real sidecar
- Remove TAURI_CONFIG env var from all CI workflows (no longer needed)
- Add sidecar.zip to .gitignore (generated by CI, not tracked)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 07:33:02 -07:00
Claude
462a4b80f6 Fix Tauri build stack overflow: zip sidecar and extract on first launch
All checks were successful
Build macOS / Build (macOS) (push) Successful in 3m50s
Build Linux / Build (Linux) (push) Successful in 8m3s
Build Windows / Build (Windows) (push) Successful in 9m8s
Tauri's build script overflows the stack when processing resource globs
matching thousands of files from PyInstaller's ML output (torch, pyannote).

Instead of bundling the sidecar directory directly:
- CI zips the sidecar output into a single sidecar.zip
- Tauri bundles just the one zip file (no recursion)
- On first launch, Rust extracts the zip to the app data directory
- Versioned extraction dir (sidecar-{version}) ensures updates re-extract

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 07:12:22 -07:00
Claude
12869e3757 Fix sidecar not found on Windows/macOS/Linux: switch from externalBin to resources
Some checks failed
Build macOS / Build (macOS) (push) Failing after 2m53s
Build Linux / Build (Linux) (push) Failing after 6m30s
Build Windows / Build (Windows) (push) Failing after 8m15s
Tauri's externalBin only bundled the single sidecar executable, but
PyInstaller's onedir output requires companion DLLs and _internal/.
The binary was also renamed with a target triple suffix that
resolve_sidecar_path() didn't look for, causing it to fall back to
dev mode which used a compile-time CI path (CARGO_MANIFEST_DIR).

- Switch from externalBin to bundle.resources to include all sidecar files
- Pass Tauri resource_dir to sidecar manager for platform-aware path resolution
- Remove rename_binary() since externalBin target triple naming is no longer needed
- Remove broken production-to-dev fallback that could never work on user machines

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 06:55:44 -07:00
Claude
bf6fb471d9 Merge sidecar and app builds into single jobs per platform
All checks were successful
Build macOS / Build (macOS) (push) Successful in 3m21s
Build Linux / Build (Linux) (push) Successful in 7m40s
Build Windows / Build (Windows) (push) Successful in 9m35s
Removes the artifact upload/download overhead between sidecar and app
build steps. Each platform now runs as a single job: build sidecar,
copy it into src-tauri/binaries, build Tauri app, upload to release.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 06:01:15 -07:00
Claude
50baa7284e Fix Windows release upload: URL-encode filenames with spaces
Some checks failed
Build Linux / Build sidecar (Linux) (push) Successful in 5m17s
Build Linux / Release (Linux) (push) Has been cancelled
Build macOS / Build app (macOS) (push) Has been cancelled
Build macOS / Release (macOS) (push) Has been cancelled
Build macOS / Build sidecar (macOS) (push) Has been cancelled
Build Linux / Build app (Linux) (push) Has been cancelled
Build Windows / Build app (Windows) (push) Has been cancelled
Build Windows / Release (Windows) (push) Has been cancelled
Build Windows / Build sidecar (Windows) (push) Has been cancelled
Use [System.Uri]::EscapeDataString for proper encoding of filenames
containing spaces in the Gitea API URL. Add size logging and error handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 05:54:59 -07:00
Claude
a3c39a2069 Fix release upload: use streaming upload and handle spaces in filenames
Some checks failed
Build Linux / Build app (Linux) (push) Has been cancelled
Build Linux / Release (Linux) (push) Has been cancelled
Build Linux / Build sidecar (Linux) (push) Has been cancelled
Build macOS / Build app (macOS) (push) Has been cancelled
Build macOS / Release (macOS) (push) Has been cancelled
Build Windows / Build app (Windows) (push) Has been cancelled
Build Windows / Release (Windows) (push) Has been cancelled
Build macOS / Build sidecar (macOS) (push) Has been cancelled
Build Windows / Build sidecar (Windows) (push) Has been cancelled
- Use curl -T (streaming) instead of --data-binary (loads into memory)
  to handle large .deb/.AppImage files
- URL-encode spaces in filenames for the Gitea API
- Use IFS= read -r to handle filenames with spaces
- Add HTTP status code logging for upload debugging

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 05:53:57 -07:00
13 changed files with 357 additions and 233 deletions

View File

@@ -2,10 +2,7 @@ name: Build Linux
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
env:
PYTHON_VERSION: "3.11"
@@ -13,12 +10,13 @@ env:
TARGET: x86_64-unknown-linux-gnu
jobs:
build-sidecar:
name: Build sidecar (Linux)
build:
name: Build (Linux)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# ── Python sidecar ──
- name: Install uv
run: |
if command -v uv &> /dev/null; then
@@ -38,20 +36,11 @@ jobs:
working-directory: python
run: uv run --python ${{ env.PYTHON_VERSION }} python build_sidecar.py --cpu-only
- name: Upload sidecar artifact
uses: actions/upload-artifact@v3
with:
name: sidecar-linux
path: python/dist/voice-to-notes-sidecar/
retention-days: 7
build-app:
name: Build app (Linux)
needs: build-sidecar
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Package sidecar for Tauri
run: |
cd python/dist/voice-to-notes-sidecar && zip -r ../../../src-tauri/sidecar.zip .
# ── Tauri app ──
- name: Set up Node.js
uses: actions/setup-node@v4
with:
@@ -64,67 +53,43 @@ jobs:
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
- name: Download sidecar artifact
uses: actions/download-artifact@v3
with:
name: sidecar-linux
path: src-tauri/binaries/
- name: Make sidecar executable
run: chmod +x src-tauri/binaries/voice-to-notes-sidecar-${{ env.TARGET }}
- name: Install npm dependencies
run: npm ci
- name: Build Tauri app
run: npm run tauri build
env:
TAURI_CONFIG: '{"bundle":{"externalBin":["binaries/voice-to-notes-sidecar"]}}'
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: app-linux
path: |
src-tauri/target/release/bundle/deb/*.deb
src-tauri/target/release/bundle/appimage/*.AppImage
retention-days: 30
release:
name: Release (Linux)
needs: build-app
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Install tools
run: sudo apt-get update && sudo apt-get install -y jq curl
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: app-linux
path: artifacts/
- name: Create or update release
# ── Release ──
- name: Upload to release
env:
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
run: |
TAG="latest"
sudo apt-get install -y jq
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
# Check if release exists
RELEASE_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
"${REPO_API}/releases/tags/${TAG}" | jq -r '.id // empty')
TAG="${GITHUB_REF_NAME}"
RELEASE_NAME="Voice to Notes ${TAG}"
echo "Release tag: ${TAG}"
if [ -z "${RELEASE_ID}" ]; then
# Create new release
# Wait for release to be created by the release workflow
for i in 1 2 3 4 5; do
RELEASE_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
"${REPO_API}/releases/tags/${TAG}" | jq -r '.id // empty')
if [ -n "${RELEASE_ID}" ] && [ "${RELEASE_ID}" != "null" ]; then
break
fi
echo "Release not found yet, waiting 10s... (attempt $i)"
sleep 10
done
# Fallback: create release if it still doesn't exist
if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "null" ]; then
RELEASE_ID=$(curl -s -X POST \
-H "Authorization: token ${BUILD_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"${TAG}\", \"name\": \"Voice to Notes (Latest Build)\", \"body\": \"Latest automated build from main branch.\", \"draft\": false, \"prerelease\": true}" \
-d "{\"tag_name\": \"${TAG}\", \"name\": \"${RELEASE_NAME}\", \"body\": \"Automated build.\", \"draft\": false, \"prerelease\": false}" \
"${REPO_API}/releases" | jq -r '.id')
fi
@@ -134,12 +99,11 @@ jobs:
exit 1
fi
# Upload artifacts (delete existing ones with same name first)
find artifacts/ -type f \( -name "*.deb" -o -name "*.AppImage" \) | while read file; do
find src-tauri/target/release/bundle -type f -name "*.deb" | while IFS= read -r file; do
filename=$(basename "$file")
echo "Uploading ${filename}..."
encoded_name=$(echo "$filename" | sed 's/ /%20/g')
echo "Uploading ${filename} ($(du -h "$file" | cut -f1))..."
# Delete existing asset with same name
ASSET_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
"${REPO_API}/releases/${RELEASE_ID}/assets" | jq -r ".[] | select(.name == \"${filename}\") | .id // empty")
if [ -n "${ASSET_ID}" ]; then
@@ -147,9 +111,10 @@ jobs:
"${REPO_API}/releases/${RELEASE_ID}/assets/${ASSET_ID}"
fi
curl -s -X POST \
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${BUILD_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@${file}" \
"${REPO_API}/releases/${RELEASE_ID}/assets?name=${filename}"
-T "$file" \
"${REPO_API}/releases/${RELEASE_ID}/assets?name=${encoded_name}")
echo "Upload response: HTTP ${HTTP_CODE}"
done

View File

@@ -2,10 +2,7 @@ name: Build macOS
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
env:
PYTHON_VERSION: "3.11"
@@ -13,12 +10,13 @@ env:
TARGET: aarch64-apple-darwin
jobs:
build-sidecar:
name: Build sidecar (macOS)
build:
name: Build (macOS)
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
# ── Python sidecar ──
- name: Install uv
run: |
if command -v uv &> /dev/null; then
@@ -38,20 +36,11 @@ jobs:
working-directory: python
run: uv run --python ${{ env.PYTHON_VERSION }} python build_sidecar.py --cpu-only
- name: Upload sidecar artifact
uses: actions/upload-artifact@v3
with:
name: sidecar-macos
path: python/dist/voice-to-notes-sidecar/
retention-days: 7
build-app:
name: Build app (macOS)
needs: build-sidecar
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Package sidecar for Tauri
run: |
cd python/dist/voice-to-notes-sidecar && zip -r ../../../src-tauri/sidecar.zip .
# ── Tauri app ──
- name: Set up Node.js
uses: actions/setup-node@v4
with:
@@ -65,60 +54,43 @@ jobs:
- name: Install system dependencies
run: brew install --quiet create-dmg || true
- name: Download sidecar artifact
uses: actions/download-artifact@v3
with:
name: sidecar-macos
path: src-tauri/binaries/
- name: Make sidecar executable
run: chmod +x src-tauri/binaries/voice-to-notes-sidecar-${{ env.TARGET }}
- name: Install npm dependencies
run: npm ci
- name: Build Tauri app
run: npm run tauri build
env:
TAURI_CONFIG: '{"bundle":{"externalBin":["binaries/voice-to-notes-sidecar"]}}'
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: app-macos
path: |
src-tauri/target/release/bundle/dmg/*.dmg
src-tauri/target/release/bundle/macos/*.app
retention-days: 30
release:
name: Release (macOS)
needs: build-app
if: github.ref == 'refs/heads/main'
runs-on: macos-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: app-macos
path: artifacts/
- name: Create or update release
# ── Release ──
- name: Upload to release
env:
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
run: |
TAG="latest"
# Ensure jq is available
which jq || brew install jq
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
# Check if release exists
RELEASE_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
"${REPO_API}/releases/tags/${TAG}" | jq -r '.id // empty')
TAG="${GITHUB_REF_NAME}"
RELEASE_NAME="Voice to Notes ${TAG}"
echo "Release tag: ${TAG}"
if [ -z "${RELEASE_ID}" ]; then
# Wait for release to be created by the release workflow
for i in 1 2 3 4 5; do
RELEASE_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
"${REPO_API}/releases/tags/${TAG}" | jq -r '.id // empty')
if [ -n "${RELEASE_ID}" ] && [ "${RELEASE_ID}" != "null" ]; then
break
fi
echo "Release not found yet, waiting 10s... (attempt $i)"
sleep 10
done
# Fallback: create release if it still doesn't exist
if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "null" ]; then
RELEASE_ID=$(curl -s -X POST \
-H "Authorization: token ${BUILD_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"${TAG}\", \"name\": \"Voice to Notes (Latest Build)\", \"body\": \"Latest automated build from main branch.\", \"draft\": false, \"prerelease\": true}" \
-d "{\"tag_name\": \"${TAG}\", \"name\": \"${RELEASE_NAME}\", \"body\": \"Automated build.\", \"draft\": false, \"prerelease\": false}" \
"${REPO_API}/releases" | jq -r '.id')
fi
@@ -128,9 +100,10 @@ jobs:
exit 1
fi
find artifacts/ -type f -name "*.dmg" | while read file; do
find src-tauri/target/release/bundle -type f -name "*.dmg" | while IFS= read -r file; do
filename=$(basename "$file")
echo "Uploading ${filename}..."
encoded_name=$(echo "$filename" | sed 's/ /%20/g')
echo "Uploading ${filename} ($(du -h "$file" | cut -f1))..."
ASSET_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
"${REPO_API}/releases/${RELEASE_ID}/assets" | jq -r ".[] | select(.name == \"${filename}\") | .id // empty")
@@ -139,9 +112,10 @@ jobs:
"${REPO_API}/releases/${RELEASE_ID}/assets/${ASSET_ID}"
fi
curl -s -X POST \
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${BUILD_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@${file}" \
"${REPO_API}/releases/${RELEASE_ID}/assets?name=${filename}"
-T "$file" \
"${REPO_API}/releases/${RELEASE_ID}/assets?name=${encoded_name}")
echo "Upload response: HTTP ${HTTP_CODE}"
done

View File

@@ -2,10 +2,7 @@ name: Build Windows
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
env:
PYTHON_VERSION: "3.11"
@@ -13,12 +10,13 @@ env:
TARGET: x86_64-pc-windows-msvc
jobs:
build-sidecar:
name: Build sidecar (Windows)
build:
name: Build (Windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
# ── Python sidecar ──
- name: Install uv
shell: powershell
run: |
@@ -42,20 +40,12 @@ jobs:
working-directory: python
run: uv run --python ${{ env.PYTHON_VERSION }} python build_sidecar.py --cpu-only
- name: Upload sidecar artifact
uses: actions/upload-artifact@v3
with:
name: sidecar-windows
path: python/dist/voice-to-notes-sidecar/
retention-days: 7
build-app:
name: Build app (Windows)
needs: build-sidecar
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Package sidecar for Tauri
shell: powershell
run: |
Compress-Archive -Path python\dist\voice-to-notes-sidecar\* -DestinationPath src-tauri\sidecar.zip
# ── Tauri app ──
- name: Set up Node.js
uses: actions/setup-node@v4
with:
@@ -72,12 +62,6 @@ jobs:
echo "$env:USERPROFILE\.cargo\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
}
- name: Download sidecar artifact
uses: actions/download-artifact@v3
with:
name: sidecar-windows
path: src-tauri/binaries/
- name: Install npm dependencies
shell: powershell
run: npm ci
@@ -85,51 +69,41 @@ jobs:
- name: Build Tauri app
shell: powershell
run: npm run tauri build
env:
TAURI_CONFIG: '{"bundle":{"externalBin":["binaries/voice-to-notes-sidecar"]}}'
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: app-windows
path: |
src-tauri/target/release/bundle/msi/*.msi
src-tauri/target/release/bundle/nsis/*.exe
retention-days: 30
release:
name: Release (Windows)
needs: build-app
if: github.ref == 'refs/heads/main'
runs-on: windows-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: app-windows
path: artifacts/
- name: Create or update release
# ── Release ──
- name: Upload to release
shell: powershell
env:
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
run: |
$TAG = "latest"
$REPO_API = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
$Headers = @{ "Authorization" = "token $env:BUILD_TOKEN" }
# Check if release exists
try {
$release = Invoke-RestMethod -Uri "${REPO_API}/releases/tags/${TAG}" -Headers $Headers -ErrorAction Stop
$RELEASE_ID = $release.id
} catch {
# Create new release
$TAG = "${{ github.ref_name }}"
$RELEASE_NAME = "Voice to Notes ${TAG}"
Write-Host "Release tag: ${TAG}"
# Wait for release to be created by the release workflow
$RELEASE_ID = $null
for ($i = 1; $i -le 5; $i++) {
try {
$release = Invoke-RestMethod -Uri "${REPO_API}/releases/tags/${TAG}" -Headers $Headers -ErrorAction Stop
$RELEASE_ID = $release.id
break
} catch {
Write-Host "Release not found yet, waiting 10s... (attempt $i)"
Start-Sleep -Seconds 10
}
}
# Fallback: create release if it still doesn't exist
if (-not $RELEASE_ID) {
$body = @{
tag_name = $TAG
name = "Voice to Notes (Latest Build)"
body = "Latest automated build from main branch."
name = $RELEASE_NAME
body = "Automated build."
draft = $false
prerelease = $true
prerelease = $false
} | ConvertTo-Json
$release = Invoke-RestMethod -Uri "${REPO_API}/releases" -Method Post -Headers $Headers -ContentType "application/json" -Body $body
$RELEASE_ID = $release.id
@@ -137,12 +111,12 @@ jobs:
Write-Host "Release ID: ${RELEASE_ID}"
# Upload artifacts
Get-ChildItem -Path artifacts -Recurse -Include *.msi,*.exe | ForEach-Object {
Get-ChildItem -Path src-tauri\target\release\bundle -Recurse -Include *.msi,*-setup.exe | ForEach-Object {
$filename = $_.Name
Write-Host "Uploading ${filename}..."
$encodedName = [System.Uri]::EscapeDataString($filename)
$size = [math]::Round($_.Length / 1MB, 1)
Write-Host "Uploading ${filename} (${size} MB)..."
# Delete existing asset with same name
try {
$assets = Invoke-RestMethod -Uri "${REPO_API}/releases/${RELEASE_ID}/assets" -Headers $Headers
$existing = $assets | Where-Object { $_.name -eq $filename }
@@ -151,8 +125,17 @@ jobs:
}
} catch {}
# Upload
Invoke-RestMethod -Uri "${REPO_API}/releases/${RELEASE_ID}/assets?name=${filename}" `
-Method Post -Headers $Headers -ContentType "application/octet-stream" `
-InFile $_.FullName
# Use curl for streaming upload (Invoke-RestMethod fails on large files)
$uploadUrl = "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encodedName}"
$result = curl.exe --fail --silent --show-error `
-X POST `
-H "Authorization: token $env:BUILD_TOKEN" `
-H "Content-Type: application/octet-stream" `
--data-binary "@$($_.FullName)" `
"$uploadUrl" 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "Upload successful: ${filename}"
} else {
Write-Host "WARNING: Upload failed for ${filename}: ${result}"
}
}

View File

@@ -0,0 +1,77 @@
name: Release
on:
push:
branches: [main]
jobs:
bump-version:
name: Bump version and tag
# Skip if this is a version-bump commit (avoid infinite loop)
if: "!contains(github.event.head_commit.message, '[skip ci]')"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure git
run: |
git config user.name "Gitea Actions"
git config user.email "actions@gitea.local"
- name: Bump patch version
run: |
# Read current version from package.json
CURRENT=$(grep '"version"' package.json | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/')
echo "Current version: ${CURRENT}"
# Increment patch number
MAJOR=$(echo "${CURRENT}" | cut -d. -f1)
MINOR=$(echo "${CURRENT}" | cut -d. -f2)
PATCH=$(echo "${CURRENT}" | cut -d. -f3)
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
echo "New version: ${NEW_VERSION}"
# Update package.json
sed -i "s/\"version\": \"${CURRENT}\"/\"version\": \"${NEW_VERSION}\"/" package.json
# Update src-tauri/tauri.conf.json
sed -i "s/\"version\": \"${CURRENT}\"/\"version\": \"${NEW_VERSION}\"/" src-tauri/tauri.conf.json
# Update src-tauri/Cargo.toml (match version = "x.y.z" in [package] section)
sed -i "s/^version = \"${CURRENT}\"/version = \"${NEW_VERSION}\"/" src-tauri/Cargo.toml
# Update python/pyproject.toml
sed -i "s/^version = \".*\"/version = \"${NEW_VERSION}\"/" python/pyproject.toml
echo "NEW_VERSION=${NEW_VERSION}" >> $GITHUB_ENV
- name: Commit and tag
env:
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
run: |
git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml python/pyproject.toml
git commit -m "chore: bump version to ${NEW_VERSION} [skip ci]"
git tag "v${NEW_VERSION}"
# Push using token for authentication
REMOTE_URL=$(git remote get-url origin | sed "s|://|://gitea-actions:${BUILD_TOKEN}@|")
git push "${REMOTE_URL}" HEAD:main
git push "${REMOTE_URL}" "v${NEW_VERSION}"
- name: Create Gitea release
env:
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
run: |
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
TAG="v${NEW_VERSION}"
RELEASE_NAME="Voice to Notes ${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}" \
"${REPO_API}/releases"
echo "Created release: ${RELEASE_NAME}"

1
.gitignore vendored
View File

@@ -50,5 +50,6 @@ Thumbs.db
# Sidecar build artifacts
src-tauri/binaries/*
!src-tauri/binaries/.gitkeep
src-tauri/sidecar.zip
python/dist/
python/build/

View File

@@ -1,6 +1,6 @@
{
"name": "voice-to-notes",
"version": "0.1.0",
"version": "0.2.1",
"description": "Desktop app for transcribing audio/video with speaker identification",
"type": "module",
"scripts": {

View File

@@ -236,10 +236,9 @@ def main() -> None:
python = create_venv_and_install(cpu_only)
output_dir = run_pyinstaller(python)
download_ffmpeg(output_dir)
rename_binary(output_dir, target_triple)
print(f"\n[build] Done! Sidecar built at: {output_dir}")
print(f"[build] Copy contents to src-tauri/binaries/ for Tauri bundling")
print(f"[build] Copy directory to src-tauri/sidecar/ for Tauri resource bundling")
if __name__ == "__main__":

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "voice-to-notes"
version = "0.1.0"
version = "0.2.1"
description = "Python sidecar for Voice to Notes — transcription, diarization, and AI services"
requires-python = ">=3.11"
license = "MIT"

View File

@@ -1,6 +1,6 @@
[package]
name = "voice-to-notes"
version = "0.1.0"
version = "0.2.1"
description = "Voice to Notes — desktop transcription with speaker identification"
authors = ["Voice to Notes Contributors"]
license = "MIT"
@@ -20,6 +20,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
rusqlite = { version = "0.31", features = ["bundled"] }
uuid = { version = "1", features = ["v4", "serde"] }
zip = { version = "2", default-features = false, features = ["deflate"] }
thiserror = "1"
chrono = { version = "0.4", features = ["serde"] }
tauri-plugin-dialog = "2.6.0"

View File

@@ -1,3 +1,21 @@
fn main() {
// Ensure sidecar.zip exists so tauri-build doesn't fail.
// CI replaces this placeholder with the real PyInstaller sidecar archive.
let zip_path = std::path::Path::new("sidecar.zip");
if !zip_path.exists() {
// Minimal valid zip (empty archive): end-of-central-directory record
let empty_zip: [u8; 22] = [
0x50, 0x4b, 0x05, 0x06, // EOCD signature
0x00, 0x00, // disk number
0x00, 0x00, // disk with central dir
0x00, 0x00, // entries on this disk
0x00, 0x00, // total entries
0x00, 0x00, 0x00, 0x00, // central dir size
0x00, 0x00, 0x00, 0x00, // central dir offset
0x00, 0x00, // comment length
];
std::fs::write(zip_path, empty_zip).expect("Failed to create placeholder sidecar.zip");
}
tauri_build::build()
}

View File

@@ -27,6 +27,14 @@ pub fn run() {
.plugin(tauri_plugin_dialog::init())
.manage(app_state)
.setup(|app| {
// Tell the sidecar manager where Tauri placed bundled resources
// and where to extract the sidecar archive
if let (Ok(resource_dir), Ok(data_dir)) =
(app.path().resource_dir(), app.path().app_local_data_dir())
{
sidecar::init_dirs(resource_dir, data_dir);
}
// Set the webview background to match the app's dark theme
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_background_color(Some(Color(10, 10, 35, 255)));

View File

@@ -2,11 +2,24 @@ pub mod ipc;
pub mod messages;
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::{Mutex, OnceLock};
use crate::sidecar::messages::IPCMessage;
/// Resource directory set by the Tauri app during setup.
static RESOURCE_DIR: OnceLock<PathBuf> = OnceLock::new();
/// App data directory for extracting the sidecar archive.
static DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
/// Initialize directories for sidecar resolution.
/// Must be called from the Tauri setup before any sidecar operations.
pub fn init_dirs(resource_dir: PathBuf, data_dir: PathBuf) {
RESOURCE_DIR.set(resource_dir).ok();
DATA_DIR.set(data_dir).ok();
}
/// Get the global sidecar manager singleton.
pub fn sidecar() -> &'static SidecarManager {
static INSTANCE: OnceLock<SidecarManager> = OnceLock::new();
@@ -41,37 +54,131 @@ impl SidecarManager {
}
/// Resolve the frozen sidecar binary path (production mode).
fn resolve_sidecar_path() -> Result<std::path::PathBuf, String> {
let exe = std::env::current_exe().map_err(|e| format!("Cannot get current exe: {e}"))?;
let exe_dir = exe
.parent()
.ok_or_else(|| "Cannot get exe parent directory".to_string())?;
///
/// First checks if the sidecar is already extracted to the app data directory.
/// If not, looks for `sidecar.zip` in the Tauri resource directory and extracts it.
fn resolve_sidecar_path() -> Result<PathBuf, String> {
let binary_name = if cfg!(target_os = "windows") {
"voice-to-notes-sidecar.exe"
} else {
"voice-to-notes-sidecar"
};
// Tauri places externalBin next to the app binary
let path = exe_dir.join(binary_name);
if path.exists() {
return Ok(path);
// Versioned extraction directory prevents stale sidecar after app updates
let extract_dir = DATA_DIR
.get()
.ok_or("App data directory not initialized")?
.join(format!("sidecar-{}", env!("CARGO_PKG_VERSION")));
let binary_path = extract_dir.join(binary_name);
// Already extracted — use it directly
if binary_path.exists() {
return Ok(binary_path);
}
// Also check inside a subdirectory (onedir PyInstaller output)
let subdir_path = exe_dir.join("voice-to-notes-sidecar").join(binary_name);
if subdir_path.exists() {
return Ok(subdir_path);
// Find sidecar.zip in resource dir or next to exe
let zip_path = Self::find_sidecar_zip()?;
Self::extract_zip(&zip_path, &extract_dir)?;
if !binary_path.exists() {
return Err(format!(
"Sidecar binary not found after extraction at {}",
binary_path.display()
));
}
// Make executable on Unix
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = std::fs::metadata(&binary_path) {
let mut perms = meta.permissions();
perms.set_mode(0o755);
let _ = std::fs::set_permissions(&binary_path, perms);
}
}
Ok(binary_path)
}
/// Locate the bundled sidecar.zip archive.
fn find_sidecar_zip() -> Result<PathBuf, String> {
let mut candidates: Vec<PathBuf> = Vec::new();
if let Some(resource_dir) = RESOURCE_DIR.get() {
candidates.push(resource_dir.join("sidecar.zip"));
}
if let Ok(exe) = std::env::current_exe() {
if let Some(exe_dir) = exe.parent() {
candidates.push(exe_dir.join("sidecar.zip"));
}
}
for path in &candidates {
if path.exists() {
return Ok(path.clone());
}
}
Err(format!(
"Sidecar binary not found. Looked for:\n {}\n {}",
path.display(),
subdir_path.display(),
"Sidecar archive not found. Checked:\n{}",
candidates
.iter()
.map(|p| format!(" {}", p.display()))
.collect::<Vec<_>>()
.join("\n"),
))
}
/// Extract a zip archive to the given directory.
fn extract_zip(zip_path: &Path, dest: &Path) -> Result<(), String> {
eprintln!(
"[sidecar-rs] Extracting sidecar from {} to {}",
zip_path.display(),
dest.display()
);
// Clean destination so we don't mix old and new files
if dest.exists() {
std::fs::remove_dir_all(dest)
.map_err(|e| format!("Failed to clean extraction dir: {e}"))?;
}
std::fs::create_dir_all(dest)
.map_err(|e| format!("Failed to create extraction dir: {e}"))?;
let file =
std::fs::File::open(zip_path).map_err(|e| format!("Cannot open sidecar zip: {e}"))?;
let mut archive =
zip::ZipArchive::new(file).map_err(|e| format!("Invalid sidecar zip: {e}"))?;
for i in 0..archive.len() {
let mut entry = archive
.by_index(i)
.map_err(|e| format!("Zip entry error: {e}"))?;
let name = entry.name().to_string();
let outpath = dest.join(&name);
if entry.is_dir() {
std::fs::create_dir_all(&outpath)
.map_err(|e| format!("Cannot create dir {}: {e}", outpath.display()))?;
} else {
if let Some(parent) = outpath.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("Cannot create dir {}: {e}", parent.display()))?;
}
let mut outfile = std::fs::File::create(&outpath)
.map_err(|e| format!("Cannot create {}: {e}", outpath.display()))?;
std::io::copy(&mut entry, &mut outfile)
.map_err(|e| format!("Write error for {}: {e}", name))?;
}
}
eprintln!("[sidecar-rs] Sidecar extracted successfully");
Ok(())
}
/// Find a working Python command for the current platform.
fn find_python_command() -> &'static str {
if cfg!(target_os = "windows") {
@@ -114,15 +221,8 @@ impl SidecarManager {
if Self::is_dev_mode() {
self.start_python_dev()
} else {
match Self::resolve_sidecar_path() {
Ok(path) => self.start_binary(&path),
Err(e) => {
eprintln!(
"[sidecar-rs] Frozen binary not found ({e}), falling back to dev mode"
);
self.start_python_dev()
}
}
let path = Self::resolve_sidecar_path()?;
self.start_binary(&path)
}
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Voice to Notes",
"version": "0.1.0",
"version": "0.2.1",
"identifier": "com.voicetonotes.app",
"build": {
"beforeDevCommand": "npm run dev",
@@ -31,7 +31,7 @@
},
"bundle": {
"active": true,
"targets": "all",
"targets": ["deb", "nsis", "msi", "dmg"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
@@ -42,14 +42,12 @@
"category": "Utility",
"shortDescription": "Transcribe audio/video with speaker identification",
"longDescription": "Voice to Notes is a desktop application that transcribes audio and video recordings with speaker identification, synchronized playback, and AI-powered analysis. Export to SRT, WebVTT, ASS captions, or plain text.",
"resources": ["sidecar.zip"],
"copyright": "Voice to Notes Contributors",
"license": "MIT",
"linux": {
"deb": {
"depends": []
},
"appimage": {
"bundleMediaFramework": true
}
},
"windows": {