audio_file, source_file, audio_wav are all optional with serde defaults.
v1 projects have audio_file, v2 projects have source_file + audio_wav.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Projects now save as folders containing .vtn + audio.wav:
My Transcript/
My Transcript.vtn
audio.wav
Audio handling:
- Always extract to 22kHz mono WAV on import (all formats, not just video)
- Prevents WebAudio crash from decoding large MP3/FLAC/OGG to PCM in memory
- WAV saved alongside .vtn on project save (moved from temp)
- Sidecar still uses original file (does its own conversion)
Project format v2:
- source_file: original import path (for re-extraction)
- audio_wav: relative path to extracted WAV (portable)
Re-link on open:
- If audio.wav exists → load directly
- If missing but source exists → re-extract automatically
- If both missing → dialog to locate file via file picker
- V1 project migration: extracts WAV on first open
New Rust commands: check_file_exists, copy_file, create_dir
extract_audio: now accepts optional output_path, uses 22kHz sample rate
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Files >1 hour are split into 5-minute chunks. Previously each chunk
showed "Starting transcription..." making it look like a restart.
Now shows "Chunk 3/12: Starting transcription..." and
"Chunk 3/12: Transcribing segment 5 (42% of audio)..."
Also skips the "Loading model..." message for chunks after the first
since the model is already loaded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AIChatPanel had its own hardcoded configMap with the old llama-server
URL (localhost:8080) and field names (local_model_path). Every chat
message reconfigured the provider with these wrong values, overriding
the correct settings applied at startup.
Fix: replace the duplicate with a call to the shared configureAIProvider().
Also strip trailing slashes from ollama_url before appending /v1 to
prevent double-slash URLs (http://localhost:11434//v1).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Cache loaded audio in _sf_load() — previously the entire WAV file was
re-read from disk for every 10s crop call. For a 3-hour file with
1000+ chunks, this meant ~345GB of disk reads. Now read once, cached.
- Better progress messages for long files: show elapsed time in m:ss
format, warn "(180min audio, this may take a while)" for files >10min
- Increased progress poll interval from 2s to 5s (less noise)
- Better time estimate: use 0.8x audio duration (was 0.5x)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Cancel button on the progress overlay during transcription
- Clicking Cancel shows confirmation: "Processing is incomplete. If you
cancel now, the transcription will need to be started over."
- "Continue Processing" dismisses the dialog, "Cancel Processing" stops
- Cancel clears partial results (segments, speakers) and resets UI
- Pipeline results are discarded if cancelled during processing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AI provider:
- Extract configureAIProvider() from saveSettings for reuse
- Call it on app startup after sidecar is ready (was only called on Save)
- Call it after first-time sidecar download completes
- Sidecar now receives correct Ollama URL/model immediately
Video extraction:
- Hide ffmpeg console window on Windows (CREATE_NO_WINDOW flag)
- Show "Extracting audio from video..." overlay with spinner during extraction
- UI stays responsive while ffmpeg runs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Video files (MP4, MKV, etc.) are now processed with ffmpeg to extract
audio to a temp WAV file before loading into wavesurfer. This prevents
the WebView crash caused by trying to fetch multi-GB files into memory.
- New extract_audio Tauri command uses ffmpeg (sidecar-bundled or system)
- Frontend detects video extensions and extracts audio automatically
- User-friendly error if ffmpeg is not installed with install instructions
- Reverted wavesurfer MediaElement approach in favor of clean extraction
- Added FFmpeg install guide to USER_GUIDE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The configure action registered the provider but never called
set_active(), so the sidecar kept using the old/default provider.
Also updated the local provider default from localhost:8080 to
localhost:11434/v1 (Ollama). Added debug logging for configure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diarization: Audio.crop patch now pads short segments with zeros to
match the expected duration. pyannote batches embeddings with vstack
which requires uniform tensor sizes — the last segment of a file can
be shorter than the 10s window.
CI: Reordered sidecar workflow to check for python/ changes FIRST,
before bumping version or configuring git. All subsequent steps are
gated on has_changes. This prevents unnecessary version bumps and
build runs when only app code changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Runs daily at 6am UTC and on manual dispatch. Separately tracks app
releases (v*) and sidecar releases (sidecar-v*), keeping the latest
5 of each and deleting older ones along with their tags.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
base_url was being set to 'http://localhost:11434/v1' by the frontend,
then LocalProvider appended another '/v1', resulting in '/v1/v1'.
Now the provider uses base_url directly (frontend already appends /v1).
Also fixed health check to hit Ollama root instead of /health.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous patch only replaced Audio.__call__ (segmentation), but
pyannote also calls Audio.crop during speaker embedding extraction.
crop loads a time segment of audio — patched to load full file via
soundfile then slice the tensor to the requested time range.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
soundfile was only a transitive dep of torchaudio but collect_all()
in PyInstaller can't bundle it if it's not installed. Adding it as
an explicit dependency ensures it's in the venv and bundled correctly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
soundfile needs collect_all() to include libsndfile native library —
hiddenimports alone wasn't enough, causing 'No module named soundfile'
in the frozen sidecar. This is needed for the pyannote Audio patch
that bypasses torchaudio/torchcodec.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- README: Updated to reflect current architecture (decoupled app/sidecar),
Ollama as local AI, CUDA support, split CI workflows
- USER_GUIDE.md: Complete how-to including first-time setup, transcription
workflow, speaker detection setup, Ollama configuration, export formats,
keyboard shortcuts, and troubleshooting
- CONTRIBUTING.md: Dev setup, project structure, conventions, CI/CD overview
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add min-width: 0 to flex container (allows shrinking for wrap)
- Add overflow-x: hidden to prevent horizontal scroll
- Add white-space: pre-wrap to segment text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- AI Provider: "Local (llama-server)" changed to "Ollama" with URL and
model fields (defaults to localhost:11434, llama3.2)
- Ollama connects via its OpenAI-compatible API (/v1 endpoint)
- Removed empty "Local AI" tab
- Renamed "Developer" tab to "Debug"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
torchaudio 2.10 unconditionally delegates load() to torchcodec, ignoring
the backend parameter. Since torchcodec is excluded from PyInstaller,
this broke our pyannote Audio monkey-patch.
Fix: replace torchaudio.load() with soundfile.read() + torch.from_numpy().
soundfile handles WAV natively (audio is pre-converted to WAV), has no
torchcodec dependency, and is already a transitive dependency.
Also added soundfile to PyInstaller hiddenimports.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CSP: Add blob: to connect-src/img-src/media-src for wavesurfer.js audio
playback. Add http://tauri.localhost to default-src for devtools.
pyannote: sys.modules block didn't work — pyannote still uses AudioDecoder
unconditionally. New approach: monkey-patch Audio.__call__ in diarize.py
to use torchaudio.load() directly, bypassing the broken torchcodec path.
Patch runs once before pipeline loading.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- DevTools off by default (no more auto-open on launch)
- New "Developer" tab in Settings with a checkbox to toggle devtools
- Toggle takes effect immediately (opens/closes inspector)
- Setting persists: devtools restored on next launch if enabled
- toggle_devtools Tauri command wraps window.open/close_devtools
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CSP: Add connect-src for ipc.localhost and asset.localhost so Tauri IPC
commands and local file loading (waveform, audio playback) work.
pyannote: Block torchcodec in sys.modules at startup so pyannote.audio
falls back to torchaudio for audio decoding. pyannote has a bug where
it uses AudioDecoder unconditionally even when torchcodec import fails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Enable Tauri devtools feature so right-click Inspect works in release
- Open devtools automatically on launch for debugging
- Add log_frontend command: frontend can write to ~/.voicetonotes/frontend.log
- Sidecar logs go to %LOCALAPPDATA%/com.voicetonotes.app/sidecar.log
- Frontend logs go to %USERPROFILE%/.voicetonotes/frontend.log
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
torchcodec is partially bundled but non-functional (missing FFmpeg DLLs),
causing pyannote.audio to try AudioDecoder which fails with NameError.
Excluding it forces pyannote to fall back to torchaudio for audio loading.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add git pull --rebase before push in both version bump workflows to
handle concurrent pushes from parallel workflows
- Add explicit python/ change detection in sidecar workflow (Gitea may
not support paths filter), skip all jobs if no python changes
- Gate all sidecar build jobs on has_changes output
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sidecar now has its own version (1.0.0) and release lifecycle:
- Sidecar tags: sidecar-v1.0.0, sidecar-v1.0.1, etc.
- App tags: v0.2.x (unchanged)
- Sidecar workflow triggers only on python/** changes or manual dispatch
- App release no longer bumps python/pyproject.toml
Sidecar version tracked via sidecar-version.txt in app data dir:
- resolve_sidecar_path() reads version from file instead of CARGO_PKG_VERSION
- download_sidecar() fetches latest sidecar-v* release from Gitea API
- check_sidecar_update() compares local vs remote sidecar versions
- Version file written after successful download
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CI split:
- release.yml: version bump + lightweight app builds (no Python/sidecar)
- build-sidecar.yml: builds CPU + CUDA sidecar variants per platform,
uploads as separate release assets, runs in parallel with app builds
- Sidecar workflow uses retry loop to find release (race with version bump)
Fixes:
- Add reqwest "json" feature for .json() method
- Add explicit type annotations for reqwest Response and bytes::Bytes
- Reuse client instance for download (was using reqwest::get directly)
Bundle targets: deb, rpm, nsis, msi, dmg (all formats, app is small now)
Windows upload finds both *.msi and *-setup.exe
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Major refactor: sidecar is no longer bundled in the installer. Instead,
it's downloaded on first launch with a setup screen offering CPU vs CUDA
choice. This solves the 2GB+ installer size limit and decouples app/sidecar.
Backend:
- New commands: check_sidecar, download_sidecar, check_sidecar_update
- Streaming download with progress events via reqwest
- Added reqwest + futures-util dependencies
- Removed sidecar.zip from bundle resources
- Restored NSIS target (no longer size-constrained)
CI:
- Each platform builds both CPU and CUDA sidecar variants (except macOS: CPU only)
- Sidecar zips uploaded as separate release assets
- Asset naming: sidecar-{os}-{arch}-{variant}.zip
Frontend:
- SidecarSetup.svelte: first-launch setup with CPU/CUDA radio choice,
progress bar, error/retry handling
- Update banner on launch if newer sidecar version available
- Conditional rendering: setup screen → main app flow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add rpm to bundle targets and install rpm on Linux CI
- Upload both .deb and .rpm from Linux build
- Install 7-Zip via choco if not already available on Windows runner
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace Compress-Archive (2GB limit) with 7z for sidecar packaging
- Remove NSIS from bundle targets — NSIS has a 2GB per-file limit that
breaks with CUDA-sized sidecar.zip; MSI (WiX) handles large files
by splitting into multiple CABs
- Update Windows upload to look for .msi only
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Default torch on PyPI is CPU-only on Windows. Must use PyTorch's own
package index (cu126) to get CUDA-enabled wheels. This also pins the
CUDA version on Linux for deterministic builds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Windows and Linux sidecar builds now use --with-cuda for GPU acceleration
(macOS stays CPU-only — Apple Silicon uses Metal, not CUDA)
- Windows upload switched from --data-binary to -T streaming for 2GB+ files
- Add cleanup_old_sidecars() that removes stale sidecar-* directories on
startup, keeping only the current version
- Add NSIS uninstall hook to remove sidecar data dir on Windows uninstall
(user data in ~/.voicetonotes is preserved)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- transcribe: catch model load failures on CUDA and retry with CPU
- hardware detect: test CUDA runtime actually works (torch.zeros on cuda)
before recommending GPU, since CPU-only builds detect CUDA via driver
but lack cublas/cuDNN libraries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Exclude ctranslate2.converters from PyInstaller bundle — these modules
import torch at module level causing circular import crashes, and are
only needed for model conversion (never used at runtime)
- Defer all heavy ML imports to first handler call instead of startup,
so the sidecar can send its ready message without loading torch/whisper
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix is_running() to check actual process liveness via try_wait()
instead of just checking if the handle exists
- Auto-restart sidecar on pipe errors (broken pipe, closed stdout)
with one retry attempt
- Hide sidecar console window on Windows (CREATE_NO_WINDOW flag)
- Log sidecar stderr to sidecar.log file for crash diagnostics
- Include exit status in error message when sidecar fails to start
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pushes from within a workflow don't trigger other workflows in Gitea,
so the separate tag-triggered build files never ran. Moved all 3
platform build jobs into release.yml with needs: bump-version so they
run directly after the version bump, tag, and release creation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
- 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>
Each platform (Linux, macOS, Windows) now has its own workflow file
that builds the sidecar, builds the Tauri app, and uploads to a shared
"latest" release independently. A failure on one platform no longer
blocks releases for the others.
- build-linux.yml: bash throughout, apt for deps
- build-macos.yml: bash throughout, brew for deps
- build-windows.yml: powershell throughout, choco for deps
- All use uv for Python, upload to shared "latest" release tag
- Each platform replaces its own artifacts on the release
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Linux: add xdg-utils to system deps (provides xdg-open needed by
Tauri's AppImage bundler)
- Windows: replace dtolnay/rust-toolchain action (uses bash internally)
with direct rustup install via PowerShell
- Unix: install Rust via rustup.rs shell script instead of GitHub action
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pyannote.audio requires ffmpeg at import time (torchcodec loads
FFmpeg shared libraries). Install via brew (macOS), apt (Linux),
choco (Windows) before building the sidecar.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Windows runner doesn't have bash. Split Python setup and build steps
into Unix (default shell) and Windows (powershell) variants.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- uv pip --python works better with the venv directory path than the
python binary path (avoids "No virtual environment found" on Windows)
- Add .exe suffix to Windows python path for non-uv fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- build_sidecar.py: pip_install() now includes 'install' in the command,
callers pass only package names (was doubling up as 'uv pip install install torch')
- CI: set shell: bash on uv steps so Windows doesn't try to use cmd.exe
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Unix runners use the bash install script, Windows uses the PowerShell
installer. Both check if uv is already present first.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
astral-sh/setup-uv is not available on Gitea's action registry.
Use the official install script instead, skipping if uv is already present.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CI: install uv via astral-sh/setup-uv, use uv to install Python
and run the build script (replaces setup-python which fails on
self-hosted macOS runners)
- build_sidecar.py: auto-detects uv and uses it for venv creation
and package installation (much faster), falls back to standard
venv + pip when uv is not available
- Remove .github/workflows duplicate
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Set AGENT_TOOLSDIRECTORY via step-level env on setup-python (not
GITHUB_ENV which only applies to subsequent steps)
- Use runner.temp for toolcache dir (always writable, no sudo needed)
- Remove .github/workflows/build.yml to prevent duplicate CI runs
- Remove unused Windows env check step
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Set AGENT_TOOLSDIRECTORY to a workspace-local path so setup-python
doesn't need /Users/runner or sudo access.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The app name is already in the window title bar, so the in-header
"Voice to Notes" heading was redundant and had poor contrast.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When editing a segment, word timing is now intelligently redistributed:
- Spelling fixes (same word count): each word keeps its original timing
- Word splits (e.g. "gonna" → "going to"): original word's time range
is divided proportionally across the new words
- Inserted words: timing interpolated from neighboring words
- Deleted words: remaining words keep their timing, gaps collapse
This preserves click-to-seek accuracy for common edits like fixing
misheard words or splitting concatenated words.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the edited text has the same word count as the original (e.g. fixing
"Whisper" to "wisper"), each word keeps its original start/end timestamps.
Only falls back to segment-level timing when words are added or removed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The display renders segment.words (not segment.text), so editing the text
field alone had no visible effect. Now finishEditing() rebuilds the words
array from the edited text so the change is immediately visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Project files (.vtn):
- Save Project: serializes transcript, speakers, audio path to JSON file
- Open Project: loads .vtn file, restores audio/transcript/speakers
- User chooses filename and location via save dialog
- Replaces SQLite-based project persistence (DB commands remain for future use)
- Text edits update in-memory store immediately, persist on explicit save
- Fix Windows path separator in project name extraction
AI chat:
- Markdown rendering in assistant messages (headers, lists, bold, code)
- Better visual distinction with border-left accents
- Styled markdown elements for dark theme
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add update_segment Tauri command (calls existing update_segment_text query)
- Wire onTextEdit handler from TranscriptEditor to invoke update_segment
- Edits are saved to SQLite immediately when user presses Enter
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Project persistence:
- save_project_transcript command: persists segments, speakers, words to SQLite
- load_project_transcript command: loads full transcript with nested words
- delete_project command: soft-delete projects
- Auto-save after pipeline completes (named from filename)
- Project dropdown in header to switch between saved transcripts
- Projects load audio, segments, and speakers from database
AI chat improvements:
- Markdown rendering in assistant messages (headers, lists, bold, italic, code)
- Better message spacing and visual distinction (border-left accents)
- Styled markdown elements matching dark theme
- Improved empty state and quick action button sizing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
upload-artifact@v4 and download-artifact@v4 require GitHub's backend
and are not supported on Gitea. v3 works with Gitea Actions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Create /Users/runner directory on macOS before setup-python (permission fix)
- Use `python -m pip` everywhere instead of calling pip directly (Windows fix)
- Refactor build_sidecar.py to use pip_install() helper via python -m pip
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- pip/setuptools/wheel for sidecar build step
- jq/curl for release API calls
- create-dmg for macOS bundling
- Linux system deps (gtk, webkit, patchelf)
- Validation check on release creation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Creates a pre-release with all platform artifacts on every push to main.
Uses BUILD_TOKEN secret for Gitea API authentication.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the blocking pipeline() call to a daemon thread and emit estimated
progress messages every 2 seconds from the main thread. The progress
estimate uses audio duration to calibrate the expected total time.
Also pass audio_duration_sec from PipelineService to DiarizeService.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split files >1 hour into 5-minute chunks via ffmpeg, transcribe each
chunk independently, then merge results with corrected timestamps.
Also add chunk-level progress markers every 10 segments for all files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, progress messages were only sent every 5th segment due to
a `segment_count % 5` guard. This made the UI feel unresponsive for
short recordings with few segments. Now every segment emits a progress
update with a more descriptive message including the segment number
and audio percentage.
Adds a test verifying that all 8 mock segments produce progress
messages, not just every 5th.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>