|
12 | 12 | # See the License for the specific language governing permissions and |
13 | 13 | # limitations under the License. |
14 | 14 |
|
15 | | -from __future__ import annotations |
| 15 | +"""Backward compatibility module for AudioCacheManager. |
16 | 16 |
|
17 | | -import logging |
18 | | -from typing import TYPE_CHECKING |
| 17 | +AudioCacheManager and AudioCacheConfig have been moved to |
| 18 | +``google.adk.live.audio_cache_manager``. |
| 19 | +""" |
19 | 20 |
|
20 | | -from google.adk.platform import time as platform_time |
21 | | -from google.genai import types |
| 21 | +from __future__ import annotations |
22 | 22 |
|
23 | | -from ...agents.invocation_context import RealtimeCacheEntry |
24 | | -from ...events.event import Event |
25 | | -from ._invocation_utils import require_agent_name |
| 23 | +import logging |
26 | 24 |
|
27 | | -if TYPE_CHECKING: |
28 | | - from ...agents.invocation_context import InvocationContext |
| 25 | +from ...live._audio_cache_manager import AudioCacheConfig as AudioCacheConfig |
| 26 | +from ...live._audio_cache_manager import AudioCacheManager as AudioCacheManager |
| 27 | +from ...live._audio_cache_manager import RealtimeCacheEntry as RealtimeCacheEntry |
29 | 28 |
|
30 | 29 | logger = logging.getLogger('google_adk.' + __name__) |
31 | | - |
32 | | - |
33 | | -def _require_audio_data(blob: types.Blob) -> bytes: |
34 | | - data = blob.data |
35 | | - if not isinstance(data, bytes): |
36 | | - raise ValueError('Audio blobs must contain byte data.') |
37 | | - return data |
38 | | - |
39 | | - |
40 | | -class AudioCacheManager: |
41 | | - """Manages audio caching and flushing for live streaming flows.""" |
42 | | - |
43 | | - def __init__(self, config: AudioCacheConfig | None = None) -> None: |
44 | | - """Initialize the audio cache manager. |
45 | | -
|
46 | | - Args: |
47 | | - config: Configuration for audio caching behavior. |
48 | | - """ |
49 | | - self.config = config or AudioCacheConfig() |
50 | | - |
51 | | - def cache_audio( |
52 | | - self, |
53 | | - invocation_context: InvocationContext, |
54 | | - audio_blob: types.Blob, |
55 | | - cache_type: str, |
56 | | - ) -> None: |
57 | | - """Cache incoming user or outgoing model audio data. |
58 | | -
|
59 | | - Args: |
60 | | - invocation_context: The current invocation context. |
61 | | - audio_blob: The audio data to cache. |
62 | | - cache_type: Type of audio to cache, either 'input' or 'output'. |
63 | | -
|
64 | | - Raises: |
65 | | - ValueError: If cache_type is not 'input' or 'output'. |
66 | | - """ |
67 | | - audio_data = _require_audio_data(audio_blob) |
68 | | - if cache_type == 'input': |
69 | | - if not invocation_context.input_realtime_cache: |
70 | | - invocation_context.input_realtime_cache = [] |
71 | | - cache = invocation_context.input_realtime_cache |
72 | | - role = 'user' |
73 | | - elif cache_type == 'output': |
74 | | - if not invocation_context.output_realtime_cache: |
75 | | - invocation_context.output_realtime_cache = [] |
76 | | - cache = invocation_context.output_realtime_cache |
77 | | - role = 'model' |
78 | | - else: |
79 | | - raise ValueError("cache_type must be either 'input' or 'output'") |
80 | | - |
81 | | - audio_entry = RealtimeCacheEntry( |
82 | | - role=role, data=audio_blob, timestamp=platform_time.get_time() |
83 | | - ) |
84 | | - cache.append(audio_entry) |
85 | | - |
86 | | - logger.debug( |
87 | | - 'Cached %s audio chunk: %d bytes, cache size: %d', |
88 | | - cache_type, |
89 | | - len(audio_data), |
90 | | - len(cache), |
91 | | - ) |
92 | | - |
93 | | - async def flush_caches( |
94 | | - self, |
95 | | - invocation_context: InvocationContext, |
96 | | - flush_user_audio: bool = True, |
97 | | - flush_model_audio: bool = True, |
98 | | - ) -> list[Event]: |
99 | | - """Flush audio caches to artifact services. |
100 | | -
|
101 | | - The multimodality data is saved in artifact service in the format of |
102 | | - audio file. The file data reference is added to the session as an event. |
103 | | - The audio file follows the naming convention: artifact_ref = |
104 | | - f"artifact://{invocation_context.app_name}/{invocation_context.user_id}/ |
105 | | - {invocation_context.session.id}/_adk_live/{filename}#{revision_id}" |
106 | | -
|
107 | | - Note: video data is not supported yet. |
108 | | -
|
109 | | - Args: |
110 | | - invocation_context: The invocation context containing audio caches. |
111 | | - flush_user_audio: Whether to flush the input (user) audio cache. |
112 | | - flush_model_audio: Whether to flush the output (model) audio cache. |
113 | | -
|
114 | | - Returns: |
115 | | - A list of Event objects created from the flushed caches. |
116 | | - """ |
117 | | - flushed_events: list[Event] = [] |
118 | | - if flush_user_audio and invocation_context.input_realtime_cache: |
119 | | - audio_event = await self._flush_cache_to_services( |
120 | | - invocation_context, |
121 | | - invocation_context.input_realtime_cache, |
122 | | - 'input_audio', |
123 | | - ) |
124 | | - if audio_event: |
125 | | - flushed_events.append(audio_event) |
126 | | - invocation_context.input_realtime_cache = [] |
127 | | - |
128 | | - if flush_model_audio and invocation_context.output_realtime_cache: |
129 | | - logger.debug('Flushed output audio cache') |
130 | | - audio_event = await self._flush_cache_to_services( |
131 | | - invocation_context, |
132 | | - invocation_context.output_realtime_cache, |
133 | | - 'output_audio', |
134 | | - ) |
135 | | - if audio_event: |
136 | | - flushed_events.append(audio_event) |
137 | | - invocation_context.output_realtime_cache = [] |
138 | | - |
139 | | - return flushed_events |
140 | | - |
141 | | - async def _flush_cache_to_services( |
142 | | - self, |
143 | | - invocation_context: InvocationContext, |
144 | | - audio_cache: list[RealtimeCacheEntry], |
145 | | - cache_type: str, |
146 | | - ) -> Event | None: |
147 | | - """Flush a list of audio cache entries to artifact services. |
148 | | -
|
149 | | - The artifact service stores the actual blob. The session stores the |
150 | | - reference to the stored blob. |
151 | | -
|
152 | | - Args: |
153 | | - invocation_context: The invocation context. |
154 | | - audio_cache: The audio cache to flush. |
155 | | - cache_type: Type identifier for the cache ('input_audio' or 'output_audio'). |
156 | | -
|
157 | | - Returns: |
158 | | - The created Event if the cache was successfully flushed, None otherwise. |
159 | | - """ |
160 | | - if not invocation_context.artifact_service or not audio_cache: |
161 | | - logger.debug('Skipping cache flush: no artifact service or empty cache') |
162 | | - return None |
163 | | - |
164 | | - try: |
165 | | - # Combine audio chunks into a single file. Use join rather than repeated |
166 | | - # `+=`, which is O(n^2) over the total audio size. |
167 | | - mime_type = audio_cache[0].data.mime_type or 'audio/pcm' |
168 | | - combined_audio_data = b''.join( |
169 | | - entry.data.data or b'' for entry in audio_cache |
170 | | - ) |
171 | | - |
172 | | - # Generate filename with timestamp from first audio chunk (when recording started) |
173 | | - timestamp = int(audio_cache[0].timestamp * 1000) # milliseconds |
174 | | - filename = f"adk_live_audio_storage_{cache_type}_{timestamp}.{mime_type.split('/')[-1]}" |
175 | | - |
176 | | - # Save to artifact service |
177 | | - combined_audio_part = types.Part( |
178 | | - inline_data=types.Blob(data=combined_audio_data, mime_type=mime_type) |
179 | | - ) |
180 | | - |
181 | | - revision_id = await invocation_context.artifact_service.save_artifact( |
182 | | - app_name=invocation_context.app_name, |
183 | | - user_id=invocation_context.user_id, |
184 | | - session_id=invocation_context.session.id, |
185 | | - filename=filename, |
186 | | - artifact=combined_audio_part, |
187 | | - ) |
188 | | - |
189 | | - # Create artifact reference for session service |
190 | | - artifact_ref = f'artifact://{invocation_context.app_name}/{invocation_context.user_id}/{invocation_context.session.id}/_adk_live/{filename}#{revision_id}' |
191 | | - |
192 | | - # Create event with file data reference to add to session |
193 | | - # For model events, author should be the agent name, not the role |
194 | | - author = ( |
195 | | - require_agent_name(invocation_context) |
196 | | - if audio_cache[0].role == 'model' |
197 | | - else audio_cache[0].role |
198 | | - ) |
199 | | - audio_event = Event( |
200 | | - id=Event.new_id(), |
201 | | - invocation_id=invocation_context.invocation_id, |
202 | | - author=author, |
203 | | - content=types.Content( |
204 | | - role=audio_cache[0].role, |
205 | | - parts=[ |
206 | | - types.Part( |
207 | | - file_data=types.FileData( |
208 | | - file_uri=artifact_ref, mime_type=mime_type |
209 | | - ) |
210 | | - ) |
211 | | - ], |
212 | | - ), |
213 | | - timestamp=audio_cache[0].timestamp, |
214 | | - ) |
215 | | - |
216 | | - logger.debug( |
217 | | - 'Successfully flushed %s cache: %d chunks, %d bytes, saved as %s', |
218 | | - cache_type, |
219 | | - len(audio_cache), |
220 | | - len(combined_audio_data), |
221 | | - filename, |
222 | | - ) |
223 | | - return audio_event |
224 | | - |
225 | | - except Exception as e: |
226 | | - logger.error('Failed to flush %s cache: %s', cache_type, e) |
227 | | - return None |
228 | | - |
229 | | - def get_cache_stats( |
230 | | - self, invocation_context: InvocationContext |
231 | | - ) -> dict[str, int]: |
232 | | - """Get statistics about current cache state. |
233 | | -
|
234 | | - Args: |
235 | | - invocation_context: The invocation context. |
236 | | -
|
237 | | - Returns: |
238 | | - Dictionary containing cache statistics. |
239 | | - """ |
240 | | - input_count = len(invocation_context.input_realtime_cache or []) |
241 | | - output_count = len(invocation_context.output_realtime_cache or []) |
242 | | - |
243 | | - input_bytes = sum( |
244 | | - len(_require_audio_data(entry.data)) |
245 | | - for entry in invocation_context.input_realtime_cache or [] |
246 | | - ) |
247 | | - output_bytes = sum( |
248 | | - len(_require_audio_data(entry.data)) |
249 | | - for entry in invocation_context.output_realtime_cache or [] |
250 | | - ) |
251 | | - |
252 | | - return { |
253 | | - 'input_chunks': input_count, |
254 | | - 'output_chunks': output_count, |
255 | | - 'input_bytes': input_bytes, |
256 | | - 'output_bytes': output_bytes, |
257 | | - 'total_chunks': input_count + output_count, |
258 | | - 'total_bytes': input_bytes + output_bytes, |
259 | | - } |
260 | | - |
261 | | - |
262 | | -class AudioCacheConfig: |
263 | | - """Configuration for audio caching behavior.""" |
264 | | - |
265 | | - def __init__( |
266 | | - self, |
267 | | - max_cache_size_bytes: int = 10 * 1024 * 1024, # 10MB |
268 | | - max_cache_duration_seconds: float = 300.0, # 5 minutes |
269 | | - auto_flush_threshold: int = 100, # Number of chunks |
270 | | - ) -> None: |
271 | | - """Initialize audio cache configuration. |
272 | | -
|
273 | | - Args: |
274 | | - max_cache_size_bytes: Maximum cache size in bytes before auto-flush. |
275 | | - max_cache_duration_seconds: Maximum duration to keep data in cache. |
276 | | - auto_flush_threshold: Number of chunks that triggers auto-flush. |
277 | | - """ |
278 | | - self.max_cache_size_bytes = max_cache_size_bytes |
279 | | - self.max_cache_duration_seconds = max_cache_duration_seconds |
280 | | - self.auto_flush_threshold = auto_flush_threshold |
0 commit comments