Skip to content

feat(export): export transcripts to .txt and .vtt (#441)#478

Open
tayyub-ai wants to merge 9 commits into
Zackriya-Solutions:devtestfrom
tayyub-ai:feat/441-export-transcription-upstream
Open

feat(export): export transcripts to .txt and .vtt (#441)#478
tayyub-ai wants to merge 9 commits into
Zackriya-Solutions:devtestfrom
tayyub-ai:feat/441-export-transcription-upstream

Conversation

@tayyub-ai
Copy link
Copy Markdown

Description

Adds an Export dropdown to the meeting transcript toolbar with two options: plain text (.txt) and WebVTT (.vtt). Both formats are built in Rust (unit-tested) and written to a user-chosen path via the existing tauri-plugin-dialog save dialog. Disabled while recording or when no transcripts exist.

.txt format

[00:01:23] We should ship the feature.
[00:01:30] Agreed.

One line per segment, sorted by audio_start_time. Timestamp bracket omitted when the segment has no timing (e.g. imported audio). Internal newlines collapsed.

.vtt format

Standard WebVTT v1 with &, <, > escaping. End-time fallback chain: audio_end_timeaudio_start_time + durationaudio_start_time + 5.0. Segments without a start time are skipped and the count is surfaced in a sonner warning toast. Overlapping mic/system audio segments are kept as distinct cues.

Default filename

<sanitized meeting title> - <YYYY-MM-DD>.{txt|vtt} (date from meeting.created_at).

Related Issue

Fixes #441

Type of Change

  • New feature

Testing

  • 20 Rust unit tests added in frontend/src-tauri/src/api/export.rs:
    • format_txt (7): happy path, NULL timestamp, newline collapse, ordering, hour rollover, empty input, trailing newline invariant
    • format_vtt (8): happy path, duration fallback, 5s fallback, NULL-start skipping, character escaping, overlapping cues, hour-boundary formatting, empty input
    • sanitize_filename_stem (5): banned characters, whitespace collapse, empty fallback, whitespace-only fallback, 80-char truncation
  • pnpm run build passes (TypeScript + Next.js static generation)
  • Fork CI green on macOS + Linux 22.04 + Linux 24.04 (see notes on Windows below)
  • Manual VLC verification not performed by contributor — my dev environment lacks a full local Tauri build setup; would appreciate a maintainer with a configured macOS dev env loading an exported .vtt into VLC against a real recording to confirm timing sync.

Documentation

  • No user-facing docs change needed (button is self-explanatory)

Notes for reviewers

Windows CI failure is pre-existing and unrelated. The Windows job fails inside llama-cpp-sys-2 v0.1.146's build script when CMake tries to build the bundled vulkan-shaders-gen tool (Not a file: cmake_install.cmake). This is in the llama-helper sidecar's dependency chain — a path my changes don't touch. Evidence:

  • The same failure occurred on this PR's first CI run before any of my Rust changes had even been compiled (at a step that runs before the Tauri build).
  • Open PR fix: upgrade whisper-rs to 0.16.0 and fix Windows build #460 ("upgrade whisper-rs to 0.16.0 and fix Windows build") is the upstream effort to address Windows build chain issues. Happy to rebase once that lands.

Speaker labels were intentionally excluded from v1. The transcripts.speaker column exists from migration 20251110000001_add_speaker_field.sql, but no writer in the live code populates it. Emitting <v Speaker> cues today would only ever produce blanks. Deferred to a follow-up paired with whoever wires diarization end-to-end.

pnpm-lock.yaml diff includes a small overrides reconciliation. pnpm install --frozen-lockfile was already failing against the pre-existing lockfile on devtest (overrides-config mismatch). The non-frozen install fixed that drift in the same hunk as the new @tauri-apps/plugin-dialog dep. Happy to split into a separate chore(deps) commit if you'd prefer.

dialog:allow-save is the only new capability. The Rust tauri-plugin-dialog was already in Cargo.toml (2.7.1 per Cargo.lock) and registered in lib.rs:411. The JS package was pinned to match (^2.7.0) — see commit fix(export): bump @tauri-apps/plugin-dialog to ^2.7.0 for the version-mismatch fix discovered in CI.

Checklist

  • Code follows project style
  • Self-reviewed the code
  • Branch targets devtest
  • No unrelated changes (lockfile reconciliation noted above)

tayyub-ai added 9 commits May 26, 2026 21:15
Empty skeleton + module registration so subsequent TDD steps compile.
Pure-function validation runs in a throwaway cargo project at
/tmp/meetily-441-validate (host machine lacks full Xcode so the meetily
crate cannot build locally; full-crate verification is deferred to the
fork CI).
Seven unit tests covering happy path with timestamp, NULL timestamp,
internal-newline collapse, ordering by audio_start_time, HH:MM:SS hour
rollover, the empty-input case, and trailing-newline invariant.

Validated locally in a throwaway cargo project (the real crate cannot
build on this machine without full Xcode); full-crate verification is
deferred to the fork CI run before opening the upstream PR.
WebVTT v1 output. Eight unit tests cover happy path, end-time fallback
chain (audio_end_time -> start+duration -> start+5s), NULL start-time
skipping (reported via tuple), special-char escaping, overlapping cues
preserved as distinct entries, hour-boundary timestamp formatting, and
the empty-input case.
Strips path separators (/, \, :, *, ?, ", <, >, |) and control chars,
collapses runs of whitespace to single spaces, trims whitespace and
dots, truncates to 80 chars, and falls back to meeting-<id-prefix>
when the title is empty. Five unit tests.
Non-paginated read ordered by audio_start_time with NULLs sorted last
(SQLite-friendly via the `IS NULL` trick rather than NULLS LAST). The
upcoming export command streams the whole transcript in one query.
…ions#441)

Wires the pure formatters to MeetingsRepository::get_all_transcripts_for_meeting
and writes the formatted content to a user-supplied path via std::fs::write.
Returns ExportResult so the frontend can render segment counts (and the
skipped-segment count for VTT) in its success toast.

Registered in lib.rs alongside the other api:: commands.
…ackriya-Solutions#441)

- tauri.conf.json: add "dialog:allow-save" to main capability so the
  renderer can pop a native save-as dialog. The Rust plugin
  (tauri-plugin-dialog 2.3.0) was already in Cargo.toml and registered
  in lib.rs:411.
- package.json: add "@tauri-apps/plugin-dialog" ~2.3.0 (pnpm resolved
  to 2.3.3).
- pnpm-lock.yaml: contains the new plugin-dialog entries plus a small
  overrides-config reconciliation that was already needed against the
  current devtest state (pnpm install --frozen-lockfile failed against
  the pre-existing lockfile, so the non-frozen install fixed both
  things together).
…tions#441)

Adds a third button between Recording and Enhance: an Export dropdown
with .txt and .vtt items. Each item:
- builds a default filename `<sanitized title> - <YYYY-MM-DD>.<ext>`
  (date from meeting.created_at)
- opens the native save-as dialog via @tauri-apps/plugin-dialog
- invokes api_export_transcript with the chosen path
- shows a sonner success toast (or warning when VTT skipped any
  cues for missing timing); error toast on failure; silent no-op
  when the user cancels the dialog
- disabled while recording, while in-flight, or when transcripts.length
  is zero (tooltip explains which case applies)

Threads meetingTitle, meetingCreatedAt, and isRecording through
TranscriptPanel; meeting-details/page-content passes them from the
meeting object.

Verified: pnpm run build succeeds (TypeScript + Next.js static
generation pass).
…ions#441)

Tauri requires the npm and Rust plugin packages to share the same
major.minor. Cargo.lock pins tauri-plugin-dialog to 2.7.1; the previous
~2.3.0 npm range resolved to 2.3.3 which the tauri build refused with
"Found version mismatched Tauri packages". Bumping the JS range to
^2.7.0 lets pnpm resolve to 2.7.1 to match.

Caught by build-devtest CI run Zackriya-Solutions#1.
bobby-claw pushed a commit to NERV-es/meetily that referenced this pull request Jun 1, 2026
…ackriya-Solutions#457 Zackriya-Solutions#430 Zackriya-Solutions#470 Zackriya-Solutions#420

Ported from upstream Zackriya-Solutions/meetily:
- Zackriya-Solutions#475: Whisper vocabulary hints (meeting domain support)
- Zackriya-Solutions#478: Export transcripts as TXT/VTT with file-save dialog
- Zackriya-Solutions#457: Integration API via deep-link URL scheme + distributed notifications
- Zackriya-Solutions#430: Apple Speech engine (cfg-gated behind 'apple-speech' feature)
- Zackriya-Solutions#470: BlockNote v0.16 editor upgrade
- Zackriya-Solutions#420: Monochrome tray icon (proper macOS template image)

Apple Speech engine gated behind feature flag due to upstream
Send/Sync issues with objc2 types — enable with:
  cargo build --features apple-speech
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant