n8n, Gemini TTS 를 활용한 5000자 이상 대본 -> 음성 파일 자동화 가능

소개

8월 24일에 있을 JS healing art 집담회 20분 발표를 준비하면서, 약 5000자 분량의 발표 대본을 음성 파일로 변환해보면 어떨까 하는 아이디어가 떠올랐습니다. 단순히 텍스트를 읽는 것이 아니라, 실제 발표처럼 음성으로 들어보면서 발표 준비를하면 좋겠다고 생각했죠. 이전에 이렇게 발표 대본을 먼저 만들고 그에 맞게 PPT를 만들면 수정 횟수도 줄일 수 있었던 경험이 있어 긴 대본에 대한 음성 파일을 만들고 싶었습니다.
5000자까지는 ElevenLabs나 Google AI studio에서 한 번에 생성 할 수 있기 때문에 굳이 자동화를 할 필요는 없습니다.

긴 대본을 한번에 음성화하기 위해서는 자동화가 필요합니다.
Gemini 2.5 Pro와 n8n, Google AI studio TTS를 활용하여 워크플로우를 구축했습니다.

진행 방법

🔧 사용 도구

  • Gemini 2.5 Pro: 대본 생성 및 정제, n8n 코드 및 워크플로우 짜기

  • Google AI Studio: Gemini TTS API 제공

  • n8n: 전체 자동화 워크플로우 구성

🔄 자동화 흐름

신호의 다른 단계를 보여주는 다이어그램
  1. n8n에서 텍스트 입력을 받을 수 있도록 n8n form node 설정

  2. Dialogue generator node (Code node) : 대본을 5000 바이트 이하로 잘라주는 역할

const inputText = $json.text || $json.script_text || '';
const CHUNK_SIZE = 5000; // 바이트 단위
const buffer = Buffer.from(inputText, 'utf8');
const chunks = [];
let offset = 0;
let idx = 0;

while (offset < buffer.length) {
  let end = offset + CHUNK_SIZE;
  if (end < buffer.length) {
    while (end > offset && (buffer[end] & 0xC0) === 0x80) end--;
  }
  let chunk = buffer.slice(offset, end).toString('utf8');
  chunks.push({ text: chunk, index: idx++ });
  offset = end;
}

return chunks.map(chunk => ({ json: chunk }));
  1. TTS body maker (Code node) : 어떤 모델을 사용해서 음성파일을 만들지를 결정

for (const item of items) {
  const originalText = item.json.text;

  // Gemini 멀티모달 모델 공식 문서에 맞는 Body 구조
  item.json.body = {
    "contents": [{
      "parts": [{
        "text": originalText
      }]
    }],
    "generationConfig": {
      "responseModalities": ["AUDIO"],
      "speechConfig": {
        "voiceConfig": {
          "prebuiltVoiceConfig": {
            "voiceName": "Sadaltager" // 원하는 목소리로 변경 가능
          }
        }
      }
    },
    // 모델 이름을 body에 포함해야 할 수 있습니다.
    "model": "gemini-2.5-flash-preview-tts"
  };
}

return items;
  1. TTS : HTTP node를 활용하여 TTS 음성 파일 생성

    텍스트 편집기를 보여주는 웹 페이지의 스크린 샷
  2. TTS file :

const results = []; // 결과를 담을 빈 배열

// items 배열의 모든 항목을 순회(loop)합니다.
for (const item of items) {
  // 각 item에서 오디오 데이터 추출
  const base64Data = item.json.candidates[0].content.parts[0].inlineData.data;

  // 추출한 데이터를 n8n의 binary 형식에 맞게 새로운 아이템으로 가공
  const newItem = {
    json: {},
    binary: {
      data: {
        data: base64Data,
        mimeType: 'audio/pcm', // 임시 MIME 타입
        fileName: `chunk_${results.length}.pcm`
      }
    }
  };

  // 가공된 새 아이템을 결과 배열에 추가
  results.push(newItem);
}

// 모든 처리 결과를 담은 배열을 반환
return results;
  1. Merge node : 분리된 파일을 하나로 합치기 위해 모으는 과정

다른 유형의 명함을 보여주는 웹 페이지의 스크린 샷

Append 모드는 '뒤에 덧붙인다'는 뜻으로, 노드로 들어온 모든 데이터(3개의 오디오 조각)를 순서대로 차곡차곡 쌓아서 하나의 묶음으로 만들어 다음 노드에 전달하는, 지금 우리에게 꼭 필요한 기능입니다.
7. wav.assemble : 모아진 파일을 하나의 wav 파일로 만들기

// Merge 노드에서 전달받은 모든 아이템(PCM 오디오 조각들)
const pcmBuffers = items.map(item => Buffer.from(item.binary.data.data, 'base64'));

// 모든 PCM 버퍼를 하나로 합칩니다.
const totalPcmData = Buffer.concat(pcmBuffers);
const dataLen = totalPcmData.length;

// WAV 파일 헤더 생성 (24000Hz, 16-bit, Mono 기준)
const sampleRate = 24000, channels = 1, bitDepth = 16;
const blockAlign = channels * (bitDepth / 8);
const byteRate = sampleRate * blockAlign;
const riffSize = 36 + dataLen;

const header = Buffer.alloc(44);
header.write('RIFF', 0);
header.writeUInt32LE(riffSize, 4);
header.write('WAVE', 8);
header.write('fmt ', 12);
header.writeUInt32LE(16, 16);      // Sub-chunk size
header.writeUInt16LE(1, 20);       // PCM format
header.writeUInt16LE(channels, 22);
header.writeUInt32LE(sampleRate, 24);
header.writeUInt32LE(byteRate, 28);
header.writeUInt16LE(blockAlign, 32);
header.writeUInt16LE(bitDepth, 34);
header.write('data', 36);
header.writeUInt32LE(dataLen, 40);

// 헤더와 합쳐진 PCM 데이터를 붙여 완전한 WAV 버퍼를 생성
const finalWavBuffer = Buffer.concat([header, totalPcmData]);

// 최종 WAV 파일을 다음 노드로 전달
return [{
  json: {},
  binary: {
    data: {
      data: finalWavBuffer.toString('base64'),
      mimeType: 'audio/wav',
      fileName: 'final_audio.wav'
    }
  }
}];
다양한 유형의 정보를 보여주는 웹 페이지의 스크린 샷
  1. 구글 이메일 : 첨부파일의 형태로 보내기

    여기는 드라이브나 다른 여러가지 형태로 가능합니다. 단 텔레그램은 50MB이상은 보낼 수 없다고 해서 저는 이메일을 선택했습니다.

결과와 배운 점

  • 5000자 (15000바이트) 이상의 분량은 생각보다 길었습니다.

  • 그래서 저는 5000바이트로 잘랐는데 10000바이트 정도로 잘라서 처리해도 될 것 같습니다.

  • TTS 결과물은 잘라서 만들었기 때문에 연결부분에서 어색함은 당연하고 같은 모델을 써도 연결 이후 음성은 처음과 달라지는 것은 어쩔 수 없는 것 같습니다. 일관성 유지는 어렵다는 결론.

  • Elevenlabs API를 쓰면 어떻게 될지는 해봐야 알것 같습니다.

  • 예상보다 쉽게 구성되었고, 특히 n8n과 google AI studio API 연결은 어렵지 않아 쉽게 해결되었습니다.

  • "직접 시도해보지 않으면 알 수 없는 것들이 있으니 여러가지를 많이 시도해 보면 좋을 것 같습니다.

도움 받은 글 (선택)

2

👉 이 게시글도 읽어보세요