mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +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 { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { LocalizeKeys } from "../../../../common/translations/localize";
|
import { LocalizeKeys } from "../../../../common/translations/localize";
|
||||||
import { AssistPipeline } from "../../../../data/assist_pipeline";
|
import "../../../../components/ha-button";
|
||||||
import { HomeAssistant } from "../../../../types";
|
|
||||||
import "../../../../components/ha-form/ha-form";
|
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")
|
@customElement("assist-pipeline-detail-tts")
|
||||||
export class AssistPipelineDetailTTS extends LitElement {
|
export class AssistPipelineDetailTTS extends LitElement {
|
||||||
@ -38,8 +40,6 @@ export class AssistPipelineDetailTTS extends LitElement {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: { name: "", type: "constant" },
|
: { name: "", type: "constant" },
|
||||||
|
|
||||||
{ name: "", type: "constant" },
|
|
||||||
{
|
{
|
||||||
name: "tts_voice",
|
name: "tts_voice",
|
||||||
selector: {
|
selector: {
|
||||||
@ -63,25 +63,61 @@ export class AssistPipelineDetailTTS extends LitElement {
|
|||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="intro">
|
<div class="content">
|
||||||
<h3>Text-to-speech</h3>
|
<div class="intro">
|
||||||
<p>
|
<h3>Text-to-speech</h3>
|
||||||
When you are using the pipeline as a voice assistant, the
|
<p>
|
||||||
text-to-speech engine turns the conversation text responses into
|
When you are using the pipeline as a voice assistant, the
|
||||||
audio.
|
text-to-speech engine turns the conversation text responses into
|
||||||
</p>
|
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>
|
</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>
|
</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) {
|
private _supportedLanguagesChanged(ev) {
|
||||||
this._supportedLanguages = ev.detail.value;
|
this._supportedLanguages = ev.detail.value;
|
||||||
}
|
}
|
||||||
@ -91,7 +127,8 @@ export class AssistPipelineDetailTTS extends LitElement {
|
|||||||
.section {
|
.section {
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-sizing: border-box;
|
}
|
||||||
|
.content {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.intro {
|
.intro {
|
||||||
@ -110,6 +147,10 @@ export class AssistPipelineDetailTTS extends LitElement {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
.footer {
|
||||||
|
border-top: 1px solid var(--divider-color);
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1330,6 +1330,13 @@
|
|||||||
"enter_code": {
|
"enter_code": {
|
||||||
"title": "Enter code",
|
"title": "Enter code",
|
||||||
"input_label": "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": {
|
"duration": {
|
||||||
@ -2035,6 +2042,7 @@
|
|||||||
"update_assistant_action": "Update",
|
"update_assistant_action": "Update",
|
||||||
"add_assistant_title": "Add assistant",
|
"add_assistant_title": "Add assistant",
|
||||||
"add_assistant_action": "Create",
|
"add_assistant_action": "Create",
|
||||||
|
"try_tts": "Try voice",
|
||||||
"form": {
|
"form": {
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"conversation_engine": "Conversation agent",
|
"conversation_engine": "Conversation agent",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user