-
-
Notifications
You must be signed in to change notification settings - Fork 369
Add AI Radio Plugin #3407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Add AI Radio Plugin #3407
Changes from 26 commits
53da7c3
095ff9c
121b3d4
ca2350b
e283e9b
ad0ecdb
5a8566f
74c3fb2
920126e
ce3504f
adc30e4
0ebd06d
c6394ac
43acf80
a0658cb
9206e5d
8c261f0
0885cc3
0b5860c
149f26b
2e1e67a
c12f698
f2b1944
423695e
198fb93
42088b1
d40a395
f424660
85cb4b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| """AI Radio plugin package.""" | ||
|
|
||
| from .config import get_config_entries | ||
| from .provider import AIRadioProvider, setup | ||
|
|
||
| __all__ = ["AIRadioProvider", "get_config_entries", "setup"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| """Configuration entries for the AI Radio plugin.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| from music_assistant_models.config_entries import ConfigEntry, ConfigValueType | ||
| from music_assistant_models.enums import ConfigEntryType | ||
|
|
||
| from .constants import ( | ||
| CONF_ELEVENLABS_API_KEY, | ||
| CONF_OPENAI_API_KEY, | ||
| CONF_UI_AUTO_REFRESH_SECONDS, | ||
| ) | ||
|
|
||
|
|
||
| async def get_config_entries( | ||
| mass: MusicAssistant, | ||
| instance_id: str | None = None, # noqa: ARG001 | ||
| action: str | None = None, # noqa: ARG001 | ||
| values: dict[str, ConfigValueType] | None = None, # noqa: ARG001 | ||
| ) -> tuple[ConfigEntry, ...]: | ||
| """Return Config entries to setup this provider.""" | ||
| base_url = mass.webserver.base_url.rstrip("/") | ||
| web_ui_url = f"{base_url}/#/ai-radio" | ||
| return ( | ||
| ConfigEntry( | ||
| key="web_ui_url", | ||
| type=ConfigEntryType.LABEL, | ||
| label="Click (?) to open AI Radio User Interface", | ||
| description=web_ui_url, | ||
| ), | ||
| ConfigEntry( | ||
| key=CONF_OPENAI_API_KEY, | ||
| type=ConfigEntryType.SECURE_STRING, | ||
| label="OpenAI API Key", | ||
| description="Required for AI text generation and OpenAI TTS.", | ||
| required=False, | ||
| ), | ||
| ConfigEntry( | ||
| key=CONF_ELEVENLABS_API_KEY, | ||
| type=ConfigEntryType.SECURE_STRING, | ||
| label="ElevenLabs API Key", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just wondering about pricing here. I see ElevenLabs offers a free tier of 10k tokens. Is that enough to run the radio DJ daily?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
| required=False, | ||
| ), | ||
| ConfigEntry( | ||
| key=CONF_UI_AUTO_REFRESH_SECONDS, | ||
| type=ConfigEntryType.INTEGER, | ||
| default_value=2, | ||
| range=(1, 30), | ||
| label="Web UI Auto Refresh Interval (seconds)", | ||
| description=( | ||
| "How often the AI Radio web UI refreshes session/player status automatically." | ||
| ), | ||
| category="advanced", | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| if TYPE_CHECKING: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be at the top of the file. |
||
| from music_assistant.mass import MusicAssistant | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| """Constants for the AI Radio plugin.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Any | ||
|
|
||
| CONF_OPENAI_API_KEY = "openai_api_key" | ||
| CONF_ELEVENLABS_API_KEY = "elevenlabs_api_key" | ||
| CONF_UI_AUTO_REFRESH_SECONDS = "ui_auto_refresh_seconds" | ||
|
|
||
| DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1" | ||
| DEFAULT_LLM_MODEL = "gpt-4o-mini" | ||
| DEFAULT_LLM_INSTRUCTIONS = ( | ||
| "Host personality: warm, sharp, music-literate, and slightly premium " | ||
| "without sounding formal. Program instructions: write for spoken delivery, " | ||
| "keep segments concise, avoid bullet-point phrasing, avoid clichés, " | ||
| "mention concrete details when available, and maintain a believable " | ||
| "radio flow between sections." | ||
| ) | ||
| DEFAULT_TEMPERATURE = 0.7 | ||
| DEFAULT_MAX_TOKENS = 900 | ||
| DEFAULT_TTS_PROVIDER = "openai" | ||
| DEFAULT_OPENAI_TTS_MODEL = "gpt-4o-mini-tts" | ||
| DEFAULT_OPENAI_TTS_VOICE = "ballad" | ||
| DEFAULT_OPENAI_TTS_INSTRUCTIONS = ( | ||
| "Delivery instructions: confident radio host, natural pacing, " | ||
| "clear sentence endings, subtle energy lift on intros and transitions, " | ||
| "and a warm close without exaggerated theatrics." | ||
| ) | ||
| DEFAULT_ELEVENLABS_MODEL = "eleven_multilingual_v2" | ||
| DEFAULT_SECTION_STORE_PATH = "ai_radio_sections" | ||
| DEFAULT_WEATHER_PROVIDER = "open_meteo" | ||
| DEFAULT_WEATHER_TIMEOUT_SECONDS = 20 | ||
| DEFAULT_MAX_CONCURRENT_RUNS = 1 | ||
|
|
||
| SUPPORTED_FEATURES: set[Any] = set() | ||
| EMPTY_SECTION_ID = "EMPTY_SECTION" | ||
| VALID_WEB_SEARCH_MODES = {"disabled", "allow", "force"} | ||
| WEB_SEARCH_MODE_RANK = {"disabled": 0, "allow": 1, "force": 2} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| { | ||
| "id": "example_station", | ||
| "name": "Example AI Radio Station", | ||
| "source_playlist_id": "4", | ||
| "source_playlist_provider": "library", | ||
| "target_playlist_provider": "builtin", | ||
| "default_player_id": "", | ||
| "max_duration_minutes": 60, | ||
| "dynamic_batch_size": 1, | ||
| "dynamic_prefetch_remaining_tracks": 2, | ||
| "dynamic_poll_seconds": 5, | ||
| "clear_queue_on_start": true, | ||
| "general": { | ||
| "timezone": "UTC", | ||
| "location": { | ||
| "city": "Berlin", | ||
| "country": "Germany" | ||
| }, | ||
| "weather_provider": "open_meteo", | ||
| "weather_timeout_seconds": 20, | ||
| "model": "gpt-4o-mini", | ||
| "temperature": 0.7, | ||
| "max_tokens": 900, | ||
| "instructions": "Host personality: warm, sharp, music-literate, and slightly premium without sounding formal. Program instructions: write for spoken delivery, keep segments concise, avoid bullet-point phrasing, avoid clichés, mention concrete details when available, and maintain a believable radio flow between sections.", | ||
| "section_store_path": "ai_radio_sections", | ||
| "tts_provider": "openai", | ||
| "openai_tts_model": "gpt-4o-mini-tts", | ||
| "openai_tts_voice": "ballad", | ||
| "openai_tts_instructions": "Delivery instructions: confident radio host, natural pacing, clear sentence endings, subtle energy lift on intros and transitions, and a warm close without exaggerated theatrics.", | ||
| "elevenlabs_model": "eleven_multilingual_v2", | ||
| "elevenlabs_voice_id": "" | ||
| }, | ||
| "sections": [ | ||
| { | ||
| "id": "Song_Introduction_Start", | ||
| "name": "Song Introduction Start", | ||
| "type": "ai_text", | ||
| "web_search": "disabled", | ||
| "prompt": "The next track is <next_songinfo>. Open the program like a polished radio host: brief welcome, confident energy, one concrete hook about the song or artist, and a clean handoff into the music.", | ||
| "constraints": { | ||
| "max_chars": 650 | ||
| } | ||
| }, | ||
| { | ||
| "id": "Song_Transition", | ||
| "name": "Song Transition", | ||
| "type": "ai_text", | ||
| "web_search": "allow", | ||
| "prompt": "The previous track was <prev_songinfo> and the next track is <next_songinfo>. Create a natural radio transition that connects both songs, sounds informed but concise, and avoids filler or repetition.", | ||
| "constraints": { | ||
| "max_chars": 650 | ||
| } | ||
| }, | ||
| { | ||
| "id": "Global_News", | ||
| "name": "Global News", | ||
| "type": "ai_text", | ||
| "web_search": "force", | ||
| "prompt": "Create a short global news bulletin anchored to <timestamp>. Use web search. Include two or three current items that are broadly relevant, clearly separated, fact-focused, and written for spoken delivery.", | ||
| "constraints": { | ||
| "max_chars": 700 | ||
| } | ||
| }, | ||
| { | ||
| "id": "Weather_Short", | ||
| "name": "Weather Short", | ||
| "type": "ai_text", | ||
| "web_search": "disabled", | ||
| "prompt": "Using <weather_hourly> and <timestamp>, deliver a short spoken weather update with the current outlook, a useful next-hours summary, and smooth radio phrasing.", | ||
| "constraints": { | ||
| "max_chars": 500 | ||
| } | ||
| }, | ||
| { | ||
| "id": "Song_Introduction_End", | ||
| "name": "Song Introduction End", | ||
| "type": "ai_text", | ||
| "web_search": "disabled", | ||
| "prompt": "The last track played was <prev_songinfo>. Close the program with a memorable sign-off: brief reflection, warm farewell, and language that sounds like the end of a real radio segment.", | ||
| "constraints": { | ||
| "max_chars": 650 | ||
| } | ||
| }, | ||
| { | ||
| "id": "Between_Songs_Smoother", | ||
| "name": "Between Songs Mix", | ||
| "type": "ai_meta", | ||
| "prompt": "Merge the drafts below into one coherent radio break. Preserve factual content, remove duplication, and make the final segment sound like one host speaking naturally.\n<section_drafts>" | ||
| } | ||
| ], | ||
| "section_order": [ | ||
| { | ||
| "when": "start_of_playlist", | ||
| "flow": [ | ||
| { | ||
| "MUST": "Song_Introduction_Start" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "when": "between_songs", | ||
| "flow": [ | ||
| { | ||
| "ALTERNATIVE": { | ||
| "choices": [ | ||
| { | ||
| "section": "Song_Transition", | ||
| "weight": 100 | ||
| } | ||
| ] | ||
| } | ||
| }, | ||
| { | ||
| "OPTIONAL": { | ||
| "section": "Weather_Short", | ||
| "chance": 0.2, | ||
| "guards": { | ||
| "min_gap_songs": 3, | ||
| "max_per_60min": 1, | ||
| "require_placeholders_present": [ | ||
| "<weather_hourly>" | ||
| ] | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| "OPTIONAL": { | ||
| "section": "Global_News", | ||
| "chance": 0.12, | ||
| "guards": { | ||
| "min_gap_songs": 4, | ||
| "max_per_60min": 1, | ||
| "require_placeholders_present": [ | ||
| "<timestamp>" | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "when": "end_of_playlist", | ||
| "flow": [ | ||
| { | ||
| "MUST": "Song_Introduction_End" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } |
|
swiftbird07 marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "type": "plugin", | ||
| "stage": "alpha", | ||
| "domain": "ai_radio", | ||
| "name": "AI Radio", | ||
| "description": "Generate AI moderator sections for playlists and run dynamic radio-style queue generation. Open AI Radio from the main frontend navigation.", | ||
| "codeowners": ["@swiftbird07"], | ||
| "requirements": [], | ||
| "documentation": "https://music-assistant.io/plugins/ai-radio", | ||
| "multi_instance": false, | ||
| "builtin": false | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the use of this png? I don't think it's used anywhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its used to generate the cover images of the inserted MP3 files:
https://github.com/swiftbird07/ma-server/blob/149f26b6bb9c1db52078fc7890751eb4f07282b8/music_assistant/providers/ai_radio/runtime.py#L893-L901