郑永雄 Claude Opus 4.8 commited on
Commit
a46efa8
·
1 Parent(s): 508a0ac

feat(web): FastAPI 后端 + Streamlit 前端

Browse files

- FastAPI: /api/process, /api/stream (WebSocket), /api/scenes, /api/models, /api/health
- Streamlit: 音频上传+录音, 原始转录vs纠正对比, 实体/意图/行动项展示
- 启动脚本: python web/run.py 一键启动

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (30) hide show
  1. .claude/settings.local.json +10 -0
  2. README.md +22 -0
  3. src/speechbridge/__pycache__/__init__.cpython-311.pyc +0 -0
  4. src/speechbridge/__pycache__/config.cpython-311.pyc +0 -0
  5. src/speechbridge/__pycache__/models.cpython-311.pyc +0 -0
  6. src/speechbridge/__pycache__/pipeline.cpython-311.pyc +0 -0
  7. src/speechbridge/asr/__pycache__/__init__.cpython-311.pyc +0 -0
  8. src/speechbridge/asr/__pycache__/engine.cpython-311.pyc +0 -0
  9. src/speechbridge/asr/__pycache__/whisper_engine.cpython-311.pyc +0 -0
  10. src/speechbridge/audio/__pycache__/__init__.cpython-311.pyc +0 -0
  11. src/speechbridge/audio/__pycache__/loader.cpython-311.pyc +0 -0
  12. src/speechbridge/audio/__pycache__/preprocessor.cpython-311.pyc +0 -0
  13. src/speechbridge/audio/__pycache__/vad.cpython-311.pyc +0 -0
  14. src/speechbridge/output/__pycache__/__init__.cpython-311.pyc +0 -0
  15. src/speechbridge/output/__pycache__/json_formatter.cpython-311.pyc +0 -0
  16. src/speechbridge/output/__pycache__/text_formatter.cpython-311.pyc +0 -0
  17. src/speechbridge/output/__pycache__/tts_engine.cpython-311.pyc +0 -0
  18. src/speechbridge/recovery/__pycache__/__init__.cpython-311.pyc +0 -0
  19. src/speechbridge/recovery/__pycache__/engine.cpython-311.pyc +0 -0
  20. src/speechbridge/recovery/__pycache__/llm_engine.cpython-311.pyc +0 -0
  21. src/speechbridge/recovery/__pycache__/prompts.cpython-311.pyc +0 -0
  22. tests/__pycache__/conftest.cpython-311-pytest-9.0.3.pyc +0 -0
  23. tests/__pycache__/test_audio_loader.cpython-311-pytest-9.0.3.pyc +0 -0
  24. tests/__pycache__/test_recovery.cpython-311-pytest-9.0.3.pyc +0 -0
  25. tests/__pycache__/test_vad.cpython-311-pytest-9.0.3.pyc +0 -0
  26. web/__pycache__/api.cpython-311.pyc +0 -0
  27. web/__pycache__/app.cpython-311.pyc +0 -0
  28. web/api.py +236 -0
  29. web/app.py +194 -0
  30. web/run.py +54 -0
.claude/settings.local.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(pip install *)",
5
+ "Bash(pip3 install *)",
6
+ "Bash(uv --version)",
7
+ "Bash(uv pip *)"
8
+ ]
9
+ }
10
+ }
README.md CHANGED
@@ -71,6 +71,24 @@ print(result.recovery.corrected_text)
71
  print(result.recovery.intent)
72
  ```
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  ## 技术栈
75
 
76
  | 组件 | 技术 |
@@ -93,6 +111,10 @@ speechbridge/
93
  │ ├── config.py # 配置管理
94
  │ ├── models.py # 数据模型
95
  │ └── pipeline.py # 主管线
 
 
 
 
96
  ├── cli.py # 命令行入口
97
  ├── tests/ # 测试
98
  └── examples/ # 示例
 
71
  print(result.recovery.intent)
72
  ```
73
 
74
+ ### Web 界面
75
+
76
+ ```bash
77
+ # 启动 FastAPI + Streamlit
78
+ python web/run.py
79
+
80
+ # 或分别启动
81
+ uvicorn web.api:app --reload --port 8000 # API: http://localhost:8000/docs
82
+ streamlit run web/app.py # UI: http://localhost:8501
83
+ ```
84
+
85
+ **API 端点**:
86
+ - `POST /api/process` — 上传音频处理
87
+ - `WS /api/stream` — WebSocket 实时音频流
88
+ - `GET /api/scenes` — 场景列表
89
+ - `GET /api/models` — 可用模型
90
+ - `GET /api/health` — 健康检查
91
+
92
  ## 技术栈
93
 
94
  | 组件 | 技术 |
 
111
  │ ├── config.py # 配置管理
112
  │ ├── models.py # 数据模型
113
  │ └── pipeline.py # 主管线
114
+ ├── web/
115
+ │ ├── api.py # FastAPI 后端
116
+ │ ├── app.py # Streamlit 前端
117
+ │ └── run.py # 启动脚本
118
  ├── cli.py # 命令行入口
119
  ├── tests/ # 测试
120
  └── examples/ # 示例
src/speechbridge/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (260 Bytes). View file
 
src/speechbridge/__pycache__/config.cpython-311.pyc ADDED
Binary file (6.42 kB). View file
 
src/speechbridge/__pycache__/models.cpython-311.pyc ADDED
Binary file (4.7 kB). View file
 
src/speechbridge/__pycache__/pipeline.cpython-311.pyc ADDED
Binary file (7.91 kB). View file
 
src/speechbridge/asr/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (179 Bytes). View file
 
src/speechbridge/asr/__pycache__/engine.cpython-311.pyc ADDED
Binary file (1.51 kB). View file
 
src/speechbridge/asr/__pycache__/whisper_engine.cpython-311.pyc ADDED
Binary file (5.11 kB). View file
 
src/speechbridge/audio/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (181 Bytes). View file
 
src/speechbridge/audio/__pycache__/loader.cpython-311.pyc ADDED
Binary file (5.67 kB). View file
 
src/speechbridge/audio/__pycache__/preprocessor.cpython-311.pyc ADDED
Binary file (3.15 kB). View file
 
src/speechbridge/audio/__pycache__/vad.cpython-311.pyc ADDED
Binary file (3.11 kB). View file
 
src/speechbridge/output/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (182 Bytes). View file
 
src/speechbridge/output/__pycache__/json_formatter.cpython-311.pyc ADDED
Binary file (2.44 kB). View file
 
src/speechbridge/output/__pycache__/text_formatter.cpython-311.pyc ADDED
Binary file (3.25 kB). View file
 
src/speechbridge/output/__pycache__/tts_engine.cpython-311.pyc ADDED
Binary file (2.66 kB). View file
 
src/speechbridge/recovery/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (184 Bytes). View file
 
src/speechbridge/recovery/__pycache__/engine.cpython-311.pyc ADDED
Binary file (1.26 kB). View file
 
src/speechbridge/recovery/__pycache__/llm_engine.cpython-311.pyc ADDED
Binary file (6.07 kB). View file
 
src/speechbridge/recovery/__pycache__/prompts.cpython-311.pyc ADDED
Binary file (5.04 kB). View file
 
tests/__pycache__/conftest.cpython-311-pytest-9.0.3.pyc ADDED
Binary file (2.67 kB). View file
 
tests/__pycache__/test_audio_loader.cpython-311-pytest-9.0.3.pyc ADDED
Binary file (13.4 kB). View file
 
tests/__pycache__/test_recovery.cpython-311-pytest-9.0.3.pyc ADDED
Binary file (11.6 kB). View file
 
tests/__pycache__/test_vad.cpython-311-pytest-9.0.3.pyc ADDED
Binary file (7.14 kB). View file
 
web/__pycache__/api.cpython-311.pyc ADDED
Binary file (10.9 kB). View file
 
web/__pycache__/app.cpython-311.pyc ADDED
Binary file (10.7 kB). View file
 
web/api.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI 后端"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ from fastapi import FastAPI, File, HTTPException, Query, UploadFile, WebSocket, WebSocketDisconnect
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from fastapi.responses import FileResponse, JSONResponse
12
+
13
+ from src.speechbridge.config import PipelineConfig
14
+ from src.speechbridge.models import OutputFormat, Scene
15
+ from src.speechbridge.pipeline import SpeechBridgePipeline
16
+
17
+ logging.basicConfig(level=logging.INFO)
18
+ logger = logging.getLogger(__name__)
19
+
20
+ app = FastAPI(
21
+ title="SpeechBridge API",
22
+ description="智能语义恢复系统 — 理解用户想表达什么",
23
+ version="0.1.0",
24
+ )
25
+
26
+ # CORS
27
+ app.add_middleware(
28
+ CORSMiddleware,
29
+ allow_origins=["*"],
30
+ allow_credentials=True,
31
+ allow_methods=["*"],
32
+ allow_headers=["*"],
33
+ )
34
+
35
+ # 全局管线实例
36
+ _pipeline: SpeechBridgePipeline | None = None
37
+
38
+
39
+ def get_pipeline() -> SpeechBridgePipeline:
40
+ global _pipeline
41
+ if _pipeline is None:
42
+ _pipeline = SpeechBridgePipeline(PipelineConfig.from_env())
43
+ return _pipeline
44
+
45
+
46
+ @app.get("/")
47
+ async def root():
48
+ return {"name": "SpeechBridge", "version": "0.1.0", "status": "running"}
49
+
50
+
51
+ @app.get("/api/scenes")
52
+ async def list_scenes():
53
+ """获取支持的场景列表"""
54
+ return {
55
+ "scenes": [
56
+ {"value": "general", "label": "通用", "description": "通用语义恢复"},
57
+ {"value": "meeting", "label": "会议", "description": "提取行动项和决策"},
58
+ {"value": "education", "label": "教育", "description": "课堂讨论和提问理解"},
59
+ {"value": "medical", "label": "医疗", "description": "症状和医疗术语提取"},
60
+ {"value": "accessibility", "label": "无障碍", "description": "语言障碍用户专项处理"},
61
+ ]
62
+ }
63
+
64
+
65
+ @app.get("/api/models")
66
+ async def list_models():
67
+ """获取可用模型信息"""
68
+ return {
69
+ "asr_models": ["tiny", "base", "small", "medium", "large-v3-turbo"],
70
+ "llm_providers": ["openai", "anthropic", "ollama"],
71
+ "tts_voices": [
72
+ "en-US-AriaNeural",
73
+ "en-US-GuyNeural",
74
+ "en-GB-SoniaNeural",
75
+ "zh-CN-XiaoxiaoNeural",
76
+ ],
77
+ }
78
+
79
+
80
+ @app.post("/api/process")
81
+ async def process_audio(
82
+ file: UploadFile = File(...),
83
+ scene: str = Query("general", description="应用场景"),
84
+ output_format: str = Query("json", description="输出格式: text/json/tts"),
85
+ accessibility: bool = Query(False, description="语言障碍模式"),
86
+ ):
87
+ """
88
+ 处理上传的音频文件.
89
+
90
+ - **file**: 音频文件 (WAV, MP3, M4A, FLAC, OGG)
91
+ - **scene**: 应用场景 (general/meeting/education/medical/accessibility)
92
+ - **output_format**: 输出格式 (text/json/tts)
93
+ - **accessibility**: 是否启用语言障碍模式
94
+ """
95
+ # 验证场景
96
+ try:
97
+ scene_enum = Scene(scene)
98
+ except ValueError:
99
+ raise HTTPException(400, f"不支持的场景: {scene}")
100
+
101
+ try:
102
+ output_enum = OutputFormat(output_format)
103
+ except ValueError:
104
+ raise HTTPException(400, f"不支持的输出格式: {output_format}")
105
+
106
+ # 验证文件类型
107
+ allowed_types = {"audio/wav", "audio/x-wav", "audio/mpeg", "audio/mp3",
108
+ "audio/m4a", "audio/flac", "audio/ogg", "audio/webm",
109
+ "application/octet-stream"}
110
+ if file.content_type and file.content_type not in allowed_types:
111
+ # 允许 application/octet-stream 作为 fallback
112
+ pass
113
+
114
+ # 读取文件
115
+ content = await file.read()
116
+ if len(content) == 0:
117
+ raise HTTPException(400, "文件为空")
118
+
119
+ # 处理
120
+ pipeline = get_pipeline()
121
+ if accessibility:
122
+ pipeline.config.accessibility.enabled = True
123
+
124
+ try:
125
+ result = pipeline.process(
126
+ content,
127
+ filename=file.filename,
128
+ scene=scene_enum,
129
+ output_format=output_enum,
130
+ )
131
+ except Exception as e:
132
+ logger.error(f"处理失败: {e}", exc_info=True)
133
+ raise HTTPException(500, f"处理失败: {str(e)}")
134
+
135
+ # 返回结果
136
+ if output_format == "tts" and result.output_audio_path:
137
+ return FileResponse(
138
+ result.output_audio_path,
139
+ media_type="audio/mpeg",
140
+ filename="speechbridge_output.mp3",
141
+ )
142
+
143
+ return JSONResponse({
144
+ "asr": {
145
+ "text": result.asr.text,
146
+ "confidence": result.asr.segment_confidence,
147
+ "language": result.asr.language,
148
+ "duration": result.asr.duration,
149
+ },
150
+ "recovery": {
151
+ "original_text": result.recovery.original_text,
152
+ "corrected_text": result.recovery.corrected_text,
153
+ "intent": result.recovery.intent,
154
+ "entities": [
155
+ {"name": e.name, "type": e.type, "value": e.value}
156
+ for e in result.recovery.entities
157
+ ],
158
+ "actions": result.recovery.actions,
159
+ "corrections": [
160
+ {"original": c.original, "corrected": c.corrected, "reason": c.reason}
161
+ for c in result.recovery.corrections
162
+ ],
163
+ "confidence": result.recovery.confidence,
164
+ "processing_time": result.recovery.processing_time,
165
+ },
166
+ "total_time": result.total_time,
167
+ })
168
+
169
+
170
+ @app.websocket("/api/stream")
171
+ async def stream_audio(websocket: WebSocket):
172
+ """
173
+ WebSocket 实时音频流处理.
174
+
175
+ 客户端发送音频数据块, 服务端返回识别和恢复结果.
176
+ 消息协议:
177
+ - 客户端发送二进制帧: 音频数据 (16kHz mono float32)
178
+ - 客户端发送文本帧: JSON 配置 {"scene": "general", "accessibility": false}
179
+ - 服务端返回文本帧: JSON 结果
180
+ """
181
+ await websocket.accept()
182
+ logger.info("WebSocket 连接已建立")
183
+
184
+ scene = Scene.GENERAL
185
+ accessibility = False
186
+ audio_chunks = []
187
+
188
+ try:
189
+ while True:
190
+ message = await websocket.receive()
191
+
192
+ if "text" in message:
193
+ # 配置消息
194
+ import json
195
+ try:
196
+ config = json.loads(message["text"])
197
+ scene = Scene(config.get("scene", "general"))
198
+ accessibility = config.get("accessibility", False)
199
+
200
+ # 如果收到 finalize 信号, 处理累积的音频
201
+ if config.get("finalize") and audio_chunks:
202
+ pipeline = get_pipeline()
203
+ if accessibility:
204
+ pipeline.config.accessibility.enabled = True
205
+
206
+ result = pipeline.process_stream(
207
+ audio_chunks, scene=scene, output_format=OutputFormat.JSON
208
+ )
209
+ await websocket.send_text(result.output_text)
210
+ audio_chunks = []
211
+
212
+ except (json.JSONDecodeError, ValueError) as e:
213
+ await websocket.send_text(json.dumps({"error": str(e)}))
214
+
215
+ elif "bytes" in message:
216
+ # 音频数据
217
+ audio_chunks.append(message["bytes"])
218
+
219
+ except WebSocketDisconnect:
220
+ logger.info("WebSocket 连接已断开")
221
+ except Exception as e:
222
+ logger.error(f"WebSocket 错误: {e}", exc_info=True)
223
+ try:
224
+ await websocket.close()
225
+ except Exception:
226
+ pass
227
+
228
+
229
+ @app.get("/api/health")
230
+ async def health():
231
+ """健康检查"""
232
+ pipeline = get_pipeline()
233
+ return {
234
+ "status": "healthy",
235
+ "asr_loaded": pipeline.asr.is_loaded(),
236
+ }
web/app.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Streamlit 前端"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ import streamlit as st
10
+
11
+ from src.speechbridge.config import PipelineConfig
12
+ from src.speechbridge.models import OutputFormat, Scene
13
+ from src.speechbridge.pipeline import SpeechBridgePipeline
14
+
15
+
16
+ @st.cache_resource
17
+ def load_pipeline():
18
+ """加载管线 (缓存)"""
19
+ config = PipelineConfig.from_env()
20
+ return SpeechBridgePipeline(config)
21
+
22
+
23
+ def main():
24
+ st.set_page_config(
25
+ page_title="SpeechBridge",
26
+ page_icon="🌉",
27
+ layout="wide",
28
+ )
29
+
30
+ st.title("🌉 SpeechBridge")
31
+ st.caption("智能语义恢复系统 — 理解你想表达什么,而不仅仅是识别你说了什么")
32
+
33
+ # 侧边栏配置
34
+ with st.sidebar:
35
+ st.header("⚙️ 配置")
36
+
37
+ scene = st.selectbox(
38
+ "应用场景",
39
+ options=[s.value for s in Scene],
40
+ format_func={
41
+ "general": "🌐 通用",
42
+ "meeting": "🏢 会议",
43
+ "education": "📚 教育",
44
+ "medical": "🏥 医疗",
45
+ "accessibility": "♿ 无障碍",
46
+ }.get,
47
+ index=0,
48
+ )
49
+
50
+ accessibility = st.toggle("语言障碍模式", value=False)
51
+
52
+ st.divider()
53
+
54
+ output_format = st.radio(
55
+ "输出格式",
56
+ options=["text", "json"],
57
+ index=0,
58
+ )
59
+
60
+ st.divider()
61
+
62
+ st.markdown("### 📖 使用说明")
63
+ st.markdown("""
64
+ 1. 上传音频文件或录制语音
65
+ 2. 选择应用场景
66
+ 3. 查看语义恢复结果
67
+
68
+ **支持格式**: WAV, MP3, M4A, FLAC, OGG
69
+ """)
70
+
71
+ # 主区域: 两列布局
72
+ col_input, col_output = st.columns(2)
73
+
74
+ with col_input:
75
+ st.header("🎤 输入")
76
+
77
+ # 输入方式选择
78
+ input_method = st.radio(
79
+ "输入方式",
80
+ options=["upload", "record"],
81
+ format_func={"upload": "📁 上传文件", "record": "🎙️ 实时录音"}.get,
82
+ horizontal=True,
83
+ )
84
+
85
+ audio_data = None
86
+ filename = None
87
+
88
+ if input_method == "upload":
89
+ uploaded = st.file_uploader(
90
+ "上传音频文件",
91
+ type=["wav", "mp3", "m4a", "flac", "ogg"],
92
+ help="支持 WAV, MP3, M4A, FLAC, OGG 格式",
93
+ )
94
+ if uploaded:
95
+ audio_data = uploaded.read()
96
+ filename = uploaded.name
97
+ st.audio(audio_data, format=f"audio/{uploaded.name.split('.')[-1]}")
98
+
99
+ else:
100
+ recorded = st.audio_input("录制语音")
101
+ if recorded:
102
+ audio_data = recorded.read()
103
+ filename = "recording.wav"
104
+ st.audio(audio_data, format="audio/wav")
105
+
106
+ with col_output:
107
+ st.header("📝 输出")
108
+
109
+ if audio_data and st.button("🚀 开始处理", type="primary", use_container_width=True):
110
+ pipeline = load_pipeline()
111
+
112
+ if accessibility:
113
+ pipeline.config.accessibility.enabled = True
114
+
115
+ scene_enum = Scene(scene)
116
+ output_enum = OutputFormat(output_format)
117
+
118
+ with st.spinner("处理中..."):
119
+ try:
120
+ result = pipeline.process(
121
+ audio_data,
122
+ filename=filename,
123
+ scene=scene_enum,
124
+ output_format=output_enum,
125
+ )
126
+ except Exception as e:
127
+ st.error(f"处理失败: {e}")
128
+ return
129
+
130
+ # 存储结果到 session state
131
+ st.session_state["result"] = result
132
+
133
+ # 显示结果
134
+ if "result" in st.session_state:
135
+ result = st.session_state["result"]
136
+
137
+ # 原始转录 vs 纠正文本
138
+ st.subheader("对比")
139
+ col_orig, col_fixed = st.columns(2)
140
+
141
+ with col_orig:
142
+ st.markdown("**原始转录**")
143
+ st.info(result.asr.text if result.asr.text else "(无)")
144
+
145
+ with col_fixed:
146
+ st.markdown("**纠正后文本**")
147
+ st.success(result.recovery.corrected_text if result.recovery.corrected_text else "(无)")
148
+
149
+ # 意图
150
+ if result.recovery.intent:
151
+ st.subheader("🎯 意图")
152
+ st.write(result.recovery.intent)
153
+
154
+ # 实体
155
+ if result.recovery.entities:
156
+ st.subheader("📦 提取的实体")
157
+ entity_data = [
158
+ {"名称": e.name, "类型": e.type, "值": e.value}
159
+ for e in result.recovery.entities
160
+ ]
161
+ st.dataframe(entity_data, use_container_width=True)
162
+
163
+ # 纠正详情
164
+ if result.recovery.corrections:
165
+ st.subheader("🔧 纠正详情")
166
+ for c in result.recovery.corrections:
167
+ st.markdown(f'- "{c.original}" → "{c.corrected}" _{c.reason}_')
168
+
169
+ # 行动项
170
+ if result.recovery.actions:
171
+ st.subheader("📋 行动项")
172
+ for i, a in enumerate(result.recovery.actions, 1):
173
+ st.markdown(f"{i}. {a}")
174
+
175
+ # 元信息
176
+ st.divider()
177
+ meta_cols = st.columns(4)
178
+ with meta_cols[0]:
179
+ st.metric("ASR 置信度", f"{result.asr.segment_confidence:.2f}")
180
+ with meta_cols[1]:
181
+ st.metric("恢复置信度", f"{result.recovery.confidence:.2f}")
182
+ with meta_cols[2]:
183
+ st.metric("语义恢复耗时", f"{result.recovery.processing_time:.2f}s")
184
+ with meta_cols[3]:
185
+ st.metric("总耗时", f"{result.total_time:.2f}s")
186
+
187
+ # JSON 原始数据
188
+ with st.expander("🔍 查看原始 JSON 数据"):
189
+ st.json(json.loads(result.output_text) if output_format == "json"
190
+ else {"text": result.output_text})
191
+
192
+
193
+ if __name__ == "__main__":
194
+ main()
web/run.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """启动 Web 服务"""
2
+
3
+ import subprocess
4
+ import sys
5
+
6
+
7
+ def main():
8
+ print("=" * 50)
9
+ print("SpeechBridge Web 服务")
10
+ print("=" * 50)
11
+ print()
12
+ print("启动中...")
13
+ print()
14
+ print("FastAPI 后端: http://localhost:8000")
15
+ print("API 文档: http://localhost:8000/docs")
16
+ print("Streamlit: http://localhost:8501")
17
+ print()
18
+ print("按 Ctrl+C 停止所有服务")
19
+ print("=" * 50)
20
+
21
+ processes = []
22
+
23
+ try:
24
+ # 启动 FastAPI
25
+ api_proc = subprocess.Popen(
26
+ [sys.executable, "-m", "uvicorn", "web.api:app", "--host", "0.0.0.0", "--port", "8000", "--reload"],
27
+ stdout=subprocess.PIPE,
28
+ stderr=subprocess.STDOUT,
29
+ )
30
+ processes.append(("FastAPI", api_proc))
31
+
32
+ # 启动 Streamlit
33
+ streamlit_proc = subprocess.Popen(
34
+ [sys.executable, "-m", "streamlit", "run", "web/app.py", "--server.port", "8501",
35
+ "--server.headless", "true"],
36
+ stdout=subprocess.PIPE,
37
+ stderr=subprocess.STDOUT,
38
+ )
39
+ processes.append(("Streamlit", streamlit_proc))
40
+
41
+ # 等待任一进程退出
42
+ for name, proc in processes:
43
+ proc.wait()
44
+
45
+ except KeyboardInterrupt:
46
+ print("\n停止服务...")
47
+ for name, proc in processes:
48
+ proc.terminate()
49
+ proc.wait()
50
+ print("已停止")
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()