Conversation
…time behavior - refactor AI Radio plugin structure and web UI flows (wizard, sections, run controls) - improve config clarity/help text and standalone MA-styled web UI experience - add detailed runtime phases/logging and human-readable session status rendering - improve dynamic batching with earlier prefetch trigger (`dynamic_prefetch_remaining_tracks`) - add advanced plugin setting for UI auto-refresh interval (`ui_auto_refresh_seconds`) - fix typing issues and expand provider tests
Remove AI-Radio subproject
…e prompts, and improve styles; add air.png to mp3 metadata.
…tyling for better layout and usability. Adjust grid alignment, input heights, and import label styles for improved user experience.
…o-generate IDs from names, and enhance UI layout for better usability.
…ndling, session cancellation, and normalization logic
…ove error handling
🔒 Dependency Security Report✅ No dependency changes detected in this PR. |
There was a problem hiding this comment.
Pull request overview
Adds a new AI Radio plugin provider to Music Assistant that can generate radio-style moderator sections (LLM + TTS) and insert them into a playlist (playlist mode) or generate them on-the-fly while queueing (dynamic mode).
Changes:
- Introduces a new
ai_radioplugin provider with storage/normalization and runtime execution logic (LLM generation, TTS synthesis, optional weather enrichment). - Registers new admin-only API commands for station/section CRUD and run control (start/stop/status).
- Adds unit tests for storage normalization, runtime session state handling, and provider validation helpers.
Reviewed changes
Copilot reviewed 15 out of 17 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| music_assistant/providers/ai_radio/provider.py | Plugin provider entrypoint, API route registration, run lifecycle management. |
| music_assistant/providers/ai_radio/runtime.py | Core runtime for playlist/dynamic modes, planning/generation/TTS, weather token prep. |
| music_assistant/providers/ai_radio/storage.py | Persistent storage + normalization for stations/sections and defaults. |
| music_assistant/providers/ai_radio/models.py | Dataclasses/utilities for session state, slot planning, text limiting, etc. |
| music_assistant/providers/ai_radio/constants.py | Plugin constants and defaults. |
| music_assistant/providers/ai_radio/config.py | Provider config entries (API keys, UI refresh interval, UI link label). |
| music_assistant/providers/ai_radio/manifest.json | Declares the plugin manifest. |
| music_assistant/providers/ai_radio/example_station.json | Example station definition for reference/testing. |
| tests/providers/ai_radio/* | Unit tests covering storage/runtime/provider helper behavior. |
| scripts/update_and_redeploy_container.sh | Local dev helper script for building and redeploying a container. |
| DEVELOPMENT.md | Adds instructions for building a local Python artifact for Docker builds. |
| track_entry_local_indices = [ | ||
| index for index, uri in enumerate(entries) if "://track/" in uri |
| parsed = {item["id"]: item for item in defaults} | ||
| await self._write_sections() |
| raw_constraints = section.get("constraints") | ||
| max_chars = 0 | ||
| if isinstance(raw_constraints, dict): | ||
| max_chars = int(raw_constraints.get("max_chars", 0) or 0) |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new AI Radio plugin provider that can generate LLM-driven “radio host” sections (with optional weather/news) and synthesize them to TTS MP3s, supporting both prebuilt playlist mode and dynamic queue mode.
Changes:
- Introduces the
ai_radioplugin provider (storage, runtime execution, config entries, manifest, example station). - Adds unit tests covering storage normalization, runtime session state/logging, and provider helper validation.
- Adds a local Docker redeploy helper script and a short DEVELOPMENT.md note about building Python artifacts.
Reviewed changes
Copilot reviewed 15 out of 17 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| music_assistant/providers/ai_radio/provider.py | Plugin provider wiring: API commands, station/section CRUD, session lifecycle. |
| music_assistant/providers/ai_radio/runtime.py | Core runtime: planning sections, LLM calls, weather tokens, TTS synthesis, queue/playlist publishing. |
| music_assistant/providers/ai_radio/storage.py | Disk persistence + normalization/validation for stations/sections/general settings. |
| music_assistant/providers/ai_radio/models.py | Dataclasses/utilities used across runtime/provider (slots, session state, helpers). |
| music_assistant/providers/ai_radio/config.py | Provider config entries (API keys, UI refresh, UI URL label). |
| music_assistant/providers/ai_radio/constants.py | Plugin constants/defaults (models, providers, limits, tokens). |
| music_assistant/providers/ai_radio/manifest.json | Declares the plugin provider and its metadata. |
| music_assistant/providers/ai_radio/example_station.json | Example station payload for reference/testing. |
| music_assistant/providers/ai_radio/init.py | Exposes setup + config entries for provider loading. |
| tests/providers/ai_radio/test_storage.py | Unit tests for storage normalization/materialization behavior. |
| tests/providers/ai_radio/test_runtime.py | Unit tests for session execution state changes and weather-token prep warnings. |
| tests/providers/ai_radio/test_provider.py | Unit tests for provider validation logic (dynamic-mode requirements, UI settings). |
| tests/providers/ai_radio/init.py | Test package marker. |
| scripts/update_and_redeploy_container.sh | Local helper to build artifacts and redeploy a Docker container. |
| DEVELOPMENT.md | Adds short instructions for building dist/ artifacts for Docker builds. |
| parsed = {item["id"]: item for item in defaults} | ||
| await self._write_sections() |
| raw_constraints = section.get("constraints") | ||
| max_chars = 0 | ||
| if isinstance(raw_constraints, dict): | ||
| max_chars = int(raw_constraints.get("max_chars", 0) or 0) |
| "instructions": str(source_general.get("instructions", defaults["instructions"])), | ||
| "openai_base_url": _text("openai_base_url"), | ||
| "section_store_path": _text("section_store_path"), | ||
| "weather_provider": _text("weather_provider"), | ||
| "weather_timeout_seconds": _number("weather_timeout_seconds", int), | ||
| "tts_provider": _text("tts_provider"), | ||
| "openai_tts_model": _text("openai_tts_model"), | ||
| "openai_tts_voice": _text("openai_tts_voice"), | ||
| "openai_tts_instructions": str( | ||
| source_general.get( | ||
| "openai_tts_instructions", | ||
| defaults["openai_tts_instructions"], | ||
| ) | ||
| ), | ||
| "elevenlabs_model": _text("elevenlabs_model"), | ||
| "elevenlabs_voice_id": str( | ||
| source_general.get("elevenlabs_voice_id", defaults["elevenlabs_voice_id"]) | ||
| ).strip(), | ||
| } |
| "authorization": f"Bearer {api_key}", | ||
| "content-type": "application/json", | ||
| "accept": accept, | ||
| }, |
| CONTAINER_NAME="${CONTAINER_NAME:-music-assistant-local}" | ||
| IMAGE_NAME="${IMAGE_NAME:-ma-server-local}" | ||
| MASS_VERSION="${MASS_VERSION:-0.0.0}" | ||
| NETWORK_MODE="${NETWORK_MODE:-host}" | ||
|
|
||
| DATA_DIR="${DATA_DIR:-/path/to/mass_data}" | ||
| MUSIC_DIR="${MUSIC_DIR:-/path/to/music}" | ||
| TIMEZONE_FILE="${TIMEZONE_FILE:-/path/to/timezone/file}" | ||
|
|
||
| TOTAL_STEPS=5 | ||
| if [[ -n "$FRONTEND_PATH_ARG" ]]; then | ||
| TOTAL_STEPS=6 | ||
| fi |
There was a problem hiding this comment.
What is the use of this png? I don't think it's used anywhere?
There was a problem hiding this comment.
Its used to generate the cover images of the inserted MP3 files:
title = f"{section.section_name} [{run_id}]"
cover_art_path = Path(__file__).with_name("air.png")
await asyncio.to_thread(
write_id3_tags,
str(file_path),
title,
"AI Radio",
str(cover_art_path) if cover_art_path.exists() else None,
)| ConfigEntry( | ||
| key=CONF_ELEVENLABS_API_KEY, | ||
| type=ConfigEntryType.SECURE_STRING, | ||
| label="ElevenLabs API Key", |
There was a problem hiding this comment.
Just wondering about pricing here. I see ElevenLabs offers a free tier of 10k tokens. Is that enough to run the radio DJ daily?
There was a problem hiding this comment.
Yeah it's tricky, and the tokens run out surprisingly fast when debugging stuff 😅 For daily usage the paid tier is something you cant really avoid (except when keeping the text characters to a minimum and using the cheaper model only).
Alternatively OpenAI TTS is not too bad as well and there you pay only a few cents per usage.
- date operations now use MA's helper instead of datetime
There was a problem hiding this comment.
Pull request overview
Adds a new AI Radio plugin provider to Music Assistant, enabling “radio-like” playback by generating LLM-based spoken segments (and TTS MP3s) that can be inserted into a generated playlist or dynamically into a queue at runtime.
Changes:
- Introduces the
ai_radioplugin provider with storage/normalization, runtime generation (LLM + TTS), and admin-only API commands. - Adds default section/station templates plus an example station JSON.
- Adds unit tests covering storage normalization, runtime session handling, and provider helper behaviors.
Reviewed changes
Copilot reviewed 13 out of 15 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/providers/ai_radio/init.py | Adds test package marker for AI Radio tests. |
| tests/providers/ai_radio/test_storage.py | Unit tests for storage normalization/materialization logic. |
| tests/providers/ai_radio/test_runtime.py | Unit tests for runtime session flow and weather-token prep behavior. |
| tests/providers/ai_radio/test_provider.py | Unit tests for provider session resolution and run-start validation. |
| music_assistant/providers/ai_radio/init.py | Exposes provider setup and config entry factory. |
| music_assistant/providers/ai_radio/config.py | Adds plugin config entries (API keys, UI refresh interval, UI URL label). |
| music_assistant/providers/ai_radio/constants.py | Defines plugin constants/defaults (models, providers, paths, modes). |
| music_assistant/providers/ai_radio/models.py | Adds AI Radio dataclasses and helper utilities (slots, coercion, ID3 tagging). |
| music_assistant/providers/ai_radio/provider.py | Implements plugin provider: API endpoints, station/section CRUD, run lifecycle. |
| music_assistant/providers/ai_radio/runtime.py | Implements run execution logic: planning, LLM generation, TTS, weather lookup, queue/playlist composition. |
| music_assistant/providers/ai_radio/storage.py | Implements persistence and normalization for stations/sections JSON storage. |
| music_assistant/providers/ai_radio/manifest.json | Registers the AI Radio plugin manifest (alpha stage). |
| music_assistant/providers/ai_radio/example_station.json | Provides an example station configuration payload for reference/testing. |
| await super().unload(is_removed) | ||
|
|
||
| async def update_config(self, config: ProviderConfig, changed_keys: set[str]) -> None: | ||
| """Apply config updates without forcing a provider reload.""" |
There was a problem hiding this comment.
Copilot has a point here. Since you're not doing anything fancy, why override this function at all?
| parsed = {item["id"]: item for item in defaults} | ||
| await self._write_sections() |
| if not stations_file_exists: | ||
| default_station = self._default_station_template() | ||
| self._stations = {default_station["id"]: default_station} | ||
| await self._write_stations() | ||
| return |
| "target_playlist_provider": str(station.get("target_playlist_provider", "builtin")), | ||
| "default_player_id": str(station.get("default_player_id", "")), | ||
| "max_duration_minutes": float(station.get("max_duration_minutes", 0) or 0), | ||
| "dynamic_batch_size": max(1, int(station.get("dynamic_batch_size", 1) or 1)), | ||
| "dynamic_poll_seconds": max(1, int(station.get("dynamic_poll_seconds", 5) or 5)), | ||
| "dynamic_prefetch_remaining_tracks": max( | ||
| 1, int(station.get("dynamic_prefetch_remaining_tracks", 2) or 2) | ||
| ), |
MarvinSchenkel
left a comment
There was a problem hiding this comment.
First off, amazing job, this is a very cool feature 👏 .
Please also have a look at the copilot comments. Most of them are valid.
As for a rule of thumb on storage, any data that should be persisted for a longer time (e.g. the json files containing stations and segments, but also the audio files for the permanent playlists) can we saved to mass.storage_path. The audio files for the dynamic radio mode should be saved to the cache path (mass.cache_path) to ensure they are clean up and not included in backups.
Marking this PR as draft, please have a look at the feedback and let us know when we can have another look by clicking 'ready for review' 🙏
| from .constants import EMPTY_SECTION_ID | ||
|
|
||
|
|
||
| class AIRadioError(RuntimeError): |
There was a problem hiding this comment.
Any specific reason we need to create a new error here instead of reusing the MusicAssistantError?
| """Raised when AI Radio encounters an unrecoverable error.""" | ||
|
|
||
|
|
||
| def utc_now_iso() -> str: |
There was a problem hiding this comment.
We have helpers available for datetime in helpers/datetime.py. Please use those.
| } | ||
|
|
||
|
|
||
| def slugify(value: str) -> str: |
There was a problem hiding this comment.
I would keep this file exclusively for models. So just move all the functions below this comment into 'helpers.py'
| ) | ||
|
|
||
|
|
||
| if TYPE_CHECKING: |
There was a problem hiding this comment.
This should be at the top of the file.
| await super().unload(is_removed) | ||
|
|
||
| async def update_config(self, config: ProviderConfig, changed_keys: set[str]) -> None: | ||
| """Apply config updates without forcing a provider reload.""" |
There was a problem hiding this comment.
Copilot has a point here. Since you're not doing anything fancy, why override this function at all?
|
Also when this gets closer to being merged please raise a PR against the beta branch of our docs repo. See the existing pages for inspiration. As this would appear to be an advanced plugin I would expect some clear instructions for how to setup the AI part (or link to existing instructions) |
As discussed in Discord here is the pull request for my AI Radio plugin.
The plugin allows a user to define a 'station' and specify a MA playlist and the tool will generate the text and respective TTS mp3 files and insert them into a new playlist (or to a current queue) making it more like a radio like experience with weather reports, song intro and outros, news etc.
Features
Notes
scripts/update_and_redeploy_container.shto help me update my local deployment, but if you want I can remove thatDEVELOPMENT.mdas close as possible. Though this is the first time contributing to this project so there are certainly things I missed so have mercy 😅Screenshots
Backend (API)
Frontend