audex 1.0.7a3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. audex/__init__.py +9 -0
  2. audex/__main__.py +7 -0
  3. audex/cli/__init__.py +189 -0
  4. audex/cli/apis/__init__.py +12 -0
  5. audex/cli/apis/init/__init__.py +34 -0
  6. audex/cli/apis/init/gencfg.py +130 -0
  7. audex/cli/apis/init/setup.py +330 -0
  8. audex/cli/apis/init/vprgroup.py +125 -0
  9. audex/cli/apis/serve.py +141 -0
  10. audex/cli/args.py +356 -0
  11. audex/cli/exceptions.py +44 -0
  12. audex/cli/helper/__init__.py +0 -0
  13. audex/cli/helper/ansi.py +193 -0
  14. audex/cli/helper/display.py +288 -0
  15. audex/config/__init__.py +64 -0
  16. audex/config/core/__init__.py +30 -0
  17. audex/config/core/app.py +29 -0
  18. audex/config/core/audio.py +45 -0
  19. audex/config/core/logging.py +163 -0
  20. audex/config/core/session.py +11 -0
  21. audex/config/helper/__init__.py +1 -0
  22. audex/config/helper/client/__init__.py +1 -0
  23. audex/config/helper/client/http.py +28 -0
  24. audex/config/helper/client/websocket.py +21 -0
  25. audex/config/helper/provider/__init__.py +1 -0
  26. audex/config/helper/provider/dashscope.py +13 -0
  27. audex/config/helper/provider/unisound.py +18 -0
  28. audex/config/helper/provider/xfyun.py +23 -0
  29. audex/config/infrastructure/__init__.py +31 -0
  30. audex/config/infrastructure/cache.py +51 -0
  31. audex/config/infrastructure/database.py +48 -0
  32. audex/config/infrastructure/recorder.py +32 -0
  33. audex/config/infrastructure/store.py +19 -0
  34. audex/config/provider/__init__.py +18 -0
  35. audex/config/provider/transcription.py +109 -0
  36. audex/config/provider/vpr.py +99 -0
  37. audex/container.py +40 -0
  38. audex/entity/__init__.py +468 -0
  39. audex/entity/doctor.py +109 -0
  40. audex/entity/doctor.pyi +51 -0
  41. audex/entity/fields.py +401 -0
  42. audex/entity/segment.py +115 -0
  43. audex/entity/segment.pyi +38 -0
  44. audex/entity/session.py +133 -0
  45. audex/entity/session.pyi +47 -0
  46. audex/entity/utterance.py +142 -0
  47. audex/entity/utterance.pyi +48 -0
  48. audex/entity/vp.py +68 -0
  49. audex/entity/vp.pyi +35 -0
  50. audex/exceptions.py +157 -0
  51. audex/filters/__init__.py +692 -0
  52. audex/filters/generated/__init__.py +21 -0
  53. audex/filters/generated/doctor.py +987 -0
  54. audex/filters/generated/segment.py +723 -0
  55. audex/filters/generated/session.py +978 -0
  56. audex/filters/generated/utterance.py +939 -0
  57. audex/filters/generated/vp.py +815 -0
  58. audex/helper/__init__.py +1 -0
  59. audex/helper/hash.py +33 -0
  60. audex/helper/mixin.py +65 -0
  61. audex/helper/net.py +19 -0
  62. audex/helper/settings/__init__.py +830 -0
  63. audex/helper/settings/fields.py +317 -0
  64. audex/helper/stream.py +153 -0
  65. audex/injectors/__init__.py +1 -0
  66. audex/injectors/config.py +12 -0
  67. audex/injectors/lifespan.py +7 -0
  68. audex/lib/__init__.py +1 -0
  69. audex/lib/cache/__init__.py +383 -0
  70. audex/lib/cache/inmemory.py +513 -0
  71. audex/lib/database/__init__.py +83 -0
  72. audex/lib/database/sqlite.py +406 -0
  73. audex/lib/exporter.py +189 -0
  74. audex/lib/injectors/__init__.py +1 -0
  75. audex/lib/injectors/cache.py +25 -0
  76. audex/lib/injectors/container.py +47 -0
  77. audex/lib/injectors/exporter.py +26 -0
  78. audex/lib/injectors/recorder.py +33 -0
  79. audex/lib/injectors/server.py +17 -0
  80. audex/lib/injectors/session.py +18 -0
  81. audex/lib/injectors/sqlite.py +24 -0
  82. audex/lib/injectors/store.py +13 -0
  83. audex/lib/injectors/transcription.py +42 -0
  84. audex/lib/injectors/usb.py +12 -0
  85. audex/lib/injectors/vpr.py +65 -0
  86. audex/lib/injectors/wifi.py +7 -0
  87. audex/lib/recorder.py +844 -0
  88. audex/lib/repos/__init__.py +149 -0
  89. audex/lib/repos/container.py +23 -0
  90. audex/lib/repos/database/__init__.py +1 -0
  91. audex/lib/repos/database/sqlite.py +672 -0
  92. audex/lib/repos/decorators.py +74 -0
  93. audex/lib/repos/doctor.py +286 -0
  94. audex/lib/repos/segment.py +302 -0
  95. audex/lib/repos/session.py +285 -0
  96. audex/lib/repos/tables/__init__.py +70 -0
  97. audex/lib/repos/tables/doctor.py +137 -0
  98. audex/lib/repos/tables/segment.py +113 -0
  99. audex/lib/repos/tables/session.py +140 -0
  100. audex/lib/repos/tables/utterance.py +131 -0
  101. audex/lib/repos/tables/vp.py +102 -0
  102. audex/lib/repos/utterance.py +288 -0
  103. audex/lib/repos/vp.py +286 -0
  104. audex/lib/restful.py +251 -0
  105. audex/lib/server/__init__.py +97 -0
  106. audex/lib/server/auth.py +98 -0
  107. audex/lib/server/handlers.py +248 -0
  108. audex/lib/server/templates/index.html.j2 +226 -0
  109. audex/lib/server/templates/login.html.j2 +111 -0
  110. audex/lib/server/templates/static/script.js +68 -0
  111. audex/lib/server/templates/static/style.css +579 -0
  112. audex/lib/server/types.py +123 -0
  113. audex/lib/session.py +503 -0
  114. audex/lib/store/__init__.py +238 -0
  115. audex/lib/store/localfile.py +411 -0
  116. audex/lib/transcription/__init__.py +33 -0
  117. audex/lib/transcription/dashscope.py +525 -0
  118. audex/lib/transcription/events.py +62 -0
  119. audex/lib/usb.py +554 -0
  120. audex/lib/vpr/__init__.py +38 -0
  121. audex/lib/vpr/unisound/__init__.py +185 -0
  122. audex/lib/vpr/unisound/types.py +469 -0
  123. audex/lib/vpr/xfyun/__init__.py +483 -0
  124. audex/lib/vpr/xfyun/types.py +679 -0
  125. audex/lib/websocket/__init__.py +8 -0
  126. audex/lib/websocket/connection.py +485 -0
  127. audex/lib/websocket/pool.py +991 -0
  128. audex/lib/wifi.py +1146 -0
  129. audex/lifespan.py +75 -0
  130. audex/service/__init__.py +27 -0
  131. audex/service/decorators.py +73 -0
  132. audex/service/doctor/__init__.py +652 -0
  133. audex/service/doctor/const.py +36 -0
  134. audex/service/doctor/exceptions.py +96 -0
  135. audex/service/doctor/types.py +54 -0
  136. audex/service/export/__init__.py +236 -0
  137. audex/service/export/const.py +17 -0
  138. audex/service/export/exceptions.py +34 -0
  139. audex/service/export/types.py +21 -0
  140. audex/service/injectors/__init__.py +1 -0
  141. audex/service/injectors/container.py +53 -0
  142. audex/service/injectors/doctor.py +34 -0
  143. audex/service/injectors/export.py +27 -0
  144. audex/service/injectors/session.py +49 -0
  145. audex/service/session/__init__.py +754 -0
  146. audex/service/session/const.py +34 -0
  147. audex/service/session/exceptions.py +67 -0
  148. audex/service/session/types.py +91 -0
  149. audex/types.py +39 -0
  150. audex/utils.py +287 -0
  151. audex/valueobj/__init__.py +81 -0
  152. audex/valueobj/common/__init__.py +1 -0
  153. audex/valueobj/common/auth.py +84 -0
  154. audex/valueobj/common/email.py +16 -0
  155. audex/valueobj/common/ops.py +22 -0
  156. audex/valueobj/common/phone.py +84 -0
  157. audex/valueobj/common/version.py +72 -0
  158. audex/valueobj/session.py +19 -0
  159. audex/valueobj/utterance.py +15 -0
  160. audex/view/__init__.py +51 -0
  161. audex/view/container.py +17 -0
  162. audex/view/decorators.py +303 -0
  163. audex/view/pages/__init__.py +1 -0
  164. audex/view/pages/dashboard/__init__.py +286 -0
  165. audex/view/pages/dashboard/wifi.py +407 -0
  166. audex/view/pages/login.py +110 -0
  167. audex/view/pages/recording.py +348 -0
  168. audex/view/pages/register.py +202 -0
  169. audex/view/pages/sessions/__init__.py +196 -0
  170. audex/view/pages/sessions/details.py +224 -0
  171. audex/view/pages/sessions/export.py +443 -0
  172. audex/view/pages/settings.py +374 -0
  173. audex/view/pages/voiceprint/__init__.py +1 -0
  174. audex/view/pages/voiceprint/enroll.py +195 -0
  175. audex/view/pages/voiceprint/update.py +195 -0
  176. audex/view/static/css/dashboard.css +452 -0
  177. audex/view/static/css/glass.css +22 -0
  178. audex/view/static/css/global.css +541 -0
  179. audex/view/static/css/login.css +386 -0
  180. audex/view/static/css/recording.css +439 -0
  181. audex/view/static/css/register.css +293 -0
  182. audex/view/static/css/sessions/styles.css +501 -0
  183. audex/view/static/css/settings.css +186 -0
  184. audex/view/static/css/voiceprint/enroll.css +43 -0
  185. audex/view/static/css/voiceprint/styles.css +209 -0
  186. audex/view/static/css/voiceprint/update.css +44 -0
  187. audex/view/static/images/logo.svg +95 -0
  188. audex/view/static/js/recording.js +42 -0
  189. audex-1.0.7a3.dist-info/METADATA +361 -0
  190. audex-1.0.7a3.dist-info/RECORD +192 -0
  191. audex-1.0.7a3.dist-info/WHEEL +4 -0
  192. audex-1.0.7a3.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,330 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import getpass
5
+ import pathlib
6
+
7
+ from pydantic import Field
8
+
9
+ from audex.cli.args import BaseArgs
10
+ from audex.cli.exceptions import InvalidArgumentError
11
+ from audex.cli.helper import display
12
+ from audex.config import Config
13
+ from audex.utils import Unset
14
+
15
+
16
+ class Args(BaseArgs):
17
+ config: pathlib.Path = Field(
18
+ ...,
19
+ alias="c",
20
+ description="Path to the input configuration file.",
21
+ )
22
+
23
+ output: pathlib.Path = Field(
24
+ ...,
25
+ alias="o",
26
+ description="Path to save the configured file.",
27
+ )
28
+
29
+ def run(self) -> None:
30
+ display.banner("Audex Setup Wizard", subtitle="Interactive Configuration Setup")
31
+
32
+ # Load input configuration
33
+ with display.section("Loading Configuration"):
34
+ if not self.config.exists():
35
+ raise InvalidArgumentError(
36
+ arg="config",
37
+ value=str(self.config),
38
+ reason=f"Configuration file not found: {self.config}",
39
+ )
40
+
41
+ display.info(f"Loading from: {self.config}")
42
+
43
+ if self.config.suffix in {".yaml", ".yml"}:
44
+ cfg = Config.from_yaml(self.config)
45
+ elif self.config.suffix in {".json", ".jsonc", ".json5"}:
46
+ cfg = Config.from_json(self.config)
47
+ else:
48
+ raise InvalidArgumentError(
49
+ arg="config",
50
+ value=str(self.config),
51
+ reason=f"Unsupported config format: {self.config.suffix}",
52
+ )
53
+
54
+ display.success("Configuration loaded successfully")
55
+
56
+ # Configure Transcription Provider
57
+ cfg = self._config_transcription(cfg)
58
+
59
+ # Configure VPR Provider
60
+ cfg = self._config_vpr(cfg)
61
+
62
+ # Save configuration
63
+ with display.section("Saving Configuration"):
64
+ display.info(f"Output: {self.output}")
65
+
66
+ if self.output.exists():
67
+ if display.confirm(f"File already exists.Overwrite {self.output}?", default=False):
68
+ import datetime
69
+
70
+ backup_path = self.output.with_suffix(
71
+ f".backup.{datetime.datetime.now():%Y%m%d_%H%M%S}.yml"
72
+ )
73
+ display.info(f"Backing up to: {backup_path}")
74
+ self.output.rename(backup_path)
75
+ else:
76
+ display.warning("Operation cancelled")
77
+ return
78
+
79
+ self.output.parent.mkdir(parents=True, exist_ok=True)
80
+
81
+ with display.loading("Writing configuration..."):
82
+ cfg.to_yaml(self.output, exclude_unset=True, exclude_none=True, with_comments=False)
83
+
84
+ display.success("Configuration saved successfully")
85
+ display.path(self.output, label="Saved to", exists=self.output.exists())
86
+
87
+ # Initialize VPR Group
88
+ if display.confirm("Initialize VPR Group now?", default=True):
89
+ self._init_vpr_group(cfg, self.output)
90
+
91
+ # Show summary
92
+ self._show_summary(cfg, self.output)
93
+
94
+ def _config_transcription(self, cfg: Config) -> Config:
95
+ """Configure transcription provider."""
96
+ with display.section("Transcription Provider Configuration"):
97
+ current_provider = cfg.provider.transcription.provider
98
+ display.info(f"Current provider: {current_provider}")
99
+
100
+ # Check if Dashscope API key is set
101
+ dashscope_key = cfg.provider.transcription.dashscope.credential.api_key
102
+ is_unset = dashscope_key in ("<UNSET>", None, "") or isinstance(dashscope_key, Unset)
103
+
104
+ if is_unset:
105
+ display.warning("Dashscope API Key is NOT configured")
106
+ api_key = self._prompt_secret("Enter Dashscope API Key")
107
+ cfg.provider.transcription.dashscope.credential.api_key = api_key
108
+ display.success("Dashscope API Key updated")
109
+ else:
110
+ display.success("Dashscope API Key is configured")
111
+ display.info(f"Current key: {self._mask_secret(str(dashscope_key))}")
112
+ if display.confirm("Update Dashscope API Key?", default=False):
113
+ api_key = self._prompt_secret("Enter new Dashscope API Key")
114
+ cfg.provider.transcription.dashscope.credential.api_key = api_key
115
+ display.success("Dashscope API Key updated")
116
+
117
+ return cfg
118
+
119
+ def _config_vpr(self, cfg: Config) -> Config:
120
+ """Configure VPR provider."""
121
+ with display.section("VPR Provider Configuration"):
122
+ current_provider = cfg.provider.vpr.provider
123
+ display.info(f"Current provider: {current_provider}")
124
+
125
+ # Ask if user wants to change provider
126
+ if not display.confirm(f"Keep VPR provider as '{current_provider}'?", default=True):
127
+ print("\nAvailable VPR Providers:")
128
+ print(" 1) xfyun")
129
+ print(" 2) unisound")
130
+
131
+ choice = input("\nSelect provider [1]: ").strip() or "1"
132
+
133
+ if choice == "1":
134
+ selected_provider = "xfyun"
135
+ elif choice == "2":
136
+ selected_provider = "unisound"
137
+ else:
138
+ display.warning("Invalid choice, keeping current provider")
139
+ selected_provider = current_provider
140
+
141
+ cfg.provider.vpr.provider = selected_provider # type: ignore
142
+ display.success(f"VPR provider set to: {selected_provider}")
143
+ else:
144
+ selected_provider = current_provider
145
+
146
+ # Configure credentials for selected provider
147
+ if selected_provider == "xfyun":
148
+ cfg = self._config_xfyun(cfg)
149
+ elif selected_provider == "unisound":
150
+ cfg = self._config_unisound(cfg)
151
+
152
+ return cfg
153
+
154
+ def _config_xfyun(self, cfg: Config) -> Config:
155
+ """Configure XFYun credentials."""
156
+ print("\n--- XFYun Configuration ---")
157
+
158
+ xfyun_cfg = cfg.provider.vpr.xfyun.credential
159
+
160
+ # App ID
161
+ current_app_id = xfyun_cfg.app_id
162
+ is_unset = current_app_id in ("<UNSET>", None, "") or isinstance(current_app_id, Unset)
163
+
164
+ if is_unset:
165
+ display.warning("XFYun App ID is NOT configured")
166
+ app_id = input("Enter XFYun App ID: ").strip()
167
+ cfg.provider.vpr.xfyun.credential.app_id = app_id
168
+ display.success("XFYun App ID updated")
169
+ else:
170
+ display.info(f"Current App ID: {current_app_id}")
171
+ if display.confirm("Update XFYun App ID?", default=False):
172
+ app_id = input("Enter XFYun App ID: ").strip()
173
+ cfg.provider.vpr.xfyun.credential.app_id = app_id
174
+ display.success("XFYun App ID updated")
175
+
176
+ # API Key
177
+ current_api_key = xfyun_cfg.api_key
178
+ is_unset = current_api_key in ("<UNSET>", None, "") or isinstance(current_api_key, Unset)
179
+
180
+ if is_unset:
181
+ display.warning("XFYun API Key is NOT configured")
182
+ api_key = self._prompt_secret("Enter XFYun API Key")
183
+ cfg.provider.vpr.xfyun.credential.api_key = api_key
184
+ display.success("XFYun API Key updated")
185
+ else:
186
+ display.info(f"Current API Key: {self._mask_secret(str(current_api_key))}")
187
+ if display.confirm("Update XFYun API Key?", default=False):
188
+ api_key = self._prompt_secret("Enter XFYun API Key")
189
+ cfg.provider.vpr.xfyun.credential.api_key = api_key
190
+ display.success("XFYun API Key updated")
191
+
192
+ # API Secret
193
+ current_secret = xfyun_cfg.api_secret
194
+ is_unset = current_secret in ("<UNSET>", None, "") or isinstance(current_secret, Unset)
195
+
196
+ if is_unset:
197
+ display.warning("XFYun API Secret is NOT configured")
198
+ api_secret = self._prompt_secret("Enter XFYun API Secret")
199
+ cfg.provider.vpr.xfyun.credential.api_secret = api_secret
200
+ display.success("XFYun API Secret updated")
201
+ else:
202
+ display.info(f"Current API Secret: {self._mask_secret(str(current_secret))}")
203
+ if display.confirm("Update XFYun API Secret?", default=False):
204
+ api_secret = self._prompt_secret("Enter XFYun API Secret")
205
+ cfg.provider.vpr.xfyun.credential.api_secret = api_secret
206
+ display.success("XFYun API Secret updated")
207
+
208
+ return cfg
209
+
210
+ def _config_unisound(self, cfg: Config) -> Config:
211
+ """Configure Unisound credentials."""
212
+ print("\n--- Unisound Configuration ---")
213
+
214
+ unisound_cfg = cfg.provider.vpr.unisound.credential
215
+
216
+ # AppKey
217
+ current_appkey = unisound_cfg.appkey
218
+ is_unset = current_appkey in ("<UNSET>", None, "") or isinstance(current_appkey, Unset)
219
+
220
+ if is_unset:
221
+ display.warning("Unisound AppKey is NOT configured")
222
+ appkey = input("Enter Unisound AppKey: ").strip()
223
+ cfg.provider.vpr.unisound.credential.appkey = appkey
224
+ display.success("Unisound AppKey updated")
225
+ else:
226
+ display.info(f"Current AppKey: {current_appkey}")
227
+ if display.confirm("Update Unisound AppKey?", default=False):
228
+ appkey = input("Enter Unisound AppKey: ").strip()
229
+ cfg.provider.vpr.unisound.credential.appkey = appkey
230
+ display.success("Unisound AppKey updated")
231
+
232
+ # Secret
233
+ current_secret = unisound_cfg.secret
234
+ is_unset = current_secret in ("<UNSET>", None, "") or isinstance(current_secret, Unset)
235
+
236
+ if is_unset:
237
+ display.warning("Unisound Secret is NOT configured")
238
+ secret = self._prompt_secret("Enter Unisound Secret")
239
+ cfg.provider.vpr.unisound.credential.secret = secret
240
+ display.success("Unisound Secret updated")
241
+ else:
242
+ display.info(f"Current Secret: {self._mask_secret(str(current_secret))}")
243
+ if display.confirm("Update Unisound Secret?", default=False):
244
+ secret = self._prompt_secret("Enter Unisound Secret")
245
+ cfg.provider.vpr.unisound.credential.secret = secret
246
+ display.success("Unisound Secret updated")
247
+
248
+ return cfg
249
+
250
+ def _init_vpr_group(self, cfg: Config, config_path: pathlib.Path) -> None:
251
+ """Initialize VPR group."""
252
+ display.step("Initializing VPR Group", step=1)
253
+
254
+ from audex.lib.injectors.container import InfrastructureContainer
255
+
256
+ infra_container = InfrastructureContainer(config=cfg)
257
+ vpr = infra_container.vpr()
258
+
259
+ async def create_group() -> str:
260
+ from audex.lib.vpr import GroupAlreadyExistsError
261
+
262
+ async with vpr:
263
+ try:
264
+ return await vpr.create_group(None)
265
+ except GroupAlreadyExistsError:
266
+ display.warning("VPR Group already exists, skipping creation")
267
+ # Try to read existing group ID
268
+ if cfg.provider.vpr.provider == "xfyun":
269
+ gid_path = pathlib.Path(cfg.provider.vpr.xfyun.group_id_path)
270
+ else:
271
+ gid_path = pathlib.Path(cfg.provider.vpr.unisound.group_id_path)
272
+
273
+ if gid_path.exists():
274
+ return gid_path.read_text().strip()
275
+ raise
276
+
277
+ try:
278
+ with display.loading("Creating VPR group..."):
279
+ group_id = asyncio.run(create_group())
280
+
281
+ display.success(f"VPR Group ID: {group_id}")
282
+
283
+ # Save group ID
284
+ if cfg.provider.vpr.provider == "xfyun":
285
+ gid_path = pathlib.Path(cfg.provider.vpr.xfyun.group_id_path)
286
+ else:
287
+ gid_path = pathlib.Path(cfg.provider.vpr.unisound.group_id_path)
288
+
289
+ gid_path.parent.mkdir(parents=True, exist_ok=True)
290
+ gid_path.write_text(group_id)
291
+
292
+ display.success(f"Group ID saved to: {gid_path}")
293
+
294
+ except Exception as e:
295
+ display.warning(f"Failed to initialize VPR group: {e}")
296
+ display.info("You can manually initialize later with:")
297
+ display.info(f" python3 -m audex init vprgroup --config {config_path}")
298
+
299
+ def _show_summary(self, cfg: Config, output_path: pathlib.Path) -> None:
300
+ """Show configuration summary."""
301
+ print()
302
+ display.header("Setup Summary")
303
+
304
+ summary_data = {
305
+ "Config File": str(output_path),
306
+ "Transcription Provider": cfg.provider.transcription.provider,
307
+ "VPR Provider": cfg.provider.vpr.provider,
308
+ }
309
+
310
+ display.key_value(summary_data)
311
+
312
+ print()
313
+ display.success("Setup completed successfully! 🎉")
314
+ print()
315
+ display.info("Next steps:")
316
+ print(" • Run Audex: audex")
317
+ print(f" • Edit config: {output_path}")
318
+ print()
319
+
320
+ @staticmethod
321
+ def _prompt_secret(prompt: str) -> str:
322
+ """Prompt for secret input (hidden)."""
323
+ return getpass.getpass(f"{prompt}: ")
324
+
325
+ @staticmethod
326
+ def _mask_secret(secret: str) -> str:
327
+ """Mask a secret for display."""
328
+ if not secret or len(secret) < 8:
329
+ return "***"
330
+ return f"{secret[:4]}...{secret[-4:]}"
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import pathlib
5
+
6
+ from pydantic import Field
7
+
8
+ from audex.cli.args import BaseArgs
9
+ from audex.cli.exceptions import IllegalOperationError
10
+ from audex.cli.exceptions import InvalidArgumentError
11
+ from audex.cli.helper import display
12
+ from audex.config import Config
13
+ from audex.config import build_config
14
+ from audex.config import setconfig
15
+
16
+
17
+ class Args(BaseArgs):
18
+ config: pathlib.Path = Field(
19
+ ...,
20
+ alias="c",
21
+ description="Path to the configuration file.",
22
+ )
23
+
24
+ name: str | None = Field(
25
+ default=None,
26
+ alias="n",
27
+ description="Name of the VPR group to create.",
28
+ )
29
+
30
+ def run(self) -> None:
31
+ display.banner("VPR Initialization", subtitle="Initialize Voice Print Recognition Group")
32
+
33
+ # Load configuration
34
+ with display.section("Loading Configuration"):
35
+ if self.config:
36
+ display.info(f"Loading config from: {self.config}")
37
+
38
+ if self.config.suffix in {".yaml", ".yml"}:
39
+ setconfig(Config.from_yaml(self.config))
40
+ else:
41
+ raise InvalidArgumentError(
42
+ arg="config",
43
+ value=self.config,
44
+ reason="Unsupported config file format: "
45
+ f"{self.config.suffix}. Supported formats are . yaml, .yml, "
46
+ f".json, . jsonc, .json5",
47
+ )
48
+ display.success("Configuration loaded successfully")
49
+ else:
50
+ display.info("Using default configuration")
51
+
52
+ cfg = build_config()
53
+
54
+ # Show VPR provider info
55
+ with display.section("VPR Provider Information"):
56
+ provider_info: dict[str, str] = {
57
+ "Provider": str(cfg.provider.vpr.provider),
58
+ "Group Name": self.name or "(auto-generated)",
59
+ }
60
+
61
+ if cfg.provider.vpr.provider == "xfyun":
62
+ provider_info["Group ID Path"] = str(cfg.provider.vpr.xfyun.group_id_path)
63
+ elif cfg.provider.vpr.provider == "unisound":
64
+ provider_info["Group ID Path"] = str(cfg.provider.vpr.unisound.group_id_path)
65
+
66
+ display.key_value(provider_info)
67
+
68
+ # Confirm before proceeding
69
+ if not display.confirm("Proceed with VPR group creation?", default=True):
70
+ display.warning("Operation cancelled by user")
71
+ return
72
+
73
+ # Initialize infrastructure
74
+ from audex.lib.injectors.container import InfrastructureContainer
75
+
76
+ display.step("Initializing VPR infrastructure", step=1)
77
+ infra_container = InfrastructureContainer(config=cfg)
78
+ vpr = infra_container.vpr()
79
+
80
+ # Create VPR group
81
+ async def create_group(name: str | None) -> str:
82
+ from audex.lib.vpr import GroupAlreadyExistsError
83
+
84
+ async with vpr:
85
+ try:
86
+ return await vpr.create_group(name)
87
+ except GroupAlreadyExistsError as e:
88
+ raise IllegalOperationError(operation="Create VPR Group", reason=str(e)) from e
89
+
90
+ with display.loading("Creating VPR group..."):
91
+ group_id = asyncio.run(create_group(self.name))
92
+
93
+ display.success(f"VPR group created successfully: {group_id}")
94
+
95
+ # Save group ID to file
96
+ display.step("Saving group ID to file", step=2)
97
+
98
+ group_id_path: pathlib.Path | None = None
99
+
100
+ if cfg.provider.vpr.provider == "xfyun":
101
+ group_id_path = pathlib.Path(cfg.provider.vpr.xfyun.group_id_path)
102
+ elif cfg.provider.vpr.provider == "unisound":
103
+ group_id_path = pathlib.Path(cfg.provider.vpr.unisound.group_id_path)
104
+
105
+ if group_id_path:
106
+ with display.loading(f"Writing to {group_id_path}... "):
107
+ group_id_path.parent.mkdir(parents=True, exist_ok=True)
108
+ group_id_path.write_text(group_id)
109
+
110
+ display.success(f"Group ID saved to: {group_id_path}")
111
+ display.path(group_id_path, label="File", exists=group_id_path.exists())
112
+
113
+ # Summary
114
+ print()
115
+ display.header("Initialization Summary")
116
+ summary_data = {
117
+ "Provider": cfg.provider.vpr.provider,
118
+ "Group ID": group_id,
119
+ "Group Name": self.name or "(auto-generated)",
120
+ "Saved To": str(group_id_path) if group_id_path else "N/A",
121
+ }
122
+ display.key_value(summary_data)
123
+
124
+ print()
125
+ display.success("VPR initialization completed successfully! 🎉")
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import pathlib
6
+ import signal
7
+ import typing as t
8
+
9
+ from pydantic import Field
10
+
11
+ from audex.cli.args import BaseArgs
12
+ from audex.cli.exceptions import InvalidArgumentError
13
+ from audex.cli.helper import display
14
+ from audex.config import Config
15
+ from audex.config import build_config
16
+ from audex.config import setconfig
17
+
18
+
19
+ class Args(BaseArgs):
20
+ config: pathlib.Path | None = Field(
21
+ default=None,
22
+ alias="c",
23
+ description="Path to the configuration file.",
24
+ )
25
+
26
+ host: str = Field(
27
+ default="0. 0.0.0",
28
+ description="Host address to bind the server.",
29
+ )
30
+
31
+ port: int = Field(
32
+ default=8000,
33
+ description="Port number to bind the server.",
34
+ )
35
+
36
+ def run(self) -> None:
37
+ display.banner("Audex Export Server", subtitle="HTTP Export Service for Session Data")
38
+
39
+ # Load configuration
40
+ with display.section("Loading Configuration"):
41
+ if self.config:
42
+ display.info(f"Loading config from: {self.config}")
43
+ display.path(self.config, exists=self.config.exists())
44
+
45
+ if self.config.suffix in {".yaml", ".yml"}:
46
+ setconfig(Config.from_yaml(self.config))
47
+ display.success("YAML configuration loaded")
48
+ else:
49
+ raise InvalidArgumentError(
50
+ arg="config",
51
+ value=self.config,
52
+ reason="Unsupported config file format: "
53
+ f"{self.config.suffix}. Supported formats are .yaml, . yml, "
54
+ f". json, .jsonc, .json5",
55
+ )
56
+ else:
57
+ display.info("Using default configuration")
58
+
59
+ # Show server configuration
60
+ with display.section("Server Configuration"):
61
+ server_info = {
62
+ "Host": self.host,
63
+ "Port": self.port,
64
+ "URL": f"http://{self.host}:{self.port}",
65
+ }
66
+ display.key_value(server_info)
67
+
68
+ # Initialize infrastructure
69
+ display.step("Initializing infrastructure", step=1)
70
+ cfg = build_config()
71
+
72
+ with display.loading("Setting up database and server... "):
73
+ from audex.lib.injectors.container import InfrastructureContainer
74
+
75
+ infra_container = InfrastructureContainer(config=cfg)
76
+ sqlite = infra_container.sqlite()
77
+ server = infra_container.server()
78
+
79
+ display.success("Infrastructure initialized")
80
+
81
+ # Show available endpoints
82
+ display.step("Starting HTTP server", step=2)
83
+ with display.section("Available Endpoints"):
84
+ endpoints = [
85
+ "GET /login - Login page",
86
+ "GET / - Index page",
87
+ "POST /api/login - Authenticate",
88
+ "POST /api/logout - Logout",
89
+ "GET /api/sessions - List all sessions",
90
+ "GET /api/sessions/{id}/export - Export single session",
91
+ "POST /api/sessions/export-multiple - Export multiple sessions",
92
+ "GET /static/* - Static files",
93
+ ]
94
+ display.list_items(endpoints, bullet="→")
95
+
96
+ print()
97
+ display.info(f"Server will be available at: http://{self.host}:{self.port}")
98
+ display.info("Press Ctrl+C to stop")
99
+
100
+ # Separator before server logs
101
+ print()
102
+ display.separator()
103
+ print()
104
+
105
+ # Context manager for server lifecycle
106
+ @contextlib.asynccontextmanager
107
+ async def serve() -> t.AsyncGenerator[asyncio.Task[None], None]:
108
+ async with sqlite:
109
+ display.info("Database connection established")
110
+ # Start server in background task
111
+ server_task = asyncio.create_task(server.start(self.host, self.port))
112
+ try:
113
+ yield server_task
114
+ finally:
115
+ await server.close()
116
+ display.info("Server stopped")
117
+
118
+ async def run_server() -> None:
119
+ stop_event = asyncio.Event()
120
+
121
+ def set_event(event: asyncio.Event) -> None:
122
+ event.set()
123
+
124
+ loop = asyncio.get_running_loop()
125
+ loop.add_signal_handler(signal.SIGINT, set_event, stop_event)
126
+ loop.add_signal_handler(signal.SIGTERM, set_event, stop_event)
127
+
128
+ async with serve() as server_task:
129
+ # Wait for either stop signal or server to finish
130
+ await asyncio.wait(
131
+ [asyncio.create_task(stop_event.wait()), server_task],
132
+ return_when=asyncio.FIRST_COMPLETED,
133
+ )
134
+
135
+ try:
136
+ asyncio.run(run_server())
137
+ except KeyboardInterrupt:
138
+ display.warning("Received interrupt signal")
139
+ finally:
140
+ print()
141
+ display.success("Export server stopped gracefully")