mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Try TTS in pipeline form (#16292)
This commit is contained in:
parent
a0263f25c4
commit
07cef18918
211
src/dialogs/tts-try/dialog-tts-try.ts
Normal file
211
src/dialogs/tts-try/dialog-tts-try.ts
Normal 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;
|
||||
}
|
||||
}
|
21
src/dialogs/tts-try/show-dialog-tts-try.ts
Normal file
21
src/dialogs/tts-try/show-dialog-tts-try.ts
Normal 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,
|
||||
});
|
||||
};
|
@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user