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