Учебник преобразования текста в речь с использованием 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
хранит английские голоса во внутреннем элементе SpeechServicefindVoice
поиск голоса по имени голосаupdateVoice
обновляет имя голоса в локальном хранилищеmakeRequest
загружает значения свойств из локального хранилища и создает запрос SpeechSynthesisUttencetoggle
заканчивает и снова говорит текст
Использование 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 для обновления локального хранилища и создания нового речевого запроса.