/
/
/
1"""Constants for the AI Radio plugin."""
2
3from __future__ import annotations
4
5from typing import Any
6
7from music_assistant_models.enums import ContentType
8from music_assistant_models.media_items import AudioFormat
9
10CONF_AI_ENGINE = "ai_engine"
11CONF_TTS_ENGINE = "tts_engine"
12CONF_TTS_LOUDNESS_BOOST = "tts_loudness_boost"
13CONF_TIMEZONE = "timezone"
14CONF_WEATHER_CITY = "weather_city"
15CONF_WEATHER_COUNTRY = "weather_country"
16CONF_WEATHER_PROVIDER = "weather_provider"
17CONF_WEATHER_TIMEOUT = "weather_timeout_seconds"
18
19# providers load concurrently, so the plugin supplying the engines may still be
20# loading when AI Radio initializes: wait this long for it before giving up
21ENGINE_DISCOVERY_TIMEOUT = 30
22
23# grace period for an engine that disappears while AI Radio is loaded. Generous enough
24# to sit out a Home Assistant restart, so a running show is not torn down for it
25ENGINE_RECHECK_GRACE = 300
26
27# how long to wait before reloading after an engine stayed missing, matching the
28# cadence the load path uses for its own retries
29ENGINE_RETRY_DELAY = 120
30
31TRANSLATION_OWNER = "provider.ai_radio"
32
33DEFAULT_LLM_INSTRUCTIONS = (
34 "Host personality: warm, sharp, music-literate, and slightly premium "
35 "without sounding formal. Program instructions: write for spoken delivery, "
36 "keep segments concise, avoid bullet-point phrasing, avoid clichés, "
37 "mention concrete details when available, and maintain a believable "
38 "radio flow between sections."
39)
40# appended to every AI query on top of the station's own instructions: how a name has to be
41# spelled to survive the TTS engine is a pipeline concern, not a per-station style choice
42TTS_PRONUNCIATION_INSTRUCTIONS = (
43 "The output is sent directly to a text-to-speech engine. "
44 "Always write names exactly as they should be spoken aloud. Replace stylized spellings, "
45 "acronyms, abbreviations, and unusual artist or band names with their natural spoken "
46 "equivalents. Never include the original spelling, pronunciation explanation, phonetic "
47 "notation, or both versions. Output only the spoken version. Examples: INXS â In Excess; "
48 "Mi-Sex â My Sex; P!nk â Pink; blink-182 â Blink One Eighty-Two. If a name could be "
49 "mispronounced by the TTS engine, rewrite it into the clearest natural spoken form "
50 "without explaining the change. "
51 "Names and titles often stay in their original language while the voice reads everything "
52 "with the pronunciation rules of the script's language. When a name would be mangled that "
53 "way, respell it phonetically for the script's language so it still sounds like the "
54 "original; leave names that already read correctly untouched."
55)
56MERGE_SECTION_PROMPT = (
57 "Merge the drafts below into one coherent radio break. "
58 "Preserve factual content, remove duplication, and make the "
59 "final segment sound like one host speaking naturally.\n"
60 "<section_drafts>"
61)
62DEFAULT_WEATHER_PROVIDER = "open_meteo"
63DEFAULT_WEATHER_TIMEOUT_SECONDS = 20
64
65# countries and US territories that use Fahrenheit for everyday temperatures
66FAHRENHEIT_COUNTRY_CODES = frozenset(
67 {"US", "PR", "GU", "VI", "AS", "MP", "LR", "MM", "BS", "BZ", "KY", "PW"}
68)
69DEFAULT_MAX_CONCURRENT_RUNS = 1
70MAX_FINISHED_SESSIONS = 20
71
72# a show whose playback never starts within this window is declared failed
73SHOW_START_TIMEOUT_SECONDS = 300
74
75# last-resort guard so a wedged engine fails the clip instead of hanging the session.
76# Kept above the deadlines the engines apply themselves (120s in the OpenAI-compatible
77# providers), so their own, more specific error is the one that surfaces.
78AI_QUERY_TIMEOUT_SECONDS = 180
79
80# ffprobe reports no status code, so its message is all we have to spot a failed render
81TTS_SERVER_ERROR_MARKERS = ("Server returned 5XX", "HTTP error 5")
82
83DEFAULT_TTS_LOUDNESS_BOOST = 3
84
85# speech carries ~16 dB between its average level and its peaks, so a plain gain that
86# reaches the target clips instead. speechnorm evens the clip out so the level is carried
87# by the whole clip, the trim then places it, and the limiter backstops the peaks
88TTS_SPEECHNORM_FILTER = "speechnorm=e=12.5:r=0.0005:l=1"
89TTS_PEAK_CEILING_DB = -1.5
90
91# one measurement stands in for every clip an engine voices, but a fragment of a few
92# words is not representative enough of its level to become that reference
93MIN_LOUDNESS_REFERENCE_SECONDS = 2
94
95# a clip is seconds of audio, so a measurement that takes this long is a wedged fetch
96LOUDNESS_MEASURE_TIMEOUT = 60
97
98# spoken clips are handed to MA already decoded, so the filter chain runs once here
99# instead of once per output
100TTS_CLIP_PCM_FORMAT = AudioFormat(
101 content_type=ContentType.PCM_S16LE,
102 sample_rate=48000,
103 bit_depth=16,
104 channels=2,
105)
106
107SUPPORTED_FEATURES: set[Any] = set()
108EMPTY_SECTION_ID = "EMPTY_SECTION"
109VALID_WEB_SEARCH_MODES = {"disabled", "allow", "force"}
110WEB_SEARCH_MODE_RANK = {"disabled": 0, "allow": 1, "force": 2}
111
112# QueueItem.extra_attributes keys carrying a clip's pending render state. Scalars only â
113# extra_attributes is serialized to clients and persisted with the queue.
114ATTR_SESSION_ID = "ai_radio_session_id"
115ATTR_STATION_ID = "ai_radio_station_id"
116ATTR_PROMPT = "ai_radio_prompt"
117ATTR_MAX_CHARS = "ai_radio_max_chars"
118ATTR_WEB_SEARCH_MODE = "ai_radio_web_search_mode"
119ATTR_RENDERED_TEXT = "ai_radio_rendered_text"
120ATTR_HOST_ID = "ai_radio_host_id"
121ATTR_QUEUE_DJ = "ai_radio_queue_dj"
122ATTR_GAP_NEXT_ID = "ai_radio_gap_next_id"
123ATTR_WEATHER_REQUIRED = "ai_radio_weather_required"
124
125# placeholders resolved at render time rather than at plan time, so the aired script
126# reflects the moment it plays
127DEFERRED_PLACEHOLDERS = frozenset({"<timestamp>", "<weather_hourly>", "<weather_daily>"})
128
129# the deferred placeholders that need a successful weather fetch to say anything at all
130WEATHER_PLACEHOLDER_TOKENS = ("<weather_hourly>", "<weather_daily>")
131
132# substituted for an unresolved weather token in clips that still air
133NO_WEATHER_DATA_INSTRUCTION = (
134 "(no weather data available - leave out all weather talk, do not invent a forecast)"
135)
136
137# HA drops a tts_proxy token 60s after its last use at the lowest configurable time_memory
138CLIP_STREAMDETAILS_EXPIRATION = 60
139
140# a cached clip with less life than this left is not worth handing out, so it is re-minted
141MIN_CLIP_MEDIA_LIFETIME = 5
142