Update voice wizard (#24750)

* Update pipeline step, add option for speech-to-phrase addon

* adjust to core changes

* Update conversation.ts

* Update voice-assistant-setup-step-pipeline.ts

* Update voice-assistant-setup-dialog.ts

* review
This commit is contained in:
Bram Kragten 2025-03-26 14:05:51 +01:00 committed by GitHub
parent 7cc6397324
commit f6467a35db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 776 additions and 394 deletions

View File

@ -13,6 +13,49 @@ import "./ha-list-item";
import "./ha-select"; import "./ha-select";
import type { HaSelect } from "./ha-select"; import type { HaSelect } from "./ha-select";
export const getLanguageOptions = (
languages: string[],
nativeName: boolean,
noSort: boolean,
locale?: FrontendLocaleData
) => {
let options: { label: string; value: string }[] = [];
if (nativeName) {
const translations = translationMetadata.translations;
options = languages.map((lang) => {
let label = translations[lang]?.nativeName;
if (!label) {
try {
// this will not work if Intl.DisplayNames is polyfilled, it will return in the language of the user
label = new Intl.DisplayNames(lang, {
type: "language",
fallback: "code",
}).of(lang)!;
} catch (_err) {
label = lang;
}
}
return {
value: lang,
label,
};
});
} else if (locale) {
options = languages.map((lang) => ({
value: lang,
label: formatLanguageCode(lang, locale),
}));
}
if (!noSort && locale) {
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, locale.language)
);
}
return options;
};
@customElement("ha-language-picker") @customElement("ha-language-picker")
export class HaLanguagePicker extends LitElement { export class HaLanguagePicker extends LitElement {
@property() public value?: string; @property() public value?: string;
@ -68,6 +111,7 @@ export class HaLanguagePicker extends LitElement {
const languageOptions = this._getLanguagesOptions( const languageOptions = this._getLanguagesOptions(
this.languages ?? this._defaultLanguages, this.languages ?? this._defaultLanguages,
this.nativeName, this.nativeName,
this.noSort,
this.hass?.locale this.hass?.locale
); );
const selectedItemIndex = languageOptions.findIndex( const selectedItemIndex = languageOptions.findIndex(
@ -82,45 +126,7 @@ export class HaLanguagePicker extends LitElement {
} }
} }
private _getLanguagesOptions = memoizeOne( private _getLanguagesOptions = memoizeOne(getLanguageOptions);
(languages: string[], nativeName: boolean, locale?: FrontendLocaleData) => {
let options: { label: string; value: string }[] = [];
if (nativeName) {
const translations = translationMetadata.translations;
options = languages.map((lang) => {
let label = translations[lang]?.nativeName;
if (!label) {
try {
// this will not work if Intl.DisplayNames is polyfilled, it will return in the language of the user
label = new Intl.DisplayNames(lang, {
type: "language",
fallback: "code",
}).of(lang)!;
} catch (_err) {
label = lang;
}
}
return {
value: lang,
label,
};
});
} else if (locale) {
options = languages.map((lang) => ({
value: lang,
label: formatLanguageCode(lang, locale),
}));
}
if (!this.noSort && locale) {
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, locale.language)
);
}
return options;
}
);
private _computeDefaultLanguageOptions() { private _computeDefaultLanguageOptions() {
this._defaultLanguages = Object.keys(translationMetadata.translations); this._defaultLanguages = Object.keys(translationMetadata.translations);
@ -130,6 +136,7 @@ export class HaLanguagePicker extends LitElement {
const languageOptions = this._getLanguagesOptions( const languageOptions = this._getLanguagesOptions(
this.languages ?? this._defaultLanguages, this.languages ?? this._defaultLanguages,
this.nativeName, this.nativeName,
this.noSort,
this.hass?.locale this.hass?.locale
); );

View File

@ -124,3 +124,22 @@ export const debugAgent = (
language, language,
device_id, device_id,
}); });
export interface LanguageScore {
cloud: number;
focused_local: number;
full_local: number;
}
export type LanguageScores = Record<string, LanguageScore>;
export const getLanguageScores = (
hass: HomeAssistant,
language?: string,
country?: string
): Promise<{ languages: LanguageScores; preferred_language: string | null }> =>
hass.callWS({
type: "conversation/agent/homeassistant/language_scores",
language,
country,
});

22
src/data/wyoming.ts Normal file
View File

@ -0,0 +1,22 @@
import type { HomeAssistant } from "../types";
export interface WyomingInfo {
asr: WyomingAsrInfo[];
handle: [];
intent: [];
tts: WyomingTtsInfo[];
wake: [];
}
interface WyomingBaseInfo {
name: string;
version: string;
attribution: Record<string, string>;
}
interface WyomingTtsInfo extends WyomingBaseInfo {}
interface WyomingAsrInfo extends WyomingBaseInfo {}
export const fetchWyomingInfo = (hass: HomeAssistant) =>
hass.callWS<{ info: Record<string, WyomingInfo> }>({ type: "wyoming/info" });

View File

@ -1,14 +1,19 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiChevronLeft, mdiClose } from "@mdi/js"; import { mdiChevronLeft, mdiClose, mdiMenuDown } from "@mdi/js";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, 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 { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { formatLanguageCode } from "../../common/language/format_language";
import "../../components/chips/ha-assist-chip";
import "../../components/ha-dialog"; import "../../components/ha-dialog";
import { getLanguageOptions } from "../../components/ha-language-picker";
import "../../components/ha-md-button-menu";
import type { AssistSatelliteConfiguration } from "../../data/assist_satellite"; import type { AssistSatelliteConfiguration } from "../../data/assist_satellite";
import { fetchAssistSatelliteConfiguration } from "../../data/assist_satellite"; import { fetchAssistSatelliteConfiguration } from "../../data/assist_satellite";
import { getLanguageScores } from "../../data/conversation";
import { UNAVAILABLE } from "../../data/entity"; import { UNAVAILABLE } from "../../data/entity";
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry"; import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
@ -18,11 +23,11 @@ import "./voice-assistant-setup-step-area";
import "./voice-assistant-setup-step-change-wake-word"; import "./voice-assistant-setup-step-change-wake-word";
import "./voice-assistant-setup-step-check"; import "./voice-assistant-setup-step-check";
import "./voice-assistant-setup-step-cloud"; import "./voice-assistant-setup-step-cloud";
import "./voice-assistant-setup-step-local";
import "./voice-assistant-setup-step-pipeline"; import "./voice-assistant-setup-step-pipeline";
import "./voice-assistant-setup-step-success"; import "./voice-assistant-setup-step-success";
import "./voice-assistant-setup-step-update"; import "./voice-assistant-setup-step-update";
import "./voice-assistant-setup-step-wake-word"; import "./voice-assistant-setup-step-wake-word";
import "./voice-assistant-setup-step-local";
export const enum STEP { export const enum STEP {
INIT, INIT,
@ -49,6 +54,12 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
@state() private _error?: string; @state() private _error?: string;
@state() private _language?: string;
@state() private _languages: string[] = [];
@state() private _localOption?: string;
private _previousSteps: STEP[] = []; private _previousSteps: STEP[] = [];
private _nextStep?: STEP; private _nextStep?: STEP;
@ -67,6 +78,12 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
this.renderRoot.querySelector("ha-dialog")?.close(); this.renderRoot.querySelector("ha-dialog")?.close();
} }
protected willUpdate(changedProps) {
if (changedProps.has("_step") && this._step === STEP.PIPELINE) {
this._getLanguages();
}
}
private _dialogClosed() { private _dialogClosed() {
this._params = undefined; this._params = undefined;
this._assistConfiguration = undefined; this._assistConfiguration = undefined;
@ -139,9 +156,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
@click=${this.closeDialog} @click=${this.closeDialog}
></ha-icon-button>` ></ha-icon-button>`
: nothing} : nothing}
${this._step === STEP.WAKEWORD || ${this._step === STEP.WAKEWORD || this._step === STEP.AREA
this._step === STEP.AREA ||
this._step === STEP.PIPELINE
? html`<ha-button ? html`<ha-button
@click=${this._goToNextStep} @click=${this._goToNextStep}
class="skip-btn" class="skip-btn"
@ -150,7 +165,43 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
"ui.panel.config.voice_assistants.satellite_wizard.skip" "ui.panel.config.voice_assistants.satellite_wizard.skip"
)}</ha-button )}</ha-button
>` >`
: nothing} : this._step === STEP.PIPELINE
? this._language
? html`<ha-md-button-menu
slot="actionItems"
positioning="fixed"
>
<ha-assist-chip
.label=${formatLanguageCode(
this._language,
this.hass.locale
)}
slot="trigger"
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
${getLanguageOptions(
this._languages,
false,
false,
this.hass.locale
).map(
(lang) =>
html`<ha-md-menu-item
.value=${lang.value}
@click=${this._handlePickLanguage}
@keydown=${this._handlePickLanguage}
.selected=${this._language === lang.value}
>
${lang.label}
</ha-md-menu-item>`
)}
</ha-md-button-menu>`
: nothing
: nothing}
</ha-dialog-header> </ha-dialog-header>
<div <div
class="content" class="content"
@ -207,8 +258,11 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
: this._step === STEP.PIPELINE : this._step === STEP.PIPELINE
? html`<ha-voice-assistant-setup-step-pipeline ? html`<ha-voice-assistant-setup-step-pipeline
.hass=${this.hass} .hass=${this.hass}
.languages=${this._languages}
.language=${this._language}
.assistConfiguration=${this._assistConfiguration} .assistConfiguration=${this._assistConfiguration}
.assistEntityId=${assistSatelliteEntityId} .assistEntityId=${assistSatelliteEntityId}
@language-changed=${this._languageChanged}
></ha-voice-assistant-setup-step-pipeline>` ></ha-voice-assistant-setup-step-pipeline>`
: this._step === STEP.CLOUD : this._step === STEP.CLOUD
? html`<ha-voice-assistant-setup-step-cloud ? html`<ha-voice-assistant-setup-step-cloud
@ -217,6 +271,8 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
: this._step === STEP.LOCAL : this._step === STEP.LOCAL
? html`<ha-voice-assistant-setup-step-local ? html`<ha-voice-assistant-setup-step-local
.hass=${this.hass} .hass=${this.hass}
.language=${this._language}
.localOption=${this._localOption}
.assistConfiguration=${this .assistConfiguration=${this
._assistConfiguration} ._assistConfiguration}
></ha-voice-assistant-setup-step-local>` ></ha-voice-assistant-setup-step-local>`
@ -233,6 +289,27 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
`; `;
} }
private async _getLanguages() {
if (this._languages.length) {
return;
}
const scores = await getLanguageScores(this.hass);
this._languages = Object.entries(scores.languages)
.filter(
([_lang, score]) =>
score.cloud > 0 || score.full_local > 0 || score.focused_local > 0
)
.map(([lang, _score]) => lang);
this._language =
scores.preferred_language &&
this._languages.includes(scores.preferred_language)
? scores.preferred_language
: undefined;
}
private async _fetchAssistConfiguration() { private async _fetchAssistConfiguration() {
try { try {
this._assistConfiguration = await fetchAssistSatelliteConfiguration( this._assistConfiguration = await fetchAssistSatelliteConfiguration(
@ -248,6 +325,19 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
} }
} }
private _handlePickLanguage(ev) {
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") return;
this._language = ev.target.value;
}
private _languageChanged(ev: CustomEvent) {
if (!ev.detail.value) {
return;
}
this._language = ev.detail.value;
}
private _goToPreviousStep() { private _goToPreviousStep() {
if (!this._previousSteps.length) { if (!this._previousSteps.length) {
return; return;
@ -267,6 +357,9 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
} }
if (ev?.detail?.step) { if (ev?.detail?.step) {
this._step = ev.detail.step; this._step = ev.detail.step;
if (ev.detail.step === STEP.LOCAL) {
this._localOption = ev.detail.option;
}
} else if (this._nextStep) { } else if (this._nextStep) {
this._step = this._nextStep; this._step = this._nextStep;
this._nextStep = undefined; this._nextStep = undefined;
@ -305,6 +398,13 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
margin: 24px; margin: 24px;
display: block; display: block;
} }
ha-md-button-menu {
height: 48px;
display: flex;
align-items: center;
margin-right: 12px;
margin-inline-end: 12px;
}
`, `,
]; ];
} }
@ -322,8 +422,10 @@ declare global {
updateConfig?: boolean; updateConfig?: boolean;
noPrevious?: boolean; noPrevious?: boolean;
nextStep?: STEP; nextStep?: STEP;
option?: string;
} }
| undefined; | undefined;
"prev-step": undefined; "prev-step": undefined;
"language-changed": { value: string };
} }
} }

View File

@ -16,7 +16,10 @@ import {
fetchConfigFlowInProgress, fetchConfigFlowInProgress,
handleConfigFlowStep, handleConfigFlowStep,
} from "../../data/config_flow"; } from "../../data/config_flow";
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry"; import {
type ExtEntityRegistryEntry,
getExtendedEntityRegistryEntries,
} from "../../data/entity_registry";
import { import {
fetchHassioAddonsInfo, fetchHassioAddonsInfo,
installHassioAddon, installHassioAddon,
@ -24,10 +27,12 @@ import {
} from "../../data/hassio/addon"; } from "../../data/hassio/addon";
import { listSTTEngines } from "../../data/stt"; import { listSTTEngines } from "../../data/stt";
import { listTTSEngines, listTTSVoices } from "../../data/tts"; import { listTTSEngines, listTTSVoices } from "../../data/tts";
import { fetchWyomingInfo } from "../../data/wyoming";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url"; import { documentationUrl } from "../../util/documentation-url";
import { AssistantSetupStyles } from "./styles"; import { AssistantSetupStyles } from "./styles";
import { STEP } from "./voice-assistant-setup-dialog"; import { STEP } from "./voice-assistant-setup-dialog";
import { listAgents } from "../../data/conversation";
@customElement("ha-voice-assistant-setup-step-local") @customElement("ha-voice-assistant-setup-step-local")
export class HaVoiceAssistantSetupStepLocal extends LitElement { export class HaVoiceAssistantSetupStepLocal extends LitElement {
@ -36,6 +41,12 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public assistConfiguration?: AssistSatelliteConfiguration; public assistConfiguration?: AssistSatelliteConfiguration;
@property({ attribute: false }) public localOption!:
| "focused_local"
| "full_local";
@property({ attribute: false }) public language!: string;
@state() private _state: "INSTALLING" | "NOT_SUPPORTED" | "ERROR" | "INTRO" = @state() private _state: "INSTALLING" | "NOT_SUPPORTED" | "ERROR" | "INTRO" =
"INTRO"; "INTRO";
@ -43,9 +54,9 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
@state() private _error?: string; @state() private _error?: string;
@state() private _localTts?: EntityRegistryDisplayEntry[]; @state() private _localTts?: ExtEntityRegistryEntry[];
@state() private _localStt?: EntityRegistryDisplayEntry[]; @state() private _localStt?: ExtEntityRegistryEntry[];
protected override render() { protected override render() {
return html`<div class="content"> return html`<div class="content">
@ -159,58 +170,62 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
} }
private async _checkLocal() { private async _checkLocal() {
this._findLocalEntities(); await this._findLocalEntities();
if (!this._localTts || !this._localStt) { if (!this._localTts || !this._localStt) {
return; return;
} }
if (this._localTts.length && this._localStt.length) {
this._pickOrCreatePipelineExists();
return;
}
if (!isComponentLoaded(this.hass, "hassio")) {
this._state = "NOT_SUPPORTED";
return;
}
this._state = "INSTALLING";
try { try {
if (this._localTts.length && this._localStt.length) {
await this._pickOrCreatePipelineExists();
return;
}
if (!isComponentLoaded(this.hass, "hassio")) {
this._state = "NOT_SUPPORTED";
return;
}
this._state = "INSTALLING";
const { addons } = await fetchHassioAddonsInfo(this.hass); const { addons } = await fetchHassioAddonsInfo(this.hass);
const whisper = addons.find((addon) => addon.slug === "core_whisper"); const ttsAddon = addons.find(
const piper = addons.find((addon) => addon.slug === "core_piper"); (addon) => addon.slug === this._ttsAddonName
);
const sttAddon = addons.find(
(addon) => addon.slug === this._sttAddonName
);
if (!this._localTts.length) { if (!this._localTts.length) {
if (!piper) { if (!ttsAddon) {
this._detailState = this.hass.localize( this._detailState = this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.local.state.installing_piper" `ui.panel.config.voice_assistants.satellite_wizard.local.state.installing_${this._ttsProviderName}`
); );
await installHassioAddon(this.hass, "core_piper"); await installHassioAddon(this.hass, this._ttsAddonName);
} }
if (!piper || piper.state !== "started") { if (!ttsAddon || ttsAddon.state !== "started") {
this._detailState = this.hass.localize( this._detailState = this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.local.state.starting_piper" `ui.panel.config.voice_assistants.satellite_wizard.local.state.starting_${this._ttsProviderName}`
); );
await startHassioAddon(this.hass, "core_piper"); await startHassioAddon(this.hass, this._ttsAddonName);
} }
this._detailState = this.hass.localize( this._detailState = this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.local.state.setup_piper" `ui.panel.config.voice_assistants.satellite_wizard.local.state.setup_${this._ttsProviderName}`
); );
await this._setupConfigEntry("piper"); await this._setupConfigEntry("tts");
} }
if (!this._localStt.length) { if (!this._localStt.length) {
if (!whisper) { if (!sttAddon) {
this._detailState = this.hass.localize( this._detailState = this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.local.state.installing_whisper" `ui.panel.config.voice_assistants.satellite_wizard.local.state.installing_${this._sttProviderName}`
); );
await installHassioAddon(this.hass, "core_whisper"); await installHassioAddon(this.hass, this._sttAddonName);
} }
if (!whisper || whisper.state !== "started") { if (!sttAddon || sttAddon.state !== "started") {
this._detailState = this.hass.localize( this._detailState = this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.local.state.starting_whisper" `ui.panel.config.voice_assistants.satellite_wizard.local.state.starting_${this._sttProviderName}`
); );
await startHassioAddon(this.hass, "core_whisper"); await startHassioAddon(this.hass, this._sttAddonName);
} }
this._detailState = this.hass.localize( this._detailState = this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.local.state.setup_whisper" `ui.panel.config.voice_assistants.satellite_wizard.local.state.setup_${this._sttProviderName}`
); );
await this._setupConfigEntry("whisper"); await this._setupConfigEntry("stt");
} }
this._detailState = this.hass.localize( this._detailState = this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.local.state.creating_pipeline" "ui.panel.config.voice_assistants.satellite_wizard.local.state.creating_pipeline"
@ -222,20 +237,72 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
} }
} }
private _findLocalEntities() { private readonly _ttsProviderName = "piper";
private readonly _ttsAddonName = "core_piper";
private readonly _ttsHostName = "core-piper";
private readonly _ttsPort = "10200";
private get _sttProviderName() {
return this.localOption === "focused_local"
? "speech-to-phrase"
: "faster-whisper";
}
private get _sttAddonName() {
return this.localOption === "focused_local"
? "core_speech-to-phrase"
: "core_whisper";
}
private get _sttHostName() {
return this.localOption === "focused_local"
? "core-speech-to-phrase"
: "core-whisper";
}
private readonly _sttPort = "10300";
private async _findLocalEntities() {
const wyomingEntities = Object.values(this.hass.entities).filter( const wyomingEntities = Object.values(this.hass.entities).filter(
(entity) => entity.platform === "wyoming" (entity) => entity.platform === "wyoming"
); );
this._localTts = wyomingEntities.filter( if (!wyomingEntities.length) {
(ent) => computeDomain(ent.entity_id) === "tts" this._localStt = [];
this._localTts = [];
return;
}
const wyomingInfo = await fetchWyomingInfo(this.hass);
const entityRegs = Object.values(
await getExtendedEntityRegistryEntries(
this.hass,
wyomingEntities.map((ent) => ent.entity_id)
)
); );
this._localStt = wyomingEntities.filter(
(ent) => computeDomain(ent.entity_id) === "stt" this._localTts = entityRegs.filter(
(ent) =>
computeDomain(ent.entity_id) === "tts" &&
ent.config_entry_id &&
wyomingInfo.info[ent.config_entry_id]?.tts.some(
(provider) => provider.name === this._ttsProviderName
)
);
this._localStt = entityRegs.filter(
(ent) =>
computeDomain(ent.entity_id) === "stt" &&
ent.config_entry_id &&
wyomingInfo.info[ent.config_entry_id]?.asr.some(
(provider) => provider.name === this._sttProviderName
)
); );
} }
private async _setupConfigEntry(addon: string) { private async _setupConfigEntry(type: "tts" | "stt") {
const configFlow = await this._findConfigFlowInProgress(addon); const configFlow = await this._findConfigFlowInProgress(type);
if (configFlow) { if (configFlow) {
const step = await handleConfigFlowStep( const step = await handleConfigFlowStep(
@ -248,30 +315,36 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
} }
} }
return this._createConfigEntry(addon); return this._createConfigEntry(type);
} }
private async _findConfigFlowInProgress(addon: string) { private async _findConfigFlowInProgress(type: "tts" | "stt") {
const configFlows = await fetchConfigFlowInProgress(this.hass.connection); const configFlows = await fetchConfigFlowInProgress(this.hass.connection);
return configFlows.find( return configFlows.find(
(flow) => (flow) =>
flow.handler === "wyoming" && flow.handler === "wyoming" &&
flow.context.source === "hassio" && flow.context.source === "hassio" &&
(flow.context.configuration_url.includes(`core_${addon}`) || (flow.context.configuration_url.includes(
flow.context.title_placeholders.title.toLowerCase().includes(addon)) type === "tts" ? this._ttsHostName : this._sttHostName
) ||
flow.context.title_placeholders.title
.toLowerCase()
.includes(
type === "tts" ? this._ttsProviderName : this._sttProviderName
))
); );
} }
private async _createConfigEntry(addon: string) { private async _createConfigEntry(type: "tts" | "stt") {
const configFlow = await createConfigFlow(this.hass, "wyoming"); const configFlow = await createConfigFlow(this.hass, "wyoming");
const step = await handleConfigFlowStep(this.hass, configFlow.flow_id, { const step = await handleConfigFlowStep(this.hass, configFlow.flow_id, {
host: `core-${addon}`, host: type === "tts" ? this._ttsHostName : this._sttHostName,
port: addon === "piper" ? 10200 : 10300, port: type === "tts" ? this._ttsPort : this._sttPort,
}); });
if (step.type !== "create_entry") { if (step.type !== "create_entry") {
throw new Error( throw new Error(
`${this.hass.localize("ui.panel.config.voice_assistants.satellite_wizard.local.errors.failed_create_entry", { addon })}${"errors" in step ? `: ${step.errors.base}` : ""}` `${this.hass.localize("ui.panel.config.voice_assistants.satellite_wizard.local.errors.failed_create_entry", { addon: type === "tts" ? this._ttsProviderName : this._sttProviderName })}${"errors" in step ? `: ${step.errors.base}` : ""}`
); );
} }
} }
@ -341,27 +414,54 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
const pipelines = await listAssistPipelines(this.hass); const pipelines = await listAssistPipelines(this.hass);
const agent = (
await listAgents(
this.hass,
this.language || this.hass.config.language,
this.hass.config.country || undefined
)
).agents.find((agnt) => agnt.id === "conversation.home_assistant");
if (!agent?.supported_languages.length) {
throw new Error(
"Conversation agent does not support requested language."
);
}
const ttsEngine = ( const ttsEngine = (
await listTTSEngines( await listTTSEngines(
this.hass, this.hass,
this.hass.config.language, this.language,
this.hass.config.country || undefined this.hass.config.country || undefined
) )
).providers.find((provider) => provider.engine_id === ttsEntityId); ).providers.find((provider) => provider.engine_id === ttsEntityId);
if (!ttsEngine?.supported_languages?.length) {
throw new Error("TTS engine does not support requested language.");
}
const ttsVoices = await listTTSVoices( const ttsVoices = await listTTSVoices(
this.hass, this.hass,
ttsEntityId, ttsEntityId,
ttsEngine?.supported_languages![0] || this.hass.config.language ttsEngine.supported_languages[0]
); );
if (!ttsVoices.voices?.length) {
throw new Error("No voice available for requested language.");
}
const sttEngine = ( const sttEngine = (
await listSTTEngines( await listSTTEngines(
this.hass, this.hass,
this.hass.config.language, this.language,
this.hass.config.country || undefined this.hass.config.country || undefined
) )
).providers.find((provider) => provider.engine_id === sttEntityId); ).providers.find((provider) => provider.engine_id === sttEntityId);
if (!sttEngine?.supported_languages?.length) {
throw new Error("STT engine does not support requested language.");
}
let pipelineName = this.hass.localize( let pipelineName = this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.local.local_pipeline" "ui.panel.config.voice_assistants.satellite_wizard.local.local_pipeline"
); );
@ -378,21 +478,21 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
return createAssistPipeline(this.hass, { return createAssistPipeline(this.hass, {
name: pipelineName, name: pipelineName,
language: this.hass.config.language.split("-")[0], language: this.language.split("-")[0],
conversation_engine: "conversation.home_assistant", conversation_engine: "conversation.home_assistant",
conversation_language: this.hass.config.language.split("-")[0], conversation_language: agent.supported_languages[0],
stt_engine: sttEntityId, stt_engine: sttEntityId,
stt_language: sttEngine!.supported_languages![0], stt_language: sttEngine.supported_languages[0],
tts_engine: ttsEntityId, tts_engine: ttsEntityId,
tts_language: ttsEngine!.supported_languages![0], tts_language: ttsEngine.supported_languages[0],
tts_voice: ttsVoices.voices![0].voice_id, tts_voice: ttsVoices.voices[0].voice_id,
wake_word_entity: null, wake_word_entity: null,
wake_word_id: null, wake_word_id: null,
}); });
} }
private async _findEntitiesAndCreatePipeline(tryNo = 0) { private async _findEntitiesAndCreatePipeline(tryNo = 0) {
this._findLocalEntities(); await this._findLocalEntities();
if (!this._localTts?.length || !this._localStt?.length) { if (!this._localTts?.length || !this._localStt?.length) {
if (tryNo > 3) { if (tryNo > 3) {
throw new Error( throw new Error(

View File

@ -1,22 +1,30 @@
import { mdiOpenInNew } from "@mdi/js";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, 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 { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { formatLanguageCode } from "../../common/language/format_language";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../../components/ha-select-box";
import type { SelectBoxOption } from "../../components/ha-select-box";
import { import {
createAssistPipeline, createAssistPipeline,
listAssistPipelines, listAssistPipelines,
} from "../../data/assist_pipeline"; } from "../../data/assist_pipeline";
import type { AssistSatelliteConfiguration } from "../../data/assist_satellite"; import type { AssistSatelliteConfiguration } from "../../data/assist_satellite";
import { fetchCloudStatus } from "../../data/cloud"; import { fetchCloudStatus } from "../../data/cloud";
import type { LanguageScores } from "../../data/conversation";
import { getLanguageScores, listAgents } from "../../data/conversation";
import { listSTTEngines } from "../../data/stt"; import { listSTTEngines } from "../../data/stt";
import { listTTSEngines, listTTSVoices } from "../../data/tts"; import { listTTSEngines, listTTSVoices } from "../../data/tts";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { AssistantSetupStyles } from "./styles"; import { AssistantSetupStyles } from "./styles";
import { STEP } from "./voice-assistant-setup-dialog"; import { STEP } from "./voice-assistant-setup-dialog";
import { documentationUrl } from "../../util/documentation-url";
const OPTIONS = ["cloud", "focused_local", "full_local"] as const;
@customElement("ha-voice-assistant-setup-step-pipeline") @customElement("ha-voice-assistant-setup-step-pipeline")
export class HaVoiceAssistantSetupStepPipeline extends LitElement { export class HaVoiceAssistantSetupStepPipeline extends LitElement {
@ -29,166 +37,231 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
@property({ attribute: false }) public assistEntityId?: string; @property({ attribute: false }) public assistEntityId?: string;
@property({ attribute: false }) public language?: string;
@property({ attribute: false }) public languages: string[] = [];
@state() private _cloudChecked = false; @state() private _cloudChecked = false;
@state() private _showFirst = false; @state() private _value?: (typeof OPTIONS)[number];
@state() private _showSecond = false; @state() private _languageScores?: LanguageScores;
@state() private _showThird = false;
@state() private _showFourth = false;
protected override willUpdate(changedProperties: PropertyValues): void { protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties); super.willUpdate(changedProperties);
if (!this.hasUpdated) { if (!this.hasUpdated) {
this._checkCloud(); this._fetchData();
}
if (
(changedProperties.has("language") ||
changedProperties.has("_languageScores")) &&
this.language &&
this._languageScores
) {
const lang = this.language;
if (this._value && this._languageScores[lang][this._value] === 0) {
this._value = undefined;
}
if (!this._value) {
this._value = this._getOptions(
this._languageScores[lang],
this.hass.localize
).supportedOptions[0]?.value as
| "cloud"
| "focused_local"
| "full_local"
| undefined;
}
} }
} }
protected override firstUpdated(changedProperties: PropertyValues) { private _getOptions = memoizeOne((score, localize: LocalizeFunc) => {
super.firstUpdated(changedProperties); const supportedOptions: SelectBoxOption[] = [];
setTimeout(() => { const unsupportedOptions: SelectBoxOption[] = [];
this._showFirst = true;
}, 200); OPTIONS.forEach((option) => {
setTimeout(() => { if (score[option] > 0) {
this._showSecond = true; supportedOptions.push({
}, 600); label: localize(
setTimeout(() => { `ui.panel.config.voice_assistants.satellite_wizard.pipeline.options.${option}.label`
this._showThird = true; ),
}, 2000); description: localize(
setTimeout(() => { `ui.panel.config.voice_assistants.satellite_wizard.pipeline.options.${option}.description`
this._showFourth = true; ),
}, 3000); value: option,
} });
} else {
unsupportedOptions.push({
label: localize(
`ui.panel.config.voice_assistants.satellite_wizard.pipeline.options.${option}.label`
),
value: option,
});
}
});
return { supportedOptions, unsupportedOptions };
});
protected override render() { protected override render() {
if (!this._cloudChecked) { if (!this._cloudChecked || !this._languageScores) {
return nothing; return nothing;
} }
return html`<div class="content"> if (!this.language) {
<h1> const language = formatLanguageCode(
${this.hass.localize( this.hass.config.language,
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.title" this.hass.locale
)} );
</h1> return html`<div class="content">
<p class="secondary"> <h1>
${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.secondary"
)}
</p>
<div class="container">
<div class="messages-container cloud">
<div class="message user ${this._showFirst ? "show" : ""}">
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
</div>
${this._showFirst
? html`<div class="timing user">
0.2
${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.seconds"
)}
</div>`
: nothing}
${this._showFirst
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
${!this._showSecond ? "…" : "Turned on the lights"}
</div>`
: nothing}
${this._showSecond
? html`<div class="timing hass">
0.4
${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.seconds"
)}
</div>`
: nothing}
</div>
<h2>Home Assistant Cloud</h2>
<p>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.cloud.description" "ui.panel.config.voice_assistants.satellite_wizard.pipeline.unsupported_language.header"
)} )}
</p> </h1>
<ha-button @click=${this._setupCloud} unelevated ${this.hass.localize(
>${this.hass.localize("ui.panel.config.common.learn_more")}</ha-button "ui.panel.config.voice_assistants.satellite_wizard.pipeline.unsupported_language.secondary",
{ language }
)}
<ha-language-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.unsupported_language.language_picker"
)}
.languages=${this.languages}
@value-changed=${this._languageChanged}
></ha-language-picker>
<a
href=${documentationUrl(
this.hass,
"/voice_control/contribute-voice/"
)}
>${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.unsupported_language.contribute",
{ language }
)}</a
> >
</div> </div>`;
<div class="container"> }
<div class="messages-container rpi">
<div class="message user ${this._showThird ? "show" : ""}"> const score = this._languageScores[this.language];
${!this._showThird ? "…" : "Turn on the lights in the bedroom"}
</div> const options = this._getOptions(
${this._showThird score || { cloud: 3, focused_local: 0, full_local: 0 },
? html`<div class="timing user"> this.hass.localize
2 );
${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.seconds" const performance = !this._value
)} ? ""
</div>` : this._value === "full_local"
: nothing} ? "low"
${this._showThird : "high";
? html`<div class="message hass ${this._showFourth ? "show" : ""}">
${!this._showFourth ? "…" : "Turned on the lights"} const commands = !this._value
</div>` ? ""
: nothing} : score?.[this._value] > 2
${this._showFourth ? "high"
? html`<div class="timing hass"> : score?.[this._value] > 1
1 ? "ready"
${this.hass.localize( : score?.[this._value] > 0
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.seconds" ? "low"
)} : "";
</div>`
: nothing} return html`<div class="content">
</div> <h1>
<h2>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.local.title" "ui.panel.config.voice_assistants.satellite_wizard.pipeline.title"
)} )}
</h2> </h1>
<p> <div class="bar-header">
${this.hass.localize( <span
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.local.description"
)}
</p>
<div class="row">
<a
href=${documentationUrl(
this.hass,
"/voice_control/voice_remote_local_assistant/"
)}
target="_blank"
rel="noreferrer noopener"
>
<ha-button>
<ha-svg-icon .path=${mdiOpenInNew} slot="icon"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.common.learn_more"
)}</ha-button
>
</a>
<ha-button @click=${this._setupLocal} unelevated
>${this.hass.localize( >${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.local.setup" "ui.panel.config.voice_assistants.satellite_wizard.pipeline.performance.header"
)}</ha-button )}</span
><span
>${!performance
? ""
: this.hass.localize(
`ui.panel.config.voice_assistants.satellite_wizard.pipeline.performance.${performance}`
)}</span
> >
</div> </div>
<div class="perf-bar ${performance}">
<div class="segment"></div>
<div class="segment"></div>
<div class="segment"></div>
</div>
<div class="bar-header">
<span
>${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.commands.header"
)}</span
><span
>${!commands
? ""
: this.hass.localize(
`ui.panel.config.voice_assistants.satellite_wizard.pipeline.commands.${commands}`
)}</span
>
</div>
<div class="perf-bar ${commands}">
<div class="segment"></div>
<div class="segment"></div>
<div class="segment"></div>
</div>
<ha-select-box
max_columns="1"
.options=${options.supportedOptions}
.value=${this._value}
@value-changed=${this._valueChanged}
></ha-select-box>
${options.unsupportedOptions.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.unsupported"
)}
</h3>
<ha-select-box
max_columns="1"
.options=${options.unsupportedOptions}
disabled
></ha-select-box>`
: nothing}
</div> </div>
</div>`; <div class="footer">
<ha-button
@click=${this._createPipeline}
unelevated
.disabled=${!this._value}
>${this.hass.localize("ui.common.next")}</ha-button
>
</div>`;
} }
private async _checkCloud() { private async _fetchData() {
if (!isComponentLoaded(this.hass, "cloud")) { const cloud =
(await this._hasCloud()) && (await this._createCloudPipeline());
if (!cloud) {
this._cloudChecked = true; this._cloudChecked = true;
return; this._languageScores = (await getLanguageScores(this.hass)).languages;
}
}
private async _hasCloud(): Promise<boolean> {
if (!isComponentLoaded(this.hass, "cloud")) {
return false;
} }
const cloudStatus = await fetchCloudStatus(this.hass); const cloudStatus = await fetchCloudStatus(this.hass);
if (!cloudStatus.logged_in || !cloudStatus.active_subscription) { if (!cloudStatus.logged_in || !cloudStatus.active_subscription) {
this._cloudChecked = true; return false;
return;
} }
return true;
}
private async _createCloudPipeline(): Promise<boolean> {
let cloudTtsEntityId; let cloudTtsEntityId;
let cloudSttEntityId; let cloudSttEntityId;
for (const entity of Object.values(this.hass.entities)) { for (const entity of Object.values(this.hass.entities)) {
@ -206,186 +279,212 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
} }
} }
} }
const pipelines = await listAssistPipelines(this.hass); try {
const preferredPipeline = pipelines.pipelines.find( const pipelines = await listAssistPipelines(this.hass);
(pipeline) => pipeline.id === pipelines.preferred_pipeline const preferredPipeline = pipelines.pipelines.find(
); (pipeline) => pipeline.id === pipelines.preferred_pipeline
if (preferredPipeline) {
if (
preferredPipeline.conversation_engine ===
"conversation.home_assistant" &&
preferredPipeline.tts_engine === cloudTtsEntityId &&
preferredPipeline.stt_engine === cloudSttEntityId
) {
await this.hass.callService(
"select",
"select_option",
{ option: "preferred" },
{ entity_id: this.assistConfiguration?.pipeline_entity_id }
);
fireEvent(this, "next-step", { step: STEP.SUCCESS, noPrevious: true });
return;
}
}
let cloudPipeline = pipelines.pipelines.find(
(pipeline) =>
pipeline.conversation_engine === "conversation.home_assistant" &&
pipeline.tts_engine === cloudTtsEntityId &&
pipeline.stt_engine === cloudSttEntityId
);
if (!cloudPipeline) {
const ttsEngine = (
await listTTSEngines(
this.hass,
this.hass.config.language,
this.hass.config.country || undefined
)
).providers.find((provider) => provider.engine_id === cloudTtsEntityId);
const ttsVoices = await listTTSVoices(
this.hass,
cloudTtsEntityId,
ttsEngine?.supported_languages![0] || this.hass.config.language
); );
const sttEngine = ( if (preferredPipeline) {
await listSTTEngines( if (
this.hass, preferredPipeline.conversation_engine ===
this.hass.config.language, "conversation.home_assistant" &&
this.hass.config.country || undefined preferredPipeline.tts_engine === cloudTtsEntityId &&
) preferredPipeline.stt_engine === cloudSttEntityId
).providers.find((provider) => provider.engine_id === cloudSttEntityId); ) {
await this.hass.callService(
let pipelineName = "Home Assistant Cloud"; "select",
let i = 1; "select_option",
while ( { option: "preferred" },
pipelines.pipelines.find( { entity_id: this.assistConfiguration?.pipeline_entity_id }
// eslint-disable-next-line no-loop-func );
(pipeline) => pipeline.name === pipelineName fireEvent(this, "next-step", {
) step: STEP.SUCCESS,
) { noPrevious: true,
pipelineName = `Home Assistant Cloud ${i}`; });
i++; return true;
}
} }
cloudPipeline = await createAssistPipeline(this.hass, { let cloudPipeline = pipelines.pipelines.find(
name: pipelineName, (pipeline) =>
language: this.hass.config.language.split("-")[0], pipeline.conversation_engine === "conversation.home_assistant" &&
conversation_engine: "conversation.home_assistant", pipeline.tts_engine === cloudTtsEntityId &&
conversation_language: this.hass.config.language.split("-")[0], pipeline.stt_engine === cloudSttEntityId
stt_engine: cloudSttEntityId, );
stt_language: sttEngine!.supported_languages![0],
tts_engine: cloudTtsEntityId,
tts_language: ttsEngine!.supported_languages![0],
tts_voice: ttsVoices.voices![0].voice_id,
wake_word_entity: null,
wake_word_id: null,
});
}
await this.hass.callService( if (!cloudPipeline) {
"select", const agent = (
"select_option", await listAgents(
{ option: cloudPipeline.name }, this.hass,
{ entity_id: this.assistConfiguration?.pipeline_entity_id } this.language || this.hass.config.language,
); this.hass.config.country || undefined
fireEvent(this, "next-step", { step: STEP.SUCCESS, noPrevious: true }); )
).agents.find((agnt) => agnt.id === "conversation.home_assistant");
if (!agent?.supported_languages.length) {
return false;
}
const ttsEngine = (
await listTTSEngines(
this.hass,
this.language || this.hass.config.language,
this.hass.config.country || undefined
)
).providers.find((provider) => provider.engine_id === cloudTtsEntityId);
if (!ttsEngine?.supported_languages?.length) {
return false;
}
const ttsVoices = await listTTSVoices(
this.hass,
cloudTtsEntityId,
ttsEngine.supported_languages[0]
);
const sttEngine = (
await listSTTEngines(
this.hass,
this.language || this.hass.config.language,
this.hass.config.country || undefined
)
).providers.find((provider) => provider.engine_id === cloudSttEntityId);
if (!sttEngine?.supported_languages?.length) {
return false;
}
let pipelineName = "Home Assistant Cloud";
let i = 1;
while (
pipelines.pipelines.find(
// eslint-disable-next-line no-loop-func
(pipeline) => pipeline.name === pipelineName
)
) {
pipelineName = `Home Assistant Cloud ${i}`;
i++;
}
cloudPipeline = await createAssistPipeline(this.hass, {
name: pipelineName,
language: (this.language || this.hass.config.language).split("-")[0],
conversation_engine: "conversation.home_assistant",
conversation_language: agent.supported_languages[0],
stt_engine: cloudSttEntityId,
stt_language: sttEngine.supported_languages[0],
tts_engine: cloudTtsEntityId,
tts_language: ttsEngine.supported_languages[0],
tts_voice: ttsVoices.voices![0].voice_id,
wake_word_entity: null,
wake_word_id: null,
});
}
await this.hass.callService(
"select",
"select_option",
{ option: cloudPipeline.name },
{ entity_id: this.assistConfiguration?.pipeline_entity_id }
);
fireEvent(this, "next-step", { step: STEP.SUCCESS, noPrevious: true });
return true;
} catch (_e) {
return false;
}
}
private _valueChanged(ev: CustomEvent) {
this._value = ev.detail.value;
} }
private async _setupCloud() { private async _setupCloud() {
this._nextStep(STEP.CLOUD); if (await this._hasCloud()) {
this._createCloudPipeline();
return;
}
fireEvent(this, "next-step", { step: STEP.CLOUD });
} }
private async _setupLocal() { private _createPipeline() {
this._nextStep(STEP.LOCAL); if (this._value === "cloud") {
this._setupCloud();
} else if (this._value === "focused_local") {
this._setupLocalFocused();
} else {
this._setupLocalFull();
}
} }
private _nextStep(step?: STEP) { private _setupLocalFocused() {
fireEvent(this, "next-step", { step }); fireEvent(this, "next-step", { step: STEP.LOCAL, option: this._value });
}
private _setupLocalFull() {
fireEvent(this, "next-step", { step: STEP.LOCAL, option: this._value });
}
private _languageChanged(ev: CustomEvent) {
if (!ev.detail.value) {
return;
}
fireEvent(this, "language-changed", { value: ev.detail.value });
} }
static styles = [ static styles = [
AssistantSetupStyles, AssistantSetupStyles,
css` css`
.container { :host {
border-radius: 16px; text-align: left;
border: 1px solid var(--divider-color);
overflow: hidden;
padding-bottom: 16px;
} }
.container:last-child { .perf-bar {
margin-top: 16px; width: 100%;
} height: 10px;
.messages-container {
padding: 24px;
box-sizing: border-box;
height: 195px;
background: var(--input-fill-color);
display: flex; display: flex;
flex-direction: column; gap: 4px;
}
.message {
white-space: nowrap;
font-size: 18px;
clear: both;
margin: 8px 0; margin: 8px 0;
padding: 8px;
border-radius: 15px;
height: 36px;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
width: 30px;
} }
.rpi .message { .segment {
transition: width 1s; flex-grow: 1;
background-color: var(--disabled-color);
transition: background-color 0.3s;
} }
.cloud .message { .segment:first-child {
transition: width 0.5s; border-radius: 4px 0 0 4px;
} }
.segment:last-child {
.message.user { border-radius: 0 4px 4px 0;
margin-left: 24px;
margin-inline-start: 24px;
margin-inline-end: initial;
align-self: self-end;
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--primary-color);
color: var(--text-primary-color);
direction: var(--direction);
} }
.timing.user { .perf-bar.high .segment {
align-self: self-end; background-color: var(--success-color);
} }
.perf-bar.ready .segment:nth-child(-n + 2) {
.message.user.show { background-color: var(--warning-color);
width: 295px;
} }
.perf-bar.low .segment:nth-child(1) {
.message.hass { background-color: var(--error-color);
margin-right: 24px;
margin-inline-end: 24px;
margin-inline-start: initial;
align-self: self-start;
border-bottom-left-radius: 0px;
background-color: var(--secondary-background-color);
color: var(--primary-text-color);
direction: var(--direction);
} }
.timing.hass { .bar-header {
align-self: self-start;
}
.message.hass.show {
width: 184px;
}
.row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin: 0 16px; margin: 8px 0;
margin-top: 16px;
}
ha-select-box {
display: block;
}
ha-select-box:first-of-type {
margin-top: 32px;
}
.footer {
margin-top: 16px;
}
ha-language-picker {
display: block;
margin-top: 16px;
margin-bottom: 16px;
} }
`, `,
]; ];

View File

@ -187,7 +187,15 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
} }
private _canOverrideAlphanumericInput(e: KeyboardEvent) { private _canOverrideAlphanumericInput(e: KeyboardEvent) {
const el = e.composedPath()[0] as Element; const composedPath = e.composedPath();
if (
composedPath.some((el) => "tagName" in el && el.tagName === "HA-MENU")
) {
return false;
}
const el = composedPath[0] as Element;
if (el.tagName === "TEXTAREA") { if (el.tagName === "TEXTAREA") {
return false; return false;

View File

@ -3388,16 +3388,38 @@
"no_selection": "Please select an area" "no_selection": "Please select an area"
}, },
"pipeline": { "pipeline": {
"title": "What hardware do you want to use?", "title": "How do you want your voice to be processed?",
"secondary": "How quickly your assistant responds depends on the power of the hardware.", "performance": {
"seconds": "seconds", "header": "Performance on low-powered system",
"cloud": { "low": "Low",
"description": "Ideal if you don't have a powerful system at home." "high": "High"
}, },
"local": { "commands": {
"title": "Do-it-yourself", "header": "Supported commands",
"description": "Install add-ons or containers to run it on your own system. Powerful hardware is needed for fast responses.", "low": "Needs work",
"setup": "Set up" "ready": "Ready to be used",
"high": "Fully supported"
},
"options": {
"cloud": {
"label": "Home Assistant Cloud",
"description": "Offloads speech processing to a fast, private cloud. Offering the highest accuracy and widest language support. Home Assistant Cloud is a subscription service that includes voice processing."
},
"focused_local": {
"label": "Focused local processing",
"description": "Limited to a set list of common home control phrases, this allows any system to process speech locally and offline."
},
"full_local": {
"label": "Full local processing",
"description": "Full speech processing is done locally, requiring high processing power for adequate speed and accuracy."
}
},
"unsupported": "Unsupported",
"unsupported_language": {
"header": "Your language is not supported",
"secondary": "Your language {language} is not supported yet. You can select a different language to continue.",
"language_picker": "Language",
"contribute": "Contribute to the voice initiative for {language}"
} }
}, },
"cloud": { "cloud": {
@ -3418,9 +3440,12 @@
"installing_piper": "Installing Piper add-on", "installing_piper": "Installing Piper add-on",
"starting_piper": "Starting Piper add-on", "starting_piper": "Starting Piper add-on",
"setup_piper": "Setting up Piper", "setup_piper": "Setting up Piper",
"installing_whisper": "Installing Whisper add-on", "installing_faster-whisper": "Installing Whisper add-on",
"starting_whisper": "Starting Whisper add-on", "starting_faster-whisper": "Starting Whisper add-on",
"setup_whisper": "Setting up Whisper", "setup_faster-whisper": "Setting up Whisper",
"installing_speech-to-phrase": "Installing Speech-to-Phrase add-on",
"starting_speech-to-phrase": "Starting Speech-to-Phrase add-on",
"setup_speech-to-phrase": "Setting up Speech-to-Phrase",
"creating_pipeline": "Creating assistant" "creating_pipeline": "Creating assistant"
}, },
"errors": { "errors": {