DEV Community

Cover image for 免費開源的語音辨識功能:Cloudflare Workers AI + Whisper
Let's Write
Let's Write

Posted on • Edited on • Originally published at letswrite.tw

免費開源的語音辨識功能:Cloudflare Workers AI + Whisper

本篇要解決的問題

之前寫過二篇開源的語音辨識功能:

免費開源的語音辨識功能:Google Colab + Whisper large v3

免費開源的語音辨識功能:Google Colab + Faster Whisper

這篇算是第三篇,是這幾天想調整一下 Cloudflare 上的設定時,看到有多了 Workers AI 的功能,點一點後意外發現的。

原本很開心的以為終於有個好操作的免費版可以使用,但實際使用時,發現 Workers AI 對檔案大小有限制,而且是超過 2MB 就會直接跳「AiError」不給辨識。

不能超過 2MB 的檔案?

想了一想,應該就只有短影音之類的了,所以覺得用 Workers AI 來語音辨識好像不怎麼實用。

只是都已經研究出使用方式了,就還是整理為本篇筆記文,期待以後會再放寬檔案大小的限制。


註冊 Cloudflare 帳號

Cloudflare 是佛心來的,免費帳號就可以擁有很多功能,包含今天這篇 Workers AI。

進到官方網站後,點右上角的「註冊」按鈕,就可以免費註冊:

https://www.cloudflare.com/zh-tw/


開通 Speech to Text App 功能

註冊成功後,左側選單點擊「AI > Workers AI」,接著右側點擊「從 Worker 範本建立」:

點擊從 Worker 範本建立

點擊從 Worker 範本建立

可以看到 Workers AI 的範本有很多,有興趣的朋友可以玩玩其他的。

本篇我們要使用的是語音轉文字,所以點擊「Speech to Text App」:

Speech to Text App

Speech to Text App

點擊後,會看見頁面上 Cloudflare 已經提供了需要的檔案,基本的程式碼也寫出來了。

這一步需要做的,就是修改名稱,然後按下「部署」:

修改名稱,點擊部署

修改名稱,點擊部署

名稱會影響的是後續我們調用 API 時的 URL,可以取一個自己能辨識的。

Cloudflare 部署進度很快,不用 10 秒就會部署完成,成功後會看到以下畫面:

部署完成

部署完成


修改程式碼

Workers AI 給的程式碼是基本的使用方式,我們要調整成我們好用的。

本篇,August 會把程式碼調整成前端可以用 API 的方式來取得辨識的結果。

以下程式碼,是 ChatGPT + Claude AI 提供的程式碼,August 再稍為修改一下而成的,上圖中點擊「編輯代碼」後,把以下程式碼複製、貼上去後,再按鈕部署,這步驟就完成了:

const CORS_HEADERS = {
  'Access-Control-Allow-Origin': '*', // 這邊可以限制網域
  'Access-Control-Allow-Methods': 'POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};

export default {
  async fetch(request, env) {
    if (request.method === 'OPTIONS') {
      return new Response(null, { headers: CORS_HEADERS });
    }

    if (request.method !== 'POST') {
      return new Response('Method Not Allowed', {
        status: 405,
        headers: CORS_HEADERS,
      });
    }

    const contentType = request.headers.get('Content-Type');
    if (!contentType || !contentType.includes('multipart/form-data')) {
      return new Response(JSON.stringify({ error: 'Invalid Content-Type' }), {
        status: 400,
        headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
      });
    }

    try {
      const formData = await request.formData();
      const file = formData.get('file');

      if (!file) {
        return new Response(JSON.stringify({ error: 'No file uploaded' }), {
          status: 400,
          headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
        });
      }

      const blob = await file.arrayBuffer();

      const inputs = {
        audio: [...new Uint8Array(blob)],
      };

      const response = await env.AI.run('@cf/openai/whisper', inputs);

      return new Response(JSON.stringify(response), {
        headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
      });
    } catch (error) {
      return new Response(JSON.stringify({ error: error }), {
        status: 500,
        headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
      });
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

'Access-Control-Allow-Origin': '*' 這行記得修改,可以限制我們自己的網域才能使用,* 代表全宇宙都可以使用。為了本篇的示範方便,August 這邊才寫為 *

最後的畫面會像這樣:

更新程式碼後部署

更新程式碼後部署

取得 API URL

Cloudflare 部署 Workers AI 後,在我們第一部修改名稱時,就會看到這個對外的網址,如果忘記了,可以點擊畫面中的「workers.dev」取得:

取得 API URL

取得 API URL

點擊後會新開一個頁籤,這個頁籤的網址就是我們下一步調用 API 時的 URL。


前端建立頁面調用 API

前端的工,就是放一個 input type="file",再放一個 button 執行點擊後調用 API,收到回應後再把回應值塞到指定的 div 裡。

之前寫語音辨識為文字的筆記文,有人留言說需要字幕檔的方式,所以以下的程式碼也有加上「下載為字幕檔」的功能。

當然,有了 ChatGPT 的時代,很多程式碼都不用自己從 0 到 1 了,以下程式碼是 ChatGPT 生成一版後,August 再稍微調整的:

<!DOCTYPE html>
<html lang="zh-Hant">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>語音轉文字</title>

  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/dark.css">
</head>

<body>
  <h1>語音轉文字</h1>
  <input type="file" id="fileInput" accept="audio/*,video/*" required>
  <button id="submit" type="button">上傳並轉換</button>

  <div class="output-section">
    <h2>原始辨識結果</h2>
    <pre id="originalOutput"></pre>
  </div>

  <div class="output-section">
    <h2>字幕檔 (SRT)</h2>
    <pre id="srtOutput"></pre>
    <button id="downloadButton" style="display: none;">下載字幕檔</button>
  </div>

  <script>
    document.getElementById('submit').addEventListener('click', async (event) => {
      event.preventDefault();

      const fileInput = document.getElementById('fileInput');
      if (fileInput.files.length === 0) {
        alert('請選擇一個音頻檔案');
        return;
      }

      const formData = new FormData();
      formData.append('file', fileInput.files[0]);

      const uri = 'https://xxx.xxx.xxx'; // 替換成自己的 URL
      const response = await fetch(uri, {
        method: 'POST',
        body: formData
      });

      if (response.ok) {
        const data = await response.json();
        if (data.vtt) {
          document.getElementById('originalOutput').innerText = data.text.replace(/ /g, '');
          const srtContent = vttToSrt(data.vtt);
          document.getElementById('srtOutput').innerText = srtContent;
          document.getElementById('downloadButton').style.display = 'block';
          document.getElementById('downloadButton').addEventListener('click', () => {
            const blob = new Blob([srtContent], { type: 'text/srt' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'subtitles.srt';
            a.click();
            URL.revokeObjectURL(url);
          });
        } else {
          document.getElementById('originalOutput').innerText = '無法取得字幕檔案';
          document.getElementById('srtOutput').innerText = '';
          document.getElementById('downloadButton').style.display = 'none';
        }
      } else {
        document.getElementById('originalOutput').innerText = '語音轉文字失敗,請檢查伺服器設置。';
        document.getElementById('srtOutput').innerText = '';
        document.getElementById('downloadButton').style.display = 'none';
      }
    });

    function vttToSrt(vtt) {
      const lines = vtt.split('\n');
      let srt = '';
      let counter = 1;

      for (let i = 0; i < lines.length; i++) {
        if (lines[i].includes('-->')) {
          srt += `${counter}\n`;
          srt += lines[i].replace('.', ',') + '\n';
          counter++;
        } else {
          srt += lines[i] + '\n';
        }
      }
      return srt;
    }
  </script>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

複製貼上後,需要手動修改的是這行:

const uri = 'https://xxx.xxx.xxx';
Enter fullscreen mode Exit fullscreen mode

換成我們在上一步,從 Cloudflare Workers AI 取得的 URL 即可。

頁面打開來,會長得像這樣:

頁面樣子

頁面樣子

要注意一下,這邊沒有寫 loading 效果,所以當選好了檔案,點擊「上傳並轉換」後,實際上背後已經在調用 API 了,請自己開啟 Chrome 的 Network 面版查看。

成功的話頁面上會秀出辨識結果。

失敗的話,要從 Console 面版去看錯誤訊息,通常失敗的原因就是檔案大小超過 2MB。

Top comments (0)