Skip to content

Conversation

@AnkushMalaker
Copy link
Collaborator

@AnkushMalaker AnkushMalaker commented Jan 14, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added annotation system for correcting memory and transcript content with accept/reject workflow
    • Inline editing of memory details and transcript segments with keyboard shortcuts
    • Audio segment download capability with custom time ranges from conversations
    • Background scheduler for AI-powered error detection suggestions
  • Chores

    • Restructured environment configuration to prioritize secrets management
    • Expanded audio processing to support longer conversations (up to 2 hours)

✏️ Tip: You can customize this high-level summary in your review settings.

- Introduced a new annotation model to support user edits and AI-powered suggestions for memories and transcripts.
- Added annotation routes for CRUD operations, enabling the creation and management of annotations via the API.
- Enhanced the audio processing workflow to support fetching audio segments from the backend, improving speaker recognition accuracy.
- Updated the speaker recognition client to handle conversation-based audio fetching, allowing for better management of large audio files.
- Implemented a cron job for generating AI suggestions on potential errors in transcripts and memories, improving user experience and content accuracy.
- Enhanced the web UI to support inline editing of transcript segments and memory content, providing a more interactive user experience.
- Updated configuration files to support new features and improve overall system flexibility.
- Introduced a new configuration loader using OmegaConf for unified management of backend settings.
- Updated existing configuration functions to leverage the new loader, enhancing flexibility and maintainability.
- Added support for environment variable interpolation in configuration files.
- Refactored various components to retrieve settings from the new configuration system, improving consistency across the application.
- Updated requirements to include OmegaConf as a dependency.
- Enhanced documentation and comments for clarity on configuration management.
- Updated the .env.template to clarify its purpose for secret values and streamline setup instructions.
- Removed the deprecated diarization_config.json.template file, as it is no longer needed.
- Added new environment variables for Langfuse and Tailscale integration to enhance observability and remote service access.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 14, 2026

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

This pull request introduces a centralized OmegaConf-based configuration system, implements a polymorphic annotation framework for memory and transcript corrections, adds background job scheduling for AI-powered suggestions, refactors speaker recognition to use token-based backend authentication, and enhances the API and frontend with annotation management and inline editing capabilities.

Changes

Cohort / File(s) Summary
Configuration System Refactor
backends/advanced/.env.template, backends/advanced/src/advanced_omi_backend/config.py, backends/advanced/src/advanced_omi_backend/config_loader.py, config/defaults.yml, config/config.yml.template
Migrated from scattered environment variables to OmegaConf-based centralized configuration. Introduced config_loader.py with disk-backed defaults and user overrides. Updated config.py to use OmegaConf for settings retrieval and persistence. Reduced .env.template from 233 to 32 lines, moving non-secret configs to YAML. Added comprehensive backend, LLM, transcription, diarization, and cleanup settings in defaults.yml.
Annotation System
backends/advanced/src/advanced_omi_backend/models/annotation.py, backends/advanced/src/advanced_omi_backend/routers/modules/annotation_routes.py, backends/advanced/src/advanced_omi_backend/workers/annotation_jobs.py
Introduced polymorphic annotation model supporting memory and transcript annotations with CRUD endpoints. Added enums for type, source, and status. Implemented FastAPI routes for create, read, and status-update operations. Added background jobs for surfacing AI error suggestions and fine-tuning hallucination detection models.
Cron Scheduler & Background Jobs
backends/advanced/src/advanced_omi_backend/cron.py, backends/advanced/docker-compose.yml
New scheduler module runs daily suggestion surfacing and weekly model fine-tuning via new Docker Compose service annotation-cron. Integrates with MongoDB and handles DEV_MODE for testing.
Speaker Recognition Refactoring
backends/advanced/src/advanced_omi_backend/speaker_recognition_client.py, backends/advanced/src/advanced_omi_backend/workers/speaker_jobs.py, extras/speaker-recognition/src/simple_speaker_recognition/api/routers/identification.py, extras/speaker-recognition/src/simple_speaker_recognition/core/backend_client.py, extras/speaker-recognition/src/simple_speaker_recognition/core/audio_backend.py
Shifted from in-memory audio processing to token-based backend authentication. Updated diarize_identify_match to accept conversation_id and backend_token instead of audio bytes. Added BackendClient for fetching audio from backend. Implemented chunked diarization with configurable max duration and overlap. Removed local audio chunk merging.
Audio Processing
backends/advanced/src/advanced_omi_backend/utils/audio_chunk_utils.py
Added reconstruct_audio_segment() for time-bounded audio reconstruction. Extended maximum audio duration from 30 minutes to 2 hours (7200 seconds). Updated validation and logging accordingly.
API Enhancements
backends/advanced/src/advanced_omi_backend/controllers/audio_controller.py, backends/advanced/src/advanced_omi_backend/routers/modules/conversation_routes.py, backends/advanced/src/advanced_omi_backend/routers/api_router.py, backends/advanced/src/advanced_omi_backend/routers/modules/__init__.py, backends/advanced/webui/src/services/api.ts
Added new conversation metadata and audio segment endpoints. Integrated annotation routes into main API router. Extended frontend API client with six annotation endpoints (create, fetch, accept/reject suggestions). Updated post-conversation job chain with transcript_version_id parameter.
Memory Service Updates
backends/advanced/src/advanced_omi_backend/services/memory/providers/chronicle.py, backends/advanced/src/advanced_omi_backend/routers/modules/admin_routes.py, backends/advanced/src/advanced_omi_backend/controllers/system_controller.py, backends/advanced/src/advanced_omi_backend/workers/cleanup_jobs.py
Added update_memory() method for in-place memory content and embedding updates. Updated all settings access to use OmegaConf-based getters instead of file-based loaders. Renamed save_diarization_settings() to save_diarization_settings_controller() to avoid conflict with config module.
Model & App Factory
backends/advanced/src/advanced_omi_backend/app_factory.py
Registered Annotation model with Beanie document models during app initialization.
Frontend Components
backends/advanced/webui/src/pages/MemoryDetail.tsx, backends/advanced/webui/src/pages/Conversations.tsx
Added inline editing with keyboard shortcuts (Ctrl/Cmd+Enter to save, Esc/Cmd+Escape to cancel). Integrated annotation creation on save. Updated UI with editing states, error handling, and visual differentiation (yellow-50 for editing, blue-50 for playing).
Dependencies & Config
backends/advanced/pyproject.toml, extras/speaker-recognition/pyproject.toml, setup-requirements.txt, extras/speaker-recognition/.env.template, extras/speaker-recognition/docker-compose.yml, extras/speaker-recognition/src/simple_speaker_recognition/api/service.py, backends/advanced/src/advanced_omi_backend/app_factory.py
Added omegaconf>=2.3.0, httpx>=0.27.0, and pyyaml>=6.0.0 dependencies. Stripped speaker-recognition .env.template from deployment config to secrets-only format. Added config volume mount to speaker-service container. Implemented root config loading in speaker service via load_speaker_config_from_root(). Removed diarization_config.json.template.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Feature/annotations' is vague and generic. While it references 'annotations', this PR encompasses numerous substantial features beyond just annotations including OmegaConf configuration management, audio segment fetching, speaker recognition improvements, cron jobs, UI editing, and environment restructuring. Expand the title to reflect the primary change or most significant feature. Consider 'Add annotations system with OmegaConf config management' or 'Add user annotations and AI suggestions for transcripts/memories' to better convey scope and intent.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 95.52% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@AnkushMalaker
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
backends/advanced/src/advanced_omi_backend/workers/cleanup_jobs.py (1)

139-140: Bug: settings is undefined – this will raise NameError at runtime.

Line 140 references settings.retention_days, but settings was never defined in this function. It appears this line wasn't updated during the migration to dictionary-based config access.

🐛 Proposed fix
     if retention_days is None:
-        retention_days = settings.retention_days
+        retention_days = settings_dict['retention_days']
backends/advanced/src/advanced_omi_backend/speaker_recognition_client.py (2)

91-93: Inconsistent error return shapes.

When disabled (line 92-93), the method returns {}. On HTTP errors (line 163), it also returns {}. However, exception handlers (lines 175-184) return {"error": ..., "message": ..., "segments": []}.

Callers expecting the segments key will fail when receiving {}. Consider standardizing the return shape:

♻️ Suggested fix for consistency
         if not self.enabled:
             logger.info(f"🎤 Speaker recognition disabled, returning empty result")
-            return {}
+            return {"segments": []}
                     if response.status != 200:
                         response_text = await response.text()
                         logger.error(
                             f"🎤 ❌ Speaker service returned status {response.status}: {response_text}"
                         )
-                        return {}
+                        return {"error": "http_error", "message": response_text, "segments": []}

Also applies to: 163-163, 175-184


336-347: Inconsistent config access: uses private _diarization_settings instead of get_diarization_settings().

This method imports _diarization_settings directly from system_controller, while diarize_identify_match (line 99) and diarize_and_identify (line 222) use the new get_diarization_settings() function.

Update for consistency with the new OmegaConf-based config:

🐛 Suggested fix
                     # Get current diarization settings
-                    from advanced_omi_backend.controllers.system_controller import _diarization_settings
+                    from advanced_omi_backend.config import get_diarization_settings
+                    _diarization_settings = get_diarization_settings()
🤖 Fix all issues with AI agents
In `@backends/advanced/src/advanced_omi_backend/controllers/system_controller.py`:
- Around line 181-182: The call to save_cleanup_settings is not checking its
boolean return value, so failures still produce a success response; update the
controller that calls save_cleanup_settings to capture its return (e.g., ok =
save_cleanup_settings(settings)) and mirror the error handling used in
save_diarization_settings_controller: if ok is False, log the error and return a
failure response (HTTP error or structured error body) instead of proceeding to
a success response; ensure you reference save_cleanup_settings in the
log/response to aid debugging.
- Line 79: Router still calls the old function name
system_controller.save_diarization_settings which was renamed to
save_diarization_settings_controller; update the router handler to call and
await the new function name, e.g. replace
system_controller.save_diarization_settings(settings) with await
system_controller.save_diarization_settings_controller(settings) so the router
invokes the correct async function.

In
`@backends/advanced/src/advanced_omi_backend/routers/modules/annotation_routes.py`:
- Around line 286-298: The Conversation.find_one call that fetches by
Conversation.conversation_id should also filter by the owner to enforce
defense-in-depth: change the lookup in the annotation update path to include
Conversation.user_id == annotation.user_id (i.e., use
Conversation.find_one(Conversation.conversation_id ==
annotation.conversation_id, Conversation.user_id == annotation.user_id)); keep
the subsequent get_active_transcript, segment bounds check, text assignment to
transcript.segments[annotation.segment_index].text = annotation.corrected_text,
and await conversation.save() unchanged.
- Around line 74-90: The current flow marks and saves the annotation as ACCEPTED
before calling memory_service.update_memory, leading to inconsistency if
update_memory fails; change the flow in the route handling code that checks
annotation.status == AnnotationStatus.ACCEPTED so that you first call await
memory_service.update_memory(...) using annotation_data.memory_id and
annotation_data.corrected_text, ensure it succeeds, and only then set
annotation.status = AnnotationStatus.ACCEPTED and persist/save the annotation
(or alternatively, if you must save earlier, implement a rollback that resets
annotation.status to PENDING and re-saves the annotation when update_memory
raises); update logging to record success/failure tied to annotation.id and
include the chosen rollback or save-after-update behavior consistently.

In
`@backends/advanced/src/advanced_omi_backend/routers/modules/conversation_routes.py`:
- Around line 259-267: Replace the broad except block around the call to
reconstruct_audio_segment with more precise handling: catch expected errors
(e.g., ValueError, IOError or a domain-specific ReconstructionError if present)
instead of bare Exception, log the failure with logger.exception(...) to
preserve the traceback and context for conversation_id, and re-raise the FastAPI
HTTPException using "raise HTTPException(... ) from e" to preserve the exception
chain; keep references to reconstruct_audio_segment, logger, HTTPException and
conversation_id when making these changes.

In
`@backends/advanced/src/advanced_omi_backend/services/memory/providers/chronicle.py`:
- Around line 365-374: The code incorrectly calls
self.llm_provider.generate_embedding(...) which doesn't exist; replace both
calls to generate_embedding with the correct method generate_embeddings and take
the first embedding from the returned list (e.g., call
generate_embeddings(new_content) and assign new_embedding = result[0]); update
both places in the conditional around existing_memory.embedding so new_embedding
is always set from either existing_memory.embedding or the first element
returned by generate_embeddings.

In `@backends/advanced/src/advanced_omi_backend/speaker_recognition_client.py`:
- Around line 69-75: The user_id parameter of diarize_identify_match is unused
and the form data currently hardcodes "1"; either remove user_id from the
function signature (and update all callers/tests) or use it in the form payload
by replacing the hardcoded "1" values with the validated user_id (ensure
non-empty/expected format), update the form field name where transcript_data is
packaged, and adjust any unit/integration tests and callers that pass user_id
accordingly; reference the diarize_identify_match method and the form-data
assembly spots that currently use the literal "1" to make these changes.

In `@backends/advanced/src/advanced_omi_backend/utils/audio_chunk_utils.py`:
- Around line 531-549: The validation of start_time/end_time runs before
clamping end_time to the conversation duration, which can turn a previously
valid range into an invalid one (silently producing empty audio); after fetching
total_duration from Conversation.get and clamping end_time (and optionally
clamping start_time to <= total_duration), re-run the range check and raise a
ValueError if start_time >= end_time (or alternatively clamp both start_time =
min(start_time, total_duration) and end_time = min(end_time, total_duration) and
then validate), so update the logic around Conversation.get, total_duration, and
the clamping of end_time to ensure you validate the final clamped values before
proceeding.

In `@backends/advanced/src/advanced_omi_backend/workers/annotation_jobs.py`:
- Around line 104-106: The log call is printing PII via user.email; update the
logger.debug invocation used in the annotation job (the line calling
logger.debug with f"   Analyzed user {user.email} (placeholder)") to avoid
logging emails—use a non-PII identifier such as user.id or an anonymized token
instead (e.g., reference user.id in the message), and ensure any tests or other
code that reads that log are adjusted accordingly.

In `@backends/advanced/webui/src/pages/MemoryDetail.tsx`:
- Around line 94-129: The client currently assumes
annotationsApi.createMemoryAnnotation succeeded and updates local state
immediately in handleSaveEdit; change handleSaveEdit to inspect the API response
from annotationsApi.createMemoryAnnotation (e.g., a returned annotation object
or a memory_updated flag) and only call setMemory / setIsEditing(false) when the
response explicitly indicates the memory was updated; if the response indicates
a failure to apply the memory change (or returns an error), setSaveError with
the backend detail and do not update local state. Locate handleSaveEdit and the
annotationsApi.createMemoryAnnotation call to implement this check and handle
both success and failure paths (and consider falling back to retry or refreshing
the memory from the server if no explicit flag is returned).

In `@config/defaults.yml`:
- Around line 254-255: The default backend_api_url currently points to
host.docker.internal which doesn't resolve on many native Linux Docker setups;
change the default value for backend_api_url to use http://127.0.0.1:8000 (or
localhost) and add a brief note in the config/comments that Linux users who rely
on host.docker.internal must add an explicit --add-host or override the value
via environment/config, referencing the backend_api_url key so maintainers can
locate and document/override it.
- Around line 242-258: The OmegaConf environment interpolations for optional
secrets (hf_token and deepgram_api_key) use `${oc.env:VAR}` which throws
InterpolationKeyError if unset; update those keys (hf_token and
deepgram_api_key) to include explicit defaults using the correct syntax like
`${oc.env:HF_TOKEN,""}` or `${oc.env:DEEPGRAM_API_KEY,""}` (or ,null if you want
null) so the config loads when the env vars are absent. Ensure you only change
the hf_token and deepgram_api_key lines to include the default value and leave
other keys (similarity_threshold, max_diarize_duration, diarize_chunk_overlap,
backend_api_url) untouched.

In
`@extras/speaker-recognition/src/simple_speaker_recognition/api/routers/identification.py`:
- Around line 306-309: The log call using log.info with the literal "Processing
diarize-identify-match request" incorrectly uses an f-string prefix; remove the
leading "f" on that specific log.info invocation so the string is a plain
literal while leaving the other log.info calls (which use
expressions/placeholders like the mode conditional and transcript length)
unchanged.
- Around line 319-325: Remove the unused local full_text assignment and preserve
exception chaining: in the JSON parsing block that reads transcript_data into
transcript and words, drop the unused full_text = transcript.get("text", "")
line and change the except handler to raise HTTPException(400, f"Invalid
transcript_data JSON: {str(e)}") from e so the JSONDecodeError is chained; keep
the variable name e for the caught JSONDecodeError to reference in the chained
raise.
- Around line 336-366: The code assumes metadata['duration'] exists and will
KeyError if missing; update the conversation branch in identification.py to use
metadata.get('duration') and validate it is a number (e.g., not None and >0),
and if invalid raise an HTTPException(400) with a clear message about
missing/invalid duration; also wrap the calls to
BackendClient.get_conversation_metadata and get_audio_segment in a try/except
that converts backend errors into user-friendly HTTPException messages (while
leaving the finally await backend_client.close() intact) so failures return
meaningful HTTP errors instead of raw exceptions.
- Around line 368-386: The temporary file created with
tempfile.NamedTemporaryFile (tmp_path) can leak if get_audio_info() raises or
any other exception occurs; wrap the file creation and subsequent processing
(call to get_audio_info and duration validation against MAX_AUDIO_DURATION) in a
try/finally (or try/except where finally unlinks tmp_path) so that
tmp_path.unlink(missing_ok=True) is always called on error/exit, or perform
get_audio_info and validation inside the with-block before breaking the context,
ensuring tmp_path is deleted in the finally block when an exception is raised.
🧹 Nitpick comments (24)
backends/advanced/src/advanced_omi_backend/controllers/audio_controller.py (1)

117-117: Dead code: unused version_id assignment.

This version_id is never used before being overwritten at line 180. Consider removing it to avoid confusion.

♻️ Suggested fix
-                version_id = str(uuid.uuid4())
-
                 # Generate title from filename
backends/advanced/src/advanced_omi_backend/utils/audio_chunk_utils.py (1)

503-528: Consider documenting that returned audio may exceed the requested time range.

The function returns complete overlapping chunks rather than trimming to exact start_time/end_time boundaries. For example, requesting 5.0-15.0 seconds from a conversation with 10-second chunks would return 20 seconds of audio (chunks 0-10 and 10-20). The existing reconstruct_audio_segments documents this behavior but this function's docstring doesn't mention it.

Suggested docstring addition
     Returns:
         WAV audio bytes (16kHz mono or original format)
+
+    Note:
+        The returned audio includes complete overlapping chunks and may be
+        slightly longer than the requested range. Callers should trim or
+        merge overlapping regions during post-processing if exact boundaries
+        are required.
backends/advanced/src/advanced_omi_backend/services/memory/providers/chronicle.py (1)

324-331: Unused user_email parameter.

The user_email parameter is declared but never used in the method body. If it's kept for API consistency with other providers, prefix it with an underscore to suppress the linter warning and signal intent:

     async def update_memory(
         self,
         memory_id: str,
         content: Optional[str] = None,
         metadata: Optional[dict[str, Any]] = None,
         user_id: Optional[str] = None,
-        user_email: Optional[str] = None
+        _user_email: Optional[str] = None
     ) -> bool:

Alternatively, if it might be used in the future (e.g., for audit logging), consider adding a brief comment explaining its purpose.

extras/speaker-recognition/src/simple_speaker_recognition/core/backend_client.py (2)

14-24: Consider passing base_url to AsyncClient for proper URL handling.

The AsyncClient is created without the base_url parameter, requiring you to construct full URLs manually in each method. Using httpx.AsyncClient(base_url=..., timeout=...) would simplify the URL construction and is the idiomatic httpx pattern.

Additionally, consider implementing async context manager support (__aenter__/__aexit__) for safer resource management.

♻️ Suggested improvement
     def __init__(self, base_url: str, timeout: float = 30.0):
         """
         Initialize backend client.

         Args:
             base_url: Backend API base URL (e.g., http://host.docker.internal:8000)
             timeout: Request timeout in seconds (default: 30.0)
         """
         self.base_url = base_url.rstrip('/')
         self.timeout = timeout
-        self.client = httpx.AsyncClient(timeout=self.timeout)
+        self.client = httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout)
+
+    async def __aenter__(self):
+        return self
+
+    async def __aexit__(self, exc_type, exc_val, exc_tb):
+        await self.close()

100-103: Consider adding exception handling in close() for resilience.

Based on the pattern in mycelia.py (lines 820-829), the close method catches exceptions during aclose() and logs them without re-raising. This prevents cleanup failures from masking the original exception when called in finally blocks.

♻️ More resilient close implementation
     async def close(self):
         """Close HTTP client and release resources."""
-        await self.client.aclose()
-        logger.debug("Backend client closed")
+        try:
+            await self.client.aclose()
+            logger.debug("Backend client closed")
+        except Exception as e:
+            logger.warning(f"Error closing backend client: {e}")
backends/advanced/src/advanced_omi_backend/workers/cleanup_jobs.py (1)

15-15: Unused import: CleanupSettings is imported but never used.

The dataclass CleanupSettings is imported but not referenced anywhere in this file.

♻️ Proposed fix
-from advanced_omi_backend.config import get_cleanup_settings, CleanupSettings
+from advanced_omi_backend.config import get_cleanup_settings
backends/advanced/src/advanced_omi_backend/workers/speaker_jobs.py (2)

211-231: Token-based authentication approach is sound.

The shift to having the speaker service fetch audio from the backend using a JWT is a good architectural improvement – it decouples audio reconstruction from the job and lets the speaker service manage its own memory constraints.

Minor: Remove the extraneous f prefix on line 231 since the string has no placeholders.

♻️ Proposed fix for static analysis warning
-        logger.info(f"🔐 Generated backend token for speaker service")
+        logger.info("🔐 Generated backend token for speaker service")

243-257: Speaker service call pattern looks good.

The clear comments explaining the speaker service's responsibilities (fetching metadata, chunking decisions, audio segment requests) improve maintainability.

Minor: Remove the extraneous f prefix on line 249.

♻️ Proposed fix for static analysis warning
-    logger.info(f"🎤 Calling speaker recognition service with conversation_id...")
+    logger.info("🎤 Calling speaker recognition service with conversation_id...")
backends/advanced/src/advanced_omi_backend/models/annotation.py (1)

46-53: Consider validation for polymorphic field consistency.

The model allows creating annotations where neither memory_id nor conversation_id is set (or both are set), which could lead to inconsistent data. Consider adding a model validator to enforce that the appropriate reference field is set based on annotation_type.

♻️ Example validator
from pydantic import model_validator

class Annotation(Document):
    # ... existing fields ...

    `@model_validator`(mode='after')
    def validate_polymorphic_refs(self) -> 'Annotation':
        if self.annotation_type == AnnotationType.MEMORY:
            if not self.memory_id:
                raise ValueError("memory_id required for MEMORY annotations")
        elif self.annotation_type == AnnotationType.TRANSCRIPT:
            if not self.conversation_id or self.segment_index is None:
                raise ValueError("conversation_id and segment_index required for TRANSCRIPT annotations")
        return self
config/config.yml.template (1)

220-223: Consider using environment variable interpolation for backend_api_url.

The diarization chunking configuration looks reasonable. However, host.docker.internal works out-of-the-box on Docker Desktop (macOS/Windows) but requires extra configuration on Linux Docker (--add-host=host.docker.internal:host-gateway).

For consistency with other URLs in this file (e.g., model_url entries), consider using environment variable interpolation:

Suggested change
   max_diarize_duration: 60  # Maximum audio duration (seconds) for single PyAnnote call
   diarize_chunk_overlap: 5.0  # Overlap (seconds) between chunks
-  backend_api_url: http://host.docker.internal:8000  # Backend API URL
+  backend_api_url: ${BACKEND_API_URL:-http://host.docker.internal:8000}  # Backend API URL

This allows users on Linux or non-Docker environments to override the URL without modifying the config file.

backends/advanced/docker-compose.yml (1)

118-143: Consider adding a healthcheck for operational visibility.

The annotation-cron service lacks a healthcheck, unlike chronicle-backend and other services. While cron jobs don't serve HTTP requests, a healthcheck can help detect if the scheduler is stuck or has crashed.

Additionally, the inline comment on line 143 mentions "Optional profile - enable with: docker compose --profile annotation up" which is accurate, but ensure this aligns with documentation if users expect different profile names.

💡 Optional healthcheck example
     restart: unless-stopped
+    healthcheck:
+      test: ["CMD", "pgrep", "-f", "advanced_omi_backend.cron"]
+      interval: 60s
+      timeout: 10s
+      retries: 3
+      start_period: 30s
     profiles:
       - annotation  # Optional profile - enable with: docker compose --profile annotation up
config/defaults.yml (1)

268-290: Backend secrets using oc.env without defaults will fail if unset.

Similar to the speaker_recognition section, the backend auth and API keys use strict oc.env interpolation that will throw errors if the corresponding environment variables are not defined. This includes AUTH_SECRET_KEY, ADMIN_EMAIL, ADMIN_PASSWORD, OPENAI_API_KEY, and DEEPGRAM_API_KEY.

If these are truly required, consider adding validation with clear error messages. If optional, provide empty defaults.

💡 Suggested fix for optional keys
   llm:
     provider: openai  # or ollama
-    api_key: ${oc.env:OPENAI_API_KEY}
+    api_key: ${oc.env:OPENAI_API_KEY,}
     base_url: https://api.openai.com/v1
     model: gpt-4o-mini
     timeout: 60

   transcription:
     provider: deepgram  # or parakeet
-    api_key: ${oc.env:DEEPGRAM_API_KEY}
+    api_key: ${oc.env:DEEPGRAM_API_KEY,}
     base_url: https://api.deepgram.com
backends/advanced/webui/src/pages/Conversations.tsx (1)

928-936: Verify behavior when conversation_id is missing but edit is initiated.

Line 930 checks conversation.conversation_id before calling handleStartSegmentEdit, but the first argument passed is conversationKey which falls back to audio_uuid (line 853-854). This guard is correct, but the click will silently do nothing when conversation_id is missing.

Consider adding a visual indicator (e.g., tooltip or cursor style) to signal that editing is unavailable for conversations without a conversation_id.

extras/speaker-recognition/src/simple_speaker_recognition/api/service.py (1)

56-57: Consider reducing log verbosity for loaded config.

Logging the entire resolved config at INFO level could expose sensitive information if configuration values (like URLs or tokens) are included. Consider logging at DEBUG level or only logging non-sensitive keys.

📝 Suggested change
-        log.info(f"Loaded speaker_recognition config: {resolved}")
+        log.debug(f"Loaded speaker_recognition config: {list(resolved.keys())}")
         return resolved
backends/advanced/src/advanced_omi_backend/cron.py (3)

53-64: Database client is never closed.

The AsyncIOMotorClient is created but never stored for later cleanup. If the scheduler is restarted or stopped, the connection may leak.

Consider storing the client reference and closing it gracefully:

♻️ Suggested improvement
+# Module-level client reference for cleanup
+_mongo_client: Optional[AsyncIOMotorClient] = None

 async def init_db():
     """Initialize database connection"""
+    global _mongo_client
     try:
-        client = AsyncIOMotorClient(MONGODB_URI)
+        _mongo_client = AsyncIOMotorClient(MONGODB_URI)
         await init_beanie(
-            database=client.chronicle,
+            database=_mongo_client.chronicle,
             document_models=[Annotation, Conversation, User],
         )
         logger.info("✅ Database connection initialized")
     except Exception as e:
-        logger.error(f"❌ Failed to initialize database: {e}")
+        logger.exception(f"❌ Failed to initialize database: {e}")
         raise

Also, per static analysis hint, use logger.exception on line 63 to include the stack trace automatically.


74-76: Jobs will run immediately on first startup.

Setting last_suggestion_run and last_training_run to now means the elapsed time is ~0 seconds on the first loop iteration. This is actually correct behavior (jobs won't run immediately). However, if the intent is to run jobs immediately on startup, this would need adjustment.

If immediate execution on startup is desired:

♻️ Optional: Run jobs immediately on startup
-    last_suggestion_run = datetime.now(timezone.utc)
-    last_training_run = datetime.now(timezone.utc)
+    # Set to epoch to trigger immediate run on startup
+    last_suggestion_run = datetime.min.replace(tzinfo=timezone.utc)
+    last_training_run = datetime.min.replace(tzinfo=timezone.utc)

104-110: KeyboardInterrupt handling inside async loop may be unreliable.

In asyncio.run(), SIGINT typically cancels the running task via CancelledError before raising KeyboardInterrupt at the top level. The inner handler (lines 104-106) may not reliably catch keyboard interrupts; the outer handler at lines 117-118 is more appropriate.

Consider catching asyncio.CancelledError instead for graceful shutdown within the coroutine:

♻️ Suggested improvement
-        except KeyboardInterrupt:
-            logger.info("⛔ Scheduler stopped by user")
-            break
+        except asyncio.CancelledError:
+            logger.info("⛔ Scheduler cancelled")
+            raise  # Re-raise to allow proper cleanup
backends/advanced/src/advanced_omi_backend/workers/annotation_jobs.py (1)

11-13: Unused imports: timedelta and Conversation.

These imports are only used in commented-out TODO code. Consider removing them until the implementation is complete to avoid confusion:

♻️ Optional cleanup
-from datetime import datetime, timezone, timedelta
+from datetime import datetime, timezone
 from typing import List

 from advanced_omi_backend.models.annotation import (
     Annotation,
     AnnotationSource,
     AnnotationStatus,
     AnnotationType,
 )
-from advanced_omi_backend.models.conversation import Conversation
 from advanced_omi_backend.models.user import User

Also applies to: 21-22

extras/speaker-recognition/src/simple_speaker_recognition/core/audio_backend.py (1)

157-163: soundfile import inside function may fail silently.

The import soundfile as sf at line 159 is inside the with block. If soundfile is not installed, this will raise ImportError which gets caught by the outer exception handling and returns an empty list, potentially masking a configuration issue.

Consider importing at module level or providing a clearer error:

♻️ Option 1: Import at module level (preferred)

Add to imports at the top of the file:

import soundfile as sf

Then remove the inline import at line 159.

♻️ Option 2: Explicit error handling
             # Write chunk to temp file for PyAnnote
             with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmp:
-                import soundfile as sf
+                try:
+                    import soundfile as sf
+                except ImportError:
+                    raise ImportError("soundfile library required for audio chunking. Install with: pip install soundfile")
                 # Extract tensor data and write as WAV
                 audio_tensor = chunk_audio.squeeze().cpu().numpy()
                 sf.write(tmp.name, audio_tensor, 16000)
                 chunk_path = Path(tmp.name)
backends/advanced/src/advanced_omi_backend/config_loader.py (2)

65-66: Use logger.exception for stack traces on config load errors.

Per static analysis hint, logger.exception automatically includes the stack trace:

♻️ Suggested fix
         except Exception as e:
-            logger.error(f"Error loading config from {config_path}: {e}")
+            logger.exception(f"Error loading config from {config_path}: {e}")

119-152: Consider thread/process safety for config file writes.

save_config_section performs read-modify-write without file locking. In multi-process deployments, concurrent calls could cause data loss.

If runtime config updates are expected to be rare and single-threaded, this is acceptable. Otherwise, consider adding file locking:

♻️ Optional: Add file locking for safety
+import fcntl
+
 def save_config_section(section_path: str, values: dict) -> bool:
     ...
     try:
         config_path = get_config_dir() / "config.yml"

-        # Load existing config
-        existing_config = {}
-        if config_path.exists():
-            existing_config = OmegaConf.load(config_path)
+        # Use file locking for concurrent access safety
+        with open(config_path, 'r+' if config_path.exists() else 'w+') as f:
+            fcntl.flock(f, fcntl.LOCK_EX)
+            try:
+                existing_config = OmegaConf.load(config_path) if config_path.exists() else {}
+                OmegaConf.update(existing_config, section_path, values, merge=True)
+                OmegaConf.save(existing_config, config_path)
+            finally:
+                fcntl.flock(f, fcntl.LOCK_UN)

Also, use logger.exception on line 151 per static analysis hint.

backends/advanced/src/advanced_omi_backend/routers/modules/annotation_routes.py (1)

13-13: Unused import: JSONResponse.

JSONResponse is imported but never used in this module. All endpoints return Pydantic models or dictionaries.

🧹 Remove unused import
 from fastapi import APIRouter, Depends, HTTPException
-from fastapi.responses import JSONResponse
backends/advanced/src/advanced_omi_backend/config.py (1)

115-116: Consider moving asdict import to top of file.

dataclasses is already imported at line 10 for the @dataclass decorator. The asdict function can be imported alongside it rather than inside the function body.

🧹 Move import to top of file
-from dataclasses import dataclass
+from dataclasses import asdict, dataclass

Then remove the inline import:

 def save_cleanup_settings(settings: CleanupSettings) -> bool:
     ...
-    from dataclasses import asdict
     return save_config_section('backend.cleanup', asdict(settings))
backends/advanced/.env.template (1)

17-18: Consider adding a generation command hint for AUTH_SECRET_KEY.

Adding an example command helps users generate a secure key without searching for how to do it.

📝 Add generation hint
 # JWT signing key (generate a long random string)
+# Generate with: openssl rand -hex 32
 AUTH_SECRET_KEY=
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 078c602 and b660f65.

📒 Files selected for processing (35)
  • backends/advanced/.env.template
  • backends/advanced/diarization_config.json.template
  • backends/advanced/docker-compose.yml
  • backends/advanced/pyproject.toml
  • backends/advanced/src/advanced_omi_backend/app_factory.py
  • backends/advanced/src/advanced_omi_backend/config.py
  • backends/advanced/src/advanced_omi_backend/config_loader.py
  • backends/advanced/src/advanced_omi_backend/controllers/audio_controller.py
  • backends/advanced/src/advanced_omi_backend/controllers/system_controller.py
  • backends/advanced/src/advanced_omi_backend/cron.py
  • backends/advanced/src/advanced_omi_backend/models/annotation.py
  • backends/advanced/src/advanced_omi_backend/routers/api_router.py
  • backends/advanced/src/advanced_omi_backend/routers/modules/__init__.py
  • backends/advanced/src/advanced_omi_backend/routers/modules/admin_routes.py
  • backends/advanced/src/advanced_omi_backend/routers/modules/annotation_routes.py
  • backends/advanced/src/advanced_omi_backend/routers/modules/conversation_routes.py
  • backends/advanced/src/advanced_omi_backend/services/memory/providers/chronicle.py
  • backends/advanced/src/advanced_omi_backend/speaker_recognition_client.py
  • backends/advanced/src/advanced_omi_backend/utils/audio_chunk_utils.py
  • backends/advanced/src/advanced_omi_backend/workers/annotation_jobs.py
  • backends/advanced/src/advanced_omi_backend/workers/cleanup_jobs.py
  • backends/advanced/src/advanced_omi_backend/workers/speaker_jobs.py
  • backends/advanced/webui/src/pages/Conversations.tsx
  • backends/advanced/webui/src/pages/MemoryDetail.tsx
  • backends/advanced/webui/src/services/api.ts
  • config/config.yml.template
  • config/defaults.yml
  • extras/speaker-recognition/.env.template
  • extras/speaker-recognition/docker-compose.yml
  • extras/speaker-recognition/pyproject.toml
  • extras/speaker-recognition/src/simple_speaker_recognition/api/routers/identification.py
  • extras/speaker-recognition/src/simple_speaker_recognition/api/service.py
  • extras/speaker-recognition/src/simple_speaker_recognition/core/audio_backend.py
  • extras/speaker-recognition/src/simple_speaker_recognition/core/backend_client.py
  • setup-requirements.txt
💤 Files with no reviewable changes (1)
  • backends/advanced/diarization_config.json.template
🧰 Additional context used
🧬 Code graph analysis (17)
backends/advanced/src/advanced_omi_backend/app_factory.py (1)
backends/advanced/src/advanced_omi_backend/models/annotation.py (1)
  • Annotation (36-100)
backends/advanced/webui/src/pages/MemoryDetail.tsx (1)
backends/advanced/webui/src/services/api.ts (1)
  • annotationsApi (149-177)
backends/advanced/webui/src/pages/Conversations.tsx (1)
backends/advanced/webui/src/services/api.ts (1)
  • annotationsApi (149-177)
backends/advanced/src/advanced_omi_backend/services/memory/providers/chronicle.py (4)
backends/advanced/src/advanced_omi_backend/services/memory/base.py (4)
  • update_memory (187-210)
  • update_memory (425-443)
  • initialize (81-90)
  • initialize (356-362)
backends/advanced/src/advanced_omi_backend/services/memory/providers/openmemory_mcp.py (2)
  • update_memory (322-365)
  • initialize (71-104)
backends/advanced/src/advanced_omi_backend/services/memory/providers/vector_stores.py (2)
  • update_memory (315-358)
  • initialize (52-118)
backends/advanced/src/advanced_omi_backend/services/memory/providers/mcp_client.py (1)
  • update_memory (407-452)
backends/advanced/src/advanced_omi_backend/workers/speaker_jobs.py (4)
backends/advanced/src/advanced_omi_backend/auth.py (1)
  • generate_jwt_for_user (116-145)
backends/advanced/src/advanced_omi_backend/models/user.py (1)
  • user_id (71-73)
backends/advanced/src/advanced_omi_backend/speaker_recognition_client.py (1)
  • diarize_identify_match (69-184)
extras/speaker-recognition/src/simple_speaker_recognition/api/routers/identification.py (1)
  • diarize_identify_match (270-498)
backends/advanced/src/advanced_omi_backend/routers/modules/admin_routes.py (2)
backends/advanced/src/advanced_omi_backend/config.py (1)
  • get_cleanup_settings (94-102)
backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py (1)
  • get_cleanup_settings (61-65)
backends/advanced/src/advanced_omi_backend/models/annotation.py (1)
backends/advanced/src/advanced_omi_backend/models/user.py (1)
  • user_id (71-73)
extras/speaker-recognition/src/simple_speaker_recognition/api/service.py (1)
backends/advanced/src/advanced_omi_backend/models/annotation.py (1)
  • Config (138-139)
backends/advanced/src/advanced_omi_backend/utils/audio_chunk_utils.py (2)
backends/advanced/src/advanced_omi_backend/models/conversation.py (1)
  • Conversation (17-328)
backends/advanced/src/advanced_omi_backend/models/audio_chunk.py (1)
  • AudioChunkDocument (16-158)
extras/speaker-recognition/src/simple_speaker_recognition/core/backend_client.py (2)
backends/advanced/src/advanced_omi_backend/routers/modules/conversation_routes.py (1)
  • get_conversation_metadata (170-205)
backends/advanced/src/advanced_omi_backend/services/memory/providers/mycelia.py (1)
  • aclose (821-830)
extras/speaker-recognition/src/simple_speaker_recognition/core/audio_backend.py (2)
extras/speaker-omni-experimental/qwen_speaker_diarizer.py (1)
  • chunk_audio (142-192)
extras/speaker-recognition/webui/src/services/api.ts (1)
  • delete (67-70)
backends/advanced/src/advanced_omi_backend/config_loader.py (1)
backends/advanced/src/advanced_omi_backend/config.py (1)
  • reload_config (50-52)
backends/advanced/src/advanced_omi_backend/speaker_recognition_client.py (3)
backends/advanced/src/advanced_omi_backend/config.py (1)
  • get_diarization_settings (59-67)
backends/advanced/src/advanced_omi_backend/controllers/system_controller.py (1)
  • get_diarization_settings (65-76)
backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py (1)
  • get_diarization_settings (46-48)
extras/speaker-recognition/src/simple_speaker_recognition/api/routers/identification.py (7)
extras/speaker-recognition/src/simple_speaker_recognition/core/backend_client.py (4)
  • BackendClient (11-103)
  • get_conversation_metadata (26-55)
  • get_audio_segment (57-98)
  • close (100-103)
backends/advanced/src/advanced_omi_backend/routers/modules/conversation_routes.py (2)
  • get_conversation_metadata (170-205)
  • get_audio_segment (209-278)
extras/speaker-recognition/src/simple_speaker_recognition/utils/audio_processing.py (1)
  • get_audio_info (53-87)
extras/speaker-recognition/src/simple_speaker_recognition/api/routers/websocket_wrapper.py (1)
  • get_audio_backend (45-48)
extras/speaker-recognition/src/simple_speaker_recognition/api/routers/deepgram_wrapper.py (1)
  • get_audio_backend (35-38)
extras/speaker-recognition/src/simple_speaker_recognition/api/routers/enrollment.py (1)
  • get_audio_backend (36-39)
extras/speaker-recognition/src/simple_speaker_recognition/core/audio_backend.py (1)
  • async_diarize (110-197)
backends/advanced/src/advanced_omi_backend/routers/modules/conversation_routes.py (4)
backends/advanced/src/advanced_omi_backend/models/conversation.py (1)
  • Conversation (17-328)
extras/speaker-recognition/src/simple_speaker_recognition/core/backend_client.py (2)
  • get_conversation_metadata (26-55)
  • get_audio_segment (57-98)
backends/advanced/src/advanced_omi_backend/models/user.py (1)
  • user_id (71-73)
backends/advanced/src/advanced_omi_backend/utils/audio_chunk_utils.py (1)
  • reconstruct_audio_segment (498-597)
backends/advanced/src/advanced_omi_backend/workers/cleanup_jobs.py (2)
backends/advanced/src/advanced_omi_backend/config.py (2)
  • get_cleanup_settings (94-102)
  • CleanupSettings (88-91)
backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py (1)
  • get_cleanup_settings (61-65)
backends/advanced/src/advanced_omi_backend/controllers/system_controller.py (2)
backends/advanced/src/advanced_omi_backend/config.py (4)
  • get_diarization_settings (59-67)
  • save_diarization_settings (70-80)
  • CleanupSettings (88-91)
  • save_cleanup_settings (105-116)
backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py (3)
  • get_diarization_settings (46-48)
  • save_diarization_settings (52-57)
  • save_cleanup_settings (69-79)
🪛 Ruff (0.14.11)
backends/advanced/src/advanced_omi_backend/services/memory/providers/chronicle.py

330-330: Unused method argument: user_email

(ARG002)


389-389: Consider moving this statement to an else block

(TRY300)

backends/advanced/src/advanced_omi_backend/workers/speaker_jobs.py

231-231: f-string without any placeholders

Remove extraneous f prefix

(F541)


249-249: f-string without any placeholders

Remove extraneous f prefix

(F541)

backends/advanced/src/advanced_omi_backend/workers/annotation_jobs.py

109-109: String contains ambiguous (INFORMATION SOURCE). Did you mean i (LATIN SMALL LETTER I)?

(RUF001)


159-159: String contains ambiguous (INFORMATION SOURCE). Did you mean i (LATIN SMALL LETTER I)?

(RUF001)


198-198: Comment contains ambiguous (INFORMATION SOURCE). Did you mean i (LATIN SMALL LETTER I)?

(RUF003)


209-209: String contains ambiguous (INFORMATION SOURCE). Did you mean i (LATIN SMALL LETTER I)?

(RUF001)

backends/advanced/src/advanced_omi_backend/models/annotation.py

79-85: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

extras/speaker-recognition/src/simple_speaker_recognition/api/service.py

57-57: Consider moving this statement to an else block

(TRY300)


59-59: Do not catch blind exception: Exception

(BLE001)

backends/advanced/src/advanced_omi_backend/utils/audio_chunk_utils.py

533-533: Avoid specifying long messages outside the exception class

(TRY003)


535-535: Avoid specifying long messages outside the exception class

(TRY003)


541-541: Avoid specifying long messages outside the exception class

(TRY003)


546-546: Avoid specifying long messages outside the exception class

(TRY003)


557-557: Avoid specifying long messages outside the exception class

(TRY003)

backends/advanced/src/advanced_omi_backend/cron.py

63-63: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

backends/advanced/src/advanced_omi_backend/routers/modules/annotation_routes.py

36-36: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


55-55: Abstract raise to an inner function

(TRY301)


56-56: Do not catch blind exception: Exception

(BLE001)


57-57: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


58-58: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


85-85: Do not catch blind exception: Exception

(BLE001)


86-86: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


98-101: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


100-100: Use explicit conversion flag

Replace with conversion flag

(RUF010)


107-107: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


124-124: Abstract raise to an inner function

(TRY301)


132-132: Abstract raise to an inner function

(TRY301)


175-175: Do not catch blind exception: Exception

(BLE001)


176-176: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


188-191: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


190-190: Use explicit conversion flag

Replace with conversion flag

(RUF010)


197-197: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


211-214: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


213-213: Use explicit conversion flag

Replace with conversion flag

(RUF010)


220-220: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


234-237: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


236-236: Use explicit conversion flag

Replace with conversion flag

(RUF010)


244-244: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


257-257: Abstract raise to an inner function

(TRY301)


280-280: Do not catch blind exception: Exception

(BLE001)


281-281: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


302-302: Do not catch blind exception: Exception

(BLE001)


303-303: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


309-309: Consider moving this statement to an else block

(TRY300)


315-318: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


317-317: Use explicit conversion flag

Replace with conversion flag

(RUF010)

backends/advanced/src/advanced_omi_backend/config_loader.py

56-56: Do not catch blind exception: Exception

(BLE001)


65-65: Do not catch blind exception: Exception

(BLE001)


66-66: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


148-148: Consider moving this statement to an else block

(TRY300)


150-150: Do not catch blind exception: Exception

(BLE001)


151-151: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

backends/advanced/src/advanced_omi_backend/speaker_recognition_client.py

74-74: Unused method argument: user_id

(ARG002)

extras/speaker-recognition/src/simple_speaker_recognition/api/routers/identification.py

271-271: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


306-306: f-string without any placeholders

Remove extraneous f prefix

(F541)


323-323: Local variable full_text is assigned to but never used

Remove assignment to unused variable full_text

(F841)


325-325: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


325-325: Use explicit conversion flag

Replace with conversion flag

(RUF010)

backends/advanced/src/advanced_omi_backend/routers/modules/conversation_routes.py

172-172: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


213-213: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


265-265: Do not catch blind exception: Exception

(BLE001)


266-266: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


267-267: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


267-267: Use explicit conversion flag

Replace with conversion flag

(RUF010)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: robot-tests

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.



async def save_diarization_settings(settings: dict):
async def save_diarization_settings_controller(settings: dict):
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "save_diarization_settings" backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py

Repository: SimpleOpenSoftware/chronicle

Length of output: 185


🏁 Script executed:

rg -n "save_diarization_settings" backends/advanced/src/advanced_omi_backend/controllers/system_controller.py

Repository: SimpleOpenSoftware/chronicle

Length of output: 232


🏁 Script executed:

cat -n backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py | grep -A 10 -B 5 "save_diarization"

Repository: SimpleOpenSoftware/chronicle

Length of output: 871


Fix router call to use renamed function.

The function was renamed from save_diarization_settings to save_diarization_settings_controller in system_controller.py (line 79), but the router in system_routes.py (line 57) still calls system_controller.save_diarization_settings(settings). Update the router to call the renamed function to prevent an AttributeError at runtime:

return await system_controller.save_diarization_settings_controller(settings)
🤖 Prompt for AI Agents
In `@backends/advanced/src/advanced_omi_backend/controllers/system_controller.py`
at line 79, Router still calls the old function name
system_controller.save_diarization_settings which was renamed to
save_diarization_settings_controller; update the router handler to call and
await the new function name, e.g. replace
system_controller.save_diarization_settings(settings) with await
system_controller.save_diarization_settings_controller(settings) so the router
invokes the correct async function.

Comment on lines +181 to +182
# Save using OmegaConf
save_cleanup_settings(settings)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing error handling for save_cleanup_settings return value.

Unlike save_diarization_settings_controller (lines 108-122), this function doesn't check the boolean return value of save_cleanup_settings(). If the save fails, the function will still return a success response.

🔧 Suggested fix
     # Save using OmegaConf
-    save_cleanup_settings(settings)
+    if not save_cleanup_settings(settings):
+        raise ValueError("Failed to save cleanup settings")

     logger.info(f"Admin {user.email} updated cleanup settings: auto_cleanup={auto_cleanup_enabled}, retention={retention_days}d")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Save using OmegaConf
save_cleanup_settings(settings)
# Save using OmegaConf
if not save_cleanup_settings(settings):
raise ValueError("Failed to save cleanup settings")
🤖 Prompt for AI Agents
In `@backends/advanced/src/advanced_omi_backend/controllers/system_controller.py`
around lines 181 - 182, The call to save_cleanup_settings is not checking its
boolean return value, so failures still produce a success response; update the
controller that calls save_cleanup_settings to capture its return (e.g., ok =
save_cleanup_settings(settings)) and mirror the error handling used in
save_diarization_settings_controller: if ok is False, log the error and return a
failure response (HTTP error or structured error body) instead of proceeding to
a success response; ensure you reference save_cleanup_settings in the
log/response to aid debugging.

Comment on lines +74 to +90
# Update memory content if accepted
if annotation.status == AnnotationStatus.ACCEPTED:
try:
await memory_service.update_memory(
memory_id=annotation_data.memory_id,
content=annotation_data.corrected_text,
user_id=current_user.user_id,
)
logger.info(
f"Updated memory {annotation_data.memory_id} with corrected text"
)
except Exception as e:
logger.error(f"Error updating memory: {e}")
# Annotation is saved, but memory update failed - log but don't fail the request
logger.warning(
f"Memory annotation {annotation.id} saved but memory update failed"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Data inconsistency risk when memory update fails.

If the memory update fails (lines 85-90), the annotation is already saved with ACCEPTED status, but the memory content remains unchanged. The user sees the annotation as accepted, believing the correction was applied, when it actually wasn't.

Consider either:

  1. Rolling back the annotation status to PENDING on failure
  2. Saving the annotation only after successful memory update
  3. Adding a field to track whether the correction was actually applied
🔧 Suggested fix: Save annotation after memory update succeeds
         # Create annotation
         annotation = Annotation(
             annotation_type=AnnotationType.MEMORY,
             user_id=current_user.user_id,
             memory_id=annotation_data.memory_id,
             original_text=annotation_data.original_text,
             corrected_text=annotation_data.corrected_text,
             status=annotation_data.status,
         )
-        await annotation.save()
-        logger.info(
-            f"Created memory annotation {annotation.id} for memory {annotation_data.memory_id}"
-        )

         # Update memory content if accepted
         if annotation.status == AnnotationStatus.ACCEPTED:
             try:
                 await memory_service.update_memory(
                     memory_id=annotation_data.memory_id,
                     content=annotation_data.corrected_text,
                     user_id=current_user.user_id,
                 )
                 logger.info(
                     f"Updated memory {annotation_data.memory_id} with corrected text"
                 )
             except Exception as e:
                 logger.error(f"Error updating memory: {e}")
-                # Annotation is saved, but memory update failed - log but don't fail the request
-                logger.warning(
-                    f"Memory annotation {annotation.id} saved but memory update failed"
-                )
+                # Memory update failed - save annotation as PENDING instead
+                annotation.status = AnnotationStatus.PENDING
+                logger.warning(
+                    f"Memory update failed, saving annotation as PENDING"
+                )
+
+        await annotation.save()
+        logger.info(
+            f"Created memory annotation {annotation.id} for memory {annotation_data.memory_id}"
+        )

         return AnnotationResponse.model_validate(annotation)
🧰 Tools
🪛 Ruff (0.14.11)

85-85: Do not catch blind exception: Exception

(BLE001)


86-86: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🤖 Prompt for AI Agents
In
`@backends/advanced/src/advanced_omi_backend/routers/modules/annotation_routes.py`
around lines 74 - 90, The current flow marks and saves the annotation as
ACCEPTED before calling memory_service.update_memory, leading to inconsistency
if update_memory fails; change the flow in the route handling code that checks
annotation.status == AnnotationStatus.ACCEPTED so that you first call await
memory_service.update_memory(...) using annotation_data.memory_id and
annotation_data.corrected_text, ensure it succeeds, and only then set
annotation.status = AnnotationStatus.ACCEPTED and persist/save the annotation
(or alternatively, if you must save earlier, implement a rollback that resets
annotation.status to PENDING and re-saves the annotation when update_memory
raises); update logging to record success/failure tied to annotation.id and
include the chosen rollback or save-after-update behavior consistently.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Comment on lines +263 to +306
# If accepting a pending suggestion, apply the correction
if (
status == AnnotationStatus.ACCEPTED
and old_status == AnnotationStatus.PENDING
):
if annotation.is_memory_annotation():
# Update memory
try:
memory_service = get_memory_service()
await memory_service.update_memory(
memory_id=annotation.memory_id,
content=annotation.corrected_text,
user_id=current_user.user_id,
)
logger.info(
f"Applied suggestion to memory {annotation.memory_id}"
)
except Exception as e:
logger.error(f"Error applying memory suggestion: {e}")
# Don't fail the status update if memory update fails
elif annotation.is_transcript_annotation():
# Update transcript segment
try:
conversation = await Conversation.find_one(
Conversation.conversation_id == annotation.conversation_id
)
if conversation:
transcript = conversation.get_active_transcript()
if (
transcript
and annotation.segment_index < len(transcript.segments)
):
transcript.segments[
annotation.segment_index
].text = annotation.corrected_text
await conversation.save()
logger.info(
f"Applied suggestion to transcript segment {annotation.segment_index}"
)
except Exception as e:
logger.error(f"Error applying transcript suggestion: {e}")
# Don't fail the status update if segment update fails

await annotation.save()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Annotation status saved as ACCEPTED even when correction fails.

If the memory update (lines 270-282) or transcript update (lines 285-304) fails, the exception is caught and logged, but annotation.save() at line 306 still persists the ACCEPTED status. This creates the same data inconsistency issue noted in create_memory_annotation - the user sees the annotation as accepted but the correction wasn't applied.

Consider only saving the ACCEPTED status if the correction was successfully applied, or tracking application status separately.

🧰 Tools
🪛 Ruff (0.14.11)

280-280: Do not catch blind exception: Exception

(BLE001)


281-281: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


302-302: Do not catch blind exception: Exception

(BLE001)


303-303: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Comment on lines +286 to +298
conversation = await Conversation.find_one(
Conversation.conversation_id == annotation.conversation_id
)
if conversation:
transcript = conversation.get_active_transcript()
if (
transcript
and annotation.segment_index < len(transcript.segments)
):
transcript.segments[
annotation.segment_index
].text = annotation.corrected_text
await conversation.save()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Defense-in-depth: Add user_id filter when fetching conversation.

The conversation lookup at line 287 doesn't include a user_id filter. While the annotation itself is already validated to belong to the current user (line 254), adding the user filter provides defense-in-depth against any future bugs that might allow cross-user annotation references.

🛡️ Add user_id filter
                 try:
                     conversation = await Conversation.find_one(
-                        Conversation.conversation_id == annotation.conversation_id
+                        Conversation.conversation_id == annotation.conversation_id,
+                        Conversation.user_id == current_user.user_id,
                     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
conversation = await Conversation.find_one(
Conversation.conversation_id == annotation.conversation_id
)
if conversation:
transcript = conversation.get_active_transcript()
if (
transcript
and annotation.segment_index < len(transcript.segments)
):
transcript.segments[
annotation.segment_index
].text = annotation.corrected_text
await conversation.save()
conversation = await Conversation.find_one(
Conversation.conversation_id == annotation.conversation_id,
Conversation.user_id == current_user.user_id,
)
if conversation:
transcript = conversation.get_active_transcript()
if (
transcript
and annotation.segment_index < len(transcript.segments)
):
transcript.segments[
annotation.segment_index
].text = annotation.corrected_text
await conversation.save()
🤖 Prompt for AI Agents
In
`@backends/advanced/src/advanced_omi_backend/routers/modules/annotation_routes.py`
around lines 286 - 298, The Conversation.find_one call that fetches by
Conversation.conversation_id should also filter by the owner to enforce
defense-in-depth: change the lookup in the annotation update path to include
Conversation.user_id == annotation.user_id (i.e., use
Conversation.find_one(Conversation.conversation_id ==
annotation.conversation_id, Conversation.user_id == annotation.user_id)); keep
the subsequent get_active_transcript, segment bounds check, text assignment to
transcript.segments[annotation.segment_index].text = annotation.corrected_text,
and await conversation.save() unchanged.

Comment on lines +254 to +255
# Backend API URL for fetching audio segments (used by speaker service)
backend_api_url: http://host.docker.internal:8000
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

host.docker.internal may not resolve on Linux without additional setup.

The backend_api_url defaults to http://host.docker.internal:8000, which works out-of-the-box on Docker Desktop (macOS/Windows) but requires explicit --add-host configuration on native Linux Docker. This could cause connectivity issues for Linux users.

Consider documenting this requirement or providing a fallback mechanism.

🤖 Prompt for AI Agents
In `@config/defaults.yml` around lines 254 - 255, The default backend_api_url
currently points to host.docker.internal which doesn't resolve on many native
Linux Docker setups; change the default value for backend_api_url to use
http://127.0.0.1:8000 (or localhost) and add a brief note in the config/comments
that Linux users who rely on host.docker.internal must add an explicit
--add-host or override the value via environment/config, referencing the
backend_api_url key so maintainers can locate and document/override it.

Comment on lines +306 to 309
log.info(f"Processing diarize-identify-match request")
log.info(f"Mode: {'conversation' if conversation_id else 'file upload'}")
log.info(f"Parameters - user_id: {user_id}, min_duration: {min_duration}, similarity_threshold: {similarity_threshold}, min_speakers: {min_speakers}, max_speakers: {max_speakers}, collar: {collar}, min_duration_off: {min_duration_off}")
log.info(f"Transcript data length: {len(transcript_data) if transcript_data else 0}")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unnecessary f-string prefix.

Line 306 has an f-string without any placeholders. This was flagged by static analysis.

🔧 Fix
-    log.info(f"Processing diarize-identify-match request")
+    log.info("Processing diarize-identify-match request")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
log.info(f"Processing diarize-identify-match request")
log.info(f"Mode: {'conversation' if conversation_id else 'file upload'}")
log.info(f"Parameters - user_id: {user_id}, min_duration: {min_duration}, similarity_threshold: {similarity_threshold}, min_speakers: {min_speakers}, max_speakers: {max_speakers}, collar: {collar}, min_duration_off: {min_duration_off}")
log.info(f"Transcript data length: {len(transcript_data) if transcript_data else 0}")
log.info("Processing diarize-identify-match request")
log.info(f"Mode: {'conversation' if conversation_id else 'file upload'}")
log.info(f"Parameters - user_id: {user_id}, min_duration: {min_duration}, similarity_threshold: {similarity_threshold}, min_speakers: {min_speakers}, max_speakers: {max_speakers}, collar: {collar}, min_duration_off: {min_duration_off}")
log.info(f"Transcript data length: {len(transcript_data) if transcript_data else 0}")
🧰 Tools
🪛 Ruff (0.14.11)

306-306: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for AI Agents
In
`@extras/speaker-recognition/src/simple_speaker_recognition/api/routers/identification.py`
around lines 306 - 309, The log call using log.info with the literal "Processing
diarize-identify-match request" incorrectly uses an f-string prefix; remove the
leading "f" on that specific log.info invocation so the string is a plain
literal while leaving the other log.info calls (which use
expressions/placeholders like the mode conditional and transcript length)
unchanged.

Comment on lines 319 to 325
# Parse transcript data
try:
transcript = json.loads(transcript_data)
words = transcript.get("words", [])
full_text = transcript.get("text", "")
except json.JSONDecodeError as e:
raise HTTPException(400, f"Invalid transcript_data JSON: {str(e)}")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add exception chaining and remove unused variable.

Two issues in this block:

  1. full_text is assigned but never used (F841)
  2. The HTTPException should use raise ... from e to preserve the exception chain (B904)
🔧 Fix
     # Parse transcript data
     try:
         transcript = json.loads(transcript_data)
         words = transcript.get("words", [])
-        full_text = transcript.get("text", "")
+        # Note: full_text = transcript.get("text", "") available if needed
     except json.JSONDecodeError as e:
-        raise HTTPException(400, f"Invalid transcript_data JSON: {str(e)}")
+        raise HTTPException(400, f"Invalid transcript_data JSON: {e!s}") from e
🧰 Tools
🪛 Ruff (0.14.11)

323-323: Local variable full_text is assigned to but never used

Remove assignment to unused variable full_text

(F841)


325-325: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


325-325: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 Prompt for AI Agents
In
`@extras/speaker-recognition/src/simple_speaker_recognition/api/routers/identification.py`
around lines 319 - 325, Remove the unused local full_text assignment and
preserve exception chaining: in the JSON parsing block that reads
transcript_data into transcript and words, drop the unused full_text =
transcript.get("text", "") line and change the except handler to raise
HTTPException(400, f"Invalid transcript_data JSON: {str(e)}") from e so the
JSONDecodeError is chained; keep the variable name e for the caught
JSONDecodeError to reference in the chained raise.

Comment on lines +336 to +366
# Mode 1: Conversation mode - fetch audio from backend
if conversation_id:
from simple_speaker_recognition.core.backend_client import BackendClient

backend_client = BackendClient(settings.backend_api_url)
try:
# Get conversation metadata
metadata = await backend_client.get_conversation_metadata(conversation_id, backend_token)
total_duration = metadata['duration']

log.info(f"Conversation {conversation_id[:12]}: duration={total_duration:.1f}s")

# Validate: 2 hour maximum
if total_duration > MAX_AUDIO_DURATION:
raise HTTPException(400, f"Audio duration {total_duration:.1f}s exceeds maximum allowed duration of {MAX_AUDIO_DURATION}s (2 hours)")

# Fetch full audio from backend
log.info(f"Fetching audio from backend for conversation {conversation_id[:12]}")
wav_bytes = await backend_client.get_audio_segment(
conversation_id,
backend_token,
start=0.0,
duration=total_duration
)

# Write to temp file
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
tmp_file.write(wav_bytes)
tmp_path = Path(tmp_file.name)
finally:
await backend_client.close()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential KeyError if metadata lacks 'duration' field.

If the backend returns metadata without a duration key (e.g., due to API version mismatch or corrupted data), line 344 will raise a KeyError. Consider using .get() with validation.

Also, the try/finally block correctly ensures backend_client.close() is called, but if an exception occurs during get_conversation_metadata or get_audio_segment, the error message may not be user-friendly.

🔧 Safer metadata access
         try:
             # Get conversation metadata
             metadata = await backend_client.get_conversation_metadata(conversation_id, backend_token)
-            total_duration = metadata['duration']
+            total_duration = metadata.get('duration')
+            if total_duration is None:
+                raise HTTPException(400, f"Conversation {conversation_id[:12]} has no duration metadata")

             log.info(f"Conversation {conversation_id[:12]}: duration={total_duration:.1f}s")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Mode 1: Conversation mode - fetch audio from backend
if conversation_id:
from simple_speaker_recognition.core.backend_client import BackendClient
backend_client = BackendClient(settings.backend_api_url)
try:
# Get conversation metadata
metadata = await backend_client.get_conversation_metadata(conversation_id, backend_token)
total_duration = metadata['duration']
log.info(f"Conversation {conversation_id[:12]}: duration={total_duration:.1f}s")
# Validate: 2 hour maximum
if total_duration > MAX_AUDIO_DURATION:
raise HTTPException(400, f"Audio duration {total_duration:.1f}s exceeds maximum allowed duration of {MAX_AUDIO_DURATION}s (2 hours)")
# Fetch full audio from backend
log.info(f"Fetching audio from backend for conversation {conversation_id[:12]}")
wav_bytes = await backend_client.get_audio_segment(
conversation_id,
backend_token,
start=0.0,
duration=total_duration
)
# Write to temp file
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
tmp_file.write(wav_bytes)
tmp_path = Path(tmp_file.name)
finally:
await backend_client.close()
# Mode 1: Conversation mode - fetch audio from backend
if conversation_id:
from simple_speaker_recognition.core.backend_client import BackendClient
backend_client = BackendClient(settings.backend_api_url)
try:
# Get conversation metadata
metadata = await backend_client.get_conversation_metadata(conversation_id, backend_token)
total_duration = metadata.get('duration')
if total_duration is None:
raise HTTPException(400, f"Conversation {conversation_id[:12]} has no duration metadata")
log.info(f"Conversation {conversation_id[:12]}: duration={total_duration:.1f}s")
# Validate: 2 hour maximum
if total_duration > MAX_AUDIO_DURATION:
raise HTTPException(400, f"Audio duration {total_duration:.1f}s exceeds maximum allowed duration of {MAX_AUDIO_DURATION}s (2 hours)")
# Fetch full audio from backend
log.info(f"Fetching audio from backend for conversation {conversation_id[:12]}")
wav_bytes = await backend_client.get_audio_segment(
conversation_id,
backend_token,
start=0.0,
duration=total_duration
)
# Write to temp file
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
tmp_file.write(wav_bytes)
tmp_path = Path(tmp_file.name)
finally:
await backend_client.close()
🤖 Prompt for AI Agents
In
`@extras/speaker-recognition/src/simple_speaker_recognition/api/routers/identification.py`
around lines 336 - 366, The code assumes metadata['duration'] exists and will
KeyError if missing; update the conversation branch in identification.py to use
metadata.get('duration') and validate it is a number (e.g., not None and >0),
and if invalid raise an HTTPException(400) with a clear message about
missing/invalid duration; also wrap the calls to
BackendClient.get_conversation_metadata and get_audio_segment in a try/except
that converts backend errors into user-friendly HTTPException messages (while
leaving the finally await backend_client.close() intact) so failures return
meaningful HTTP errors instead of raw exceptions.

Comment on lines +368 to +386
# Mode 2: File upload mode
else:
# Create temporary file from upload
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
tmp_file.write(await file.read())
tmp_path = Path(tmp_file.name)

# Get audio duration for validation
from simple_speaker_recognition.utils.audio_processing import get_audio_info
audio_info = get_audio_info(str(tmp_path))
total_duration = audio_info.get('duration_seconds', 0)

log.info(f"Uploaded file: {file.filename}, duration={total_duration:.1f}s")

# Validate: 2 hour maximum
if total_duration > MAX_AUDIO_DURATION:
tmp_path.unlink(missing_ok=True)
raise HTTPException(400, f"Audio duration {total_duration:.1f}s exceeds maximum allowed duration of {MAX_AUDIO_DURATION}s (2 hours)")

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Resource leak: temp file not cleaned up if validation fails.

In file upload mode, if get_audio_info() fails or the duration exceeds the limit, the temporary file at tmp_path may not be cleaned up. Line 384 handles the duration exceeded case, but any exception from get_audio_info() would leave the file behind.

🔧 Ensure cleanup on all error paths
     # Mode 2: File upload mode
     else:
         # Create temporary file from upload
         with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
             tmp_file.write(await file.read())
             tmp_path = Path(tmp_file.name)

-        # Get audio duration for validation
-        from simple_speaker_recognition.utils.audio_processing import get_audio_info
-        audio_info = get_audio_info(str(tmp_path))
-        total_duration = audio_info.get('duration_seconds', 0)
-
-        log.info(f"Uploaded file: {file.filename}, duration={total_duration:.1f}s")
-
-        # Validate: 2 hour maximum
-        if total_duration > MAX_AUDIO_DURATION:
-            tmp_path.unlink(missing_ok=True)
-            raise HTTPException(400, f"Audio duration {total_duration:.1f}s exceeds maximum allowed duration of {MAX_AUDIO_DURATION}s (2 hours)")
+        try:
+            # Get audio duration for validation
+            from simple_speaker_recognition.utils.audio_processing import get_audio_info
+            audio_info = get_audio_info(str(tmp_path))
+            total_duration = audio_info.get('duration_seconds', 0)
+
+            log.info(f"Uploaded file: {file.filename}, duration={total_duration:.1f}s")
+
+            # Validate: 2 hour maximum
+            if total_duration > MAX_AUDIO_DURATION:
+                raise HTTPException(400, f"Audio duration {total_duration:.1f}s exceeds maximum allowed duration of {MAX_AUDIO_DURATION}s (2 hours)")
+        except HTTPException:
+            tmp_path.unlink(missing_ok=True)
+            raise
+        except Exception as e:
+            tmp_path.unlink(missing_ok=True)
+            raise HTTPException(400, f"Failed to read audio file: {e!s}") from e
🤖 Prompt for AI Agents
In
`@extras/speaker-recognition/src/simple_speaker_recognition/api/routers/identification.py`
around lines 368 - 386, The temporary file created with
tempfile.NamedTemporaryFile (tmp_path) can leak if get_audio_info() raises or
any other exception occurs; wrap the file creation and subsequent processing
(call to get_audio_info and duration validation against MAX_AUDIO_DURATION) in a
try/finally (or try/except where finally unlinks tmp_path) so that
tmp_path.unlink(missing_ok=True) is always called on error/exit, or perform
get_audio_info and validation inside the with-block before breaking the context,
ensuring tmp_path is deleted in the finally block when an exception is raised.

… loader

- Added custom OmegaConf resolvers to handle legacy ${VAR:-default} syntax for backward compatibility.
- Introduced a preprocessing function to convert legacy syntax in YAML files to OmegaConf-compatible format.
- Updated the load_config function to utilize the new preprocessing for loading defaults and user configurations.
- Enhanced documentation for clarity on the new legacy syntax handling.
- Introduced a new function `get_plugins_yml_path` to centralize the retrieval of the plugins.yml file path.
- Updated `system_controller.py` and `plugin_service.py` to use the new function for improved maintainability and consistency in accessing the plugins configuration.
- Enhanced code clarity by removing hardcoded paths and utilizing the centralized configuration method.
Plugin terminology: subscriptions→events, trigger→condition
Memory jobs: no longer blocked by disabled speaker recognition
Copy link
Contributor

@0xrushi 0xrushi left a comment

Choose a reason for hiding this comment

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

Can we also have some tests around the new annotation routes, specifically around the CRUD operation making sure annotation is rolled back to PENDING etc


# Test OmegaConf configuration loading
try:
from advanced_omi_backend.config_loader import load_config
Copy link
Contributor

Choose a reason for hiding this comment

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

move this to the top


# Test model registry
try:
from advanced_omi_backend.model_registry import get_models_registry
Copy link
Contributor

Choose a reason for hiding this comment

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

move to top

Comment on lines +74 to +90
# Update memory content if accepted
if annotation.status == AnnotationStatus.ACCEPTED:
try:
await memory_service.update_memory(
memory_id=annotation_data.memory_id,
content=annotation_data.corrected_text,
user_id=current_user.user_id,
)
logger.info(
f"Updated memory {annotation_data.memory_id} with corrected text"
)
except Exception as e:
logger.error(f"Error updating memory: {e}")
# Annotation is saved, but memory update failed - log but don't fail the request
logger.warning(
f"Memory annotation {annotation.id} saved but memory update failed"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Comment on lines +263 to +306
# If accepting a pending suggestion, apply the correction
if (
status == AnnotationStatus.ACCEPTED
and old_status == AnnotationStatus.PENDING
):
if annotation.is_memory_annotation():
# Update memory
try:
memory_service = get_memory_service()
await memory_service.update_memory(
memory_id=annotation.memory_id,
content=annotation.corrected_text,
user_id=current_user.user_id,
)
logger.info(
f"Applied suggestion to memory {annotation.memory_id}"
)
except Exception as e:
logger.error(f"Error applying memory suggestion: {e}")
# Don't fail the status update if memory update fails
elif annotation.is_transcript_annotation():
# Update transcript segment
try:
conversation = await Conversation.find_one(
Conversation.conversation_id == annotation.conversation_id
)
if conversation:
transcript = conversation.get_active_transcript()
if (
transcript
and annotation.segment_index < len(transcript.segments)
):
transcript.segments[
annotation.segment_index
].text = annotation.corrected_text
await conversation.save()
logger.info(
f"Applied suggestion to transcript segment {annotation.segment_index}"
)
except Exception as e:
logger.error(f"Error applying transcript suggestion: {e}")
# Don't fail the status update if segment update fails

await annotation.save()
Copy link
Contributor

Choose a reason for hiding this comment

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

+1



# Get settings for chunking configuration
from simple_speaker_recognition.api.service import auth as settings
Copy link
Contributor

Choose a reason for hiding this comment

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

move to top


# Mode 1: Conversation mode - fetch audio from backend
if conversation_id:
from simple_speaker_recognition.core.backend_client import BackendClient
Copy link
Contributor

Choose a reason for hiding this comment

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

move to top

- Updated Docker Compose files to mount the entire config directory, consolidating configuration management.
- Refactored the `save_diarization_settings` function to improve clarity and maintainability by renaming it to `save_diarization_settings_controller`.
- Enhanced the System component in the web UI to include configuration diagnostics, providing better visibility into system health and issues.
- Updated the testing documentation to reflect a new Makefile-based approach for running tests and managing containers.
- Introduced new scripts for container management, including starting, stopping, restarting, and cleaning containers while preserving logs.
- Added a cleanup script to handle data ownership and permissions correctly.
- Implemented a logging system that saves container logs automatically before cleanup.
- Enhanced the README with detailed instructions for running tests and managing the test environment.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants