Skip to content

Fix transcript speaker editing and persistence#6348

Merged
kodjima33 merged 2 commits into
mainfrom
codex/fix-transcript-speaker-rename
Apr 6, 2026
Merged

Fix transcript speaker editing and persistence#6348
kodjima33 merged 2 commits into
mainfrom
codex/fix-transcript-speaker-rename

Conversation

@kodjima33
Copy link
Copy Markdown
Collaborator

Summary

  • preserve stable backend transcript segment ids through desktop decoding, local storage, and Firestore speaker assignment updates
  • fix the transcript speaker rename flow by centering dismissable sheets reliably and exposing speaker labels as real accessible buttons
  • add desktop automation hooks and regression tests used to verify the speaker rename flow on the Mac mini

Testing

  • xcrun swift test -c debug --package-path Desktop --filter TranscriptSpeakerAssignmentTests
  • Mac mini manual verification: clicked transcript speaker label in Omi Dev, confirmed Name Speaker sheet opened, saved a speaker assignment, reopened the conversation, restarted Omi Dev, and confirmed the renamed speaker persisted instead of reverting to Speaker 1

@kodjima33 kodjima33 merged commit 1c761fc into main Apr 6, 2026
1 check failed
@kodjima33 kodjima33 deleted the codex/fix-transcript-speaker-rename branch April 6, 2026 00:07
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 6, 2026

Greptile Summary

This PR fixes the transcript speaker rename workflow end-to-end on macOS: backendId is now preserved through decode → local SQLite → Firestore PATCH, speaker labels are real focusable buttons, and a two-phase commit (server PATCH first, local SQLite update second) ensures a background sync can never silently revert an assignment. All remaining findings are P2 style/hardening concerns and are non-blocking.

Confidence Score: 5/5

Safe to merge — all findings are P2 style/hardening concerns with no runtime impact under current code paths

The two-phase commit (server then local) is sound and the new regression tests verify the backendId round-trip. The isSaving flag is never reset, but the sheet is always dismissed anyway. The tappedSegmentIndex fallback to 0 cannot fire in the current call graph. The Firestore race condition is pre-existing and not worsened by this PR.

NameSpeakerSheet.swift (isSaving / tappedSegmentIndex fallback) and firestore.rs (non-atomic segment PATCH)

Important Files Changed

Filename Overview
desktop/Desktop/Sources/MainWindow/Components/NameSpeakerSheet.swift New speaker-naming modal — isSaving is never reset and tappedSegmentIndex has a silent-wrong-segment fallback; neither causes a runtime bug under current code but both are worth hardening
desktop/Desktop/Sources/MainWindow/Components/SpeakerBubbleView.swift Speaker label promoted to real Button with accessibilityIdentifier/Label for automation; logic is correct
desktop/Desktop/Sources/MainWindow/Pages/ConversationDetailView.swift Adds dismissableSheet for speaker naming, assignmentMetadata static helper, and persistSpeakerAssignment; two-phase commit order is correct
desktop/Desktop/Sources/Rewind/Core/TranscriptionStorage.swift Adds updateSpeakerAssignmentByBackendId using json_each for batch SQLite updates with backendId-primary / segmentOrder-fallback matching
desktop/Desktop/Sources/Rewind/Core/TranscriptionModels.swift toTranscriptSegment preserves backendId; from(_:) correctly maps segmentId from TranscriptSegment.backendId; round-trip verified by new tests
desktop/Backend-Rust/src/services/firestore.rs assign_segments_bulk now writes segment id back to Firestore fixing the persistence regression; non-atomic read-modify-write is a pre-existing design concern
desktop/Desktop/Sources/AppState.swift SpeakerSegment.id stabilized via segmentId with speaker+start fallback; assignSpeakerToSegments wires AppState to APIClient
desktop/Desktop/Tests/TranscriptSpeakerAssignmentTests.swift New regression tests covering backendId decode round-trip, TranscriptSegmentRequest encoding, DB round-trip, and assignmentMetadata target/fallback logic
desktop/Desktop/Tests/OnboardingFlowTests.swift Existing onboarding step-count and migration tests; unchanged by this PR
desktop/Desktop/Sources/DesktopAutomationBridge.swift Adds /conversation/open HTTP endpoint to the automation bridge for the regression test harness
desktop/Backend-Rust/src/models/conversation.rs TranscriptSegment.id declared as Option with serde(default), enabling stable Firestore segment ID round-trips while remaining backward-compatible

Sequence Diagram

sequenceDiagram
    participant U as User
    participant NS as NameSpeakerSheet
    participant CDV as ConversationDetailView
    participant AS as AppState
    participant API as Rust Backend
    participant FS as Firestore
    participant TS as TranscriptionStorage (SQLite)

    U->>NS: Tap speaker label → sheet opens
    U->>NS: Select person / You, tap Save
    NS->>CDV: onSave(personId, isUser, segmentIndices)
    CDV->>CDV: assignmentMetadata(segmentIndices)
    Note over CDV: targets = backendIds + #index:N fallbacks
    CDV->>AS: onAssignSpeaker(conversationId, targets, personId, isUser)
    AS->>API: PATCH /v1/conversations/:id/segments/assign-bulk
    API->>FS: read conv → mutate segments → PATCH transcript_segments
    FS-->>API: 200 OK
    API-->>AS: 200 OK
    AS-->>CDV: success = true
    CDV->>TS: updateSpeakerAssignmentByBackendId(backendIds, fallbackOrders)
    CDV->>CDV: updateDisplayedConversation() — optimistic UI update
    CDV->>CDV: selectedSegmentForNaming = nil → dismiss sheet
Loading

Comments Outside Diff (2)

  1. desktop/Desktop/Sources/MainWindow/Components/NameSpeakerSheet.swift, line 34-36 (link)

    P2 tappedSegmentIndex silently falls back to 0

    When tagAllFromSpeaker = false, the save uses [tappedSegmentIndex]. If allSegments.firstIndex(where:) fails to find the segment, the fallback ?? 0 will tag the very first segment in the conversation instead of doing nothing — silently assigning the wrong speaker. In practice segment is always present in allSegments (both are captured from the same snapshot), but the silent wrong-index fallback is worth hardening.

    Consider making the return type Int? and handling the nil case in save():

    private var tappedSegmentIndex: Int? {
        allSegments.firstIndex(where: { $0.id == segment.id })
    }
    // in save():
    let segmentIndices = tagAllFromSpeaker
        ? sameSpeakerIndices
        : tappedSegmentIndex.map { [$0] } ?? []
    
  2. desktop/Backend-Rust/src/services/firestore.rs, line 9141-9170 (link)

    P2 Non-atomic read-modify-write on transcript_segments

    assign_segments_bulk fetches the whole conversation document, mutates the in-memory segment list, then PATCHes the entire transcript_segments array back. Any concurrent write to the same document (e.g., the Python backend finalizing the conversation at the same moment) that starts between the read and the PATCH will be silently overwritten. This is a pre-existing structural pattern, but it becomes more relevant now that the desktop client actively exercises this path on every speaker-naming action.

    The standard Firestore mitigation is a transaction (runTransaction) or an optimistic-locking retry loop using the document's updateTime returned from the GET.

Reviews (1): Last reviewed commit: "Merge main into transcript speaker fix b..." | Re-trigger Greptile

Comment on lines 294 to 298
private func save() {
isSaving = true
let indices = tagAllFromSpeaker ? sameSpeakerIndices : [tappedSegmentIndex]
onSave(selectedPersonId, isUserSelected, indices)
let segmentIndices = tagAllFromSpeaker ? sameSpeakerIndices : [tappedSegmentIndex]
onSave(selectedPersonId, isUserSelected, segmentIndices)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 isSaving never reset to false

save() sets isSaving = true synchronously but the flag is never cleared. The sheet is always dismissed by the parent (via selectedSegmentForNaming = nil), so the stuck spinner is invisible today. If any future code path keeps the sheet open after a failed save (e.g., a retry UX), the Save button will be permanently disabled.

Suggested change
private func save() {
isSaving = true
let indices = tagAllFromSpeaker ? sameSpeakerIndices : [tappedSegmentIndex]
onSave(selectedPersonId, isUserSelected, indices)
let segmentIndices = tagAllFromSpeaker ? sameSpeakerIndices : [tappedSegmentIndex]
onSave(selectedPersonId, isUserSelected, segmentIndices)
}
private func save() {
isSaving = true
let segmentIndices = tagAllFromSpeaker ? sameSpeakerIndices : [tappedSegmentIndex]
onSave(selectedPersonId, isUserSelected, segmentIndices)
// Note: isSaving is reset implicitly when the sheet is dismissed.
// If you add a stay-open error path, reset isSaving = false there.
}

Glucksberg pushed a commit to Glucksberg/omi-local that referenced this pull request Apr 28, 2026
## Summary
- preserve stable backend transcript segment ids through desktop
decoding, local storage, and Firestore speaker assignment updates
- fix the transcript speaker rename flow by centering dismissable sheets
reliably and exposing speaker labels as real accessible buttons
- add desktop automation hooks and regression tests used to verify the
speaker rename flow on the Mac mini

## Testing
- xcrun swift test -c debug --package-path Desktop --filter
TranscriptSpeakerAssignmentTests
- Mac mini manual verification: clicked transcript speaker label in Omi
Dev, confirmed `Name Speaker` sheet opened, saved a speaker assignment,
reopened the conversation, restarted Omi Dev, and confirmed the renamed
speaker persisted instead of reverting to `Speaker 1`
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