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,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from audex.exceptions import AudexError
|
|
6
|
+
from audex.exceptions import InternalError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DoctorServiceError(AudexError):
|
|
10
|
+
"""Base exception for doctor service errors."""
|
|
11
|
+
|
|
12
|
+
default_message = "Doctor service error"
|
|
13
|
+
code: t.ClassVar[int] = 0x30
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InternalDoctorServiceError(InternalError):
|
|
17
|
+
"""Internal error in doctor service."""
|
|
18
|
+
|
|
19
|
+
default_message = "Internal doctor service error"
|
|
20
|
+
code: t.ClassVar[int] = 0x31
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DoctorNotFoundError(DoctorServiceError):
|
|
24
|
+
"""Raised when doctor is not found."""
|
|
25
|
+
|
|
26
|
+
__slots__ = ("doctor_id", "message")
|
|
27
|
+
|
|
28
|
+
default_message = "Doctor not found"
|
|
29
|
+
code: t.ClassVar[int] = 0x32
|
|
30
|
+
|
|
31
|
+
def __init__(self, message: str, *, doctor_id: str) -> None:
|
|
32
|
+
"""Initialize the exception.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
message: Error message (typically in Chinese for users).
|
|
36
|
+
doctor_id: The ID of the doctor that was not found.
|
|
37
|
+
"""
|
|
38
|
+
self.doctor_id = doctor_id
|
|
39
|
+
super().__init__(message)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class InvalidCredentialsError(DoctorServiceError):
|
|
43
|
+
"""Raised for invalid login credentials."""
|
|
44
|
+
|
|
45
|
+
__slots__ = ("message", "reason")
|
|
46
|
+
|
|
47
|
+
default_message = "Invalid credentials"
|
|
48
|
+
code: t.ClassVar[int] = 0x33
|
|
49
|
+
|
|
50
|
+
def __init__(self, message: str, *, reason: str | None = None) -> None:
|
|
51
|
+
"""Initialize the exception.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
message: Error message (typically in Chinese for users).
|
|
55
|
+
reason: Machine-readable reason code (from InvalidCredentialReasons).
|
|
56
|
+
"""
|
|
57
|
+
self.reason = reason
|
|
58
|
+
super().__init__(message)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class VoiceprintNotFoundError(DoctorServiceError):
|
|
62
|
+
"""Raised when voiceprint is not found."""
|
|
63
|
+
|
|
64
|
+
__slots__ = ("doctor_id", "message")
|
|
65
|
+
|
|
66
|
+
default_message = "Voiceprint not found"
|
|
67
|
+
code: t.ClassVar[int] = 0x34
|
|
68
|
+
|
|
69
|
+
def __init__(self, message: str, *, doctor_id: str) -> None:
|
|
70
|
+
"""Initialize the exception.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
message: Error message (typically in Chinese for users).
|
|
74
|
+
doctor_id: The ID of the doctor whose voiceprint was not found.
|
|
75
|
+
"""
|
|
76
|
+
self.doctor_id = doctor_id
|
|
77
|
+
super().__init__(message)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class DuplicateEIDError(DoctorServiceError):
|
|
81
|
+
"""Raised when trying to register with an existing EID."""
|
|
82
|
+
|
|
83
|
+
__slots__ = ("eid", "message")
|
|
84
|
+
|
|
85
|
+
default_message = "Duplicate EID"
|
|
86
|
+
code: t.ClassVar[int] = 0x35
|
|
87
|
+
|
|
88
|
+
def __init__(self, message: str, *, eid: str) -> None:
|
|
89
|
+
"""Initialize the exception.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
message: Error message (typically in Chinese for users).
|
|
93
|
+
eid: The duplicate employee ID.
|
|
94
|
+
"""
|
|
95
|
+
self.eid = eid
|
|
96
|
+
super().__init__(message)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from audex.valueobj.common.auth import Password
|
|
6
|
+
from audex.valueobj.common.email import Email
|
|
7
|
+
from audex.valueobj.common.phone import CNPhone
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LoginCommand(t.NamedTuple):
|
|
11
|
+
"""Command for doctor login."""
|
|
12
|
+
|
|
13
|
+
eid: str
|
|
14
|
+
password: Password
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RegisterCommand(t.NamedTuple):
|
|
18
|
+
"""Command for doctor registration."""
|
|
19
|
+
|
|
20
|
+
eid: str
|
|
21
|
+
password: Password
|
|
22
|
+
name: str
|
|
23
|
+
department: str | None = None
|
|
24
|
+
title: str | None = None
|
|
25
|
+
hospital: str | None = None
|
|
26
|
+
phone: CNPhone | None = None
|
|
27
|
+
email: Email | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UpdateCommand(t.NamedTuple):
|
|
31
|
+
"""Command for updating doctor profile."""
|
|
32
|
+
|
|
33
|
+
name: str | None = None
|
|
34
|
+
department: str | None = None
|
|
35
|
+
title: str | None = None
|
|
36
|
+
hospital: str | None = None
|
|
37
|
+
phone: CNPhone | None = None
|
|
38
|
+
email: Email | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class VPEnrollResult(t.NamedTuple):
|
|
42
|
+
"""Result of voiceprint enrollment/update.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
vp_id: ID of the VP entity.
|
|
46
|
+
vpr_uid: UID in the VPR system.
|
|
47
|
+
audio_key: Storage key of the audio file.
|
|
48
|
+
duration_ms: Duration of the recording in milliseconds.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
vp_id: str
|
|
52
|
+
vpr_uid: str
|
|
53
|
+
audio_key: str
|
|
54
|
+
duration_ms: int
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
|
|
6
|
+
from audex.helper import net
|
|
7
|
+
from audex.lib.cache import KVCache
|
|
8
|
+
from audex.lib.exporter import Exporter
|
|
9
|
+
from audex.lib.repos.doctor import DoctorRepository
|
|
10
|
+
from audex.lib.server import Server
|
|
11
|
+
from audex.lib.session import SessionManager
|
|
12
|
+
from audex.lib.usb import USBDevice
|
|
13
|
+
from audex.lib.usb import USBManager
|
|
14
|
+
from audex.service import BaseService
|
|
15
|
+
from audex.service.decorators import require_auth
|
|
16
|
+
from audex.service.export.const import ErrorMessages
|
|
17
|
+
from audex.service.export.exceptions import ExportServiceError
|
|
18
|
+
from audex.service.export.exceptions import InternalExportServiceError
|
|
19
|
+
from audex.service.export.exceptions import NoUSBDeviceError
|
|
20
|
+
from audex.service.export.exceptions import ServerAlreadyRunningError
|
|
21
|
+
from audex.service.export.types import ExportResult
|
|
22
|
+
from audex.service.export.types import ServerInfo
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExportService(BaseService):
|
|
26
|
+
"""Service for exporting session data via HTTP or USB."""
|
|
27
|
+
|
|
28
|
+
__logtag__ = "audex.service.export"
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
session_manager: SessionManager,
|
|
33
|
+
cache: KVCache,
|
|
34
|
+
doctor_repo: DoctorRepository,
|
|
35
|
+
usb: USBManager,
|
|
36
|
+
exporter: Exporter,
|
|
37
|
+
server: Server,
|
|
38
|
+
):
|
|
39
|
+
super().__init__(session_manager=session_manager, cache=cache, doctor_repo=doctor_repo)
|
|
40
|
+
self.usb = usb
|
|
41
|
+
self.exporter = exporter
|
|
42
|
+
self.server = server
|
|
43
|
+
self._server_task: asyncio.Task[None] | None = None
|
|
44
|
+
self._server_running = False
|
|
45
|
+
|
|
46
|
+
@require_auth
|
|
47
|
+
async def start_server(self) -> ServerInfo:
|
|
48
|
+
"""Start HTTP export server.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
ServerInfo with host and port.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ServerAlreadyRunningError: If server is already running.
|
|
55
|
+
InternalExportServiceError: For internal errors.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
if self._server_running:
|
|
59
|
+
raise ServerAlreadyRunningError(ErrorMessages.SERVER_ALREADY_RUNNING)
|
|
60
|
+
|
|
61
|
+
# Start server in background task
|
|
62
|
+
addr = net.getaddr()
|
|
63
|
+
port = net.getfreeport()
|
|
64
|
+
self._server_task = asyncio.create_task(self.server.start(host="0.0.0.0", port=port))
|
|
65
|
+
self._server_running = True
|
|
66
|
+
|
|
67
|
+
# Get server info
|
|
68
|
+
server_info = ServerInfo(
|
|
69
|
+
host=addr,
|
|
70
|
+
port=port,
|
|
71
|
+
url=f"http://{addr}:{port}",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
self.logger.info(f"Started export server at {server_info.url}")
|
|
75
|
+
return server_info
|
|
76
|
+
|
|
77
|
+
except ServerAlreadyRunningError:
|
|
78
|
+
raise
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
self.logger.error(f"Failed to start server: {e}")
|
|
82
|
+
raise InternalExportServiceError(ErrorMessages.SERVER_START_FAILED) from e
|
|
83
|
+
|
|
84
|
+
@require_auth
|
|
85
|
+
async def stop_server(self) -> None:
|
|
86
|
+
"""Stop HTTP export server.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
InternalExportServiceError: For internal errors.
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
if not self._server_running:
|
|
93
|
+
self.logger.warning("Server is not running")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
# Stop server
|
|
97
|
+
await self.server.close()
|
|
98
|
+
|
|
99
|
+
# Cancel task if exists
|
|
100
|
+
if self._server_task:
|
|
101
|
+
self._server_task.cancel()
|
|
102
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
103
|
+
await self._server_task
|
|
104
|
+
self._server_task = None
|
|
105
|
+
|
|
106
|
+
self._server_running = False
|
|
107
|
+
self.logger.info("Stopped export server")
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
self.logger.error(f"Failed to stop server: {e}")
|
|
111
|
+
raise InternalExportServiceError(ErrorMessages.SERVER_STOP_FAILED) from e
|
|
112
|
+
|
|
113
|
+
@require_auth
|
|
114
|
+
async def is_server_running(self) -> bool:
|
|
115
|
+
"""Check if HTTP export server is running."""
|
|
116
|
+
return self._server_running
|
|
117
|
+
|
|
118
|
+
@require_auth
|
|
119
|
+
async def list_usb_devices(self) -> list[USBDevice]:
|
|
120
|
+
"""List all connected USB storage devices.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
List of connected USB devices.
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
devices = self.usb.list_devices()
|
|
127
|
+
self.logger.debug(f"Found {len(devices)} USB device(s)")
|
|
128
|
+
return devices
|
|
129
|
+
except Exception as e:
|
|
130
|
+
self.logger.error(f"Failed to list USB devices: {e}")
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
@require_auth
|
|
134
|
+
async def export_to_usb(
|
|
135
|
+
self,
|
|
136
|
+
session_ids: list[str],
|
|
137
|
+
device: USBDevice | None = None,
|
|
138
|
+
) -> ExportResult:
|
|
139
|
+
"""Export sessions to USB device.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
session_ids: List of session IDs to export.
|
|
143
|
+
device: Target USB device. If None, uses first available device.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
ExportResult with success status and details.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
NoUSBDeviceError: If no USB device is available.
|
|
150
|
+
InternalExportServiceError: For internal errors.
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
# Get USB device
|
|
154
|
+
if device is None:
|
|
155
|
+
devices = await self.list_usb_devices()
|
|
156
|
+
if not devices:
|
|
157
|
+
raise NoUSBDeviceError(ErrorMessages.NO_USB_DEVICE)
|
|
158
|
+
device = devices[0]
|
|
159
|
+
self.logger.info(f"Using first USB device: {device.mount_point}")
|
|
160
|
+
|
|
161
|
+
# Verify doctor owns all sessions
|
|
162
|
+
session = await self.session_manager.get_session()
|
|
163
|
+
if not session:
|
|
164
|
+
raise ExportServiceError(ErrorMessages.NO_ACTIVE_SESSION)
|
|
165
|
+
|
|
166
|
+
for session_id in session_ids:
|
|
167
|
+
sess = await self.exporter.session_repo.read(session_id)
|
|
168
|
+
if not sess or sess.doctor_id != session.doctor_id:
|
|
169
|
+
raise ExportServiceError(
|
|
170
|
+
f"无权访问会话 {session_id}",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Export each session
|
|
174
|
+
export_tasks = []
|
|
175
|
+
|
|
176
|
+
for session_id in session_ids:
|
|
177
|
+
# Generate ZIP
|
|
178
|
+
zip_data = await self.exporter.export_session_zip(session_id)
|
|
179
|
+
|
|
180
|
+
# Get session info for filename
|
|
181
|
+
sess = await self.exporter.session_repo.read(session_id)
|
|
182
|
+
filename = f"{session_id}"
|
|
183
|
+
if sess and sess.patient_name:
|
|
184
|
+
filename = f"{sess.patient_name}_{session_id}"
|
|
185
|
+
filename += ".zip"
|
|
186
|
+
|
|
187
|
+
# Write to temp file
|
|
188
|
+
import pathlib
|
|
189
|
+
import tempfile
|
|
190
|
+
|
|
191
|
+
temp_path = pathlib.Path(tempfile.mkdtemp()) / filename
|
|
192
|
+
temp_path.write_bytes(zip_data)
|
|
193
|
+
|
|
194
|
+
# Add export task
|
|
195
|
+
self.usb.add_export_task(
|
|
196
|
+
source=temp_path,
|
|
197
|
+
dest_name=f"audex_export/{filename}",
|
|
198
|
+
is_directory=False,
|
|
199
|
+
)
|
|
200
|
+
export_tasks.append((session_id, temp_path))
|
|
201
|
+
|
|
202
|
+
# Export to USB
|
|
203
|
+
results = await self.usb.export(device)
|
|
204
|
+
|
|
205
|
+
# Clean up temp files
|
|
206
|
+
for _, temp_path in export_tasks:
|
|
207
|
+
try:
|
|
208
|
+
temp_path.unlink()
|
|
209
|
+
temp_path.parent.rmdir()
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
# Clear export tasks
|
|
214
|
+
self.usb.clear_export_tasks()
|
|
215
|
+
|
|
216
|
+
# Check results
|
|
217
|
+
success_count = sum(1 for success in results.values() if success)
|
|
218
|
+
total_count = len(results)
|
|
219
|
+
|
|
220
|
+
export_result = ExportResult(
|
|
221
|
+
success=success_count == total_count,
|
|
222
|
+
total=total_count,
|
|
223
|
+
success_count=success_count,
|
|
224
|
+
failed_count=total_count - success_count,
|
|
225
|
+
device_label=device.label or device.mount_point,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
self.logger.info(f"USB export completed: {success_count}/{total_count} successful")
|
|
229
|
+
|
|
230
|
+
return export_result
|
|
231
|
+
|
|
232
|
+
except (NoUSBDeviceError, ExportServiceError):
|
|
233
|
+
raise
|
|
234
|
+
except Exception as e:
|
|
235
|
+
self.logger.error(f"USB export failed: {e}")
|
|
236
|
+
raise InternalExportServiceError(ErrorMessages.USB_EXPORT_FAILED) from e
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ErrorMessages:
|
|
5
|
+
"""Error messages in Chinese for export service."""
|
|
6
|
+
|
|
7
|
+
# Server errors
|
|
8
|
+
SERVER_ALREADY_RUNNING = "服务器已在运行"
|
|
9
|
+
SERVER_START_FAILED = "启动服务器失败"
|
|
10
|
+
SERVER_STOP_FAILED = "停止服务器失败"
|
|
11
|
+
|
|
12
|
+
# USB errors
|
|
13
|
+
NO_USB_DEVICE = "未检测到U盘,请插入U盘后重试"
|
|
14
|
+
USB_EXPORT_FAILED = "导出到U盘失败"
|
|
15
|
+
|
|
16
|
+
# Session errors
|
|
17
|
+
NO_ACTIVE_SESSION = "登录状态已过期,请重新登录"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from audex.exceptions import AudexError
|
|
6
|
+
from audex.exceptions import InternalError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ExportServiceError(AudexError):
|
|
10
|
+
"""Base exception for export service errors."""
|
|
11
|
+
|
|
12
|
+
default_message = "Export service error"
|
|
13
|
+
code: t.ClassVar[int] = 0x50
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InternalExportServiceError(InternalError):
|
|
17
|
+
"""Internal error in export service."""
|
|
18
|
+
|
|
19
|
+
default_message = "Internal export service error"
|
|
20
|
+
code: t.ClassVar[int] = 0x51
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class NoUSBDeviceError(ExportServiceError):
|
|
24
|
+
"""Raised when no USB device is available."""
|
|
25
|
+
|
|
26
|
+
default_message = "No USB device available"
|
|
27
|
+
code: t.ClassVar[int] = 0x52
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ServerAlreadyRunningError(ExportServiceError):
|
|
31
|
+
"""Raised when server is already running."""
|
|
32
|
+
|
|
33
|
+
default_message = "Server already running"
|
|
34
|
+
code: t.ClassVar[int] = 0x53
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ServerInfo(t.NamedTuple):
|
|
7
|
+
"""HTTP server information."""
|
|
8
|
+
|
|
9
|
+
host: str
|
|
10
|
+
port: int
|
|
11
|
+
url: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ExportResult(t.NamedTuple):
|
|
15
|
+
"""Result of USB export operation."""
|
|
16
|
+
|
|
17
|
+
success: bool
|
|
18
|
+
total: int
|
|
19
|
+
success_count: int
|
|
20
|
+
failed_count: int
|
|
21
|
+
device_label: str
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dependency_injector import containers
|
|
4
|
+
from dependency_injector import providers
|
|
5
|
+
|
|
6
|
+
from audex.config import Config
|
|
7
|
+
from audex.service.injectors.doctor import make_doctor_service
|
|
8
|
+
from audex.service.injectors.export import make_export_service
|
|
9
|
+
from audex.service.injectors.session import make_session_service
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ServiceContainer(containers.DeclarativeContainer):
|
|
13
|
+
# Dependencies
|
|
14
|
+
config = providers.Dependency(instance_of=Config)
|
|
15
|
+
infrastructure = providers.DependenciesContainer()
|
|
16
|
+
repository = providers.DependenciesContainer()
|
|
17
|
+
|
|
18
|
+
# Components
|
|
19
|
+
doctor = providers.Factory(
|
|
20
|
+
make_doctor_service,
|
|
21
|
+
session_manager=infrastructure.session_manager,
|
|
22
|
+
cache=infrastructure.cache,
|
|
23
|
+
config=config,
|
|
24
|
+
doctor_repo=repository.doctor,
|
|
25
|
+
vp_repo=repository.vp,
|
|
26
|
+
vpr=infrastructure.vpr,
|
|
27
|
+
recorder=infrastructure.recorder,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
session = providers.Factory(
|
|
31
|
+
make_session_service,
|
|
32
|
+
session_manager=infrastructure.session_manager,
|
|
33
|
+
cache=infrastructure.cache,
|
|
34
|
+
config=config,
|
|
35
|
+
doctor_repo=repository.doctor,
|
|
36
|
+
session_repo=repository.session,
|
|
37
|
+
segment_repo=repository.segment,
|
|
38
|
+
utterance_repo=repository.utterance,
|
|
39
|
+
vp_repo=repository.vp,
|
|
40
|
+
vpr=infrastructure.vpr,
|
|
41
|
+
transcription=infrastructure.transcription,
|
|
42
|
+
recorder=infrastructure.recorder,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
export = providers.Factory(
|
|
46
|
+
make_export_service,
|
|
47
|
+
session_manager=infrastructure.session_manager,
|
|
48
|
+
cache=infrastructure.cache,
|
|
49
|
+
doctor_repo=repository.doctor,
|
|
50
|
+
usb=infrastructure.usb,
|
|
51
|
+
exporter=infrastructure.exporter,
|
|
52
|
+
server=infrastructure.server,
|
|
53
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from audex.config import Config
|
|
4
|
+
from audex.lib.cache import KVCache
|
|
5
|
+
from audex.lib.recorder import AudioRecorder
|
|
6
|
+
from audex.lib.repos.doctor import DoctorRepository
|
|
7
|
+
from audex.lib.repos.vp import VPRepository
|
|
8
|
+
from audex.lib.session import SessionManager
|
|
9
|
+
from audex.lib.vpr import VPR
|
|
10
|
+
from audex.service.doctor import DoctorService
|
|
11
|
+
from audex.service.doctor import DoctorServiceConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def make_doctor_service(
|
|
15
|
+
session_manager: SessionManager,
|
|
16
|
+
cache: KVCache,
|
|
17
|
+
config: Config,
|
|
18
|
+
doctor_repo: DoctorRepository,
|
|
19
|
+
vp_repo: VPRepository,
|
|
20
|
+
vpr: VPR,
|
|
21
|
+
recorder: AudioRecorder,
|
|
22
|
+
) -> DoctorService:
|
|
23
|
+
return DoctorService(
|
|
24
|
+
session_manager=session_manager,
|
|
25
|
+
cache=cache,
|
|
26
|
+
config=DoctorServiceConfig(
|
|
27
|
+
vpr_sr=config.core.audio.vpr_sample_rate,
|
|
28
|
+
vpr_text_content=config.core.audio.vpr_text_content,
|
|
29
|
+
),
|
|
30
|
+
doctor_repo=doctor_repo,
|
|
31
|
+
vp_repo=vp_repo,
|
|
32
|
+
vpr=vpr,
|
|
33
|
+
recorder=recorder,
|
|
34
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from audex.lib.cache import KVCache
|
|
4
|
+
from audex.lib.exporter import Exporter
|
|
5
|
+
from audex.lib.repos.doctor import DoctorRepository
|
|
6
|
+
from audex.lib.server import Server
|
|
7
|
+
from audex.lib.session import SessionManager
|
|
8
|
+
from audex.lib.usb import USBManager
|
|
9
|
+
from audex.service.export import ExportService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def make_export_service(
|
|
13
|
+
session_manager: SessionManager,
|
|
14
|
+
cache: KVCache,
|
|
15
|
+
doctor_repo: DoctorRepository,
|
|
16
|
+
usb: USBManager,
|
|
17
|
+
exporter: Exporter,
|
|
18
|
+
server: Server,
|
|
19
|
+
) -> ExportService:
|
|
20
|
+
return ExportService(
|
|
21
|
+
session_manager=session_manager,
|
|
22
|
+
cache=cache,
|
|
23
|
+
doctor_repo=doctor_repo,
|
|
24
|
+
usb=usb,
|
|
25
|
+
exporter=exporter,
|
|
26
|
+
server=server,
|
|
27
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from audex.config import Config
|
|
4
|
+
from audex.lib.cache import KVCache
|
|
5
|
+
from audex.lib.recorder import AudioRecorder
|
|
6
|
+
from audex.lib.repos.doctor import DoctorRepository
|
|
7
|
+
from audex.lib.repos.segment import SegmentRepository
|
|
8
|
+
from audex.lib.repos.session import SessionRepository
|
|
9
|
+
from audex.lib.repos.utterance import UtteranceRepository
|
|
10
|
+
from audex.lib.repos.vp import VPRepository
|
|
11
|
+
from audex.lib.session import SessionManager
|
|
12
|
+
from audex.lib.transcription import Transcription
|
|
13
|
+
from audex.lib.vpr import VPR
|
|
14
|
+
from audex.service.session import SessionService
|
|
15
|
+
from audex.service.session import SessionServiceConfig
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def make_session_service(
|
|
19
|
+
session_manager: SessionManager,
|
|
20
|
+
cache: KVCache,
|
|
21
|
+
config: Config,
|
|
22
|
+
doctor_repo: DoctorRepository,
|
|
23
|
+
session_repo: SessionRepository,
|
|
24
|
+
segment_repo: SegmentRepository,
|
|
25
|
+
utterance_repo: UtteranceRepository,
|
|
26
|
+
vp_repo: VPRepository,
|
|
27
|
+
vpr: VPR,
|
|
28
|
+
transcription: Transcription,
|
|
29
|
+
recorder: AudioRecorder,
|
|
30
|
+
) -> SessionService:
|
|
31
|
+
return SessionService(
|
|
32
|
+
session_manager=session_manager,
|
|
33
|
+
cache=cache,
|
|
34
|
+
config=SessionServiceConfig(
|
|
35
|
+
audio_key_prefix=config.core.audio.key_prefix,
|
|
36
|
+
segment_buffer_ms=config.core.audio.segment_buffer,
|
|
37
|
+
sr=config.core.audio.sample_rate,
|
|
38
|
+
vpr_sr=config.core.audio.vpr_sample_rate,
|
|
39
|
+
vpr_threshold=config.core.audio.vpr_threshold,
|
|
40
|
+
),
|
|
41
|
+
doctor_repo=doctor_repo,
|
|
42
|
+
session_repo=session_repo,
|
|
43
|
+
segment_repo=segment_repo,
|
|
44
|
+
utterance_repo=utterance_repo,
|
|
45
|
+
vp_repo=vp_repo,
|
|
46
|
+
vpr=vpr,
|
|
47
|
+
transcription=transcription,
|
|
48
|
+
recorder=recorder,
|
|
49
|
+
)
|