diff --git a/.env.tavily b/.env.tavily new file mode 100644 index 0000000..9a159eb --- /dev/null +++ b/.env.tavily @@ -0,0 +1,2 @@ +# Tavily API Configuration +TAVILY_API_KEY=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 \ No newline at end of file diff --git a/scripts/podcast-generator/README.md b/scripts/podcast-generator/README.md new file mode 100644 index 0000000..ad0a855 --- /dev/null +++ b/scripts/podcast-generator/README.md @@ -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. \ No newline at end of file diff --git a/scripts/podcast-generator/generate-podcast.sh b/scripts/podcast-generator/generate-podcast.sh new file mode 100755 index 0000000..15f8201 --- /dev/null +++ b/scripts/podcast-generator/generate-podcast.sh @@ -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 + + + + $podcast_title + $podcast_link + en-us + © 2026 OpenClaw + OpenClaw + $podcast_description + + + no + +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 + + $episode_title + + $pub_date + $filename + 5:00 + Daily tech digest for $episode_date + + +EOF + fi + done + + cat >> "$RSS_FILE" << EOF + + +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 "$@" \ No newline at end of file diff --git a/tavily-extract.sh b/tavily-extract.sh new file mode 100755 index 0000000..7bbfe9b --- /dev/null +++ b/tavily-extract.sh @@ -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 "$@" \ No newline at end of file diff --git a/tavily-search.sh b/tavily-search.sh new file mode 100755 index 0000000..0288147 --- /dev/null +++ b/tavily-search.sh @@ -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 "$@" \ No newline at end of file