OpenAI 텍스트-음성

OpenAI 텍스트-음성 (TTS)

OpenAI Audio API는 TTS(text-to-speech) 모델 기반의 speech 엔드포인트를 제공해서, 블로그 글을 내레이션하거나, 여러 언어로 음성 오디오를 만들거나, 스트리밍으로 실시간 오디오 출력을 제공할 수 있게 해줘요. 이 글에서는 의존성 추가와 자동 설정, 음성 옵션, 실시간 스트리밍, 그리고 HTTP 클라이언트 커스터마이징과 마이그레이션 가이드까지 폭넓게 다뤄볼게요.

출처: 문서

본문

Introduction

Audio API는 OpenAI의 TTS(text-to-speech) 모델을 기반으로 하는 speech 엔드포인트를 제공해서, 사용자가 다음을 할 수 있게 해줘요:

  • 작성한 블로그 글을 내레이션하기.

  • 여러 언어로 음성 오디오 만들기.

  • 스트리밍을 사용해 실시간 오디오 출력 제공하기.

__ 2.0.0-M5 버전부터 Spring AI는 모든 OpenAI 모델을 위해 내부적으로 공식 openai-java SDK를 사용해요. 전환은 매끄러울 것으로 예상되며, OpenAI API 프로퍼티와 빌더의 기존 사용자에게는 호환성이 깨지는 변경이 없어요. 문제를 발견하면 Spring AI GitHub Issues로 알려주세요.

Prerequisites

  1. OpenAI 계정을 만들고 API 키를 받으세요. OpenAI signup page에서 가입하고, API Keys page에서 API 키를 생성할 수 있어요.

  2. 프로젝트의 빌드 파일에 spring-ai-openai 의존성을 추가하세요. 자세한 내용은 Dependency Management 섹션을 참고해주세요.

Auto-configuration

__ Spring AI 자동 설정과 스타터 모듈 아티팩트 이름에 큰 변경이 있었어요. 자세한 내용은 upgrade notes를 참고해주세요.

Spring AI는 OpenAI Text-to-Speech 클라이언트에 대한 Spring Boot 자동 설정을 제공해요. 활성화하려면 프로젝트의 Maven pom.xml 파일에 다음 의존성을 추가하세요:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

또는 Gradle build.gradle 빌드 파일에 추가할 수도 있어요:

dependencies {
    implementation 'org.springframework.ai:spring-ai-starter-model-openai'
}
__ 빌드 파일에 Spring AI BOM을 추가하려면 Dependency Management 섹션을 참고해주세요.

Speech Properties

Connection Properties

프리픽스 spring.ai.openai는 OpenAI에 연결할 수 있게 해주는 프로퍼티 프리픽스예요.

Property Description Default
spring.ai.openai.base-url 연결할 URL api.openai.com
spring.ai.openai.api-key API 키 -
spring.ai.openai.organization-id 선택적으로 API 요청에 사용할 조직을 지정할 수 있어요. -
spring.ai.openai.project-id 선택적으로 API 요청에 사용할 프로젝트를 지정할 수 있어요. -
spring.ai.openai.timeout OpenAI 클라이언트의 요청 타임아웃. 60 sec.
spring.ai.openai.max-retries OpenAI 클라이언트의 최대 재시도 횟수. 3
spring.ai.openai.proxy OpenAI 클라이언트의 프록시 설정. -
spring.ai.openai.custom-headers OpenAI 클라이언트 요청에 추가할 커스텀 HTTP 헤더. empty
spring.ai.openai.connection-pool-metrics-enabled 기저 OkHttp 클라이언트의 연결 풀 메트릭을 활성화할지 여부. false
__ 여러 조직에 속한 사용자(또는 레거시 사용자 API 키로 프로젝트에 접근하는 사용자)의 경우, 선택적으로 API 요청에 사용할 조직과 프로젝트를 지정할 수 있어요. 이 API 요청의 사용량은 지정된 조직과 프로젝트의 사용량으로 집계돼요.

Configuration Properties

__ 오디오 음성 자동 설정의 활성화/비활성화는 이제 프리픽스 spring.ai.model.audio.speech가 붙은 최상위 프로퍼티로 설정해요. 활성화하려면 spring.ai.model.audio.speech=openai (기본값으로 활성화됨). 비활성화하려면 spring.ai.model.audio.speech=none (또는 openai와 일치하지 않는 어떤 값). 이 변경은 여러 모델의 설정을 허용하기 위한 것이에요.

프리픽스 spring.ai.openai.audio.speech는 OpenAI Text-to-Speech 클라이언트를 구성할 수 있게 해주는 프로퍼티 프리픽스예요.

Property Description Default
spring.ai.model.audio.speech Audio Speech Model 활성화 openai
spring.ai.openai.audio.speech.base-url 연결할 URL api.openai.com
spring.ai.openai.audio.speech.api-key API 키 -
spring.ai.openai.audio.speech.organization-id 선택적으로 API 요청에 사용할 조직을 지정할 수 있어요. -
spring.ai.openai.audio.speech.project-id 선택적으로 API 요청에 사용할 프로젝트를 지정할 수 있어요. -
spring.ai.openai.audio.speech.model 오디오 생성에 사용할 모델의 ID. 사용 가능한 모델: gpt-4o-mini-tts (기본값, 속도와 비용에 최적화), gpt-4o-tts (더 높은 품질), tts-1 (레거시, 속도에 최적화), 또는 tts-1-hd (레거시, 품질에 최적화). gpt-4o-mini-tts
spring.ai.openai.audio.speech.voice 합성에 사용할 음성. OpenAI TTS API의 경우, 선택한 모델에 사용 가능한 음성 중 하나: alloy, echo, fable, onyx, nova, shimmer. alloy
spring.ai.openai.audio.speech.response-format 오디오 출력의 형식. 지원되는 형식은 mp3, opus, aac, flac, wav, pcm. mp3
spring.ai.openai.audio.speech.speed 음성 합성의 속도. 허용 범위는 0.25(가장 느림)에서 4.0(가장 빠름). 1.0
spring.ai.openai.audio.speech.instructions 음성 전달을 위한 제어 지침 (예: 톤, 속도). gpt-4o-mini-tts 같은 모델에서만 지원되며, tts-1과 tts-1-hd에서는 무시돼요. -
__ 공통 spring.ai.openai.base-url, spring.ai.openai.api-key, spring.ai.openai.organization-id, spring.ai.openai.project-id 프로퍼티를 재정의할 수 있어요. spring.ai.openai.audio.speech.base-url, spring.ai.openai.audio.speech.api-key, spring.ai.openai.audio.speech.organization-id, spring.ai.openai.audio.speech.project-id 프로퍼티가 설정되면 공통 프로퍼티보다 우선해요. 서로 다른 모델과 서로 다른 모델 엔드포인트에 서로 다른 OpenAI 계정을 사용하려 할 때 유용해요.
__ spring.ai.openai.audio.speech 프리픽스가 붙은 모든 프로퍼티는 런타임에 재정의할 수 있어요.

Runtime Options

OpenAiAudioSpeechOptions 클래스는 텍스트-음성 요청을 할 때 사용할 옵션을 제공해요. 시작 시 spring.ai.openai.audio.speech로 지정한 옵션이 사용되지만 런타임에 이를 재정의할 수 있어요.

OpenAiAudioSpeechOptions 클래스는 TextToSpeechOptions 인터페이스를 구현하며, 이식 가능한 옵션과 OpenAI 특정 설정 옵션을 모두 제공해요.

예:

OpenAiAudioSpeechOptions speechOptions = OpenAiAudioSpeechOptions.builder()
    .model("gpt-4o-mini-tts")
    .voice(OpenAiAudioApi.SpeechRequest.Voice.ALLOY)
    .responseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
    .speed(1.0)
    .instructions("Friendly; warm tone; natural pauses")
    .build();

TextToSpeechPrompt speechPrompt = new TextToSpeechPrompt("Hello, this is a text-to-speech example.", speechOptions);
TextToSpeechResponse response = openAiAudioSpeechModel.call(speechPrompt);

Manual Configuration

프로젝트의 Maven pom.xml 파일에 spring-ai-openai 의존성을 추가하세요:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai</artifactId>
</dependency>

또는 Gradle build.gradle 빌드 파일에 추가할 수도 있어요:

dependencies {
    implementation 'org.springframework.ai:spring-ai-openai'
}
__ 빌드 파일에 Spring AI BOM을 추가하려면 Dependency Management 섹션을 참고해주세요.

다음으로 OpenAiAudioSpeechModel을 만들어볼게요:

var openAiAudioApi = new OpenAiAudioApi()
    .apiKey(System.getenv("OPENAI_API_KEY"))
    .build();

var openAiAudioSpeechModel = new OpenAiAudioSpeechModel(openAiAudioApi);

var speechOptions = OpenAiAudioSpeechOptions.builder()
    .responseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
    .speed(1.0)
    .model(OpenAiAudioApi.TtsModel.GPT_4_O_MINI_TTS.value)
    .build();

var speechPrompt = new TextToSpeechPrompt("Hello, this is a text-to-speech example.", speechOptions);
TextToSpeechResponse response = openAiAudioSpeechModel.call(speechPrompt);

// Accessing metadata (rate limit info)
OpenAiAudioSpeechResponseMetadata metadata = (OpenAiAudioSpeechResponseMetadata) response.getMetadata();

byte[] responseAsBytes = response.getResult().getOutput();

Streaming Real-time Audio

Speech API는 chunk transfer encoding을 사용한 실시간 오디오 스트리밍을 지원해요. 즉 전체 파일이 생성되고 접근 가능해지기 전에도 오디오를 재생할 수 있어요.

OpenAiAudioSpeechModel은 StreamingTextToSpeechModel 인터페이스를 구현해서 표준 및 스트리밍 기능을 모두 제공해요.

var openAiAudioApi = new OpenAiAudioApi()
    .apiKey(System.getenv("OPENAI_API_KEY"))
    .build();

var openAiAudioSpeechModel = new OpenAiAudioSpeechModel(openAiAudioApi);

OpenAiAudioSpeechOptions speechOptions = OpenAiAudioSpeechOptions.builder()
    .voice(OpenAiAudioApi.SpeechRequest.Voice.ALLOY)
    .speed(1.0)
    .responseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
    .model(OpenAiAudioApi.TtsModel.GPT_4_O_MINI_TTS.value)
    .build();

TextToSpeechPrompt speechPrompt = new TextToSpeechPrompt("Today is a wonderful day to build something people love!", speechOptions);

Flux<TextToSpeechResponse> responseStream = openAiAudioSpeechModel.stream(speechPrompt);

// You can also stream raw audio bytes directly
Flux<byte[]> audioByteStream = openAiAudioSpeechModel.stream("Hello, world!");

스트림의 각 요소는 최대 8KB의 오디오를 담아요. 8KB는 OpenAiAudioSpeechModel이 기저 HTTP 응답에서 바이트를 끌어올 때 사용하는 내부 읽기 버퍼 크기예요. 각 읽기 시점에 OpenAI에서 도착한 데이터 양에 따라 청크는 그보다 작을 수 있어요.

__ 이 스트림을 블로킹 java.io.InputStream만 받는 API에 넘겨야 한다면, 중요한 실무 관례와 테스트된 참조 구현은 Consuming the Stream as a Blocking InputStream을 참고해주세요.

각 열린 스트림은 응답 기간 내내 스레드 하나를 점유해요. 그래서 OpenAiAudioSpeechModel은 Reactor의 공유 Schedulers.boundedElastic() 대신 전용 bounded-elastic 스케줄러에서 스트리밍을 실행해서, 과중한 음성 스트리밍 부하가 애플리케이션의 다른 곳에서 이루어지는 무관한 블로킹 작업을 굶기지 않도록 해요. OpenAiAudioSpeechModel.builder().streamScheduler(customScheduler)로 재정의해서 컴포넌트 간에 스케줄러를 공유하거나, 예상되는 동시 스트림 부하에 맞게 풀 크기를 조정할 수 있어요.

Customizing the HTTP Client

Spring AI는 내부적으로 공식 openai-java SDK를 사용하며, SpringAiOpenAiHttpClient.Builder가 만든 커스텀 OkHttp 클라이언트로 HTTP 전송을 구성해요. 기저 OkHttpClient가 생성되기 전에 하나 이상의 OpenAiHttpClientBuilderCustomizer 빈을 노출해서 그 빌더를 가로챌 수 있어요. 각 커스터마이저는 모든 OpenAI 모델(chat, embedding, image, audio, moderation)이 사용하는 동일한 빌더를 받으므로, 커스터마이징이 균일하게 적용돼요.

@FunctionalInterface
public interface OpenAiHttpClientBuilderCustomizer {
    void customize(SpringAiOpenAiHttpClient.Builder builder);
}

전형적인 사용 사례:

  • OkHttp Interceptor 인스턴스 등록 (인증, 전파 헤더, 커스텀 로깅);

  • 디스패처 ExecutorService 교체 (예: 비동기 I/O를 가상 스레드로 라우팅);

  • 빌더가 노출하는 프록시, SSL, 호스트 이름 검증, 연결 풀 크기 구성.

커스터마이저가 여러 개 있으면 @Order / Ordered 순서로, Spring AI 기본값 이후에 적용되므로 사용자 코드가 우선해요.

OpenAi*Model.Builder를 통해 모델을 수동으로 연결할 때도 동일한 훅을 사용할 수 있어요:

var chatModel = OpenAiChatModel.builder()
    .options(OpenAiChatOptions.builder().model("gpt-4o").build())
    .httpClientBuilderCustomizer(myCustomizer)
    .build();

Migration Guide

더 이상 사용하지 않는(deprecated) SpeechModel과 SpeechPrompt 클래스에서 업그레이드한다면, 이 가이드가 새로운 공유 인터페이스로 마이그레이션하는 자세한 지침을 제공해요.

Breaking Changes Summary

이 마이그레이션에는 다음의 호환성이 깨지는 변경이 포함돼요:

  1. Removed Classes : 여섯 개의 deprecated 클래스가 org.springframework.ai.openai.audio.speech 패키지에서 제거됐어요

  2. Package Changes : 핵심 TTS 클래스가 org.springframework.ai.audio.tts 패키지로 이동했어요

  3. Type Changes : speed 매개변수가 모든 OpenAI TTS 컴포넌트에서 Float에서 Double로 변경됐어요

  4. Interface Hierarchy : TextToSpeechModel이 이제 StreamingTextToSpeechModel을 확장해요

Class Mapping Reference

Deprecated (Removed) New Interface
SpeechModel TextToSpeechModel
StreamingSpeechModel StreamingTextToSpeechModel
SpeechPrompt TextToSpeechPrompt
SpeechResponse TextToSpeechResponse
SpeechMessage TextToSpeechMessage
Speech (in org.springframework.ai.openai.audio.speech) Speech (in org.springframework.ai.audio.tts)

Step-by-Step Migration Instructions

Step 1: Update Imports

이전 org.springframework.ai.openai.audio.speech 패키지의 모든 import를 새로운 공유 인터페이스로 바꿔주세요:

Find:    import org.springframework.ai.openai.audio.speech.SpeechModel;
Replace: import org.springframework.ai.audio.tts.TextToSpeechModel;

Find:    import org.springframework.ai.openai.audio.speech.StreamingSpeechModel;
Replace: import org.springframework.ai.audio.tts.StreamingTextToSpeechModel;

Find:    import org.springframework.ai.openai.audio.speech.SpeechPrompt;
Replace: import org.springframework.ai.audio.tts.TextToSpeechPrompt;

Find:    import org.springframework.ai.openai.audio.speech.SpeechResponse;
Replace: import org.springframework.ai.audio.tts.TextToSpeechResponse;

Find:    import org.springframework.ai.openai.audio.speech.SpeechMessage;
Replace: import org.springframework.ai.audio.tts.TextToSpeechMessage;

Find:    import org.springframework.ai.openai.audio.speech.Speech;
Replace: import org.springframework.ai.audio.tts.Speech;

Step 2: Update Type References

코드의 모든 타입 참조를 바꿔주세요:

Find:    SpeechModel
Replace: TextToSpeechModel

Find:    StreamingSpeechModel
Replace: StreamingTextToSpeechModel

Find:    SpeechPrompt
Replace: TextToSpeechPrompt

Find:    SpeechResponse
Replace: TextToSpeechResponse

Find:    SpeechMessage
Replace: TextToSpeechMessage

Step 3: Update Speed Parameter (Float → Double)

speed 매개변수가 Float에서 Double로 변경됐어요. 모든 발생 지점을 업데이트하세요:

Find:    .speed(1.0f)
Replace: .speed(1.0)

Find:    .speed(0.5f)
Replace: .speed(0.5)

Find:    Float speed
Replace: Double speed

Float 값이 있는 직렬화된 데이터나 설정 파일이 있다면 그것도 업데이트해야 해요:

// Before
{
  "speed": 1.0
}

// After (no code change needed for JSON, but be aware of type change in Java)
{
  "speed": 1.0
}

Step 4: Update Bean Declarations

Spring Boot 자동 설정이나 수동 빈 정의가 있다면:

// Before
@Bean
public SpeechModel speechModel(OpenAiAudioApi audioApi) {
    return new OpenAiAudioSpeechModel(audioApi);
}

// After
@Bean
public TextToSpeechModel textToSpeechModel(OpenAiAudioApi audioApi) {
    return new OpenAiAudioSpeechModel(audioApi);
}

Code Migration Examples

Example 1: Basic Text-to-Speech Conversion

Before (deprecated):

import org.springframework.ai.openai.audio.speech.*;

@Service
public class OldNarrationService {

    private final SpeechModel speechModel;

    public OldNarrationService(SpeechModel speechModel) {
        this.speechModel = speechModel;
    }

    public byte[] createNarration(String text) {
        SpeechPrompt prompt = new SpeechPrompt(text);
        SpeechResponse response = speechModel.call(prompt);
        return response.getResult().getOutput();
    }
}

After (using shared interfaces):

import org.springframework.ai.audio.tts.*;
import org.springframework.ai.openai.OpenAiAudioSpeechModel;

@Service
public class NarrationService {

    private final TextToSpeechModel textToSpeechModel;

    public NarrationService(TextToSpeechModel textToSpeechModel) {
        this.textToSpeechModel = textToSpeechModel;
    }

    public byte[] createNarration(String text) {
        TextToSpeechPrompt prompt = new TextToSpeechPrompt(text);
        TextToSpeechResponse response = textToSpeechModel.call(prompt);
        return response.getResult().getOutput();
    }
}

Example 2: Text-to-Speech with Custom Options

Before (deprecated):

import org.springframework.ai.openai.audio.speech.*;
import org.springframework.ai.openai.api.OpenAiAudioApi;

SpeechModel model = new OpenAiAudioSpeechModel(audioApi);

OpenAiAudioSpeechOptions options = OpenAiAudioSpeechOptions.builder()
    .model("tts-1")
    .voice(OpenAiAudioApi.SpeechRequest.Voice.NOVA)
    .speed(1.0f)  // Float value
    .responseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
    .build();

SpeechPrompt prompt = new SpeechPrompt("Hello, world!", options);
SpeechResponse response = model.call(prompt);
byte[] audio = response.getResult().getOutput();

After (using shared interfaces):

import org.springframework.ai.audio.tts.*;
import org.springframework.ai.openai.OpenAiAudioSpeechModel;
import org.springframework.ai.openai.OpenAiAudioSpeechOptions;
import org.springframework.ai.openai.api.OpenAiAudioApi;

TextToSpeechModel model = new OpenAiAudioSpeechModel(audioApi);

OpenAiAudioSpeechOptions options = OpenAiAudioSpeechOptions.builder()
    .model("tts-1")
    .voice(OpenAiAudioApi.SpeechRequest.Voice.NOVA)
    .speed(1.0)  // Double value
    .responseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
    .build();

TextToSpeechPrompt prompt = new TextToSpeechPrompt("Hello, world!", options);
TextToSpeechResponse response = model.call(prompt);
byte[] audio = response.getResult().getOutput();

Example 3: Streaming Text-to-Speech

Before (deprecated):

import org.springframework.ai.openai.audio.speech.*;
import reactor.core.publisher.Flux;

StreamingSpeechModel model = new OpenAiAudioSpeechModel(audioApi);
SpeechPrompt prompt = new SpeechPrompt("Stream this text");

Flux<SpeechResponse> stream = model.stream(prompt);
stream.subscribe(response -> {
    byte[] audioChunk = response.getResult().getOutput();
    // Process audio chunk
});

After (using shared interfaces):

import org.springframework.ai.audio.tts.*;
import org.springframework.ai.openai.OpenAiAudioSpeechModel;
import reactor.core.publisher.Flux;

TextToSpeechModel model = new OpenAiAudioSpeechModel(audioApi);
TextToSpeechPrompt prompt = new TextToSpeechPrompt("Stream this text");

Flux<TextToSpeechResponse> stream = model.stream(prompt);
stream.subscribe(response -> {
    byte[] audioChunk = response.getResult().getOutput();
    // Process audio chunk
});

Example 4: Dependency Injection with Spring Boot

Before (deprecated):

@RestController
public class OldSpeechController {

    private final SpeechModel speechModel;

    @Autowired
    public OldSpeechController(SpeechModel speechModel) {
        this.speechModel = speechModel;
    }

    @PostMapping("/narrate")
    public ResponseEntity<byte[]> narrate(@RequestBody String text) {
        SpeechPrompt prompt = new SpeechPrompt(text);
        SpeechResponse response = speechModel.call(prompt);
        return ResponseEntity.ok()
            .contentType(MediaType.parseMediaType("audio/mpeg"))
            .body(response.getResult().getOutput());
    }
}

After (using shared interfaces):

@RestController
public class SpeechController {

    private final TextToSpeechModel textToSpeechModel;

    @Autowired
    public SpeechController(TextToSpeechModel textToSpeechModel) {
        this.textToSpeechModel = textToSpeechModel;
    }

    @PostMapping("/narrate")
    public ResponseEntity<byte[]> narrate(@RequestBody String text) {
        TextToSpeechPrompt prompt = new TextToSpeechPrompt(text);
        TextToSpeechResponse response = textToSpeechModel.call(prompt);
        return ResponseEntity.ok()
            .contentType(MediaType.parseMediaType("audio/mpeg"))
            .body(response.getResult().getOutput());
    }
}

Spring Boot Configuration Changes

Spring Boot 자동 설정 프로퍼티는 동일하게 유지돼요. application.properties나 application.yml 파일을 변경할 필요가 없어요.

하지만 명시적인 빈 참조나 qualifier가 있다면 업데이트하세요:

// Before
@Qualifier("speechModel")

// After
@Qualifier("textToSpeechModel")

Benefits of the Migration

  • Portability : 코드를 한 번 작성하고 OpenAI, ElevenLabs 또는 다른 TTS 제공자 간에 쉽게 전환

  • Consistency : ChatModel 및 다른 Spring AI 추상화와 동일한 패턴

  • Type Safety : 적절한 인터페이스 구현으로 개선된 타입 계층 구조

  • Future-Proof : 새로운 TTS 제공자가 기존 코드와 자동으로 동작

  • Standardization : 모든 TTS 제공자에서 speed 매개변수의 일관된 Double 타입

Common Migration Issues and Solutions

Issue 1: Compilation Error - Cannot Find Symbol SpeechModel

Error:

error: cannot find symbol SpeechModel

Solution: Step 1에서 설명한 대로 import를 업데이트하고, SpeechModel을 TextToSpeechModel로 바꾸세요.

Issue 2: Type Mismatch - Float Cannot Be Converted to Double

Error:

error: incompatible types: float cannot be converted to Double

Solution: 부동소수점 리터럴의 f 접미사를 제거하세요 (예: 1.0f를 1.0으로 변경).

Issue 3: Bean Creation Error at Runtime

Error:

NoSuchBeanDefinitionException: No qualifying bean of type 'SpeechModel'

Solution: 의존성 주입을 SpeechModel 대신 TextToSpeechModel을 사용하도록 업데이트하세요.

Example Code

더 알아보기 (Learn more)