mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-21 16:26:43 +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;
|
||||
google_report_state: boolean;
|
||||
tts_default_voice: [string, string];
|
||||
}
|
||||
|
||||
export type CloudStatusLoggedIn = CloudStatusBase & {
|
||||
@ -113,6 +114,7 @@ export const updateCloudPref = (
|
||||
google_report_state?: CloudPreferences["google_report_state"];
|
||||
google_default_expose?: CloudPreferences["google_default_expose"];
|
||||
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
|
||||
tts_default_voice?: CloudPreferences["tts_default_voice"];
|
||||
}
|
||||
) =>
|
||||
hass.callWS({
|
||||
@ -144,3 +146,10 @@ export const updateCloudAlexaEntityConfig = (
|
||||
entity_id: entityId,
|
||||
...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-google-pref";
|
||||
import "./cloud-remote-pref";
|
||||
import "./cloud-tts-pref";
|
||||
import "./cloud-webhooks";
|
||||
|
||||
/*
|
||||
@ -133,6 +134,12 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
dir="[[_rtlDirection]]"
|
||||
></cloud-remote-pref>
|
||||
|
||||
<cloud-tts-pref
|
||||
hass="[[hass]]"
|
||||
cloud-status="[[cloudStatus]]"
|
||||
dir="[[_rtlDirection]]"
|
||||
></cloud-tts-pref>
|
||||
|
||||
<cloud-alexa-pref
|
||||
hass="[[hass]]"
|
||||
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",
|
||||
"not_connected": "Not Connected",
|
||||
"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": {
|
||||
"title": "Remote Control",
|
||||
"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