跳至主要内容

文字轉語音 - Wio Terminal

在這部分的課程中,你將把文字轉換為語音,以提供語音反饋。

文字轉語音

你在上一課中使用的語音服務 SDK 可以用來將語音轉換為文字,也可以用來將文字轉換回語音。

獲取語音列表

在請求語音時,你需要提供要使用的語音,因為語音可以使用各種不同的聲音生成。每種語言都支持一系列不同的聲音,你可以從語音服務 SDK 獲取每種語言支持的聲音列表。微控制器的限制在這裡發揮作用 - 獲取文字轉語音服務支持的語音列表的調用是一個超過 77KB 大小的 JSON 文檔,對 Wio Terminal 來說太大而無法處理。在撰寫本文時,完整列表包含 215 種聲音,每種聲音由如下的 JSON 文檔定義:

{
"Name": "Microsoft Server Speech Text to Speech Voice (en-US, AriaNeural)",
"DisplayName": "Aria",
"LocalName": "Aria",
"ShortName": "en-US-AriaNeural",
"Gender": "Female",
"Locale": "en-US",
"StyleList": [
"chat",
"customerservice",
"narration-professional",
"newscast-casual",
"newscast-formal",
"cheerful",
"empathetic"
],
"SampleRateHertz": "24000",
"VoiceType": "Neural",
"Status": "GA"
}

這個 JSON 是 Aria 聲音的定義,它有多種語音風格。在將文字轉換為語音時,只需要使用短名稱 en-US-AriaNeural

與其在微控制器上下載和解碼整個列表,你需要編寫一些無伺服器代碼來檢索你所使用語言的聲音列表,並從你的 Wio Terminal 調用這個代碼。然後你的代碼可以從列表中選擇一個合適的聲音,例如找到的第一個聲音。

任務 - 創建一個無伺服器函數來獲取聲音列表

  1. 在 VS Code 中打開你的 smart-timer-trigger 項目,並打開終端,確保虛擬環境已激活。如果沒有,請終止並重新創建終端。

  2. 打開 local.settings.json 文件,並添加語音 API 密鑰和位置的設置:

    "SPEECH_KEY": "<key>",
    "SPEECH_LOCATION": "<location>"

    <key> 替換為你的語音服務資源的 API 密鑰。將 <location> 替換為你創建語音服務資源時使用的位置。

  3. 使用以下命令在 VS Code 終端中在函數應用項目的根文件夾中添加一個名為 get-voices 的新 HTTP 觸發器:

    func new --name get-voices --template "HTTP trigger"

    這將創建一個名為 get-voices 的 HTTP 觸發器。

  4. 用以下內容替換 get-voices 文件夾中的 __init__.py 文件的內容:

    import json
    import os
    import requests

    import azure.functions as func

    def main(req: func.HttpRequest) -> func.HttpResponse:
    location = os.environ['SPEECH_LOCATION']
    speech_key = os.environ['SPEECH_KEY']

    req_body = req.get_json()
    language = req_body['language']

    url = f'https://{location}.tts.speech.microsoft.com/cognitiveservices/voices/list'

    headers = {
    'Ocp-Apim-Subscription-Key': speech_key
    }

    response = requests.get(url, headers=headers)
    voices_json = json.loads(response.text)

    voices = filter(lambda x: x['Locale'].lower() == language.lower(), voices_json)
    voices = map(lambda x: x['ShortName'], voices)

    return func.HttpResponse(json.dumps(list(voices)), status_code=200)

    這段代碼向端點發出 HTTP 請求以獲取聲音列表。這個聲音列表是一個包含所有語言聲音的大塊 JSON,因此會過濾出請求正文中傳遞的語言的聲音,然後提取並返回短名稱作為 JSON 列表。短名稱是將文字轉換為語音所需的值,因此只返回這個值。

    💁 你可以根據需要更改過濾器以選擇你想要的聲音。

    這將數據大小從 77KB(撰寫本文時)減少到一個更小的 JSON 文檔。例如,對於美國聲音,這是 408 字節。

  5. 在本機運行你的函數應用。然後你可以使用 curl 之類的工具來調用它,就像你測試 text-to-timer HTTP 觸發器一樣。確保以 JSON 正文的形式傳遞你的語言:

    {
    "language":"<language>"
    }

    <language> 替換為你的語言,例如 en-GBzh-CN

💁 你可以在 code-spoken-response/functions 文件夾中找到這段代碼。

任務 - 從你的 Wio Terminal 獲取語音

  1. 如果尚未打開,請在 VS Code 中打開 smart-timer 項目。

  2. 打開 config.h 頭文件並添加你的函數應用的 URL:

    const char *GET_VOICES_FUNCTION_URL = "<URL>";

    <URL> 替換為你的函數應用中 get-voices HTTP 觸發器的 URL。這將與 TEXT_TO_TIMER_FUNCTION_URL 的值相同,只是函數名稱為 get-voices 而不是 text-to-timer

  3. src 文件夾中創建一個名為 text_to_speech.h 的新文件。這將用於定義一個類來將文本轉換為語音。

  4. 在新的 text_to_speech.h 文件頂部添加以下包含指令:

    #pragma once

    #include <Arduino.h>
    #include <ArduinoJson.h>
    #include <HTTPClient.h>
    #include <Seeed_FS.h>
    #include <SD/Seeed_SD.h>
    #include <WiFiClient.h>
    #include <WiFiClientSecure.h>

    #include "config.h"
    #include "speech_to_text.h"
  5. 在此下方添加以下代碼以聲明 TextToSpeech 類,以及可以在應用程序的其餘部分中使用的實例:

    class TextToSpeech
    {
    public:
    private:
    };

    TextToSpeech textToSpeech;
  6. 要調用你的函數應用,你需要聲明一個 WiFi 客戶端。將以下內容添加到類的 private 部分:

    WiFiClient _client;
  7. private 部分中,添加一個字段來選擇語音:

    String _voice;
  8. public 部分中,添加一個 init 函數來獲取第一個語音:

    void init()
    {
    }
  9. 要獲取語音,需要將 JSON 文檔發送到函數應用並指定語言。將以下代碼添加到 init 函數中以創建此 JSON 文檔:

    DynamicJsonDocument doc(1024);
    doc["language"] = LANGUAGE;

    String body;
    serializeJson(doc, body);
  10. 接下來創建一個 HTTPClient,然後使用它調用函數應用以獲取語音,並發送 JSON 文檔:

    HTTPClient httpClient;
    httpClient.begin(_client, GET_VOICES_FUNCTION_URL);

    int httpResponseCode = httpClient.POST(body);
  11. 在此下方添加代碼以檢查響應代碼,如果是 200(成功),則提取語音列表,從列表中檢索第一個語音:

    if (httpResponseCode == 200)
    {
    String result = httpClient.getString();
    Serial.println(result);

    DynamicJsonDocument doc(1024);
    deserializeJson(doc, result.c_str());

    JsonArray obj = doc.as<JsonArray>();
    _voice = obj[0].as<String>();

    Serial.print("Using voice ");
    Serial.println(_voice);
    }
    else
    {
    Serial.print("Failed to get voices - error ");
    Serial.println(httpResponseCode);
    }
  12. 在此之後,結束 HTTP 客戶端連接:

    httpClient.end();
  13. 打開 main.cpp 文件,並在頂部添加以下包含指令以包含此新頭文件:

    #include "text_to_speech.h"
  14. setup 函數中,在調用 speechToText.init(); 之後,添加以下內容以初始化 TextToSpeech 類:

    textToSpeech.init();
  15. 構建此代碼,將其上傳到你的 Wio Terminal 並通過串行監視器進行測試。確保你的函數應用正在運行。

    你將看到從函數應用返回的可用語音列表,以及選定的語音。

    --- Available filters and text transformations: colorize, debug, default, direct, hexlify, log2file, nocontrol, printable, send_on_enter, time
    --- More details at http://bit.ly/pio-monitor-filters
    --- Miniterm on /dev/cu.usbmodem1101 9600,8,N,1 ---
    --- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
    Connecting to WiFi..
    Connected!
    Got access token.
    ["en-US-JennyNeural", "en-US-JennyMultilingualNeural", "en-US-GuyNeural", "en-US-AriaNeural", "en-US-AmberNeural", "en-US-AnaNeural", "en-US-AshleyNeural", "en-US-BrandonNeural", "en-US-ChristopherNeural", "en-US-CoraNeural", "en-US-ElizabethNeural", "en-US-EricNeural", "en-US-JacobNeural", "en-US-MichelleNeural", "en-US-MonicaNeural", "en-US-AriaRUS", "en-US-BenjaminRUS", "en-US-GuyRUS", "en-US-ZiraRUS"]
    Using voice en-US-JennyNeural
    Ready.

將文本轉換為語音

一旦你有了要使用的語音,它就可以用來將文本轉換為語音。與將語音轉換為文本時的內存限制相同,因此你需要將語音寫入 SD 卡,以便通過 ReSpeaker 播放。

💁 在本項目的早期課程中,你使用閃存來存儲從麥克風捕獲的語音。這節課使用 SD 卡,因為使用 Seeed 音頻庫從中播放音頻更容易。

還有另一個限制需要考慮,即來自語音服務的可用音頻數據以及 Wio Terminal 支持的格式。與完整的計算機不同,微控制器的音頻庫在支持的音頻格式方面可能非常有限。例如,Seeed Arduino Audio 庫只能以 44.1KHz 的採樣率播放聲音。Azure 語音服務可以提供多種格式的音頻,但它們都不使用此採樣率,它們僅提供 8KHz、16KHz、24KHz 和 48KHz。因此,需要將音頻重新採樣為 44.1KHz,這需要比 Wio Terminal 擁有的更多資源,尤其是內存。

在需要操作此類數據時,通常最好使用無服務器代碼,尤其是當數據是通過 Web 調用獲取時。Wio Terminal 可以調用無服務器函數,傳遞要轉換的文本,無服務器函數可以調用語音服務將文本轉換為語音,並將音頻重新採樣為所需的採樣率。然後它可以以 Wio Terminal 需要的形式返回音頻,以便存儲在 SD 卡上並通過 ReSpeaker 播放。

任務 - 創建一個無服務器函數來將文本轉換為語音

  1. 在 VS Code 中打開你的 smart-timer-trigger 項目,並打開終端,確保虛擬環境已激活。如果沒有,請終止並重新創建終端。

  2. 使用以下命令在此應用中添加一個名為 text-to-speech 的新 HTTP 觸發器,從函數應用項目的根文件夾內的 VS Code 終端中運行:

    func new --name text-to-speech --template "HTTP trigger"

    這將創建一個名為 text-to-speech 的 HTTP 觸發器。

  3. librosa Pip 包具有重新採樣音頻的功能,因此將其添加到 requirements.txt 文件中:

    librosa

    添加後,使用以下命令從 VS Code 終端安裝 Pip 包:

    pip install -r requirements.txt

    ⚠️ 如果你使用的是 Linux,包括 Raspberry Pi OS,你可能需要使用以下命令安裝 libsndfile

    sudo apt update
    sudo apt install libsndfile1-dev
  4. 要將文本轉換為語音,你不能直接使用語音 API 密鑰,而是需要請求訪問令牌,使用 API 密鑰來驗證訪問令牌請求。打開 text-to-speech 文件夾中的 __init__.py 文件,並將其中的所有代碼替換為以下內容:

    import io
    import os
    import requests

    import librosa
    import soundfile as sf
    import azure.functions as func

    location = os.environ['SPEECH_LOCATION']
    speech_key = os.environ['SPEECH_KEY']

    def get_access_token():
    headers = {
    'Ocp-Apim-Subscription-Key': speech_key
    }

    token_endpoint = f'https://{location}.api.cognitive.microsoft.com/sts/v1.0/issuetoken'
    response = requests.post(token_endpoint, headers=headers)
    return str(response.text)

    這定義了將從設置中讀取的位置和語音密鑰常量。然後定義 get_access_token 函數來檢索語音服務的訪問令牌。

  5. 在此代碼下方,添加以下內容:

    playback_format = 'riff-48khz-16bit-mono-pcm'

    def main(req: func.HttpRequest) -> func.HttpResponse:
    req_body = req.get_json()
    language = req_body['language']
    voice = req_body['voice']
    text = req_body['text']

    url = f'https://{location}.tts.speech.microsoft.com/cognitiveservices/v1'

    headers = {
    'Authorization': 'Bearer ' + get_access_token(),
    'Content-Type': 'application/ssml+xml',
    'X-Microsoft-OutputFormat': playback_format
    }

    ssml = f'<speak version=\'1.0\' xml:lang=\'{language}\'>'
    ssml += f'<voice xml:lang=\'{language}\' name=\'{voice}\'>'
    ssml += text
    ssml += '</voice>'
    ssml += '</speak>'

    response = requests.post(url, headers=headers, data=ssml.encode('utf-8'))

    raw_audio, sample_rate = librosa.load(io.BytesIO(response.content), sr=48000)
    resampled = librosa.resample(raw_audio, sample_rate, 44100)

    output_buffer = io.BytesIO()
    sf.write(output_buffer, resampled, 44100, 'PCM_16', format='wav')
    output_buffer.seek(0)

    return func.HttpResponse(output_buffer.read(), status_code=200)

    這定義了將文本轉換為語音的 HTTP 觸發器。它從發送到請求的 JSON 正文中提取要轉換的文本、語言和語音,構建一些 SSML 以請求語音,然後調用相關的 REST API,使用訪問令牌進行身份驗證。此 REST API 調用返回編碼為 16 位、48KHz 單聲道 WAV 文件的音頻,由 playback_format 的值定義,該值發送到 REST API 調用。

    然後,這些音頻由 librosa 從 48KHz 的採樣率重新採樣為 44.1KHz 的採樣率,然後將這些音頻保存到二進制緩衝區,然後返回。

  6. 在本機運行你的函數應用,或將其部署到雲端。然後你可以使用 curl 之類的工具來調用它,就像你測試 text-to-timer HTTP 觸發器一樣。確保以 JSON 正文的形式傳遞語言、語音和文本:

    {
    "language": "<language>",
    "voice": "<voice>",
    "text": "<text>"
    }

    <language> 替換為你的語言,例如 en-GBzh-CN。將 <voice> 替換為你想要使用的語音。將 <text> 替換為你想要轉換為語音的文本。你可以將輸出保存到文件中,並使用任何可以播放 WAV 文件的音頻播放器播放它。

    例如,要使用 Jenny Neural 語音將 "Hello" 轉換為美式英語語音,並在本地運行函數應用,你可以使用以下 curl 命令:

    curl -X GET 'http://localhost:7071/api/text-to-speech' \
    -H 'Content-Type: application/json' \
    -o hello.wav \
    -d '{
    "language":"en-US",
    "voice": "en-US-JennyNeural",
    "text": "Hello"
    }'

    這將把音頻保存到當前目錄中的 hello.wav

💁 你可以在 code-spoken-response/functions 文件夾中找到這段代碼。

任務 - 從你的 Wio Terminal 獲取語音

  1. 如果尚未打開,請在 VS Code 中打開 smart-timer 項目。

  2. 打開 config.h 頭文件並添加你的函數應用的 URL:

    const char *TEXT_TO_SPEECH_FUNCTION_URL = "<URL>";

    <URL> 替換為你的函數應用中 text-to-speech HTTP 觸發器的 URL。這將與 TEXT_TO_TIMER_FUNCTION_URL 的值相同,只是函數名稱為 text-to-speech 而不是 text-to-timer

  3. 打開 text_to_speech.h 頭文件,並在 TextToSpeech 類的 public 部分添加以下方法:

    void convertTextToSpeech(String text)
    {
    }
  4. convertTextToSpeech 方法中,添加以下代碼以創建要發送到函數應用的 JSON:

    DynamicJsonDocument doc(1024);
    doc["language"] = LANGUAGE;
    doc["voice"] = _voice;
    doc["text"] = text;

    String body;
    serializeJson(doc, body);

    這將語言、語音和文本寫入 JSON 文檔,然後將其序列化為字符串。

  5. 在此下方,添加以下代碼以調用函數應用:

    HTTPClient httpClient;
    httpClient.begin(_client, TEXT_TO_SPEECH_FUNCTION_URL);

    int httpResponseCode = httpClient.POST(body);

    這將創建一個 HTTPClient,然後使用 JSON 文檔向文本轉語音 HTTP 觸發器發出 POST 請求。

  6. 如果調用成功,則可以將函數應用調用返回的原始二進制數據流式傳輸到 SD 卡上的文件。添加以下代碼來執行此操作:

    if (httpResponseCode == 200)
    {
    File wav_file = SD.open("SPEECH.WAV", FILE_WRITE);
    httpClient.writeToStream(&wav_file);
    wav_file.close();
    }
    else
    {
    Serial.print("Failed to get speech - error ");
    Serial.println(httpResponseCode);
    }

    此代碼檢查響應,如果是 200(成功),則將二進制數據流式傳輸到 SD 卡根目錄中的名為 SPEECH.WAV 的文件。

  7. 在此方法的末尾,關閉 HTTP 連接:

    httpClient.end();
  8. 現在可以將要說的文本轉換為音頻。在 main.cpp 文件中,將以下行添加到 say 函數的末尾,以將要說的文本轉換為音頻:

    textToSpeech.convertTextToSpeech(text);

任務 - 從你的 Wio Terminal 播放音頻

即將推出

將你的函數應用部署到雲端

在本地運行函數應用的原因是因為 Linux 上的 librosa Pip 包依賴於一個默認未安裝的庫,並且需要在函數應用運行之前安裝。函數應用是無服務器的 - 沒有你可以自己管理的服務器,因此無法提前安裝此庫。

解決此問題的方法是使用 Docker 容器部署你的函數應用。每當需要啟動函數應用的新實例時(例如當需求超過可用資源時,或者如果函數應用長時間未使用並被關閉),雲端會部署此容器。

你可以在 Microsoft Docs 上的使用自定義容器在 Linux 上創建函數的文檔 中找到設置函數應用並通過 Docker 部署的說明。

部署完成後,你可以將你的 Wio Terminal 代碼移植到訪問此函數:

  1. 將 Azure Functions 證書添加到 config.h

    const char *FUNCTIONS_CERTIFICATE =
    "-----BEGIN CERTIFICATE-----\r\n"
    "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\r\n"
    "-----END CERTIFICATE-----\r\n";
  2. 將所有 <WiFiClient.h> 的包含更改為 <WiFiClientSecure.h>

  3. 將所有 WiFiClient 字段更改為 WiFiClientSecure

  4. 在每個具有 WiFiClientSecure 字段的類中,添加構造函數並在該構造函數中設置證書:

    _client.setCACert(FUNCTIONS_CERTIFICATE);