Add Tavily search tools and podcast generator scripts

This commit is contained in:
Matt Bruce 2026-02-24 04:10:56 -06:00
parent ef7ab7c815
commit 91af3c741b
5 changed files with 352 additions and 0 deletions

2
.env.tavily Normal file
View File

@ -0,0 +1,2 @@
# Tavily API Configuration
TAVILY_API_KEY=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

View File

@ -0,0 +1,109 @@
# Daily Digest Podcast Generator
Converts daily digest blog posts into audio podcast format using OpenAI TTS.
## Overview
This system automatically:
1. Fetches the latest daily digest from blog-backup
2. Converts the content to speech using OpenAI TTS
3. Generates an RSS feed for podcast distribution
4. Stores audio files for serving
## Setup
### 1. OpenAI API Key
You need an OpenAI API key with TTS access:
```bash
# Create the environment file
echo "OPENAI_API_KEY=sk-your-key-here" > ~/.openclaw/workspace/.env.openai
```
Get your API key from: https://platform.openai.com/api-keys
### 2. Test the Generator
```bash
cd ~/.openclaw/workspace/scripts/podcast-generator
./generate-podcast.sh
```
### 3. Manual Generation
To generate a podcast for a specific digest:
```bash
# The script auto-detects the latest digest
./generate-podcast.sh
```
### 4. RSS Feed
The RSS feed is generated at:
- Local: `~/.openclaw/workspace/podcast/rss.xml`
- Web: Should be hosted on Mission Control or deployed to static hosting
## Cost Estimate
- Daily digest: ~2,000 characters
- OpenAI TTS: $0.015 per 1,000 characters
- **Cost per episode: ~$0.03**
- Monthly cost (30 episodes): ~$0.90
## Architecture
```
Daily Digest Posted (blog-backup)
Cron Trigger (7:30 AM CST)
Podcast Generator Script
OpenAI TTS API
MP3 File + RSS Update
Mission Control (/podcast/rss.xml)
Podcast Apps (Apple, Spotify, etc.)
```
## Files
- `generate-podcast.sh` - Main conversion script
- `~/.openclaw/workspace/podcast/audio/` - Stored MP3 files
- `~/.openclaw/workspace/podcast/rss.xml` - RSS feed
## Integration with Daily Digest
Add to the daily digest cron job after successful posting:
```bash
# After posting digest, generate podcast
/Users/mattbruce/.openclaw/workspace/scripts/podcast-generator/generate-podcast.sh
```
## Future Enhancements
- [ ] Add intro/outro music
- [ ] Multiple voice options
- [ ] Chapter markers for sections
- [ ] Auto-upload to Spotify for Creators
- [ ] Analytics tracking
## Troubleshooting
### "OPENAI_API_KEY not set"
Create the `.env.openai` file with your API key.
### "No digest found"
The blog-backup may not have a digest for today yet. Check https://blog-backup-two.vercel.app
### Audio file is empty
Check OpenAI API rate limits and billing status.
## License
Part of OpenClaw infrastructure.

View File

@ -0,0 +1,217 @@
#!/bin/bash
# Daily Digest to Podcast Converter
# Converts blog-backup daily digest posts to audio podcast format
set -e
# Configuration
WORKSPACE_DIR="/Users/mattbruce/.openclaw/workspace"
PODCAST_DIR="$WORKSPACE_DIR/podcast"
AUDIO_DIR="$PODCAST_DIR/audio"
RSS_FILE="$PODCAST_DIR/rss.xml"
BLOG_BACKUP_URL="https://blog-backup-two.vercel.app"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log() {
echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
# Create directories
mkdir -p "$AUDIO_DIR"
# Check if OpenAI API key is set
if [ -z "$OPENAI_API_KEY" ]; then
# Try to load from environment file
if [ -f "$WORKSPACE_DIR/.env.openai" ]; then
export $(cat "$WORKSPACE_DIR/.env.openai" | xargs)
fi
fi
if [ -z "$OPENAI_API_KEY" ]; then
error "OPENAI_API_KEY not set. Please set it or create $WORKSPACE_DIR/.env.openai"
exit 1
fi
# Function to fetch latest digest
fetch_latest_digest() {
log "Fetching latest daily digest from blog-backup..."
# Get the latest message from Supabase
local response=$(curl -s "https://qnatchrjlpehiijwtreh.supabase.co/rest/v1/blog_messages?select=id,date,content,tags&order=created_at.desc&limit=1" \
-H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuYXRjaHJqbHBlaGlpand0cmVoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzE2NDA0MzYsImV4cCI6MjA4NzIxNjQzNn0.47XOMrQBzcQEh71phQflPoO4v79Jk3rft7BC72KHDvA" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuYXRjaHJqbHBlaGlpand0cmVoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzE2NDA4MzYsImV4cCI6MjA4NzIxNjQzNn0.47XOMrQBzcQEh71phQflPoO4v79Jk3rft7BC72KHDvA")
echo "$response"
}
# Function to convert text to speech
convert_to_speech() {
local text="$1"
local output_file="$2"
log "Converting text to speech..."
# Use OpenAI TTS API
curl -s -X POST "https://api.openai.com/v1/audio/speech" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"tts-1\",
\"input\": $(echo "$text" | jq -R -s .),
\"voice\": \"alloy\",
\"response_format\": \"mp3\"
}" \
--output "$output_file"
if [ -f "$output_file" ] && [ -s "$output_file" ]; then
log "Audio file created: $output_file"
return 0
else
error "Failed to create audio file"
return 1
fi
}
# Function to generate RSS feed
generate_rss() {
log "Generating RSS feed..."
local podcast_title="OpenClaw Daily Digest"
local podcast_description="Daily tech news and insights for developers"
local podcast_link="https://mission-control-rho-pink.vercel.app/podcast"
local podcast_image="https://mission-control-rho-pink.vercel.app/podcast-cover.jpg"
cat > "$RSS_FILE" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<title>$podcast_title</title>
<link>$podcast_link</link>
<language>en-us</language>
<copyright>© 2026 OpenClaw</copyright>
<itunes:author>OpenClaw</itunes:author>
<description>$podcast_description</description>
<itunes:image href="$podcast_image"/>
<itunes:category text="Technology"/>
<itunes:explicit>no</itunes:explicit>
EOF
# Add episodes (most recent first)
for mp3_file in $(ls -t "$AUDIO_DIR"/*.mp3 2>/dev/null | head -20); do
if [ -f "$mp3_file" ]; then
local filename=$(basename "$mp3_file")
local episode_date=$(echo "$filename" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' || date '+%Y-%m-%d')
local episode_title="Daily Digest - $(date -j -f '%Y-%m-%d' "$episode_date" '+%B %d, %Y' 2>/dev/null || echo "$episode_date")"
local file_size=$(stat -f%z "$mp3_file" 2>/dev/null || stat -c%s "$mp3_file" 2>/dev/null || echo "0")
local pub_date=$(date -j -f '%Y-%m-%d' "$episode_date" '+%a, %d %b %Y 07:00:00 CST' 2>/dev/null || date '+%a, %d %b %Y 07:00:00 CST')
cat >> "$RSS_FILE" << EOF
<item>
<title>$episode_title</title>
<enclosure url="$podcast_link/audio/$filename" length="$file_size" type="audio/mpeg"/>
<pubDate>$pub_date</pubDate>
<guid isPermaLink="false">$filename</guid>
<itunes:duration>5:00</itunes:duration>
<description>Daily tech digest for $episode_date</description>
</item>
EOF
fi
done
cat >> "$RSS_FILE" << EOF
</channel>
</rss>
EOF
log "RSS feed generated: $RSS_FILE"
}
# Function to clean text for TTS (remove markdown, URLs, etc.)
clean_text_for_tts() {
local text="$1"
# Remove markdown links [text](url) -> text
text=$(echo "$text" | sed -E 's/\[([^]]+)\]\([^)]+\)/\1/g')
# Remove markdown headers
text=$(echo "$text" | sed -E 's/^#+ //g')
# Remove markdown bold/italic
text=$(echo "$text" | sed -E 's/\*\*//g; s/\*//g; s/__//g; s/_//g')
# Remove code blocks
text=$(echo "$text" | sed -E 's/```[^`]*```//g')
# Remove inline code
text=$(echo "$text" | sed -E 's/`([^`]+)`/\1/g')
# Remove horizontal rules
text=$(echo "$text" | sed -E 's/^---$//g')
# Remove extra whitespace
text=$(echo "$text" | sed -E 's/^[[:space:]]*//g; s/[[:space:]]*$//g')
echo "$text"
}
# Main execution
main() {
log "Starting Daily Digest to Podcast conversion..."
# Fetch latest digest
local digest_json=$(fetch_latest_digest)
if [ -z "$digest_json" ] || [ "$digest_json" = "[]" ]; then
error "No digest found"
exit 1
fi
# Parse digest info
local digest_id=$(echo "$digest_json" | jq -r '.[0].id')
local digest_date=$(echo "$digest_json" | jq -r '.[0].date')
local digest_content=$(echo "$digest_json" | jq -r '.[0].content')
log "Found digest for date: $digest_date"
# Check if already converted
local output_file="$AUDIO_DIR/daily-digest-$digest_date.mp3"
if [ -f "$output_file" ]; then
warn "Audio already exists for $digest_date, skipping conversion"
exit 0
fi
# Clean text for TTS
log "Cleaning text for TTS..."
local clean_text=$(clean_text_for_tts "$digest_content")
# Add intro
local full_text="OpenClaw Daily Digest for $(date -j -f '%Y-%m-%d' "$digest_date" '+%B %d, %Y' 2>/dev/null || echo "$digest_date"). $clean_text"
# Convert to speech
convert_to_speech "$full_text" "$output_file"
# Generate RSS feed
generate_rss
log "✅ Podcast generation complete!"
log "Audio file: $output_file"
log "RSS feed: $RSS_FILE"
}
# Run main function
main "$@"

12
tavily-extract.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
# Tavily Extract wrapper with auto-loaded key
# Load API key from config if not set
if [ -z "$TAVILY_API_KEY" ]; then
if [ -f "$HOME/.openclaw/workspace/.env.tavily" ]; then
export TAVILY_API_KEY=$(grep "TAVILY_API_KEY" "$HOME/.openclaw/workspace/.env.tavily" | cut -d'=' -f2)
fi
fi
# Run the Tavily extract
node /Users/mattbruce/.agents/skills/tavily/scripts/extract.mjs "$@"

12
tavily-search.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
# Tavily API wrapper with auto-loaded key
# Load API key from config if not set
if [ -z "$TAVILY_API_KEY" ]; then
if [ -f "$HOME/.openclaw/workspace/.env.tavily" ]; then
export TAVILY_API_KEY=$(grep "TAVILY_API_KEY" "$HOME/.openclaw/workspace/.env.tavily" | cut -d'=' -f2)
fi
fi
# Run the Tavily command
node /Users/mattbruce/.agents/skills/tavily/scripts/search.mjs "$@"