介紹如何透過 n8n 製作一個自動語音天氣預報機器人,我會抓取氣象署的 API,接著透過 AI 轉換成語音後發送到 Line。

 

Step 1: 建立初始節點

這邊可以用 Schedule Trigger,這樣之後就可以自動生成了。

 

Step 2: 抓取氣象資訊

接著建立 Http Request 節點,我是抓 36 小時天氣預報,並依序填入:

 

Step 3: 整理氣象資訊

要發送給 TTS 的話要先把內容給整理好,由於氣象資訊可以直接組合成一個語句,所以就不透過大語言模型,我直接建一個 Code 節點來處理資訊:

const location = items[0].json.records.location[0];
const elements = Object.fromEntries(location.weatherElement.map(e => [e.elementName, e]));

// 找出所有要素中最早的時間段
const allStarts = location.weatherElement
  .flatMap(e => (e.time || []).map(t => t.startTime))
  .filter(Boolean);
const targetStart = allStarts.sort()[0];

// 取指定時間段參數
function pickByStart(el, start) {
  if (!el || !Array.isArray(el.time)) return null;
  const hit = el.time.find(t => t.startTime === start) || el.time[0];
  return hit ? { value: hit.parameter?.parameterName ?? null, start: hit.startTime, end: hit.endTime } : null;
}
function pickPoP(start) {
  const candidates = [elements.PoP, elements.PoP6h, elements.PoP12h];
  for (const el of candidates) {
    const res = pickByStart(el, start);
    if (res) return res;
  }
  return null;
}

const wx   = pickByStart(elements.Wx, targetStart);
const pop  = pickPoP(targetStart);
const minT = pickByStart(elements.MinT, targetStart);
const maxT = pickByStart(elements.MaxT, targetStart);

// 安全解析時間字串(避免 new Date(YYYY-MM-DD HH:mm:ss) 解析誤差)
function parseDateStr(str) {
  if (!str) return null;
  const [date, time] = str.split(' ');
  const [y, m, d] = date.split('-').map(Number);
  const [hh='0', mm='0', ss='0'] = (time || '').split(':');
  return new Date(Number(y), Number(m)-1, Number(d), Number(hh), Number(mm), Number(ss));
}

// 格式:M月D日 + (凌晨/早上/下午/晚上) + 12小時制
function formatTime(str) {
  if (!str) return "";
  const d = parseDateStr(str);
  const M = d.getMonth() + 1;
  const D = d.getDate();
  const h = d.getHours();

  let label = "";
  if (h < 6) label = "凌晨";
  else if (h < 12) label = "早上";
  else if (h < 18) label = "下午";
  else label = "晚上";

  // 12 小時制(18 -> 6,13 -> 1;特別處理 0 點顯示為 0 點)
  let h12 = h % 12 || 12;
  if (h === 0) h12 = 0;

  return `${M}月${D}日 ${label}${h12}點`;
}

const startStr = formatTime(wx?.start || targetStart);
const endStr   = formatTime(wx?.end);

// 只輸出口語化 message
return [{
  json: {
    message: `${startStr} 到 ${endStr},${location.locationName}的天氣預報是 ${wx?.value || "—"},氣溫在 ${minT?.value || "—"} 到 ${maxT?.value || "—"} 度之間,降雨機率大約 ${pop?.value || "—"}%。`
  }
}];

 

Step 4: 透過 TTS 轉成語音

這裡你可以用 OpenAI 節點,但我發現預設節點沒有比較新的 TTS 模型,所以我就改用自訂 API 的方式去呼叫,我這邊建立一個 Http Request 節點,並設定:

  • Method: POST
  • URL: https://api.openai.com/v1/audio/speech
  • Send Header 打開
    • Authorization: Bearer 你的OpenAI API token
    • Content-Type: application/json
  • Send Body 打開
    • Body Content Type: JSON
    • Specify Body: Using JSON
{
  "model": "gpt-4o-mini-tts",
  "input": "{{ $json.message }}",
  "instructions": "請用台灣本地口音的標準國語發音,語氣自然親切,避免兒化音,使用台灣慣用詞。",
  "audio": { "voice": "verse", "format": "mp3" }
}

這裡我就是把前一個節點整理的文字丟給 OpenAI,我使用的是 gpt-4o-mini-tts 模型,並指定他用台灣腔。

 

Step 5: 整理音訊檔

因為要傳到雲端空間有時候傳過去的格式有問題就會失敗,我這邊先新增一個 Code 節點去處理上一個節點來的音訊檔。

// 把 binary 的 key 整到 data,並補上檔名與 MIME
for (const item of items) {
  if (!item.binary) throw new Error('這個 item 沒有 binary');
  const keys = Object.keys(item.binary);
  const srcKey = keys.includes('data') ? 'data' : keys[0];
  const b = item.binary[srcKey];
  if (!b?.data) throw new Error('binary 沒內容');

  item.binary.data = b;                     // 讓 key 一律叫 data
  item.binary.data.fileName = 'speech.mp3'; // 一定要有 .mp3
  item.binary.data.mimeType = 'audio/mpeg'; // 對應 mp3
  if (srcKey !== 'data') delete item.binary[srcKey];
}
return items;

 

Step 6: 上傳至雲端空間

由於要發送給 Line 需要給他一個在網路上的連結,所以需要把檔案傳到伺服器上,我這邊示範傳到一個雲端暫存空間,因為比較單純大家都能做到,不過如果是真正的服務就一定要有自己的空間且不會刪檔會比較好。

新增 Http Request 節點,並設定:

 

Step 7: 處理連結網址

接著處理上傳到雲端後的網址,我這邊先處理 tmpfiles 去抓到檔案真正的路徑,另外由於要發送給 Line 的連結一定要是 https,而 tmpfiles 回傳的路徑預設會是 http,所以我就用一個 Code 節點來整理這些資訊。

const pageUrl = $json.data?.url || '';
// 確保使用 https + dl 直連
const link = pageUrl
  .replace('http://', 'https://')
  .replace('://tmpfiles.org/', '://tmpfiles.org/dl/');
return [{ json: { link } }];

 

Step 8: 發送至 Line

最後就是發送到 Line 啦,新增一個 Http Requst 節點,然後設定:

  • Method: POST
  • URL: https://api.line.me/v2/bot/message/push
  • Send Header 打開
    • Authorization: Bearer Channel_token
    • Content-Type: application/json
  • Send Body 打開
  • Body Content Type: JSON
  • Specify Body: Using JSON
{
  "to": 接收者 Line ID,
  "messages": [
    {
      "type":"audio",
      "originalContentUrl":"{{$json.link}}",
      "duration":4000
    }
  ]

}

這裡因為一定要給 duration 資訊,不過不一定要正確,所以我就都給一個估計的值,如果希望很精準再去處理就好。

最後成功就可以在 Line 看到處理完傳過來的語音訊息啦。

完整工作流會長這樣: