Java音频转写踩坑实录之OpenAI语音接口超时与重复请求
作者:二川bro
前言
把会议录音送去转文字,听起来就一条HTTP请求的事。跟把大象装进冰箱一样,总共三步,谁都觉得自己行。区别在于:装冰箱没人给你限30秒,这条请求要是超时了,可是会甩脸色给你看的。
先别急着冲上去写代码。入门的第一目标,不是"马上拿到全文",而是让这条请求有边界:只收授权的格式,密钥只从环境变量里来,连接和总请求都设上超时,失败了能告诉前端"该重传"还是"改天再来"。
1 动手之前,先把丑话说在前面
1.1 你以为的调用,和现实里的调用
你以为的:发一条POST,喝口水的功夫,全文到手,优雅转笔。
现实:大文件、慢网速、用户对着按钮狂点,这三位凑一块儿,能把你的接口按在地上摩擦。
1.2 三个原则,先立好
- 只接收授权格式。别来者不拒,谁递你都收,那是门口保安,不是后端接口。
- 密钥只从环境变量取。写死在代码里,等于把家门钥匙贴在门上,还附一张字条:钥匙在此,欢迎光临。
- 连接超时5秒,总请求超时30秒。超了就明明白白报错,别让人家对着转圈图标干等。
2 准备环节
2.1 你要准备的东西
JDK 17及以上,一个你拿到授权的音频文件,一个OPENAI_API_KEY。就这三样,别的都是锦上添花。
跑起来也就三条命令的事:
export OPENAI_API_KEY='你的密钥' javac Transcribe.java && java Transcribe meeting.mp3
2.2 完整代码
import java.io.*;
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.time.Duration;
public class Transcribe {
static final String B = "----snail-ai-boundary";
static void part(OutputStream out, String name, String value) throws IOException {
out.write(("--" + B + "\r\nContent-Disposition: form-data; name=\"" + name + "\"\r\n\r\n" + value + "\r\n").getBytes());
}
static byte[] form(Path audio) throws IOException {
if (Files.size(audio) > 20 * 1024 * 1024) throw new IllegalArgumentException("教学示例限制20MB");
ByteArrayOutputStream out = new ByteArrayOutputStream();
part(out, "model", "gpt-4o-mini-transcribe");
out.write(("--" + B + "\r\nContent-Disposition: form-data; name=\"file\"; filename=\"" + audio.getFileName() + "\"\r\nContent-Type: audio/mpeg\r\n\r\n").getBytes());
out.write(Files.readAllBytes(audio));
out.write(("\r\n--" + B + "--\r\n").getBytes());
return out.toByteArray();
}
public static void main(String[] args) throws Exception {
if (args.length != 1) throw new IllegalArgumentException("用法: 音频路径");
String key = System.getenv("OPENAI_API_KEY");
if (key == null || key.isBlank()) throw new IllegalStateException("缺少OPENAI_API_KEY");
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.openai.com/v1/audio/transcriptions"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + key)
.header("Content-Type", "multipart/form-data; boundary=" + B)
.POST(HttpRequest.BodyPublishers.ofByteArray(form(Paths.get(args[0]))))
.build();
HttpResponse<String> res = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build()
.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() / 100 != 2) throw new IOException("转写失败 HTTP=" + res.statusCode());
System.out.println("转写成功;生产环境请将JSON交给解析器并脱敏保存。");
}
}
3 代码里藏着的小心机
3.1 multipart:自己动手,丰衣足食
form方法里手动拼multipart,边界线、文件段、模型字段,一个不落。看着是土了点,但胜在你能看见每一根线头,出了问题不至于抓瞎。用别人的库拼,出了错你还得先怀疑人生、再怀疑库。
3.2 超时也分两层
connectTimeout管"能不能连上",timeout管"整趟能不能跑完"。一个是约会路上堵车,一个是约会本身聊崩了——两码事,得分清。混在一起,出问题你都不知道该怪哪头。
3.3 状态码不是拿来欣赏的
res.statusCode() / 100 != 2,直接扔异常。2xx是自己人,3xx是别人家的,4xx、5xx是来讨债的。不检查状态码就往下走,等于不看天气预报就出门——雨不淋你淋谁。
4 那些年我们踩过的坑
4.1 边界和Content-Type打架
Content-Type里写的boundary,和body里拼的不一致,服务端直接一脸懵:您这是哪条道上的multipart?这种错不报错还好,一报就是"请检查请求格式",能查到你怀疑人生。
4.2 MIME被误标
音频明明是个wav,你非要标audio/mpeg。服务端倒是不跟你计较,但人家按mpeg的规矩解析,结果就像你穿白大褂去卖豆腐——卖是能卖,行家一眼就看穿。
4.3 用户一刷新,录音传三遍
前端没做防重,用户等得不耐烦,刷新一下页面,后端收到三份一模一样的录音。这不是三份快乐,是三次转写费用,外加三份待清理的垃圾。
4.4 AI听错人名,合同背锅
模型把"张三"听成"张伞",没人校对,直接写进合同:张伞先生负责项目验收。法务看完这份合同,第一反应是查一下公司里有没有这位同事。
5 上生产之前,把这几件事办了
5.1 别让HTTP控制器干等30秒
真实服务里,千万别让控制器同步等30秒。接到文件,建个任务扔给队列,让前端轮询任务状态。让HTTP线程原地等30秒,等于让前台一个人扛下整个双十一的咨询量。
整个流程大概是这么个闭环:
用户选好已授权的音频 → 过类型和大小门禁 → 进后台转写任务 → 30秒内出结果?出就保存文本和状态,没出就记录原因等重试 → 文本到手,人工校对一遍专名,完事。
5.2 别信扩展名,要看MIME
扩展名是给人看的,MIME才是给程序看的。生产环境里读MIME、限时长、做恶意文件扫描,一个都不能少。录音这东西,可能比你想象中更能藏东西。
5.3 工程化清单
任务ID、重试上限、幂等哈希、加密存储、删除策略、专名词表,外加把人工修订反向喂给质量抽样。这一套下来,才算从"能跑"走向"能扛"。
以上就是Java音频转写踩坑实录之OpenAI语音接口超时与重复请求的详细内容,更多关于Java OpenAI语音接口超时与重复请求的资料请关注脚本之家其它相关文章!
