会议录音的第一处工程坑不是"模型听不懂",而是上传、排队和超时。一个网页一直转圈,会让用户误以为资料丢了。下面的 Java 17 示例用标准 HttpClient 调用文件转写 REST 接口,明确区分成功、网络失败、HTTP 失败和空文本。没有可靠 Java SDK 时,标准 HTTP 客户端反而更容易审查依赖与请求边界。
官方 File transcription 文档列出音频转写能力和接口细节。模型名会随账号和产品更新,示例用 gpt-6-transcribe 仅作占位;部署前请在官方模型页核对实际可用 ID、文件限制与价格。
准备与完整代码
保存为 Transcribe.java,设置 export OPENAI_API_KEY='...',执行 javac Transcribe.java && java Transcribe meeting.mp3。只使用 JDK 17,无 Maven 依赖。示例未在本次任务中实际调用线上 API;本机有 JDK 但本次未生成独立源文件执行编译,请在你的 Java 17 环境先编译再接入。
java
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.time.Duration;
import java.util.UUID;
public class Transcribe {
static final String API = "https://api.openai.com/v1/audio/transcriptions";
static String esc(String s) { return s.replace("\\", "\\\\").replace("\"", "\\\""); }
static String body(Path file, String boundary) throws Exception {
String head = "--"+boundary+"\r\nContent-Disposition: form-data; name=\"model\"\r\n\r\n"
+ "gpt-6-transcribe\r\n--"+boundary+"\r\nContent-Disposition: form-data; name=\"file\"; filename=\""
+ esc(file.getFileName().toString())+"\"\r\nContent-Type: audio/mpeg\r\n\r\n";
return head + new String(Files.readAllBytes(file)) + "\r\n--"+boundary+"--\r\n";
}
public static void main(String[] args) throws Exception {
if (args.length != 1) throw new IllegalArgumentException("用法: java Transcribe meeting.mp3");
String key = System.getenv("OPENAI_API_KEY");
if (key == null || key.isBlank()) throw new IllegalStateException("请设置OPENAI_API_KEY");
Path audio = Path.of(args[0]); if (!Files.isRegularFile(audio)) throw new IllegalArgumentException("音频不存在");
String boundary = "----Java" + UUID.randomUUID();
HttpRequest req = HttpRequest.newBuilder(URI.create(API)).timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + key).header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofString(body(audio, boundary))).build();
try {
HttpResponse<String> res = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build()
.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() / 100 != 2) throw new RuntimeException("HTTP " + res.statusCode());
if (!res.body().contains("text")) throw new RuntimeException("返回中没有文本字段");
System.out.println("DONE: 请将文本送入人工校对,而非直接入档");
} catch (java.net.http.HttpTimeoutException e) { System.out.println("TIMEOUT: 放入异步重试队列");
} catch (Exception e) { System.out.println("FAILED: " + e.getMessage()); }
}
}
示例强调状态而非解析完整 JSON:生产项目应引入成熟 JSON 库,并使用字节级 multipart 构造,避免把任意二进制强转字符串。预期成功输出以 DONE: 开头;30 秒内没有响应输出 TIMEOUT:。
常见错误包括:上传内容类型和真实文件不符;录音未取得参会人授权;音频过长却硬塞进同步请求;把空转写当作"无发言"。它适合会议纪要草稿、客服质检前处理;不适合直接生成法律、医疗或人事结论。工程化时加任务 ID、对象存储、指数退避和保留期删除策略。5分钟练习:为 FAILED 状态加一个不含音频内容的审计日志。
队列比"再试一次"更重要
把请求超时简单重发,可能在服务端其实已经成功时得到两份转写。更好的模式是先生成任务 ID,上传到受控对象存储,再由工作线程领取;每一次状态变化都写入 QUEUED、RUNNING、DONE、TIMEOUT 或 FAILED。前端根据状态展示"可重试"而不是假装仍在处理。重试只能针对网络错误、429 和临时 5xx;无授权、格式不支持和文件缺失应立刻失败。长音频还应切段,但必须保存段序号与时间戳,合并时不能靠模型猜顺序。音频内容和转写文本都可能包含个人信息,因此下载链接、日志和人工校对权限要分开控制。
官方文档:File transcription。你的转写任务应在多少秒后交给人工或异步队列?
关注「蜗牛聊AI」,一起看懂技术变化背后的真正机会。
本文首发于 java4u.cn,转载请注明出处。