{"slug": "i-built-a-local-ai-deck-music-assistant-on-a-mac-mini", "title": "I built a local AI deck music assistant on a Mac mini", "summary": "A developer built Robot, a local AI deck music assistant on a Mac mini that uses Open WebUI, Ollama, Flask, AppleScript, Airfoil, Chrome, and Apple Shortcuts to control outdoor speakers and play YouTube playlists via voice or tap commands from iPhone, Apple Watch, or laptop.", "body_md": "I wanted one-tap deck music from my phone, watch, or laptop — powered by a local LLM on a Mac mini.\n\nSo I built **Robot**, a home-lab music assistant that combines **Open WebUI + Ollama + Flask + AppleScript + Airfoil + Chrome + Apple Shortcuts**.\n\nNow I can say or tap **“Play Deck Music”** and Robot:\n\n- Connects my\n**Backyard** speakers in Airfoil - Opens my\n**Deck Music** YouTube playlist in Chrome - Clicks YouTube’s\n**Play all** button via injected JavaScript - Starts music outside\n- Lets me pause, skip, or shut everything off from my phone, watch, or Open WebUI\n\nRobot currently supports:\n\n**Play Deck Music****Play/Pause Music****Next Track****Turn on Backyard speakers****Turn on Backyard + Living Room speakers****Turn off Backyard****Turn off all speakers****List Airfoil speakers**\n\nThe fun part is that there are **multiple ways to control the same system**:\n\n**Open WebUI chat**(`\"Play deck music\"`\n\n)**Apple Shortcuts** on iPhone / Apple Watch**Raycast** shortcut to open Robot from Mac- direct HTTP calls to Flask for testing/debugging\n\nAt a high level, the system looks like this:\n\n```\nApple Shortcut / Open WebUI / Raycast\n                ↓\n            Flask API\n                ↓\n      shell scripts + AppleScript\n                ↓\n    Airfoil + Chrome + YouTube\n                ↓\n Backyard / Living Room / Deck speakers\n```\n\n**Open WebUI** runs on the Mac mini and exposes tool calls like`play_deck_music()`\n\n- those tool calls hit a\n**Flask API** running on port`5055`\n\n- Flask routes trigger\n**shell scripts** - shell scripts use\n**AppleScript** to control**Airfoil** and**Google Chrome** - Chrome opens a YouTube playlist and executes JavaScript to click\n**Play all** - Airfoil handles routing the audio to the correct speakers\n\nThe current backyard setup includes:\n\n**Mac mini (“Robot”)**— local AI + automation brain** Klipsch outdoor speakers**mounted under the eaves** Airfoil**for audio routing** Google Chrome**as the playback source** Deck box**housing parts of the outdoor audio setup** Apple Shortcuts**on iPhone / Apple Watch for quick controls\n\nInside the deck box I’ve got a collection of audio gear, cabling, power, and ventilation. It’s not a polished product build — it’s a home-lab / backyard system that became genuinely useful.\n\nOne design choice I like is that Robot does **not** try to become a DJ.\n\nI still curate the playlist myself from my phone. Robot’s job is the activity layer:\n\n- route the right speakers\n- open the right playlist\n- start playback\n- pause / skip / shut it all down\n\nThat keeps the system simple and makes it feel more reliable.\n\nExamples of commands I can give Robot:\n\n`\"Play deck music\"`\n\n`\"Pause the music\"`\n\n`\"Skip this song\"`\n\n`\"Turn on the backyard and living room\"`\n\n`\"Turn everything off\"`\n\nI currently have shortcuts like:\n\n- 🎵\n**Play Deck Music** - ⏯\n**Play/Pause Music** - ⏭\n**Next Track** - 🔇\n**All Off** - 🤖\n**Robot**(opens Open WebUI)\n\nI also have a Raycast command that opens Robot instantly from my Mac.\n\nOne subtle but important lesson was **where Flask is listening**.\n\nOriginally I had Flask running like this:\n\n```\napp.run(host=\"127.0.0.1\", port=5055)\n```\n\nThat worked from the Mac mini itself and from Open WebUI running in Docker, but **did not work from iPhone Safari / Apple Shortcuts**.\n\nChanging to:\n\n```\napp.run(host=\"0.0.0.0\", port=5055)\n```\n\nallowed devices on my LAN to reach the API at:\n\n```\nhttp://192.168.x.x:5055\n```\n\nThat was the key to getting Apple Shortcuts working.\n\nDepending on where the request originates:\n\n-\n**Robot itself**`http://127.0.0.1:5055`\n\n-\n**Open WebUI container**`http://host.docker.internal:5055`\n\n-\n**iPhone / Apple Shortcuts / other LAN devices**`http://192.168.x.x:5055`\n\nThat ended up being one of the most useful “glue code” lessons in the project.\n\nSome actions — especially **Play Deck Music** — are slow by nature:\n\n- turn on Airfoil speakers\n- open Chrome\n- load the playlist\n- click\n**Play all** - wait for music to start\n\nFor Apple Shortcuts, it’s better if those endpoints return immediately and let the work continue in the background. In practice, that means using `subprocess.Popen()`\n\nfor long-running routes rather than blocking on `subprocess.run()`\n\n.\n\nFor example, the `play_pause`\n\nroute looks like this:\n\n``` python\n@app.route(\"/music/play_pause\", methods=[\"POST\"])\ndef play_pause():\n    subprocess.Popen(\n        [SCRIPTS[\"play_pause_music\"]],\n        stdout=subprocess.DEVNULL,\n        stderr=subprocess.DEVNULL\n    )\n\n    return jsonify({\n        \"action\": \"play_pause_music\",\n        \"status\": \"started\"\n    })\n```\n\nPlenty.\n\n- This is very much a\n**Mac mini + scripts + AppleScript + local AI** project, not a polished product - YouTube automation is inherently a little brittle\n- local models sometimes need a nudge to use the tool instead of talking about the tool\n- the deck box contains a bunch of real-world compromise and “figure it out as you go” engineering\n\nBut that’s also why I like it. It’s not pretending to be a smart home platform — it’s a useful, very personal home-lab system.\n\nA few ideas I want to explore next:\n\n- volume up / volume down\n- better “what’s currently playing?” support\n- more robust multi-zone presets\n- tighter Apple Watch controls\n- physical buttons outside\n- better prompting / tool descriptions so the local model chooses the right tool more reliably\n\nBecause I wanted a computer in my house that does something actually useful.\n\nNot “summarize the internet.” Not “answer generic questions.” Just:\n\nturn on the deck music and make the backyard feel alive\n\nAnd now it does.\n\nExample layout:\n\n```\nrobot-actions/\n├── airfoil/\n│   ├── backyard-chrome-on.sh\n│   ├── backyard-living-room-on.sh\n│   ├── backyard-off.sh\n│   ├── all-off.sh\n│   ├── play-deck-music.sh\n│   ├── play-pause-music.sh\n│   └── next-track.sh\n├── server/\n│   └── airfoil_server.py\n└── README-assets/\n    ├── deck-wide.jpg\n    └── deck-box-inside.jpg\n```\n\nIf you want to build something similar, I’d recommend:\n\n- keep your playlist URL in an environment variable or config file\n- separate\n**fast controls**(Shortcuts) from** flexible controls**(chat) - make the AI control\n**activities**, not everything - expect a bit of glue work between networking, browser automation, and audio routing\n\nIf you’re a home-lab person, that’s half the fun anyway.", "url": "https://wpnews.pro/news/i-built-a-local-ai-deck-music-assistant-on-a-mac-mini", "canonical_source": "https://github.com/wkswede/robot-deck-audio", "published_at": "2026-06-24 13:31:10+00:00", "updated_at": "2026-06-24 13:39:43.362873+00:00", "lang": "en", "topics": ["ai-tools", "ai-agents", "developer-tools"], "entities": ["Mac mini", "Open WebUI", "Ollama", "Flask", "AppleScript", "Airfoil", "Chrome", "Apple Shortcuts"], "alternates": {"html": "https://wpnews.pro/news/i-built-a-local-ai-deck-music-assistant-on-a-mac-mini", "markdown": "https://wpnews.pro/news/i-built-a-local-ai-deck-music-assistant-on-a-mac-mini.md", "text": "https://wpnews.pro/news/i-built-a-local-ai-deck-music-assistant-on-a-mac-mini.txt", "jsonld": "https://wpnews.pro/news/i-built-a-local-ai-deck-music-assistant-on-a-mac-mini.jsonld"}}