Update app.py
Browse files
app.py
CHANGED
|
@@ -6,7 +6,6 @@ import traceback
|
|
| 6 |
import numpy as np
|
| 7 |
import librosa
|
| 8 |
import soundfile as sf
|
| 9 |
-
import noisereduce as nr
|
| 10 |
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Depends, Security
|
| 11 |
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 12 |
from fastapi.responses import JSONResponse, PlainTextResponse
|
|
@@ -29,13 +28,13 @@ def verify_token(credentials: HTTPAuthorizationCredentials = Security(security))
|
|
| 29 |
)
|
| 30 |
return True
|
| 31 |
|
| 32 |
-
# 亲昵口语热词配置
|
| 33 |
CUSTOM_HOTWORDS = os.getenv(
|
| 34 |
"HOTWORDS",
|
| 35 |
-
"爹地 妈咪 学长 学姐 欧巴 欧尼 宝贝 亲爱的 哥哥 姐姐 弟弟 妹妹 师傅 老公 老婆 爹咪 小路 小路哥哥 因因 岚岚 甜甜 芝芝瓜 小选 小蛇 宝宝 叶瑄 艾因 路辰 罗夏 司岚 星星嘟嘟 牧首 教宗 虎先生 小叶哥哥"
|
| 36 |
)
|
| 37 |
|
| 38 |
-
# 口语错别字纠错映射
|
| 39 |
COLLOQUIAL_CORRECTIONS = {
|
| 40 |
r"跌地|跌弟|爹底|迭地|嗲地": "爹地",
|
| 41 |
r"学掌|鞋长|薛长|雪长": "学长",
|
|
@@ -47,41 +46,32 @@ COLLOQUIAL_CORRECTIONS = {
|
|
| 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
|
| 54 |
|
| 55 |
# ================= 2. 加载模型 =================
|
| 56 |
-
# ASR 文字与标点转录模型
|
| 57 |
asr_model = AutoModel(
|
| 58 |
model="iic/SenseVoiceSmall",
|
| 59 |
device="cpu",
|
| 60 |
disable_update=True
|
| 61 |
)
|
| 62 |
|
| 63 |
-
# 独立声学情绪与语调表征模型
|
| 64 |
emo_model = AutoModel(
|
| 65 |
model="iic/emotion2vec_plus_seed",
|
| 66 |
device="cpu",
|
| 67 |
disable_update=True
|
| 68 |
)
|
| 69 |
|
| 70 |
-
# ================= 3. 音频
|
| 71 |
def preprocess_audio(input_path: str, target_sr: int = 16000):
|
| 72 |
-
"""
|
| 73 |
y, sr = librosa.load(input_path, sr=target_sr, mono=True)
|
| 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:
|
|
@@ -91,89 +81,90 @@ def preprocess_audio(input_path: str, target_sr: int = 16000):
|
|
| 91 |
sf.write(processed_path, y, sr, subtype="PCM_16")
|
| 92 |
return processed_path, duration
|
| 93 |
|
| 94 |
-
# ================= 4.
|
| 95 |
-
def
|
| 96 |
-
"""
|
| 97 |
-
将 Emotion2Vec+ 的帧级时序概率矩阵安全聚合为易读的时间轴
|
| 98 |
-
强制转为 Python 原生 float/str 类型,杜绝 JSON 序列化崩溃
|
| 99 |
-
"""
|
| 100 |
labels = ["angry", "disgusted", "fearful", "happy", "neutral", "sad", "surprised"]
|
|
|
|
|
|
|
|
|
|
| 101 |
timeline = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 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 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
"start": 0.0,
|
| 164 |
"end": round(float(total_duration), 2),
|
| 165 |
-
"tone":
|
| 166 |
-
"confidence":
|
| 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))
|
|
@@ -181,7 +172,6 @@ def evaluate_tone_and_emotion(text: str, emotion_scores: dict, raw_top_emotion:
|
|
| 181 |
if (happy_score + surprised_score >= 0.55) and has_soft_words:
|
| 182 |
return "coquetry", top_score
|
| 183 |
|
| 184 |
-
# 低沉嗓音中性保底
|
| 185 |
if top_score < 0.60:
|
| 186 |
return "neutral", top_score
|
| 187 |
|
|
@@ -192,7 +182,7 @@ def evaluate_tone_and_emotion(text: str, emotion_scores: dict, raw_top_emotion:
|
|
| 192 |
async def root():
|
| 193 |
return {
|
| 194 |
"status": "online",
|
| 195 |
-
"service": "SenseVoice + Emotion2Vec+
|
| 196 |
"docs": "/docs",
|
| 197 |
"endpoint": "/v1/audio/transcriptions"
|
| 198 |
}
|
|
@@ -212,72 +202,49 @@ async def transcribe_audio(
|
|
| 212 |
|
| 213 |
processed_path = None
|
| 214 |
try:
|
| 215 |
-
# 1.
|
| 216 |
processed_path, duration = preprocess_audio(raw_path)
|
| 217 |
|
| 218 |
-
# 2. SenseVoice
|
| 219 |
asr_res = asr_model.generate(
|
| 220 |
input=processed_path,
|
| 221 |
language=language if language in ["zh", "en", "yue", "ja", "ko"] else "auto",
|
| 222 |
use_itn=True,
|
| 223 |
-
emotion=False,
|
| 224 |
-
event=False,
|
| 225 |
hotword=CUSTOM_HOTWORDS
|
| 226 |
)
|
| 227 |
raw_text = asr_res[0]["text"] if asr_res and len(asr_res) > 0 else ""
|
| 228 |
clean_text = re.sub(r"<\|.*?\|>", "", raw_text).strip()
|
| 229 |
clean_text = correct_colloquial_words(clean_text)
|
| 230 |
|
| 231 |
-
# 3. Emotion2Vec+
|
| 232 |
-
|
| 233 |
input=processed_path,
|
| 234 |
granularity="utterance"
|
| 235 |
)
|
| 236 |
-
|
| 237 |
-
|
| 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 |
-
#
|
| 264 |
final_emotion, confidence = evaluate_tone_and_emotion(
|
| 265 |
clean_text,
|
| 266 |
emotion_scores,
|
| 267 |
raw_top_emotion
|
| 268 |
)
|
| 269 |
|
| 270 |
-
#
|
| 271 |
response_payload = {
|
| 272 |
"text": str(clean_text),
|
| 273 |
"duration": round(float(duration), 2),
|
| 274 |
-
"
|
|
|
|
| 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":
|
|
@@ -291,11 +258,9 @@ async def transcribe_audio(
|
|
| 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 |
-
# 清理临时文件
|
| 299 |
if os.path.exists(raw_path):
|
| 300 |
os.remove(raw_path)
|
| 301 |
if processed_path and os.path.exists(processed_path):
|
|
|
|
| 6 |
import numpy as np
|
| 7 |
import librosa
|
| 8 |
import soundfile as sf
|
|
|
|
| 9 |
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Depends, Security
|
| 10 |
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 11 |
from fastapi.responses import JSONResponse, PlainTextResponse
|
|
|
|
| 28 |
)
|
| 29 |
return True
|
| 30 |
|
| 31 |
+
# 亲昵口语热词配置
|
| 32 |
CUSTOM_HOTWORDS = os.getenv(
|
| 33 |
"HOTWORDS",
|
| 34 |
+
"爹地 妈咪 Daddy 学长 学姐 欧巴 欧尼 宝贝 亲爱的 哥哥 姐姐 弟弟 妹妹 师傅 老公 老婆 爹咪 小路 小路哥哥 因因 岚岚 甜甜 芝芝瓜 小选 小蛇 宝宝 叶瑄 艾因 路辰 罗夏 司岚 星星嘟嘟 牧首 教宗 虎先生 小叶哥哥"
|
| 35 |
)
|
| 36 |
|
| 37 |
+
# 口语错别字规则纠错映射
|
| 38 |
COLLOQUIAL_CORRECTIONS = {
|
| 39 |
r"跌地|跌弟|爹底|迭地|嗲地": "爹地",
|
| 40 |
r"学掌|鞋长|薛长|雪长": "学长",
|
|
|
|
| 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
|
| 52 |
|
| 53 |
# ================= 2. 加载模型 =================
|
|
|
|
| 54 |
asr_model = AutoModel(
|
| 55 |
model="iic/SenseVoiceSmall",
|
| 56 |
device="cpu",
|
| 57 |
disable_update=True
|
| 58 |
)
|
| 59 |
|
|
|
|
| 60 |
emo_model = AutoModel(
|
| 61 |
model="iic/emotion2vec_plus_seed",
|
| 62 |
device="cpu",
|
| 63 |
disable_update=True
|
| 64 |
)
|
| 65 |
|
| 66 |
+
# ================= 3. 无损音频标准化 =================
|
| 67 |
def preprocess_audio(input_path: str, target_sr: int = 16000):
|
| 68 |
+
"""仅做重采样与响度归一化,严禁有损频域降噪以保护 ASR 精度"""
|
| 69 |
y, sr = librosa.load(input_path, sr=target_sr, mono=True)
|
| 70 |
if len(y) == 0:
|
| 71 |
return input_path, 0.0
|
| 72 |
|
| 73 |
duration = float(len(y) / sr)
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
# 峰值归一化至 -0.5 dB
|
| 76 |
max_val = float(np.max(np.abs(y)))
|
| 77 |
if max_val > 0:
|
|
|
|
| 81 |
sf.write(processed_path, y, sr, subtype="PCM_16")
|
| 82 |
return processed_path, duration
|
| 83 |
|
| 84 |
+
# ================= 4. 情绪与时序解析 =================
|
| 85 |
+
def parse_emotion_and_timeline(emo_res, total_duration: float, window_size: float = 0.5):
|
| 86 |
+
"""统一提取全局情绪与时序走势"""
|
|
|
|
|
|
|
|
|
|
| 87 |
labels = ["angry", "disgusted", "fearful", "happy", "neutral", "sad", "surprised"]
|
| 88 |
+
|
| 89 |
+
top_emotion = "neutral"
|
| 90 |
+
emotion_scores = {}
|
| 91 |
timeline = []
|
| 92 |
+
|
| 93 |
+
if not emo_res or len(emo_res) == 0:
|
| 94 |
+
return top_emotion, emotion_scores, timeline
|
| 95 |
+
|
| 96 |
+
item = emo_res[0]
|
| 97 |
+
top_emotion = str(item.get("key", item.get("emotion", "neutral")))
|
| 98 |
|
| 99 |
+
# 提取置信度
|
| 100 |
+
raw_scores = item.get("scores", {})
|
| 101 |
+
if isinstance(raw_scores, dict):
|
| 102 |
+
emotion_scores = {str(k): round(float(v), 4) for k, v in raw_scores.items()}
|
| 103 |
+
|
| 104 |
+
# 提取时间轴
|
| 105 |
+
raw_feats = item.get("feats") if "feats" in item else None
|
| 106 |
+
if raw_feats is not None:
|
| 107 |
+
try:
|
| 108 |
+
feats = np.array(raw_feats, dtype=np.float32)
|
| 109 |
+
if feats.ndim == 1:
|
| 110 |
+
feats = np.expand_dims(feats, axis=0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
+
total_frames = feats.shape[0]
|
| 113 |
+
if total_frames > 0:
|
| 114 |
+
frames_per_window = max(1, int(total_frames * (window_size / max(total_duration, 0.1))))
|
| 115 |
+
num_windows = int(np.ceil(total_frames / frames_per_window))
|
| 116 |
+
|
| 117 |
+
for i in range(num_windows):
|
| 118 |
+
start_f = i * frames_per_window
|
| 119 |
+
end_f = min(total_frames, (i + 1) * frames_per_window)
|
| 120 |
+
chunk = feats[start_f:end_f]
|
| 121 |
+
mean_vec = np.mean(chunk, axis=0) if chunk.ndim > 1 else chunk
|
| 122 |
+
|
| 123 |
+
start_t = round(float(i * window_size), 2)
|
| 124 |
+
end_t = round(float(min(total_duration, (i + 1) * window_size)), 2)
|
| 125 |
+
|
| 126 |
+
if len(mean_vec) == len(labels):
|
| 127 |
+
t_idx = int(np.argmax(mean_vec))
|
| 128 |
+
t_label = labels[t_idx]
|
| 129 |
+
conf = float(mean_vec[t_idx])
|
| 130 |
+
else:
|
| 131 |
+
t_label = top_emotion
|
| 132 |
+
conf = 0.8
|
| 133 |
+
|
| 134 |
+
state = "moderate / flat"
|
| 135 |
+
if t_label in ["angry", "surprised"]:
|
| 136 |
+
state = "high / rising"
|
| 137 |
+
elif t_label in ["sad"]:
|
| 138 |
+
state = "low / falling"
|
| 139 |
+
elif t_label == "happy":
|
| 140 |
+
state = "cheerful / dynamic"
|
| 141 |
+
|
| 142 |
+
timeline.append({
|
| 143 |
+
"start": start_t,
|
| 144 |
+
"end": end_t,
|
| 145 |
+
"tone": str(t_label),
|
| 146 |
+
"confidence": round(float(conf), 4),
|
| 147 |
+
"energy_state": state
|
| 148 |
+
})
|
| 149 |
+
except Exception as e:
|
| 150 |
+
print(f"[Warn] Timeline parse failed: {e}")
|
| 151 |
+
|
| 152 |
+
# 若无有效帧数据则生成单段保底
|
| 153 |
+
if not timeline:
|
| 154 |
+
timeline.append({
|
| 155 |
"start": 0.0,
|
| 156 |
"end": round(float(total_duration), 2),
|
| 157 |
+
"tone": top_emotion,
|
| 158 |
+
"confidence": round(float(max(emotion_scores.values())), 4) if emotion_scores else 1.0,
|
| 159 |
"energy_state": "moderate / flat"
|
| 160 |
+
})
|
| 161 |
|
| 162 |
+
return top_emotion, emotion_scores, timeline
|
| 163 |
|
| 164 |
def evaluate_tone_and_emotion(text: str, emotion_scores: dict, raw_top_emotion: str):
|
| 165 |
+
"""多模态语气判断"""
|
| 166 |
top_score = float(emotion_scores.get(raw_top_emotion, 0.0))
|
| 167 |
|
|
|
|
| 168 |
happy_score = float(emotion_scores.get("happy", 0.0))
|
| 169 |
surprised_score = float(emotion_scores.get("surprised", 0.0))
|
| 170 |
has_soft_words = bool(re.search(r"[嘛啦呀吧啵呗嘤~~…]", text))
|
|
|
|
| 172 |
if (happy_score + surprised_score >= 0.55) and has_soft_words:
|
| 173 |
return "coquetry", top_score
|
| 174 |
|
|
|
|
| 175 |
if top_score < 0.60:
|
| 176 |
return "neutral", top_score
|
| 177 |
|
|
|
|
| 182 |
async def root():
|
| 183 |
return {
|
| 184 |
"status": "online",
|
| 185 |
+
"service": "SenseVoice + Emotion2Vec+ STT API",
|
| 186 |
"docs": "/docs",
|
| 187 |
"endpoint": "/v1/audio/transcriptions"
|
| 188 |
}
|
|
|
|
| 202 |
|
| 203 |
processed_path = None
|
| 204 |
try:
|
| 205 |
+
# 1. 无损标准化前处理
|
| 206 |
processed_path, duration = preprocess_audio(raw_path)
|
| 207 |
|
| 208 |
+
# 2. SenseVoice 原生调用(标准 ITN,不硬传干扰参数)
|
| 209 |
asr_res = asr_model.generate(
|
| 210 |
input=processed_path,
|
| 211 |
language=language if language in ["zh", "en", "yue", "ja", "ko"] else "auto",
|
| 212 |
use_itn=True,
|
|
|
|
|
|
|
| 213 |
hotword=CUSTOM_HOTWORDS
|
| 214 |
)
|
| 215 |
raw_text = asr_res[0]["text"] if asr_res and len(asr_res) > 0 else ""
|
| 216 |
clean_text = re.sub(r"<\|.*?\|>", "", raw_text).strip()
|
| 217 |
clean_text = correct_colloquial_words(clean_text)
|
| 218 |
|
| 219 |
+
# 3. Emotion2Vec+ 单次推理获取情绪与特征
|
| 220 |
+
emo_res = emo_model.generate(
|
| 221 |
input=processed_path,
|
| 222 |
granularity="utterance"
|
| 223 |
)
|
| 224 |
+
raw_top_emotion, emotion_scores, prosody_timeline = parse_emotion_and_timeline(
|
| 225 |
+
emo_res,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
duration,
|
|
|
|
| 227 |
window_size=0.5
|
| 228 |
)
|
| 229 |
|
| 230 |
+
# 4. 多模态综合语气裁决
|
| 231 |
final_emotion, confidence = evaluate_tone_and_emotion(
|
| 232 |
clean_text,
|
| 233 |
emotion_scores,
|
| 234 |
raw_top_emotion
|
| 235 |
)
|
| 236 |
|
| 237 |
+
# 5. 构造双重兼容的返回体(同时保留 emotion 和 overall_emotion)
|
| 238 |
response_payload = {
|
| 239 |
"text": str(clean_text),
|
| 240 |
"duration": round(float(duration), 2),
|
| 241 |
+
"emotion": str(final_emotion), # 兼容旧版及常规调用
|
| 242 |
+
"overall_emotion": str(final_emotion), # 兼容时序扩展
|
| 243 |
"confidence": round(float(confidence), 4),
|
| 244 |
"emotion_scores": emotion_scores,
|
| 245 |
"prosody_timeline": prosody_timeline
|
| 246 |
}
|
| 247 |
|
|
|
|
| 248 |
if response_format == "text":
|
| 249 |
return PlainTextResponse(clean_text)
|
| 250 |
elif response_format == "verbose_json":
|
|
|
|
| 258 |
return JSONResponse(response_payload)
|
| 259 |
|
| 260 |
except Exception as e:
|
|
|
|
| 261 |
traceback.print_exc()
|
| 262 |
raise HTTPException(status_code=500, detail=str(e))
|
| 263 |
finally:
|
|
|
|
| 264 |
if os.path.exists(raw_path):
|
| 265 |
os.remove(raw_path)
|
| 266 |
if processed_path and os.path.exists(processed_path):
|