Skip to content

fix(backend): stop expiring speech profiles in has_profile check (#5128)#7821

Merged
kodjima33 merged 1 commit into
mainfrom
watchdog/issue-5128-speech-profile-expiry
Jun 11, 2026
Merged

fix(backend): stop expiring speech profiles in has_profile check (#5128)#7821
kodjima33 merged 1 commit into
mainfrom
watchdog/issue-5128-speech-profile-expiry

Conversation

@kodjima33

Copy link
Copy Markdown
Collaborator

Bug (#5128, Bug 1)

Users who already have a speech profile are re-prompted to "Teach Omi your voice". Live on prod for every user whose profile is older than 90 days (i.e. most long-time users).

Root cause

Commit 34be170 (#3891, Dec 24 2025) added a 90-day expiry to the /v3/speech-profile endpoint only:

return {'has_profile': get_user_has_speech_profile(uid, max_age_days=90)}

Nothing else honors that expiry:

  • routers/transcribe.py:764 enables speaker identification via get_user_has_speech_profile(uid)no age limit
  • routers/transcribe.py:1833 downloads the profile for embedding extraction regardless of age
  • get_profile_audio_if_exists() has no age limit

So a >90-day profile is actively used for diarization while the app is told has_profile: false and shows the re-teach banner every launch. The expiry never had the (presumably intended) effect of refreshing what the pipeline uses — its only production effect is the spurious nag.

Fix

Remove the age cutoff from get_user_has_speech_profile so the banner endpoint agrees with the listen pipeline: an existing profile is a profile. Zero pipeline behavior change. Bonus: drops the extra per-request GCS metadata round-trip (blob.reload()) the age check required.

max_age_days had no other callers, so the dead parameter is removed too.

Tests

tests/unit/test_speech_profile_existence.py (registered in test.sh): existence semantics (old profile counts, missing blob/bucket don't), no blob.reload() metadata fetch, signature guard (uid only), and a structural guard that the router passes no age cutoff. All assertions verified locally on Python 3.11 against the real modules (pytest not installed locally; CI runs the suite). black clean, lint_async_blockers clean.

Note: Bug 2 in #5128 (recording UX glitches) is speculative/multi-cause and is not addressed here.

🤖 automated by hourly watchdog; opened for review, not merged.

Fixes #5128

🤖 Generated with Claude Code

The /v3/speech-profile endpoint applied a 90-day expiry
(get_user_has_speech_profile(uid, max_age_days=90), added in 34be170),
but nothing else honors that expiry: the /v4/listen pipeline
(routers/transcribe.py) enables speaker identification and downloads the
profile for embedding extraction regardless of age. So users whose
profile is older than 90 days — and actively used for diarization — are
told has_profile=false and nagged to "Teach Omi your voice" again.

Remove the age cutoff so the banner endpoint agrees with the pipeline:
an existing profile is a profile. Also drops the per-request blob
metadata round-trip (blob.reload) the age check needed.

Adds tests/unit/test_speech_profile_existence.py (registered in test.sh)
covering existence semantics, the signature, and a structural guard that
the router passes no age cutoff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a regression where users with speech profiles older than 90 days were incorrectly shown the "Teach Omi your voice" re-enrollment banner on every launch, because the /v3/speech-profile endpoint was filtering profiles by age while the transcription pipeline used them without any age restriction.

  • utils/other/storage.py: Removes the max_age_days parameter and inline age-check logic from get_user_has_speech_profile, collapsing it to a single blob.exists() call. This also eliminates the extra blob.reload() GCS metadata round-trip that the expiry check required.
  • routers/speech_profile.py: Drops the max_age_days=90 argument so the /v3/speech-profile endpoint now agrees with the transcription pipeline on what constitutes a valid profile.
  • tests/unit/test_speech_profile_existence.py + test.sh: Adds a new test suite covering profile existence semantics, the absence of blob.reload(), the function signature (no age parameter), and a structural guard that the router does not reintroduce an age cutoff.

Confidence Score: 4/5

Safe to merge — the change removes a one-line age filter that was never applied by the transcription pipeline, restoring consistency between the API and the actual diarization behavior.

The core logic change is minimal and verifiably correct: both call sites in transcribe.py and speech_profile.py already passed no age limit, so collapsing to a plain blob.exists() call only removes the divergence. The one point of minor attention is the test test_endpoint_does_not_pass_age_cutoff, which derives the router path from storage_mod.file at runtime — correct given the backend/utils/other/storage.py nesting, but would silently break if the module path ever changes.

No files require special attention. The structural test in test_speech_profile_existence.py warrants a glance if the directory layout changes in the future.

Important Files Changed

Filename Overview
backend/utils/other/storage.py get_user_has_speech_profile simplified to a single blob.exists() call; max_age_days parameter and blob.reload() metadata fetch removed; all other callers used no age limit anyway
backend/routers/speech_profile.py Endpoint call updated to drop max_age_days=90; now consistent with the transcribe.py pipeline call at line 764
backend/tests/unit/test_speech_profile_existence.py New unit tests covering existence semantics, missing bucket/blob, signature guard, and a filesystem-based structural guard that the router omits max_age_days; the filesystem read in test_endpoint_does_not_pass_age_cutoff is slightly brittle but functionally correct
backend/test.sh New test module registered in the test suite alongside existing unit tests

Sequence Diagram

sequenceDiagram
    participant App as Mobile App
    participant EP as /v3/speech-profile
    participant Storage as utils/other/storage.py
    participant GCS as Google Cloud Storage
    participant TP as Transcription Pipeline

    Note over App,TP: Before fix (regression)
    App->>EP: GET /v3/speech-profile
    EP->>Storage: "get_user_has_speech_profile(uid, max_age_days=90)"
    Storage->>GCS: blob.exists()
    Storage->>GCS: blob.reload() [fetch metadata]
    Storage-->>EP: "False (profile >90 days old)"
    EP-->>App: "{has_profile: false}"
    App-->>App: Show re-enroll banner

    TP->>Storage: get_user_has_speech_profile(uid)
    Storage->>GCS: blob.exists()
    Storage-->>TP: True
    TP-->>TP: Uses profile for diarization

    Note over App,TP: After fix (this PR)
    App->>EP: GET /v3/speech-profile
    EP->>Storage: get_user_has_speech_profile(uid)
    Storage->>GCS: blob.exists()
    Storage-->>EP: True
    EP-->>App: "{has_profile: true}"

    TP->>Storage: get_user_has_speech_profile(uid)
    Storage->>GCS: blob.exists()
    Storage-->>TP: True
    TP-->>TP: Uses profile for diarization
Loading

Reviews (1): Last reviewed commit: "fix(backend): stop expiring speech profi..." | Re-trigger Greptile

@kodjima33 kodjima33 merged commit 8aeb1c8 into main Jun 11, 2026
3 checks passed
@kodjima33 kodjima33 deleted the watchdog/issue-5128-speech-profile-expiry branch June 11, 2026 14:40
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.

bug: Speech profile prompt shown to users with existing profiles + recording UX glitches

1 participant