From 0e06cf26cb6f34804d08f524fceae664ab3cd952 Mon Sep 17 00:00:00 2001 From: Nik Shevchenko <43514161+kodjima33@users.noreply.github.com> Date: Wed, 15 May 2024 13:51:24 -0700 Subject: [PATCH 01/20] Update README.md From d35a7e51402449d66a3b879bb5374fb2e95228d7 Mon Sep 17 00:00:00 2001 From: Harshith Sunku Date: Wed, 26 Feb 2025 17:20:08 +0530 Subject: [PATCH 02/20] Start work on #1752 From 4d2aacd706b98a54470ebd8b7b74aa468ff20a40 Mon Sep 17 00:00:00 2001 From: Nik Shevchenko <43514161+kodjima33@users.noreply.github.com> Date: Fri, 27 Jun 2025 17:18:39 -0700 Subject: [PATCH 03/20] Documentation edits made through Mintlify web editor From ed08a35e3d9dfb37e56c6714c08f4c555677449e Mon Sep 17 00:00:00 2001 From: Thinh Date: Wed, 20 Aug 2025 15:53:08 +0700 Subject: [PATCH 04/20] introduce subscription launch date for credit usage (#2826) * introduce subscription launch date for credit usage * inform users after 15m of silence to prevent burning through their credits * Record usages every 60s instead of 30s --- .../dev_omi_backend_listen_values.yaml | 2 + .../prod_omi_backend_listen_values.yaml | 4 +- backend/database/redis_db.py | 13 +++-- backend/database/user_usage.py | 15 ++++++ backend/routers/transcribe.py | 29 +++++++++-- backend/routers/users.py | 4 +- backend/utils/llm/notifications.py | 27 ++++++++-- backend/utils/notifications.py | 49 ++++++++++++++++++- backend/utils/subscription.py | 28 ++++++++++- 9 files changed, 155 insertions(+), 16 deletions(-) diff --git a/backend/charts/backend-listen/dev_omi_backend_listen_values.yaml b/backend/charts/backend-listen/dev_omi_backend_listen_values.yaml index 4ccb07924d..01eb698baf 100644 --- a/backend/charts/backend-listen/dev_omi_backend_listen_values.yaml +++ b/backend/charts/backend-listen/dev_omi_backend_listen_values.yaml @@ -231,6 +231,8 @@ env: value: "price_1RrxXL1F8wnoWYvwIddzR902" - name: STRIPE_UNLIMITED_ANNUAL_PRICE_ID value: "price_1RrxXL1F8wnoWYvw3kDbWmjs" + - name: SUBSCRIPTION_LAUNCH_DATE + value: "2025-08-21" resources: # We usually recommend not to specify default resources and to leave this as a conscious diff --git a/backend/charts/backend-listen/prod_omi_backend_listen_values.yaml b/backend/charts/backend-listen/prod_omi_backend_listen_values.yaml index d3fca1e790..63229c0880 100644 --- a/backend/charts/backend-listen/prod_omi_backend_listen_values.yaml +++ b/backend/charts/backend-listen/prod_omi_backend_listen_values.yaml @@ -255,7 +255,7 @@ env: name: prod-omi-backend-secrets key: ENCRYPTION_SECRET - name: BASIC_TIER_MINUTES_LIMIT_PER_MONTH - value: "1000000" + value: "1200" - name: BASIC_TIER_WORDS_TRANSCRIBED_LIMIT_PER_MONTH value: "0" - name: BASIC_TIER_INSIGHTS_GAINED_LIMIT_PER_MONTH @@ -266,6 +266,8 @@ env: value: "price_1RtJPm1F8wnoWYvwhVJ38kLb" - name: STRIPE_UNLIMITED_ANNUAL_PRICE_ID value: "price_1RtJQ71F8wnoWYvwKMPaGlGY" + - name: SUBSCRIPTION_LAUNCH_DATE + value: "2025-08-21" resources: # We usually recommend not to specify default resources and to leave this as a conscious diff --git a/backend/database/redis_db.py b/backend/database/redis_db.py index 9307d66ad9..5159978220 100644 --- a/backend/database/redis_db.py +++ b/backend/database/redis_db.py @@ -526,9 +526,6 @@ def delete_cached_mcp_api_key(hashed_key: str): r.delete(f'mcp_api_key:{hashed_key}') - - - # ****************************************************** # **************** DATA MIGRATION STATUS *************** # ****************************************************** @@ -577,3 +574,13 @@ def set_credit_limit_notification_sent(uid: str, ttl: int = 60 * 60 * 24): def has_credit_limit_notification_been_sent(uid: str) -> bool: """Check if credit limit notification was already sent to user recently""" return r.exists(f'users:{uid}:credit_limit_notification_sent') + + +def set_silent_user_notification_sent(uid: str, ttl: int = 60 * 60 * 24): + """Cache that silent user notification was sent to user (24 hours TTL by default)""" + r.set(f'users:{uid}:silent_notification_sent', '1', ex=ttl) + + +def has_silent_user_notification_been_sent(uid: str) -> bool: + """Check if silent user notification was already sent to user recently""" + return r.exists(f'users:{uid}:silent_notification_sent') diff --git a/backend/database/user_usage.py b/backend/database/user_usage.py index 96c8e0d258..41100234ef 100644 --- a/backend/database/user_usage.py +++ b/backend/database/user_usage.py @@ -100,6 +100,21 @@ def get_monthly_usage_stats(uid: str, date: datetime) -> dict: return _aggregate_stats(query) +def get_monthly_usage_stats_since(uid: str, date: datetime, start_date: datetime) -> dict: + """Aggregates hourly usage stats for a given month from Firestore, starting from a specific date.""" + user_ref = db.collection('users').document(uid) + hourly_usage_collection = user_ref.collection('hourly_usage') + + start_doc_id = f'{start_date.year}-{start_date.month:02d}-{start_date.day:02d}-00' + + query = ( + hourly_usage_collection.where(filter=FieldFilter('year', '==', date.year)) + .where(filter=FieldFilter('month', '==', date.month)) + .where(filter=FieldFilter('id', '>=', start_doc_id)) + ) + return _aggregate_stats(query) + + def get_yearly_usage_stats(uid: str, date: datetime) -> dict: """Aggregates hourly usage stats for a given year from Firestore.""" user_ref = db.collection('users').document(uid) diff --git a/backend/routers/transcribe.py b/backend/routers/transcribe.py index 42358dc171..22d1fc000e 100644 --- a/backend/routers/transcribe.py +++ b/backend/routers/transcribe.py @@ -18,6 +18,7 @@ import database.users as user_db from database import redis_db from database.redis_db import get_cached_user_geolocation +from models.users import PlanType from models.conversation import ( Conversation, TranscriptSegment, @@ -61,7 +62,7 @@ from utils.other import endpoints as auth from utils.other.storage import get_profile_audio_if_exists -from utils.notifications import send_credit_limit_notification +from utils.notifications import send_credit_limit_notification, send_silent_user_notification router = APIRouter() @@ -134,14 +135,18 @@ async def _listen( first_audio_byte_timestamp: Optional[float] = None last_usage_record_timestamp: Optional[float] = None words_transcribed_since_last_record: int = 0 + last_transcript_time: Optional[float] = None async def _record_usage_periodically(): nonlocal websocket_active, last_usage_record_timestamp, words_transcribed_since_last_record + nonlocal last_audio_received_time, last_transcript_time + while websocket_active: - await asyncio.sleep(30) + await asyncio.sleep(60) if not websocket_active: break + # Record usages if last_usage_record_timestamp: current_time = time.time() transcription_seconds = int(current_time - last_usage_record_timestamp) @@ -153,8 +158,8 @@ async def _record_usage_periodically(): record_usage(uid, transcription_seconds=transcription_seconds, words_transcribed=words_to_record) last_usage_record_timestamp = current_time + # Send credit limit notification if not has_transcription_credits(uid): - # Send credit limit notification (with Redis caching to prevent spam) try: await send_credit_limit_notification(uid) except Exception as e: @@ -165,6 +170,21 @@ async def _record_usage_periodically(): websocket_active = False break + # Silence notification logic for basic plan users + user_subscription = user_db.get_user_valid_subscription(uid) + if not user_subscription or user_subscription.plan == PlanType.basic: + time_of_last_words = last_transcript_time or first_audio_byte_timestamp + if ( + last_audio_received_time + and time_of_last_words + and (last_audio_received_time - time_of_last_words) > 15 * 60 + ): + print(f"User {uid} has been silent for over 15 minutes. Sending notification.") + try: + await send_silent_user_notification(uid) + except Exception as e: + print(f"Error sending silent user notification: {e}") + async def _asend_message_event(msg: MessageEvent): nonlocal websocket_active print(f"Message: type ${msg.event_type}", uid) @@ -747,7 +767,7 @@ async def translate(segments: List[TranscriptSegment], conversation_id: str): async def stream_transcript_process(): nonlocal websocket_active, realtime_segment_buffers, realtime_photo_buffers, websocket, seconds_to_trim - nonlocal current_conversation_id, including_combined_segments, translation_enabled, speech_profile_processed, speaker_to_person_map, suggested_segments, words_transcribed_since_last_record + nonlocal current_conversation_id, including_combined_segments, translation_enabled, speech_profile_processed, speaker_to_person_map, suggested_segments, words_transcribed_since_last_record, last_transcript_time while websocket_active or len(realtime_segment_buffers) > 0 or len(realtime_photo_buffers) > 0: await asyncio.sleep(0.6) @@ -766,6 +786,7 @@ async def stream_transcript_process(): transcript_segments = [] if segments_to_process: + last_transcript_time = time.time() if seconds_to_trim is None: seconds_to_trim = segments_to_process[0]["start"] diff --git a/backend/routers/users.py b/backend/routers/users.py index bb9d676823..61b0238496 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -35,7 +35,7 @@ from models.users import WebhookType, UserSubscriptionResponse, SubscriptionPlan, PlanType, PricingOption from utils.apps import get_available_app_by_id -from utils.subscription import get_plan_limits, get_plan_features +from utils.subscription import get_plan_limits, get_plan_features, get_monthly_usage_for_subscription from utils import stripe as stripe_utils from utils.llm.followup import followup_question_prompt from utils.other import endpoints as auth @@ -489,7 +489,7 @@ def get_user_subscription_endpoint(uid: str = Depends(auth.get_current_user_uid) subscription.features = get_plan_features(subscription.plan) # Get current usage - usage = user_usage_db.get_monthly_usage_stats(uid, datetime.utcnow()) + usage = get_monthly_usage_for_subscription(uid) # Calculate usage metrics transcription_seconds_used = usage.get('transcription_seconds', 0) diff --git a/backend/utils/llm/notifications.py b/backend/utils/llm/notifications.py index 724702b95a..bd1a861ed2 100644 --- a/backend/utils/llm/notifications.py +++ b/backend/utils/llm/notifications.py @@ -1,3 +1,4 @@ +import random from typing import Tuple, List from .clients import llm_medium from database.memories import get_memories @@ -96,7 +97,7 @@ async def generate_credit_limit_notification(uid: str, name: str) -> Tuple[str, Key Points to Include: - They've been actively using transcription (show appreciation) - Unlimited plan removes all limits - - Can check usage/plans in app or search 'omi unlimited subs' in marketplace + - Can check usage/plans in the app under Settings > Plan & Usages - Make it feel like you're helping them, not selling to them """ @@ -109,7 +110,7 @@ async def generate_credit_limit_notification(uid: str, name: str) -> Tuple[str, The message should: - Acknowledge their active usage positively - - Suggest checking plans in the app or searching 'omi unlimited subs' in marketplace + - Suggest checking plans in the app under Settings > Plan & Usages - Feel helpful, not sales-y - Be warm and personal to {name} @@ -125,5 +126,25 @@ async def generate_credit_limit_notification(uid: str, name: str) -> Tuple[str, # Fallback message return ( "omi", - f"Hey {name}! You've been actively using transcription - that's awesome! You've hit your limit, but unlimited plans remove all restrictions. Check your usage in the app or search 'omi unlimited subs' in the marketplace!", + f"Hey {name}! You've been actively using transcription - that's awesome! You've hit your limit, but unlimited plans remove all restrictions. You can check your usage and upgrade in the app under Settings > Plan & Usages.", ) + + +def generate_silent_user_notification(name: str) -> Tuple[str, str]: + """ + Generate a funny notification for a user who has been silent for a while. + """ + messages = [ + f"Hey {name}, just checking in! My ears are open if you've got something to say.", + f"Is this thing on? Tapping my mic here, {name}. Let me know when you're ready to chat!", + f"Quiet on the set! {name}, are we rolling? Just waiting for your cue.", + f"The sound of silence... is nice, but I'm here for the words, {name}! What's on your mind?", + f"{name}, you've gone quiet! Just a heads up, I'm still here listening and using up your free minutes.", + f"Psst, {name}... My virtual ears are getting a little lonely. Anything to share?", + f"Enjoying the quiet time, {name}? Just remember, I'm on the clock, ready to transcribe!", + f"Hello from the other side... of silence! {name}, ready to talk again?", + f"I'm all ears, {name}! Just letting you know the recording is still live.", + f"Silence is golden, but words are what I live for, {name}! Let's chat when you're ready.", + ] + body = random.choice(messages) + return "omi", body diff --git a/backend/utils/notifications.py b/backend/utils/notifications.py index dd89290b39..fb61a1c16b 100644 --- a/backend/utils/notifications.py +++ b/backend/utils/notifications.py @@ -2,8 +2,17 @@ import math from firebase_admin import messaging, auth import database.notifications as notification_db -from database.redis_db import set_credit_limit_notification_sent, has_credit_limit_notification_been_sent -from .llm.notifications import generate_notification_message, generate_credit_limit_notification +from database.redis_db import ( + set_credit_limit_notification_sent, + has_credit_limit_notification_been_sent, + set_silent_user_notification_sent, + has_silent_user_notification_been_sent, +) +from .llm.notifications import ( + generate_notification_message, + generate_credit_limit_notification, + generate_silent_user_notification, +) def send_notification(token: str, title: str, body: str, data: dict = None): @@ -86,6 +95,42 @@ async def send_credit_limit_notification(user_id: str): print(f"Credit limit notification sent to user {user_id}") +async def send_silent_user_notification(user_id: str): + """Send a notification if a basic-plan user is silent for too long.""" + # Check if notification was sent recently (within 24 hours) + if has_silent_user_notification_been_sent(user_id): + print(f"Silent user notification already sent recently for user {user_id}") + return + + # Get user's notification token + token = notification_db.get_token_only(user_id) + if not token: + print(f"No notification token found for user {user_id}") + return + + # Get user name from Firebase Auth + try: + user = auth.get_user(user_id) + name = user.display_name + if not name and user.email: + name = user.email.split('@')[0].capitalize() + if not name: + name = "there" + except Exception as e: + print(f"Error getting user info from Firebase Auth: {e}") + name = "there" + + # Generate personalized credit limit message + title, body = generate_silent_user_notification(name) + + # Send notification + send_notification(token, title, body) + + # Cache that notification was sent (24 hours TTL) + set_silent_user_notification_sent(user_id) + print(f"Silent user notification sent to user {user_id}") + + async def send_bulk_notification(user_tokens: list, title: str, body: str): try: batch_size = 500 diff --git a/backend/utils/subscription.py b/backend/utils/subscription.py index 1f8fff1c01..7e9abe3190 100644 --- a/backend/utils/subscription.py +++ b/backend/utils/subscription.py @@ -87,6 +87,32 @@ def get_plan_features(plan: PlanType) -> List[str]: ] +def get_monthly_usage_for_subscription(uid: str) -> dict: + """ + Gets the current monthly usage for subscription purposes, considering the launch date from env variables. + The launch date format is expected to be YYYY-MM-DD. + If the launch date is not set, not valid, or in the future, usage is considered zero. + """ + subscription_launch_date_str = os.getenv('SUBSCRIPTION_LAUNCH_DATE') + if not subscription_launch_date_str: + # Subscription not launched, so no usage is counted against limits. + return {} + + try: + # Use strptime to enforce YYYY-MM-DD format + launch_date = datetime.strptime(subscription_launch_date_str, '%Y-%m-%d') + except ValueError: + # Invalid date format, treat as not launched. + return {} + + now = datetime.utcnow() + if now < launch_date: + # Launch date is in the future, so no usage is counted yet. + return {} + + return user_usage_db.get_monthly_usage_stats_since(uid, now, launch_date) + + def has_transcription_credits(uid: str) -> bool: """ Checks if a user has transcribing credits by verifying their valid subscription and usage. @@ -95,7 +121,7 @@ def has_transcription_credits(uid: str) -> bool: if not subscription: return False - usage = user_usage_db.get_monthly_usage_stats(uid, datetime.utcnow()) + usage = get_monthly_usage_for_subscription(uid) limits = get_plan_limits(subscription.plan) # Check transcription seconds (0 means unlimited) From 77287648b266de02cc57c03290f2ef3cb57ccd35 Mon Sep 17 00:00:00 2001 From: Thinh Date: Thu, 21 Aug 2025 12:48:24 +0700 Subject: [PATCH 05/20] Enhance the UI/UX of the chatgpt plugin (#2829) --- plugins/example/templates/chatgpt/index.html | 151 ++++++++++++++----- 1 file changed, 117 insertions(+), 34 deletions(-) diff --git a/plugins/example/templates/chatgpt/index.html b/plugins/example/templates/chatgpt/index.html index fdb391eef3..324adb4024 100644 --- a/plugins/example/templates/chatgpt/index.html +++ b/plugins/example/templates/chatgpt/index.html @@ -7,12 +7,13 @@