1. 实现功能
M5-1: Whisper API 服务
本脚本使用 FastAPI 构建一个语音转录微服务。
它提供一个 /transcribe/
端点,可以接收音频文件上传,
并返回转录后的全文文本和 SRT 格式的字幕。
2.运行效果
(whisper) root@DESKTOP-8IU6393:/home/gpu3090/vscode/M5-应用集成# curl -X POST "http://127.0.0.1:8080/transcribe/" \
-F "audio_file=@26.mp3;type=audio/wav" \
-F "model_name=large-v3" \
-F "language=zh"
{"full_text":"作品二十六号我们家的后园有半亩空地母亲说,让她慌着怪可惜的你们那么爱吃花生,就开辟出来种花生吧我们姐弟几个都很高兴埋种、翻地、播种、浇水没过几个月,居然收获了母亲说,今晚我们过一个收获节请你们父亲也来尝尝我们的新花生,好不好我们都说好母亲把花生做成了好几样食品还吩咐就在后园的茅亭里过这个节晚上天色不太好可是父亲也来了实在很难得父亲说,你们爱吃花生吗我们争着答应,爱谁能把花生的好处说出来姐姐说,花生的味儿美哥哥说,花生可以榨油我说,花生的价钱便宜谁都可以买来吃,都喜欢吃这就是它的好处父亲说,花生的好处很多有一样最可贵它的果实埋在地里不像桃子、石榴、苹果那样把鲜红嫩绿的果实高高地挂在枝头上使人一见就生爱慕之心你们看它矮矮地长在地上等到成熟了就会吃了也不能立刻分辨出来它有没有果实必须挖出来才知道我们都说是母亲也点点头父亲接下去说所以你们要像花生它虽然不好看可是很有用不是外表好看而没有实用的东西我说那么人要做有用的人不是外表好看不要做只讲体面而对别人没有好处的人了父亲说对这是我对你们的希望我们谈到夜深才散花生做的食品都吃完了父亲的话却深深地印在我的心上欢迎光临普通话学习网3w.png.compthxx.com","srt_subtitles":"1\n0:00:00,000 --> 0:00:01,919\n作品二十六号\n\n2\n0:00:01,919 --> 0:00:06,700\n我们家的后园有半亩空地\n\n3\n0:00:06,700 --> 0:00:10,939\n母亲说,让她慌着怪可惜的\n\n4\n0:00:10,939 --> 0:00:15,900\n你们那么爱吃花生,就开辟出来种花生吧\n\n5\n0:00:15,900 --> 0:00:19,699\n我们姐弟几个都很高兴\n\n6\n0:00:19,699 --> 0:00:24,160\n埋种、翻地、播种、浇水\n\n7\n0:00:24,160 --> 0:00:27,660\n没过几个月,居然收获了\n\n8\n0:00:27,660 --> 0:00:32,859\n母亲说,今晚我们过一个收获节\n\n9\n0:00:32,859 --> 0:00:38,060\n请你们父亲也来尝尝我们的新花生,好不好\n\n10\n0:00:38,060 --> 0:00:40,920\n我们都说好\n\n11\n0:00:40,920 --> 0:00:45,439\n母亲把花生做成了好几样食品\n\n12\n0:00:45,439 --> 0:00:49,900\n还吩咐就在后园的茅亭里过这个节\n\n13\n0:00:51,920 --> 0:00:54,120\n晚上天色不太好\n\n14\n0:00:54,120 --> 0:00:56,240\n可是父亲也来了\n\n15\n0:00:56,240 --> 0:00:58,240\n实在很难得\n\n16\n0:00:58,240 --> 0:01:02,820\n父亲说,你们爱吃花生吗\n\n17\n0:01:02,820 --> 0:01:05,960\n我们争着答应,爱\n\n18\n0:01:05,960 --> 0:01:09,599\n谁能把花生的好处说出来\n\n19\n0:01:09,599 --> 0:01:13,840\n姐姐说,花生的味儿美\n\n20\n0:01:13,840 --> 0:01:18,180\n哥哥说,花生可以榨油\n\n21\n0:01:18,180 --> 0:01:22,719\n我说,花生的价钱便宜\n\n22\n0:01:22,719 --> 0:01:25,859\n谁都可以买来吃,都喜欢吃\n\n23\n0:01:26,239 --> 0:01:27,780\n这就是它的好处\n\n24\n0:01:27,780 --> 0:01:33,379\n父亲说,花生的好处很多\n\n25\n0:01:33,379 --> 0:01:35,859\n有一样最可贵\n\n26\n0:01:35,859 --> 0:01:38,919\n它的果实埋在地里\n\n27\n0:01:38,919 --> 0:01:42,639\n不像桃子、石榴、苹果那样\n\n28\n0:01:42,639 --> 0:01:47,579\n把鲜红嫩绿的果实高高地挂在枝头上\n\n29\n0:01:47,579 --> 0:01:50,799\n使人一见就生爱慕之心\n\n30\n0:01:50,799 --> 0:01:54,359\n你们看它矮矮地长在地上\n\n31\n0:01:54,359 --> 0:01:56,119\n等到成熟了\n\n32\n0:01:56,119 --> 0:01:56,219\n就会吃了\n\n33\n0:01:56,239 --> 0:01:58,319\n也不能立刻分辨出来\n\n34\n0:01:58,319 --> 0:02:00,039\n它有没有果实\n\n35\n0:02:00,039 --> 0:02:02,619\n必须挖出来才知道\n\n36\n0:02:02,619 --> 0:02:04,679\n我们都说是\n\n37\n0:02:04,679 --> 0:02:07,060\n母亲也点点头\n\n38\n0:02:07,060 --> 0:02:10,199\n父亲接下去说\n\n39\n0:02:10,199 --> 0:02:13,039\n所以你们要像花生\n\n40\n0:02:13,039 --> 0:02:15,159\n它虽然不好看\n\n41\n0:02:15,159 --> 0:02:16,539\n可是很有用\n\n42\n0:02:16,539 --> 0:02:18,620\n不是外表好看\n\n43\n0:02:18,620 --> 0:02:20,579\n而没有实用的东西\n\n44\n0:02:20,579 --> 0:02:22,620\n我说\n\n45\n0:02:22,620 --> 0:02:25,599\n那么人要做有用的人\n\n46\n0:02:25,599 --> 0:02:26,120\n不是外表好看\n\n47\n0:02:26,120 --> 0:02:28,159\n不要做只讲体面\n\n48\n0:02:28,159 --> 0:02:31,939\n而对别人没有好处的人了\n\n49\n0:02:31,939 --> 0:02:33,280\n父亲说\n\n50\n0:02:33,280 --> 0:02:34,480\n对\n\n51\n0:02:34,480 --> 0:02:38,960\n这是我对你们的希望\n\n52\n0:02:38,960 --> 0:02:41,960\n我们谈到夜深才散\n\n53\n0:02:41,960 --> 0:02:45,680\n花生做的食品都吃完了\n\n54\n0:02:45,680 --> 0:02:52,000\n父亲的话却深深地印在我的心上\n\n55\n0:02:52,000 --> 0:02:54,939\n欢迎光临普通话学习网\n\n56\n0:02:54,939 --> 0:02:56,000\n3w.png.com\n\n57\n0:02:56,000 --> 0:02:58,819\npthxx.com\n","metadata":{"detected_language":"zh","processing_time_seconds":33.22,"model_used":"large-v3"}}(whisper) root@DESKTOP-8IU6393:/home/gpu3090/vscode/M5-应用集成#
3.实现过程
3.1 搭建环境
(base) root@DESKTOP-8IU6393:/home/gpu3090/vscode/M5-应用集成# conda activate whisper
(whisper) root@DESKTOP-8IU6393:/home/gpu3090/vscode/M5-应用集成#pip install fastapi "uvicorn[standard]" python-multipart
(whisper) root@DESKTOP-8IU6393:/home/gpu3090/vscode/M5-应用集成# uvicorn M5-1-API:app --reload --host 0.0.0.0 --port 8080
INFO: Will watch for changes in these directories: ['/home/gpu3090/vscode/M5-应用集成']
INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)
INFO: Started reloader process [3065] using WatchFiles
INFO: Started server process [3067]
INFO: Waiting for application startup.
INFO: Application startup complete.
2025-08-08 22:21:39,839 - INFO - 接收到文件: 【AI玩出花】MultiTalk实测:只需图片和音频,让画中人开口讲故事!(在线+本地部署).mp3 (类型: audio/mpeg)
2025-08-08 22:21:39,839 - INFO - 请求参数: model_name='large-v3', language='zh'
2025-08-08 22:21:40,040 - INFO - 正在使用设备: cuda
2025-08-08 22:21:40,060 - INFO - 音频文件已临时保存到: /tmp/tmpmtuvmmbc.mp3
2025-08-08 22:21:40,060 - INFO - 正在加载 Whisper 模型: large-v3...
2025-08-08 22:22:06,710 - INFO - 开始转录...
100%|████████████████████████████████████████████████████████████████████████| 92828/92828 [03:43<00:00, 416.21frames/s]
2025-08-08 22:25:53,535 - INFO - 转录完成,耗时: 226.82 秒
2025-08-08 22:25:53,540 - INFO - 已清理临时文件: /tmp/tmpmtuvmmbc.mp3
INFO: 127.0.0.1:2553 - "POST /transcribe/ HTTP/1.1" 200 OK
3.1.1 在浏览器中打开 http://127.0.0.1:8080/docs 可以看到自动生成的交互式 API 文档。
3.2 代码
import os
import whisper
import datetime
import torch
import tempfile
import logging
from fastapi import FastAPI, UploadFile, File, HTTPException, Form
from fastapi.responses import JSONResponse
# --- 配置日志 ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# --- 初始化 FastAPI 应用 ---
app = FastAPI(
title="Whisper Transcription API",
description="一个使用 OpenAI Whisper 进行语音转录和字幕生成的 API。",
version="1.0.0"
)
# --- 全局变量与模型加载 ---
# 在生产环境中,推荐在应用启动时加载模型,避免每次请求都加载。
# 但对于开发和简单应用,按需加载也可以接受。
# 这里我们选择在函数内部加载,以支持动态选择模型。
# --- 辅助函数:SRT 生成器 (从 M4 稳定版复制) ---
def generate_srt_from_segments(segments):
"""根据 Whisper 的分段信息,生成 SRT 格式的字符串。"""
srt_content = []
for i, segment in enumerate(segments):
start_time = datetime.timedelta(seconds=int(segment['start']))
end_time = datetime.timedelta(seconds=int(segment['end']))
# 格式化时间为 SRT 标准格式: HH:MM:SS,ms
start_str = str(start_time).split('.')[0] + f",{int((segment['start'] % 1) * 1000):03d}"
end_str = str(end_time).split('.')[0] + f",{int((segment['end'] % 1) * 1000):03d}"
text = segment['text'].strip()
srt_content.append(f"{i + 1}")
srt_content.append(f"{start_str} --> {end_str}")
srt_content.append(f"{text}\n")
return "\n".join(srt_content)
# --- API 端点定义 ---
@app.post("/transcribe/",
summary="转录音频文件",
description="上传一个音频文件,获取全文转录和SRT字幕。")
async def transcribe_audio(
# 使用 File(...) 接收文件上传
audio_file: UploadFile = File(..., description="要转录的音频文件 (如 .mp3, .wav, .m4a)"),
# 使用 Form(...) 接收表单数据
model_name: str = Form("base", description="要使用的 Whisper 模型 (如 'tiny', 'base', 'medium', 'large-v3')"),
language: str = Form(None, description="音频的语言代码 (如 'en', 'zh')。如果留空,Whisper 会自动检测。")
):
"""
处理音频转录请求的核心函数。
"""
logging.info(f"接收到文件: {audio_file.filename} (类型: {audio_file.content_type})")
logging.info(f"请求参数: model_name='{model_name}', language='{language}'")
# 检查设备
device = "cuda" if torch.cuda.is_available() else "cpu"
logging.info(f"正在使用设备: {device}")
# 使用临时文件来处理上传的音频
# 这是处理上传文件的标准且安全的方式
try:
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(audio_file.filename)[1]) as temp_audio_file:
# 将上传的文件内容写入临时文件
content = await audio_file.read()
temp_audio_file.write(content)
temp_audio_path = temp_audio_file.name
logging.info(f"音频文件已临时保存到: {temp_audio_path}")
# 加载 Whisper 模型
logging.info(f"正在加载 Whisper 模型: {model_name}...")
model = whisper.load_model(model_name, device=device)
# 执行转录
logging.info("开始转录...")
start_time = datetime.datetime.now()
# 如果 language 为 None 或空字符串,Whisper 会自动检测
lang_option = language if language and language.strip() else None
result = model.transcribe(temp_audio_path, language=lang_option, verbose=False)
end_time = datetime.datetime.now()
processing_time = (end_time - start_time).total_seconds()
logging.info(f"转录完成,耗时: {processing_time:.2f} 秒")
# 生成 SRT 字幕
srt_subtitles = generate_srt_from_segments(result['segments'])
# 准备返回的 JSON 数据
response_data = {
"full_text": result['text'],
"srt_subtitles": srt_subtitles,
"metadata": {
"detected_language": result['language'],
"processing_time_seconds": round(processing_time, 2),
"model_used": model_name
}
}
return JSONResponse(content=response_data)
except Exception as e:
logging.error(f"处理过程中发生错误: {e}", exc_info=True)
# 如果发生任何错误,都返回一个 HTTP 500 错误
raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
finally:
# 确保临时文件在处理完成后被删除
if 'temp_audio_path' in locals() and os.path.exists(temp_audio_path):
os.remove(temp_audio_path)
logging.info(f"已清理临时文件: {temp_audio_path}")
# --- 健康检查端点 ---
@app.get("/", summary="API 根目录与健康检查")
async def read_root():
return {"message": "Whisper Transcription API 正在运行。请访问 /docs 查看 API 文档。"}