Skip to content

Commit 246893b

Browse files
committed
Refactor audio stream workers and enhance configuration management
- Introduced a base class `BaseStreamWorker` to standardize audio stream worker implementations, improving code reuse and maintainability. - Updated `DeepgramStreamWorker` and `ParakeetStreamWorker` to inherit from `BaseStreamWorker`, streamlining their initialization and configuration validation processes. - Enhanced error handling and logging for Redis connections and consumer initialization in the worker classes. - Refactored memory processing logic to improve conversation text extraction and speaker identification. - Added unit tests for audio stream workers to ensure reliability and correct functionality across various scenarios.
1 parent 880d9c5 commit 246893b

9 files changed

Lines changed: 1472 additions & 1160 deletions

File tree

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,6 @@ async def create_annotation(
7373

7474
# Trigger memory reprocessing
7575
enqueue_memory_processing(
76-
client_id=conversation.client_id,
77-
user_id=str(current_user.id),
78-
user_email=current_user.email,
7976
conversation_id=conversation.conversation_id,
8077
priority=JobPriority.NORMAL
8178
)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Deepgram audio stream worker.
4+
5+
Starts a consumer that reads from audio:stream:deepgram and transcribes audio.
6+
"""
7+
8+
import os
9+
10+
from advanced_omi_backend.services.transcription.deepgram import DeepgramStreamConsumer
11+
from advanced_omi_backend.workers.base_audio_worker import BaseStreamWorker
12+
13+
14+
class DeepgramStreamWorker(BaseStreamWorker):
15+
"""Deepgram audio stream worker implementation."""
16+
17+
def __init__(self):
18+
super().__init__(service_name="Deepgram audio stream worker")
19+
20+
def validate_config(self):
21+
"""Check that config.yml has Deepgram configured."""
22+
# The registry provider will load configuration from config.yml
23+
api_key = os.getenv("DEEPGRAM_API_KEY")
24+
if not api_key:
25+
self.logger.warning("DEEPGRAM_API_KEY environment variable not set")
26+
self.logger.warning("Ensure config.yml has a default 'stt' model configured for Deepgram")
27+
self.logger.warning("Audio transcription will use alternative providers if configured in config.yml")
28+
29+
def get_consumer(self, redis_client):
30+
"""Create Deepgram consumer with balanced buffer size."""
31+
# Create consumer with balanced buffer size
32+
# 20 chunks = ~5 seconds of audio
33+
# Balance between transcription accuracy and latency
34+
# Consumer uses registry-driven provider from config.yml
35+
return DeepgramStreamConsumer(
36+
redis_client=redis_client,
37+
buffer_chunks=20 # 5 seconds - good context without excessive delay
38+
)
39+
40+
41+
if __name__ == "__main__":
42+
DeepgramStreamWorker.start()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Parakeet audio stream worker.
4+
5+
Starts a consumer that reads from audio:stream:* and transcribes audio using Parakeet.
6+
"""
7+
8+
import os
9+
10+
from advanced_omi_backend.services.transcription.parakeet_stream_consumer import ParakeetStreamConsumer
11+
from advanced_omi_backend.workers.base_audio_worker import BaseStreamWorker
12+
13+
14+
class ParakeetStreamWorker(BaseStreamWorker):
15+
"""Parakeet audio stream worker implementation."""
16+
17+
def __init__(self):
18+
super().__init__(service_name="Parakeet audio stream worker")
19+
20+
def validate_config(self):
21+
"""Check that config.yml has Parakeet configured."""
22+
# The registry provider will load configuration from config.yml
23+
service_url = os.getenv("PARAKEET_ASR_URL")
24+
if not service_url:
25+
self.logger.warning("PARAKEET_ASR_URL environment variable not set")
26+
self.logger.warning("Ensure config.yml has a default 'stt' model configured for Parakeet")
27+
self.logger.warning("Audio transcription will use alternative providers if configured in config.yml")
28+
29+
def get_consumer(self, redis_client):
30+
"""Create Parakeet consumer with balanced buffer size."""
31+
# Create consumer with balanced buffer size
32+
# 20 chunks = ~5 seconds of audio
33+
# Balance between transcription accuracy and latency
34+
# Consumer uses registry-driven provider from config.yml
35+
return ParakeetStreamConsumer(
36+
redis_client=redis_client,
37+
buffer_chunks=20 # 5 seconds - good context without excessive delay
38+
)
39+
40+
41+
if __name__ == "__main__":
42+
ParakeetStreamWorker.start()
43+
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"""
2+
Base audio stream worker.
3+
4+
Provides a template for stream workers with consistent Redis connection,
5+
signal handling, and error management.
6+
"""
7+
8+
import asyncio
9+
import logging
10+
import os
11+
import signal
12+
import sys
13+
from abc import ABC, abstractmethod
14+
15+
import redis.asyncio as redis
16+
17+
# Configure basic logging if not already configured
18+
if not logging.getLogger().handlers:
19+
logging.basicConfig(
20+
level=logging.INFO,
21+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
22+
)
23+
24+
class BaseStreamWorker(ABC):
25+
"""
26+
Base class for audio stream workers using the Template Method pattern.
27+
28+
Subclasses must implement:
29+
- validate_config(): Check environment/config requirements
30+
- get_consumer(redis_client): Return the specific consumer instance
31+
"""
32+
33+
def __init__(self, service_name: str):
34+
self.service_name = service_name
35+
self.logger = logging.getLogger(self.__class__.__name__)
36+
self.redis_client = None
37+
self.consumer = None
38+
39+
@abstractmethod
40+
def validate_config(self):
41+
"""
42+
Check required environment variables or configuration.
43+
Should log warnings/errors if configuration is missing.
44+
"""
45+
pass
46+
47+
@abstractmethod
48+
def get_consumer(self, redis_client):
49+
"""
50+
Create and return the consumer instance.
51+
52+
Args:
53+
redis_client: Initialized Redis client
54+
55+
Returns:
56+
An instance complying with the BaseAudioStreamConsumer interface
57+
"""
58+
pass
59+
60+
async def run(self):
61+
"""Main execution loop."""
62+
self.logger.info(f"🚀 Starting {self.service_name}")
63+
64+
self.validate_config()
65+
66+
redis_url = os.getenv("REDIS_URL", "redis://localhost:6379/0")
67+
68+
try:
69+
self.redis_client = await redis.from_url(
70+
redis_url,
71+
encoding="utf-8",
72+
decode_responses=False
73+
)
74+
self.logger.info("Connected to Redis")
75+
except Exception as e:
76+
self.logger.error(f"Failed to connect to Redis: {e}")
77+
sys.exit(1)
78+
79+
try:
80+
self.consumer = self.get_consumer(self.redis_client)
81+
except Exception as e:
82+
self.logger.error(f"Failed to initialize consumer: {e}")
83+
await self.redis_client.aclose()
84+
sys.exit(1)
85+
86+
# Setup graceful shutdown
87+
loop = asyncio.get_running_loop()
88+
stop_event = asyncio.Event()
89+
90+
def signal_handler():
91+
self.logger.info("Received stop signal, shutting down...")
92+
stop_event.set()
93+
94+
# Register signal handlers
95+
for sig in (signal.SIGINT, signal.SIGTERM):
96+
try:
97+
loop.add_signal_handler(sig, signal_handler)
98+
except NotImplementedError:
99+
# Fallback for environments where add_signal_handler is not supported
100+
# (e.g. some Windows environments or custom loops)
101+
self.logger.warning(f"Could not add signal handler for {sig}")
102+
103+
try:
104+
self.logger.info(f"✅ {self.service_name} ready")
105+
106+
# Run consumer as a task
107+
consumer_task = asyncio.create_task(self.consumer.start_consuming())
108+
stop_wait_task = asyncio.create_task(stop_event.wait())
109+
110+
# Wait for either the consumer to finish (error/done) or stop signal
111+
done, pending = await asyncio.wait(
112+
[consumer_task, stop_wait_task],
113+
return_when=asyncio.FIRST_COMPLETED
114+
)
115+
116+
# Check if consumer failed
117+
if consumer_task in done:
118+
try:
119+
await consumer_task
120+
except asyncio.CancelledError:
121+
pass
122+
except Exception as e:
123+
self.logger.error(f"Consumer task failed: {e}", exc_info=True)
124+
# We continue to cleanup
125+
126+
# Trigger stop on consumer
127+
self.logger.info("Stopping consumer...")
128+
await self.consumer.stop()
129+
130+
# Ensure consumer task finishes if it was running
131+
if consumer_task in pending:
132+
try:
133+
await consumer_task
134+
except asyncio.CancelledError:
135+
pass
136+
except Exception as e:
137+
# Ignore expected errors during shutdown if any
138+
self.logger.debug(f"Consumer shutdown exception: {e}")
139+
140+
except Exception as e:
141+
self.logger.error(f"Worker runtime error: {e}", exc_info=True)
142+
sys.exit(1)
143+
finally:
144+
if self.redis_client:
145+
await self.redis_client.aclose()
146+
self.logger.info(f"👋 {self.service_name} stopped")
147+
148+
@classmethod
149+
def start(cls):
150+
"""Entry point for script execution."""
151+
instance = cls()
152+
try:
153+
asyncio.run(instance.run())
154+
except KeyboardInterrupt:
155+
# Handle keyboard interrupt outside the loop if it propagates
156+
pass

0 commit comments

Comments
 (0)