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
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
|
+
)
|
audex/entity/segment.py
ADDED
|
@@ -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)
|
audex/entity/segment.pyi
ADDED
|
@@ -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: ...
|
audex/entity/session.py
ADDED
|
@@ -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()
|