메인 콘텐츠로 건너뛰기
VoiceSDK는 자동 채팅 시스템 통합과 음성 활성화 애플리케이션 구축을 위한 React hooks를 포함한 고급 음성 대화 기능을 제공합니다. AI 에이전트와의 자연스러운 음성 상호작용을 만들고, 오디오 시각화, 전사 및 대화 관리를 완료하세요. 이 기사에서는 빠르게 시작하고 작동 방식을 이해하는 데 도움이 되는 빠른 시작 예시를 제공합니다. 나머지 기사에서는 사용할 수 있는 다양한 메서드, 용도 및 모범 사례를 자세히 설명합니다.

설치

npm install @odin-ai-staging/sdk @elevenlabs/react

빠른 시작

기본 음성 대화

이 예시에서는 TypeScript와 React 애플리케이션 모두에서 VoiceSDK를 사용하여 실시간 음성 대화를 구현하는 방법을 알아봅니다. AI 음성 에이전트가 대화를 처리할 특정 agentId를 포함한 API 자격 증명으로 VoiceSDK를 초기화하는 것으로 시작합니다.
import { VoiceSDK } from '@odin-ai-staging/sdk';

// SDK 초기화
const voiceSDK = new VoiceSDK({
  baseUrl: 'https://your-api-endpoint.com/',
  projectId: 'your-project-id',
  apiKey: 'your-api-key',
  apiSecret: 'your-api-secret',
  agentId: 'your-agent-id'
});

// 음성 대화 시작
async function startVoiceChat() {
  const sessionId = await voiceSDK.startVoiceConversation({
    saveToChat: true,
    callbacks: {
      onConnect: () => console.log('음성 연결됨'),
      onMessage: (message) => console.log('음성 메시지:', message),
      onDisconnect: () => console.log('음성 연결 해제됨'),
      onTranscription: (text, isFinal) => {
        if (isFinal) console.log('사용자 말:', text);
      }
    }
  });
  
  console.log('음성 세션 시작됨:', sessionId);
}

React Hook 사용

import { useVoiceConversation } from '@odin-ai-staging/sdk';

function VoiceChat() {
  const {
    status,
    startSession,
    endSession,
    setVolume,
    getInputByteFrequencyData,
    conversationState
  } = useVoiceConversation({
    sdkConfig: {
      baseUrl: 'https://your-api-endpoint.com/',
      projectId: 'your-project-id',
      agentId: 'your-agent-id'
    },
    callbacks: {
      onConnect: () => console.log('연결됨!'),
      onMessage: (message) => console.log('메시지:', message)
    }
  });

  return (
    <div>
      <button 
        onClick={() => startSession()}
        disabled={status === 'connected'}
      >
        음성 채팅 시작
      </button>
      
      <button 
        onClick={() => endSession()}
        disabled={status !== 'connected'}
      >
        채팅 종료
      </button>
      
      <div>상태: {status}</div>
      <div>볼륨: {conversationState.volume}</div>
    </div>
  );
}

구성

VoiceSDKConfig 인터페이스

interface VoiceSDKConfig extends BaseClientConfig {
  agentId?: string;            // 대화의 기본 에이전트 ID
  defaultVoiceSettings?: VoiceSettings;  // 기본 음성 설정
}

VoiceSettings

interface VoiceSettings {
  stability?: number;          // 음성 안정성 (0.0 ~ 1.0)
  similarityBoost?: number;    // 음성 유사도 부스트 (0.0 ~ 1.0)
  style?: number;             // 음성 스타일 (0.0 ~ 1.0)
  useSpeakerBoost?: boolean;  // 스피커 부스트 활성화
}
설정 예시:
const voiceSDK = new VoiceSDK({
  baseUrl: 'https://api.example.com/',
  projectId: 'proj_123',
  apiKey: 'your-api-key',
  apiSecret: 'your-api-secret',
  agentId: 'agent_456',
  defaultVoiceSettings: {
    stability: 0.8,
    similarityBoost: 0.7,
    style: 0.3,
    useSpeakerBoost: true
  }
});

핵심 기능

음성 대화 세션

VoiceSDK는 자동 채팅 통합과 함께 음성 대화 세션을 관리합니다:
interface VoiceConversationSession {
  id: string;                    // 세션 식별자
  chatId?: string;               // 연결된 채팅 ID
  startTime: number;             // 세션 시작 타임스탬프
  endTime?: number;              // 세션 종료 타임스탬프
  messages: VoiceMessage[];      // 세션의 음성 메시지
  metadata?: {
    agentId?: string;
    voiceSettings?: VoiceSettings;
    totalDuration?: number;
    userInfo?: { name: string; id: string };
  };
}

음성 메시지

interface VoiceMessage {
  id: string;                    // 메시지 ID
  type: 'user_speech' | 'ai_speech' | 'system';
  text: string;                  // 전사/생성된 텍스트
  audioUrl?: string;             // 오디오 파일 URL
  timestamp: number;             // 메시지 타임스탬프
  duration?: number;             // 오디오 지속 시간(초)
  voiceSettings?: VoiceSettings; // 사용된 음성 설정
  saved?: boolean;               // 데이터베이스에 저장되었는지 여부
}

세션 관리

startVoiceConversation(options?)

새 음성 대화 세션을 시작합니다.
async startVoiceConversation(
  options?: StartVoiceConversationOptions
): Promise<string>
StartVoiceConversationOptions:
interface StartVoiceConversationOptions {
  callbacks?: VoiceConversationCallbacks;
  saveToChat?: boolean;          // 대화 기록에 자동 저장
  existingChatId?: string;       // 기존 채팅 계속
  agentId?: string;             // 기본 에이전트 재정의
  voiceSettings?: VoiceSettings; // 사용자 지정 음성 설정
  userInfo?: { name: string; id: string };
}
예시:
const sessionId = await voiceSDK.startVoiceConversation({
  saveToChat: true,
  existingChatId: 'chat_123',
  voiceSettings: {
    stability: 0.9,
    similarityBoost: 0.8
  },
  userInfo: {
    name: '홍길동',
    id: 'user_456'
  },
  callbacks: {
    onConnect: () => console.log('음성 대화 시작됨'),
    onMessage: (message) => handleVoiceMessage(message),
    onTranscription: (text, isFinal) => {
      if (isFinal) displayTranscription(text);
    },
    onConversationSaved: (chatId, messageId) => {
      console.log(`대화가 채팅 ${chatId}에 저장됨`);
    }
  }
});

endVoiceSession(sessionId, reason?)

음성 대화 세션을 종료합니다.

getVoiceState(sessionId)

현재 음성 대화 상태를 가져옵니다.
const state = voiceSDK.getVoiceState(sessionId);
if (state) {
  console.log('연결 상태:', state.connectionStatus);
  console.log('말하고 있음:', state.isSpeaking);
  console.log('볼륨:', state.volume);
}

React 통합

useVoiceConversation Hook

useVoiceConversation hook은 상태 관리가 포함된 React 통합을 제공합니다:
import React, { useState } from 'react';
import { useVoiceConversation } from '@odin-ai-staging/sdk';

function VoiceConversationComponent() {
  const [messages, setMessages] = useState<string[]>([]);
  const [isRecording, setIsRecording] = useState(false);

  const {
    status,
    isSpeaking,
    startSession,
    endSession,
    setVolume,
    conversationState,
    currentSessionId,
    getInputByteFrequencyData
  } = useVoiceConversation({
    sdkConfig: {
      baseUrl: process.env.REACT_APP_API_BASE_URL,
      projectId: process.env.REACT_APP_PROJECT_ID,
      agentId: process.env.REACT_APP_AGENT_ID
    },
    callbacks: {
      onConnect: () => {
        console.log('음성 채팅에 연결됨');
        setIsRecording(true);
      },
      onDisconnect: () => {
        console.log('음성 채팅 연결 해제됨');
        setIsRecording(false);
      },
      onTranscription: (text, isFinal) => {
        if (isFinal) {
          setMessages(prev => [...prev, `사용자: ${text}`]);
        }
      },
      onMessage: (message) => {
        if (message.type === 'ai_speech') {
          setMessages(prev => [...prev, `AI: ${message.text}`]);
        }
      },
      onError: (error) => {
        console.error('음성 오류:', error);
        setIsRecording(false);
      }
    }
  });

  const handleStartConversation = async () => {
    try {
      await startSession({
        saveToChat: true,
        voiceSettings: {
          stability: 0.8,
          similarityBoost: 0.7
        }
      });
    } catch (error) {
      console.error('대화 시작 실패:', error);
    }
  };

  return (
    <div className="voice-conversation">
      <div className="controls">
        <button 
          onClick={handleStartConversation}
          disabled={status === 'connected'}
        >
          음성 채팅 시작
        </button>
        
        <button 
          onClick={() => endSession()}
          disabled={status !== 'connected'}
        >
          음성 채팅 종료
        </button>
      </div>

      <div className="status">
        <div>상태: {status}</div>
        <div>말하기: {isSpeaking ? '예' : '아니오'}</div>
        <div>녹음: {isRecording ? '예' : '아니오'}</div>
        <div>볼륨: {conversationState.volume}</div>
      </div>

      <div className="volume-control">
        <label>볼륨:</label>
        <input
          type="range"
          min="0"
          max="100"
          value={conversationState.volume}
          onChange={(e) => setVolume({ volume: parseInt(e.target.value) })}
        />
      </div>

      <div className="messages">
        {messages.map((message, index) => (
          <div key={index} className="message">
            {message}
          </div>
        ))}
      </div>

      {currentSessionId && (
        <AudioVisualizer 
          getInputData={getInputByteFrequencyData}
          isActive={status === 'connected'}
        />
      )}
    </div>
  );
}

음성 제어

볼륨 제어

// 볼륨 설정 (0-100)
await voiceSDK.setVolume(sessionId, 75);

마이크 제어

// 마이크 음소거/음소거 해제
await voiceSDK.setMicrophoneMuted(sessionId, true);  // 음소거
await voiceSDK.setMicrophoneMuted(sessionId, false); // 음소거 해제

음성 설정 업데이트

// 대화 중 음성 설정 업데이트
await voiceSDK.updateVoiceSettings(sessionId, {
  stability: 0.9,
  similarityBoost: 0.8,
  style: 0.4
});

오디오 시각화

실시간 오디오 데이터

// 시각화를 위한 오디오 주파수 데이터 가져오기
const audioData = voiceSDK.getAudioFrequencyData(sessionId);

if (audioData) {
  const inputData = audioData.input;   // 사용자 오디오 입력
  const outputData = audioData.output; // AI 오디오 출력
  
  // 오디오 시각화에 사용
  renderAudioVisualization(inputData, outputData);
}

오디오 시각화 컴포넌트

import React, { useRef, useEffect } from 'react';

interface AudioVisualizerProps {
  getInputData: () => Uint8Array | null;
  isActive: boolean;
}

function AudioVisualizer({ getInputData, isActive }: AudioVisualizerProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    if (!isActive) return;

    const canvas = canvasRef.current;
    if (!canvas) return;

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const animate = () => {
      const data = getInputData();
      
      if (data) {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        
        const barWidth = canvas.width / data.length;
        
        for (let i = 0; i < data.length; i++) {
          const barHeight = (data[i] / 255) * canvas.height;
          
          ctx.fillStyle = `hsl(${i * 2}, 100%, 50%)`;
          ctx.fillRect(
            i * barWidth,
            canvas.height - barHeight,
            barWidth - 1,
            barHeight
          );
        }
      }
      
      requestAnimationFrame(animate);
    };

    animate();
  }, [isActive, getInputData]);

  return (
    <canvas 
      ref={canvasRef}
      width={400}
      height={100}
      className="audio-visualizer"
    />
  );
}

채팅 통합

자동 채팅 저장

음성 대화는 채팅 시스템에 자동으로 저장될 수 있습니다:
const sessionId = await voiceSDK.startVoiceConversation({
  saveToChat: true,  // 자동 저장 활성화
  existingChatId: 'chat_123',  // 선택 사항: 기존 채팅 계속
  callbacks: {
    onConversationSaved: (chatId, messageId) => {
      console.log(`음성 대화가 채팅 ${chatId}에 저장됨`);
      refreshChatHistory(chatId);
    }
  }
});

오류 처리

try {
  const sessionId = await voiceSDK.startVoiceConversation({
    callbacks: {
      onError: (error) => {
        console.error('음성 대화 오류:', error);
        
        if (error.message.includes('microphone')) {
          showMicrophonePermissionDialog();
        } else if (error.message.includes('network')) {
          showNetworkErrorMessage();
        }
      },
      onDisconnect: (details) => {
        console.log('연결 해제됨:', details?.reason);
        
        if (details?.reason === 'user_ended') {
          showConversationSummary();
        } else if (details?.reason === 'error') {
          showReconnectOption();
        }
      }
    }
  });
} catch (error) {
  console.error('음성 대화 시작 실패:', error);
}

예시

음성 활성화 고객 지원

import { VoiceSDK, ChatSDK } from '@odin-ai-staging/sdk';

class VoiceCustomerSupport {
  private voiceSDK: VoiceSDK;
  private chatSDK: ChatSDK;
  private activeSession?: string;

  constructor() {
    const config = {
      baseUrl: process.env.API_BASE_URL,
      projectId: process.env.PROJECT_ID,
      apiKey: process.env.API_KEY,
      apiSecret: process.env.API_SECRET,
    };

    this.voiceSDK = new VoiceSDK(config);
    this.chatSDK = new ChatSDK(config);
  }

  async startSupportSession(customerId: string, issueType: string) {
    try {
      const chat = await this.chatSDK.createChat(
        `음성 지원 - ${issueType}`,
        []
      );

      this.activeSession = await this.voiceSDK.startVoiceConversation({
        saveToChat: true,
        existingChatId: chat.chat_id,
        agentId: this.getAgentForIssueType(issueType),
        userInfo: {
          name: `고객 ${customerId}`,
          id: customerId
        },
        callbacks: {
          onConnect: () => {
            console.log('지원 세션 시작됨');
          },
          onTranscription: (text, isFinal) => {
            if (isFinal) {
              console.log('고객 말:', text.substring(0, 100));
            }
          },
          onMessage: (message) => {
            if (message.type === 'ai_speech') {
              console.log('에이전트 응답:', message.text.length);
            }
          },
          onConversationSaved: (chatId, messageId) => {
            console.log(`지원 대화가 채팅 ${chatId}에 저장됨`);
          }
        }
      });

      return {
        sessionId: this.activeSession,
        chatId: chat.chat_id
      };
    } catch (error) {
      console.error('지원 세션 시작 실패:', error);
      throw error;
    }
  }

  async endSupportSession() {
    if (this.activeSession) {
      await this.voiceSDK.endVoiceSession(this.activeSession);
      this.activeSession = undefined;
    }
  }

  private getAgentForIssueType(issueType: string): string {
    const agentMap: Record<string, string> = {
      'technical': 'agent_technical_support',
      'billing': 'agent_billing_support',
      'general': 'agent_general_support'
    };
    return agentMap[issueType] || agentMap['general'];
  }
}

모범 사례

오류 처리 및 대체

const voiceSupport = {
  async startWithFallback() {
    try {
      return await this.voiceSDK.startVoiceConversation(options);
    } catch (error) {
      console.warn('음성 실패, 텍스트 채팅으로 대체:', error);
      return await this.chatSDK.createChat('지원 채팅 (텍스트)');
    }
  }
};

리소스 관리

class VoiceManager {
  private activeSessions = new Set<string>();

  async startSession(options: any) {
    const sessionId = await this.voiceSDK.startVoiceConversation(options);
    this.activeSessions.add(sessionId);
    return sessionId;
  }

  async cleanup() {
    for (const sessionId of this.activeSessions) {
      try {
        await this.voiceSDK.endVoiceSession(sessionId);
      } catch (error) {
        console.warn('세션 종료 실패:', sessionId, error);
      }
    }
    this.activeSessions.clear();
  }
}

접근성

function VoiceAccessibleChat() {
  const [transcript, setTranscript] = useState('');
  
  const { startSession } = useVoiceConversation({
    callbacks: {
      onTranscription: (text, isFinal) => {
        setTranscript(text);
        if (isFinal) {
          announceToScreenReader(`말씀하신 내용: ${text}`);
        }
      }
    }
  });

  return (
    <div>
      <button 
        aria-label="음성 대화 시작"
        onClick={startSession}
      >
        🎤 음성 채팅 시작
      </button>
      
      <div 
        aria-live="polite"
        aria-label="음성 전사"
      >
        {transcript}
      </div>
    </div>
  );
}