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.
- audex/__init__.py +9 -0
- audex/__main__.py +7 -0
- audex/cli/__init__.py +189 -0
- audex/cli/apis/__init__.py +12 -0
- audex/cli/apis/init/__init__.py +34 -0
- audex/cli/apis/init/gencfg.py +130 -0
- audex/cli/apis/init/setup.py +330 -0
- audex/cli/apis/init/vprgroup.py +125 -0
- audex/cli/apis/serve.py +141 -0
- audex/cli/args.py +356 -0
- audex/cli/exceptions.py +44 -0
- audex/cli/helper/__init__.py +0 -0
- audex/cli/helper/ansi.py +193 -0
- audex/cli/helper/display.py +288 -0
- audex/config/__init__.py +64 -0
- audex/config/core/__init__.py +30 -0
- audex/config/core/app.py +29 -0
- audex/config/core/audio.py +45 -0
- audex/config/core/logging.py +163 -0
- audex/config/core/session.py +11 -0
- audex/config/helper/__init__.py +1 -0
- audex/config/helper/client/__init__.py +1 -0
- audex/config/helper/client/http.py +28 -0
- audex/config/helper/client/websocket.py +21 -0
- audex/config/helper/provider/__init__.py +1 -0
- audex/config/helper/provider/dashscope.py +13 -0
- audex/config/helper/provider/unisound.py +18 -0
- audex/config/helper/provider/xfyun.py +23 -0
- audex/config/infrastructure/__init__.py +31 -0
- audex/config/infrastructure/cache.py +51 -0
- audex/config/infrastructure/database.py +48 -0
- audex/config/infrastructure/recorder.py +32 -0
- audex/config/infrastructure/store.py +19 -0
- audex/config/provider/__init__.py +18 -0
- audex/config/provider/transcription.py +109 -0
- audex/config/provider/vpr.py +99 -0
- audex/container.py +40 -0
- audex/entity/__init__.py +468 -0
- audex/entity/doctor.py +109 -0
- audex/entity/doctor.pyi +51 -0
- audex/entity/fields.py +401 -0
- audex/entity/segment.py +115 -0
- audex/entity/segment.pyi +38 -0
- audex/entity/session.py +133 -0
- audex/entity/session.pyi +47 -0
- audex/entity/utterance.py +142 -0
- audex/entity/utterance.pyi +48 -0
- audex/entity/vp.py +68 -0
- audex/entity/vp.pyi +35 -0
- audex/exceptions.py +157 -0
- audex/filters/__init__.py +692 -0
- audex/filters/generated/__init__.py +21 -0
- audex/filters/generated/doctor.py +987 -0
- audex/filters/generated/segment.py +723 -0
- audex/filters/generated/session.py +978 -0
- audex/filters/generated/utterance.py +939 -0
- audex/filters/generated/vp.py +815 -0
- audex/helper/__init__.py +1 -0
- audex/helper/hash.py +33 -0
- audex/helper/mixin.py +65 -0
- audex/helper/net.py +19 -0
- audex/helper/settings/__init__.py +830 -0
- audex/helper/settings/fields.py +317 -0
- audex/helper/stream.py +153 -0
- audex/injectors/__init__.py +1 -0
- audex/injectors/config.py +12 -0
- audex/injectors/lifespan.py +7 -0
- audex/lib/__init__.py +1 -0
- audex/lib/cache/__init__.py +383 -0
- audex/lib/cache/inmemory.py +513 -0
- audex/lib/database/__init__.py +83 -0
- audex/lib/database/sqlite.py +406 -0
- audex/lib/exporter.py +189 -0
- audex/lib/injectors/__init__.py +1 -0
- audex/lib/injectors/cache.py +25 -0
- audex/lib/injectors/container.py +47 -0
- audex/lib/injectors/exporter.py +26 -0
- audex/lib/injectors/recorder.py +33 -0
- audex/lib/injectors/server.py +17 -0
- audex/lib/injectors/session.py +18 -0
- audex/lib/injectors/sqlite.py +24 -0
- audex/lib/injectors/store.py +13 -0
- audex/lib/injectors/transcription.py +42 -0
- audex/lib/injectors/usb.py +12 -0
- audex/lib/injectors/vpr.py +65 -0
- audex/lib/injectors/wifi.py +7 -0
- audex/lib/recorder.py +844 -0
- audex/lib/repos/__init__.py +149 -0
- audex/lib/repos/container.py +23 -0
- audex/lib/repos/database/__init__.py +1 -0
- audex/lib/repos/database/sqlite.py +672 -0
- audex/lib/repos/decorators.py +74 -0
- audex/lib/repos/doctor.py +286 -0
- audex/lib/repos/segment.py +302 -0
- audex/lib/repos/session.py +285 -0
- audex/lib/repos/tables/__init__.py +70 -0
- audex/lib/repos/tables/doctor.py +137 -0
- audex/lib/repos/tables/segment.py +113 -0
- audex/lib/repos/tables/session.py +140 -0
- audex/lib/repos/tables/utterance.py +131 -0
- audex/lib/repos/tables/vp.py +102 -0
- audex/lib/repos/utterance.py +288 -0
- audex/lib/repos/vp.py +286 -0
- audex/lib/restful.py +251 -0
- audex/lib/server/__init__.py +97 -0
- audex/lib/server/auth.py +98 -0
- audex/lib/server/handlers.py +248 -0
- audex/lib/server/templates/index.html.j2 +226 -0
- audex/lib/server/templates/login.html.j2 +111 -0
- audex/lib/server/templates/static/script.js +68 -0
- audex/lib/server/templates/static/style.css +579 -0
- audex/lib/server/types.py +123 -0
- audex/lib/session.py +503 -0
- audex/lib/store/__init__.py +238 -0
- audex/lib/store/localfile.py +411 -0
- audex/lib/transcription/__init__.py +33 -0
- audex/lib/transcription/dashscope.py +525 -0
- audex/lib/transcription/events.py +62 -0
- audex/lib/usb.py +554 -0
- audex/lib/vpr/__init__.py +38 -0
- audex/lib/vpr/unisound/__init__.py +185 -0
- audex/lib/vpr/unisound/types.py +469 -0
- audex/lib/vpr/xfyun/__init__.py +483 -0
- audex/lib/vpr/xfyun/types.py +679 -0
- audex/lib/websocket/__init__.py +8 -0
- audex/lib/websocket/connection.py +485 -0
- audex/lib/websocket/pool.py +991 -0
- audex/lib/wifi.py +1146 -0
- audex/lifespan.py +75 -0
- audex/service/__init__.py +27 -0
- audex/service/decorators.py +73 -0
- audex/service/doctor/__init__.py +652 -0
- audex/service/doctor/const.py +36 -0
- audex/service/doctor/exceptions.py +96 -0
- audex/service/doctor/types.py +54 -0
- audex/service/export/__init__.py +236 -0
- audex/service/export/const.py +17 -0
- audex/service/export/exceptions.py +34 -0
- audex/service/export/types.py +21 -0
- audex/service/injectors/__init__.py +1 -0
- audex/service/injectors/container.py +53 -0
- audex/service/injectors/doctor.py +34 -0
- audex/service/injectors/export.py +27 -0
- audex/service/injectors/session.py +49 -0
- audex/service/session/__init__.py +754 -0
- audex/service/session/const.py +34 -0
- audex/service/session/exceptions.py +67 -0
- audex/service/session/types.py +91 -0
- audex/types.py +39 -0
- audex/utils.py +287 -0
- audex/valueobj/__init__.py +81 -0
- audex/valueobj/common/__init__.py +1 -0
- audex/valueobj/common/auth.py +84 -0
- audex/valueobj/common/email.py +16 -0
- audex/valueobj/common/ops.py +22 -0
- audex/valueobj/common/phone.py +84 -0
- audex/valueobj/common/version.py +72 -0
- audex/valueobj/session.py +19 -0
- audex/valueobj/utterance.py +15 -0
- audex/view/__init__.py +51 -0
- audex/view/container.py +17 -0
- audex/view/decorators.py +303 -0
- audex/view/pages/__init__.py +1 -0
- audex/view/pages/dashboard/__init__.py +286 -0
- audex/view/pages/dashboard/wifi.py +407 -0
- audex/view/pages/login.py +110 -0
- audex/view/pages/recording.py +348 -0
- audex/view/pages/register.py +202 -0
- audex/view/pages/sessions/__init__.py +196 -0
- audex/view/pages/sessions/details.py +224 -0
- audex/view/pages/sessions/export.py +443 -0
- audex/view/pages/settings.py +374 -0
- audex/view/pages/voiceprint/__init__.py +1 -0
- audex/view/pages/voiceprint/enroll.py +195 -0
- audex/view/pages/voiceprint/update.py +195 -0
- audex/view/static/css/dashboard.css +452 -0
- audex/view/static/css/glass.css +22 -0
- audex/view/static/css/global.css +541 -0
- audex/view/static/css/login.css +386 -0
- audex/view/static/css/recording.css +439 -0
- audex/view/static/css/register.css +293 -0
- audex/view/static/css/sessions/styles.css +501 -0
- audex/view/static/css/settings.css +186 -0
- audex/view/static/css/voiceprint/enroll.css +43 -0
- audex/view/static/css/voiceprint/styles.css +209 -0
- audex/view/static/css/voiceprint/update.css +44 -0
- audex/view/static/images/logo.svg +95 -0
- audex/view/static/js/recording.js +42 -0
- audex-1.0.7a3.dist-info/METADATA +361 -0
- audex-1.0.7a3.dist-info/RECORD +192 -0
- audex-1.0.7a3.dist-info/WHEEL +4 -0
- 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
|
+
)
|