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
audex/entity/fields.py ADDED
@@ -0,0 +1,401 @@
1
+ # ruff: noqa: N802
2
+ from __future__ import annotations
3
+
4
+ import datetime
5
+ import typing as t
6
+
7
+ if t.TYPE_CHECKING:
8
+ from audex.entity import Entity
9
+
10
+
11
+ T = t.TypeVar("T")
12
+
13
+
14
+ class FieldSpec(t.Generic[T]):
15
+ """A descriptor for entity fields with validation and default value
16
+ support.
17
+
18
+ This descriptor provides type-safe field access with optional validation,
19
+ default values, immutability, and nullable support.
20
+
21
+ Attributes:
22
+ default: The default value for the field.
23
+ default_factory: A callable that returns the default value.
24
+ nullable: Whether the field can be None.
25
+ immutable: Whether the field can be modified after initial assignment.
26
+ sortable: Whether the field supports sorting operations.
27
+ name: The public name of the field (set by __set_name__).
28
+ private_name: The private attribute name for storing the value.
29
+ _field_type: Stored type information for stub generation.
30
+
31
+ Args:
32
+ default: Default value for the field.
33
+ default_factory: Callable that returns a default value.
34
+ nullable: Whether None is allowed as a value.
35
+ immutable: Whether the field can be modified after being set.
36
+ sortable: Whether the field supports sorting operations.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ *,
42
+ default: T | None = None,
43
+ default_factory: t.Callable[[], T] | None = None,
44
+ nullable: bool = False,
45
+ immutable: bool = False,
46
+ sortable: bool = True,
47
+ ) -> None:
48
+ self.default = default
49
+ self.default_factory = default_factory
50
+ self.nullable = nullable
51
+ self.immutable = immutable
52
+ self.sortable = sortable
53
+ self.name: str = ""
54
+ self.private_name: str = ""
55
+ self._field_type: type | None = None # Store type information
56
+
57
+ def __set_name__(self, owner: type, name: str) -> None:
58
+ """Set the field name when the descriptor is assigned to a class
59
+ attribute.
60
+
61
+ Args:
62
+ owner: The class that owns this descriptor.
63
+ name: The name of the attribute.
64
+ """
65
+ self.name = name
66
+ self.private_name = f"_field_{name}"
67
+
68
+ @t.overload
69
+ def __get__(self, obj: None, objtype: type[t.Any] | None = None) -> t.Self: ...
70
+ @t.overload
71
+ def __get__(self, obj: Entity, objtype: type[t.Any] | None = None) -> T: ...
72
+ def __get__(self, obj: Entity | None, objtype: type[t.Any] | None = None) -> T | t.Self:
73
+ """Get the field value."""
74
+ if obj is None:
75
+ return self
76
+
77
+ if not hasattr(obj, self.private_name):
78
+ if self.default_factory is not None:
79
+ value = self.default_factory()
80
+ elif self.default is not None:
81
+ value = self.default
82
+ elif self.nullable:
83
+ value = None
84
+ else:
85
+ raise AttributeError(f"Field '{self.name}' has not been set")
86
+ setattr(obj, self.private_name, value)
87
+
88
+ return t.cast(T, getattr(obj, self.private_name))
89
+
90
+ def __set__(self, obj: Entity, value: T) -> None:
91
+ """Set the field value."""
92
+ if hasattr(obj, self.private_name) and self.immutable:
93
+ raise AttributeError(f"Field '{self.name}' is immutable")
94
+
95
+ if value is None and not self.nullable:
96
+ raise ValueError(f"Field '{self.name}' cannot be None")
97
+
98
+ setattr(obj, self.private_name, value)
99
+
100
+ def __delete__(self, obj: Entity) -> None:
101
+ """Delete the field value."""
102
+ if self.immutable:
103
+ raise AttributeError(f"Field '{self.name}' is immutable")
104
+ if hasattr(obj, self.private_name):
105
+ delattr(obj, self.private_name)
106
+
107
+
108
+ # Modified factory functions to store type information
109
+
110
+
111
+ class StringBackedFieldSpec(FieldSpec[T]):
112
+ """A field descriptor for values that are persisted as strings in
113
+ the database."""
114
+
115
+
116
+ @t.overload
117
+ def StringBackedField(
118
+ _field_type: type[T],
119
+ *,
120
+ default: T | None = None,
121
+ default_factory: t.Callable[[], T] | None = None,
122
+ nullable: bool = False,
123
+ immutable: bool = False,
124
+ sortable: bool = True,
125
+ ) -> StringBackedFieldSpec[T]: ...
126
+ @t.overload
127
+ def StringBackedField(
128
+ _field_type: None = None,
129
+ *,
130
+ default: t.Any | None = None,
131
+ default_factory: t.Callable[[], t.Any] | None = None,
132
+ nullable: bool = False,
133
+ immutable: bool = False,
134
+ sortable: bool = True,
135
+ ) -> StringBackedFieldSpec[t.Any]: ...
136
+ def StringBackedField(
137
+ _field_type: type[t.Any] | None = None,
138
+ *,
139
+ default: t.Any | None = None,
140
+ default_factory: t.Callable[[], t.Any] | None = None,
141
+ nullable: bool = False,
142
+ immutable: bool = False,
143
+ sortable: bool = True,
144
+ ) -> t.Any:
145
+ """Factory function to create a StringBackedFieldSpec."""
146
+ field = StringBackedFieldSpec(
147
+ default=default,
148
+ default_factory=default_factory,
149
+ nullable=nullable,
150
+ immutable=immutable,
151
+ sortable=sortable,
152
+ )
153
+ field._field_type = _field_type
154
+ return field
155
+
156
+
157
+ class ListFieldSpec(FieldSpec[list[T]]):
158
+ """A field descriptor for list/collection values."""
159
+
160
+ def __init__(
161
+ self,
162
+ *,
163
+ default: list[T] | None = None,
164
+ default_factory: t.Callable[[], list[T]] | None = None,
165
+ nullable: bool = False,
166
+ immutable: bool = False,
167
+ sortable: bool = False,
168
+ ) -> None:
169
+ super().__init__(
170
+ default=default,
171
+ default_factory=default_factory,
172
+ nullable=nullable,
173
+ immutable=immutable,
174
+ sortable=sortable,
175
+ )
176
+
177
+
178
+ @t.overload
179
+ def ListField(
180
+ _item_type: type[T],
181
+ *,
182
+ default: list[T] | None = None,
183
+ default_factory: t.Callable[[], list[T]] | None = None,
184
+ nullable: bool = False,
185
+ immutable: bool = False,
186
+ sortable: bool = False,
187
+ ) -> ListFieldSpec[T]: ...
188
+ @t.overload
189
+ def ListField(
190
+ _item_type: None = None,
191
+ *,
192
+ default: list[t.Any] | None = None,
193
+ default_factory: t.Callable[[], list[t.Any]] | None = None,
194
+ nullable: bool = False,
195
+ immutable: bool = False,
196
+ sortable: bool = False,
197
+ ) -> ListFieldSpec[t.Any]: ...
198
+ def ListField(
199
+ _item_type: type[t.Any] | None = None,
200
+ *,
201
+ default: list[t.Any] | None = None,
202
+ default_factory: t.Callable[[], list[t.Any]] | None = None,
203
+ nullable: bool = False,
204
+ immutable: bool = False,
205
+ sortable: bool = False,
206
+ ) -> ListFieldSpec[t.Any]:
207
+ """Factory function to create a ListFieldSpec."""
208
+ field = ListFieldSpec(
209
+ default=default,
210
+ default_factory=default_factory,
211
+ nullable=nullable,
212
+ immutable=immutable,
213
+ sortable=sortable,
214
+ )
215
+ field._field_type = _item_type
216
+ return field
217
+
218
+
219
+ class ForeignFieldSpec(FieldSpec[T]):
220
+ """A field descriptor for foreign/complex type values."""
221
+
222
+ def __init__(
223
+ self,
224
+ *,
225
+ default: T | None = None,
226
+ default_factory: t.Callable[[], T] | None = None,
227
+ nullable: bool = False,
228
+ immutable: bool = False,
229
+ sortable: bool = False,
230
+ ) -> None:
231
+ super().__init__(
232
+ default=default,
233
+ default_factory=default_factory,
234
+ nullable=nullable,
235
+ immutable=immutable,
236
+ sortable=sortable,
237
+ )
238
+
239
+
240
+ @t.overload
241
+ def ForeignField(
242
+ _field_type: type[T],
243
+ *,
244
+ default: T | None = None,
245
+ default_factory: t.Callable[[], T] | None = None,
246
+ nullable: bool = False,
247
+ immutable: bool = False,
248
+ sortable: bool = False,
249
+ ) -> ForeignFieldSpec[T]: ...
250
+ @t.overload
251
+ def ForeignField(
252
+ _field_type: None = None,
253
+ *,
254
+ default: t.Any | None = None,
255
+ default_factory: t.Callable[[], t.Any] | None = None,
256
+ nullable: bool = False,
257
+ immutable: bool = False,
258
+ sortable: bool = False,
259
+ ) -> ForeignFieldSpec[t.Any]: ...
260
+ def ForeignField(
261
+ _field_type: type[t.Any] | None = None,
262
+ *,
263
+ default: t.Any | None = None,
264
+ default_factory: t.Callable[[], t.Any] | None = None,
265
+ nullable: bool = False,
266
+ immutable: bool = False,
267
+ sortable: bool = False,
268
+ ) -> t.Any:
269
+ """Factory function to create a ForeignFieldSpec."""
270
+ field = ForeignFieldSpec(
271
+ default=default,
272
+ default_factory=default_factory,
273
+ nullable=nullable,
274
+ immutable=immutable,
275
+ sortable=sortable,
276
+ )
277
+ field._field_type = _field_type
278
+ return field
279
+
280
+
281
+ # Keep all other field types as they were
282
+ class StringFieldSpec(FieldSpec[str]):
283
+ """A field descriptor for string values."""
284
+
285
+
286
+ def StringField(
287
+ *,
288
+ default: str | None = None,
289
+ default_factory: t.Callable[[], str] | None = None,
290
+ nullable: bool = False,
291
+ immutable: bool = False,
292
+ sortable: bool = False,
293
+ ) -> t.Any:
294
+ return StringFieldSpec(
295
+ default=default,
296
+ default_factory=default_factory,
297
+ nullable=nullable,
298
+ immutable=immutable,
299
+ sortable=sortable,
300
+ )
301
+
302
+
303
+ class IntegerFieldSpec(FieldSpec[int]):
304
+ """A field descriptor for integer values."""
305
+
306
+
307
+ def IntegerField(
308
+ *,
309
+ default: int | None = None,
310
+ default_factory: t.Callable[[], int] | None = None,
311
+ nullable: bool = False,
312
+ immutable: bool = False,
313
+ sortable: bool = True,
314
+ ) -> t.Any:
315
+ return IntegerFieldSpec(
316
+ default=default,
317
+ default_factory=default_factory,
318
+ nullable=nullable,
319
+ immutable=immutable,
320
+ sortable=sortable,
321
+ )
322
+
323
+
324
+ class FloatFieldSpec(FieldSpec[float]):
325
+ """A field descriptor for float values."""
326
+
327
+
328
+ def FloatField(
329
+ *,
330
+ default: float | None = None,
331
+ default_factory: t.Callable[[], float] | None = None,
332
+ nullable: bool = False,
333
+ immutable: bool = False,
334
+ sortable: bool = True,
335
+ ) -> t.Any:
336
+ return FloatFieldSpec(
337
+ default=default,
338
+ default_factory=default_factory,
339
+ nullable=nullable,
340
+ immutable=immutable,
341
+ sortable=sortable,
342
+ )
343
+
344
+
345
+ class BoolFieldSpec(FieldSpec[bool]):
346
+ """A field descriptor for boolean values."""
347
+
348
+
349
+ def BoolField(
350
+ *,
351
+ default: bool | None = None,
352
+ default_factory: t.Callable[[], bool] | None = None,
353
+ nullable: bool = False,
354
+ immutable: bool = False,
355
+ sortable: bool = True,
356
+ ) -> t.Any:
357
+ return BoolFieldSpec(
358
+ default=default,
359
+ default_factory=default_factory,
360
+ nullable=nullable,
361
+ immutable=immutable,
362
+ sortable=sortable,
363
+ )
364
+
365
+
366
+ class DateTimeFieldSpec(FieldSpec[datetime.datetime]):
367
+ """A field descriptor for datetime values."""
368
+
369
+
370
+ def DateTimeField(
371
+ *,
372
+ default: datetime.datetime | None = None,
373
+ default_factory: t.Callable[[], datetime.datetime] | None = None,
374
+ nullable: bool = False,
375
+ immutable: bool = False,
376
+ sortable: bool = True,
377
+ ) -> t.Any:
378
+ return DateTimeFieldSpec(
379
+ default=default,
380
+ default_factory=default_factory,
381
+ nullable=nullable,
382
+ immutable=immutable,
383
+ sortable=sortable,
384
+ )
385
+
386
+
387
+ def Field(
388
+ *,
389
+ default: t.Any | None = None,
390
+ default_factory: t.Callable[[], t.Any] | None = None,
391
+ nullable: bool = False,
392
+ immutable: bool = False,
393
+ sortable: bool = False,
394
+ ) -> t.Any:
395
+ return FieldSpec(
396
+ default=default,
397
+ default_factory=default_factory,
398
+ nullable=nullable,
399
+ immutable=immutable,
400
+ sortable=sortable,
401
+ )
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+
5
+ from audex import utils
6
+ from audex.entity import BaseEntity
7
+ from audex.entity import touch_after
8
+ from audex.entity.fields import DateTimeField
9
+ from audex.entity.fields import IntegerField
10
+ from audex.entity.fields import StringField
11
+
12
+
13
+ class Segment(BaseEntity):
14
+ """Segment entity representing a continuous audio recording segment.
15
+
16
+ Represents one continuous recording segment within a session. Since
17
+ recording can be stopped and restarted, a session may have multiple
18
+ segments. Each segment tracks its own timing, audio file location, and
19
+ sequence order within the session.
20
+
21
+ Attributes:
22
+ id: The unique identifier of the segment. Auto-generated with "segment-"
23
+ prefix.
24
+ session_id: The ID of the session this segment belongs to. Foreign key
25
+ reference to Session entity.
26
+ sequence: The sequence number of this segment within the session.
27
+ Starts from 1 and increments for each new segment.
28
+ audio_key: The audio file key/path in storage. Points to the raw
29
+ audio recording file.
30
+ started_at: Timestamp when this segment started recording.
31
+ ended_at: Timestamp when this segment stopped recording. None if
32
+ still recording.
33
+ duration_ms: Duration of the segment in milliseconds. Calculated when
34
+ recording stops.
35
+
36
+ Inherited Attributes:
37
+ created_at: Timestamp when the segment was created.
38
+ updated_at: Timestamp when the segment was last updated.
39
+
40
+ Example:
41
+ ```python
42
+ # Create first segment when starting recording
43
+ segment = Segment(
44
+ session_id="session-xyz789",
45
+ sequence=1,
46
+ audio_key="audio/session-xyz789/segment-001.wav",
47
+ started_at=utils.utcnow(),
48
+ )
49
+
50
+ # Stop recording and calculate duration
51
+ segment.ended_at = utils.utcnow()
52
+ segment.duration_ms = int(
53
+ (segment.ended_at - segment.started_at).total_seconds()
54
+ * 1000
55
+ )
56
+ segment.touch()
57
+
58
+ # Create second segment when resuming
59
+ segment2 = Segment(
60
+ session_id="session-xyz789",
61
+ sequence=2,
62
+ audio_key="audio/session-xyz789/segment-002.wav",
63
+ started_at=utils.utcnow(),
64
+ )
65
+ ```
66
+ """
67
+
68
+ id: str = StringField(default_factory=lambda: utils.gen_id(prefix="segment-"))
69
+ session_id: str = StringField()
70
+ sequence: int = IntegerField()
71
+ audio_key: str = StringField()
72
+ started_at: datetime.datetime = DateTimeField(default_factory=utils.utcnow)
73
+ ended_at: datetime.datetime | None = DateTimeField(nullable=True)
74
+ duration_ms: int | None = IntegerField(nullable=True)
75
+
76
+ @property
77
+ def is_recording(self) -> bool:
78
+ """Check if this segment is currently recording.
79
+
80
+ Returns:
81
+ True if ended_at is None, False otherwise.
82
+ """
83
+ return self.ended_at is None
84
+
85
+ @touch_after
86
+ def incr(self) -> None:
87
+ """Increment the sequence number of this segment by 1.
88
+
89
+ Note:
90
+ The updated_at timestamp is automatically updated.
91
+ """
92
+ self.sequence += 1
93
+
94
+ @touch_after
95
+ def decr(self) -> None:
96
+ """Decrement the sequence number of this segment by 1.
97
+
98
+ Note:
99
+ The updated_at timestamp is automatically updated.
100
+ """
101
+ if self.sequence > 1:
102
+ self.sequence -= 1
103
+ raise ValueError("Sequence number cannot be less than 1.")
104
+
105
+ @touch_after
106
+ def stop(self) -> None:
107
+ """Stop the recording of this segment by setting ended_at and
108
+ calculating duration_ms.
109
+
110
+ Note:
111
+ The updated_at timestamp is automatically updated.
112
+ """
113
+ if self.ended_at is None:
114
+ self.ended_at = utils.utcnow()
115
+ self.duration_ms = int((self.ended_at - self.started_at).total_seconds() * 1000)
@@ -0,0 +1,38 @@
1
+ # This file is auto-generated by PrototypeX stub generator.
2
+ # Do not edit manually - changes will be overwritten.
3
+ # Regenerate using: python -m scripts.genstubs gen
4
+
5
+ from __future__ import annotations
6
+
7
+ import datetime
8
+
9
+ from audex.entity import BaseEntity
10
+
11
+ class Segment(BaseEntity):
12
+ id: str
13
+ created_at: datetime.datetime
14
+ updated_at: datetime.datetime | None
15
+ session_id: str
16
+ sequence: int
17
+ audio_key: str
18
+ started_at: datetime.datetime
19
+ ended_at: datetime.datetime | None
20
+ duration_ms: int | None
21
+ def __init__(
22
+ self,
23
+ *,
24
+ id: str = ...,
25
+ created_at: datetime.datetime = ...,
26
+ updated_at: datetime.datetime | None = None,
27
+ session_id: str,
28
+ sequence: int,
29
+ audio_key: str,
30
+ started_at: datetime.datetime = ...,
31
+ ended_at: datetime.datetime | None = None,
32
+ duration_ms: int | None = None,
33
+ ) -> None: ...
34
+ @property
35
+ def is_recording(self) -> bool: ...
36
+ def decr(self) -> None: ...
37
+ def incr(self) -> None: ...
38
+ def stop(self) -> None: ...
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+
5
+ from audex import utils
6
+ from audex.entity import BaseEntity
7
+ from audex.entity import touch_after
8
+ from audex.entity.fields import DateTimeField
9
+ from audex.entity.fields import StringBackedField
10
+ from audex.entity.fields import StringField
11
+ from audex.valueobj.session import SessionStatus
12
+
13
+
14
+ class Session(BaseEntity):
15
+ """Session entity representing a doctor-patient conversation
16
+ session.
17
+
18
+ Represents a single recording session where a doctor converses with a
19
+ patient. A session can be started and stopped multiple times, creating
20
+ multiple audio segments. Each session belongs to a specific doctor and
21
+ tracks the overall session status and timing.
22
+
23
+ Attributes:
24
+ id: The unique identifier of the session. Auto-generated with "session-"
25
+ prefix.
26
+ doctor_id: The ID of the doctor who owns this session. Foreign key
27
+ reference to Doctor entity.
28
+ patient_name: The name of the patient in this session. Optional field
29
+ for record keeping.
30
+ clinic_number: Outpatient clinic number. Optional.
31
+ medical_record_number: Medical record/case number. Optional.
32
+ diagnosis: Preliminary or final diagnosis. Optional.
33
+ status: The current status of the session (DRAFT, IN_PROGRESS, COMPLETED,
34
+ CANCELLED). Defaults to DRAFT.
35
+ started_at: Timestamp when the session first started recording. None
36
+ if not yet started.
37
+ ended_at: Timestamp when the session was completed or cancelled. None
38
+ if still in progress or draft.
39
+ notes: Additional notes about the session. Optional field for doctor's
40
+ remarks.
41
+
42
+ Inherited Attributes:
43
+ created_at: Timestamp when the session was created.
44
+ updated_at: Timestamp when the session was last updated.
45
+
46
+ Example:
47
+ ```python
48
+ # Create new session
49
+ session = Session(
50
+ doctor_id="doctor-abc123",
51
+ patient_name="李女士",
52
+ clinic_number="20250123-001",
53
+ medical_record_number="MR-2025-001",
54
+ diagnosis="上呼吸道感染",
55
+ notes="初诊",
56
+ )
57
+
58
+ # Start recording
59
+ session.start()
60
+ print(session.status) # SessionStatus.IN_PROGRESS
61
+
62
+ # Complete session
63
+ session.complete()
64
+ print(session.status) # SessionStatus.COMPLETED
65
+ ```
66
+ """
67
+
68
+ id: str = StringField(default_factory=lambda: utils.gen_id(prefix="session-"))
69
+ doctor_id: str = StringField()
70
+ patient_name: str | None = StringField(nullable=True)
71
+ clinic_number: str | None = StringField(nullable=True)
72
+ medical_record_number: str | None = StringField(nullable=True)
73
+ diagnosis: str | None = StringField(nullable=True)
74
+ status: SessionStatus = StringBackedField(SessionStatus, default=SessionStatus.DRAFT)
75
+ started_at: datetime.datetime | None = DateTimeField(nullable=True)
76
+ ended_at: datetime.datetime | None = DateTimeField(nullable=True)
77
+ notes: str | None = StringField(nullable=True)
78
+
79
+ @property
80
+ def is_active(self) -> bool:
81
+ """Check if the session is currently active (in progress).
82
+
83
+ Returns:
84
+ True if status is IN_PROGRESS, False otherwise.
85
+ """
86
+ return self.status == SessionStatus.IN_PROGRESS
87
+
88
+ @property
89
+ def is_finished(self) -> bool:
90
+ """Check if the session is finished (completed or cancelled).
91
+
92
+ Returns:
93
+ True if status is COMPLETED or CANCELLED, False otherwise.
94
+ """
95
+ return self.status in (SessionStatus.COMPLETED, SessionStatus.CANCELLED)
96
+
97
+ @touch_after
98
+ def start(self) -> None:
99
+ """Start the session recording.
100
+
101
+ Sets status to IN_PROGRESS and records the start timestamp if this
102
+ is the first time starting.
103
+
104
+ Note:
105
+ The updated_at timestamp is automatically updated.
106
+ """
107
+ if self.status == SessionStatus.DRAFT and self.started_at is None:
108
+ self.started_at = utils.utcnow()
109
+ self.status = SessionStatus.IN_PROGRESS
110
+
111
+ @touch_after
112
+ def complete(self) -> None:
113
+ """Complete the session.
114
+
115
+ Sets status to COMPLETED and records the end timestamp.
116
+
117
+ Note:
118
+ The updated_at timestamp is automatically updated.
119
+ """
120
+ self.status = SessionStatus.COMPLETED
121
+ self.ended_at = utils.utcnow()
122
+
123
+ @touch_after
124
+ def cancel(self) -> None:
125
+ """Cancel the session.
126
+
127
+ Sets status to CANCELLED and records the end timestamp.
128
+
129
+ Note:
130
+ The updated_at timestamp is automatically updated.
131
+ """
132
+ self.status = SessionStatus.CANCELLED
133
+ self.ended_at = utils.utcnow()