Daily news briefing generator — produces a conversational radio-host-style audio briefing + DOCX document covering weather, X/Twitter trends, web trends, world news, politics, tech, local news, sports, markets, and crypto. macOS only (uses Apple TTS and afplay). Use when user asks for a news briefing, morning briefing, daily update, or similar.
Your personal daily news briefing — audio + document.
On demand, research and compose a comprehensive ~10 minute news briefing in a conversational radio-host style. Output: audio file (MP3) + formatted document (DOCX).
say (any language)~/.briefing-room/config.json (settings) and ~/Documents/Briefing Room/ (output)On first use, check if ~/.briefing-room/config.json exists. If not, run:
python3 SKILL_DIR/scripts/config.py init
This creates default config. The user can customize:
en, sk, de, etc.Show setup status:
python3 SKILL_DIR/scripts/config.py status
When user asks for a briefing (e.g. "give me a briefing", "morning update", "what's happening today"):
afplay /System/Library/Sounds/Blow.aiff &Language override: If user says "po slovensky", "v slovenčine", "auf deutsch", "en français", etc. → pass that to the sub-agent. Otherwise use the configured default language. Any language macOS supports will work — the agent writes the script in that language and TTS auto-detects a matching voice.
sessions_spawn(
task="<full pipeline instructions — see below>",
label="briefing-room",
runTimeoutSeconds=600,
cleanup="delete"
)
The task message should include ALL the pipeline steps below so the sub-agent is fully self-contained. Replace all SKILL_DIR references with the actual absolute path to this skill's directory.
Host name: Read host.name from config. If empty, use your own agent name (from your identity). Pass it to the sub-agent as the radio host name (e.g. "Good morning, I'm Jackie, and this is your Briefing Room...").
Config file: ~/.briefing-room/config.json
Read values:
python3 SKILL_DIR/scripts/config.py get location.city
python3 SKILL_DIR/scripts/config.py get language
python3 SKILL_DIR/scripts/config.py get voices.en.mlx_voice
Set values:
python3 SKILL_DIR/scripts/config.py set location.city "Vienna"
python3 SKILL_DIR/scripts/config.py set location.latitude 48.21
python3 SKILL_DIR/scripts/config.py set location.longitude 16.37
python3 SKILL_DIR/scripts/config.py set language "de"
| Key | Default | Description |
|---|---|---|
location.city | Bratislava | City name for weather + local news |
location.latitude | 48.15 | Weather API latitude |
location.longitude | 17.11 | Weather API longitude |
location.timezone | Europe/Bratislava | Timezone for weather API |
language | en | Default briefing language |
output.folder | ~/Documents/Briefing Room | Output directory |
audio.enabled | true | Generate audio |
audio.format | mp3 | Audio format (mp3, wav, aiff) |
audio.tts_engine | auto | TTS engine (auto, mlx, kokoro, builtin) |
sections | all 11 (see below) | Which sections to include |
host.name | (empty = agent name) | Radio host name for the briefing |
trends.regions | united-states,united-kingdom, | X/Twitter trend regions (comma-separated, trailing comma = worldwide) |
webtrends.regions | US,GB, | Google Trends regions (ISO codes, trailing comma = worldwide) |
Each language can have its own TTS engine and voice:
{
"voices": {
"en": {
"engine": "mlx",
"mlx_voice": "af_heart",
"mlx_voice_blend": {"af_heart": 0.6, "af_sky": 0.4},
"builtin_voice": "Samantha",
"speed": 1.05
},
"sk": {
"engine": "builtin",
"builtin_voice": "Laura (Enhanced)",
"builtin_rate": 220
},
"de": {
"engine": "builtin",
"builtin_voice": "Petra (Premium)",
"builtin_rate": 200
}
}
}
Engine priority (when auto):
Users can add any language by adding a voices entry + a matching builtin_voice from say -v '?'.
~/Documents/Briefing Room/YYYY-MM-DD/
├── briefing-YYYY-MM-DD-HHMM.docx # Formatted document
└── briefing-YYYY-MM-DD-HHMM.mp3 # Audio briefing (~10 min)
Do NOT save the .md working file in the output folder. Use /tmp/ for working files, delete after.
# Read config
CITY=$(python3 SKILL_DIR/scripts/config.py get location.city)
LAT=$(python3 SKILL_DIR/scripts/config.py get location.latitude)
LON=$(python3 SKILL_DIR/scripts/config.py get location.longitude)
TZ=$(python3 SKILL_DIR/scripts/config.py get location.timezone)
LANG=$(python3 SKILL_DIR/scripts/config.py get language)
OUTPUT_FOLDER=$(python3 SKILL_DIR/scripts/config.py get output.folder)
DATE=$(date +%Y-%m-%d)
TIMESTAMP=$(date +%Y-%m-%d-%H%M)
OUTPUT_DIR="$OUTPUT_FOLDER/$DATE"
mkdir -p "$OUTPUT_DIR"
Use the configured location coordinates:
# Current weather
TZ_ENC="${TZ/\//%2F}"
BASE="https://api.open-meteo.com/v1/forecast"
CURRENT="temperature_2m,relative_humidity_2m"
CURRENT="$CURRENT,apparent_temperature,precipitation"
CURRENT="$CURRENT,weather_code,wind_speed_10m"
curl -s "$BASE?latitude=$LAT&longitude=$LON\
¤t=$CURRENT&timezone=$TZ_ENC"
# 7-day forecast
DAILY="temperature_2m_max,temperature_2m_min"
DAILY="$DAILY,precipitation_sum,weather_code"
curl -s "$BASE?latitude=$LAT&longitude=$LON\
&daily=$DAILY&timezone=$TZ_ENC"
Or use the helper: bash SKILL_DIR/scripts/briefing.sh weather
Map weather_code to descriptions:
Use web_search tool for each section. Add current date to queries for freshness. Use the configured $CITY for local news.
X/Twitter Trends (from getdaytrends.com — real-time, no API key):
bash SKILL_DIR/scripts/briefing.sh trends
This fetches top 25 trends from US, UK, and Worldwide. Use the output to:
web_search to get context on the top trends you selectedWeb Trends (from Google Trends RSS — what people are searching):
bash SKILL_DIR/scripts/briefing.sh webtrends
This fetches trending Google searches from US, UK, and Worldwide with:
World News:
web_search("top world news today {date}", count=8)
web_search("breaking news today", count=5)
Politics:
web_search("US politics news today {date}", count=5)
web_search("EU politics news today {date}", count=5)
web_search("geopolitics news today", count=5)
⚠️ Source diversity: All sources have bias. For balanced reporting:
Tech & AI:
web_search("tech news today {date}", count=5)
web_search("AI artificial intelligence news today {date}", count=5)
Local news (based on configured city):
web_search("$CITY news today {date}", count=5)
Also search in the configured language if not English:
web_search("$CITY [news today] in $LANG {date}", count=5)
Examples:
"Bratislava správy dnes""Wien Nachrichten heute""Praha zprávy dnes"Sports:
web_search("sports news today {date}", count=5)
web_search("football soccer results today", count=5)
# Or use helper:
bash SKILL_DIR/scripts/briefing.sh crypto
curl -s "https://api.coinbase.com/v2/prices/BTC-USD/spot"
curl -s "https://api.coinbase.com/v2/prices/ETH-USD/spot"
curl -s "https://api.coinbase.com/v2/prices/SOL-USD/spot"
curl -s "https://api.coinbase.com/v2/prices/XRP-USD/spot"
web_search("S&P 500 Dow Jones Nasdaq today {date}", count=5)
web_search("stock market today movers {date}", count=5)
web_search("gold price silver price today", count=3)
web_search("crypto market today {date}", count=5)
Write as a conversational radio-host monologue.
Style guidelines:
# Title header — pandoc adds title from metadataIf language is not English, write the entire script in that language.
Section order:
This Day in History: No research needed — use your own knowledge. Pick 1-2 interesting, surprising, or fun events that happened on today's date. Mix it up: science, culture, politics, weird stuff. Keep it conversational: "And before I let you go — did you know that on this day in 1996..."
Only include sections from the configured sections list. Skip sections the user has removed.
Save as /tmp/briefing_draft_$TIMESTAMP.md (working file).
For the markdown, include:
## 🌤️ Weather, ## 🌍 World, ## 📜 This Day in History, etc.pandoc "/tmp/briefing_draft_$TIMESTAMP.md" \
-o "$OUTPUT_DIR/briefing-$TIMESTAMP.docx" \
--metadata title="Briefing Room - $DATE"
If pandoc is not available, skip DOCX and note it.
Read the config to determine TTS engine and voice for the current language.
MLX-Audio (English, or if configured for language):
python3 SKILL_DIR/scripts/config.py get voices.$LANG.engine
# → if "mlx":
import os, re, glob, json, subprocess
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d-%H%M") # must match TIMESTAMP from Step 0
# Read config
config_path = os.path.expanduser("~/.briefing-room/config.json")
with open(config_path) as f:
config = json.load(f)
lang = config.get("language", "en")
voices = config.get("voices", {})
voice_cfg = voices.get(lang, voices.get("en", {}))
# Read and strip markdown from draft
with open(f"/tmp/briefing_draft_{timestamp}.md", "r") as f:
text = f.read()
text = re.sub(r'#+ ', '', text)
text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text)
text = re.sub(r'\*([^*]+)\*', r'\1', text)
text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text)
text = re.sub(r'---+', '', text)
text = re.sub(r'\n{3,}', '\n\n', text)
# Resolve voice
blend = voice_cfg.get("mlx_voice_blend")
voice = voice_cfg.get("mlx_voice", "af_heart")
if blend:
model = config.get("mlx_audio", {}).get("model", "mlx-community/Kokoro-82M-bf16")
model_slug = model.replace("/", "--")
cache_dir = os.path.expanduser(f"~/.cache/huggingface/hub/models--{model_slug}")
parts = []
for v, w in sorted(blend.items(), key=lambda x: -x[1]):
parts.append(f"{v}_{int(w * 100)}")
blend_name = "_".join(parts) + ".safetensors"
matches = glob.glob(os.path.join(cache_dir, "snapshots/*/voices", blend_name))
if matches:
voice = matches[0]
speed = voice_cfg.get("speed", 1.05)
lang_code = config.get("mlx_audio", {}).get("lang_code", "a")
# Find MLX-Audio
mlx_path = config.get("mlx_audio", {}).get("path", "")
if not mlx_path:
for p in ["~/.openclaw/tools/mlx-audio", "~/.local/share/mlx-audio"]:
ep = os.path.expanduser(p)
if os.path.exists(os.path.join(ep, ".venv/bin/python3")):
mlx_path = ep
break
# Generate via subprocess (uses MLX-Audio's venv)
python_bin = os.path.join(mlx_path, ".venv/bin/python3")
# ... generate_audio call with resolved voice, speed, lang_code
Built-in Apple TTS (any language):
If there's no voice configured for the language, auto-detect one:
# Try to get configured voice, fall back to auto-detect
VOICE=$(python3 SKILL_DIR/scripts/config.py get voices.$LANG.builtin_voice)
if [ "$VOICE" = "None" ] || [ -z "$VOICE" ]; then
# Auto-detect: match locale (e.g. sk_SK, de_DE, fr_FR)
# Prefer Enhanced/Premium voices, fall back to any
VOICE=$(say -v '?' | grep "${LANG}_" \
| grep -i "Enhanced\|Premium" | head -1 \
| sed 's/ *[a-z][a-z]_[A-Z][A-Z].*//' | xargs)
[ -z "$VOICE" ] && VOICE=$(say -v '?' \
| grep "${LANG}_" | head -1 \
| sed 's/ *[a-z][a-z]_[A-Z][A-Z].*//' | xargs)
fi
RATE=$(python3 SKILL_DIR/scripts/config.py get voices.$LANG.builtin_rate)
# Strip markdown for TTS
DRAFT="/tmp/briefing_draft_$TIMESTAMP.md"
TTS_TXT="/tmp/briefing_tts_$TIMESTAMP.txt"
sed -E 's/#+//g; s/\*+//g; s/\[([^]]*)\]\([^)]*\)/\1/g' \
"$DRAFT" > "$TTS_TXT"
say -v "$VOICE" ${RATE:+-r $RATE} \
-o "$OUTPUT_DIR/briefing-$TIMESTAMP.aiff" \
-f "$TTS_TXT"
rm -f "/tmp/briefing_tts_$TIMESTAMP.txt"
Kokoro PyTorch (fallback):
Similar to MLX but uses PyTorch backend. See TubeScribe skill for Kokoro usage patterns.
# Find the raw audio file (MLX outputs .wav, Apple TTS outputs .aiff)
RAW=""
for ext in wav aiff; do
if [ -f "$OUTPUT_DIR/briefing-$TIMESTAMP.$ext" ]; then
RAW="$OUTPUT_DIR/briefing-$TIMESTAMP.$ext"
break
fi
done
if [ -n "$RAW" ]; then
ffmpeg -y \
-i "$RAW" \
-codec:a libmp3lame -qscale:a 2 \
"$OUTPUT_DIR/briefing-$TIMESTAMP.mp3"
if [ -s "$OUTPUT_DIR/briefing-$TIMESTAMP.mp3" ]; then
rm "$RAW"
fi
fi
rm -f "/tmp/briefing_draft_$TIMESTAMP.md"
open "$OUTPUT_DIR"
Do NOT auto-play. Briefings are long and need playback controls.
Report back with:
bash SKILL_DIR/scripts/briefing.sh setup # Check dependencies + config
bash SKILL_DIR/scripts/briefing.sh weather # Fetch weather (uses config location)
bash SKILL_DIR/scripts/briefing.sh trends # Fetch X/Twitter trends (US + UK + Worldwide)
bash SKILL_DIR/scripts/briefing.sh webtrends # Fetch Google Trends (US + UK + Worldwide)
bash SKILL_DIR/scripts/briefing.sh crypto # Fetch crypto prices
bash SKILL_DIR/scripts/briefing.sh open # Open today's folder
bash SKILL_DIR/scripts/briefing.sh list # List all briefings
bash SKILL_DIR/scripts/briefing.sh clean # Remove briefings >30 days old
bash SKILL_DIR/scripts/briefing.sh config # Show raw config JSON
voices entry + installing the voice via say -v '?'Required:
curl — API calls (built into macOS)web_search tool — News research (OpenClaw built-in)Recommended:
pandoc — DOCX generation: brew install pandocffmpeg — MP3 conversion: brew install ffmpegBuilt-in (macOS):
say — multilingual TTS (always available as fallback)| Issue | Action |
|---|---|
| No config file | Run python3 SKILL_DIR/scripts/config.py init |
| API timeout | Retry once, skip that source, note it |
| Web search empty | Try alternative query, note gaps |
| TTS fails | Fall back to Apple say (always available) |
| Pandoc not found | Skip DOCX, deliver MP3 only |
| No internet | Cannot generate — inform user |