OpenAI Realtime Console

Summary

์ด OpenAI Realtime Console ํ”„๋กœ์ ํŠธ๋Š” OpenAI Realtime API๋ฅผ ๊ฒ€์‚ฌํ•˜๊ณ  ๋””๋ฒ„๊น…ํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ๋˜ํ•œ Realtime API๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜๋Š” ์ฐธ์กฐ ํด๋ผ์ด์–ธํŠธ์™€ ์˜ค๋””์˜ค ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์›๋ฌธ: https://github.com/openai/openai-realtime-console/

OpenAI Realtime Console์€ OpenAI Realtime API๋ฅผ ์œ„ํ•œ ๊ฒ€์‚ฌ๊ธฐ ๋ฐ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ API ์ฐธ๊ณ  ์ž๋ฃŒ๋กœ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ํ”„๋กœ์ ํŠธ๋Š” ๋‘ ๊ฐœ์˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ํ•จ๊ป˜ ์ œ๊ณต๋˜๋ฉฐ, ํ•˜๋‚˜๋Š” ๋ธŒ๋ผ์šฐ์ €์™€ Node.js์šฉ ์ฐธ์กฐ ํด๋ผ์ด์–ธํŠธ ์—ญํ• ์„ ํ•˜๋Š” openai/openai-realtime-api-beta์ด๊ณ , ๋‹ค๋ฅธ ํ•˜๋‚˜๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ ๊ฐ„๋‹จํ•œ ์˜ค๋””์˜ค ๊ด€๋ฆฌ๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋Š” /src/lib/wavtools์ž…๋‹ˆ๋‹ค.

์‹œ์ž‘ํ•˜๊ธฐ

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ํด๋ก ํ•˜๊ณ  ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.

$ git clone org-14957082@github.com:openai/openai-realtime-console.git
$ cd openai-realtime-console
$ npm i

์„œ๋ฒ„ ์‹œ์ž‘ํ•˜๊ธฐ

$ npm start

๋ธŒ๋ผ์šฐ์ €๋ฅผ ์—ด๊ณ  localhost:3000์— ์ ‘์†ํ•ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉํ•˜๊ธฐ

Realtime API์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋Š” OpenAI API ํ‚ค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์‹œ์ž‘ ์‹œ ์ž…๋ ฅํ•˜๋ผ๋Š” ๋ฉ”์‹œ์ง€๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์ด ํ‚ค๋Š” localStorage๋ฅผ ํ†ตํ•ด ์ €์žฅ๋˜๋ฉฐ, UI์—์„œ ์–ธ์ œ๋“ ์ง€ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์„ธ์…˜์„ ์‹œ์ž‘ํ•˜๋ ค๋ฉด ์—ฐ๊ฒฐ(connect) ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด์„œ๋Š” ๋งˆ์ดํฌ ์•ก์„ธ์Šค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ๋‹ค์Œ manual(Push-to-talk) ๋ฐ vad(์Œ์„ฑ ํ™œ๋™ ๊ฐ์ง€) ๋Œ€ํ™” ๋ชจ๋“œ ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•˜๊ณ  ์–ธ์ œ๋“ ์ง€ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‘ ๊ฐ€์ง€ ๊ธฐ๋Šฅ์ด ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค;

ํ‘ธ์‹œ ํˆฌ ํ† ํฌ(Push-to-talk) ๋˜๋Š” VAD ๋ชจ๋“œ์—์„œ ์–ธ์ œ๋“ ์ง€ ๋ชจ๋ธ์„ ์ž์œ ๋กญ๊ฒŒ ์ค‘๋‹จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฆด๋ ˆ์ด ์„œ๋ฒ„ ์‚ฌ์šฉํ•˜๊ธฐ

๋ณด๋‹ค ๊ฐ•๋ ฅํ•œ ๊ตฌํ˜„์„ ๊ตฌ์ถ•ํ•˜๊ณ  ์ž์ฒด ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฐธ์กฐ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์‚ฌ์šฉํ•ด๋ณด๊ณ  ์‹ถ๋‹ค๋ฉด Node.js ๋ฆด๋ ˆ์ด ์„œ๋ฒ„๋ฅผ ํฌํ•จํ–ˆ์Šต๋‹ˆ๋‹ค.

$ npm run relay

localhost:8081์—์„œ ์ž๋™์œผ๋กœ ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ ๊ตฌ์„ฑ์œผ๋กœ .env ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

OPENAI_API_KEY=YOUR_API_KEY
REACT_APP_LOCAL_RELAY_SERVER_URL=http://localhost:8081

.env. ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ ์šฉํ•˜๋ ค๋ฉด React ์•ฑ๊ณผ ๋ฆด๋ ˆ์ด ์„œ๋ฒ„๋ฅผ ๋ชจ๋‘ ์žฌ์‹œ์ž‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋กœ์ปฌ ์„œ๋ฒ„ URL์€ ConsolePage.tsx๋ฅผ ํ†ตํ•ด ๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค. ์–ธ์ œ๋“ ์ง€ ๋ฆด๋ ˆ์ด ์„œ๋ฒ„ ์‚ฌ์šฉ์„ ์ค‘์ง€ํ•˜๋ ค๋ฉด ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์‚ญ์ œํ•˜๊ฑฐ๋‚˜ ๋นˆ ๋ฌธ์ž์—ด๋กœ ์„ค์ •ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

/**
 * ๋กœ์ปฌ ๋ฆด๋ ˆ์ด ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•˜๋ฉด 
 * API ํ‚ค๋ฅผ ์ˆจ๊ธฐ๊ณ  ์„œ๋ฒ„์—์„œ ์‚ฌ์šฉ์ž ์ง€์ • ๋กœ์ง์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
 *
 * ๋กœ์ปฌ ๋ฆด๋ ˆ์ด ์„œ๋ฒ„ ์ฃผ์†Œ๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค:
 * REACT_APP_LOCAL_RELAY_SERVER_URL=http://localhost:8081
 *
 * `.env` ํŒŒ์ผ์— OPENAI_API_KEY=๋ฅผ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
 * `npm run relay`๋ฅผ `npm start`๊ณผ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
 */
const LOCAL_RELAY_SERVER_URL: string =
  process.env.REACT_APP_LOCAL_RELAY_SERVER_URL || '';

์ด ์„œ๋ฒ„๋Š” ๋‹จ์ˆœ ๋ฉ”์‹œ์ง€ ์ค‘๊ณ„ ์šฉ๋„๋กœ๋งŒ ์‚ฌ์šฉ๋˜์ง€๋งŒ ํ™•์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

์ด๋Ÿฌํ•œ ๊ธฐ๋Šฅ์€ ์ง์ ‘ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์‹ค์‹œ๊ฐ„ API ๋ ˆํผ๋Ÿฐ์Šค ํด๋ผ์ด์–ธํŠธ

์ตœ์‹  ๋ ˆํผ๋Ÿฐ์Šค ํด๋ผ์ด์–ธํŠธ ๋ฐ ๋ฌธ์„œ๋Š” GitHub์—์„œ openai/openai-realtime-api-beta์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ํด๋ผ์ด์–ธํŠธ๋Š” ๋ชจ๋“  React(ํ”„๋ก ํŠธ์—”๋“œ) ๋˜๋Š” Node.js ํ”„๋กœ์ ํŠธ์—์„œ ์ง์ ‘ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ „์ฒด ๋ฌธ์„œ๋Š” GitHub ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”. ํ•˜์ง€๋งŒ ์—ฌ๊ธฐ ๊ฐ€์ด๋“œ๋ฅผ ์‹œ์ž‘ํ•˜๊ธฐ ์œ„ํ•œ ์ž…๋ฌธ์„œ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { RealtimeClient } from '/src/lib/realtime-api-beta/index.js';

const client = new RealtimeClient({ apiKey: process.env.OPENAI_API_KEY });

// ์—ฐ๊ฒฐ์— ์•ž์„œ ๋งค๊ฐœ๋ณ€์ˆ˜ ์„ค์ • ๊ฐ€๋Šฅ
client.updateSession({ instructions: 'You are a great, upbeat friend.' });
client.updateSession({ voice: 'alloy' });
client.updateSession({ turn_detection: 'server_vad' });
client.updateSession({ input_audio_transcription: { model: 'whisper-1' } });

// ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์„ค์ •
client.on('conversation.updated', ({ item, delta }) => {
  const items = client.conversation.getItems(); // ์ด๊ฒƒ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ชจ๋“  ํ•ญ๋ชฉ์„ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  /* ๋Œ€ํ™”์— ๋Œ€ํ•œ ๋ชจ๋“  ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ํฌํ•จํ•˜๋ฉฐ, ๋ธํƒ€(delta)๊ฐ€ ์ฑ„์›Œ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. */
});

// Realtime API์— ์—ฐ๊ฒฐ
await client.connect();

// ์•„์ดํ…œ ์ „์†ก ๋ฐ ์ƒ์„ฑ ํŠธ๋ฆฌ๊ฑฐ
client.sendUserMessageContent([{ type: 'text', text: `How are you?` }]);

์ŠคํŠธ๋ฆฌ๋ฐ ์˜ค๋””์˜ค ๋ณด๋‚ด๊ธฐ

์ŠคํŠธ๋ฆฌ๋ฐ ์˜ค๋””์˜ค๋ฅผ ๋ณด๋‚ด๋ ค๋ฉด .appendInputAudio() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. turn_detection: 'disabled' ๋ชจ๋“œ์ธ ๊ฒฝ์šฐ .generate()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ชจ๋ธ์— ์‘๋‹ตํ•˜๋„๋ก ์ง€์‹œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

// ์‚ฌ์šฉ์ž ์˜ค๋””์˜ค ์ „์†ก, Int16Array ๋˜๋Š” ArrayBuffer์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.
// ๊ธฐ๋ณธ ์˜ค๋””์˜ค ํฌ๋งท์€ 24,000Hz ์ƒ˜ํ”Œ ์†๋„์˜ pcm16์ž…๋‹ˆ๋‹ค.
// 0.1์ดˆ ๋‹จ์œ„๋กœ 1์ดˆ ๋ถ„๋Ÿ‰์˜ ๋…ธ์ด์ฆˆ๋ฅผ ์ฑ„์›๋‹ˆ๋‹ค.
for (let i = 0; i < 10; i++) {
  const data = new Int16Array(2400);
  for (let n = 0; n < 2400; n++) {
    const value = Math.floor((Math.random() * 2 - 1) * 0x8000);
    data[n] = value;
  }
  client.appendInputAudio(data);
}

// ๋ณด๋ฅ˜ ์ค‘์ธ ์˜ค๋””์˜ค๊ฐ€ ์ปค๋ฐ‹๋˜๊ณ  ๋ชจ๋ธ์— ๋‹ค์Œ์„ ์ƒ์„ฑํ•˜๋„๋ก ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.
client.createResponse();

๋„๊ตฌ ์ถ”๊ฐ€ ๋ฐ ์‚ฌ์šฉ

๋„๊ตฌ ์ž‘์—…์€ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. .addTool()์„ ํ˜ธ์ถœํ•˜๊ณ  ์ฝœ๋ฐฑ์„ ๋‘ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์„ค์ •ํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ฝœ๋ฐฑ์€ ๋„๊ตฌ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜์™€ ํ•จ๊ป˜ ์‹คํ–‰๋˜๊ณ  ๊ฒฐ๊ณผ๋Š” ์ž๋™์œผ๋กœ ๋ชจ๋ธ๋กœ ๋‹ค์‹œ ์ „์†ก๋ฉ๋‹ˆ๋‹ค.

// ์ฝœ๋ฐฑ์„ ์ง€์ •ํ•˜์—ฌ ๋„๊ตฌ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
client.addTool(
  {
    name: 'get_weather',
    description:
      'Retrieves the weather for a given lat, lng coordinate pair. Specify a label for the location.',
    parameters: {
      type: 'object',
      properties: {
        lat: {
          type: 'number',
          description: 'Latitude',
        },
        lng: {
          type: 'number',
          description: 'Longitude',
        },
        location: {
          type: 'string',
          description: 'Name of the location',
        },
      },
      required: ['lat', 'lng', 'location'],
    },
  },
  async ({ lat, lng, location }) => {
    const result = await fetch(
      `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}&current=temperature_2m,wind_speed_10m`
    );
    const json = await result.json();
    return json;
  }
);

๋ชจ๋ธ ์ค‘๋‹จํ•˜๊ธฐ

ํŠนํžˆ turn_detection: 'disabled' ๋ชจ๋“œ์—์„œ ๋ชจ๋ธ์„ ์ˆ˜๋™์œผ๋กœ ์ค‘๋‹จํ•˜๊ณ  ์‹ถ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด ๋‹ค์Œ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

// id๋Š” ํ˜„์žฌ ์ƒ์„ฑ ์ค‘์ธ ํ•ญ๋ชฉ์˜ ID์ž…๋‹ˆ๋‹ค. 
// sampleCount๋Š” ์ฒญ์ทจ์ž๊ฐ€ ๋“ค์€ ์˜ค๋””์˜ค ์ƒ˜ํ”Œ์˜ ์ˆ˜์ž…๋‹ˆ๋‹ค.
client.cancelResponse(id, sampleCount);

์ด ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ชจ๋ธ์ด ์ฆ‰์‹œ ์ƒ์„ฑ์„ ์ค‘๋‹จํ•  ๋ฟ๋งŒ ์•„๋‹ˆ๋ผsampleCount ์ดํ›„์˜ ๋ชจ๋“  ์˜ค๋””์˜ค๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ํ…์ŠคํŠธ ์‘๋‹ต์„ ์ง€์›€์œผ๋กœ์จ ์žฌ์ƒ ์ค‘์ธ ํ•ญ๋ชฉ๋„ ์ž˜๋ฆฝ๋‹ˆ๋‹ค. ์ด ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ชจ๋ธ์„ ์ค‘๋‹จํ•˜๊ณ  ์‚ฌ์šฉ์ž ์ƒํƒœ๋ณด๋‹ค ์•ž์„œ ์ƒ์„ฑ๋œ ๋ชจ๋“  ๊ฒƒ์„ '๊ธฐ์–ต'ํ•˜์ง€ ๋ชปํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฐธ์กฐ ํด๋ผ์ด์–ธํŠธ ์ด๋ฒคํŠธ

RealtimeClient์—๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ œ์–ด ํ๋ฆ„์„ ์œ„ํ•œ ๋‹ค์„ฏ ๊ฐ€์ง€ ์ฃผ์š” ํด๋ผ์ด์–ธํŠธ ์ด๋ฒคํŠธ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ์— ๋Œ€ํ•œ ๊ฐœ์š”์ผ ๋ฟ์ด๋ฉฐ, ์ „์ฒด ์‹ค์‹œ๊ฐ„ API ์ด๋ฒคํŠธ ์‚ฌ์–‘์€ ์ƒ๋‹นํžˆ ๋ฐฉ๋Œ€ํ•˜๋ฏ€๋กœ ๋” ๋งŽ์€ ์ œ์–ด๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ GitHub ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ํ™•์ธํ•˜์„ธ์š”: openai/openai-realtime-api-beta.

// ์—ฐ๊ฒฐ ์‹คํŒจ์™€ ๊ฐ™์€ ์˜ค๋ฅ˜
client.on('error', (event) => {
  /* do something */
});

// VAD ๋ชจ๋“œ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ๋งํ•˜๊ธฐ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.
// ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด์ „ ์‘๋‹ต์˜ ์˜ค๋””์˜ค ์žฌ์ƒ์„ ์ค‘์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
client.on('conversation.interrupted', () => {
  /* do something */
});

// ๋Œ€ํ™”์— ๋Œ€ํ•œ ๋ชจ๋“  ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
// ๋ธํƒ€(delta)๊ฐ€ ์ฑ„์›Œ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
client.on('conversation.updated', ({ item, delta }) => {
  // get all items, e.g. if you need to update a chat window
  const items = client.conversation.getItems();
  switch (item.type) {
    case 'message':
      // system, user, or assistant message (item.role)
      break;
    case 'function_call':
      // always a function call from the model
      break;
    case 'function_call_output':
      // always a response from the user / application
      break;
  }
  if (delta) {
    // ํŠน์ • ์ด๋ฒคํŠธ์— ๋Œ€ํ•ด ๋‹ค์Œ ์ค‘ ํ•˜๋‚˜๋งŒ ์ฑ„์›Œ์ง‘๋‹ˆ๋‹ค.
    // delta.audio = Int16Array, audio added
    // delta.transcript = string, transcript added
    // delta.arguments = string, function arguments added
  }
});

// ๋Œ€ํ™”์— ํ•ญ๋ชฉ์ด ์ถ”๊ฐ€๋œ ํ›„์—๋งŒ ํŠธ๋ฆฌ๊ฑฐ๋ฉ๋‹ˆ๋‹ค.
client.on('conversation.item.appended', ({ item }) => {
  /* item ์ƒํƒœ๋Š” 'in_progress' ๋˜๋Š” 'completed' ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. */
});

// ๋Œ€ํ™”์—์„œ ํ•ญ๋ชฉ์ด ์™„๋ฃŒ๋œ ํ›„์—๋งŒ ํŠธ๋ฆฌ๊ฑฐ๋จ
// ํ•ญ์ƒ conversation.item.appended ์ดํ›„์— ํŠธ๋ฆฌ๊ฑฐ๋ฉ๋‹ˆ๋‹ค.
client.on('conversation.item.completed', ({ item }) => {
  /* item ์ƒํƒœ๋Š” ํ•ญ์ƒ 'completed'์ž…๋‹ˆ๋‹ค. */
});

Wavtools

Wavtools์—๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ PCM16 ์˜ค๋””์˜ค ์ŠคํŠธ๋ฆผ์„ ์‰ฝ๊ฒŒ ๊ด€๋ฆฌํ•˜๊ณ  ๋…น์Œ ๋ฐ ์žฌ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ด ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

WavRecorder ํ€ต์Šคํƒ€ํŠธ

import { WavRecorder } from '/src/lib/wavtools/index.js';

const wavRecorder = new WavRecorder({ sampleRate: 24000 });
wavRecorder.getStatus(); // "ended"

// ๊ถŒํ•œ ์š”์ฒญ, ๋งˆ์ดํฌ ์—ฐ๊ฒฐ
await wavRecorder.begin();
wavRecorder.getStatus(); // "paused"

// ๋…น์Œ ์‹œ์ž‘
// ์ด ์ฝœ๋ฐฑ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ 8192๊ฐœ์˜ ์ƒ˜ํ”Œ ์ฒญํฌ๋กœ ํŠธ๋ฆฌ๊ฑฐ๋ฉ๋‹ˆ๋‹ค.
// { mono, raw }๋Š” Int16Array(PCM16) ๋ชจ๋…ธ ๋ฐ ์ „์ฒด ์ฑ„๋„ ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค.
await wavRecorder.record((data) => {
  const { mono, raw } = data;
});
wavRecorder.getStatus(); // "recording"

// ๋…น์Œ ์ค‘์ง€
await wavRecorder.pause();
wavRecorder.getStatus(); // "paused"

// "audio/wav" ์˜ค๋””์˜ค ํŒŒ์ผ ์ถœ๋ ฅ
const audio = await wavRecorder.save();

// ํ˜„์žฌ ์˜ค๋””์˜ค ๋ฒ„ํผ๋ฅผ ์ง€์šฐ๊ณ  ๋…น์Œ์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.
await wavRecorder.clear();
await wavRecorder.record();

// ์‹œ๊ฐํ™”๋ฅผ ์œ„ํ•œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
const frequencyData = wavRecorder.getFrequencies();

// ๋…น์Œ ์ค‘์ง€, ๋งˆ์ดํฌ, ์ถœ๋ ฅ ํŒŒ์ผ ์—ฐ๊ฒฐ ํ•ด์ œ
await wavRecorder.pause();
const finalAudio = await wavRecorder.end();

// ์žฅ์น˜ ๋ณ€๊ฒฝ์„ ์ˆ˜์‹ ํ•ฉ๋‹ˆ๋‹ค(์˜ˆ: ๋ˆ„๊ตฐ๊ฐ€ ๋งˆ์ดํฌ๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ) 
// deviceList๋Š” MediaDeviceInfo[] + `default` ์†์„ฑ์˜ ๋ฐฐ์—ด์ž…๋‹ˆ๋‹ค.
wavRecorder.listenForDeviceChange((deviceList) => {});

WavStreamPlayer ํ€ต์Šคํƒ€ํŠธ

import { WavStreamPlayer } from '/src/lib/wavtools/index.js';

const wavStreamPlayer = new WavStreamPlayer({ sampleRate: 24000 });

// ์˜ค๋””์˜ค ์ถœ๋ ฅ์— ์—ฐ๊ฒฐ
await wavStreamPlayer.connect();

// 1์ดˆ ๋ถ„๋Ÿ‰์˜ ๋นˆ PCM16 ์˜ค๋””์˜ค ์ƒ์„ฑ
const audio = new Int16Array(24000);

// 3์ดˆ ๋ถ„๋Ÿ‰์˜ ์˜ค๋””์˜ค๋ฅผ ๋Œ€๊ธฐ์—ด์— ์ถ”๊ฐ€ํ•˜๋ฉด ์ฆ‰์‹œ ์žฌ์ƒ์ด ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค.
wavStreamPlayer.add16BitPCM(audio, 'my-track');
wavStreamPlayer.add16BitPCM(audio, 'my-track');
wavStreamPlayer.add16BitPCM(audio, 'my-track');

// ์‹œ๊ฐํ™”๋ฅผ ์œ„ํ•œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
const frequencyData = wavStreamPlayer.getFrequencies();

// ์–ธ์ œ๋“ ์ง€ ์˜ค๋””์˜ค๋ฅผ ์ค‘๋‹จ(์žฌ์ƒ ์ค‘์ง€)ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
// ๋‹ค์‹œ ์‹œ์ž‘ํ•˜๋ ค๋ฉด .add16BitPCM()์„ ๋‹ค์‹œ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
const trackOffset = await wavStreamPlayer.interrupt();
trackOffset.trackId; // "my-track"
trackOffset.offset; // sample number
trackOffset.currentTime; // time in track