# Simple OpenAI Whisper Java Client

`whisperer` is a Micronaut CLI that records microphone input until you press Enter, forwards the WAV file to the OpenAI Audio Transcriptions API, then prints and copies the transcript. The project lives in `<your-repo>/examples/whisperer` and targets macOS users who want a fast, keyboard-friendly dictation workflow.

## Why Another Whisper Client?

* `pbcopy` integration keeps hands on the keyboard—no manual copy/paste after dictation.

* GraalVM native image builds let the CLI start instantly and access CoreAudio without a JVM startup delay.

* The Micronaut runtime keeps the code modular while staying lightweight for a CLI tool.

## Command Flow

The entry point is the Picocli-powered command in `<your-repo>/examples/whisperer/src/main/java/com/iqbalaissaoui/WhispererCommand.java`.

```JAVA
@Command(name = "whisperer", description = "Press-to-stop voice capture that sends audio to OpenAI's transcription API")
public class WhispererCommand implements Callable<Integer> {
    public Integer call() {
        tempFile = Files.createTempFile("whisperer-", ".wav");
        audioRecorder.start(tempFile);
        waitForEnter();
        audioRecorder.stop();
        audioRecorder.awaitCompletion();
        String transcript = transcriptionService.transcribe(tempFile);
        System.out.println(transcript);
        clipboardService.copyToClipboard(transcript);
    }
}
```

Key runtime behavior:

* Forces `java.home` when missing so Java Sound can discover CoreAudio providers inside a native image.

* Shows a "● recording" prompt, waits for Enter, and then joins the background writer thread before calling OpenAI.

* Sends failures to stderr, prints the temp WAV path for inspection, and cleans up files after a successful clipboard copy.

## Capturing Microphone Audio

`AudioRecorder` walks through possible sample rates, channels, and endianness to find a working `TargetDataLine`. It streams audio to WAV using Java Sound’s `AudioSystem`.

```JAVA
for (AudioFormat candidate : candidateFormats()) {
    DataLine.Info info = new DataLine.Info(TargetDataLine.class, candidate);
    for (Mixer.Info mixerInfo : mixers) {
        Mixer mixer = AudioSystem.getMixer(mixerInfo);
        if (!mixer.isLineSupported(info)) {
            continue;
        }
        TargetDataLine candidateLine = (TargetDataLine) mixer.getLine(info);
        candidateLine.open(candidate);
        candidateLine.start();
        line = candidateLine;
        selectedFormat = candidate;
        break outer;
    }
}
```

Highlights:

* Runs the WAV writer on a dedicated daemon thread so Picocli stays responsive.

* When the mixer cannot satisfy the requested format, it reopens the line to discover the mixer’s default format.

* Offers helpful diagnostics if no microphone is available or permissions are missing.

## Talking to the OpenAI Audio API

`TranscriptionService` uses Micronaut’s `HttpClient` to POST a multipart request to `/v1/audio/transcriptions`.

```JAVA
MultipartBody body = MultipartBody.builder()
        .addPart("model", model)
        .addPart("response_format", "text")
        .addPart("prompt", prompt)
        .addPart("file", fileName, MediaType.of("audio/wav"), audioBytes)
        .build();

MutableHttpRequest<?> request = HttpRequest.POST(URI.create(API), body)
        .accept(MediaType.TEXT_PLAIN_TYPE)
        .contentType(MediaType.MULTIPART_FORM_DATA_TYPE)
        .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey);
```

* Reads the binary WAV into memory for the multipart upload (typical mic captures are tiny, so this stays lightweight).

* Accepts overrides via Micronaut properties: `openai.model` defaults to `whisper-1`, and `openai.prompt` defaults to `Output in English`.

* Throws a descriptive `IllegalStateException` when OpenAI responds with non-2xx status codes to surface API errors in the CLI.

## Clipboard Integration

`ClipboardService` shells out to `pbcopy`, letting the transcript land directly on the macOS clipboard. Any non-zero exit code triggers a warning, making it easy to diagnose if `pbcopy` is unavailable.

## Running Locally

1. Export the OpenAI API key:

```BASH
export OPENAI_API_KEY=sk-...
```

2. Launch the CLI in JVM mode:

```BASH
./mvnw exec:exec -Dexec.mainClass=com.iqbalaissaoui.WhispererCommand
```

Speak, press Enter, and the transcript appears in both the terminal output and your clipboard.

### Building the Native Image

The Maven build is preconfigured for GraalVM via `-Dpackaging=native-image`.

```BASH
./mvnw -Dpackaging=native-image package
./target/whisperer
```

Tips:

* Ensure `JAVA_HOME` or `GRAALVM_HOME` points to your GraalVM distribution before running the native binary; otherwise Java Sound cannot bootstrap.

* Use `mvn -Pnative package` if you prefer Maven’s native profile instead of the packaging flag.

## Testing Strategy

`WhispererCommandTest` runs Picocli inside a Micronaut context marked with `Environment.TEST`. The command short-circuits before touching the microphone, which keeps CI runs hermetic while still checking that Picocli wiring and the `-v` flag work.

## Source Code

> **Tip:**
> References
>
>
>
> [Whisperer source on GitLab](https://gitlab.com/iaissaoui/examples/-/tree/main/whisperer?ref_type=heads)

## macOS Automator Launcher

To avoid running the binary manually, create an Automator "Quick Action" that executes the bundled `whisperer.sh` script. Assign it a keyboard shortcut, and macOS will start `whisperer` and begin recording whenever you trigger the shortcut.

## Challenges

* Language drift: Whisper occasionally ignores the language you speak, especially in multilingual sessions, and eagerly translates English prompts into another language (or vice versa). The CLI already sends an `openai.prompt` with "Output in English" by default, but that is just a hint—Whisper may still decide to translate.

* What helps: When you need a strict language, pass the `language` field in `TranscriptionService` (for English, `en`) or override the Micronaut property (`./mvnw -Dopenai.language=en`). You can also tighten the prompt—e.g., `-Dopenai.prompt="Transcribe exactly what you hear in English."`—and add a quick post-processing check in the command to detect non-English text and re-run with a more forceful prompt. Those adjustments keep Whisper focused on transcription instead of translation while still letting you fall back to its defaults when you want auto-language detection.

