Try TTS in pipeline form (#16292)

This commit is contained in:
Paul Bottein 2023-04-24 19:39:16 +02:00 committed by GitHub
parent a0263f25c4
commit 07cef18918
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 301 additions and 20 deletions

View File

@ -0,0 +1,211 @@
import { mdiPlayCircleOutline } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { LocalStorage } from "../../common/decorators/local-storage";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-textarea";
import type { HaTextArea } from "../../components/ha-textarea";
import { convertTextToSpeech } from "../../data/tts";
import { HomeAssistant } from "../../types";
import { showAlertDialog } from "../generic/show-dialog-box";
import { TTSTryDialogParams } from "./show-dialog-tts-try";
import "../../components/ha-circular-progress";
@customElement("dialog-tts-try")
export class TTSTryDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _loadingExample = false;
@state() private _params?: TTSTryDialogParams;
@state() private _valid = false;
@query("#message") private _messageInput?: HaTextArea;
@LocalStorage("ttsTryMessages", false, false) private _messages?: Record<
string,
string
>;
private _audio?: HTMLAudioElement;
public showDialog(params: TTSTryDialogParams) {
this._params = params;
this._valid = Boolean(this._defaultMessage);
}
public closeDialog() {
this._params = undefined;
if (this._audio) {
this._audio.pause();
this._audio.removeEventListener("ended", this._audioEnded);
this._audio.removeEventListener("canplaythrough", this._audioCanPlay);
this._audio.removeEventListener("playing", this._audioPlaying);
this._audio.removeEventListener("error", this._audioError);
this._audio = undefined;
}
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private get _defaultMessage() {
const language = this._params!.language?.substring(0, 2);
const userLanguage = this.hass.locale.language.substring(0, 2);
// Load previous message in the right language
if (language && this._messages?.[language]) {
return this._messages[language];
}
// Only display example message if it's interface language
if (language === userLanguage) {
return this.hass.localize("ui.dialogs.tts-try.message_example");
}
return "";
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.tts-try.header")
)}
>
<ha-textarea
autogrow
id="message"
.label=${this.hass.localize("ui.dialogs.tts-try.message")}
.placeholder=${this.hass.localize(
"ui.dialogs.tts-try.message_placeholder"
)}
.value=${this._defaultMessage}
@input=${this._inputChanged}
?dialogInitialFocus=${!this._defaultMessage}
>
</ha-textarea>
${this._loadingExample
? html`
<ha-circular-progress
size="small"
active
alt=""
slot="primaryAction"
class="loading"
></ha-circular-progress>
`
: html`
<ha-button
?dialogInitialFocus=${Boolean(this._defaultMessage)}
slot="primaryAction"
.label=${this.hass.localize("ui.dialogs.tts-try.play")}
@click=${this._playExample}
.disabled=${!this._valid}
>
<ha-svg-icon
slot="icon"
.path=${mdiPlayCircleOutline}
></ha-svg-icon>
</ha-button>
`}
</ha-dialog>
`;
}
private async _inputChanged() {
this._valid = Boolean(this._messageInput?.value);
}
private async _playExample() {
const message = this._messageInput?.value;
if (!message) {
return;
}
const platform = this._params!.engine;
const language = this._params!.language;
const voice = this._params!.voice;
if (language) {
this._messages = {
...this._messages,
[language.substring(0, 2)]: message,
};
}
this._loadingExample = true;
if (!this._audio) {
this._audio = new Audio();
this._audio.addEventListener("ended", this._audioEnded);
this._audio.addEventListener("canplaythrough", this._audioCanPlay);
this._audio.addEventListener("playing", this._audioPlaying);
this._audio.addEventListener("error", this._audioError);
}
let url;
try {
const result = await convertTextToSpeech(this.hass, {
platform,
message,
language,
options: { voice },
});
url = result.path;
} catch (err: any) {
this._loadingExample = false;
showAlertDialog(this, {
text: `Unable to load example. ${err.error || err.body || err}`,
warning: true,
});
return;
}
this._audio.src = url;
}
private _audioCanPlay = () => {
this._audio?.play();
};
private _audioPlaying = () => {
this._loadingExample = false;
};
private _audioError = () => {
showAlertDialog(this, { title: "Error playing audio." });
this._loadingExample = false;
this._audio?.removeAttribute("src");
};
private _audioEnded = () => {
this._audio?.removeAttribute("src");
};
static get styles(): CSSResultGroup {
return css`
ha-dialog {
--mdc-dialog-max-width: 500px;
}
ha-textarea,
ha-select {
width: 100%;
}
ha-select {
margin-top: 8px;
}
.loading {
height: 36px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-tts-try": TTSTryDialog;
}
}

View File

@ -0,0 +1,21 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface TTSTryDialogParams {
engine: string;
language?: string;
voice?: string;
}
export const loadTTSTryDialog = () => import("./dialog-tts-try");
export const showTTSTryDialog = (
element: HTMLElement,
dialogParams: TTSTryDialogParams
): void => {
fireEvent(element, "show-dialog", {
addHistory: false,
dialogTag: "dialog-tts-try",
dialogImport: loadTTSTryDialog,
dialogParams,
});
};

View File

@ -1,10 +1,12 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { LocalizeKeys } from "../../../../common/translations/localize";
import { AssistPipeline } from "../../../../data/assist_pipeline";
import { HomeAssistant } from "../../../../types";
import "../../../../components/ha-button";
import "../../../../components/ha-form/ha-form";
import { AssistPipeline } from "../../../../data/assist_pipeline";
import { showTTSTryDialog } from "../../../../dialogs/tts-try/show-dialog-tts-try";
import { HomeAssistant } from "../../../../types";
@customElement("assist-pipeline-detail-tts")
export class AssistPipelineDetailTTS extends LitElement {
@ -38,8 +40,6 @@ export class AssistPipelineDetailTTS extends LitElement {
},
}
: { name: "", type: "constant" },
{ name: "", type: "constant" },
{
name: "tts_voice",
selector: {
@ -63,25 +63,61 @@ export class AssistPipelineDetailTTS extends LitElement {
protected render() {
return html`
<div class="section">
<div class="intro">
<h3>Text-to-speech</h3>
<p>
When you are using the pipeline as a voice assistant, the
text-to-speech engine turns the conversation text responses into
audio.
</p>
<div class="content">
<div class="intro">
<h3>Text-to-speech</h3>
<p>
When you are using the pipeline as a voice assistant, the
text-to-speech engine turns the conversation text responses into
audio.
</p>
</div>
<ha-form
.schema=${this._schema(
this.data?.language,
this._supportedLanguages
)}
.data=${this.data}
.hass=${this.hass}
.computeLabel=${this._computeLabel}
@supported-languages-changed=${this._supportedLanguagesChanged}
></ha-form>
</div>
${
this.data?.tts_engine
? html`<div class="footer">
<ha-button
.label=${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.pipeline.detail.try_tts"
)}
@click=${this._preview}
>
</ha-button>
</div>`
: nothing
}
</div>
<ha-form
.schema=${this._schema(this.data?.language, this._supportedLanguages)}
.data=${this.data}
.hass=${this.hass}
.computeLabel=${this._computeLabel}
@supported-languages-changed=${this._supportedLanguagesChanged}
></ha-form>
</div>
`;
}
private async _preview() {
if (!this.data) return;
const engine = this.data.tts_engine;
const language = this.data.tts_language || undefined;
const voice = this.data.tts_voice || undefined;
if (!engine) return;
showTTSTryDialog(this, {
engine,
language,
voice,
});
}
private _supportedLanguagesChanged(ev) {
this._supportedLanguages = ev.detail.value;
}
@ -91,7 +127,8 @@ export class AssistPipelineDetailTTS extends LitElement {
.section {
border: 1px solid var(--divider-color);
border-radius: 8px;
box-sizing: border-box;
}
.content {
padding: 16px;
}
.intro {
@ -110,6 +147,10 @@ export class AssistPipelineDetailTTS extends LitElement {
margin-top: 0;
margin-bottom: 0;
}
.footer {
border-top: 1px solid var(--divider-color);
padding: 8px 16px;
}
`;
}
}

View File

@ -1330,6 +1330,13 @@
"enter_code": {
"title": "Enter code",
"input_label": "Code"
},
"tts-try": {
"header": "Try Text to Speech",
"message": "Message",
"message_example": "Hello. How can I assist?",
"message_placeholder": "Enter a sentence to speak.",
"play": "Play"
}
},
"duration": {
@ -2035,6 +2042,7 @@
"update_assistant_action": "Update",
"add_assistant_title": "Add assistant",
"add_assistant_action": "Create",
"try_tts": "Try voice",
"form": {
"name": "Name",
"conversation_engine": "Conversation agent",