jjf1999 commited on
Commit
dadde25
·
verified ·
1 Parent(s): 520df37

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +114 -98
app.py CHANGED
@@ -2,6 +2,7 @@ import os
2
  import re
3
  import shutil
4
  import tempfile
 
5
  import numpy as np
6
  import librosa
7
  import soundfile as sf
@@ -28,7 +29,7 @@ def verify_token(credentials: HTTPAuthorizationCredentials = Security(security))
28
  )
29
  return True
30
 
31
- # 亲昵口语热词配置
32
  CUSTOM_HOTWORDS = os.getenv(
33
  "HOTWORDS",
34
  "爹地 妈咪 学长 学姐 欧巴 欧尼 宝贝 亲爱的 哥哥 姐姐 弟弟 妹妹 师傅 老公 老婆 爹咪 小路 小路哥哥 因因 岚岚 甜甜 芝芝瓜 小选 小蛇 宝宝 叶瑄 艾因 路辰 罗夏 司岚 星星嘟嘟 牧首 教宗 虎先生 小叶哥哥"
@@ -46,6 +47,7 @@ COLLOQUIAL_CORRECTIONS = {
46
  }
47
 
48
  def correct_colloquial_words(text: str) -> str:
 
49
  for pattern, target in COLLOQUIAL_CORRECTIONS.items():
50
  text = re.sub(pattern, target, text)
51
  return text
@@ -72,16 +74,16 @@ def preprocess_audio(input_path: str, target_sr: int = 16000):
72
  if len(y) == 0:
73
  return input_path, 0.0
74
 
75
- duration = len(y) / sr
76
 
77
  # 适度频谱降噪 (强度 0.6)
78
  try:
79
  y = nr.reduce_noise(y=y, sr=sr, stationary=True, prop_decrease=0.6)
80
- except Exception:
81
- pass
82
 
83
  # 峰值归一化至 -0.5 dB
84
- max_val = np.max(np.abs(y))
85
  if max_val > 0:
86
  y = (y / max_val) * 0.95
87
 
@@ -89,87 +91,93 @@ def preprocess_audio(input_path: str, target_sr: int = 16000):
89
  sf.write(processed_path, y, sr, subtype="PCM_16")
90
  return processed_path, duration
91
 
92
- # ================= 4. 语调时序与情绪动态解析 =================
93
- def extract_prosody_timeline(emo_res, total_duration: float, window_size: float = 0.5):
94
  """
95
- 将 Emotion2Vec+ 的帧级时序概率矩阵聚合为易读的时间轴
 
96
  """
97
- if not emo_res or len(emo_res) == 0:
98
- return []
99
-
100
- item = emo_res[0]
101
-
102
- # 尝试提取帧级时序矩阵 (Shape: [T, Num_Classes] 或 scores 列表)
103
- feats = item.get("feats") if "feats" in item else item.get("scores")
104
-
105
- # 若模型仅返回整句级结果时的安全降级
106
- if feats is None or not isinstance(feats, (list, np.ndarray)) or len(feats) == 0:
107
- return [{
108
- "start": 0.0,
109
- "end": round(total_duration, 2),
110
- "tone": item.get("key", item.get("emotion", "neutral")),
111
- "confidence": round(float(max(item.get("scores", {}).values())), 4) if item.get("scores") else 1.0,
112
- "energy_state": "moderate"
113
- }]
114
-
115
- feats = np.array(feats)
116
- total_frames = len(feats)
117
- if total_frames == 0:
118
- return []
119
-
120
- # 标签名称映射列表 (Emotion2Vec 典型标签索引)
121
  labels = ["angry", "disgusted", "fearful", "happy", "neutral", "sad", "surprised"]
122
-
123
  timeline = []
124
- # 按 window_size (秒) 划分时间窗并计算均值
125
- frames_per_window = max(1, int(total_frames * (window_size / max(total_duration, 0.1))))
126
-
127
- num_windows = int(np.ceil(total_frames / frames_per_window))
128
 
129
- for i in range(num_windows):
130
- start_frame = i * frames_per_window
131
- end_frame = min(total_frames, (i + 1) * frames_per_window)
132
-
133
- window_feat = feats[start_frame:end_frame]
134
- mean_scores = np.mean(window_feat, axis=0) if window_feat.ndim > 1 else window_feat
135
-
136
- start_time = round(i * window_size, 2)
137
- end_time = round(min(total_duration, (i + 1) * window_size), 2)
138
-
139
- # 找出当前时间段最突出的情绪/语调
140
- top_idx = int(np.argmax(mean_scores)) if len(mean_scores) == len(labels) else 0
141
- top_label = labels[top_idx] if top_idx < len(labels) else "neutral"
142
- conf = float(mean_scores[top_idx]) if top_idx < len(mean_scores) else 0.5
143
 
144
- # 根据情绪类型判定当前声学唤醒度/语调状态
145
- if top_label in ["angry", "surprised"]:
146
- energy_state = "high / rising" # 激昂 / 语调上扬
147
- elif top_label in ["sad"]:
148
- energy_state = "low / falling" # 低沉 / 语调下沉
149
- elif top_label == "happy":
150
- energy_state = "cheerful / dynamic" # 轻快 / 活跃
151
- else:
152
- energy_state = "moderate / flat" # 平稳 / ���常
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
- timeline.append({
155
- "start": start_time,
156
- "end": end_time,
157
- "tone": top_label,
158
- "confidence": round(conf, 4),
159
- "energy_state": energy_state
160
- })
 
 
161
 
162
  return timeline
163
 
164
  def evaluate_tone_and_emotion(text: str, emotion_scores: dict, raw_top_emotion: str):
165
  """全局综合语气判定:结合声学分数与文本关键词"""
166
- top_score = emotion_scores.get(raw_top_emotion, 0.0)
167
 
168
  # 撒娇 / 卖萌判定
169
- happy_score = emotion_scores.get("happy", 0.0)
170
- surprised_score = emotion_scores.get("surprised", 0.0)
171
  has_soft_words = bool(re.search(r"[嘛啦呀吧啵呗嘤~~…]", text))
172
-
173
  if (happy_score + surprised_score >= 0.55) and has_soft_words:
174
  return "coquetry", top_score
175
 
@@ -225,23 +233,32 @@ async def transcribe_audio(
225
  input=processed_path,
226
  granularity="utterance"
227
  )
228
-
229
  raw_top_emotion = "neutral"
230
  emotion_scores = {}
231
  if emo_res_global and len(emo_res_global) > 0:
232
  item = emo_res_global[0]
233
- raw_top_emotion = item.get("key", item.get("emotion", "neutral"))
234
  raw_scores = item.get("scores", {})
235
- emotion_scores = {k: round(float(v), 4) for k, v in raw_scores.items()}
 
236
 
237
  # 4. Emotion2Vec+ 提取帧级动态时序 (Frame-level)
238
- emo_res_frame = emo_model.generate(
239
- input=processed_path,
240
- granularity="frame"
241
- )
242
-
 
 
 
243
  # 生成语调变化时间轴 (按 0.5s 窗口聚合)
244
- prosody_timeline = extract_prosody_timeline(emo_res_frame, duration, window_size=0.5)
 
 
 
 
 
245
 
246
  # 5. 全局多模态语气裁决
247
  final_emotion, confidence = evaluate_tone_and_emotion(
@@ -250,33 +267,32 @@ async def transcribe_audio(
250
  raw_top_emotion
251
  )
252
 
253
- # 6. 按客户端格式输出
 
 
 
 
 
 
 
 
 
 
254
  if response_format == "text":
255
  return PlainTextResponse(clean_text)
256
  elif response_format == "verbose_json":
257
- return JSONResponse({
258
  "task": "transcribe",
259
- "language": language,
260
- "duration": round(duration, 2),
261
- "text": clean_text,
262
- "overall_emotion": final_emotion,
263
- "confidence": round(float(confidence), 4),
264
- "emotion_scores": emotion_scores,
265
- "prosody_timeline": prosody_timeline,
266
  "segments": []
267
  })
 
268
  else:
269
- # 默认 json 格式
270
- return JSONResponse({
271
- "text": clean_text,
272
- "duration": round(duration, 2),
273
- "overall_emotion": final_emotion,
274
- "confidence": round(float(confidence), 4),
275
- "emotion_scores": emotion_scores,
276
- "prosody_timeline": prosody_timeline
277
- })
278
 
279
  except Exception as e:
 
 
280
  raise HTTPException(status_code=500, detail=str(e))
281
  finally:
282
  # 清理临时文件
 
2
  import re
3
  import shutil
4
  import tempfile
5
+ import traceback
6
  import numpy as np
7
  import librosa
8
  import soundfile as sf
 
29
  )
30
  return True
31
 
32
+ # 亲昵口语热词配置(完整保留原有全部人物与称呼)
33
  CUSTOM_HOTWORDS = os.getenv(
34
  "HOTWORDS",
35
  "爹地 妈咪 学长 学姐 欧巴 欧尼 宝贝 亲爱的 哥哥 姐姐 弟弟 妹妹 师傅 老公 老婆 爹咪 小路 小路哥哥 因因 岚岚 甜甜 芝芝瓜 小选 小蛇 宝宝 叶瑄 艾因 路辰 罗夏 司岚 星星嘟嘟 牧首 教宗 虎先生 小叶哥哥"
 
47
  }
48
 
49
  def correct_colloquial_words(text: str) -> str:
50
+ """自动修复口语常见同音错别字"""
51
  for pattern, target in COLLOQUIAL_CORRECTIONS.items():
52
  text = re.sub(pattern, target, text)
53
  return text
 
74
  if len(y) == 0:
75
  return input_path, 0.0
76
 
77
+ duration = float(len(y) / sr)
78
 
79
  # 适度频谱降噪 (强度 0.6)
80
  try:
81
  y = nr.reduce_noise(y=y, sr=sr, stationary=True, prop_decrease=0.6)
82
+ except Exception as e:
83
+ print(f"[Warn] 降噪跳过: {e}")
84
 
85
  # 峰值归一化至 -0.5 dB
86
+ max_val = float(np.max(np.abs(y)))
87
  if max_val > 0:
88
  y = (y / max_val) * 0.95
89
 
 
91
  sf.write(processed_path, y, sr, subtype="PCM_16")
92
  return processed_path, duration
93
 
94
+ # ================= 4. 语调时序与情绪动态解析 (防崩安全版) =================
95
+ def extract_prosody_timeline(emo_frame_res, total_duration: float, fallback_emotion: str, window_size: float = 0.5):
96
  """
97
+ 将 Emotion2Vec+ 的帧级时序概率矩阵安全聚合为易读的时间轴
98
+ 强制转为 Python 原生 float/str 类型,杜绝 JSON 序列化崩溃
99
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  labels = ["angry", "disgusted", "fearful", "happy", "neutral", "sad", "surprised"]
 
101
  timeline = []
 
 
 
 
102
 
103
+ try:
104
+ if not emo_frame_res or len(emo_frame_res) == 0:
105
+ raise ValueError("Empty emo_frame_res")
106
+
107
+ item = emo_frame_res[0]
108
+ raw_feats = item.get("feats") if "feats" in item else item.get("scores")
 
 
 
 
 
 
 
 
109
 
110
+ if raw_feats is None:
111
+ raise ValueError("No feats or scores found in frame result")
112
+
113
+ feats = np.array(raw_feats, dtype=np.float32)
114
+ if feats.ndim == 1:
115
+ feats = np.expand_dims(feats, axis=0)
116
+
117
+ total_frames = feats.shape[0]
118
+ if total_frames == 0:
119
+ raise ValueError("Zero frames")
120
+
121
+ frames_per_window = max(1, int(total_frames * (window_size / max(total_duration, 0.1))))
122
+ num_windows = int(np.ceil(total_frames / frames_per_window))
123
+
124
+ for i in range(num_windows):
125
+ start_f = i * frames_per_window
126
+ end_f = min(total_frames, (i + 1) * frames_per_window)
127
+ chunk = feats[start_f:end_f]
128
+
129
+ mean_vec = np.mean(chunk, axis=0) if chunk.ndim > 1 else chunk
130
+
131
+ start_t = round(float(i * window_size), 2)
132
+ end_t = round(float(min(total_duration, (i + 1) * window_size)), 2)
133
+
134
+ if len(mean_vec) == len(labels):
135
+ top_idx = int(np.argmax(mean_vec))
136
+ top_tone = labels[top_idx]
137
+ conf = float(mean_vec[top_idx])
138
+ else:
139
+ top_tone = fallback_emotion
140
+ conf = 0.8
141
+
142
+ # 根据情绪类型判定当前声学唤醒度/语调状态
143
+ if top_tone in ["angry", "surprised"]:
144
+ energy_state = "high / rising" # 激昂 / 语调上扬
145
+ elif top_tone in ["sad"]:
146
+ energy_state = "low / falling" # 低沉 / 语调下沉
147
+ elif top_tone == "happy":
148
+ energy_state = "cheerful / dynamic" # 轻快 / 活跃
149
+ else:
150
+ energy_state = "moderate / flat" # 平稳 / 正常
151
+
152
+ timeline.append({
153
+ "start": start_t,
154
+ "end": end_t,
155
+ "tone": str(top_tone),
156
+ "confidence": round(float(conf), 4),
157
+ "energy_state": energy_state
158
+ })
159
 
160
+ except Exception as err:
161
+ print(f"[Warn] Timeline extraction fallback: {err}")
162
+ timeline = [{
163
+ "start": 0.0,
164
+ "end": round(float(total_duration), 2),
165
+ "tone": str(fallback_emotion),
166
+ "confidence": 0.8,
167
+ "energy_state": "moderate / flat"
168
+ }]
169
 
170
  return timeline
171
 
172
  def evaluate_tone_and_emotion(text: str, emotion_scores: dict, raw_top_emotion: str):
173
  """全局综合语气判定:结合声学分数与文本关键词"""
174
+ top_score = float(emotion_scores.get(raw_top_emotion, 0.0))
175
 
176
  # 撒娇 / 卖萌判定
177
+ happy_score = float(emotion_scores.get("happy", 0.0))
178
+ surprised_score = float(emotion_scores.get("surprised", 0.0))
179
  has_soft_words = bool(re.search(r"[嘛啦呀吧啵呗嘤~~…]", text))
180
+
181
  if (happy_score + surprised_score >= 0.55) and has_soft_words:
182
  return "coquetry", top_score
183
 
 
233
  input=processed_path,
234
  granularity="utterance"
235
  )
236
+
237
  raw_top_emotion = "neutral"
238
  emotion_scores = {}
239
  if emo_res_global and len(emo_res_global) > 0:
240
  item = emo_res_global[0]
241
+ raw_top_emotion = str(item.get("key", item.get("emotion", "neutral")))
242
  raw_scores = item.get("scores", {})
243
+ if isinstance(raw_scores, dict):
244
+ emotion_scores = {str(k): round(float(v), 4) for k, v in raw_scores.items()}
245
 
246
  # 4. Emotion2Vec+ 提取帧级动态时序 (Frame-level)
247
+ try:
248
+ emo_res_frame = emo_model.generate(
249
+ input=processed_path,
250
+ granularity="frame"
251
+ )
252
+ except Exception:
253
+ emo_res_frame = []
254
+
255
  # 生成语调变化时间轴 (按 0.5s 窗口聚合)
256
+ prosody_timeline = extract_prosody_timeline(
257
+ emo_res_frame,
258
+ duration,
259
+ raw_top_emotion,
260
+ window_size=0.5
261
+ )
262
 
263
  # 5. 全局多模态语气裁决
264
  final_emotion, confidence = evaluate_tone_and_emotion(
 
267
  raw_top_emotion
268
  )
269
 
270
+ # 6. 构造纯 Python 基础类型的安全字典
271
+ response_payload = {
272
+ "text": str(clean_text),
273
+ "duration": round(float(duration), 2),
274
+ "overall_emotion": str(final_emotion),
275
+ "confidence": round(float(confidence), 4),
276
+ "emotion_scores": emotion_scores,
277
+ "prosody_timeline": prosody_timeline
278
+ }
279
+
280
+ # 7. 按客户端格式输出
281
  if response_format == "text":
282
  return PlainTextResponse(clean_text)
283
  elif response_format == "verbose_json":
284
+ response_payload.update({
285
  "task": "transcribe",
286
+ "language": str(language),
 
 
 
 
 
 
287
  "segments": []
288
  })
289
+ return JSONResponse(response_payload)
290
  else:
291
+ return JSONResponse(response_payload)
 
 
 
 
 
 
 
 
292
 
293
  except Exception as e:
294
+ # 将完整错误堆栈打入 Hugging Face 日志,方便排查
295
+ traceback.print_exc()
296
  raise HTTPException(status_code=500, detail=str(e))
297
  finally:
298
  # 清理临时文件