audex 1.0.7a3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. audex/__init__.py +9 -0
  2. audex/__main__.py +7 -0
  3. audex/cli/__init__.py +189 -0
  4. audex/cli/apis/__init__.py +12 -0
  5. audex/cli/apis/init/__init__.py +34 -0
  6. audex/cli/apis/init/gencfg.py +130 -0
  7. audex/cli/apis/init/setup.py +330 -0
  8. audex/cli/apis/init/vprgroup.py +125 -0
  9. audex/cli/apis/serve.py +141 -0
  10. audex/cli/args.py +356 -0
  11. audex/cli/exceptions.py +44 -0
  12. audex/cli/helper/__init__.py +0 -0
  13. audex/cli/helper/ansi.py +193 -0
  14. audex/cli/helper/display.py +288 -0
  15. audex/config/__init__.py +64 -0
  16. audex/config/core/__init__.py +30 -0
  17. audex/config/core/app.py +29 -0
  18. audex/config/core/audio.py +45 -0
  19. audex/config/core/logging.py +163 -0
  20. audex/config/core/session.py +11 -0
  21. audex/config/helper/__init__.py +1 -0
  22. audex/config/helper/client/__init__.py +1 -0
  23. audex/config/helper/client/http.py +28 -0
  24. audex/config/helper/client/websocket.py +21 -0
  25. audex/config/helper/provider/__init__.py +1 -0
  26. audex/config/helper/provider/dashscope.py +13 -0
  27. audex/config/helper/provider/unisound.py +18 -0
  28. audex/config/helper/provider/xfyun.py +23 -0
  29. audex/config/infrastructure/__init__.py +31 -0
  30. audex/config/infrastructure/cache.py +51 -0
  31. audex/config/infrastructure/database.py +48 -0
  32. audex/config/infrastructure/recorder.py +32 -0
  33. audex/config/infrastructure/store.py +19 -0
  34. audex/config/provider/__init__.py +18 -0
  35. audex/config/provider/transcription.py +109 -0
  36. audex/config/provider/vpr.py +99 -0
  37. audex/container.py +40 -0
  38. audex/entity/__init__.py +468 -0
  39. audex/entity/doctor.py +109 -0
  40. audex/entity/doctor.pyi +51 -0
  41. audex/entity/fields.py +401 -0
  42. audex/entity/segment.py +115 -0
  43. audex/entity/segment.pyi +38 -0
  44. audex/entity/session.py +133 -0
  45. audex/entity/session.pyi +47 -0
  46. audex/entity/utterance.py +142 -0
  47. audex/entity/utterance.pyi +48 -0
  48. audex/entity/vp.py +68 -0
  49. audex/entity/vp.pyi +35 -0
  50. audex/exceptions.py +157 -0
  51. audex/filters/__init__.py +692 -0
  52. audex/filters/generated/__init__.py +21 -0
  53. audex/filters/generated/doctor.py +987 -0
  54. audex/filters/generated/segment.py +723 -0
  55. audex/filters/generated/session.py +978 -0
  56. audex/filters/generated/utterance.py +939 -0
  57. audex/filters/generated/vp.py +815 -0
  58. audex/helper/__init__.py +1 -0
  59. audex/helper/hash.py +33 -0
  60. audex/helper/mixin.py +65 -0
  61. audex/helper/net.py +19 -0
  62. audex/helper/settings/__init__.py +830 -0
  63. audex/helper/settings/fields.py +317 -0
  64. audex/helper/stream.py +153 -0
  65. audex/injectors/__init__.py +1 -0
  66. audex/injectors/config.py +12 -0
  67. audex/injectors/lifespan.py +7 -0
  68. audex/lib/__init__.py +1 -0
  69. audex/lib/cache/__init__.py +383 -0
  70. audex/lib/cache/inmemory.py +513 -0
  71. audex/lib/database/__init__.py +83 -0
  72. audex/lib/database/sqlite.py +406 -0
  73. audex/lib/exporter.py +189 -0
  74. audex/lib/injectors/__init__.py +1 -0
  75. audex/lib/injectors/cache.py +25 -0
  76. audex/lib/injectors/container.py +47 -0
  77. audex/lib/injectors/exporter.py +26 -0
  78. audex/lib/injectors/recorder.py +33 -0
  79. audex/lib/injectors/server.py +17 -0
  80. audex/lib/injectors/session.py +18 -0
  81. audex/lib/injectors/sqlite.py +24 -0
  82. audex/lib/injectors/store.py +13 -0
  83. audex/lib/injectors/transcription.py +42 -0
  84. audex/lib/injectors/usb.py +12 -0
  85. audex/lib/injectors/vpr.py +65 -0
  86. audex/lib/injectors/wifi.py +7 -0
  87. audex/lib/recorder.py +844 -0
  88. audex/lib/repos/__init__.py +149 -0
  89. audex/lib/repos/container.py +23 -0
  90. audex/lib/repos/database/__init__.py +1 -0
  91. audex/lib/repos/database/sqlite.py +672 -0
  92. audex/lib/repos/decorators.py +74 -0
  93. audex/lib/repos/doctor.py +286 -0
  94. audex/lib/repos/segment.py +302 -0
  95. audex/lib/repos/session.py +285 -0
  96. audex/lib/repos/tables/__init__.py +70 -0
  97. audex/lib/repos/tables/doctor.py +137 -0
  98. audex/lib/repos/tables/segment.py +113 -0
  99. audex/lib/repos/tables/session.py +140 -0
  100. audex/lib/repos/tables/utterance.py +131 -0
  101. audex/lib/repos/tables/vp.py +102 -0
  102. audex/lib/repos/utterance.py +288 -0
  103. audex/lib/repos/vp.py +286 -0
  104. audex/lib/restful.py +251 -0
  105. audex/lib/server/__init__.py +97 -0
  106. audex/lib/server/auth.py +98 -0
  107. audex/lib/server/handlers.py +248 -0
  108. audex/lib/server/templates/index.html.j2 +226 -0
  109. audex/lib/server/templates/login.html.j2 +111 -0
  110. audex/lib/server/templates/static/script.js +68 -0
  111. audex/lib/server/templates/static/style.css +579 -0
  112. audex/lib/server/types.py +123 -0
  113. audex/lib/session.py +503 -0
  114. audex/lib/store/__init__.py +238 -0
  115. audex/lib/store/localfile.py +411 -0
  116. audex/lib/transcription/__init__.py +33 -0
  117. audex/lib/transcription/dashscope.py +525 -0
  118. audex/lib/transcription/events.py +62 -0
  119. audex/lib/usb.py +554 -0
  120. audex/lib/vpr/__init__.py +38 -0
  121. audex/lib/vpr/unisound/__init__.py +185 -0
  122. audex/lib/vpr/unisound/types.py +469 -0
  123. audex/lib/vpr/xfyun/__init__.py +483 -0
  124. audex/lib/vpr/xfyun/types.py +679 -0
  125. audex/lib/websocket/__init__.py +8 -0
  126. audex/lib/websocket/connection.py +485 -0
  127. audex/lib/websocket/pool.py +991 -0
  128. audex/lib/wifi.py +1146 -0
  129. audex/lifespan.py +75 -0
  130. audex/service/__init__.py +27 -0
  131. audex/service/decorators.py +73 -0
  132. audex/service/doctor/__init__.py +652 -0
  133. audex/service/doctor/const.py +36 -0
  134. audex/service/doctor/exceptions.py +96 -0
  135. audex/service/doctor/types.py +54 -0
  136. audex/service/export/__init__.py +236 -0
  137. audex/service/export/const.py +17 -0
  138. audex/service/export/exceptions.py +34 -0
  139. audex/service/export/types.py +21 -0
  140. audex/service/injectors/__init__.py +1 -0
  141. audex/service/injectors/container.py +53 -0
  142. audex/service/injectors/doctor.py +34 -0
  143. audex/service/injectors/export.py +27 -0
  144. audex/service/injectors/session.py +49 -0
  145. audex/service/session/__init__.py +754 -0
  146. audex/service/session/const.py +34 -0
  147. audex/service/session/exceptions.py +67 -0
  148. audex/service/session/types.py +91 -0
  149. audex/types.py +39 -0
  150. audex/utils.py +287 -0
  151. audex/valueobj/__init__.py +81 -0
  152. audex/valueobj/common/__init__.py +1 -0
  153. audex/valueobj/common/auth.py +84 -0
  154. audex/valueobj/common/email.py +16 -0
  155. audex/valueobj/common/ops.py +22 -0
  156. audex/valueobj/common/phone.py +84 -0
  157. audex/valueobj/common/version.py +72 -0
  158. audex/valueobj/session.py +19 -0
  159. audex/valueobj/utterance.py +15 -0
  160. audex/view/__init__.py +51 -0
  161. audex/view/container.py +17 -0
  162. audex/view/decorators.py +303 -0
  163. audex/view/pages/__init__.py +1 -0
  164. audex/view/pages/dashboard/__init__.py +286 -0
  165. audex/view/pages/dashboard/wifi.py +407 -0
  166. audex/view/pages/login.py +110 -0
  167. audex/view/pages/recording.py +348 -0
  168. audex/view/pages/register.py +202 -0
  169. audex/view/pages/sessions/__init__.py +196 -0
  170. audex/view/pages/sessions/details.py +224 -0
  171. audex/view/pages/sessions/export.py +443 -0
  172. audex/view/pages/settings.py +374 -0
  173. audex/view/pages/voiceprint/__init__.py +1 -0
  174. audex/view/pages/voiceprint/enroll.py +195 -0
  175. audex/view/pages/voiceprint/update.py +195 -0
  176. audex/view/static/css/dashboard.css +452 -0
  177. audex/view/static/css/glass.css +22 -0
  178. audex/view/static/css/global.css +541 -0
  179. audex/view/static/css/login.css +386 -0
  180. audex/view/static/css/recording.css +439 -0
  181. audex/view/static/css/register.css +293 -0
  182. audex/view/static/css/sessions/styles.css +501 -0
  183. audex/view/static/css/settings.css +186 -0
  184. audex/view/static/css/voiceprint/enroll.css +43 -0
  185. audex/view/static/css/voiceprint/styles.css +209 -0
  186. audex/view/static/css/voiceprint/update.css +44 -0
  187. audex/view/static/images/logo.svg +95 -0
  188. audex/view/static/js/recording.js +42 -0
  189. audex-1.0.7a3.dist-info/METADATA +361 -0
  190. audex-1.0.7a3.dist-info/RECORD +192 -0
  191. audex-1.0.7a3.dist-info/WHEEL +4 -0
  192. audex-1.0.7a3.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,406 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+
5
+ import sqlalchemy as sa
6
+ import sqlalchemy.event as saevent
7
+ import sqlalchemy.ext.asyncio as aiosa
8
+ import sqlmodel as sqlm
9
+
10
+ from audex.lib.database import Database
11
+
12
+
13
+ class SQLitePoolConfig(t.TypedDict):
14
+ echo: bool
15
+ """Whether to log all SQL statements."""
16
+
17
+ pool_size: int
18
+ """Number of connections to maintain in the pool."""
19
+
20
+ max_overflow: int
21
+ """Max number of connections beyond pool_size."""
22
+
23
+ pool_timeout: float
24
+ """Seconds to wait before timing out on connection."""
25
+
26
+ pool_recycle: int
27
+ """Seconds after which to recycle connections."""
28
+
29
+ pool_pre_ping: bool
30
+ """Test connections before using them."""
31
+
32
+ create_all: bool
33
+ """Whether to create all tables on init."""
34
+
35
+
36
+ class SQLite(Database):
37
+ """SQLite database container with async SQLModel/SQLAlchemy support.
38
+
39
+ This class provides a high-level interface for SQLite database
40
+ operations with the following features:
41
+
42
+ 1. Async engine and session management for repository pattern
43
+ 2. Connection pooling with configurable parameters
44
+ 3. Raw SQL execution with transaction control
45
+ 4. Schema management utilities (create_all/drop_all)
46
+ 5. Unified lifecycle management through AsyncContextMixin
47
+
48
+ Attributes:
49
+ uri: SQLite connection URI.
50
+ engine: SQLAlchemy async engine (initialized after init()).
51
+ sessionmaker: Async session factory (initialized after init()).
52
+ cfg: Connection pool configuration.
53
+
54
+ Args:
55
+ uri: SQLite connection URI (must use aiosqlite driver).
56
+ Example: "sqlite+aiosqlite:///./database.db" (relative path)
57
+ Example: "sqlite+aiosqlite:////absolute/path/database.db" (absolute
58
+ path)
59
+ Example: "sqlite+aiosqlite:///:memory:" (in-memory database)
60
+ tables: List of SQLModel classes to manage. Used for
61
+ create_all/drop_all.
62
+ echo: Whether to log all SQL statements (useful for debugging).
63
+ pool_size: Number of connections to maintain in the pool.
64
+ Note: SQLite with aiosqlite uses NullPool by default in async mode.
65
+ max_overflow: Max number of connections beyond pool_size.
66
+ pool_timeout: Seconds to wait before timing out on connection.
67
+ pool_recycle: Seconds after which to recycle connections.
68
+ Set to -1 to disable recycling.
69
+ pool_pre_ping: Test connections before using them. Recommended
70
+ for production to handle stale connections.
71
+ create_all: Whether to create all tables on init().
72
+
73
+ Example:
74
+ ```python
75
+ # Setup with file-based database
76
+ sqlite = SQLite(
77
+ uri="sqlite+aiosqlite:///./app.db",
78
+ tables=[User, Post],
79
+ echo=True,
80
+ )
81
+
82
+ # Setup with in-memory database (useful for testing)
83
+ sqlite = SQLite(
84
+ uri="sqlite+aiosqlite:///:memory:",
85
+ tables=[User, Post],
86
+ )
87
+
88
+ # Initialize
89
+ await sqlite.init()
90
+
91
+ # Create tables
92
+ await sqlite.create_all()
93
+
94
+ # Use session for ORM operations
95
+ async with sqlite.session() as session:
96
+ user = await session.get(User, user_id)
97
+ user.username = "new_name"
98
+ await session.commit()
99
+
100
+ # Execute raw SQL
101
+ result = await sqlite.exec(
102
+ "SELECT * FROM users WHERE age > :age",
103
+ readonly=True,
104
+ age=21,
105
+ )
106
+
107
+ # Cleanup
108
+ await sqlite.close()
109
+ ```
110
+
111
+ Note:
112
+ - The URI must use the aiosqlite driver for async support.
113
+ - SQLite doesn't support some PostgreSQL features (e.g., JSONB operators).
114
+ - Use JSON1 extension for JSON operations (enabled by default).
115
+ - File path format: Use three slashes for relative paths, four for absolute.
116
+ """
117
+
118
+ def __init__(
119
+ self,
120
+ uri: str,
121
+ *,
122
+ tables: list[type[sqlm.SQLModel]] | None = None,
123
+ echo: bool = False,
124
+ pool_size: int = 20,
125
+ max_overflow: int = 10,
126
+ pool_timeout: float = 30.0,
127
+ pool_recycle: int = 3600,
128
+ pool_pre_ping: bool = True,
129
+ create_all: bool = True,
130
+ ) -> None:
131
+ self.uri = uri
132
+ self.tables = tables or []
133
+ self.engine: aiosa.AsyncEngine | None = None
134
+ self.sessionmaker: aiosa.async_sessionmaker[aiosa.AsyncSession] | None = None
135
+ self.cfg = SQLitePoolConfig(
136
+ echo=echo,
137
+ pool_size=pool_size,
138
+ max_overflow=max_overflow,
139
+ pool_timeout=pool_timeout,
140
+ pool_recycle=pool_recycle,
141
+ pool_pre_ping=pool_pre_ping,
142
+ create_all=create_all,
143
+ )
144
+
145
+ async def init(self) -> None:
146
+ """Initialize the database engine and session factory.
147
+
148
+ This method creates the async engine with connection pooling and
149
+ sets up the session factory. It should be called during application
150
+ startup, typically in a lifespan context manager.
151
+
152
+ For SQLite, this also enables foreign key constraints and loads
153
+ the JSON1 extension if available.
154
+
155
+ Raises:
156
+ Exception: If engine creation fails (e.g., invalid URI).
157
+ """
158
+ # Create engine with SQLite-specific configuration
159
+ self.engine = aiosa.create_async_engine(
160
+ self.uri,
161
+ echo=self.cfg["echo"],
162
+ # SQLite-specific: Use NullPool for better async compatibility
163
+ # or StaticPool for in-memory databases
164
+ poolclass=sa.pool.NullPool if ":memory:" not in self.uri else sa.pool.StaticPool,
165
+ connect_args={
166
+ "check_same_thread": False, # Required for async SQLite
167
+ },
168
+ )
169
+
170
+ # Configure SQLite settings
171
+ @saevent.listens_for(self.engine.sync_engine, "connect")
172
+ def set_sqlite_pragma(dbapi_conn: t.Any, _connection_record: t.Any) -> None:
173
+ """Set SQLite-specific pragmas on connection."""
174
+ cursor = dbapi_conn.cursor()
175
+ # Enable foreign key constraints
176
+ cursor.execute("PRAGMA foreign_keys=ON")
177
+ # Enable WAL mode for better concurrency
178
+ cursor.execute("PRAGMA journal_mode=WAL")
179
+ cursor.close()
180
+
181
+ self.sessionmaker = aiosa.async_sessionmaker(
182
+ self.engine,
183
+ class_=aiosa.AsyncSession,
184
+ expire_on_commit=False,
185
+ )
186
+
187
+ if self.cfg["create_all"]:
188
+ await self.create_all()
189
+
190
+ async def close(self) -> None:
191
+ """Close the database engine and clean up resources.
192
+
193
+ This method disposes of the connection pool and resets the engine
194
+ and session factory. It should be called during application shutdown.
195
+
196
+ Note:
197
+ This method is idempotent and safe to call multiple times.
198
+ """
199
+ if self.engine:
200
+ await self.engine.dispose()
201
+ self.engine = None
202
+ self.sessionmaker = None
203
+
204
+ def session(self) -> aiosa.AsyncSession:
205
+ """Create a new async database session.
206
+
207
+ Returns:
208
+ An async session context manager.
209
+
210
+ Raises:
211
+ RuntimeError: If sessionmaker is not initialized (call init() first).
212
+
213
+ Example:
214
+ ```python
215
+ async with sqlite.session() as session:
216
+ # Start a transaction
217
+ user = await session.get(User, user_id)
218
+ user.username = "new_name"
219
+ await session.commit()
220
+ ```
221
+
222
+ Note:
223
+ The session is automatically committed on successful exit and
224
+ rolled back on exception. You can also manually commit/rollback
225
+ within the context.
226
+ """
227
+ if not self.sessionmaker:
228
+ raise RuntimeError("Sessionmaker not initialized. Call init() first.")
229
+
230
+ return self.sessionmaker()
231
+
232
+ async def exec(self, sql: str, /, readonly: bool = False, **params: t.Any) -> sa.Result[t.Any]:
233
+ """Execute a raw SQL statement.
234
+
235
+ This method provides direct SQL execution for cases where ORM
236
+ abstractions are insufficient or when specific optimizations
237
+ are needed.
238
+
239
+ Args:
240
+ sql: Raw SQL string to execute. Use named parameters with
241
+ colon prefix.
242
+ readonly: If True, does not commit the transaction. Use this
243
+ for SELECT queries to avoid unnecessary commits.
244
+ **params: Named parameters for the SQL statement.
245
+
246
+ Returns:
247
+ SQLAlchemy Result object containing query results.
248
+
249
+ Raises:
250
+ RuntimeError: If execution fails, with the original exception
251
+ as the cause.
252
+
253
+ Example:
254
+ ```python
255
+ # Read-only query
256
+ result = await sqlite.exec(
257
+ "SELECT * FROM users WHERE age > :age",
258
+ readonly=True,
259
+ age=21,
260
+ )
261
+ users = result.fetchall()
262
+
263
+ # Write query
264
+ await sqlite.exec(
265
+ "UPDATE users SET status = :status WHERE id = :id",
266
+ readonly=False,
267
+ status="active",
268
+ id=123,
269
+ )
270
+
271
+ # Using JSON1 extension
272
+ result = await sqlite.exec(
273
+ "SELECT * FROM users WHERE json_extract(tags, '$.premium') = 1",
274
+ readonly=True,
275
+ )
276
+ ```
277
+
278
+ Warning:
279
+ Be careful with SQL injection. Always use parameterized queries
280
+ with named parameters instead of string formatting.
281
+ """
282
+ async with self.session() as session, session.begin():
283
+ result = await session.execute(sa.text(sql), params=params or None)
284
+ if not readonly:
285
+ await session.commit()
286
+ return result
287
+
288
+ async def ping(self) -> bool:
289
+ """Check database connectivity.
290
+
291
+ This method attempts to execute a simple query to verify that
292
+ the database is reachable and responsive.
293
+
294
+ Returns:
295
+ True if database is reachable, False otherwise.
296
+
297
+ Note:
298
+ This method does not raise exceptions. It catches all errors
299
+ and returns False instead.
300
+ """
301
+ if not self.engine:
302
+ return False
303
+
304
+ try:
305
+ async with self.engine.connect() as conn:
306
+ await conn.execute(sa.text("SELECT 1"))
307
+ return True
308
+ except Exception:
309
+ return False
310
+
311
+ async def create_all(self) -> None:
312
+ """Create all database tables.
313
+
314
+ This method creates tables for the specified SQLModel classes, or
315
+ all tables in the SQLModel metadata if no models are specified.
316
+
317
+ Raises:
318
+ RuntimeError: If engine is not initialized.
319
+
320
+ Example:
321
+ ```python
322
+ # Create specific tables
323
+ sqlite = SQLite(
324
+ uri="sqlite+aiosqlite:///./app.db",
325
+ tables=[User, Post, Comment],
326
+ )
327
+ await sqlite.init()
328
+ await sqlite.create_all()
329
+
330
+ # Create all tables
331
+ sqlite = SQLite(uri="sqlite+aiosqlite:///./app.db")
332
+ await sqlite.init()
333
+ await sqlite.create_all()
334
+ ```
335
+
336
+ Warning:
337
+ This is typically used for development/testing. In production,
338
+ use proper migration tools like Alembic to manage schema changes.
339
+ """
340
+ if not self.engine:
341
+ raise RuntimeError("Engine not initialized. Call init() first.")
342
+
343
+ async with self.engine.begin() as conn:
344
+ if self.tables:
345
+
346
+ def _create_tables(sync_conn: sa.Connection) -> None:
347
+ for model in self.tables:
348
+ model.metadata.create_all(bind=sync_conn)
349
+
350
+ await conn.run_sync(_create_tables)
351
+ else:
352
+ await conn.run_sync(sqlm.SQLModel.metadata.create_all)
353
+
354
+ async def drop_all(self) -> None:
355
+ """Drop all database tables.
356
+
357
+ This method drops all tables defined in the SQLModel metadata.
358
+
359
+ Raises:
360
+ RuntimeError: If engine is not initialized.
361
+
362
+ Example:
363
+ ```python
364
+ await sqlite.drop_all() # Be careful!
365
+ ```
366
+
367
+ Warning:
368
+ This is destructive and should only be used in development/testing.
369
+ All data will be lost. There is no confirmation prompt.
370
+ """
371
+ if not self.engine:
372
+ raise RuntimeError("Engine not initialized. Call init() first.")
373
+
374
+ async with self.engine.begin() as conn:
375
+ await conn.run_sync(sqlm.SQLModel.metadata.drop_all)
376
+
377
+ async def vacuum(self) -> None:
378
+ """Run VACUUM command to optimize the database file.
379
+
380
+ This command rebuilds the database file, repacking it into a minimal
381
+ amount of disk space. It's useful after deleting large amounts of data.
382
+
383
+ Raises:
384
+ RuntimeError: If engine is not initialized.
385
+
386
+ Example:
387
+ ```python
388
+ # After bulk deletions
389
+ await sqlite.exec(
390
+ "DELETE FROM old_logs WHERE created_at < :date",
391
+ date=cutoff_date,
392
+ )
393
+ await sqlite.vacuum() # Reclaim disk space
394
+ ```
395
+
396
+ Note:
397
+ VACUUM requires exclusive access to the database and may take
398
+ significant time on large databases.
399
+ """
400
+ if not self.engine:
401
+ raise RuntimeError("Engine not initialized. Call init() first.")
402
+
403
+ # VACUUM must be run outside a transaction
404
+ async with self.engine.connect() as conn:
405
+ await conn.execute(sa.text("VACUUM"))
406
+ await conn.commit()
audex/lib/exporter.py ADDED
@@ -0,0 +1,189 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import json
5
+ import pathlib
6
+ import zipfile
7
+
8
+ from audex.entity.segment import Segment
9
+ from audex.entity.session import Session
10
+ from audex.entity.utterance import Utterance
11
+ from audex.filters.generated import segment_filter
12
+ from audex.filters.generated import utterance_filter
13
+ from audex.helper.mixin import LoggingMixin
14
+ from audex.lib.repos.segment import SegmentRepository
15
+ from audex.lib.repos.session import SessionRepository
16
+ from audex.lib.repos.utterance import UtteranceRepository
17
+ from audex.lib.server.types import AudioMetadataItem
18
+ from audex.lib.server.types import AudioMetadataJSON
19
+ from audex.lib.server.types import ConversationJSON
20
+ from audex.lib.server.types import SegmentDict
21
+ from audex.lib.server.types import SessionDict
22
+ from audex.lib.server.types import SessionExportData
23
+ from audex.lib.server.types import UtteranceDict
24
+ from audex.lib.store import Store
25
+
26
+
27
+ class Exporter(LoggingMixin):
28
+ """Exporter for packaging session data and audio files."""
29
+
30
+ __logtag__ = "audex.lib.exporter"
31
+
32
+ def __init__(
33
+ self,
34
+ session_repo: SessionRepository,
35
+ segment_repo: SegmentRepository,
36
+ utterance_repo: UtteranceRepository,
37
+ store: Store,
38
+ ):
39
+ super().__init__()
40
+ self.session_repo = session_repo
41
+ self.segment_repo = segment_repo
42
+ self.utterance_repo = utterance_repo
43
+ self.store = store
44
+
45
+ async def export_session_data(self, session_id: str) -> SessionExportData:
46
+ """Export session data as structured format."""
47
+ # Get session
48
+ session = await self.session_repo.read(session_id)
49
+ if not session:
50
+ raise ValueError(f"Session {session_id} not found")
51
+
52
+ # Get utterances
53
+ utt_filter = utterance_filter().session_id.eq(session_id).sequence.asc()
54
+ utterances = await self.utterance_repo.list(utt_filter.build())
55
+
56
+ # Get segments
57
+ seg_filter = segment_filter().session_id.eq(session_id).sequence.asc()
58
+ segments = await self.segment_repo.list(seg_filter.build())
59
+
60
+ # Convert to typed dicts
61
+ return SessionExportData(
62
+ session=self._session_to_dict(session),
63
+ utterances=[self._utterance_to_dict(u) for u in utterances],
64
+ segments=[self._segment_to_dict(s) for s in segments],
65
+ )
66
+
67
+ async def export_session_zip(self, session_id: str) -> bytes:
68
+ """Export session as ZIP package."""
69
+ export_data = await self.export_session_data(session_id)
70
+ zip_buffer = io.BytesIO()
71
+
72
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf:
73
+ # Add conversation.json
74
+ conversation: ConversationJSON = {
75
+ "session": export_data["session"],
76
+ "utterances": export_data["utterances"],
77
+ "total_utterances": len(export_data["utterances"]),
78
+ "total_segments": len(export_data["segments"]),
79
+ }
80
+ zipf.writestr(
81
+ "conversation.json",
82
+ json.dumps(conversation, ensure_ascii=False, indent=2),
83
+ )
84
+
85
+ # Add audio files
86
+ audio_metadata_items: list[AudioMetadataItem] = []
87
+
88
+ for idx, segment_dict in enumerate(export_data["segments"], start=1):
89
+ audio_key = segment_dict["audio_key"]
90
+
91
+ try:
92
+ audio_data = await self.store.download(audio_key)
93
+ ext = pathlib.Path(audio_key).suffix or ".mp3"
94
+ filename = f"segment_{idx:03d}{ext}"
95
+
96
+ zipf.writestr(f"audio/{filename}", audio_data)
97
+
98
+ audio_metadata_items.append(
99
+ AudioMetadataItem(
100
+ filename=filename,
101
+ sequence=segment_dict["sequence"],
102
+ duration_ms=segment_dict["duration_ms"],
103
+ started_at=segment_dict["started_at"],
104
+ ended_at=segment_dict["ended_at"],
105
+ )
106
+ )
107
+
108
+ self.logger.debug(f"Added audio file: {filename}")
109
+
110
+ except Exception as e:
111
+ self.logger.error(f"Failed to add audio {audio_key}: {e}")
112
+
113
+ # Add audio metadata
114
+ if audio_metadata_items:
115
+ audio_metadata: AudioMetadataJSON = {
116
+ "session_id": session_id,
117
+ "total_segments": len(audio_metadata_items),
118
+ "segments": audio_metadata_items,
119
+ }
120
+ zipf.writestr(
121
+ "audio/metadata.json",
122
+ json.dumps(audio_metadata, ensure_ascii=False, indent=2),
123
+ )
124
+
125
+ zip_buffer.seek(0)
126
+ return zip_buffer.getvalue()
127
+
128
+ async def export_multiple_sessions_zip(self, session_ids: list[str]) -> bytes:
129
+ """Export multiple sessions as ZIP package."""
130
+ zip_buffer = io.BytesIO()
131
+
132
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf:
133
+ for session_id in session_ids:
134
+ try:
135
+ session_zip_data = await self.export_session_zip(session_id)
136
+
137
+ with zipfile.ZipFile(io.BytesIO(session_zip_data), "r") as session_zipf:
138
+ for file_info in session_zipf.infolist():
139
+ file_data = session_zipf.read(file_info.filename)
140
+ new_path = f"{session_id}/{file_info.filename}"
141
+ zipf.writestr(new_path, file_data)
142
+
143
+ self.logger.info(f"Added session {session_id} to export")
144
+
145
+ except Exception as e:
146
+ self.logger.error(f"Failed to export session {session_id}: {e}")
147
+
148
+ zip_buffer.seek(0)
149
+ return zip_buffer.getvalue()
150
+
151
+ def _session_to_dict(self, session: Session) -> SessionDict:
152
+ """Convert Session to typed dict."""
153
+ return SessionDict(
154
+ id=session.id,
155
+ doctor_id=session.doctor_id,
156
+ patient_name=session.patient_name,
157
+ clinic_number=session.clinic_number,
158
+ medical_record_number=session.medical_record_number,
159
+ diagnosis=session.diagnosis,
160
+ status=session.status.value,
161
+ started_at=session.started_at.isoformat() if session.started_at else None,
162
+ ended_at=session.ended_at.isoformat() if session.ended_at else None,
163
+ created_at=session.created_at.isoformat(),
164
+ )
165
+
166
+ def _utterance_to_dict(self, utterance: Utterance) -> UtteranceDict:
167
+ """Convert Utterance to typed dict."""
168
+ return UtteranceDict(
169
+ id=utterance.id,
170
+ sequence=utterance.sequence,
171
+ speaker=utterance.speaker.value,
172
+ text=utterance.text,
173
+ confidence=utterance.confidence,
174
+ start_time_ms=utterance.start_time_ms,
175
+ end_time_ms=utterance.end_time_ms,
176
+ duration_ms=utterance.duration_ms,
177
+ timestamp=utterance.timestamp.isoformat(),
178
+ )
179
+
180
+ def _segment_to_dict(self, segment: Segment) -> SegmentDict:
181
+ """Convert Segment to typed dict."""
182
+ return SegmentDict(
183
+ id=segment.id,
184
+ sequence=segment.sequence,
185
+ audio_key=segment.audio_key,
186
+ started_at=segment.started_at.isoformat(),
187
+ ended_at=segment.ended_at.isoformat() if segment.ended_at else None,
188
+ duration_ms=segment.duration_ms,
189
+ )
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+
5
+ if t.TYPE_CHECKING:
6
+ from audex.config import Config
7
+ from audex.lib.cache import KVCache
8
+
9
+
10
+ def make_cache(config: Config) -> KVCache:
11
+ from audex.lib.cache import KeyBuilder
12
+ from audex.lib.cache.inmemory import InmemoryCache
13
+
14
+ key_builder = KeyBuilder(
15
+ split_char=config.infrastructure.cache.split_char,
16
+ prefix=config.infrastructure.cache.prefix,
17
+ )
18
+
19
+ return InmemoryCache(
20
+ key_builder=key_builder,
21
+ cache_type=config.infrastructure.cache.inmemory.cache_type,
22
+ maxsize=config.infrastructure.cache.inmemory.max_size,
23
+ default_ttl=config.infrastructure.cache.inmemory.default_ttl,
24
+ negative_ttl=config.infrastructure.cache.inmemory.negative_ttl,
25
+ )
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from dependency_injector import containers
4
+ from dependency_injector import providers
5
+
6
+ from audex.config import Config
7
+ from audex.lib.injectors.cache import make_cache
8
+ from audex.lib.injectors.exporter import make_exporter
9
+ from audex.lib.injectors.recorder import make_recorder
10
+ from audex.lib.injectors.server import make_server
11
+ from audex.lib.injectors.session import make_session_manager
12
+ from audex.lib.injectors.sqlite import make_sqlite
13
+ from audex.lib.injectors.store import make_store
14
+ from audex.lib.injectors.transcription import make_transcription
15
+ from audex.lib.injectors.usb import make_usb_manager
16
+ from audex.lib.injectors.vpr import make_vpr
17
+ from audex.lib.injectors.wifi import make_wifi_manager
18
+ from audex.lib.repos.container import RepositoryContainer
19
+
20
+
21
+ class InfrastructureContainer(containers.DeclarativeContainer):
22
+ # Dependencies
23
+ config = providers.Dependency(instance_of=Config)
24
+
25
+ # Components
26
+ session_manager = providers.Singleton(make_session_manager, config=config)
27
+ cache = providers.Singleton(make_cache, config=config)
28
+ usb = providers.Singleton(make_usb_manager)
29
+ wifi = providers.Singleton(make_wifi_manager)
30
+ sqlite = providers.Singleton(make_sqlite, config=config)
31
+ store = providers.Singleton(make_store, config=config)
32
+ vpr = providers.Singleton(make_vpr, config=config)
33
+ recorder = providers.Singleton(make_recorder, config=config, store=store)
34
+ transcription = providers.Singleton(make_transcription, config=config)
35
+ repository = providers.Container(RepositoryContainer, sqlite=sqlite)
36
+ exporter = providers.Factory(
37
+ make_exporter,
38
+ session_repo=repository.session,
39
+ segment_repo=repository.segment,
40
+ utterance_repo=repository.utterance,
41
+ store=store,
42
+ )
43
+ server = providers.Factory(
44
+ make_server,
45
+ doctor_repo=repository.doctor,
46
+ exporter=exporter,
47
+ )
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+
5
+ if t.TYPE_CHECKING:
6
+ from audex.lib.exporter import Exporter
7
+ from audex.lib.repos.segment import SegmentRepository
8
+ from audex.lib.repos.session import SessionRepository
9
+ from audex.lib.repos.utterance import UtteranceRepository
10
+ from audex.lib.store import Store
11
+
12
+
13
+ def make_exporter(
14
+ session_repo: SessionRepository,
15
+ segment_repo: SegmentRepository,
16
+ utterance_repo: UtteranceRepository,
17
+ store: Store,
18
+ ) -> Exporter:
19
+ from audex.lib.exporter import Exporter
20
+
21
+ return Exporter(
22
+ session_repo=session_repo,
23
+ segment_repo=segment_repo,
24
+ utterance_repo=utterance_repo,
25
+ store=store,
26
+ )