Jak přidat převod textu na řeč s rozhraním HTML5 Speech Synthesis API

Jak používat rozhraní API syntézy řeči HTML5 k přidávání textu do řeči do aplikace s několika hlasovými možnostmi.

Začínáme

Pro tento tutoriál použijeme full-stack JavaScriptový framework CheatCode, Joystick. Joystick spojuje rozhraní front-end UI s back-endem Node.js pro vytváření aplikací.

Pro začátek budeme chtít nainstalovat Joystick přes NPM. Před instalací se ujistěte, že používáte Node.js 16+, abyste zajistili kompatibilitu (pokud se potřebujete naučit, jak nainstalovat Node.js nebo spustit více verzí na vašem počítači, přečtěte si nejprve tento tutoriál):

Terminál

npm i -g @joystick.js/cli

Tím se Joystick nainstaluje globálně do vašeho počítače. Po instalaci vytvořte nový projekt:

Terminál

joystick create app

Po několika sekundách se zobrazí zpráva o odhlášení na cd do nového projektu a spusťte joystick start :

Terminál

cd app && joystick start

Poté by vaše aplikace měla být spuštěna a my jsme připraveni začít.

Přidání Bootstrap

Nejprve se ponoříme do kódu a chceme do naší aplikace přidat framework Bootstrap CSS. Zatímco vy nemáte Abychom toho dosáhli, naše aplikace bude vypadat o něco hezčí a nebudeme muset pro naše uživatelské rozhraní míchat CSS. Chcete-li to provést, přidáme odkaz Bootstrap CDN do /index.html soubor v kořenovém adresáři našeho projektu:

/index.html

<!doctype html>
<html class="no-js" lang="en">
  <head>
    <meta charset="utf-8">
    <title>Joystick</title>
    <meta name="description" content="An awesome JavaScript app that's under development.">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="theme-color" content="#FFCC00">
    <link rel="apple-touch-icon" href="/apple-touch-icon-152x152.png">
    <link rel="stylesheet" href="/_joystick/index.css">
    <link rel="manifest" href="/manifest.json">
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
    ${css}
  </head>
  <body>
    ...
  </body>
</html>

Zde, těsně nad ${css} část v souboru jsme vložili do <link></link> tag z dokumentace Bootstrap, který nám poskytuje přístup k CSS části rámce.

A je to. Joystick se automaticky restartuje a načte jej do prohlížeče, abychom jej mohli začít používat.

Zapojení komponenty joysticku s převodem textu na řeč

V aplikaci Joystick je naše uživatelské rozhraní vytvořeno pomocí vestavěné knihovny uživatelského rozhraní rámce @joystick.js/ui . Když jsme spustili joystick create app výše jsme dostali několik příkladů komponent, se kterými jsme mohli pracovat. Přepíšeme /ui/pages/index/index.js soubor s nějakým HTML, který bude sloužit jako uživatelské rozhraní našeho překladače.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  css: `
    h4 {
      border-bottom: 1px solid #eee;
      padding-bottom: 20px;
      margin-bottom: 40px;
    }

    textarea {
      margin-bottom: 40px;
    }
  `,
  render: () => {
    return `
      <div>
        <h4>Text to Speech Translator</h4>
        <form>
          <textarea class="form-control" name="textToTranslate" placeholder="Type the text to speak here and then press Speak below."></textarea>
          <button class="btn btn-primary">Speak</button>
        </form>
        <div class="players"></div>
      </div>
    `;
  },
});

export default Index;

Chcete-li začít, chceme nahradit komponentu, která byla v tomto souboru, tím, co vidíme výše. Zde definujeme jednoduchou komponentu se dvěma věcmi:render funkce, která vrací řetězec HTML, který chceme zobrazit v prohlížeči a nad ním, řetězec css které chceme použít na HTML, který vykreslujeme (Joystick automaticky upraví rozsah CSS, který zde předáme, na HTML vrácený naším render funkce).

Pokud načteme http://localhost:2600 v prohlížeči (port 2600 je místo, kde se joystick standardně spouští, když spustíme joystick start ), měli bychom vidět verzi HTML ve stylu Bootstrap výše.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  events: {
    'submit form': (event, component) => {
      event.preventDefault();

      const text = event?.target?.textToTranslate?.value;
      const hasText = text.trim() !== '';

      if (!hasText) {
        return component.methods.speak('Well you have to say something!');
      }

      component.methods.speak(text);
    },
  },
  css: `...`,
  render: () => {
    return `
      <div>
        <h4>Text to Speech Translator</h4>
        <form>
          <textarea class="form-control" name="textToTranslate" placeholder="Type the text to speak here and then press Speak below."></textarea>
          <button class="btn btn-primary">Speak</button>
        </form>
        <div class="players"></div>
      </div>
    `;
  },
});

export default Index;

Dále chceme přidat events námitky proti naší složce. Jak název napovídá, zde definujeme posluchače událostí pro naši komponentu. Zde definujeme posluchače pro submit událost na <form></form> prvek vykreslovaný naší komponentou. Stejně jako naše CSS, i Joystick automaticky přiřazuje naše události k vykreslovanému HTML.

Přiřazeno k tomu submit form vlastnost na našem events object je funkce, která bude volána vždy, když je detekována událost odeslání na našem <form></form> .

Uvnitř této funkce nejprve vezmeme event (toto je událost DOM prohlížeče) jako první argument a okamžitě zavolejte event.preventDefault() na to. To zabrání prohlížeči v pokusu o provedení HTTP POST na action atribut na našem formuláři. Jak název napovídá, toto je výchozí chování pro prohlížeče (nemáme action atribut na našem formuláři, protože chceme kontrolovat odesílání pomocí JavaScriptu).

Dále, jakmile je toto zastaveno, chceme získat hodnotu zadanou do našeho <textarea></textarea> . K tomu můžeme použít textToTranslate vlastnost na event.target objekt. Zde event.target odkazuje na <form></form> prvek tak, jak je vykreslen v prohlížeči (jeho reprezentace v paměti).

Máme přístup k textToTranslate protože prohlížeč mu v paměti automaticky přiřadí všechna pole ve formuláři pomocí name pole atribut jako název vlastnosti. Když se podíváme zblízka na naše <textarea></textarea> , můžeme vidět, že má name atribut textToTranslate . Pokud toto změníme na pizza , napsali bychom event?.target?.pizza?.value místo toho.

S touto hodnotou uloženou v text proměnná, dále vytvoříme další proměnnou hasText který obsahuje kontrolu, abychom se ujistili, že naše text proměnná není prázdný řetězec (.trim() část zde "ořezává" všechny mezery v případě, že uživatel znovu a znovu mačká mezerník).

Pokud ve vstupu nemáme žádný text, chceme "namluvit" frázi "No, musíš něco říct!" Za předpokladu, že jsme udělali získat nějaký text, chceme jen "mluvit" to text hodnotu.

Všimněte si, že zde voláme na číslo component.methods.speak které jsme ještě nedefinovali. Použijeme methods joysticku feature (kde můžeme definovat různé funkce na naší komponentě).

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  methods: {
    speak: (text = '') => {  
      window.speechSynthesis.cancel();

      const message = new SpeechSynthesisUtterance(text);

      speechSynthesis.speak(message);
    },
  },
  events: {
    'submit form': (event, component) => {
      event.preventDefault();

      const text = event?.target?.textToTranslate?.value;
      const hasText = text.trim() !== '';

      if (!hasText) {
        return component.methods.speak('Well you have to say something!');
      }

      component.methods.speak(text);
    },
  },
  css: `...`,
  render: () => {
    return `
      <div>
        <h4>Text to Speech Translator</h4>
        <form>
          <textarea class="form-control" name="textToTranslate" placeholder="Type the text to speak here and then press Speak below."></textarea>
          <button class="btn btn-primary">Speak</button>
        </form>
        <div class="players"></div>
      </div>
    `;
  },
});

export default Index;

Nyní k té zábavnější části. Protože API syntézy řeči je implementováno v prohlížečích (viz kompatibilita – je docela dobrá), nemusíme nic instalovat ani importovat; celé API je globálně přístupné v prohlížeči.

Přidání methods objekt těsně nad naším events , přiřadíme speak metodu, kterou jsme zavolali z našeho submit form obsluha události.

Uvnitř toho není moc co dělat:

  1. V případě, že změníme text, který jsme zadali, a klikneme na tlačítko „Hovořit“ uprostřed přehrávání, chceme zavolat window.speechSynthesis.cancel() metoda, která sdělí rozhraní API, aby vyčistilo frontu přehrávání. Pokud to neuděláme, pouze přidá přehrávání do své fronty a bude pokračovat v přehrávání toho, co jsme mu předali (i po obnovení prohlížeče).
  2. Vytvořte instanci SpeechSynthesisUtterance() což je třída, která přebírá text, o kterém chceme mluvit.
  3. Předejte tuto instanci do speechSynthesis.speak() metoda.

A je to. Jakmile do pole napíšeme nějaký text a stiskneme „Speak“, váš prohlížeč (za předpokladu, že podporuje API) by měl začít blábolit.

Úžasný. Ale ještě jsme neskončili. Věřte nebo ne, API Synthesis Speech obsahuje také možnost používat různé hlasy. Dále budeme aktualizovat HTML vrácené naším render funkce zahrnout seznam hlasů, ze kterých si můžete vybrat, a aktualizovat methods.speak přijímat různé hlasy.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  state: {
    voices: [],
  },
  lifecycle: {
    onMount: (component) => {
      window.speechSynthesis.onvoiceschanged = () => {
        const voices = window.speechSynthesis.getVoices();
        component.setState({ voices });
      };
    },
  },
  methods: {
    getLanguageName: (language = '') => {
      if (language) {
        const regionNamesInEnglish = new Intl.DisplayNames(['en'], { type: 'region' });
        return regionNamesInEnglish.of(language?.split('-').pop());
      }

      return 'Unknown';
    },
    speak: (text = '', voice = '', component) => {  
      window.speechSynthesis.cancel();

      const message = new SpeechSynthesisUtterance(text);

      if (voice) {
        const selectedVoice = component?.state?.voices?.find((voiceOption) => voiceOption?.voiceURI === voice);
        message.voice = selectedVoice;
      }

      speechSynthesis.speak(message);
    },
  },
  events: {
    'submit form': (event, component) => {
      event.preventDefault();
      const text = event?.target?.textToTranslate?.value;
      const voice = event?.target?.voice?.value;
      const hasText = text.trim() !== '';

      if (!hasText) {
        return component.methods.speak('Well you have to say something!', voice);
      }

      component.methods.speak(text, voice);
    },
  },
  css: `
    h4 {
      border-bottom: 1px solid #eee;
      padding-bottom: 20px;
      margin-bottom: 40px;
    }

    select {
      margin-bottom: 20px;
    }

    textarea {
      margin-bottom: 40px;
    }
  `,
  render: ({ state, each, methods }) => {
    return `
      <div>
        <h4>Text to Speech Translator</h4>
        <form>
          <label class="form-label">Voice</label>
          <select class="form-control" name="voice">
            ${each(state?.voices, (voice) => {
              return `
                <option value="${voice.voiceURI}">${voice.name} (${methods.getLanguageName(voice.lang)})</option>
              `;
            })}
          </select>
          <textarea class="form-control" name="textToTranslate" placeholder="Type the text to speak here and then press Speak below."></textarea>
          <button class="btn btn-primary">Speak</button>
        </form>
        <div class="players"></div>
      </div>
    `;
  },
});

export default Index;

Abychom to urychlili, vypsali jsme zbytek kódu, který budeme potřebovat výše – pojďme si to projít.

Za prvé, abychom získali přístup k dostupným hlasům nabízeným rozhraním API, musíme počkat, až se načtou do prohlížeče. Nad naším methods jsme do naší komponenty lifecycle přidali další možnost a tomu jsme přiřadili onMount() funkce.

Tato funkce je volána joystickem ihned po připojení naší komponenty k DOM. Je to dobrý způsob, jak spouštět kód, který je závislý na uživatelském rozhraní, nebo, jako v tomto případě, způsob, jak naslouchat a zpracovávat globální události nebo události na úrovni prohlížeče (na rozdíl od událostí generovaných kódem HTML vykresleným naší komponentou).

Než však získáme hlasy, musíme naslouchat window.speechSynthesis.onvoiceschanged událost. Tato událost se spustí, jakmile jsou načteny hlasy (hovoříme o zlomcích sekund, ale dostatečně pomalu na to, abychom chtěli čekat na úrovni kódu).

Uvnitř onMount , tuto hodnotu přiřadíme funkci, která bude volána, když se událost spustí na window . Uvnitř této funkce voláme window.speechSynthesis.getVoices() funkce, která nám vrátí seznam objektů popisujících všechny dostupné hlasy. Takže to můžeme použít v našem uživatelském rozhraní, vezmeme component argument předán do onMount funkci a zavolejte její setState() funkce, předávání objektu s vlastností voices .

Protože chceme přiřadit hodnotu stavu voices na obsah proměnné const voices zde můžeme přeskočit psaní component.setState({ voices: voices }) a použijte pouze zkrácenou verzi.

Důležité :nahoře nad lifecycle možnost, všimněte si, že jsme přidali další možnost state nastavit na objekt a na tomto objektu vlastnost voices nastavit na prázdné pole. Toto je výchozí hodnota pro naše voices pole, které vstoupí do hry příště v našem render funkce.

Zde vidíme, že jsme aktualizovali náš render funkce k použití destrukcí JavaScriptu, abychom mohli „vytrhnout“ vlastnosti z argumentu, který je předán – instance komponenty – pro použití v HTML, které vracíme z funkce.

Zde zavádíme state , each a methods . state a methods jsou hodnoty, které jsme nastavili výše v komponentě. each je to, co je známé jako "funkce vykreslení" (nezaměňovat s funkcí přiřazenou k render možnost na naší komponentě).

Jak název napovídá, each() se používá pro opakování nebo opakování seznamu a vrácení nějakého HTML pro každou položku v tomto seznamu.

Zde můžeme vidět použití interpolace řetězců JavaScript (označeno ${} mezi otevřením a zavřením <select></select> tag), abychom předali naše volání na each() . Na each() , předáme seznam nebo pole (v tomto případě state.voices ) jako první argument a pro druhý, funkce, která bude volána a obdrží aktuální hodnotu, která se iteruje.

Uvnitř této funkce chceme vrátit nějaké HTML, které bude na výstupu pro každý položka v state.voices pole.

Protože jsme uvnitř <select></select> chceme vykreslit volbu výběru pro každý z hlasů, které jsme získali z API syntézy řeči. Jak jsme zmínili výše, každý voice je pouze objekt JavaScriptu s některými vlastnostmi. Ty, na kterých nám záleží, jsou voice.voiceURI (jedinečné ID/název hlasu) a voice.name (doslovné jméno mluvčího).

V neposlední řadě nám záleží také na tom, jakým jazykem se mluví. Toto je předáno jako lang na každém voice objekt ve formě standardního kódu jazyka ISO. Chcete-li získat „přátelskou“ reprezentaci (např. France nebo Germany ), musíme převést kód ISO. Zde voláme metodu getLanguageName() definované v našem methods objekt, který přijímá voice.lang hodnotu a převede ji na řetězec přátelský k lidem.

Když se podíváme na tuto funkci shora, vezmeme language in jako argument (řetězec, který jsme předali z našeho each() ) a pokud to není prázdná hodnota, vytvořte instanci Intl.DisplayNames() třída (Intl je další globální dostupný v prohlížeči), předáváme mu pole oblastí, které chceme podporovat (protože autor je škubánek, stačí en ) a v možnostech pro druhý argument nastavením názvu type na "region."

Výsledek je uložen v regionNamesInEnglish , voláme na tuto proměnnou .of() metodou, předáním language argument předán naší funkci. Když jej předáme, zavoláme .split('-') metodou na to říct „rozdělte tento řetězec na dva na - znak (to znamená, že předáme en-US dostali bychom pole jako ['en', 'US'] ) a poté ve výsledném poli zavolejte .pop() způsob, jak říct "vyndejte poslední položku a vraťte nám ji." V tomto případě je poslední položkou US jako řetězec, což je formát předpokládaný .of() metoda.

Ještě jeden krok. Všimněte si, že dole v našem submit form obslužný program události, přidali jsme proměnnou pro voice (pomocí stejné techniky k načtení její hodnoty jako u textToTranslate ) a poté jej předejte jako druhý argument našemu methods.speak() funkce.

Zpět do této funkce přidáme voice jako druhý argument spolu s component jako třetí (Joystick automaticky předal component jako poslední argument našich metod – byl by první, pokud nebyly předány žádné argumenty, nebo v tomto příkladu třetí, pokud jsou předány dva argumenty).

Do naší funkce jsme přidali if (voice) check a uvnitř toho spustíme .find() na state.voices pole říct „najděte nám objekt s .voiceURI hodnota se rovná voice argument, který jsme předali speak funkce (toto je en-US řetězec nebo voice.lang ). S tím jsme jen nastavili .voice na našem message (SpeechSynthesisUtterance instance třídy) a odtud přebírá API.

Hotovo! Pokud je vše na svém místě, měli bychom mít funkční překladač textu na řeč.

Zabalit

V tomto tutoriálu jsme se naučili, jak napsat komponentu pomocí @joystick.js/ui framework, který nám pomůže vytvořit rozhraní API pro převod textu na řeč. Naučili jsme se, jak naslouchat událostem DOM a jak využít rozhraní Speech Synthesis API v prohlížeči, aby za nás mluvilo. Také jsme se dozvěděli o Intl knihovna zabudovaná do prohlížeče, která nám pomáhá převést kód ISO pro řetězec data na jméno vhodné pro lidi. Nakonec jsme se naučili, jak dynamicky přepínat hlasy pomocí rozhraní Speech Synthesis API, abychom podporovali různé tóny a jazyky.