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,443 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+
5
+ from dependency_injector.wiring import Provide
6
+ from dependency_injector.wiring import inject
7
+ from fastapi import Depends
8
+ from nicegui import ui
9
+
10
+ from audex.container import Container
11
+ from audex.service.doctor import DoctorService
12
+ from audex.service.export import ExportService
13
+ from audex.service.session import SessionService
14
+ from audex.view.decorators import handle_errors
15
+
16
+
17
+ @ui.page("/sessions/export")
18
+ @handle_errors
19
+ @inject
20
+ async def render(
21
+ doctor_service: DoctorService = Depends(Provide[Container.service.doctor]),
22
+ session_service: SessionService = Depends(Provide[Container.service.session]),
23
+ export_service: ExportService = Depends(Provide[Container.service.export]),
24
+ ) -> None:
25
+ """Render export options page."""
26
+
27
+ # Get current doctor
28
+ doctor = await doctor_service.current_doctor()
29
+
30
+ # Add CSS
31
+ ui.add_head_html('<link rel="stylesheet" href="/static/css/sessions/styles.css">')
32
+
33
+ # Fetch sessions
34
+ sessions = await session_service.list(doctor_id=doctor.id, page_size=100)
35
+
36
+ # State
37
+ server_running = {"value": False}
38
+
39
+ # Before unload script
40
+ ui.add_head_html("""
41
+ <script>
42
+ window.addEventListener('beforeunload', function(e) {
43
+ if (window.serverRunning) {
44
+ e.preventDefault();
45
+ e.returnValue = '服务器正在运行,确定要离开吗?';
46
+ return e.returnValue;
47
+ }
48
+ });
49
+ </script>
50
+ """)
51
+
52
+ # Header
53
+ with (
54
+ ui.header().classes("header-glass items-center justify-between px-6 py-3"),
55
+ ui.row().classes("items-center gap-3"),
56
+ ):
57
+
58
+ async def go_back():
59
+ """返回并检查服务器状态."""
60
+ if server_running["value"]:
61
+ with (
62
+ ui.dialog() as leave_dialog,
63
+ ui.card()
64
+ .classes("dialog-card")
65
+ .style("width: 450px; padding: 28px; border-radius: 16px;"),
66
+ ):
67
+ with ui.row().classes("w-full items-center mb-6"):
68
+ ui.icon("warning", size="xl").classes("text-warning")
69
+ ui.label("确认离开").classes("text-h5 font-bold text-grey-9 ml-3 flex-1")
70
+ ui.button(icon="close", on_click=leave_dialog.close).props(
71
+ "flat round dense"
72
+ )
73
+
74
+ ui.label("服务器正在运行,离开将自动关闭服务器").classes(
75
+ "text-body1 text-grey-8 mb-2"
76
+ )
77
+ ui.label("确定要离开吗?").classes("text-body2 text-grey-7 mb-6")
78
+
79
+ with ui.row().classes("w-full gap-3 justify-end"):
80
+ ui.button("取消", on_click=leave_dialog.close).props(
81
+ "outline color=grey-8 no-caps"
82
+ ).classes("action-button")
83
+
84
+ async def confirm_leave():
85
+ await export_service.stop_server()
86
+ server_running["value"] = False
87
+ await ui.run_javascript("window.serverRunning = false;")
88
+ leave_dialog.close()
89
+ ui.navigate.to("/sessions")
90
+
91
+ ui.button("确认离开", on_click=confirm_leave).props(
92
+ "unelevated color=negative no-caps"
93
+ ).classes("action-button")
94
+
95
+ leave_dialog.open()
96
+ else:
97
+ ui.navigate.to("/sessions")
98
+
99
+ ui.button(icon="arrow_back", on_click=go_back).props("flat round").tooltip("返回历史会话")
100
+ ui.label("导出会话").classes("text-h6 font-semibold text-grey-9")
101
+
102
+ # Main content - 完全垂直居中于整个视口
103
+ with (
104
+ ui.element("div")
105
+ .classes("w-full bg-white")
106
+ .style(
107
+ "position: fixed; "
108
+ "top: 0; "
109
+ "left: 0; "
110
+ "right: 0; "
111
+ "bottom: 0; "
112
+ "display: flex; "
113
+ "align-items: center; "
114
+ "justify-content: center; "
115
+ "padding: 60px 80px; "
116
+ "padding-top: calc(108px + 30px); "
117
+ "box-sizing: border-box; "
118
+ "overflow: auto;"
119
+ ),
120
+ ui.element("div").style(
121
+ "display: flex; gap: 60px; align-items: center; max-width: 100%; width: 100%;"
122
+ ),
123
+ ):
124
+ # Left column
125
+ with ui.column().classes("gap-8").style("width: 360px; flex-shrink: 0;"):
126
+ # Title
127
+ with ui.column().classes("gap-2 mb-6"):
128
+ candidate_words = [":)", ":D", "🚀", "🎉", "😄", "👍"]
129
+ ui.label(random.choice(candidate_words)).classes("text-h3 font-bold text-grey-9")
130
+ ui.label("选择导出方式").classes("text-h2 gradient-text").style("line-height: 1.2;")
131
+
132
+ # Stats
133
+ with ui.card().classes("glass-card p-5 w-full").style("margin-top: 40px;"):
134
+ ui.label("统计").classes("text-subtitle2 font-semibold mb-4 text-grey-8")
135
+
136
+ with ui.column().classes("gap-3 w-full"):
137
+ with ui.row().classes("items-center justify-between w-full"):
138
+ ui.label("会话总数").classes("text-xs text-grey-7")
139
+ ui.label(str(len(sessions))).classes("text-body1 font-bold text-primary")
140
+
141
+ with ui.row().classes("items-center justify-between w-full"):
142
+ ui.label("服务器状态").classes("text-xs text-grey-7")
143
+ status_label = ui.label("未启动").classes(
144
+ "text-body1 font-bold text-grey-6"
145
+ )
146
+
147
+ # Right column - 2x2 grid
148
+ with ui.element("div").style(
149
+ "flex: 1; "
150
+ "display: grid; "
151
+ "grid-template-columns: repeat(2, 1fr); "
152
+ "gap: 20px; "
153
+ "max-width: 850px; "
154
+ "margin-left: auto;"
155
+ ):
156
+ # Card 1: Server export
157
+ server_card = (
158
+ ui.card()
159
+ .classes("super-card cursor-pointer")
160
+ .style(
161
+ "height: 220px; "
162
+ "display: flex; "
163
+ "flex-direction: column; "
164
+ "padding: 1.5rem; "
165
+ "box-sizing: border-box;"
166
+ )
167
+ )
168
+
169
+ with server_card:
170
+ server_icon = (
171
+ ui.icon("cloud", size="3em")
172
+ .classes("text-primary rotate-icon")
173
+ .style("flex-shrink: 0; margin-bottom: 0.75rem;")
174
+ )
175
+ with ui.column().classes("gap-2").style("flex: 1;"):
176
+ ui.label("服务器导出").classes("text-h6 font-bold text-grey-9")
177
+ ui.label("通过浏览器访问导出页面").classes("text-sm text-grey-7")
178
+ server_btn = (
179
+ ui.button("启动", icon="arrow_forward")
180
+ .props("color=primary flat dense")
181
+ .classes("press-button")
182
+ .style(
183
+ "align-self: flex-end; "
184
+ "flex-shrink: 0; "
185
+ "background: transparent ! important; "
186
+ "box-shadow: none !important;"
187
+ )
188
+ )
189
+
190
+ async def start_server_export():
191
+ """Start or stop server export."""
192
+ if server_running["value"]:
193
+ with (
194
+ ui.dialog() as stop_dialog,
195
+ ui.card()
196
+ .classes("dialog-card")
197
+ .style("width: 450px; padding: 28px; border-radius: 16px;"),
198
+ ):
199
+ with ui.row().classes("w-full items-center mb-6"):
200
+ ui.icon("warning", size="xl").classes("text-warning")
201
+ ui.label("确认关闭").classes(
202
+ "text-h5 font-bold text-grey-9 ml-3 flex-1"
203
+ )
204
+ ui.button(icon="close", on_click=stop_dialog.close).props(
205
+ "flat round dense"
206
+ )
207
+
208
+ ui.label("确定要停止服务器吗?").classes("text-body1 text-grey-8 mb-2")
209
+ ui.label("其他设备将无法继续访问").classes("text-body2 text-grey-7 mb-6")
210
+
211
+ with ui.row().classes("w-full gap-3 justify-end"):
212
+ ui.button("取消", on_click=stop_dialog.close).props(
213
+ "outline color=grey-8 no-caps"
214
+ ).classes("action-button")
215
+
216
+ async def confirm_stop():
217
+ await export_service.stop_server()
218
+ server_running["value"] = False
219
+ await ui.run_javascript("window.serverRunning = false;")
220
+ status_label.text = "未启动"
221
+ status_label.classes(remove="text-positive", add="text-grey-6")
222
+
223
+ # reset card UI
224
+ server_card.classes(remove="super-card-active")
225
+ server_icon.classes(remove="text-negative", add="text-primary")
226
+ server_btn.set_text("启动")
227
+ server_btn.props("icon=arrow_forward color=primary")
228
+
229
+ stop_dialog.close()
230
+ ui.notify("服务器已停止", type="info", position="top")
231
+
232
+ ui.button("确认停止", on_click=confirm_stop).props(
233
+ "unelevated color=negative no-caps"
234
+ ).classes("action-button")
235
+
236
+ stop_dialog.open()
237
+ return
238
+
239
+ # Start server
240
+ try:
241
+ info = await export_service.start_server()
242
+ server_running["value"] = True
243
+ await ui.run_javascript("window.serverRunning = true;")
244
+ status_label.text = "运行中"
245
+ status_label.classes(remove="text-grey-6", add="text-positive")
246
+
247
+ server_card.classes(add="super-card-active")
248
+ server_icon.classes(remove="text-primary", add="text-negative")
249
+ server_btn.set_text("停止")
250
+ server_btn.props("icon=stop color=negative")
251
+
252
+ # Show success dialog
253
+ with (
254
+ ui.dialog() as success_dialog,
255
+ ui.card()
256
+ .classes("dialog-card")
257
+ .style("width: 550px; padding: 32px; border-radius: 16px;"),
258
+ ):
259
+ with ui.row().classes("w-full items-center mb-6"):
260
+ ui.icon("check_circle", size="xl").classes("text-positive")
261
+ ui.label("服务器已启动").classes(
262
+ "text-h5 font-bold text-grey-9 q-ml-sm"
263
+ )
264
+ ui.space()
265
+ ui.button(icon="close", on_click=success_dialog.close).props(
266
+ "flat round dense"
267
+ )
268
+
269
+ # Server info
270
+ with ui.element("div").classes("info-box"):
271
+ ui.label("访问地址").classes("text-xs text-grey-6 mb-2")
272
+
273
+ with ui.row().classes("items-center gap-2 mb-3"):
274
+ ui.label(info.url).classes("text-body1 font-mono text-primary")
275
+
276
+ async def copy_url():
277
+ await ui.run_javascript(
278
+ f"navigator.clipboard.writeText('{info.url}')"
279
+ )
280
+ ui.notify("已复制", type="positive", position="top")
281
+
282
+ ui.button(icon="content_copy", on_click=copy_url).props(
283
+ "flat dense round size=sm"
284
+ )
285
+
286
+ ui.element("div").classes("divider")
287
+
288
+ ui.label(f"主机: {info.host}").classes("text-sm text-grey-7 mb-1")
289
+ ui.label(f"端口: {info.port}").classes("text-sm text-grey-7")
290
+
291
+ ui.label("在其他设备的浏览器中打开上述地址即可访问").classes(
292
+ "text-body2 text-grey-6 mt-4 mb-6"
293
+ )
294
+
295
+ ui.button("知道了", on_click=success_dialog.close).props(
296
+ "unelevated color=primary no-caps"
297
+ ).classes("action-button w-full").style("height: 48px;")
298
+
299
+ success_dialog.open()
300
+
301
+ except Exception:
302
+ ui.notify("启动失败", type="negative", position="top")
303
+
304
+ server_card.on("click", start_server_export)
305
+
306
+ # Card 2: USB export
307
+ async def start_usb_export():
308
+ """Start USB export."""
309
+ devices = await export_service.list_usb_devices()
310
+
311
+ if not devices:
312
+ with (
313
+ ui.dialog() as no_device_dialog,
314
+ ui.card()
315
+ .classes("dialog-card")
316
+ .style("width: 500px; padding: 32px; border-radius: 16px;"),
317
+ ):
318
+ with ui.row().classes("w-full items-center mb-6"):
319
+ ui.icon("usb_off", size="xl").classes("text-grey-4")
320
+ ui.label("未检测到U盘").classes("text-h5 font-bold text-grey-9 q-ml-sm")
321
+ ui.space()
322
+ ui.button(icon="close", on_click=no_device_dialog.close).props(
323
+ "flat round dense"
324
+ )
325
+
326
+ with ui.column().classes("items-center py-4 gap-3"):
327
+ ui.label("请插入U盘后重试").classes("text-body1 text-grey-7")
328
+
329
+ async def refresh():
330
+ no_device_dialog.close()
331
+ await start_usb_export()
332
+
333
+ ui.button("刷新", icon="refresh", on_click=refresh).props(
334
+ "flat no-caps"
335
+ ).classes("action-button mt-4")
336
+
337
+ no_device_dialog.open()
338
+ return
339
+
340
+ device = devices[0]
341
+
342
+ with (
343
+ ui.dialog() as usb_dialog,
344
+ ui.card()
345
+ .classes("dialog-card")
346
+ .style("width: 550px; padding: 32px; border-radius: 16px;"),
347
+ ):
348
+ with ui.row().classes("w-full items-center mb-6"):
349
+ ui.icon("check_circle", size="xl").classes("text-positive")
350
+ ui.label("检测到U盘").classes("text-h5 font-bold text-grey-9 q-ml-sm")
351
+ ui.space()
352
+ ui.button(icon="close", on_click=usb_dialog.close).props("flat round dense")
353
+
354
+ with ui.element("div").classes("info-box"):
355
+ with ui.row().classes("items-center gap-3 mb-3"):
356
+ ui.icon("usb", size="lg").classes("text-secondary")
357
+ ui.label(device.label or "未命名设备").classes(
358
+ "text-body1 font-semibold"
359
+ )
360
+
361
+ if device.vendor or device.model:
362
+ ui.label(f"{device.vendor or ''} {device.model or ''}").classes(
363
+ "text-sm text-grey-6 mb-2"
364
+ )
365
+
366
+ ui.element("div").classes("divider")
367
+
368
+ ui.label(f"挂载点: {device.mount_point}").classes(
369
+ "text-sm text-grey-7 mb-1"
370
+ )
371
+ if device.size_bytes:
372
+ size_gb = device.size_bytes / (1024**3)
373
+ ui.label(f"容量: {size_gb:.2f} GB").classes("text-sm text-grey-7")
374
+
375
+ with (
376
+ ui.element("div").classes("export-summary-box mt-4"),
377
+ ui.row().classes("items-center justify-between"),
378
+ ):
379
+ ui.label("会话数量").classes("text-sm text-grey-6")
380
+ ui.label(f"{len(sessions)} 个").classes("text-body1 font-semibold")
381
+
382
+ async def do_export():
383
+ export_btn.props("loading")
384
+
385
+ try:
386
+ session_ids = [s.id for s in sessions]
387
+ result = await export_service.export_to_usb(session_ids, device)
388
+
389
+ usb_dialog.close()
390
+
391
+ if result.success:
392
+ ui.notify(
393
+ f"成功导出 {result.success_count} 个会话",
394
+ type="positive",
395
+ position="top",
396
+ )
397
+ else:
398
+ ui.notify(
399
+ f"部分失败: 成功 {result.success_count}/{result.total}",
400
+ type="warning",
401
+ position="top",
402
+ )
403
+
404
+ except Exception:
405
+ ui.notify("导出失败", type="negative", position="top")
406
+ finally:
407
+ export_btn.props(remove="loading")
408
+
409
+ export_btn = (
410
+ ui.button("开始导出", on_click=do_export)
411
+ .props("unelevated color=primary size=lg no-caps")
412
+ .classes("action-button w-full mt-6")
413
+ .style("height: 48px;")
414
+ )
415
+
416
+ usb_dialog.open()
417
+
418
+ with (
419
+ ui.card()
420
+ .classes("super-card cursor-pointer")
421
+ .on("click", start_usb_export)
422
+ .style(
423
+ "height: 220px; "
424
+ "display: flex; "
425
+ "flex-direction: column; "
426
+ "padding: 1.5rem; "
427
+ "box-sizing: border-box;"
428
+ )
429
+ ):
430
+ ui.icon("usb", size="3em").classes("text-secondary rotate-icon").style(
431
+ "flex-shrink: 0; margin-bottom: 0.75rem;"
432
+ )
433
+ with ui.column().classes("gap-2").style("flex: 1;"):
434
+ ui.label("U盘导出").classes("text-h6 font-bold text-grey-9")
435
+ ui.label("直接导出到U盘设备").classes("text-sm text-grey-7")
436
+ ui.button("开始", icon="arrow_forward").props("color=secondary flat dense").classes(
437
+ "press-button"
438
+ ).style(
439
+ "align-self: flex-end; "
440
+ "flex-shrink: 0; "
441
+ "background: transparent !important; "
442
+ "box-shadow: none !important;"
443
+ )