10 Commits

Author SHA1 Message Date
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
11 changed files with 268 additions and 213 deletions

View File

@@ -13,12 +13,13 @@ env:
TARGET: x86_64-unknown-linux-gnu TARGET: x86_64-unknown-linux-gnu
jobs: jobs:
build-sidecar: build:
name: Build sidecar (Linux) name: Build (Linux)
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# ── Python sidecar ──
- name: Install uv - name: Install uv
run: | run: |
if command -v uv &> /dev/null; then if command -v uv &> /dev/null; then
@@ -38,20 +39,11 @@ jobs:
working-directory: python working-directory: python
run: uv run --python ${{ env.PYTHON_VERSION }} python build_sidecar.py --cpu-only run: uv run --python ${{ env.PYTHON_VERSION }} python build_sidecar.py --cpu-only
- name: Upload sidecar artifact - name: Package sidecar for Tauri
uses: actions/upload-artifact@v3 run: |
with: cd python/dist/voice-to-notes-sidecar && zip -r ../../../src-tauri/sidecar.zip .
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
# ── Tauri app ──
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
@@ -64,67 +56,44 @@ jobs:
- name: Install system dependencies - name: Install system dependencies
run: | run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils 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 - name: Install npm dependencies
run: npm ci run: npm ci
- name: Build Tauri app - name: Build Tauri app
run: npm run tauri build run: npm run tauri build
env:
TAURI_CONFIG: '{"bundle":{"externalBin":["binaries/voice-to-notes-sidecar"]}}'
- name: Upload artifacts # ── Release ──
uses: actions/upload-artifact@v3 - name: Upload to release
with: if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
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
env: env:
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
run: | run: |
TAG="latest" 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}"
# Check if release exists # Use version tag for tag pushes, "latest" for main
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
TAG="${GITHUB_REF#refs/tags/}"
RELEASE_NAME="Voice to Notes ${TAG}"
PRERELEASE=false
else
TAG="latest"
RELEASE_NAME="Voice to Notes (Latest Build)"
PRERELEASE=true
fi
echo "Release tag: ${TAG}"
RELEASE_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ RELEASE_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
"${REPO_API}/releases/tags/${TAG}" | jq -r '.id // empty') "${REPO_API}/releases/tags/${TAG}" | jq -r '.id // empty')
if [ -z "${RELEASE_ID}" ]; then if [ -z "${RELEASE_ID}" ]; then
# Create new release
RELEASE_ID=$(curl -s -X POST \ RELEASE_ID=$(curl -s -X POST \
-H "Authorization: token ${BUILD_TOKEN}" \ -H "Authorization: token ${BUILD_TOKEN}" \
-H "Content-Type: application/json" \ -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\": ${PRERELEASE}}" \
"${REPO_API}/releases" | jq -r '.id') "${REPO_API}/releases" | jq -r '.id')
fi fi
@@ -134,12 +103,11 @@ jobs:
exit 1 exit 1
fi fi
# Upload artifacts (delete existing ones with same name first) find src-tauri/target/release/bundle -type f -name "*.deb" | while IFS= read -r file; do
find artifacts/ -type f \( -name "*.deb" -o -name "*.AppImage" \) | while read file; do
filename=$(basename "$file") 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}" \ ASSET_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
"${REPO_API}/releases/${RELEASE_ID}/assets" | jq -r ".[] | select(.name == \"${filename}\") | .id // empty") "${REPO_API}/releases/${RELEASE_ID}/assets" | jq -r ".[] | select(.name == \"${filename}\") | .id // empty")
if [ -n "${ASSET_ID}" ]; then if [ -n "${ASSET_ID}" ]; then
@@ -147,9 +115,10 @@ jobs:
"${REPO_API}/releases/${RELEASE_ID}/assets/${ASSET_ID}" "${REPO_API}/releases/${RELEASE_ID}/assets/${ASSET_ID}"
fi fi
curl -s -X POST \ HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${BUILD_TOKEN}" \ -H "Authorization: token ${BUILD_TOKEN}" \
-H "Content-Type: application/octet-stream" \ -H "Content-Type: application/octet-stream" \
--data-binary "@${file}" \ -T "$file" \
"${REPO_API}/releases/${RELEASE_ID}/assets?name=${filename}" "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encoded_name}")
echo "Upload response: HTTP ${HTTP_CODE}"
done done

View File

@@ -13,12 +13,13 @@ env:
TARGET: aarch64-apple-darwin TARGET: aarch64-apple-darwin
jobs: jobs:
build-sidecar: build:
name: Build sidecar (macOS) name: Build (macOS)
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# ── Python sidecar ──
- name: Install uv - name: Install uv
run: | run: |
if command -v uv &> /dev/null; then if command -v uv &> /dev/null; then
@@ -38,20 +39,11 @@ jobs:
working-directory: python working-directory: python
run: uv run --python ${{ env.PYTHON_VERSION }} python build_sidecar.py --cpu-only run: uv run --python ${{ env.PYTHON_VERSION }} python build_sidecar.py --cpu-only
- name: Upload sidecar artifact - name: Package sidecar for Tauri
uses: actions/upload-artifact@v3 run: |
with: cd python/dist/voice-to-notes-sidecar && zip -r ../../../src-tauri/sidecar.zip .
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
# ── Tauri app ──
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
@@ -65,52 +57,33 @@ jobs:
- name: Install system dependencies - name: Install system dependencies
run: brew install --quiet create-dmg || true 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 - name: Install npm dependencies
run: npm ci run: npm ci
- name: Build Tauri app - name: Build Tauri app
run: npm run tauri build run: npm run tauri build
env:
TAURI_CONFIG: '{"bundle":{"externalBin":["binaries/voice-to-notes-sidecar"]}}'
- name: Upload artifacts # ── Release ──
uses: actions/upload-artifact@v3 - name: Upload to release
with: if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
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
env: env:
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
run: | run: |
TAG="latest"
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
# Check if release exists # Use version tag for tag pushes, "latest" for main
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
TAG="${GITHUB_REF#refs/tags/}"
RELEASE_NAME="Voice to Notes ${TAG}"
PRERELEASE=false
else
TAG="latest"
RELEASE_NAME="Voice to Notes (Latest Build)"
PRERELEASE=true
fi
echo "Release tag: ${TAG}"
RELEASE_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ RELEASE_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
"${REPO_API}/releases/tags/${TAG}" | jq -r '.id // empty') "${REPO_API}/releases/tags/${TAG}" | jq -r '.id // empty')
@@ -118,7 +91,7 @@ jobs:
RELEASE_ID=$(curl -s -X POST \ RELEASE_ID=$(curl -s -X POST \
-H "Authorization: token ${BUILD_TOKEN}" \ -H "Authorization: token ${BUILD_TOKEN}" \
-H "Content-Type: application/json" \ -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\": ${PRERELEASE}}" \
"${REPO_API}/releases" | jq -r '.id') "${REPO_API}/releases" | jq -r '.id')
fi fi
@@ -128,9 +101,10 @@ jobs:
exit 1 exit 1
fi 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") 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}" \ ASSET_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
"${REPO_API}/releases/${RELEASE_ID}/assets" | jq -r ".[] | select(.name == \"${filename}\") | .id // empty") "${REPO_API}/releases/${RELEASE_ID}/assets" | jq -r ".[] | select(.name == \"${filename}\") | .id // empty")
@@ -139,9 +113,10 @@ jobs:
"${REPO_API}/releases/${RELEASE_ID}/assets/${ASSET_ID}" "${REPO_API}/releases/${RELEASE_ID}/assets/${ASSET_ID}"
fi fi
curl -s -X POST \ HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${BUILD_TOKEN}" \ -H "Authorization: token ${BUILD_TOKEN}" \
-H "Content-Type: application/octet-stream" \ -H "Content-Type: application/octet-stream" \
--data-binary "@${file}" \ -T "$file" \
"${REPO_API}/releases/${RELEASE_ID}/assets?name=${filename}" "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encoded_name}")
echo "Upload response: HTTP ${HTTP_CODE}"
done done

View File

@@ -13,12 +13,13 @@ env:
TARGET: x86_64-pc-windows-msvc TARGET: x86_64-pc-windows-msvc
jobs: jobs:
build-sidecar: build:
name: Build sidecar (Windows) name: Build (Windows)
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# ── Python sidecar ──
- name: Install uv - name: Install uv
shell: powershell shell: powershell
run: | run: |
@@ -42,20 +43,12 @@ jobs:
working-directory: python working-directory: python
run: uv run --python ${{ env.PYTHON_VERSION }} python build_sidecar.py --cpu-only run: uv run --python ${{ env.PYTHON_VERSION }} python build_sidecar.py --cpu-only
- name: Upload sidecar artifact - name: Package sidecar for Tauri
uses: actions/upload-artifact@v3 shell: powershell
with: run: |
name: sidecar-windows Compress-Archive -Path python\dist\voice-to-notes-sidecar\* -DestinationPath src-tauri\sidecar.zip
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
# ── Tauri app ──
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
@@ -72,12 +65,6 @@ jobs:
echo "$env:USERPROFILE\.cargo\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append 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 - name: Install npm dependencies
shell: powershell shell: powershell
run: npm ci run: npm ci
@@ -85,51 +72,41 @@ jobs:
- name: Build Tauri app - name: Build Tauri app
shell: powershell shell: powershell
run: npm run tauri build run: npm run tauri build
env:
TAURI_CONFIG: '{"bundle":{"externalBin":["binaries/voice-to-notes-sidecar"]}}'
- name: Upload artifacts # ── Release ──
uses: actions/upload-artifact@v3 - name: Upload to release
with: if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
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
shell: powershell shell: powershell
env: env:
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
run: | run: |
$TAG = "latest"
$REPO_API = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}" $REPO_API = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
$Headers = @{ "Authorization" = "token $env:BUILD_TOKEN" } $Headers = @{ "Authorization" = "token $env:BUILD_TOKEN" }
# Check if release exists # Use version tag for tag pushes, "latest" for main
$REF = "${{ github.ref }}"
if ($REF.StartsWith("refs/tags/")) {
$TAG = $REF.Replace("refs/tags/", "")
$RELEASE_NAME = "Voice to Notes ${TAG}"
$PRERELEASE = $false
} else {
$TAG = "latest"
$RELEASE_NAME = "Voice to Notes (Latest Build)"
$PRERELEASE = $true
}
Write-Host "Release tag: ${TAG}"
try { try {
$release = Invoke-RestMethod -Uri "${REPO_API}/releases/tags/${TAG}" -Headers $Headers -ErrorAction Stop $release = Invoke-RestMethod -Uri "${REPO_API}/releases/tags/${TAG}" -Headers $Headers -ErrorAction Stop
$RELEASE_ID = $release.id $RELEASE_ID = $release.id
} catch { } catch {
# Create new release
$body = @{ $body = @{
tag_name = $TAG tag_name = $TAG
name = "Voice to Notes (Latest Build)" name = $RELEASE_NAME
body = "Latest automated build from main branch." body = "Automated build."
draft = $false draft = $false
prerelease = $true prerelease = $PRERELEASE
} | ConvertTo-Json } | ConvertTo-Json
$release = Invoke-RestMethod -Uri "${REPO_API}/releases" -Method Post -Headers $Headers -ContentType "application/json" -Body $body $release = Invoke-RestMethod -Uri "${REPO_API}/releases" -Method Post -Headers $Headers -ContentType "application/json" -Body $body
$RELEASE_ID = $release.id $RELEASE_ID = $release.id
@@ -137,12 +114,12 @@ jobs:
Write-Host "Release ID: ${RELEASE_ID}" Write-Host "Release ID: ${RELEASE_ID}"
# Upload artifacts Get-ChildItem -Path src-tauri\target\release\bundle -Recurse -Include *.msi,*-setup.exe | ForEach-Object {
Get-ChildItem -Path artifacts -Recurse -Include *.msi,*.exe | ForEach-Object {
$filename = $_.Name $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 { try {
$assets = Invoke-RestMethod -Uri "${REPO_API}/releases/${RELEASE_ID}/assets" -Headers $Headers $assets = Invoke-RestMethod -Uri "${REPO_API}/releases/${RELEASE_ID}/assets" -Headers $Headers
$existing = $assets | Where-Object { $_.name -eq $filename } $existing = $assets | Where-Object { $_.name -eq $filename }
@@ -151,8 +128,17 @@ jobs:
} }
} catch {} } catch {}
# Upload # Use curl for streaming upload (Invoke-RestMethod fails on large files)
Invoke-RestMethod -Uri "${REPO_API}/releases/${RELEASE_ID}/assets?name=${filename}" ` $uploadUrl = "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encodedName}"
-Method Post -Headers $Headers -ContentType "application/octet-stream" ` $result = curl.exe --fail --silent --show-error `
-InFile $_.FullName -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}"
}
} }

1
.gitignore vendored
View File

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

View File

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

View File

@@ -236,10 +236,9 @@ def main() -> None:
python = create_venv_and_install(cpu_only) python = create_venv_and_install(cpu_only)
output_dir = run_pyinstaller(python) output_dir = run_pyinstaller(python)
download_ffmpeg(output_dir) download_ffmpeg(output_dir)
rename_binary(output_dir, target_triple)
print(f"\n[build] Done! Sidecar built at: {output_dir}") 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__": if __name__ == "__main__":

View File

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

View File

@@ -1,3 +1,21 @@
fn main() { 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() tauri_build::build()
} }

View File

@@ -27,6 +27,14 @@ pub fn run() {
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.manage(app_state) .manage(app_state)
.setup(|app| { .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 // Set the webview background to match the app's dark theme
if let Some(window) = app.get_webview_window("main") { if let Some(window) = app.get_webview_window("main") {
let _ = window.set_background_color(Some(Color(10, 10, 35, 255))); let _ = window.set_background_color(Some(Color(10, 10, 35, 255)));

View File

@@ -2,11 +2,24 @@ pub mod ipc;
pub mod messages; pub mod messages;
use std::io::{BufRead, BufReader, Write}; use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, Command, Stdio}; use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::{Mutex, OnceLock}; use std::sync::{Mutex, OnceLock};
use crate::sidecar::messages::IPCMessage; 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. /// Get the global sidecar manager singleton.
pub fn sidecar() -> &'static SidecarManager { pub fn sidecar() -> &'static SidecarManager {
static INSTANCE: OnceLock<SidecarManager> = OnceLock::new(); static INSTANCE: OnceLock<SidecarManager> = OnceLock::new();
@@ -41,37 +54,131 @@ impl SidecarManager {
} }
/// Resolve the frozen sidecar binary path (production mode). /// 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}"))?; /// First checks if the sidecar is already extracted to the app data directory.
let exe_dir = exe /// If not, looks for `sidecar.zip` in the Tauri resource directory and extracts it.
.parent() fn resolve_sidecar_path() -> Result<PathBuf, String> {
.ok_or_else(|| "Cannot get exe parent directory".to_string())?;
let binary_name = if cfg!(target_os = "windows") { let binary_name = if cfg!(target_os = "windows") {
"voice-to-notes-sidecar.exe" "voice-to-notes-sidecar.exe"
} else { } else {
"voice-to-notes-sidecar" "voice-to-notes-sidecar"
}; };
// Tauri places externalBin next to the app binary // Versioned extraction directory prevents stale sidecar after app updates
let path = exe_dir.join(binary_name); let extract_dir = DATA_DIR
if path.exists() { .get()
return Ok(path); .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) // Find sidecar.zip in resource dir or next to exe
let subdir_path = exe_dir.join("voice-to-notes-sidecar").join(binary_name); let zip_path = Self::find_sidecar_zip()?;
if subdir_path.exists() { Self::extract_zip(&zip_path, &extract_dir)?;
return Ok(subdir_path);
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!( Err(format!(
"Sidecar binary not found. Looked for:\n {}\n {}", "Sidecar archive not found. Checked:\n{}",
path.display(), candidates
subdir_path.display(), .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. /// Find a working Python command for the current platform.
fn find_python_command() -> &'static str { fn find_python_command() -> &'static str {
if cfg!(target_os = "windows") { if cfg!(target_os = "windows") {
@@ -114,15 +221,8 @@ impl SidecarManager {
if Self::is_dev_mode() { if Self::is_dev_mode() {
self.start_python_dev() self.start_python_dev()
} else { } else {
match Self::resolve_sidecar_path() { let path = Self::resolve_sidecar_path()?;
Ok(path) => self.start_binary(&path), self.start_binary(&path)
Err(e) => {
eprintln!(
"[sidecar-rs] Frozen binary not found ({e}), falling back to dev mode"
);
self.start_python_dev()
}
}
} }
} }

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Voice to Notes", "productName": "Voice to Notes",
"version": "0.1.0", "version": "0.2.0",
"identifier": "com.voicetonotes.app", "identifier": "com.voicetonotes.app",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",
@@ -31,7 +31,7 @@
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"targets": "all", "targets": ["deb", "nsis", "msi", "dmg"],
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",
@@ -42,14 +42,12 @@
"category": "Utility", "category": "Utility",
"shortDescription": "Transcribe audio/video with speaker identification", "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.", "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", "copyright": "Voice to Notes Contributors",
"license": "MIT", "license": "MIT",
"linux": { "linux": {
"deb": { "deb": {
"depends": [] "depends": []
},
"appimage": {
"bundleMediaFramework": true
} }
}, },
"windows": { "windows": {