---
# System prepended metadata

title: AI影片工廠：用 n8n 串 Notion、Gemini TTS 自動產生配音
tags: [n8n, Gemini, Notion, TTS]

---

# AI影片工廠：用 n8n 串 Notion、Gemini TTS 自動產生配音

## 前言

最近在整理短影音自動化流程時，我把「文稿轉語音」這段獨立拉出來做，目標很單純：

1. 從 **Notion Database** 讀出要配音的文稿  
2. 呼叫 **Gemini TTS API** 產生語音  
3. 把 API 回傳的 **base64 PCM 音訊** 轉成檔案  
4. 再補成可直接播放的 **WAV**  
5. 最後自動上傳到 **Google Drive**

這篇記錄的是我目前的 n8n 流程與實作方式，特別是 **Gemini TTS 回來的是 PCM，不是可直接播放的 WAV** 這件事，這裡比較容易踩坑。

---

## 這個流程解決什麼問題？

如果你的短影音或配音流程是這種型態：

- 文案先寫在 Notion
- 想要批次產生旁白
- 音檔希望自動存進 Google Drive
- 後面再接剪輯、字幕、影片合成

那這一段流程就很適合拆出來獨立做。

---

## 流程總覽

```mermaid
flowchart LR
    A[Manual Trigger] --> B[Notion 讀取 active 文稿]
    B --> C[Split In Batches]
    C --> D[Data Table 取 Gemini API Key]
    D --> E[HTTP Request 呼叫 Gemini TTS]
    E --> F[Set 節點取出 audio_base64]
    F --> G[Convert to File 轉為 pcm_raw]
    G --> H[Code 節點補 WAV Header]
    H --> I[Google Drive Upload]
```
▼n8n flow
![image](https://hackmd.io/_uploads/BJ4k9Z3YWe.png)
---

## 使用到的服務

- **n8n**
- **Notion Database**
- **Gemini TTS API**
- **n8n Data Table**
- **Google Drive**

---

## Notion 資料結構

這個流程至少會用到以下欄位：

- `scene_script`：要送進 TTS 的配音文稿
- `Status`：目前用來篩選 `active`

也就是說，只有狀態為 `active` 的資料會進入配音流程。

---

## 流程節點說明




## 1. Manual Trigger

手動執行流程測試用。

---

## 2. Notion - Get many database pages

從 Notion Database 抓出狀態為 `active` 的資料。

目前設定重點：

- Resource: `databasePage`
- Operation: `getAll`
- Filter:
  - `Status|select = active`

這樣做的好處是可以把配音清單放在 Notion 管理，不需要每次都改 n8n 流程。

![image](https://hackmd.io/_uploads/rknvWfnKZe.png)

---

## 3. Split In Batches

我目前設 `batchSize: 6`。

原因是批次跑的時候，不想一次把全部文稿都灌進去，避免：

- API 限流
- 失敗時不好追
- 後續 Google Drive 檔案過多不好觀察

---

## 4. Data Table - 讀取 API Key

我把 API Key 放在 n8n 的 Data Table 裡，透過 `KeyName = Gemini` 去抓。

這樣比直接把 Key 寫死在 HTTP Request 節點裡好一些，之後換 Key 比較方便。

---

## 5. HTTP Request - 呼叫 Gemini TTS

這是整個流程的核心。

### Request URL

```
https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent
```

### Header

```
x-goog-api-key: {{ $json.KeyValue }}
```

### Body
<details>
  <summary>點擊查看程式碼</summary>
    
```json
{
  "contents": [
    {
      "parts": [
        {
          "text": "{{ $('Get many database pages').item.json.property_scene_script }}"
        }
      ]
    }
  ],
  "generationConfig": {
    "responseModalities": ["AUDIO"],
    "speechConfig": {
      "voiceConfig": {
        "prebuiltVoiceConfig": {
          "voiceName": "Kore"
        }
      }
    }
  },
  "model": "gemini-2.5-flash-preview-tts"
}
```
    
</details>

![image](https://hackmd.io/_uploads/r1yyn-2tZl.png)

### 這裡的重點

Gemini TTS 回來的JSON音訊資料會放在：


```text
candidates[0].content.parts[0].inlineData.data
```

這個欄位是 **base64 字串**。

而 MIME Type 會在：

```text
candidates[0].content.parts[0].inlineData.mimeType
```
   
<details>
  <summary>Gemini回傳JSON</summary>
    
```json
[
  {
    "candidates": [
      {
        "content": {
          "parts": [
            {
              "inlineData": {
                "mimeType": "audio/L16;codec=pcm;rate=24000",
                "data": "略…" }
            }
          ],
          "role": "model"
        },
        "finishReason": "STOP",
        "index": 0
      }
    ],
    "usageMetadata": {
      "promptTokenCount": 28,
      "candidatesTokenCount": 221,
      "totalTokenCount": 249,
      "promptTokensDetails": [
        {
          "modality": "TEXT",
          "tokenCount": 28
        }
      ],
      "candidatesTokensDetails": [
        {
          "modality": "AUDIO",
          "tokenCount": 221
        }
      ]
    },
    "modelVersion": "gemini-2.5-flash-preview-tts",
    "responseId": "LpKvabm8Bvml1e8P0JqdgA0"
  }
]
    
```
    
</details>

---

## 6. Edit Fields - 把Gemini API回傳內容整理出來

我用 Set 節點把需要的欄位先拉平：

- `audio_base64`
- `audio_mime`

設定如下：

```j

audio_base64 = {{ $json.candidates[0].content.parts[0].inlineData.data }}

audio_mime   = {{ $json.candidates[0].content.parts[0].inlineData.mimeType }}

```

![image](https://hackmd.io/_uploads/HkakzG6t-x.png)

這一步的目的很簡單：  
先把後面會用到的欄位整理乾淨，後續比較好接。

---

## 7. Convert to File - 先轉成 PCM 檔

這一步非常重要。

Gemini TTS 回來的是音訊 base64，但不是直接可播的 WAV。  
所以先用 `Convert to File` 把它轉成 binary。

### 目前設定

- Operation: `toBinary`
- Source Property: `audio_base64`
- Binary Property Name: `pcm_raw`

![image](https://hackmd.io/_uploads/r12EEMptZl.png)

### Options

- File Name: `voice.pcm`
- Mime Type: `audio/L16;codec=pcm;rate=24000`

這裡代表目前拿到的是：

- 16-bit PCM
- 24kHz
- 單純原始音訊資料

---

## 8. Code - 幫 PCM 補上 WAV Header

![image](https://hackmd.io/_uploads/H14fhbhtWl.png)


這一段是最容易卡住的地方。

因為很多人會以為：  
> base64 轉成檔案後就能播了

但其實 **PCM 原始資料沒有 WAV Header**，播放器通常無法正常辨識。  
所以需要自己補 44 bytes 的 WAV Header，拼回完整 WAV 檔。

### Code 節點內容

```javascript
    
function createWavHeader(dataLength, sampleRate = 24000, numChannels = 1, bitsPerSample = 16) {
  const byteRate = sampleRate * numChannels * bitsPerSample / 8;
  const blockAlign = numChannels * bitsPerSample / 8;

  const header = Buffer.alloc(44);

  header.write('RIFF', 0, 'ascii');
  header.writeUInt32LE(36 + dataLength, 4);
  header.write('WAVE', 8, 'ascii');

  header.write('fmt ', 12, 'ascii');
  header.writeUInt32LE(16, 16);
  header.writeUInt16LE(1, 20); // PCM
  header.writeUInt16LE(numChannels, 22);
  header.writeUInt32LE(sampleRate, 24);
  header.writeUInt32LE(byteRate, 28);
  header.writeUInt16LE(blockAlign, 32);
  header.writeUInt16LE(bitsPerSample, 34);

  header.write('data', 36, 'ascii');
  header.writeUInt32LE(dataLength, 40);

  return header;
}

const item = $input.item;

// 不要直接讀 item.binary.pcm_raw.data
// 用 helper 取真正的 binary buffer
const pcmBuffer = await this.helpers.getBinaryDataBuffer($itemIndex, 'pcm_raw');

if (!pcmBuffer || !pcmBuffer.length) {
  throw new Error('讀不到 pcm_raw 的實際 binary buffer');
}

const wavHeader = createWavHeader(pcmBuffer.length, 24000, 1, 16);
const wavBuffer = Buffer.concat([wavHeader, pcmBuffer]);

item.binary = item.binary || {};
item.binary.data = {
  data: wavBuffer.toString('base64'),
  mimeType: 'audio/wav',
  fileName: 'voice.wav',
  fileExtension: 'wav',
};

item.json = item.json || {};
item.json.output_file_name = 'voice.wav';
item.json.output_mime_type = 'audio/wav';
item.json.output_bytes = wavBuffer.length;
item.json.pcm_bytes = pcmBuffer.length;

return item;
```
---

## 為什麼不能直接讀 `item.binary.pcm_raw.data`？

這是我這次一個比較關鍵的坑。

在 n8n 裡，binary 資料不一定適合直接從：

```javascript
item.binary.pcm_raw.data
```

硬抓字串來處理。

比較穩定的方式是：

```
    
await this.helpers.getBinaryDataBuffer($itemIndex, 'pcm_raw');
    
```

這樣才能拿到真正的 binary buffer，不然很容易出現：

- 檔案大小怪怪的
- WAV 只有幾十 bytes
- 播放時間 0 秒
- 聲音雜訊或完全不能播

---

## 9. Google Drive Upload

WAV 產生完成後，我直接上傳到 Google Drive。

目前設定：

- Input Data Field Name: `data`
- Upload Folder: 指定目標資料夾
- 檔名：目前使用 `scene_script` 當名稱

---

## 實作完成後的結果

完成後，每一筆 Notion 的文稿都會變成一個 WAV 音檔，自動上傳到指定的 Google Drive 資料夾。

這樣後面如果要再接：

- 字幕生成
- 圖片搭配
- FFmpeg 合成影片
- 自動上傳社群平台

就會很順。

---

## 這個流程我覺得最容易踩的坑

## 1. 以為 API 回傳的是可直接播放的 WAV

不是。  
它本質上是 **PCM raw audio**，還要補 WAV Header。

---

## 2. Convert to File 成功，不代表音檔可播放

`Convert to File` 只是把 base64 轉成 binary。  
如果來源本身是 raw PCM，還是不能直接當 WAV 用。

---

## 3. Binary 不能亂抓

很多時候不是資料沒回來，而是抓法錯了。  
建議直接用：

```javascript
this.helpers.getBinaryDataBuffer()
```

---

## 4. 檔名不要直接吃整段文稿

目前你的 Google Drive 節點是：

```text
name = {{ $('Loop Over Items').item.json.property_scene_script }}
```

這在測試時可以，但正式使用比較容易出問題，例如：

- 檔名太長
- 含特殊字元
- 同名覆蓋
- Google Drive 看起來很亂

比較建議改成：

```text
scene_001.wav
scene_002.wav
```

或是至少加一個可控欄位，例如：

- `scene_no`
- `title`
- `file_name`

---

## 我後續可能會再補的優化方向

後面如果要把這段流程變成可長期使用，我覺得還可以補幾件事：

1. **產完音檔後回寫 Notion 狀態**
   - 例如從 `active` 改成 `done`
   - 避免重複生成

2. **Google Drive 回存檔案網址到 Notion**
   - 後續剪片流程更好接

3. **增加失敗重試機制**
   - 特別是 Gemini API quota 或暫時性錯誤

4. **檔名正規化**
   - 自動過濾特殊字元
   - 自動截斷過長名稱

5. **接 FFmpeg / 影片合成流程**
   - 後面就能直接串成完整短影音生產線

---

## 總結

這條 n8n 流程的核心重點其實只有一句：

> **Gemini TTS 回來的是 PCM raw audio，不是可直接播放的 WAV。**

所以如果你遇到：

- 音檔只有 0 秒
- 下載後不能播
- 聲音怪怪的
- 檔案大小不合理

大機率問題不是 TTS 沒成功，而是 **你還沒把 PCM 正確包成 WAV**。

我目前這套作法是：

- 先從 Notion 讀文稿
- 呼叫 Gemini TTS
- 取出 base64 音訊
- 轉成 PCM binary
- 用 Code 節點補 WAV Header
- 上傳 Google Drive

這樣就能穩定輸出可用的 WAV 檔。

---

## 附註

如果你也在做：

- 短影音自動生成
- AI 配音批次化
- n8n 串 Notion / Google Drive / Gemini
- 後續再接字幕與影片合成

這段流程可以直接拿去改成自己的版本。
