Introduction: Why Customize Transcription?

Welcome to the first lesson of the course, where we will explore how to make your audio transcriptions smarter and more accurate by customizing the transcription process. In many real-world situations, audio files can be in different languages or contain specific topics, names, or jargon. By customizing the transcription settings, you can help the model understand your audio better and produce more accurate results.

In this lesson, you will learn how to use custom parameters — specifically, the language and prompt options — when transcribing audio with OpenAI GPT-4o Mini in Java using direct HTTP requests. These options allow you to tell the model what language to expect and give it extra context about the audio, which can be very helpful for meetings, interviews, or technical discussions.

Recall: Basic Transcription with HTTP Requests

Before we dive into custom parameters, let's remind ourselves how a basic transcription works using direct HTTP requests. In a simple setup, you provide an audio file to the model via a multipart/form-data POST request, and it returns the text it hears using default settings.

For example, in previous lessons, you might have seen code like this:

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;

// Basic transcription request
// Create a unique boundary to separate form data parts
String boundary = "----boundary" + System.currentTimeMillis();
// Build the multipart form data with audio file
byte[] formData = buildBasicFormData(audioFile, boundary);

// Create HTTP POST request to the transcription endpoint
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(baseUrl + "/v1/audio/transcriptions"))
    .header("Authorization", "Bearer " + apiKey) // API authentication
    .header("Content-Type", "multipart/form-data; boundary=" + boundary) // Specify form data type
    .POST(HttpRequest.BodyPublishers.ofByteArray(formData)) // Send form data as request body
    .build();

// Send the request and get the response
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

This code sends an audio file to the model and returns the transcribed text. By default, the model tries to detect the language and does not use any extra context.

Key Custom Parameters: Language and Prompt

Now, let's look at how you can make your transcriptions even better by using two important parameters: language and prompt.

  • language: This parameter tells the model what language to expect in the audio. For example, if your audio is in Spanish, you can set language to "es". This helps the model avoid mistakes in language detection and improves accuracy.

  • prompt: This parameter lets you give the model extra information about the audio. For example, you can tell it, "This is a meeting about project planning," or provide a list of names or technical terms that might appear. This helps the model understand the context and transcribe tricky words more accurately.

Building HTTP Requests with Custom Parameters

Let's build up the code step by step to see how to use these custom parameters in your Java transcription project using direct HTTP requests.

1. Setting Up the Dependencies and Imports

First, ensure you have the required dependencies in your project. You'll need to add Jackson for JSON parsing to your Maven or Gradle configuration:

Maven (pom.xml):

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.2</version>
</dependency>
<dependency>
    <groupId>io.github.cdimascio</groupId>
    <artifactId>dotenv-java</artifactId>
    <version>3.0.0</version>
</dependency>

Gradle (build.gradle):

implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
implementation 'io.github.cdimascio:dotenv-java:3.0.0'

Then add the necessary imports:

// Environment variable loading
import io.github.cdimascio.dotenv.Dotenv;
// JSON parsing and handling
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
// File operations
import java.io.File;
import java.io.IOException;
// HTTP client functionality
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
// File reading operations
import java.nio.file.Files;
2. Setting Up the HTTP Client and Configuration

Next, you need to set up the HttpClient and load your configuration. This part loads your API key and base URL from a .env file and creates the HTTP client.

// Load environment variables from .env file (ignores if file doesn't exist)
Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
// Get the OpenAI API key from environment variables
String apiKey = dotenv.get("OPENAI_API_KEY");
// Get the base URL for API requests (allows for custom endpoints)
String baseUrl = dotenv.get("OPENAI_BASE_URL");
// Create HTTP client for making API requests
HttpClient httpClient = HttpClient.newHttpClient();
// Create JSON parser for handling API responses
ObjectMapper objectMapper = new ObjectMapper();
  • Dotenv.configure().ignoreIfMissing().load() loads environment variables from a .env file using the dotenv-java library.
  • apiKey is needed to authenticate with the OpenAI API.
  • baseUrl specifies the API endpoint URL, which may be different from the default OpenAI endpoint.
  • The HttpClient is the main object you use to send HTTP requests.
  • ObjectMapper is Jackson's main class for parsing JSON responses safely.
3. Preparing the Audio File

Next, you need to specify the audio file you want to transcribe.

// Create a File object pointing to the audio file to transcribe
File audioFile = new File("recording.wav");
  • audioFile is a Java File object pointing to your audio file.
4. Building Multipart Form Data with Custom Parameters

Now, let's create the multipart/form-data with custom parameters. This is where we add the language and prompt fields to the form data.

private byte[] buildFormData(File audioFile, String boundary, String language, String prompt) throws IOException {
    // StringBuilder to construct the multipart form data structure
    StringBuilder formData = new StringBuilder();
    
    // Add model field - specifies which transcription model to use
    formData.append("--").append(boundary).append("\r\n");
    formData.append("Content-Disposition: form-data; name=\"model\"\r\n\r\n");
    formData.append("gpt-4o-mini-transcribe\r\n");
    
    // Add language field only if provided - helps model focus on specific language
    if (language != null && !language.isEmpty()) {
        formData.append("--").append(boundary).append("\r\n");
        formData.append("Content-Disposition: form-data; name=\"language\"\r\n\r\n");
        formData.append(language).append("\r\n");
    }
    
    // Add prompt field only if provided - gives context to improve transcription accuracy
    if (prompt != null && !prompt.isEmpty()) {
        formData.append("--").append(boundary).append("\r\n");
        formData.append("Content-Disposition: form-data; name=\"prompt\"\r\n\r\n");
        formData.append(prompt).append("\r\n");
    }
    
    // Add file field with proper headers for audio file upload
    formData.append("--").append(boundary).append("\r\n");
    formData.append("Content-Disposition: form-data; name=\"file\"; filename=\"").append(audioFile.getName()).append("\"\r\n");
    formData.append("Content-Type: audio/wav\r\n\r\n");
    
    // Convert form data structure to bytes
    byte[] formBytes = formData.toString().getBytes();
    // Read the actual audio file content as bytes
    byte[] fileBytes = Files.readAllBytes(audioFile.toPath());
    // Create the final boundary marker to end the multipart data
    byte[] endBytes = ("\r\n--" + boundary + "--\r\n").getBytes();
    
    // Combine all parts: form headers + file content + end boundary
    byte[] result = new byte[formBytes.length + fileBytes.length + endBytes.length];
    System.arraycopy(formBytes, 0, result, 0, formBytes.length);
    System.arraycopy(fileBytes, 0, result, formBytes.length, fileBytes.length);
    System.arraycopy(endBytes, 0, result, formBytes.length + fileBytes.length, endBytes.length);
    
    return result;
}

Key points about this multipart form data construction:

  • Each field is separated by a boundary marker (--boundary)
  • The model field specifies which transcription model to use
  • The language and prompt fields are only added if they have actual values (not null or empty)
  • The file field contains the actual audio file data
  • We use proper Content-Disposition headers for each form field
5. Sending the Request and Parsing the Response

Now, send the request to the API and handle the response with proper JSON parsing using Jackson.

public String transcribeWithOptions(File audioFile, String language, String prompt) throws Exception {
    // Generate unique boundary for this request using current timestamp
    String boundary = "----boundary" + System.currentTimeMillis();
    // Build the multipart form data with all parameters
    byte[] formData = buildFormData(audioFile, boundary, language, prompt);
    
    // Create HTTP POST request to the transcription API endpoint
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(baseUrl + "/v1/audio/transcriptions"))
        .header("Authorization", "Bearer " + apiKey) // Authenticate with API key
        .header("Content-Type", "multipart/form-data; boundary=" + boundary) // Specify content type
        .POST(HttpRequest.BodyPublishers.ofByteArray(formData)) // Send form data as request body
        .build();
        
    // Send the request and receive the response
    HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
    
    // Check if the request was successful (HTTP 200)
    if (response.statusCode() == 200) {
        // Parse the JSON response safely using Jackson
        JsonNode jsonResponse = objectMapper.readTree(response.body());
        // Extract the transcribed text from the "text" field
        return jsonResponse.get("text").asText();
    } else {
        // Handle API errors by throwing an exception with error details
        throw new RuntimeException("API request failed: " + response.body());
    }
}

Understanding the Boundary Generation:

The boundary ("----boundary" + System.currentTimeMillis()) is a unique string that separates different parts of the multipart form data. Here's why we use a timestamp:

  • Uniqueness: System.currentTimeMillis() returns the current time in milliseconds since January 1, 1970, making each boundary unique across different requests
  • Simplicity: It's a simple way to generate a unique identifier without additional dependencies
  • HTTP Standard Compliance: The boundary must not appear in the actual data being sent, and using a timestamp makes this extremely unlikely

Alternative approaches you might see include:

  • UUID: UUID.randomUUID().toString() would provide better randomness but requires importing java.util.UUID
  • Random numbers: Using Math.random() or Random class for generating unique identifiers

The timestamp approach is preferred here because it's lightweight, requires no additional imports, and provides sufficient uniqueness for HTTP requests.

Key points about the request:

  • We create a unique boundary for the multipart request using the current timestamp
  • The Authorization header includes the Bearer token with our API key
  • The Content-Type header specifies multipart/form-data with the boundary
  • We use Jackson's ObjectMapper to parse the JSON response safely, which handles escaped characters and malformed JSON much better than string manipulation
  • The readTree() method parses the JSON into a JsonNode, and we extract the "text" field using get("text").asText()
  • Proper error handling throws an exception if the API call fails
Full Example

Here's how it all fits together:

import io.github.cdimascio.dotenv.Dotenv;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;

public class CustomTranscription {
    
    // Instance variables to hold configuration and HTTP client
    private final String apiKey;
    private final String baseUrl;
    private final HttpClient httpClient;
    private final ObjectMapper objectMapper;
    
    public CustomTranscription() {
        // Load configuration from environment variables
        Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
        this.apiKey = dotenv.get("OPENAI_API_KEY");
        this.baseUrl = dotenv.get("OPENAI_BASE_URL");
        // Initialize HTTP client for making API requests
        this.httpClient = HttpClient.newHttpClient();
        // Initialize JSON parser for handling responses
        this.objectMapper = new ObjectMapper();
    }
    
    public String transcribeWithOptions(File audioFile, String language, String prompt) throws Exception {
        // Generate unique boundary for this multipart request
        String boundary = "----boundary" + System.currentTimeMillis();
        // Build the complete multipart form data
        byte[] formData = buildFormData(audioFile, boundary, language, prompt);
        
        // Create HTTP POST request with all necessary headers
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(baseUrl + "/v1/audio/transcriptions"))
            .header("Authorization", "Bearer " + apiKey) // API authentication
            .header("Content-Type", "multipart/form-data; boundary=" + boundary) // Content type
            .POST(HttpRequest.BodyPublishers.ofByteArray(formData)) // Request body
            .build();
            
        // Send request and get response
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        
        // Process the response
        if (response.statusCode() == 200) {
            // Parse JSON response safely using Jackson
            JsonNode jsonResponse = objectMapper.readTree(response.body());
            // Extract and return the transcribed text
            return jsonResponse.get("text").asText();
        } else {
            // Handle API errors with detailed error message
            throw new RuntimeException("API request failed: " + response.body());
        }
    }
    
    private byte[] buildFormData(File audioFile, String boundary, String language, String prompt) throws IOException {
        // Use StringBuilder to construct multipart form data
        StringBuilder formData = new StringBuilder();
        
        // Add model field - tells API which transcription model to use
        formData.append("--").append(boundary).append("\r\n");
        formData.append("Content-Disposition: form-data; name=\"model\"\r\n\r\n");
        formData.append("gpt-4o-mini-transcribe\r\n");
        
        // Add language field only if specified - helps with language detection
        if (language != null && !language.isEmpty()) {
            formData.append("--").append(boundary).append("\r\n");
            formData.append("Content-Disposition: form-data; name=\"language\"\r\n\r\n");
            formData.append(language).append("\r\n");
        }
        
        // Add prompt field only if specified - provides context for better accuracy
        if (prompt != null && !prompt.isEmpty()) {
            formData.append("--").append(boundary).append("\r\n");
            formData.append("Content-Disposition: form-data; name=\"prompt\"\r\n\r\n");
            formData.append(prompt).append("\r\n");
        }
        
        // Add file field with proper headers for the audio file
        formData.append("--").append(boundary).append("\r\n");
        formData.append("Content-Disposition: form-data; name=\"file\"; filename=\"").append(audioFile.getName()).append("\"\r\n");
        formData.append("Content-Type: audio/wav\r\n\r\n");
        
        // Convert text parts to bytes
        byte[] formBytes = formData.toString().getBytes();
        // Read the audio file content
        byte[] fileBytes = Files.readAllBytes(audioFile.toPath());
        // Create the closing boundary
        byte[] endBytes = ("\r\n--" + boundary + "--\r\n").getBytes();
        
        // Combine all parts into a single byte array
        byte[] result = new byte[formBytes.length + fileBytes.length + endBytes.length];
        // Copy form data headers
        System.arraycopy(formBytes, 0, result, 0, formBytes.length);
        // Copy audio file content
        System.arraycopy(fileBytes, 0, result, formBytes.length, fileBytes.length);
        // Copy closing boundary
        System.arraycopy(endBytes, 0, result, formBytes.length + fileBytes.length, endBytes.length);
        
        return result;
    }
    
    public static void main(String[] args) {
        try {
            // Create transcription service instance
            CustomTranscription transcriber = new CustomTranscription();
            
            // Specify the audio file to transcribe
            File audioFile = new File("recording.wav");
            
            // Transcribe with custom language and prompt parameters
            String result = transcriber.transcribeWithOptions(
                audioFile, 
                "en",  // Language: English
                "This is a meeting about project planning." // Context prompt
            );
            
            // Display the transcribed text
            System.out.println("Text: " + result);
            
        } catch (Exception e) {
            // Handle any errors that occur during transcription
            System.err.println("Transcription failed: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Sample Output:

Text: Today we discussed the project timeline and assigned tasks to each team member.

This output will vary depending on your audio file and the prompt you provide.

Summary and Practice Preview

In this lesson, you learned how to improve your audio transcriptions by customizing the language and prompt parameters using direct HTTP requests to the OpenAI API with the GPT-4o Mini transcription model. You learned how to:

  • Construct multipart/form-data requests manually
  • Add optional parameters only when they have values
  • Parse JSON responses safely using Jackson's ObjectMapper
  • Handle API errors appropriately
  • Generate unique boundaries for multipart requests using timestamps

Setting the correct language helps the model understand the audio better, and giving a prompt provides helpful context for more accurate results.

Next, you will get a chance to practice using these custom parameters yourself. You'll try different languages and prompts to see how they affect the transcription output. This hands-on practice will help you become comfortable with customizing your transcriptions for any situation.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal