A script that uses PicoTTS to output audible plant status fails since v0.115 with "TypeError: 'de-DE' not a Frame instance" because apparently mutagen tries to set a language tag. Worked in 0.114.4 and below.
configuration.yamlIn configuration.yaml:
# Text to speech
tts:
- platform: picotts
language: 'en-GB'
base_url: https://has1.example.com
cache: false
automation:
# Plant Problem
- alias: "Problem mit Pflanze"
trigger:
platform: state
entity_id:
- plant.farn
- plant.wohnzimmer_palme
- plant.wohnzimmer_spargel
- plant.bergpalme
to:
- 'problem'
- 'ok'
action:
- service: script.say_plant_status
data_template:
plant_id: "{{ trigger.entity_id }}"
message: >-
{% set language = language | default(states('input_select.audio_language'), true) %}
{% set lang = language[0:2] %}
{% set plants = {
"plant.farn": {
"en": "your fern",
"de": "dein Farn",
},
"plant.wohnzimmer_palme": {
"en": "your livingroom palm tree",
"de": "deine Wohnzimmer-Palme",
},
"plant.bergpalme": {
"en": "your parlour palm",
"de": "deine Bergpalme",
},
"plant.wohnzimmer_spargel": {
"en": "your livingroom asparagus",
"de": "dein Wohnzimmer-Spargel",
},
} %}
{{ plants[trigger.entity_id][lang] }}
In scripts.yaml:
say_plant_status:
alias: 'Say plant status'
description: 'Alert about plant problem (single plant)'
fields:
entity_id:
description: 'Entity Id of the media player to use'
example: 'media_player.signalpi1'
audiofile:
description: 'Name of introductory audio file to play. MUST be WAV, pcm-s16le, mono, 16000 Hz.'
example: 'picotts-beep.wav'
plant_id:
description: 'The entity ID of the plant'
example: 'plant.fern'
message:
description: 'The name of the plant to speak in "This is …" (can be a template)'
example: "your fern"
language:
description: 'The language to speak in (defaults to tts: setting)'
example: 'en-GB'
sequence:
# only alert if "Audio Alerts" is on
- condition: state
entity_id: 'input_boolean.audio_alerts'
state: 'on'
# play text message
- service: tts.picotts_say
data_template:
entity_id: "{{ entity_id|default(states('var.audio_alerts_media_player'),true) }}"
language: "{{ language | default(states('input_select.audio_language'), true) }}"
message: >-
{% set language = language | default(states('input_select.audio_language'), true) %}
{% set lang = language[0:2] %}
{% set problem = state_attr(plant_id, 'problem') %}
{%- if audiofile == '' -%}
{%- elif audiofile -%}
<play file="{{ states('var.audio_alerts_base_path') }}/{{ lang }}/{{ audiofile }}"/>
{%- else -%}
<play file="{{ states('var.audio_alerts_base_path') }}/{{ lang }}/{% if problem == 'none' %}picotts-plant-ok.wav{% else %}picotts-plant-problem.wav{% endif %}"/>
{%- endif -%}
{% set problems = problem.split(', ') %}
<volume level="60"><pitch level="+75%">
{%- if lang == 'de' -%}
{%- set map_problem = {
'moisture low': "ich verdurste",
'moisture high': "es ist so nass hier",
'moisture unavailable': "",
'brightness low': "es war lange zu dunkel",
'brightness high': "mir ist zu hell",
'brightness unavailable': "",
'temperature low': "ich friere",
'temperature high': "mir ist zu warm",
'temperature unavailable': "",
'conductivity low': "ich bräuchte etwas Dünger",
'conductivity high': "ich bin ĂĽberdĂĽngt",
'conductivity unavailable': "",
} -%}
{{ 'Hallo! Hier spricht ' ~ message ~ '. ' -}}
{%- if problems == ['none'] -%}
{{ ["Mir geht es wieder prima!", "Alles ist wieder gut.", "Danke, dass du dich um mich kĂĽmmerst!"] | random }}
{%- else -%}
{%- for p in problems if not 'unavailable' in p -%}
{%- if loop.last and loop.index > 1 %} und {% elif not loop.first %}, {% endif -%}{%- if loop.first -%}{{- map_problem[p][0:1] | upper() ~ map_problem[p][1:] -}}{%- else -%}{{- map_problem[p] -}}{%- endif -%}
{%- if loop.last and loop.index > 0 %}. {% endif -%}
{%- endfor -%}Hilfst du mir bitte?
{%- endif -%}
{%- else -%}
{%- set map_problem = {
'moisture low': "I'm dried out",
'moisture high': "I'm too wet",
'moisture unavailable': "",
'brightness low': "it's been too dark for a long time",
'brightness high': "it's too bright for me",
'brightness unavailable': "",
'temperature low': "I'm cold",
'temperature high': "it's so warm",
'temperature unavailable': "",
'conductivity low': "I need some fertilzer",
'conductivity high': "I'm over-fertilized",
'conductivity unavailable': "",
} -%}
{{ 'Hello! This is ' ~ message ~ ' speaking. ' -}}
{%- if problems == ['none'] -%}
{{ ["I feel really great again!", "All is well again.", "Thank you for caring about me!"] | random }}
{%- else -%}
{%- for p in problems if not 'unavailable' in p -%}
{%- if loop.last and loop.index > 1 %} and {% elif not loop.first %}, {% endif -%}{%- if loop.first -%}{{- map_problem[p][0:1] | upper() ~ map_problem[p][1:] -}}{%- else -%}{{- map_problem[p] -}}{%- endif -%}
{%- if loop.last and loop.index > 0 %}. {% endif -%}
{%- endfor -%}Will you help me, please?
{%- endif -%}
{%- endif -%}
</pitch></volume>
Logger: homeassistant.components.script.say_plant_status
Source: components/tts/__init__.py:470
Integration: Skript (documentation, issues)
First occurred: 14:38:47 (2 occurrences)
Last logged: 15:18:54
Say plant status: Error executing script. Unexpected error for call_service at pos 2: 'de-DE' not a Frame instance
Traceback (most recent call last):
File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/helpers/script.py", line 207, in _async_step
self, f"_async_{cv.determine_script_action(self._action)}_step"
File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/helpers/script.py", line 413, in _async_call_service_step
await service_task
File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/core.py", line 1315, in async_call
task.result()
File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/core.py", line 1350, in _execute_service
await handler.func(service_call)
File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/components/tts/__init__.py", line 168, in async_say_handle
p_type, message, cache=cache, language=language, options=options
File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/components/tts/__init__.py", line 341, in async_get_url
engine, key, message, use_cache, language, options
File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/components/tts/__init__.py", line 367, in async_get_tts_audio
data = self.write_tags(filename, data, provider, message, language, options)
File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/components/tts/__init__.py", line 470, in write_tags
tts_file["artist"] = artist
File "/srv/homeassistant/lib/python3.7/site-packages/mutagen/_file.py", line 74, in __setitem__
self.tags[key] = value
File "/srv/homeassistant/lib/python3.7/site-packages/mutagen/id3/_tags.py", line 339, in __setitem__
raise TypeError("%r not a Frame instance" % tag)
TypeError: 'de-DE' not a Frame instance
Looks like the TTS component tries to add tags into the audio file (which is a WAV if using PicoTTS and might not work anyway). Even for MP3, the content de-DE needed to switch PicoTTS to German (it’s set to en-GB by default on my bilingual system) would probably not be allowed.
If setting tags in audio files, this should be done _right_—or not at all.
_Note:_ This exact same code worked flawlessly in version 0.114.4 and below for many months. I think I implemented this at about the time 0.105 or 0.107 came out.
Appreciate any help since none of my "say…" scripts work anymore with 0.115, and there are _lots_ of those …
picotts documentation
picotts source
(message by IssueLinks)
Hey there @home-assistant/core, mind taking a look at this issue as its been labeled with an integration (script) you are listed as a codeowner for? Thanks!
(message by CodeOwnersMention)
Hey there @pvizeli, mind taking a look at this issue as its been labeled with an integration (tts) you are listed as a codeowner for? Thanks!
(message by CodeOwnersMention)
Same behaviour after update to 0.115.1.
Having an issue which looks similar with google_cloud tts, Indeed no issues with it prior to 0.115
Configuration.yaml
- platform: google_cloud
key_file: xxx
language: en-US
gender: female
voice: en-US-Wavenet-F
encoding: linear16
speed: 0.9
base_url: xxx
profiles:
- large-home-entertainment-class-device
Error
2020-09-20 21:13:57 ERROR (MainThread) [homeassistant.components.script.send_tts_message] send_tts_message: Error executing script. Unexpected error for call_service at pos 3: 'en-US-Wavenet-F' not a Frame instance
Traceback (most recent call last):
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/helpers/script.py", line 206, in _async_step
await getattr(
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/helpers/script.py", line 413, in _async_call_service_step
await service_task
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/core.py", line 1315, in async_call
task.result()
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/core.py", line 1350, in _execute_service
await handler.func(service_call)
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/components/tts/__init__.py", line 167, in async_say_handle
url = await tts.async_get_url(
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/components/tts/__init__.py", line 340, in async_get_url
filename = await self.async_get_tts_audio(
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/components/tts/__init__.py", line 367, in async_get_tts_audio
data = self.write_tags(filename, data, provider, message, language, options)
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/components/tts/__init__.py", line 470, in write_tags
tts_file["artist"] = artist
File "/srv/homeassistant/lib/python3.8/site-packages/mutagen/_file.py", line 74, in __setitem__
self.tags[key] = value
File "/srv/homeassistant/lib/python3.8/site-packages/mutagen/id3/_tags.py", line 339, in __setitem__
raise TypeError("%r not a Frame instance" % tag)
TypeError: 'en-US-Wavenet-F' not a Frame instance
2020-09-20 21:13:57 ERROR (MainThread) [homeassistant] Error doing job: Task exception was never retrieved
Traceback (most recent call last):
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/helpers/script.py", line 944, in async_run
await asyncio.shield(run.async_run())
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/helpers/script.py", line 663, in async_run
await super().async_run()
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/helpers/script.py", line 198, in async_run
await self._async_step(log_exceptions=False)
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/helpers/script.py", line 206, in _async_step
await getattr(
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/helpers/script.py", line 413, in _async_call_service_step
await service_task
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/core.py", line 1315, in async_call
task.result()
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/core.py", line 1350, in _execute_service
await handler.func(service_call)
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/components/tts/__init__.py", line 167, in async_say_handle
url = await tts.async_get_url(
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/components/tts/__init__.py", line 340, in async_get_url
filename = await self.async_get_tts_audio(
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/components/tts/__init__.py", line 367, in async_get_tts_audio
data = self.write_tags(filename, data, provider, message, language, options)
File "/srv/homeassistant/lib/python3.8/site-packages/homeassistant/components/tts/__init__.py", line 470, in write_tags
tts_file["artist"] = artist
File "/srv/homeassistant/lib/python3.8/site-packages/mutagen/_file.py", line 74, in __setitem__
self.tags[key] = value
File "/srv/homeassistant/lib/python3.8/site-packages/mutagen/id3/_tags.py", line 339, in __setitem__
raise TypeError("%r not a Frame instance" % tag)
TypeError: 'en-US-Wavenet-F' not a Frame instance
Definitely looks related. I wonder if HA’s TTS component tries to set the Artist, Title, and Language tags in order to make the file display nicely in players. (Which for instance worked ok for me pre-0.115 when using Google Translate TTS.)
Unfortunately, WAV has its own system for tagging (_not_ ID3 tags), and the _TLAN_ (language) ID3v2 tag which mutagen tries to set for MP3s requires the three-character ISO-639-2 "terminology" codes to be used (i.e., deu for German).
Wild assumption (without looking at the code): Probably mutagen checks for either language (not a valid tag name) or its contents (de-DE and en-US-Wavenet-F not being valid ISO-639-2 codes) and thus reports the error "not a Frame instance").
Simpler assumption: HA tries to _read_ the language tag through mutagen but there is no such Frame.
The easy solution would be not trying to read/add/modify a language tag to the TTS audio file, especially with all the lots of voice codes Google Cloud TTS uses. Due to the sheer amount of ISO language codes, it wouldn’t even be practical trying to map these to correct ones.
_Note:_ Should HA use mutagen’s "EasyID3", the key "language" will be registered to "TLAN" by default. So one could simply unregister it, or refrain from setting ezid3["language"] altogether. (See mutagen source.)
Further reading:
Old discussion on the Mp3Tag forums
ID3v2.3.0 TLAN
ID3v2.4.0 Frames (Mutagen’s default, search for TLAN)
I have a problem with tts_google_cloud too.
- alias: Pogoda
trigger:
- platform: state
entity_id: binary_sensor.kuchnia
from: 'off'
to: 'on'
condition:
- condition: template
value_template: >
{{as_timestamp(state_attr('automation.pogoda','last_triggered'))|timestamp_custom('%-d') != as_timestamp(now())|timestamp_custom('%-d')}}
- condition: time
after: '07:00'
before: '14:00'
action:
- service: media_player.volume_set
data_template:
entity_id: media_player.jbl
volume_level: 0.4
- service: tts.google_cloud_say
entity_id: media_player.jbl
data_template:
message: 'Dzień dobry. {{states("sensor.dzien_tygodnia")}}. Aktualna temperatura zewnętrzna wynosi {{states("sensor.zewnetrzny")}} stopnie. {% if is_state("weather.dark_sky", "partlycloudy") or is_state("weather.dark_sky",
"cloudy") -%} Dziś będzie pochmurno{%- endif %}{% if is_state("weather.dark_sky",
"pouring") or is_state("weather.dark_sky", "rainy") -%} Dzień będzie deszczowy {%-
endif %} {% if is_state("weather.dark_sky", "lightning-rainy") or is_state("weather.dom",
"lightning") -%} Spodziewane sÄ… burze z opadami deszczu {%- endif %} {% if
is_state("weather.dark_sky", "sunny") or is_state("weather.dark_sky", "exceptional")
-%} Poranek będzie słoneczny {%- endif %} {% if is_state("weather.dark_sky", "snowy") or
is_state("weather.dark_sky", "snowy-rainy") -%} Spodziewane są opady śniegu {%-
endif %} {% if is_state("weather.dark_sky", "hail") -%} Spodziewany dzisiaj jest
grad {%- endif %}. Temperatura w salonie wynosi {{states("sensor.salon_temperatura")}}
stopni. {% if is_state("calendar.piotr_majda_ledbaner_pl", "on") -%} Dzisiejszy wpis w kalendarzu to: {{states.calendar.piotr_majda_ledbaner_pl.attributes.message}}. {%- endif %} {% if is_state("calendar.contacts", "on") -%} {{states.calendar.contacts.attributes.message}}. {%- endif %} {% if is_state("calendar.swieta_w_polsce", "on") -%} Dziś jest {{states.calendar.swieta_w_polsce.attributes.message}}. {%- endif %} Życzę miłego dnia.'
- delay: 00:00:30
- service: media_player.volume_set
data_template:
entity_id: media_player.jbl
volume_level: 0.3
Traceback (most recent call last):
File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 206, in _async_step
await getattr(
File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 413, in _async_call_service_step
await service_task
File "/usr/src/homeassistant/homeassistant/core.py", line 1315, in async_call
task.result()
File "/usr/src/homeassistant/homeassistant/core.py", line 1350, in _execute_service
await handler.func(service_call)
File "/usr/src/homeassistant/homeassistant/components/tts/__init__.py", line 167, in async_say_handle
url = await tts.async_get_url(
File "/usr/src/homeassistant/homeassistant/components/tts/__init__.py", line 340, in async_get_url
filename = await self.async_get_tts_audio(
File "/usr/src/homeassistant/homeassistant/components/tts/__init__.py", line 367, in async_get_tts_audio
data = self.write_tags(filename, data, provider, message, language, options)
File "/usr/src/homeassistant/homeassistant/components/tts/__init__.py", line 470, in write_tags
tts_file["artist"] = artist
File "/usr/local/lib/python3.8/site-packages/mutagen/_file.py", line 74, in __setitem__
self.tags[key] = value
File "/usr/local/lib/python3.8/site-packages/mutagen/id3/_tags.py", line 339, in __setitem__
raise TypeError("%r not a Frame instance" % tag)
TypeError: 'pl-PL-Standard-A' not a Frame instance
The error is being caused by setting the ID3 tags with a string when it needs to be of type mutagen.id3.TextFame or some other Frame subclass.
The docs though seem to reference the ability to set the tags treating the File object as a dict. However testing does not seem to actually reflect it working that way. Even when RegisterTextKey those keys as a frame so that it should automatically handle it.
import mutagen
from mutagen.id3 import TextFrame as ID3Text
##... around :470...##
tts_file["artist"] = ID3Text(encoding=3, text=artist)
tts_file["album"] = ID3Text(encoding=3, text=album)
tts_file["title"] = ID3Text(encoding=3, text=message)
The above changes to the /usr/src/homeassistant/homeassistant/components/tts/__init__.py seem to resolve the issue.
I can confirm that did fix it, thanks.
I can also confirm that the bugfix works
@digitallyserviced please open a PR for this fix 👍
Glad to see it works for everyone who monkey patched my fix.
@springstan i have made a PR for the fix I described in my comment.
The error is being caused by setting the ID3 tags with a string when it needs to be of type
mutagen.id3.TextFameor some otherFramesubclass.The docs though seem to reference the ability to set the tags treating the
Fileobject as a dict. However testing does not seem to actually reflect it working that way. Even whenRegisterTextKeythose keys as a frame so that it should automatically handle it.import mutagen from mutagen.id3 import TextFrame as ID3Text ##... around :470...## tts_file["artist"] = ID3Text(encoding=3, text=artist) tts_file["album"] = ID3Text(encoding=3, text=album) tts_file["title"] = ID3Text(encoding=3, text=message)The above changes to the
/usr/src/homeassistant/homeassistant/components/tts/__init__.pyseem to resolve the issue.
apparently this change kills the yandextts integration and throws this error:
2020-10-01 16:07:22 ERROR (MainThread)
[homeassistant.components.websocket_api.http.connection.140588446053808] TextFrame(encoding=<Encoding.UTF8: 3>, text=['ru-RU']) needs to be str for key 'artist'
Traceback (most recent call last):
File "/usr/src/homeassistant/homeassistant/components/websocket_api/commands.py", line 137, in handle_call_service
await hass.services.async_call(
File "/usr/src/homeassistant/homeassistant/core.py", line 1315, in async_call
task.result()
File "/usr/src/homeassistant/homeassistant/core.py", line 1350, in _execute_service
await handler.func(service_call)
File "/usr/src/homeassistant/homeassistant/components/tts/__init__.py", line 168, in async_say_handle
url = await tts.async_get_url(
File "/usr/src/homeassistant/homeassistant/components/tts/__init__.py", line 341, in async_get_url
filename = await self.async_get_tts_audio(
File "/usr/src/homeassistant/homeassistant/components/tts/__init__.py", line 368, in async_get_tts_audio
data = self.write_tags(filename, data, provider, message, language, options)
File "/usr/src/homeassistant/homeassistant/components/tts/__init__.py", line 474, in write_tags
tts_file.save(data_bytes)
File "/usr/local/lib/python3.8/site-packages/mutagen/_util.py", line 156, in wrapper
return func(self, h, *args, **kwargs)
File "/usr/local/lib/python3.8/site-packages/mutagen/ogg.py", line 587, in save
self.tags._inject(filething.fileobj, padding)
File "/usr/local/lib/python3.8/site-packages/mutagen/oggopus.py", line 122, in _inject
vcomment_data = b"OpusTags" + self.write(framing=False)
File "/usr/local/lib/python3.8/site-packages/mutagen/_vorbis.py", line 178, in write
self.validate()
File "/usr/local/lib/python3.8/site-packages/mutagen/_vorbis.py", line 158, in validate
raise ValueError(err)
ValueError: TextFrame(encoding=<Encoding.UTF8: 3>, text=['ru-RU']) needs to be str for key 'artist'
do you have any clue on how to fix this?
@balonchiks this should be fixed with #40740 released in 0.155.5
apparently it doesn't fix it for yandextts. i am on 0.115.6 (tried 0.155.5 too) but it still fails with the above mentioned error. will it make sense if i say that yandextts uses oggopus as a codec? could that be an issue?
Please open a new issue if this fix did not solve your problem.
Most helpful comment
Glad to see it works for everyone who monkey patched my fix.
@springstan i have made a PR for the fix I described in my comment.
40666