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。

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more

Top comments (0)

nextjs tutorial video

Youtube Tutorial Series 📺

So you built a Next.js app, but you need a clear view of the entire operation flow to be able to identify performance bottlenecks before you launch. But how do you get started? Get the essentials on tracing for Next.js from @nikolovlazar in this video series 👀

Watch the Youtube series