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,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
+ )
@@ -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)
@@ -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