Skip to content

feat(app): generate meeting notes from saved transcripts - #971

Open
mickvdv wants to merge 4 commits into
pasrom:mainfrom
mickvdv:feat/notes-from-transcript
Open

mickvdv wants to merge 4 commits into
pasrom:mainfrom
mickvdv:feat/notes-from-transcript

Conversation

@mickvdv

@mickvdv mickvdv commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Requested by Mick

What changed and why

Builds on #970 (retry meeting notes), which builds on #969; review the last commit, feat(app): generate meeting notes from saved transcripts. It reuses #970's MeetingNotesActions and DebugRPCServer+MeetingNotes.swift, so on its own it would conflict with it.

A retry needs the job, and the job is gone for a meeting from before the app kept failed ones, or one that was dismissed. Its transcript is still in the notes folder. This makes notes for transcripts the user picks, several at once, and offers the same over /v1.

 Menu bar
   Process Audio/Video Files...           ⌘P
+  Generate Meeting Notes from Transcript...

Change

  • PipelineQueue.enqueueNotes(fromTranscripts:) queues one job per .txt, in order, with PipelineJob.generatesNotesOnly, transcriptPath and namingSlug set to the file and its name, and meetingStartTime from a leading yyyyMMdd_HHmm, so the prompt gets the meeting date. A transcript with a .md of its name next to it is left out (notesExist), as is one that is not a .txt or cannot be read (unreadable).
  • runPipeline sends such a job to generateNotesOnly, right after the protocol-only resume and ahead of the engine guard, so it never transcribes, also after a relaunch. It checks again before generating: notes written, or the transcript gone, while the job waited end it in an error, never in an overwrite or a full run.
  • The picked file is the user's, not a run's output, so these jobs keep the raw transcript and never rename by the AI title, whatever the settings say.
  • PipelineController.generateNotes(fromTranscripts:) (new PipelineController+MeetingNotes.swift) refuses while the provider is None, since the jobs would end without notes, and names skipped files in a notification. settings and notifier on PipelineController went from private to internal for it.
  • Menu: Generate Meeting Notes from Transcript... (A11yID.generateNotesFromTranscriptButton), an NSOpenPanel for plain text with multiple selection, starting in the output folder.
  • /v1: POST /v1/jobs/notes/generate with {"paths": [...]}: 200 with jobIDs, notesExist and unreadable, 400 without paths, 409 while the provider is None.
  • Docs: docs/automation-api.md, docs/architecture/pipeline.md, docs/architecture-macos.md, README features.

Tests

  • Reverted, or line set to a constant: PipelineQueue+Stages.swift:230, job.generatesNotesOnly == true set to == false
  • Test that went red: NotesFromTranscriptTests.testEachTranscriptGetsItsNotesWithoutTranscribing
  • Reverted, or line set to a constant: PipelineQueue+MeetingNotes.swift:141, job.generatesNotesOnly = true set to nil
  • Test that went red: NotesFromTranscriptTests.testEachTranscriptGetsItsNotesWithoutTranscribing
  • Reverted, or line set to a constant: PipelineQueue+MeetingNotes.swift:137, saveRawTranscriptSeparately: true, useAITitleForFilenames: false set to nil, nil
  • Test that went red: NotesFromTranscriptTests.testThePickedTranscriptIsNeitherRemovedNorRenamed (queue with the other two values)
  • Reverted, or line set to a constant: PipelineController+MeetingNotes.swift:16, != .none set to != .openAICompatible
  • Test that went red: NotesFromTranscriptTests.testWithoutANotesProviderNothingIsQueuedAndTheUserIsTold
  • Reverted, or line set to a constant: AppState+RPC.swift:250, the generate: line removed
  • Test that went red: NotesFromTranscriptTests.testTheAppWiresTheV1GenerateToItsController
  • Reverted, or line set to a constant: MenuBarView.swift:211, onGenerateNotesFromTranscripts() set to onProcessFiles()
  • Test that went red: NotesFromTranscriptTests.testTheMenuItemAsksForTranscripts
  • Reverted, or line set to a constant: DebugRPCServer+MeetingNotes.swift:55, the generateMeetingNotesPath match changed to a path that never occurs
  • Test that went red: NotesFromTranscriptTests.testV1GenerateAnswersWithWhatWasQueuedAndLeftOut
  • Not checked: MeetingTranscriberApp.swift:210 (onGenerateNotesFromTranscripts: generateNotesFromTranscripts) and the NSOpenPanel itself, which the tests cannot reach (manual QA per docs/architecture/gui-testing.md; tried by hand in a local build). The App Store build: not tried; under the sandbox a picked file grants access to that file, and I did not measure whether writing the .md beside a transcript outside the output folder is allowed there.

Also: notes written while the job waited are not overwritten, a waiting job still makes only notes after a relaunch, the stamp parsing, and the skipped-files notification.

AI assistance

  • Written with: Claude (Anthropic), via Claude Code
  • Reviewed or simplified with: the same

Checklist

  • Rebased on main, no merge commits, since a merge commit pulls commits that are
    already on main into this branch and the review reads it commit by commit:
    git fetch https://github.com/pasrom/meeting-transcriber main && git rebase FETCH_HEAD
    (the URL spelled out because in a fork clone origin is your fork)
  • ./scripts/lint.sh is clean, and ./scripts/lint.sh --analyze too (CI runs both)
  • Tests pass: cd app/MeetingTranscriber && swift test
    (MenuBarIconSnapshotTests compare pixels and depend on the macOS version, so CI
    skips them; if only they fail, add --skip MenuBarIconSnapshotTests and tick this)
  • Conventional Commits, one logical change per commit

Mick van der Vegt added 4 commits November 12, 2026 11:36
When the provider cannot generate the meeting notes (a signed-out Claude
CLI, a timeout, an HTTP error), the job still finishes with its
transcript and the warning "Protocol generation failed — transcript
saved". The only notification was "Transcript Saved", which reads like
success, so a signed-out CLI went unnoticed for weeks while every meeting
lost its notes.

The job now keeps the provider's message as protocolFailure, and the done
notification for such a job reads "Meeting Notes Failed" with the reason,
time-sensitive since the user can still act on it. A signed-out Claude
CLI also gets the hint to sign in with /login. "Notify when meeting notes
fail" in Settings > Output turns it off; it is on by default.

The message is already content-free (see the catch in generateProtocol),
so it can go into a notification as it is.
….swift

A pure move, no change: DebugRPCServer.swift is at the file_length limit,
and the next commit adds a route family that needs three lines there.
A job whose notes could not be generated now stays in the menu, past the
60-second cleanup and across a relaunch, with Retry Meeting Notes, and
Retry All Meeting Notes (N) from two such jobs. The retry generates the
notes from the saved transcript through the protocol-only resume, so the
meeting is not transcribed again, and it never overwrites notes that
already exist next to the transcript.

The automation API gets the same: POST /v1/jobs/<id>/notes/retry and
POST /v1/jobs/notes/retry.
Generate Meeting Notes from Transcript... in the menu takes one or more
.txt transcripts and queues a notes-only job for each: no transcription,
the notes saved next to the transcript as <name>.md, and the transcript
itself never removed or renamed. A transcript that already has notes is
skipped and named in a notification; nothing is queued while the notes
provider is None.

The automation API gets the same: POST /v1/jobs/notes/generate with
{"paths": [...]}.
@github-actions github-actions Bot added the enhancement New feature or request label Nov 12, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant