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,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from audex.valueobj import EnumValueObject
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Op(EnumValueObject):
|
|
7
|
+
EQ = "eq"
|
|
8
|
+
NE = "ne"
|
|
9
|
+
GT = "gt"
|
|
10
|
+
LT = "lt"
|
|
11
|
+
GTE = "gte"
|
|
12
|
+
LTE = "lte"
|
|
13
|
+
CONTAINS = "contains"
|
|
14
|
+
IN = "in"
|
|
15
|
+
NIN = "nin"
|
|
16
|
+
BETWEEN = "between"
|
|
17
|
+
HAS = "has"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Order(EnumValueObject):
|
|
21
|
+
ASC = "asc"
|
|
22
|
+
DESC = "desc"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import typing as t
|
|
5
|
+
|
|
6
|
+
import pydantic as pyd
|
|
7
|
+
|
|
8
|
+
from audex.exceptions import ValidationError
|
|
9
|
+
from audex.valueobj import BaseValueObject
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Phone(BaseValueObject):
|
|
13
|
+
country_code: str = pyd.Field(
|
|
14
|
+
...,
|
|
15
|
+
min_length=1,
|
|
16
|
+
max_length=5,
|
|
17
|
+
description="Country code of the telephone number",
|
|
18
|
+
)
|
|
19
|
+
number: str = pyd.Field(
|
|
20
|
+
...,
|
|
21
|
+
min_length=4,
|
|
22
|
+
max_length=15,
|
|
23
|
+
description="Telephone number without country code",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
@pyd.model_validator(mode="after")
|
|
27
|
+
def validate_phone(self) -> t.Self:
|
|
28
|
+
if not self.country_code.isdigit():
|
|
29
|
+
raise ValueError("Country code must contain only digits")
|
|
30
|
+
if not self.number.isdigit():
|
|
31
|
+
raise ValueError("Telephone number must contain only digits")
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
def __str__(self) -> str:
|
|
35
|
+
return f"+{self.country_code} {self.number}"
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def value(self) -> str:
|
|
39
|
+
"""Get the full telephone number in international format.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A string representing the full telephone number.
|
|
43
|
+
"""
|
|
44
|
+
return str(self)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def parse(cls, phone_str: str) -> t.Self:
|
|
48
|
+
"""Create a Telephone object from a string representation.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
phone_str: A string in the format "+<country_code> <number>".
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
A Telephone object.
|
|
55
|
+
"""
|
|
56
|
+
if not re.match(r"^\+\d+ \d+$", phone_str):
|
|
57
|
+
raise ValidationError(
|
|
58
|
+
"Invalid phone string format. Expected format: '+<country_code> <number>'",
|
|
59
|
+
reason="invalid_format",
|
|
60
|
+
)
|
|
61
|
+
country_code, number = phone_str[1:].split(" ", 1)
|
|
62
|
+
return cls(country_code=country_code, number=number)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CNPhone(Phone):
|
|
66
|
+
@pyd.model_validator(mode="after")
|
|
67
|
+
def validate_chinese_phone(self) -> t.Self:
|
|
68
|
+
if self.country_code != "86":
|
|
69
|
+
raise ValidationError(
|
|
70
|
+
"Country code must be '86' for Chinese telephone numbers",
|
|
71
|
+
reason="invalid_country_code",
|
|
72
|
+
)
|
|
73
|
+
if len(self.number) != 11 or not self.number.startswith((
|
|
74
|
+
"13",
|
|
75
|
+
"14",
|
|
76
|
+
"15",
|
|
77
|
+
"17",
|
|
78
|
+
"18",
|
|
79
|
+
"19",
|
|
80
|
+
)):
|
|
81
|
+
raise ValidationError(
|
|
82
|
+
"Invalid Chinese telephone number format", reason="invalid_number_format"
|
|
83
|
+
)
|
|
84
|
+
return self
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools as ft
|
|
4
|
+
import re
|
|
5
|
+
import typing as t
|
|
6
|
+
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from audex.valueobj import BaseValueObject
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@ft.total_ordering
|
|
13
|
+
class SematicVersion(BaseValueObject):
|
|
14
|
+
major: int = Field(
|
|
15
|
+
...,
|
|
16
|
+
ge=0,
|
|
17
|
+
le=999,
|
|
18
|
+
description="Major version number",
|
|
19
|
+
)
|
|
20
|
+
minor: int = Field(
|
|
21
|
+
...,
|
|
22
|
+
ge=0,
|
|
23
|
+
le=999,
|
|
24
|
+
description="Minor version number",
|
|
25
|
+
)
|
|
26
|
+
patch: int = Field(
|
|
27
|
+
...,
|
|
28
|
+
ge=0,
|
|
29
|
+
le=999,
|
|
30
|
+
description="Patch version number",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def __str__(self) -> str:
|
|
34
|
+
return self.value
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def value(self) -> str:
|
|
38
|
+
return f"v{self.major}.{self.minor}.{self.patch}"
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def parse(cls, version_str: str) -> t.Self:
|
|
42
|
+
if not re.match(r"^v?\d+\.\d+\.\d+$", version_str):
|
|
43
|
+
raise ValueError(f"Invalid semantic version string: {version_str}")
|
|
44
|
+
if version_str.startswith("v"):
|
|
45
|
+
version_str = version_str[1:]
|
|
46
|
+
major, minor, patch = map(int, version_str.split("."))
|
|
47
|
+
return cls(major=major, minor=minor, patch=patch)
|
|
48
|
+
|
|
49
|
+
def __eq__(self, other: object) -> bool:
|
|
50
|
+
if not isinstance(other, SematicVersion):
|
|
51
|
+
return NotImplemented
|
|
52
|
+
return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
|
|
53
|
+
|
|
54
|
+
def __lt__(self, other: object) -> bool:
|
|
55
|
+
if not isinstance(other, SematicVersion):
|
|
56
|
+
return NotImplemented
|
|
57
|
+
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
|
|
58
|
+
|
|
59
|
+
def bump_major(self) -> t.Self:
|
|
60
|
+
self.major += 1
|
|
61
|
+
self.minor = 0
|
|
62
|
+
self.patch = 0
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def bump_minor(self) -> t.Self:
|
|
66
|
+
self.minor += 1
|
|
67
|
+
self.patch = 0
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def bump_patch(self) -> t.Self:
|
|
71
|
+
self.patch += 1
|
|
72
|
+
return self
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from audex.valueobj import EnumValueObject
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SessionStatus(EnumValueObject):
|
|
7
|
+
"""Session status enumeration.
|
|
8
|
+
|
|
9
|
+
Represents the current state of a recording session:
|
|
10
|
+
- DRAFT: Session created but not yet started
|
|
11
|
+
- IN_PROGRESS: Session is actively recording
|
|
12
|
+
- COMPLETED: Session finished successfully
|
|
13
|
+
- CANCELLED: Session was cancelled before completion
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
DRAFT = "draft"
|
|
17
|
+
IN_PROGRESS = "in_progress"
|
|
18
|
+
COMPLETED = "completed"
|
|
19
|
+
CANCELLED = "cancelled"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from audex.valueobj import EnumValueObject
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Speaker(EnumValueObject):
|
|
7
|
+
"""Speaker identification enumeration.
|
|
8
|
+
|
|
9
|
+
Represents the speaker in a conversation:
|
|
10
|
+
- DOCTOR: The registered doctor (verified by voiceprint)
|
|
11
|
+
- PATIENT: The patient (anyone not matching doctor's voiceprint)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
DOCTOR = "doctor"
|
|
15
|
+
PATIENT = "patient"
|
audex/view/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.resources
|
|
4
|
+
|
|
5
|
+
from fastapi import Response
|
|
6
|
+
from nicegui import app
|
|
7
|
+
from nicegui import ui
|
|
8
|
+
|
|
9
|
+
from audex.config import Config
|
|
10
|
+
from audex.helper.mixin import LoggingMixin
|
|
11
|
+
from audex.lifespan import LifeSpan
|
|
12
|
+
|
|
13
|
+
app.add_static_files("/static", importlib.resources.files("audex.view").joinpath("static"))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.get("/sw.js")
|
|
17
|
+
async def service_worker() -> Response:
|
|
18
|
+
return Response(content="// Empty service worker", media_type="application/javascript")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class View(LoggingMixin):
|
|
22
|
+
__logtag__ = "audex.view"
|
|
23
|
+
|
|
24
|
+
def __init__(self, lifespan: LifeSpan, config: Config):
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.lifespan = lifespan
|
|
27
|
+
self.config = config
|
|
28
|
+
app.on_startup(self.lifespan.startup)
|
|
29
|
+
app.on_shutdown(self.lifespan.shutdown)
|
|
30
|
+
|
|
31
|
+
def run(self) -> None:
|
|
32
|
+
import audex.view.pages.dashboard
|
|
33
|
+
import audex.view.pages.login
|
|
34
|
+
import audex.view.pages.recording
|
|
35
|
+
import audex.view.pages.register
|
|
36
|
+
import audex.view.pages.sessions
|
|
37
|
+
import audex.view.pages.sessions.details
|
|
38
|
+
import audex.view.pages.sessions.export
|
|
39
|
+
import audex.view.pages.settings
|
|
40
|
+
import audex.view.pages.voiceprint
|
|
41
|
+
import audex.view.pages.voiceprint.enroll
|
|
42
|
+
import audex.view.pages.voiceprint.update # noqa: F401
|
|
43
|
+
|
|
44
|
+
ui.run(
|
|
45
|
+
title=self.config.core.app.app_name,
|
|
46
|
+
native=self.config.core.app.native,
|
|
47
|
+
language="zh-CN",
|
|
48
|
+
reload=self.config.core.app.debug,
|
|
49
|
+
fullscreen=self.config.core.app.native,
|
|
50
|
+
tailwind=False,
|
|
51
|
+
)
|
audex/view/container.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
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.lifespan import LifeSpan
|
|
8
|
+
from audex.view import View
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ViewContainer(containers.DeclarativeContainer):
|
|
12
|
+
# Dependencies
|
|
13
|
+
config = providers.Dependency(instance_of=Config)
|
|
14
|
+
service = providers.DependenciesContainer()
|
|
15
|
+
lifespan = providers.Dependency(instance_of=LifeSpan)
|
|
16
|
+
|
|
17
|
+
view = providers.Singleton(View, lifespan=lifespan, config=config)
|
audex/view/decorators.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import traceback
|
|
5
|
+
import typing as t
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from nicegui import ui
|
|
9
|
+
|
|
10
|
+
from audex.exceptions import AudexError
|
|
11
|
+
from audex.exceptions import InternalError
|
|
12
|
+
from audex.exceptions import PermissionDeniedError
|
|
13
|
+
from audex.utils import utcnow
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ErrorInfo(t.NamedTuple):
|
|
17
|
+
"""Error information for display."""
|
|
18
|
+
|
|
19
|
+
error_type: str
|
|
20
|
+
error_code: int
|
|
21
|
+
message: str
|
|
22
|
+
timestamp: str
|
|
23
|
+
traceback: str
|
|
24
|
+
details: dict[str, t.Any]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def format_error_info(error: Exception) -> ErrorInfo:
|
|
28
|
+
"""Format exception into ErrorInfo."""
|
|
29
|
+
timestamp = utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
|
30
|
+
tb = "".join(traceback.format_exception(type(error), error, error.__traceback__))
|
|
31
|
+
|
|
32
|
+
if isinstance(error, AudexError):
|
|
33
|
+
error_type = error.__class__.__name__
|
|
34
|
+
error_code = error.code
|
|
35
|
+
message = error.message
|
|
36
|
+
details = error.as_dict()
|
|
37
|
+
else:
|
|
38
|
+
error_type = error.__class__.__name__
|
|
39
|
+
error_code = 0
|
|
40
|
+
message = str(error)
|
|
41
|
+
details = {}
|
|
42
|
+
|
|
43
|
+
return ErrorInfo(
|
|
44
|
+
error_type=error_type,
|
|
45
|
+
error_code=error_code,
|
|
46
|
+
message=message,
|
|
47
|
+
timestamp=timestamp,
|
|
48
|
+
traceback=tb,
|
|
49
|
+
details=details,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def format_error_report(error_info: ErrorInfo) -> str:
|
|
54
|
+
"""Format error report text for copying."""
|
|
55
|
+
return f"""Error Type: {error_info.error_type}
|
|
56
|
+
Error Code: {error_info.error_code}
|
|
57
|
+
Timestamp: {error_info.timestamp}
|
|
58
|
+
Message: {error_info.message}
|
|
59
|
+
|
|
60
|
+
Technical Details:
|
|
61
|
+
{error_info.details}
|
|
62
|
+
|
|
63
|
+
Stack Trace:
|
|
64
|
+
{error_info.traceback}"""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def show_internal_error_dialog(error: InternalError) -> None:
|
|
68
|
+
"""Show a modern dialog for internal errors with unified style."""
|
|
69
|
+
error_info = format_error_info(error)
|
|
70
|
+
error_report = format_error_report(error_info)
|
|
71
|
+
|
|
72
|
+
def copy_to_clipboard():
|
|
73
|
+
ui.run_javascript(f"""
|
|
74
|
+
navigator.clipboard.writeText(`{error_report}`).then(() => {{
|
|
75
|
+
console.log('复制成功');
|
|
76
|
+
}}).catch(err => {{
|
|
77
|
+
console.error('复制失败:', err);
|
|
78
|
+
}});
|
|
79
|
+
""")
|
|
80
|
+
ui.notify("错误详情已复制", type="positive", position="top", timeout=2000)
|
|
81
|
+
|
|
82
|
+
# Add unified animations and styles
|
|
83
|
+
ui.add_head_html("""
|
|
84
|
+
<style>
|
|
85
|
+
@keyframes fadeIn {
|
|
86
|
+
from { opacity: 0; }
|
|
87
|
+
to { opacity: 1; }
|
|
88
|
+
}
|
|
89
|
+
.error-dialog-backdrop {
|
|
90
|
+
position: fixed;
|
|
91
|
+
top: 0;
|
|
92
|
+
left: 0;
|
|
93
|
+
right: 0;
|
|
94
|
+
bottom: 0;
|
|
95
|
+
background: rgba(0, 0, 0, 0.5);
|
|
96
|
+
backdrop-filter: blur(4px);
|
|
97
|
+
-webkit-backdrop-filter: blur(4px);
|
|
98
|
+
z-index: 6000;
|
|
99
|
+
animation: fadeIn 0.2s ease;
|
|
100
|
+
}
|
|
101
|
+
.error-dialog-card {
|
|
102
|
+
animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
103
|
+
}
|
|
104
|
+
@keyframes slideUp {
|
|
105
|
+
from { transform: translateY(20px); opacity: 0; }
|
|
106
|
+
to { transform: translateY(0); opacity: 1; }
|
|
107
|
+
}
|
|
108
|
+
.error-action-btn {
|
|
109
|
+
border-radius: 12px ! important;
|
|
110
|
+
font-size: 16px !important;
|
|
111
|
+
font-weight: 500 ! important;
|
|
112
|
+
letter-spacing: 0.02em !important;
|
|
113
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
|
114
|
+
min-width: 120px !important;
|
|
115
|
+
height: 44px !important;
|
|
116
|
+
text-transform: none !important;
|
|
117
|
+
}
|
|
118
|
+
.error-action-btn:hover {
|
|
119
|
+
transform: translateY(-2px);
|
|
120
|
+
}
|
|
121
|
+
.error-action-btn:active {
|
|
122
|
+
transform: translateY(0);
|
|
123
|
+
}
|
|
124
|
+
.error-btn-primary {
|
|
125
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
|
126
|
+
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.25), 0 2px 8px rgba(118, 75, 162, 0.15) !important;
|
|
127
|
+
}
|
|
128
|
+
.error-btn-primary:hover {
|
|
129
|
+
background: linear-gradient(135deg, #7c8ef0 0%, #8a5db0 100%) !important;
|
|
130
|
+
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.35), 0 4px 12px rgba(118, 75, 162, 0.25) !important;
|
|
131
|
+
}
|
|
132
|
+
.error-btn-secondary {
|
|
133
|
+
background: rgba(248, 249, 250, 0.8) !important;
|
|
134
|
+
color: #6b7280 !important;
|
|
135
|
+
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
|
136
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04) !important;
|
|
137
|
+
}
|
|
138
|
+
.error-btn-secondary:hover {
|
|
139
|
+
background: rgba(243, 244, 246, 0.9) !important;
|
|
140
|
+
border-color: rgba(0, 0, 0, 0.12) !important;
|
|
141
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
|
|
142
|
+
}
|
|
143
|
+
.error-code-badge {
|
|
144
|
+
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
|
145
|
+
color: #667eea;
|
|
146
|
+
padding: 8px 16px;
|
|
147
|
+
border-radius: 12px;
|
|
148
|
+
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
|
149
|
+
font-size: 0.875rem;
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
border: 1px solid rgba(102, 126, 234, 0.2);
|
|
152
|
+
}
|
|
153
|
+
.error-help-box {
|
|
154
|
+
background: linear-gradient(135deg, #f8f9fa 0%, #f5f6f7 100%);
|
|
155
|
+
border-radius: 12px;
|
|
156
|
+
padding: 16px;
|
|
157
|
+
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
158
|
+
}
|
|
159
|
+
</style>
|
|
160
|
+
""")
|
|
161
|
+
|
|
162
|
+
# Backdrop
|
|
163
|
+
backdrop = ui.element("div").classes("error-dialog-backdrop")
|
|
164
|
+
|
|
165
|
+
with ui.dialog() as dialog:
|
|
166
|
+
dialog.props("persistent")
|
|
167
|
+
|
|
168
|
+
with (
|
|
169
|
+
ui.card()
|
|
170
|
+
.classes("error-dialog-card")
|
|
171
|
+
.style(
|
|
172
|
+
"width: 560px; max-width: 90vw; padding: 32px; "
|
|
173
|
+
"border-radius: 20px; "
|
|
174
|
+
"box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15), 0 8px 24px rgba(0, 0, 0, 0.1);"
|
|
175
|
+
)
|
|
176
|
+
):
|
|
177
|
+
# Header
|
|
178
|
+
with ui.row().classes("w-full items-center mb-6 no-wrap"):
|
|
179
|
+
ui.icon("error_outline", size="xl").classes("text-negative")
|
|
180
|
+
ui.label("出错了").classes("text-h5 font-bold text-grey-9").style(
|
|
181
|
+
"margin-left: 12px;"
|
|
182
|
+
)
|
|
183
|
+
ui.space()
|
|
184
|
+
ui.button(icon="close", on_click=lambda: (dialog.close(), backdrop.delete())).props(
|
|
185
|
+
"flat round dense"
|
|
186
|
+
).style("margin: -8px;")
|
|
187
|
+
|
|
188
|
+
# Message
|
|
189
|
+
ui.label("系统遇到了一个问题,我们正在努力解决").classes(
|
|
190
|
+
"text-body1 text-grey-8"
|
|
191
|
+
).style("margin-bottom: 20px;")
|
|
192
|
+
|
|
193
|
+
# Error code badge
|
|
194
|
+
with ui.row().classes("items-center gap-2").style("margin-bottom: 24px;"):
|
|
195
|
+
ui.label("错误代码:").classes("text-sm text-grey-7")
|
|
196
|
+
ui.html(
|
|
197
|
+
f'<div class="error-code-badge">#{error_info.error_code}</div>', sanitize=False
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
ui.separator().style("margin-bottom: 20px; background: rgba(0, 0, 0, 0.06);")
|
|
201
|
+
|
|
202
|
+
# Expandable details
|
|
203
|
+
with (
|
|
204
|
+
ui.expansion("查看技术详情", icon="code")
|
|
205
|
+
.classes("w-full")
|
|
206
|
+
.style(
|
|
207
|
+
"background: rgba(248, 249, 250, 0.5); "
|
|
208
|
+
"border-radius: 12px; "
|
|
209
|
+
"border: 1px solid rgba(0, 0, 0, 0.06);"
|
|
210
|
+
),
|
|
211
|
+
ui.scroll_area().style("max-height: 200px;"),
|
|
212
|
+
):
|
|
213
|
+
ui.code(error_report).classes("text-xs").style(
|
|
214
|
+
"background: #f8f9fa; "
|
|
215
|
+
"padding: 16px; "
|
|
216
|
+
"border-radius: 8px; "
|
|
217
|
+
"font-family: 'Monaco', 'Menlo', 'Consolas', monospace;"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Action buttons
|
|
221
|
+
with ui.row().classes("w-full gap-3 justify-end").style("margin-top: 24px;"):
|
|
222
|
+
ui.button(
|
|
223
|
+
"复制详情",
|
|
224
|
+
icon="content_copy",
|
|
225
|
+
on_click=copy_to_clipboard, # 修改这里
|
|
226
|
+
).props("no-caps").classes("error-action-btn error-btn-secondary")
|
|
227
|
+
|
|
228
|
+
ui.button("关闭", on_click=lambda: (dialog.close(), backdrop.delete())).props(
|
|
229
|
+
"unelevated no-caps"
|
|
230
|
+
).classes("error-action-btn error-btn-primary")
|
|
231
|
+
|
|
232
|
+
# Help hint
|
|
233
|
+
with (
|
|
234
|
+
ui.element("div").classes("error-help-box").style("margin-top: 20px;"),
|
|
235
|
+
ui.row().classes("items-center gap-2"),
|
|
236
|
+
):
|
|
237
|
+
ui.icon("lightbulb", size="sm").classes("text-grey-6")
|
|
238
|
+
ui.label("如果问题持续出现,请将错误代码提供给技术支持").classes(
|
|
239
|
+
"text-sm text-grey-7"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
dialog.open()
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def show_user_error_notification(error: AudexError) -> None:
|
|
246
|
+
"""Show a simple notification for user-facing errors."""
|
|
247
|
+
ui.notify(
|
|
248
|
+
error.message,
|
|
249
|
+
type="negative",
|
|
250
|
+
position="top",
|
|
251
|
+
close_button=True,
|
|
252
|
+
timeout=5000,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def handle_exception(error: Exception) -> None:
|
|
257
|
+
"""Handle exception and display appropriate UI feedback."""
|
|
258
|
+
if isinstance(error, PermissionDeniedError):
|
|
259
|
+
ui.notify(
|
|
260
|
+
"Session expired, please login",
|
|
261
|
+
type="warning",
|
|
262
|
+
position="top",
|
|
263
|
+
timeout=3000,
|
|
264
|
+
)
|
|
265
|
+
ui.navigate.to("/login")
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
if isinstance(error, InternalError):
|
|
269
|
+
logger.bind(tag="audex.view.error", **error.as_dict()).exception(
|
|
270
|
+
f"Internal error: {error.message}"
|
|
271
|
+
)
|
|
272
|
+
show_internal_error_dialog(error)
|
|
273
|
+
|
|
274
|
+
elif isinstance(error, AudexError):
|
|
275
|
+
show_user_error_notification(error)
|
|
276
|
+
|
|
277
|
+
else:
|
|
278
|
+
internal_error = InternalError(
|
|
279
|
+
message="An unexpected error occurred",
|
|
280
|
+
original_error=str(error),
|
|
281
|
+
error_type=error.__class__.__name__,
|
|
282
|
+
traceback="".join(traceback.format_exception(type(error), error, error.__traceback__)),
|
|
283
|
+
)
|
|
284
|
+
logger.bind(tag="audex.view.error", **internal_error.as_dict()).exception(
|
|
285
|
+
f"Unknown error: {error}"
|
|
286
|
+
)
|
|
287
|
+
show_internal_error_dialog(internal_error)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
PageMethodT = t.TypeVar("PageMethodT", bound=t.Callable[..., t.Awaitable[None]])
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def handle_errors(func: PageMethodT) -> PageMethodT:
|
|
294
|
+
"""Decorator to handle exceptions in page render methods."""
|
|
295
|
+
|
|
296
|
+
@functools.wraps(func)
|
|
297
|
+
async def wrapper(*args, **kwargs):
|
|
298
|
+
try:
|
|
299
|
+
return await func(*args, **kwargs)
|
|
300
|
+
except Exception as e:
|
|
301
|
+
handle_exception(e)
|
|
302
|
+
|
|
303
|
+
return wrapper
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|