튜토리얼: DSPy 프로그램에서 오디오 사용하기

튜토리얼: DSPy 프로그램에서 오디오 사용하기 (Using Audio in DSPy Programs)

이 튜토리얼은 DSPy를 사용해 오디오 기반 애플리케이션을 위한 파이프라인을 구축하는 방법을 안내할게요.

출처: 문서

본문

Spoken-SQuAD 데이터셋 로드 (Load the Spoken-SQuAD Dataset)

질문 응답에 사용되는 음성 오디오 passages를 포함하는 Spoken-SQuAD 데이터셋(공식 & 튜토리얼 데모용 HuggingFace 버전)을 사용할 거예요:

import random
import dspy
from dspy.datasets import DataLoader

kwargs = dict(fields=("context", "instruction", "answer"), input_keys=("context", "instruction"))
spoken_squad = DataLoader().from_huggingface(dataset_name="AudioLLMs/spoken_squad_test", split="train", trust_remote_code=True, **kwargs)

random.Random(42).shuffle(spoken_squad)
spoken_squad = spoken_squad[:100]

split_idx = len(spoken_squad) // 2
trainset_raw, testset_raw = spoken_squad[:split_idx], spoken_squad[split_idx:]

음성 질문 응답을 위한 DSPy 프로그램 (DSPy program for spoken question answering)

오디오 입력을 사용해 질문에 직접 답하는 간단한 DSPy 프로그램을 정의해 볼게요. 이것은 BasicQA 작업과 매우 유사한데, 유일한 차이는 passage 컨텍스트가 모델이 듣고 질문에 답할 수 있도록 오디오 파일로 제공된다는 점이에요:

class SpokenQASignature(dspy.Signature):
    """Answer the question based on the audio clip."""
    passage_audio: dspy.Audio = dspy.InputField()
    question: str = dspy.InputField()
    answer: str = dspy.OutputField(desc = 'factoid answer between 1 and 5 words')

spoken_qa = dspy.ChainOfThought(SpokenQASignature)

평가 지표 정의 (Define Evaluation Metric)

제공된 참조 답변과 비교해 답변 정확도를 측정하기 위해 Exact Match 지표(dspy.evaluate.answer_exact_match)를 사용할 거예요:

evaluate_program = dspy.Evaluate(devset=testset, metric=dspy.evaluate.answer_exact_match,display_progress=True, num_threads = 10, display_table=True)

evaluate_program(spoken_qa)

이 작은 부분집합에서 MIPROv2는 기준 성능 대비 약 10%의 개선을 이끌어냈어요.

이제 DSPy에서 오디오 입력이 가능한 LLM을 사용하는 방법을 봤으니, 설정을 뒤집어 볼게요.

다음 작업에서는 표준 텍스트 기반 LLM을 사용해 텍스트-음성(text-to-speech) 모델을 위한 프롬프트를 생성하고, 그다음 어떤 하위 작업을 위해 생성된 음성의 품질을 평가할 거예요. 이 접근 방식은 일반적으로 gpt-4o-mini-audio-preview-2024-12-17 같은 LLM이 오디오를 직접 생성하도록 하는 것보다 비용 효율적이면서도, 더 높은 품질의 음성 출력을 위해 최적화할 수 있는 파이프라인을 가능하게 합니다.

목표 감정으로 말하기 위한 TTS 지침 생성하는 DSPy 파이프라인 (DSPy pipeline for generating TTS instructions for speaking with a target emotion)

이제 텍스트 줄과 그것을 말하는 방법에 대한 지침을 모두 TTS 모델에 프롬프트함으로써 감정적으로 표현력 있는 음성을 생성하는 파이프라인을 구축할 거예요.

이 작업의 목표는 DB에서 참조 오디오의 감정과 스타일에 맞도록 TTS 출력을 안내하는 프롬프트를 DSPy로 생성하는 것입니다.

먼저 지정된 감정이나 스타일로 음성 오디오를 생성하는 TTS 생성기를 설정해 볼게요.

gpt-4o-mini-tts를 사용할 건데, 이는 원시 입력과 말하기(speaking)로 모델에 프롬프트하는 것을 지원하고 dspy.Audio로 처리된 .wav 파일로 오디오 응답을 생성합니다. 또한 TTS 출력을 위한 캐시도 설정해요.

import os
import base64
import hashlib
from openai import OpenAI

CACHE_DIR = ".audio_cache"
os.makedirs(CACHE_DIR, exist_ok=True)

def hash_key(raw_line: str, prompt: str) -> str:
    return hashlib.sha256(f"{raw_line}|||{prompt}".encode("utf-8")).hexdigest()

def generate_dspy_audio(raw_line: str, prompt: str) -> dspy.Audio:
    client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
    key = hash_key(raw_line, prompt)
    wav_path = os.path.join(CACHE_DIR, f"{key}.wav")
    if not os.path.exists(wav_path):
        response = client.audio.speech.create(
            model="gpt-4o-mini-tts",
            voice="coral", #NOTE - this can be configured to any of the 11 offered OpenAI TTS voices - https://platform.openai.com/docs/guides/text-to-speech#voice-options. 
            input=raw_line,
            instructions=prompt,
            response_format="wav"
        )
        with open(wav_path, "wb") as f:
            f.write(response.content)
    with open(wav_path, "rb") as f:
        encoded = base64.b64encode(f.read()).decode("utf-8")
    return dspy.Audio(data=encoded, audio_format="wav")

평가 지표 정의 (Define Evaluation Metric)

오디오 참조 비교는 일반적으로 음성 평가, 특히 감정 표현에서 주관적인 변동으로 인해 사소하지 않은 작업이에요. 이 튜토리얼의 목적을 위해 우리는 객관적 평가를 위한 임베딩 기반 유사도 지표를 사용하는데, Wav2Vec 2.0을 사용해 오디오를 임베딩으로 변환하고 참조 오디오와 생성 오디오 사이의 코사인 유사도를 계산해요. 오디오 품질을 더 정확하게 평가하려면 사람의 피드백이나 지각적 지표가 더 적합할 거예요.

import torch
import torchaudio
import soundfile as sf
import io

bundle = torchaudio.pipelines.WAV2VEC2_BASE
model = bundle.get_model().eval()

def decode_dspy_audio(dspy_audio):
    audio_bytes = base64.b64decode(dspy_audio.data)
    array, _ = sf.read(io.BytesIO(audio_bytes), dtype="float32")
    return torch.tensor(array).unsqueeze(0)

def extract_embedding(audio_tensor):
    with torch.inference_mode():
        return model(audio_tensor)[0].mean(dim=1)

def cosine_similarity(a, b):
    return torch.nn.functional.cosine_similarity(a, b).item()

def audio_similarity_metric(example, pred, trace=None):
    ref_audio = decode_dspy_audio(example.reference_audio)
    gen_audio = decode_dspy_audio(pred.audio)

    ref_embed = extract_embedding(ref_audio)
    gen_embed = extract_embedding(gen_audio)

    score = cosine_similarity(ref_embed, gen_embed)

    if trace is not None:
        return score > 0.8 
    return score

evaluate_program = dspy.Evaluate(devset=testset, metric=audio_similarity_metric, display_progress=True, num_threads = 10, display_table=True)

evaluate_program(EmotionStylePrompter())

TTS 지침:

Speak the following line with a tone of disgust: It's eleven o'clock
from IPython.display import Audio

audio_bytes = base64.b64decode(pred.audio.data)
array, rate = sf.read(io.BytesIO(audio_bytes), dtype="float32")
Audio(array, rate=rate)

DSPy로 최적화 (Optimize with DSPy)

dspy.MIPROv2를 활용해 하위 작업 목표를 정제하고 더 높은 품질의 TTS 지침을 생성할 수 있어요. 이는 더 정확하고 표현력 있는 오디오 생성을 이끌어냅니다:

prompt_lm = dspy.LM(model='gpt-4o-mini')

teleprompter = dspy.MIPROv2(metric=audio_similarity_metric, auto="light", prompt_model = prompt_lm)

optimized_program = teleprompter.compile(EmotionStylePrompter(),trainset=trainset)

evaluate_program(optimized_program)

MIPROv2 최적화 프로그램 지침:

Generate an OpenAI TTS instruction that makes the TTS model speak the given line with the target emotion or style, as if the speaker is a [insert persona relevant to the task, e.g. "irate customer", "angry boss", etc.]. The instruction should specify the tone, pitch, and other characteristics of the speaker's voice to convey the target emotion.

TTS 지침:

Generate a text-to-speech synthesis of the input text "It's eleven o'clock" with the following characteristics: 
- Tone: Disgusted
- Pitch: High-pitched, slightly nasal
- Emphasis: Emphasize the words to convey a sense of distaste and aversion
- Volume: Moderate to loud, with a sense of rising inflection at the end to convey the speaker's strong negative emotions
- Speaker: A person who is visibly and audibly disgusted, such as a character who has just been served a spoiled meal.
from IPython.display import Audio

audio_bytes = base64.b64decode(pred.audio.data)
array, rate = sf.read(io.BytesIO(audio_bytes), dtype="float32")
Audio(array, rate=rate)

더 알아보기 (Learn more)