Write standard ID3 frames for TTS tags (#182819)

This commit is contained in:
Bryce Boe
2026-09-21 02:39:33 -04:00
committed by GitHub
parent daa194b3f9
commit 5a9f68bb28
4 changed files with 114 additions and 18 deletions
+4 -18
View File
@@ -18,7 +18,7 @@ from typing import Any, Final, Protocol
from aiohttp import web
import mutagen
from mutagen.id3 import ID3, TextFrame as ID3Text
from mutagen.id3 import TALB, TIT2, TPE1, Encoding
import probatio
from propcache.api import cached_property
@@ -1195,23 +1195,9 @@ class SpeechManager:
if tts_file is not None:
if not tts_file.tags:
tts_file.add_tags()
if isinstance(tts_file.tags, ID3):
tts_file["artist"] = ID3Text(
encoding=3,
text=artist, # type: ignore[no-untyped-call]
)
tts_file["album"] = ID3Text(
encoding=3,
text=album, # type: ignore[no-untyped-call]
)
tts_file["title"] = ID3Text(
encoding=3,
text=message, # type: ignore[no-untyped-call]
)
else:
tts_file["artist"] = artist
tts_file["album"] = album
tts_file["title"] = message
tts_file.tags.add(TPE1(encoding=Encoding.UTF8, text=artist)) # type: ignore[no-untyped-call]
tts_file.tags.add(TALB(encoding=Encoding.UTF8, text=album)) # type: ignore[no-untyped-call]
tts_file.tags.add(TIT2(encoding=Encoding.UTF8, text=message)) # type: ignore[no-untyped-call]
data_bytes.seek(0)
tts_file.save(data_bytes)
except mutagen.MutagenError as err:
Binary file not shown.
Binary file not shown.
+110
View File
@@ -11,6 +11,8 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
import wave
from freezegun.api import FrozenDateTimeFactory
import mutagen
from mutagen.id3 import TIT2, TPE1, Encoding
import pytest
from homeassistant.components import ffmpeg, tts
@@ -2327,3 +2329,111 @@ def test_write_tags_keeps_single_id3_tag() -> None:
assert tagged.startswith(b"ID3")
assert tagged.count(b"ID3") == 1
def test_write_tags_sets_standard_id3_frames() -> None:
"""Test tagging audio carrying only the encoder frame sets standard frames."""
data = load_fixture_bytes("tagged.mp3", DOMAIN)
assert list(mutagen.File(io.BytesIO(data)).tags) == ["TSSE"]
tagged = ORIG_WRITE_TAGS(
"42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_test.mp3",
data,
"Test",
"There is someone at the door.",
"en",
None,
)
tags = mutagen.File(io.BytesIO(tagged)).tags
assert tags["TPE1"].text == ["en"]
assert tags["TALB"].text == ["Test"]
assert tags["TIT2"].text == ["There is someone at the door."]
assert "TSSE" in tags
def test_write_tags_adds_tag_to_untagged_audio() -> None:
"""Test audio arriving without an ID3 tag gets one holding the frames."""
data = load_fixture_bytes("untagged.mp3", DOMAIN)
assert not data.startswith(b"ID3")
tagged = ORIG_WRITE_TAGS(
"42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_test.mp3",
data,
"Test",
"There is someone at the door.",
"en",
None,
)
assert tagged.startswith(b"ID3")
assert tagged.count(b"ID3") == 1
tags = mutagen.File(io.BytesIO(tagged)).tags
assert tags["TPE1"].text == ["en"]
assert tags["TALB"].text == ["Test"]
assert tags["TIT2"].text == ["There is someone at the door."]
def test_write_tags_overwrites_id3v1_metadata() -> None:
"""Test audio arriving with only an ID3v1 trailer gets the frames rewritten."""
data = load_fixture_bytes("id3v1.mp3", DOMAIN)
assert not data.startswith(b"ID3")
assert data[-128:-125] == b"TAG"
tagged = ORIG_WRITE_TAGS(
"42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_test.mp3",
data,
"Test",
"There is someone at the door.",
"en",
None,
)
assert tagged.startswith(b"ID3")
tags = mutagen.File(io.BytesIO(tagged)).tags
assert tags["TPE1"].text == ["en"]
assert tags["TALB"].text == ["Test"]
assert tags["TIT2"].text == ["There is someone at the door."]
def test_write_tags_replaces_existing_frames() -> None:
"""Test frames carried through conversion from the provider are replaced."""
data = load_fixture_bytes("untagged.mp3", DOMAIN)
source = io.BytesIO(data)
source.name = "source.mp3"
source_file = mutagen.File(source)
source_file.add_tags()
source_file.tags.add(TIT2(encoding=Encoding.UTF8, text="Provider title"))
source_file.tags.add(TPE1(encoding=Encoding.UTF8, text="Provider artist"))
source.seek(0)
source_file.save(source)
tagged = ORIG_WRITE_TAGS(
"42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_test.mp3",
source.getvalue(),
"Test",
"There is someone at the door.",
"en",
None,
)
tags = mutagen.File(io.BytesIO(tagged)).tags
assert tags["TIT2"].text == ["There is someone at the door."]
assert tags["TPE1"].text == ["en"]
def test_write_tags_uses_voice_as_artist() -> None:
"""Test the voice option replaces the language as the artist frame."""
data = load_fixture_bytes("tagged.mp3", DOMAIN)
tagged = ORIG_WRITE_TAGS(
"42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_test.mp3",
data,
"Test",
"There is someone at the door.",
"en",
{"voice": "JennyNeural"},
)
tags = mutagen.File(io.BytesIO(tagged)).tags
assert tags["TPE1"].text == ["JennyNeural"]