mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 01:06:35 +00:00
Allow configuring default TTS voice for cloud (#8148)
This commit is contained in:
parent
41f8b0d19b
commit
75f228418d
@ -41,6 +41,7 @@ export interface CloudPreferences {
|
|||||||
};
|
};
|
||||||
alexa_report_state: boolean;
|
alexa_report_state: boolean;
|
||||||
google_report_state: boolean;
|
google_report_state: boolean;
|
||||||
|
tts_default_voice: [string, string];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CloudStatusLoggedIn = CloudStatusBase & {
|
export type CloudStatusLoggedIn = CloudStatusBase & {
|
||||||
@ -113,6 +114,7 @@ export const updateCloudPref = (
|
|||||||
google_report_state?: CloudPreferences["google_report_state"];
|
google_report_state?: CloudPreferences["google_report_state"];
|
||||||
google_default_expose?: CloudPreferences["google_default_expose"];
|
google_default_expose?: CloudPreferences["google_default_expose"];
|
||||||
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
|
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
|
||||||
|
tts_default_voice?: CloudPreferences["tts_default_voice"];
|
||||||
}
|
}
|
||||||
) =>
|
) =>
|
||||||
hass.callWS({
|
hass.callWS({
|
||||||
@ -144,3 +146,10 @@ export const updateCloudAlexaEntityConfig = (
|
|||||||
entity_id: entityId,
|
entity_id: entityId,
|
||||||
...values,
|
...values,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export interface CloudTTSInfo {
|
||||||
|
languages: Array<[string, string]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCloudTTSInfo = (hass: HomeAssistant) =>
|
||||||
|
hass.callWS<CloudTTSInfo>({ type: "cloud/tts/info" });
|
||||||
|
12
src/data/tts.ts
Normal file
12
src/data/tts.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
export const convertTextToSpeech = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
data: {
|
||||||
|
platform: string;
|
||||||
|
message: string;
|
||||||
|
cache?: boolean;
|
||||||
|
language?: string;
|
||||||
|
options?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
) => hass.callApi<{ url: string }>("POST", "tts_get_url", data);
|
@ -16,6 +16,7 @@ import "../../ha-config-section";
|
|||||||
import "./cloud-alexa-pref";
|
import "./cloud-alexa-pref";
|
||||||
import "./cloud-google-pref";
|
import "./cloud-google-pref";
|
||||||
import "./cloud-remote-pref";
|
import "./cloud-remote-pref";
|
||||||
|
import "./cloud-tts-pref";
|
||||||
import "./cloud-webhooks";
|
import "./cloud-webhooks";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -133,6 +134,12 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
|||||||
dir="[[_rtlDirection]]"
|
dir="[[_rtlDirection]]"
|
||||||
></cloud-remote-pref>
|
></cloud-remote-pref>
|
||||||
|
|
||||||
|
<cloud-tts-pref
|
||||||
|
hass="[[hass]]"
|
||||||
|
cloud-status="[[cloudStatus]]"
|
||||||
|
dir="[[_rtlDirection]]"
|
||||||
|
></cloud-tts-pref>
|
||||||
|
|
||||||
<cloud-alexa-pref
|
<cloud-alexa-pref
|
||||||
hass="[[hass]]"
|
hass="[[hass]]"
|
||||||
cloud-status="[[cloudStatus]]"
|
cloud-status="[[cloudStatus]]"
|
||||||
|
310
src/panels/config/cloud/account/cloud-tts-pref.ts
Normal file
310
src/panels/config/cloud/account/cloud-tts-pref.ts
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
|
||||||
|
import "@polymer/paper-item/paper-item";
|
||||||
|
import "@polymer/paper-listbox/paper-listbox";
|
||||||
|
import "@material/mwc-button";
|
||||||
|
import { mdiPlayCircleOutline } from "@mdi/js";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
internalProperty,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import "../../../../components/ha-card";
|
||||||
|
import "../../../../components/ha-switch";
|
||||||
|
import "../../../../components/ha-svg-icon";
|
||||||
|
import {
|
||||||
|
CloudStatusLoggedIn,
|
||||||
|
CloudTTSInfo,
|
||||||
|
getCloudTTSInfo,
|
||||||
|
updateCloudPref,
|
||||||
|
} from "../../../../data/cloud";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import { convertTextToSpeech } from "../../../../data/tts";
|
||||||
|
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||||
|
import { translationMetadata } from "../../../../resources/translations-metadata";
|
||||||
|
import { caseInsensitiveCompare } from "../../../../common/string/compare";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
|
||||||
|
@customElement("cloud-tts-pref")
|
||||||
|
export class CloudTTSPref extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public cloudStatus?: CloudStatusLoggedIn;
|
||||||
|
|
||||||
|
@internalProperty() private loadingExample = false;
|
||||||
|
|
||||||
|
@internalProperty() private savingPreferences = false;
|
||||||
|
|
||||||
|
@internalProperty() private ttsInfo?: CloudTTSInfo;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this.cloudStatus) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
const languages = this.getLanguages(this.ttsInfo);
|
||||||
|
const defaultVoice = this.cloudStatus.prefs.tts_default_voice;
|
||||||
|
const genders = this.getSupportedGenders(defaultVoice[0], this.ttsInfo);
|
||||||
|
const defaultLangEntryIndex = languages.findIndex(
|
||||||
|
([lang]) => lang === defaultVoice[0]
|
||||||
|
);
|
||||||
|
const defaultGenderEntryIndex = genders.findIndex(
|
||||||
|
([gender]) => gender === defaultVoice[1]
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-card
|
||||||
|
header=${this.hass.localize("ui.panel.config.cloud.account.tts.title")}
|
||||||
|
>
|
||||||
|
<div class="example">
|
||||||
|
<mwc-button
|
||||||
|
@click=${this._playExample}
|
||||||
|
.disabled=${this.loadingExample}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiPlayCircleOutline}></ha-svg-icon>
|
||||||
|
Example
|
||||||
|
</mwc-button>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.tts.info",
|
||||||
|
"service",
|
||||||
|
'"tts.cloud_say"'
|
||||||
|
)}
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
<paper-dropdown-menu-light
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.tts.default_language"
|
||||||
|
)}
|
||||||
|
.value=${defaultLangEntryIndex !== -1
|
||||||
|
? languages[defaultLangEntryIndex][1]
|
||||||
|
: ""}
|
||||||
|
.disabled=${this.savingPreferences}
|
||||||
|
@iron-select=${this._handleLanguageChange}
|
||||||
|
>
|
||||||
|
<paper-listbox
|
||||||
|
slot="dropdown-content"
|
||||||
|
.selected=${defaultLangEntryIndex}
|
||||||
|
>
|
||||||
|
${languages.map(
|
||||||
|
([key, label]) =>
|
||||||
|
html`<paper-item .value=${key}>${label}</paper-item>`
|
||||||
|
)}
|
||||||
|
</paper-listbox>
|
||||||
|
</paper-dropdown-menu-light>
|
||||||
|
|
||||||
|
<paper-dropdown-menu-light
|
||||||
|
.value=${defaultGenderEntryIndex !== -1
|
||||||
|
? genders[defaultGenderEntryIndex][1]
|
||||||
|
: ""}
|
||||||
|
.disabled=${this.savingPreferences}
|
||||||
|
@iron-select=${this._handleGenderChange}
|
||||||
|
>
|
||||||
|
<paper-listbox
|
||||||
|
slot="dropdown-content"
|
||||||
|
.selected=${defaultGenderEntryIndex}
|
||||||
|
>
|
||||||
|
${genders.map(
|
||||||
|
([key, label]) =>
|
||||||
|
html`<paper-item .value=${key}>${label}</paper-item>`
|
||||||
|
)}
|
||||||
|
</paper-listbox>
|
||||||
|
</paper-dropdown-menu-light>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
getCloudTTSInfo(this.hass).then((info) => {
|
||||||
|
this.ttsInfo = info;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps) {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (changedProps.has("cloudStatus")) {
|
||||||
|
this.savingPreferences = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLanguages = memoizeOne((info?: CloudTTSInfo) => {
|
||||||
|
const languages: Array<[string, string]> = [];
|
||||||
|
|
||||||
|
if (!info) {
|
||||||
|
return languages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const [lang] of info.languages) {
|
||||||
|
if (seen.has(lang)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(lang);
|
||||||
|
|
||||||
|
let label = lang;
|
||||||
|
|
||||||
|
if (lang in translationMetadata.translations) {
|
||||||
|
label = translationMetadata.translations[lang].nativeName;
|
||||||
|
} else {
|
||||||
|
const [langFamily, dialect] = lang.split("-");
|
||||||
|
if (langFamily in translationMetadata.translations) {
|
||||||
|
label = `${translationMetadata.translations[langFamily].nativeName}`;
|
||||||
|
|
||||||
|
if (langFamily.toLowerCase() !== dialect.toLowerCase()) {
|
||||||
|
label += ` (${dialect})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
languages.push([lang, label]);
|
||||||
|
}
|
||||||
|
return languages.sort((a, b) => caseInsensitiveCompare(a[1], b[1]));
|
||||||
|
});
|
||||||
|
|
||||||
|
private getSupportedGenders = memoizeOne(
|
||||||
|
(language: string, info?: CloudTTSInfo) => {
|
||||||
|
const genders: Array<[string, string]> = [];
|
||||||
|
|
||||||
|
if (!info) {
|
||||||
|
return genders;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [curLang, gender] of info.languages) {
|
||||||
|
if (curLang === language) {
|
||||||
|
genders.push([
|
||||||
|
gender,
|
||||||
|
this.hass.localize(`ui.panel.config.cloud.account.tts.${gender}`) ||
|
||||||
|
gender,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return genders.sort((a, b) => caseInsensitiveCompare(a[1], b[1]));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async _playExample() {
|
||||||
|
this.loadingExample = true;
|
||||||
|
const defaultVoice = this.cloudStatus!.prefs.tts_default_voice;
|
||||||
|
// Our example sentence is English. If user uses English voice, use that
|
||||||
|
// for example.
|
||||||
|
let language;
|
||||||
|
let gender;
|
||||||
|
if (defaultVoice[0].split("-")[0] === "en") {
|
||||||
|
language = defaultVoice[0];
|
||||||
|
gender = defaultVoice[1];
|
||||||
|
} else {
|
||||||
|
language = "en-US";
|
||||||
|
gender = "female";
|
||||||
|
}
|
||||||
|
|
||||||
|
let url;
|
||||||
|
try {
|
||||||
|
const result = await convertTextToSpeech(this.hass, {
|
||||||
|
platform: "cloud",
|
||||||
|
message: `Hello ${
|
||||||
|
this.hass.user!.name
|
||||||
|
}, you can play any text on any supported media player!`,
|
||||||
|
language,
|
||||||
|
options: { gender },
|
||||||
|
});
|
||||||
|
url = result.url;
|
||||||
|
} catch (err) {
|
||||||
|
this.loadingExample = false;
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(err);
|
||||||
|
showAlertDialog(this, {
|
||||||
|
text: `Unable to load example. ${err}`,
|
||||||
|
warning: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const audio = new Audio(url);
|
||||||
|
audio.play();
|
||||||
|
audio.addEventListener("playing", () => {
|
||||||
|
this.loadingExample = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _handleLanguageChange(ev) {
|
||||||
|
this.savingPreferences = true;
|
||||||
|
const langLabel = ev.currentTarget.value;
|
||||||
|
const languages = this.getLanguages(this.ttsInfo);
|
||||||
|
const language = languages.find((item) => item[1] === langLabel)![0];
|
||||||
|
|
||||||
|
const curGender = this.cloudStatus!.prefs.tts_default_voice[1];
|
||||||
|
const genders = this.getSupportedGenders(language, this.ttsInfo);
|
||||||
|
const newGender = genders.find((item) => item[0] === curGender)
|
||||||
|
? curGender
|
||||||
|
: genders[0][0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateCloudPref(this.hass, {
|
||||||
|
tts_default_voice: [language, newGender],
|
||||||
|
});
|
||||||
|
fireEvent(this, "ha-refresh-cloud-status");
|
||||||
|
} catch (err) {
|
||||||
|
this.savingPreferences = false;
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(err);
|
||||||
|
showAlertDialog(this, {
|
||||||
|
text: `Unable to save default language. ${err}`,
|
||||||
|
warning: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _handleGenderChange(ev) {
|
||||||
|
this.savingPreferences = true;
|
||||||
|
const language = this.cloudStatus!.prefs.tts_default_voice[0];
|
||||||
|
const genderLabel = ev.currentTarget.value;
|
||||||
|
const genders = this.getSupportedGenders(language, this.ttsInfo);
|
||||||
|
const gender = genders.find((item) => item[1] === genderLabel)![0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateCloudPref(this.hass, {
|
||||||
|
tts_default_voice: [language, gender],
|
||||||
|
});
|
||||||
|
fireEvent(this, "ha-refresh-cloud-status");
|
||||||
|
} catch (err) {
|
||||||
|
this.savingPreferences = false;
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(err);
|
||||||
|
showAlertDialog(this, {
|
||||||
|
text: `Unable to save default gender. ${err}`,
|
||||||
|
warning: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.example {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 16px;
|
||||||
|
}
|
||||||
|
:host([dir="rtl"]) .example {
|
||||||
|
right: auto;
|
||||||
|
left: 24px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"cloud-tts-pref": CloudTTSPref;
|
||||||
|
}
|
||||||
|
}
|
@ -1655,6 +1655,13 @@
|
|||||||
"connected": "Connected",
|
"connected": "Connected",
|
||||||
"not_connected": "Not Connected",
|
"not_connected": "Not Connected",
|
||||||
"fetching_subscription": "Fetching subscription…",
|
"fetching_subscription": "Fetching subscription…",
|
||||||
|
"tts": {
|
||||||
|
"title": "Text to Speech",
|
||||||
|
"info": "Bring personality to your home by having it speak to you by using our Text-to-Speech services. You can use this in automations and scripts by using the {service} service.",
|
||||||
|
"default_language": "Default language to use",
|
||||||
|
"male": "Male",
|
||||||
|
"female": "Female"
|
||||||
|
},
|
||||||
"remote": {
|
"remote": {
|
||||||
"title": "Remote Control",
|
"title": "Remote Control",
|
||||||
"access_is_being_prepared": "Remote access is being prepared. We will notify you when it's ready.",
|
"access_is_being_prepared": "Remote access is being prepared. We will notify you when it's ready.",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user