A JARVIS-voiced morning briefing on the Echo Studio
I wanted something to talk at me when I get up — weather, todos, the date, sober days, mortgage-due reminder, homelab status. Not when an alarm fires, because I don't use one. When I actually get up. The pipeline detects wake-up from motion data, generates a short briefing in a JARVIS-butler voice, and announces it on the Echo Studio in the living room.
The flow
systemd timer (every 10 min, 04:00–10:00 local)
→ wake_detect.py --check
Query motion-activity table for any qualifying activity since
today 03:00 local. Ignore stationary/heartbeat events. If any
qualifying activity OR it's past 09:00, proceed.
→ morning_briefing.generate_briefing(play=True)
aggregate() pulls the day's context from a dashboard JSON
endpoint (date, weather, todos, sober days, homelab status,
next-bill-due)
write_copy() runs a Sonnet call with a JARVIS-butler system
prompt and ~150 word target
→ alexa_tts.speak(text, device="Echo Studio", method="announce")
→ mark date as fired in a flag file so it doesn't repeat
The timer fires every 10 minutes during a 6-hour wake window. Once a briefing fires for the day, the flag file blocks further runs. If nothing detects motion, the 09:00 (failsafe) tick announces anyway.
Talking to Alexa from a Linux server
There's no first-party API. The community library alexapy reverse-
engineers the same endpoints the Alexa mobile app uses, including
the refresh-token flow.
Once it's got a refresh token cached, scripted TTS looks like:
from alexa_tts import speak
speak("Good morning. The weather today is...",
device="Echo Studio",
method="announce")
There's a _DeviceShim class that wraps the raw device dict so
AlexaAPI sees the attributes it expects (_locale,
_device_type, _device_family, _cluster_members). Without that
shim it crashes on attribute access.
The refresh token is durable across months but does eventually expire. When the briefing stops, the recovery procedure is "open the auth-capture-proxy URL, log in, get a new refresh token, drop it back into the creds file."
The gotcha that ate three hours
The briefing pipeline returned 200 OK from
/api/behaviors/preview. The payload was correct. set_volume(80)
worked. play_music('TUNEIN', 'NPR') actually played NPR on the
Echo Studio at the right volume. The audio path was demonstrably
fine.
But send_announcement and send_sequence("Alexa.Speak") returned
200 and produced zero sound. No chime, no speech, nothing.
Root cause: the Alexa app on my phone had Communications/ Announcements disabled for that specific Echo Studio. Routines and announcements use a different skill from music playback. The skill is gated by the per-device Communications enablement.
Three settings to flip in the Alexa app:
- More → Settings → Communications: master toggle ON
- Devices → Echo Studio → Communications: enable for that device
- Settings → Notifications → Announcements: ON
Same call started chiming and speaking through the Echo Studio immediately.
What I'd do differently
If the briefing ever goes silent again and the API still returns 200, the first thing to check is those app settings, not the credentials. I lost real time assuming "200 means it worked." Now I keep a smoke-test script that calls all three things — set_volume, play_music, and send_announcement — and reports which ones actually produce sound, so I can isolate which class of problem I'm dealing with before I touch the auth flow.
The other thing: send_tts in alexapy is non-functional because of
an Amazon API limitation, so don't bother trying it. Use
send_announcement and accept the soft chime that precedes the
speech. The chime is actually nice — it's a little "incoming
butler" sound before the voice.