java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java OpenAI语音接口超时与重复请求

Java音频转写踩坑实录之OpenAI语音接口超时与重复请求

作者:二川bro

本文详解Java后端调用OpenAl语音转写API的关键实践,涵盖multipart手动构造、双层超时控制(连接超时5秒/总超时30秒)、MIME类型校验、防重复提交、幂等性设计及生产级工程化清单,需要的朋友可以参考下

前言

把会议录音送去转文字,听起来就一条HTTP请求的事。跟把大象装进冰箱一样,总共三步,谁都觉得自己行。区别在于:装冰箱没人给你限30秒,这条请求要是超时了,可是会甩脸色给你看的。

先别急着冲上去写代码。入门的第一目标,不是"马上拿到全文",而是让这条请求有边界:只收授权的格式,密钥只从环境变量里来,连接和总请求都设上超时,失败了能告诉前端"该重传"还是"改天再来"。

1 动手之前,先把丑话说在前面

1.1 你以为的调用,和现实里的调用

你以为的:发一条POST,喝口水的功夫,全文到手,优雅转笔。

现实:大文件、慢网速、用户对着按钮狂点,这三位凑一块儿,能把你的接口按在地上摩擦。

1.2 三个原则,先立好

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语音接口超时与重复请求的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文