把会议录音送去转文字看似是一条 HTTP 请求,真正上线后却常被大文件、慢网和重复点击拖垮。正确的入门目标不是“马上得到全文”,而是让请求有边界:只接收授权格式、密钥只来自环境变量、连接与总请求都有超时、失败能告诉前端该重传还是稍后再试。下面用 Java 17 的标准 HTTP 客户端拼一个最小 multipart 请求。
OpenAI 的语音转文字指南提供 Audio Transcriptions API。这里采用 REST,因为 Java 标准库足够完成教学示例;模型名 gpt-4o-mini-transcribe 仅作当前文档中的示例,请以目标账号实际可用模型为准。示例未在本次任务中实际调用线上 API;本机 javac 缺少 Java 运行时,故仅完成静态审阅,待 Java 17 环境编译。

准备与代码
JDK 17+即可。设置 export OPENAI_API_KEY='你的密钥';保存为 Transcribe.java;运行 javac Transcribe.java && java Transcribe meeting.mp3。只上传已取得录音授权的文件,且不要把音频或返回文本打印到生产日志。
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交给解析器并脱敏保存。");
}
}
form 明确构造 multipart 边界和文件段,connectTimeout 控制建连,timeout 控制整次请求。真实服务不要让 HTTP 控制器同步等待30秒:接到文件后创建任务,交给队列工作者执行,前端轮询任务状态。也不要依据文件扩展名断言格式,生产中应读取 MIME、限制时长并做恶意文件扫描。
预期成功时打印“转写成功”;错误会显示缺失密钥、大小超过20MB、HTTP状态或网络异常。常见错误包括:边界与 Content-Type 不一致;音频 MIME 误标;用户刷新页面就再次上传;模型听错人名却把未经校对的文本写入合同。适合会议纪要初稿、客服质检和无障碍字幕,不适合在未告知录音参与者时秘密转写。
工程化增加任务ID、重试上限、幂等哈希、加密存储、删除策略和专名词表,并把人工修订反向用于质量抽样。5分钟实践:增加 .wav 白名单和一个“已存在音频哈希则不重复提交”的内存集合。
你的转写任务在30秒未返回时,是重试、排队还是提示用户稍后查看?
关注「蜗牛聊AI」,一起看懂技术变化背后的真正机会。
本文首发于 java4u.cn,转载请注明出处。

