OpenAiAudioTranscriptionModel -> ChatClient.entity(Minutes.class) -> OpenAiAudioSpeechModel, WAV chunking at a silence under the 25 MB upload limit, SDK retry behaviour, the 4096-character speech limit and a cost model. Audio endpoints are a local mock and the chat model a stub; no real model was called. Spring Boot 4.1.2, Spring AI 2.0.2, Java 25. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01CifR2BE7PBvRy5hT8TS9oz
38 lines
2.8 KiB
Markdown
38 lines
2.8 KiB
Markdown
# audio
|
|
|
|
A meeting-minutes generator on Spring AI 2.0.2: a recording goes through `OpenAiAudioTranscriptionModel`, the transcript becomes a typed `Minutes` record with `ChatClient.entity(...)`, and the action items are read back with `OpenAiAudioSpeechModel`. Files over the upload limit are split in the middle of a silence; a cost model with formulas prices a meeting.
|
|
|
|
**No real model was called.** The two audio endpoints (`/v1/audio/transcriptions`, `/v1/audio/speech`) are a local **mock** (`src/test/.../support/FakeAudioServer.java`) that the real Spring AI classes and the OpenAI Java SDK talk to over HTTP, so the requests in `output/` are what those classes really send. The "recording" is 30 synthetic tones, one per scripted sentence, and the mock's "transcription" decodes the pitch back into the sentence. The chat model is a scripted **stub**. Nothing here measures the accuracy, latency or price of any real model; the cost table applies OpenAI's published prices (read on 2026-10-26) to stated assumptions.
|
|
|
|
| | |
|
|
|---|---|
|
|
| Spring Boot | 4.1.2 |
|
|
| Spring AI | 2.0.2 (`spring-ai-openai`, `spring-ai-client-chat`) |
|
|
| Java | 25 |
|
|
| Run | `scripts/run-all.sh` (about 45 s) |
|
|
|
|
## Source
|
|
|
|
| File | What it is |
|
|
|---|---|
|
|
| [`MinutesService`](src/main/java/com/ankurm/audio/MinutesService.java) | transcribe (chunked) -> `summarise` -> `readAloud` (split at 4096 characters) |
|
|
| [`SilenceChunker`](src/main/java/com/ankurm/audio/SilenceChunker.java) | splits a WAV under a byte budget, cutting in the middle of the longest silence it finds; also the blind baseline |
|
|
| [`Minutes`](src/main/java/com/ankurm/audio/Minutes.java) | the record the model's reply is parsed into |
|
|
| [`NamedBytes`](src/main/java/com/ankurm/audio/NamedBytes.java) | a `ByteArrayResource` with a file name (the extension is how the upload is typed) |
|
|
| [`CostModel`](src/main/java/com/ankurm/audio/CostModel.java) | the three cost formulas and the prices they were run with |
|
|
| [`Wav`](src/main/java/com/ankurm/audio/Wav.java) | the 60 lines of RIFF/WAVE the chunker needs |
|
|
|
|
## Captured output (`output/`)
|
|
|
|
| File | Shows |
|
|
|---|---|
|
|
| `01-api-shape.txt` | class names and defaults in the 2.0.2 jars |
|
|
| `02-transcribe-small.txt`, `02b-filename.txt` | the multipart request; file name and extension |
|
|
| `03-large-file.txt` | 413 on a 30 MB file; two silence-aligned chunks recover every line |
|
|
| `04-blind-split.txt` | cutting every N bytes lands inside a sentence |
|
|
| `05-structured-output.txt`, `05b-bad-replies.txt` | the schema appended to the prompt; three replies that fail to parse |
|
|
| `06-speech.txt`, `06b-speech-split.txt` | the speech request; the 4096-character wall; splitting |
|
|
| `07-retries.txt` | SDK retries re-upload the whole file |
|
|
| `08-cost.txt` | cost per meeting length; file size per encoding |
|
|
| `09-end-to-end.txt` | the three steps wired together |
|