Skip to content

Accept Voice Memos drags and add batch file transcription - #716

Draft
shreeraman96 wants to merge 4 commits into
mainfrom
feat/219-voicememo-batch
Draft

Accept Voice Memos drags and add batch file transcription#716
shreeraman96 wants to merge 4 commits into
mainfrom
feat/219-voicememo-batch

Conversation

@shreeraman96

@shreeraman96 shreeraman96 commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Description

Dragging a recording from Apple Voice Memos into File Transcription did nothing, and only one file could be transcribed at a time. This PR makes both work:

  • Promise-aware drop target (PromiseAwareDropView): SwiftUI's .onDrop can never see Voice Memos drags — they are file promises with no public.file-url (open Apple gap, FB13583826). An AppKit NSViewRepresentable overlay now accepts both concrete URLs and file promises. Resolution runs entirely off the main thread (verified live: any main-thread pasteboard read can deadlock intermittently and freezes the system-wide drag session) through three racing paths: modern NSFilePromiseReceiver (completion-callback-gated, so a stalled iCloud download is never delivered truncated) → raw pasteboard data (what Voice Memos actually serves, ~100 ms) → deprecated legacy promise API as a lazy last resort (an eager legacy request holds Voice Memos' promise machinery hostage for 35–80 s and blocks its next drag).
  • Batch transcription (BatchTranscriptionCoordinator): sequential per-item state machine — per-file status/progress/error, "no speech" detection, cancellation (pending items stop instantly; the in-flight native transcription runs out, shown as "Cancelling…"), and staging-dir cleanup only after each item finishes.
  • App-level session (FileTranscriptionSession): coordinator + service moved out of the view so in-flight work survives sidebar navigation.
  • Shared-model arbitration: dictation refuses to start while a batch is transcribing, and batch items wait for live dictation to end — both paths drive the same ASR model.
  • Multi-select file picker and multi-file Finder drops (the old handler silently kept only providers.first).
  • Two Task.checkCancellation() points in MeetingTranscriptionService's chunk loop so a cancelled batch stops mid-file on the chunked path.

Staging design: each promised file gets its own temp subdirectory (two memos titled "New Recording.m4a" never collide), duplicates from losing delivery paths are swept — except dirs whose promise is still pending, because deleting a promise's destination mid-write leaves the source app's drag session unresolved and Voice Memos then refuses all further drags until restarted (verified live; cleaned up on a deferred task instead).

Type of Change

  • 🐞 Bug fix
  • ✨ New feature
  • 💥 Breaking change
  • 🧹 Chore
  • 📝 Documentation update

Related Issue or Discussion

Closes #219

Testing

  • Tested on Intel Mac
  • Tested on Apple Silicon Mac
  • Tested on macOS version: 26.5.1
  • Ran linter locally: swiftlint --strict clean on all files touched by this PR
  • Ran formatter locally: not run repo-wide — swiftformat reflows ~3k unrelated pre-existing lines on main; new files follow the existing style
  • Ran tests locally: 175/175 pass (30 new unit tests: coordinator state machine, cancellation, staging cleanup + session-root pruning, drop strategy selection, delivery dedup/sweep, format filtering)

Live end-to-end verification on this machine (Voice Memos → FluidVoice):

  • Single memo drop: promise resolved in ~0.5 s, 34-min recording transcribed in ~86 s, history entry written
  • Second memo dropped while the first was transcribing: queued and transcribed ("Transcribing 1 of 2" → both complete); Voice Memos stays responsive (the eager-legacy freeze this PR avoids was reproduced and eliminated)
  • Navigating Settings → back mid-batch: batch card intact, work uninterrupted
  • Non-audio promises (e.g. Photos images) are refused at the drag affordance

Screenshots / Video

Batch card after a drop (per-item status, header summary, Done):

Batch transcription card

Notes

  • The modern NSFilePromiseReceiver path is unreliable for Voice Memos (a Catalyst app): called on the main thread it fails instantly with Cocoa 3072; on a background thread it completes only sometimes. The raw pasteboard data path is what reliably delivers Voice Memos audio; the receiver + legacy paths cover other promise sources (Photos, Mail).
  • Reading the drag pasteboard from background threads after performDragOperation returns is outside AppKit's documented lifetime contract — it is the only arrangement found that neither deadlocks nor loses drops; commented in code with the poll-timeout safety net.
  • The issue's option 3 (an in-app Voice Memos library browser) was deliberately not pursued: the group container is TCC/Full-Disk-Access protected on modern macOS and CloudRecordings.db is an undocumented schema.
  • Watch/iCloud memos not yet downloaded locally are untested (none available); a promise that never resolves surfaces as a per-item error after the poll deadline rather than a hang.

- AppKit promise-aware drop target (SwiftUI onDrop cannot receive file
  promises): concrete URLs + file promises, resolved off-main via modern
  receiver, legacy promise API, and raw pasteboard data fallbacks
- BatchTranscriptionCoordinator: sequential per-item state machine with
  cancellation, staging cleanup, and batch/dictation arbitration
- App-level FileTranscriptionSession so in-flight work survives navigation
- Multi-select file picker; batch progress UI with per-item status
@github-actions github-actions Bot added the needs screenshots Pull request needs screenshot or video evidence. label Jul 26, 2026
@github-actions

Copy link
Copy Markdown

The PR Policy check is blocking this PR because required template information is missing.

Please update the PR description with:

  • Screenshots / Video

Visual files detected:

  • Sources/Fluid/ContentView.swift
  • Sources/Fluid/UI/MeetingTranscriptionView.swift
  • Sources/Fluid/UI/PromiseAwareDropView.swift
  • Sources/Fluid/UI/PromiseDropSupport.swift

Screenshots or video are required for UI, UX, settings, onboarding, overlay, menu bar, or visual behavior changes. If this PR has no visual changes, check the no-visual-change box in the template.

If this remains incomplete for 48 hours after opening, the PR may be closed.

@github-actions github-actions Bot removed the needs screenshots Pull request needs screenshot or video evidence. label Jul 26, 2026
NSPasteboard is not thread-safe: our worker's per-item reads raced the
promise receivers' internal pasteboard access and crashed in the type
cache (NSInternalInconsistencyException -> ggml terminate handler ->
abort). All app-side reads now happen first on one thread, receivers
start only afterwards on a serial queue, and every pasteboard call is
wrapped in an ObjC exception catcher so a future race logs instead of
killing the app.
- Single MainActor arbiter for the shared ASR model: dictation intent flag
  set synchronously before any await closes the TOCTOU gap; batch items wait
  on dictation intent, live capture, and in-flight single-file transcription
- Batch guard moved to every hotkey entry point (dictate/prompt/command/
  rewrite), with an audible cue when dictation is blocked mid-batch
- Coordinator: pending-driven processing (completed items never re-run when
  enqueueing after an undismissed batch) and self-draining restart (items
  enqueued during the cancel window are picked up, not stranded)
- CancellationError rethrown unwrapped from transcribeFile (no false failure
  analytics)
- Drop resolver: empty-delivery path no longer deletes pending receiver dirs;
  receivers hold in-flight tokens so slow (iCloud) promises aren't abandoned
  at the 30s soft timeout; failed receivers count as resolved so partial
  failures deliver promptly; partial-failure accounting includes raw-data
  item counts
- Single-file drops route into the batch when anything is in flight or a
  finished batch is still displayed; drop errors auto-dismiss
- 7 new regression tests (182 total)
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.

[✨ FEATURE] Please provide ability to transcribe multiple voice memos all at once (voice memos.app)

1 participant