DevGang
Авторизоваться

Учебник преобразования текста в речь с использованием RxJS и Angular

Это 23-й день конкурса Wes Bos JavaScript 30, и мы собираемся использовать RxJS и Angular для создания учебника по преобразованию текста в речь на английском языке. Web Speech API предоставляет интерфейсы для запроса речи и преобразования текста в речь в соответствии с выбранным голосом.

В этом сообщении в блоге мы описываем, как использовать RxJS из Event для прослушивания события изменения элементов управления вводом и обновления свойств объекта SpeechSynthesisUtterance. Интерфейс SpeechSynthesisUtterance создает речевой запрос и вызывает интерфейс синтеза речи, чтобы произнести текст и преобразовать английский текст в речь.

Создание нового проекта Angular

ng generate application day23-speech-synthesis

Создадим модуль речевых функций. Сначала мы создаем модуль функции речи и импортируем его в AppModule. Функциональный модуль инкапсулирует компонент синтеза речи, компонент текста речи и компонент голоса речи.

Импорт SpeechModule в AppModule

// speech.module.ts

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { SpeechSynthesisComponent } from './speech-synthesis/speech-synthesis.component';
import { SpeechTextComponent } from './speech-text/speech-text.component';
import { SpeechVoiceComponent } from './speech-voice/speech-voice.component';

@NgModule({
  declarations: [
    SpeechSynthesisComponent,
    SpeechVoiceComponent,
    SpeechTextComponent
  ],
  imports: [
    CommonModule,
    FormsModule,
  ],
  exports: [
    SpeechSynthesisComponent
  ]
})
export class SpeechModule { }
// app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { SpeechModule } from './speech';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    SpeechModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Объявление речевых компонентов в функциональном модуле

В модуле функций речи мы объявляем три Angular компонента, SpeechSynthesisComponent, SpeechTextComponent и SpeechVoiceComponent для создания приложения преобразования текста в речь на английском языке.

src/app
├── app.component.ts
├── app.module.ts
└── speech
    ├── index.ts
    ├── interfaces
    │   └── speech.interface.ts
    ├── services
    │   └── speech.service.ts
    ├── speech-synthesis
    │   └── speech-synthesis.component.ts
    ├── speech-text
    │   └── speech-text.component.ts
    ├── speech-voice
    │   └── speech-voice.component.ts
    └── speech.module.ts

SpeechComponent действует как оболочка, заключающая в себе SpeechTextComponent и SpeechVoiceComponent. К вашему сведению, <app-speech-synchronous> — это тег TimerComponent.

// speech-synthesis.component.ts

import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
  selector: 'app-speech-synthesis',
  template: `
    <div class="voiceinator">
      <h1>The Voiceinator 5000</h1>
      <app-speech-voice></app-speech-voice>
      <app-speech-text></app-speech-text>
    </div>`,
  styles: [` ...omitted due to brevity... `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SpeechSynthesisComponent {}

SpeehVoiceComponent инкапсулирует элементы управления вводом для изменения скорости, высоты тона и голоса речи, тогда как SpeechTextComponent состоит из текстовой области, кнопок произнесения и остановки, чтобы решить, что и когда говорить.

// speech-voice.component.ts

import { ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Observable, of } from 'rxjs';
import { SpeechService } from '../services/speech.service';

@Component({
  selector: 'app-speech-voice',
  template: `
    <ng-container>
      <select name="voice" id="voices" #voices>
        <option *ngFor="let voice of voices$ | async" [value]="voice.name">{{voice.name}} ({{voice.lang}})</option>
      </select>
      <label for="rate">Rate:</label>
      <input name="rate" type="range" min="0" max="3" value="1" step="0.1" #rate>
      <label for="pitch">Pitch:</label>
      <input name="pitch" type="range" min="0" max="2" step="0.1" #pitch value="1">
    </ng-container>
  `,
  styles: [...omitted due to brevity...],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SpeechVoiceComponent implements OnInit, OnDestroy {
  @ViewChild('rate', { static: true, read: ElementRef })
  rate!: ElementRef<HTMLInputElement>;

  @ViewChild('pitch', { static: true, read: ElementRef })
  pitch!: ElementRef<HTMLInputElement>;

  @ViewChild('voices', { static: true, read: ElementRef })
  voiceDropdown!: ElementRef<HTMLSelectElement>;

  voices$!: Observable<SpeechSynthesisVoice[]>;

  constructor(private speechService: SpeechService) { }

  ngOnInit(): void {
    this.voices$ = of([]);
  }

  ngOnDestroy(): void {}
}
// speech-text.component.ts

import { ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Subject, Subscription, fromEvent, map, merge, tap } from 'rxjs';
import { SpeechService } from '../services/speech.service';

@Component({
  selector: 'app-speech-text',
  template: `
    <ng-container>
      <textarea name="text" [(ngModel)]="msg" (change)="textChanged$.next()"></textarea>
      <button id="stop" #stop>Stop!</button>
      <button id="speak" #speak>Speak</button>
    </ng-container>
  `,
  styles: [...omitted due to brevity...],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SpeechTextComponent implements OnInit, OnDestroy {
  @ViewChild('stop', { static: true, read: ElementRef })
  btnStop!: ElementRef<HTMLButtonElement>;

  @ViewChild('speak', { static: true, read: ElementRef })
  btnSpeak!: ElementRef<HTMLButtonElement>;

  textChange$ = new Subject<void>();
  msg = 'Hello! I love JavaScript 👍';

  constructor(private speechService: SpeechService) { }

  ngOnInit(): void {
    this.speechService.updateSpeech({ name: 'text', value: this.msg });
  }

  ngOnDestroy(): void {}
}

Затем мы удаляем стандартные коды в AppComponent и визуализирую SpeechSynthesisComponent во встроенном шаблоне.

// app.component.ts

import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';

@Component({
  selector: 'app-root',
  template: '<app-speech-synthesis></app-speech-synthesis>',
  styles: [`
    :host {
      display: block;
    }
  `]
})
export class AppComponent {
  title = 'Day 23 Speech Synthesis';

  constructor(titleService: Title) {
    titleService.setTitle(this.title);
  }
}

Добавление речевого сервиса для преобразования текста в речь

Мы создаем общий сервис, чтобы добавить слой поверх Web Speech API, чтобы делать речевой запрос и произносить тексты в соответствии с выбранным голосом, скоростью и высотой тона.

// speech.service.ts

import { Injectable } from '@angular/core';
import { SpeechProperties } from '../interfaces/speech.interface';

@Injectable({
  providedIn: 'root'
})
export class SpeechService {
  private voices: SpeechSynthesisVoice[] = [];

  updateSpeech(property: SpeechProperties): void {
    const { name, value } = property;
    if ((name === 'text')) {
      localStorage.setItem(name, value);
    } else if (['rate', 'pitch'].includes(name)) {
      localStorage.setItem(name, `${value}`);
    }
    this.toggle();
  }

  setVoices(voices: SpeechSynthesisVoice[]): void {
    this.voices = voices;
  }

  updateVoice(voiceName: string): void {
    localStorage.setItem('voice', voiceName);
    this.toggle();
  }

  private findVoice(voiceName: string): SpeechSynthesisVoice | null {
    const voice = this.voices.find(v => v.name === voiceName);
    return voice ? voice : null;
  }

  toggle(startOver = true): void {
    const speech = this.makeRequest();
    speechSynthesis.cancel();
    if (startOver) {
      speechSynthesis.speak(speech);
    }
  }

  private makeRequest() {
    const speech = new SpeechSynthesisUtterance();
    speech.text = localStorage.getItem('text') || '';
    speech.rate = +(localStorage.getItem('rate') || '1');
    speech.pitch = +(localStorage.getItem('pitch') || '1');
    const voice = this.findVoice(localStorage.getItem('voice') || '');
    if (voice) {
      speech.voice = voice;
    }
    return speech;
  }
}
  • updateSpeech обновляет высоту звука, скорость или текст в локальном хранилище
  • setVoices хранит английские голоса во внутреннем элементе SpeechService
  • findVoice поиск голоса по имени голоса
  • updateVoice обновляет имя голоса в локальном хранилище
  • makeRequest загружает значения свойств из локального хранилища и создает запрос SpeechSynthesisUttence
  • toggle заканчивает и снова говорит текст

Использование RxJS и Angular для реализации SpeechVoiceComponent

Мы собираемся определить Observable для извлечения английских голосов и заполнения раскрывающегося списка голосов.

Используйте ViewChild для получения ссылок на диапазоны ввода и раскрывающийся список голосов.

@ViewChild('rate', { static: true, read: ElementRef })
rate!: ElementRef<HTMLInputElement>;

@ViewChild('pitch', { static: true, read: ElementRef })
pitch!: ElementRef<HTMLInputElement>;

@ViewChild('voices', { static: true, read: ElementRef })
voiceDropdown!: ElementRef<HTMLSelectElement>;
Declare subscription instance member and unsubscribe in ngDestroy()

subscription = new Subscription();

ngOnDestroy(): void {
    this.subscription.unsubscribe();
}

Объявите voices$ Observable и заполните выпадающий список опций в ngOnInit().

ngOnInit(): void {
    this.voices$ = fromEvent(speechSynthesis, 'voiceschanged')
       .pipe(
          map(() => speechSynthesis.getVoices().filter(voice => voice.lang.includes('en'))),
          tap((voices) => this.speechService.setVoices(voices)),
       );
}

Во встроенном шаблоне используйте асинхронный канал для разрешения this.voices$ и заполнения параметров в раскрывающемся списке голосов.

<select name="voice" id="voices" #voices>
     <option *ngFor="let voice of voices$ | async" [value]="voice.name">{{voice.name}} ({{voice.lang}})</option>
</select>

Используйте fromEvent для прослушивания события изменения раскрывающегося списка, обновления имени голоса в локальном хранилище и произнесения текстов.

const voiceDropdownNative = this.voiceDropdown.nativeElement;
this.subscription.add(
   fromEvent(this.voiceDropdown.nativeElement, 'change')
     .pipe(
        tap(() => this.speechService.updateVoice(voiceDropdownNative.value))
     ).subscribe()
);

Точно так же используйте fromEvent для прослушивания события изменения входных диапазонов, частоты обновления и высоты тона в локальном хранилище и произнесения текстов.

const rateNative = this.rate.nativeElement;
const pitchNative = this.pitch.nativeElement;
this.subscription.add(
      merge(fromEvent(rateNative, 'change'), fromEvent(pitchNative, 'change'))
        .pipe(
          map((e) => e.target as HTMLInputElement),
          map((e) => ({ name: e.name as 'rate' | 'pitch', value: e.value })),
          tap((property) => this.speechService.updateSpeech(property))
      ).subscribe()
);

Использование RxJS и Angular для реализации SpeechTextComponent

Используйте ViewChild для получения ссылок на текстовую область и кнопки

@ViewChild('stop', { static: true, read: ElementRef })
btnStop!: ElementRef<HTMLButtonElement>;

@ViewChild('speak', { static: true, read: ElementRef })
btnSpeak!: ElementRef<HTMLButtonElement>;
Declare subscription instance member and unsubscribe in ngDestroy()

subscription = new Subscription();

ngOnDestroy(): void {
    this.subscription.unsubscribe();
}

Объявите тему textChanged$, которая выдает значение при изменении текста в текстовой области, и уберите табуляцию, чтобы потерять фокус.

// speech-text.component.ts

textChanged$ = new Subject<void>();

ngOnInit() {
    this.subscription.add(
       this.textChanged$
         .pipe(tap(() => this.speechService.updateSpeech({ name: 'text', value: this.msg })))
         .subscribe()
    );
}

Субъект вызывает SpeechService, чтобы обновить сообщение в локальном хранилище и произнести текст.

Точно так же используйте fromEvent для прослушивания событий нажатия кнопок. Когда нажимается кнопка «Говорить», поток останавливается и начинает произносить текст в текстовой области. При нажатии кнопки отмены поток немедленно останавливает речь.

ngOnInit() {
    const btnStop$ = fromEvent(this.btnStop.nativeElement, 'click').pipe(map(() => false));
    const btnSpeak$ = fromEvent(this.btnSpeak.nativeElement, 'click').pipe(map(() => true));
    this.subscription.add(
      merge(btnStop$, btnSpeak$)
        .pipe(tap(() => this.speechService.updateSpeech({ name: 'text', value: this.msg })))
        .subscribe((startOver) => this.speechService.toggle(startOver))
    );
}
  • fromEvent(this.btnStop.nativeElement, 'click').pipe(map(() => false)) – кнопка остановки сопоставляется с false, чтобы немедленно закончить речь
  • fromEvent(this.btnSpeak.nativeElement, 'click').pipe(map(() => true)) – кнопка говорить отображает true, чтобы остановить и перезапустить речь

Пример готов, и у нас есть страница, которая понимает английские слова и может их произносить.

Последние мысли

В этом посте мы показали, как использовать RxJS и Angular для создания приложения преобразования текста в речь, которое читает и произносит тексты на английском языке. Web Speech API поддерживает различные разговорные языки и голоса для произнесения текстов с различными параметрами (скорость, высота тона, текст и громкость). Дочерние компоненты создают Observables для передачи значений в общий SpeechService для обновления локального хранилища и создания нового речевого запроса.

#Angular #TypeScript #RxJS
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу