Skill: ashutosh887/moodcast (version 1.0.2)

--- name: moodcast description: Transform any text into emotionally expressive audio with ambient soundscapes using ElevenLabs v3 audio tags and Sound Effects API metadata: {"moltbot":{"requires":{"env":["ELEVENLABS_API_KEY"]},"primaryEnv":"ELEVENLABS_API_KEY","homepage":"https://github.com/ashutosh887/moodcast"}} --- # MoodCast Transform any text into emotionally expressive audio with ambient soundscapes. MoodCast analyzes your content, adds expressive delivery using ElevenLabs v3 audio tags, and layers matching ambient soundscapes. ## When to Use This Skill Use MoodCast when the user wants to: - Hear text read with natural emotional expression - Create audio versions of articles, stories, or scripts - Generate expressive voiceovers with ambient atmosphere - Listen to morning briefings that actually sound engaging - Transform boring text into captivating audio content **Trigger phrases:** "read this dramatically", "make this sound good", "create audio for", "moodcast this", "read with emotion", "narrate this" **Slash command:** `/moodcast` ## Core Capabilities ### 1. Emotion-Aware Text Enhancement Automatically analyzes text and inserts appropriate v3 audio tags: - **Emotions:** `[excited]`, `[nervous]`, `[angry]`, `[sorrowful]`, `[calm]`, `[happy]` - **Delivery:** `[whispers]`, `[shouts]`, `[rushed]`, `[slows down]` - **Reactions:** `[laughs]`, `[sighs]`, `[gasps]`, `[giggles]`, `[crying]` - **Pacing:** `[pause]`, `[breathes]`, `[stammers]`, `[hesitates]` ### 2. Ambient Soundscape Generation Creates matching background audio using Sound Effects API: - News → subtle office ambiance - Story → atmospheric soundscape matching mood - Motivational → uplifting background - Scary → tense, eerie atmosphere ### 3. Multi-Voice Dialogue For conversations/scripts, assigns different voices to speakers with appropriate emotional delivery. ## Instructions ### Quick Read (Single Command) ```bash python3 {baseDir}/scripts/moodcast.py --text "Your text here" ``` ### With Ambient Sound ```bash python3 {baseDir}/scripts/moodcast.py --text "Your text here" --ambient "coffee shop background noise" ``` ### Save to File ```bash python3 {baseDir}/scripts/moodcast.py --text "Your text here" --output story.mp3 ``` ### Different Moods ```bash python3 {baseDir}/scripts/moodcast.py --text "Your text" --mood dramatic python3 {baseDir}/scripts/moodcast.py --text "Your text" --mood calm python3 {baseDir}/scripts/moodcast.py --text "Your text" --mood excited python3 {baseDir}/scripts/moodcast.py --text "Your text" --mood scary ``` ### List Available Voices ```bash python3 {baseDir}/scripts/moodcast.py --list-voices ``` ### Custom Configuration ```bash python3 {baseDir}/scripts/moodcast.py --text "Your text" --voice VOICE_ID --model eleven_v3 --output-format mp3_44100_128 ``` ## Emotion Detection Rules The skill automatically detects and enhances: | Text Pattern | Audio Tag Added | |-------------|-----------------| | "amazing", "incredible", "wow" | `[excited]` | | "scared", "afraid", "terrified" | `[nervous]` | | "angry", "furious", "hate" | `[angry]` | | "sad", "sorry", "unfortunately" | `[sorrowful]` | | "secret", "quiet", "between us" | `[whispers]` | | "!" exclamations | `[excited]` | | "..." trailing off | `[pause]` | | "haha", "lol" | `[laughs]` | | Questions | Natural rising intonation | ## Example Transformations **Input:** ``` Breaking news! Scientists have discovered something incredible. This could change everything we know about the universe... I can't believe it. ``` **Enhanced Output:** ``` [excited] Breaking news! Scientists have discovered something incredible. [pause] This could change everything we know about the universe... [gasps] [whispers] I can't believe it. ``` **Input:** ``` It was a dark night. The old house creaked. Something moved in the shadows... "Who's there?" she whispered. ``` **Enhanced Output:** ``` [slows down] It was a dark night. [pause] The old house creaked. [nervous] Something moved in the shadows... [whispers] "Who's there?" she whispered. ``` ## Environment Variables - `ELEVENLABS_API_KEY` (required) - Your ElevenLabs API key - `MOODCAST_DEFAULT_VOICE` (optional) - Default voice ID (defaults to `CwhRBWXzGAHq8TQ4Fs17`) - `MOODCAST_MODEL` (optional) - Default model ID (defaults to `eleven_v3`) - `MOODCAST_OUTPUT_FORMAT` (optional) - Default output format (defaults to `mp3_44100_128`) - `MOODCAST_AUTO_AMBIENT` (optional) - Set to `"true"` for automatic ambient sounds when using `--mood` **Configuration Priority:** CLI arguments override environment variables, which override hardcoded defaults. ## Technical Notes - Uses ElevenLabs Eleven v3 model for audio tag support - Sound Effects API for ambient generation (up to 30 seconds) - Free tier: 10,000 credits/month (~10 min audio) - Max 2,400 characters per chunk (v3 supports 5,000, but we split conservatively for reliability) - Audio tags must be lowercase: `[whispers]` not `[WHISPERS]` ## Tips for Best Results 1. **Dramatic content** works best - stories, news, scripts 2. **Shorter segments** (under 500 chars) sound more natural 3. **Combine with ambient** for immersive experience 4. **Roger and Rachel** voices are most expressive with v3 ## Credits Built by [ashutosh887](https://github.com/ashutosh887) Using ElevenLabs Text-to-Speech v3 + Sound Effects API Created for #ClawdEleven Hackathon

path: README.md
content: # MoodCast

**Transform any text into emotionally expressive audio with ambient soundscapes.**

MoodCast is a [Moltbot](https://molt.bot) skill that uses ElevenLabs' most advanced features to create compelling audio content. It analyzes your text, adds emotional expression using Eleven v3 audio tags, and can layer ambient soundscapes for immersive experiences.

[![Demo Video](https://img.shields.io/badge/Demo-Watch%20Video-red?style=for-the-badge&logo=youtube)](YOUR_VIDEO_LINK_HERE)
[![Moltbot Skill](https://img.shields.io/badge/Moltbot-Skill-blue?style=for-the-badge)](https://molt.bot)
[![ElevenLabs](https://img.shields.io/badge/Powered%20by-ElevenLabs-purple?style=for-the-badge)](https://elevenlabs.io)

---

## Features

| Feature | Description |
|---------|-------------|
| **Emotion Detection** | Automatically analyzes text and inserts v3 audio tags (`[excited]`, `[whispers]`, `[laughs]`, etc.) |
| **Ambient Soundscapes** | Generates matching background sounds using Sound Effects API |
| **Multiple Moods** | Pre-configured moods: dramatic, calm, excited, scary, news, story |
| **Smart Text Processing** | Auto-splits long text, handles multiple speakers |

## Demo

**Input:**
```
Breaking news! Scientists have discovered something incredible. 
This could change everything we know about the universe...
I can't believe it.
```

**MoodCast Output:**
```
[excited] Breaking news! Scientists have discovered something incredible.
[pause] This could change everything we know about the universe...
[gasps] [whispers] I can't believe it.
```

*The AI voice delivers this with genuine excitement, dramatic pauses, and a whispered ending.*

---

## Quick Start

### 1. Install the Skill

```bash
# Option 1: Clone to your Moltbot skills directory
git clone https://github.com/ashutosh887/moodcast ~/.clawdbot/skills/moodcast

# Option 2: Install via MoltHub (recommended)
npx molthub@latest install moodcast

# Option 3: Install to workspace (for per-agent skills)
# After installing, move to workspace or use git clone method
```

### 2. Set Your API Key

```bash
export [REDACTED_SECRET]"
```

Or add to `~/.clawdbot/moltbot.json`:
```json
{
  "skills": {
    "entries": {
      "moodcast": {
        "enabled": true,
        "apiKey": "your-api-key-here",
        "env": {
          "ELEVENLABS_API_KEY": "your-api-key-here"
        }
      }
    }
  }
}
```

Note: `apiKey` automatically maps to `ELEVENLABS_API_KEY` when the skill declares `primaryEnv`.

### 3. Use It!

**Via Moltbot (WhatsApp/Telegram/Discord/iMessage):**
```
Hey Molty, moodcast this: "It was a dark and stormy night..."
```

Or use the slash command:
```
/moodcast "It was a dark and stormy night..."
```

**Via Command Line:**
```bash
python3 ~/.clawdbot/skills/moodcast/scripts/moodcast.py --text "Hello world!"
```

---

## Usage Examples

### Basic Usage
```bash
python3 moodcast.py --text "This is amazing news!"
```

### With Mood Preset
```bash
python3 moodcast.py --text "The door creaked open slowly..." --mood scary
```

### With Ambient Sound
```bash
python3 moodcast.py --text "Welcome to my café" --ambient "coffee shop busy morning"
```

### Save to File
```bash
python3 moodcast.py --text "Your story here" --output narration.mp3
```

### Show Enhanced Text
```bash
python3 moodcast.py --text "Wow this is great!" --show-enhanced
# Output: [excited] Wow this is great!
```

### Custom Configuration
```bash
# Custom voice, model, and output format
python3 moodcast.py --text "Hello" --voice VOICE_ID --model eleven_v3 --output-format mp3_44100_128

# Override mood's default voice
python3 moodcast.py --text "Dramatic scene" --mood dramatic --voice CUSTOM_VOICE_ID

# Skip emotion enhancement
python3 moodcast.py --text "Plain text" --no-enhance
```

---

## Supported Audio Tags (Eleven v3)

MoodCast automatically detects emotions and inserts these tags:

### Emotions
| Tag | Triggers |
|-----|----------|
| `[excited]` | amazing, incredible, wow, !!! |
| `[happy]` | happy, delighted, thrilled |
| `[nervous]` | scared, afraid, terrified |
| `[angry]` | angry, furious, hate |
| `[sorrowful]` | sad, sorry, tragic |
| `[calm]` | peaceful, gentle, quiet |

### Delivery
| Tag | Effect |
|-----|--------|
| `[whispers]` | Soft, secretive tone |
| `[shouts]` | Loud, emphatic delivery |
| `[slows down]` | Deliberate pacing |
| `[rushed]` | Fast, urgent speech |

### Reactions
| Tag | Effect |
|-----|--------|
| `[laughs]` | Natural laughter |
| `[sighs]` | Weary exhale |
| `[gasps]` | Surprise intake |
| `[giggles]` | Light laughter |
| `[pause]` | Dramatic beat |

---

## Mood Presets

| Mood | Voice | Style | Best For |
|------|-------|-------|----------|
| `dramatic` | Roger | Theatrical, expressive | Stories, scripts |
| `calm` | Lily | Soothing, peaceful | Meditation, ASMR |
| `excited` | Liam | Energetic, upbeat | News, announcements |
| `scary` | Roger (deep) | Tense, ominous | Horror, thrillers |
| `news` | Lily | Professional, clear | Briefings, reports |
| `story` | Rachel | Warm, engaging | Audiobooks, tales |

---

## Configuration

### Command Line Arguments

| Argument | Short | Description |
|----------|-------|-------------|
| `--text` | `-t` | Text to convert to speech (required) |
| `--mood` | `-m` | Mood preset: dramatic, calm, excited, scary, news, story |
| `--voice` | `-v` | Voice ID (overrides mood's default voice) |
| `--model` | | Model ID (default: `eleven_v3`) |
| `--output-format` | | Output format (default: `mp3_44100_128`) |
| `--ambient` | `-a` | Generate ambient sound effect (prompt) |
| `--ambient-duration` | | Ambient duration in seconds (max 30, default: 10) |
| `--output` | `-o` | Save audio to file instead of playing |
| `--no-enhance` | | Skip automatic emotion enhancement |
| `--show-enhanced` | | Print enhanced text before generating |
| `--list-voices` | | List available voices |

### Environment Variables

| Variable | Required | Description | Default |
|----------|----------|-------------|---------|
| `ELEVENLABS_API_KEY` | Yes | Your ElevenLabs API key | - |
| `MOODCAST_DEFAULT_VOICE` | No | Default voice ID (overridden by `--voice` or `--mood`) | `CwhRBWXzGAHq8TQ4Fs17` |
| `MOODCAST_MODEL` | No | Default model ID (overridden by `--model`) | `eleven_v3` |
| `MOODCAST_OUTPUT_FORMAT` | No | Default output format (overridden by `--output-format`) | `mp3_44100_128` |
| `MOODCAST_AUTO_AMBIENT` | No | Auto-generate ambient sounds when using `--mood` | - |

**Priority order:** CLI arguments > Environment variables > Hardcoded defaults

### Moltbot Config (`~/.clawdbot/moltbot.json`)

```json
{
  "skills": {
    "entries": {
      "moodcast": {
        "enabled": true,
        "apiKey": "xi-xxxxxxxxxxxx",
        "env": {
          "ELEVENLABS_API_KEY": "xi-xxxxxxxxxxxx",
          "MOODCAST_AUTO_AMBIENT": "true"
        }
      }
    }
  }
}
```

Note: `apiKey` is a convenience field that maps to `ELEVENLABS_API_KEY` when `primaryEnv` is set in the skill metadata.

---

## ElevenLabs APIs Used

This skill demonstrates **deep integration** with multiple ElevenLabs APIs:

### 1. Text-to-Speech (Eleven v3)
- Model: `eleven_v3` for audio tag support
- Format: `mp3_44100_128`
- Features: Full audio tag expression system

### 2. Sound Effects API
- Generates ambient soundscapes from text prompts
- Up to 30 seconds per generation
- Seamless looping support

### 3. Voices API
- Lists available voices
- Supports custom voice selection
- Mood-based voice matching

---

## Project Structure

```
moodcast/
├── SKILL.md           # Moltbot skill definition (AgentSkills format)
├── README.md          # Project documentation
├── requirements.txt   # Python dependencies
├── .gitignore         # Git ignore rules
├── scripts/
│   └── moodcast.py    # Main Python script
└── examples/
    ├── news.txt       # News article example
    ├── scary.txt      # Horror story example
    ├── dramatic.txt   # Dramatic narrative example
    ├── calm.txt       # Peaceful scene example
    └── story.txt      # Adventure story example
```

## Skill Installation Locations

Moltbot loads skills from three locations (in precedence order):
1. **Workspace skills**: `<workspace>/skills/moodcast` (per-agent, highest precedence)
2. **Managed skills**: `~/.clawdbot/skills/moodcast` (shared across agents)
3. **Bundled skills**: Shipped with Moltbot install (lowest precedence)

Use `npx molthub@latest install moodcast` to install to the managed directory, or clone directly to your workspace for per-agent installation.

---

## Technical Details

### API Integration

| Criteria | Implementation |
|----------|----------------|
| **ElevenLabs API usage** | Uses Eleven v3 audio tags (deepest TTS feature), Sound Effects API, Voices API |
| **Practical use cases** | Content creators, writers, podcasters, anyone who wants expressive audio |
| **Demo approach** | Single clear hook: "Text that feels emotion" with live demonstration |

---

## License

MIT License - feel free to use, modify, and share!

---

## Acknowledgments

- [ElevenLabs](https://elevenlabs.io) for the incredible audio AI APIs
- [Moltbot](https://molt.bot) / [Peter Steinberger](https://twitter.com/steipete) for the amazing AI assistant platform

Built for the #ClawdEleven Hackathon (ElevenLabs × Moltbot)


path: examples/calm.txt
content: Rain all morning. Didn’t mind. Made coffee, sat by the window.

No meetings today. Just the sound of it on the roof. Kind of lost track of time for a bit.

That’s it. Nothing else. Felt good.


path: examples/dramatic.txt
content: Bottom of the ninth. Two outs. We're down by one.

He steps up to the plate. Crowd's gone quiet. You could hear a pin drop. My hands were shaking—I'm not even playing.

First pitch. Ball. Second pitch. He swings. Miss.

One more. He takes a breath. Nods. The pitcher winds up.

Crack. You know that sound. The whole place just—erupted. I still get chills thinking about it.


path: examples/news.txt
content: Okay so—big news. The team at CERN just put out a statement. They’ve actually done it. Confirmed.

I’m not even kidding. After like, what, ten years? They’ve finally got the result they were looking for. The lead scientist was on the call earlier—she sounded exhausted. Said they’re still processing what it means.

So yeah. Might have to rewrite the textbooks. Again. Wild day.


path: examples/scary.txt
content: Last night I woke up around 3. Heard something. Not the usual house noises—something else.

I lay there for a bit. Then I heard it again. Upstairs. Like someone walking. Slow.

I live alone. I didn’t move. My heart was going so loud I was sure whatever it was could hear it.

The steps stopped. Then they started again. Coming toward the stairs.

I still haven’t gone up there.


path: examples/story.txt
content: So my grandma used to tell us this one. There was this kid, right—lived in a small town. Found a box in the basement. Old. Had her name on it. Not our grandma’s. Another one. From way back.

Nobody’d opened it in years. She did. Inside was a map. Drawn by hand. And a note that said “when you’re ready.”

She was twelve. She was definitely ready. Took her bike. Followed the map. Ended up at the old mill by the river.

I’ll tell you what was in the mill next time. Promise.


path: requirements.txt
content: elevenlabs>=1.0.0


path: scripts/moodcast.py
content: #!/usr/bin/env python3
"""
MoodCast - Emotionally Expressive Audio Generator
Transform any text into emotionally expressive audio with ambient soundscapes using ElevenLabs v3 audio tags and Sound Effects API.

Author: ashutosh887 (https://github.com/ashutosh887)
Built for #ClawdEleven Hackathon
"""

import os
import sys
import re
import argparse
import tempfile
import subprocess
from typing import Optional, List

try:
    from elevenlabs.client import ElevenLabs
    from elevenlabs import play
except ImportError:
    print("Installing elevenlabs package...")
    subprocess.check_call([sys.executable, "-m", "pip", "install", "elevenlabs", "-q"])
    from elevenlabs.client import ElevenLabs
    from elevenlabs import play

DEFAULT_VOICE_ID = os.environ.get("MOODCAST_DEFAULT_VOICE", "CwhRBWXzGAHq8TQ4Fs17")
MODEL_ID = os.environ.get("MOODCAST_MODEL", "eleven_v3")
OUTPUT_FORMAT = os.environ.get("MOODCAST_OUTPUT_FORMAT", "mp3_44100_128")

MOOD_VOICES = {
    "dramatic": "CwhRBWXzGAHq8TQ4Fs17",
    "calm": "pFZP5JQG7iQjIQuC4Bku",
    "excited": "TX3LPaxmHKxFdv7VOQHJ",
    "scary": "CwhRBWXzGAHq8TQ4Fs17",
    "news": "pFZP5JQG7iQjIQuC4Bku",
    "story": "EXAVITQu4vr4xnSDxMaL",
}

EMOTION_PATTERNS = [
    (r'\b(amazing|incredible|wow|awesome|fantastic|brilliant)\b', '[excited]'),
    (r'\b(happy|delighted|thrilled|ecstatic|overjoyed)\b', '[happy]'),
    (r'[!]{2,}', '[excited]'),
    (r'\b(scared|afraid|terrified|frightened|horror)\b', '[nervous]'),
    (r'\b(nervous|anxious|worried|uneasy)\b', '[nervous]'),
    (r'\b(angry|furious|hate|enraged|outraged)\b', '[angry]'),
    (r'\b(damn|hell|stupid|idiot)\b', '[angry]'),
    (r'\b(sad|sorry|unfortunately|tragic|heartbreaking)\b', '[sorrowful]'),
    (r'\b(crying|tears|grief|mourning|loss)\b', '[crying]'),
    (r'\b(secret|whisper|quiet|shh|between us|confidential)\b', '[whispers]'),
    (r'\b(psst|listen closely)\b', '[whispers]'),
    (r'\b(attention|listen up|important|urgent|emergency)\b', '[shouts]'),
    (r'^[A-Z\s]{10,}[!]', '[shouts]'),
    (r'\b(haha|hehe|lol|funny|hilarious)\b', '[laughs]'),
    (r'\b(sigh|exhausted|tired|finally)\b', '[sighs]'),
    (r'\b(oh my|what|really|no way)\b', '[gasps]'),
    (r'\.\.\.$', '[pause]'),
    (r'\b(um|uh|well|so)\b', '[hesitates]'),
    (r'[—–-]{2,}', '[pause]'),
]

MOOD_TAGS = {
    "dramatic": ['[pause]', '[gasps]', '[whispers]', '[shouts]'],
    "calm": ['[calm]', '[pause]', '[breathes]'],
    "excited": ['[excited]', '[happy]', '[laughs]'],
    "scary": ['[nervous]', '[whispers]', '[slows down]', '[pause]'],
    "news": ['[pause]', '[calm]'],
    "story": ['[pause]', '[whispers]', '[excited]', '[nervous]'],
}

AMBIENT_PRESETS = {
    "dramatic": "orchestral tension building suspense cinematic",
    "calm": "gentle rain on window peaceful ambient",
    "excited": "upbeat cheerful background music celebration",
    "scary": "creepy ambient horror dark atmosphere wind howling",
    "news": "subtle office background white noise professional",
    "story": "fantasy adventure ambient atmospheric",
    "default": "soft ambient background peaceful",
}

def get_client() -> ElevenLabs:
    """Initialize ElevenLabs client."""
    [REDACTED_SECRET]"ELEVENLABS_API_KEY")
    if not [REDACTED_SECRET]"Error: ELEVENLABS_API_KEY environment variable not set")
        print("   Set it with: export [REDACTED_SECRET]'")
        sys.exit(1)
    return ElevenLabs([REDACTED_SECRET])


def enhance_text_with_emotions(text: str, mood: Optional[str] = None) -> str:
    """
    Analyze text and insert appropriate v3 audio tags for emotional delivery.
    """
    enhanced = text
    tags_added = set()
    
    for pattern, tag in EMOTION_PATTERNS:
        matches = list(re.finditer(pattern, enhanced, re.IGNORECASE | re.MULTILINE))
        for match in reversed(matches):
            if tag not in tags_added or tag in ['[pause]', '[laughs]']:
                start = match.start()
                sentence_start = enhanced.rfind('.', 0, start)
                sentence_start = max(0, sentence_start + 1)
                
                if not re.match(r'\s*\[', enhanced[sentence_start:start]):
                    enhanced = enhanced[:start] + f"{tag} " + enhanced[start:]
                    tags_added.add(tag)
    
    if mood and mood in MOOD_TAGS:
        if not enhanced.startswith('['):
            mood_tag = MOOD_TAGS[mood][0]
            enhanced = f"{mood_tag} {enhanced}"
    
    enhanced = re.sub(r'\s+', ' ', enhanced)
    enhanced = re.sub(r'\[\s+', '[', enhanced)
    enhanced = re.sub(r'\s+\]', ']', enhanced)
    
    return enhanced.strip()


def split_text_for_api(text: str, max_chars: int = 2400) -> List[str]:
    """
    Split text into chunks under the API character limit.
    Tries to split at sentence boundaries.
    Note: Eleven v3 supports up to 5,000 chars, but we use 2,400 for safety.
    """
    if len(text) <= max_chars:
        return [text]
    
    chunks = []
    current_chunk = ""
    sentences = re.split(r'(?<=[.!?])\s+', text)
    
    for sentence in sentences:
        if len(current_chunk) + len(sentence) + 1 <= max_chars:
            current_chunk += " " + sentence if current_chunk else sentence
        else:
            if current_chunk:
                chunks.append(current_chunk.strip())
            current_chunk = sentence
    
    if current_chunk:
        chunks.append(current_chunk.strip())
    
    return chunks


def generate_speech(
    client: ElevenLabs,
    text: str,
    voice_id: str = DEFAULT_VOICE_ID,
    model_id: str = MODEL_ID,
    output_format: str = OUTPUT_FORMAT,
    output_path: Optional[str] = None
) -> bytes:
    """
    Generate speech using ElevenLabs v3 with audio tags.
    """
    try:
        audio = client.text_to_speech.convert(
            text=text,
            voice_id=voice_id,
            model_id=model_id,
            output_format=output_format,
        )
        
        audio_bytes = b""
        for chunk in audio:
            audio_bytes += chunk
        
        if output_path:
            with open(output_path, "wb") as f:
                f.write(audio_bytes)
            print(f"Audio saved to: {output_path}")
        
        return audio_bytes
        
    except Exception as e:
        print(f"Error generating speech: {e}")
        raise


def generate_sound_effect(
    client: ElevenLabs,
    prompt: str,
    duration: float = 10.0,
    output_path: Optional[str] = None
) -> bytes:
    """
    Generate ambient sound effect using Sound Effects API.
    """
    try:
        audio = client.text_to_sound_effects.convert(
            text=prompt,
            duration_seconds=duration,
        )
        
        audio_bytes = b""
        for chunk in audio:
            audio_bytes += chunk
        
        if output_path:
            with open(output_path, "wb") as f:
                f.write(audio_bytes)
            print(f"Sound effect saved to: {output_path}")
        
        return audio_bytes
        
    except Exception as e:
        print(f"Warning: Sound effect generation failed: {e}")
        return b""


def play_audio(audio_bytes: bytes):
    """Play audio bytes through speakers."""
    try:
        with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
            f.write(audio_bytes)
            temp_path = f.name
        
        players = ["afplay", "mpv", "ffplay", "aplay"]
        for player in players:
            try:
                if player == "ffplay":
                    subprocess.run([player, "-nodisp", "-autoexit", temp_path], 
                                 check=True, capture_output=True)
                else:
                    subprocess.run([player, temp_path], check=True, capture_output=True)
                break
            except (FileNotFoundError, subprocess.CalledProcessError):
                continue
        
        os.unlink(temp_path)
        
    except Exception as e:
        print(f"Warning: Could not play audio: {e}")


def list_voices(client: ElevenLabs):
    """List available voices."""
    print("\nAvailable Voices:\n")
    print(f"{'Name':<20} {'Voice ID':<30} {'Labels'}")
    print("-" * 70)
    
    try:
        voices = client.voices.get_all()
        for voice in voices.voices[:20]:
            labels = ", ".join(voice.labels.values()) if voice.labels else ""
            print(f"{voice.name:<20} {voice.voice_id:<30} {labels[:30]}")
    except Exception as e:
        print(f"Error listing voices: {e}")
    
    print("\nTip: Use --voice VOICE_ID to select a specific voice")


def main():
    parser = argparse.ArgumentParser(
        description="MoodCast - Transform text into emotionally expressive audio with ambient soundscapes",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python3 moodcast.py --text "This is amazing news!"
  python3 moodcast.py --text "A dark night..." --mood scary
  python3 moodcast.py --text "Hello world" --ambient "coffee shop"
  python3 moodcast.py --text "Story..." --output story.mp3
  python3 moodcast.py --text "Test" --voice VOICE_ID --model eleven_v3 --output-format mp3_44100_128
  python3 moodcast.py --list-voices
        """
    )
    
    parser.add_argument("--text", "-t", help="Text to convert to speech")
    parser.add_argument("--mood", "-m", choices=list(MOOD_VOICES.keys()),
                       help="Mood preset (affects voice and emotion tags)")
    parser.add_argument("--voice", "-v", help=f"Voice ID to use (overrides mood selection; default: {DEFAULT_VOICE_ID})")
    parser.add_argument("--model", help=f"Model ID (default: {MODEL_ID}, or MOODCAST_MODEL env var)")
    parser.add_argument("--output-format", help=f"Output format (default: {OUTPUT_FORMAT}, or MOODCAST_OUTPUT_FORMAT env var)")
    parser.add_argument("--ambient", "-a", help="Generate ambient sound effect (prompt)")
    parser.add_argument("--ambient-duration", type=float, default=10.0,
                       help="Ambient sound duration in seconds (max 30)")
    parser.add_argument("--output", "-o", help="Save audio to file instead of playing")
    parser.add_argument("--no-enhance", action="store_true",
                       help="Skip automatic emotion enhancement")
    parser.add_argument("--show-enhanced", action="store_true",
                       help="Print enhanced text before generating")
    parser.add_argument("--list-voices", action="store_true",
                       help="List available voices")
    
    args = parser.parse_args()
    
    client = get_client()
    
    if args.list_voices:
        list_voices(client)
        return
    
    if not args.text:
        parser.print_help()
        print("\nError: --text is required")
        sys.exit(1)
    
    voice_id = args.voice or (MOOD_VOICES.get(args.mood) if args.mood else None) or DEFAULT_VOICE_ID
    model_id = args.model or MODEL_ID
    output_format = args.output_format or OUTPUT_FORMAT
    
    text = args.text
    if not args.no_enhance:
        text = enhance_text_with_emotions(text, args.mood)
    
    if args.show_enhanced:
        print(f"\nEnhanced text:\n{text}\n")
    
    chunks = split_text_for_api(text)
    if len(chunks) > 1:
        print(f"Text split into {len(chunks)} chunks")
    
    print(f"Generating expressive audio...")
    all_audio = b""
    
    for i, chunk in enumerate(chunks):
        if len(chunks) > 1:
            print(f"   Processing chunk {i+1}/{len(chunks)}...")
        audio_bytes = generate_speech(client, chunk, voice_id, model_id, output_format, None)
        all_audio += audio_bytes
    
    ambient_audio = b""
    if args.ambient:
        print(f"Generating ambient soundscape...")
        ambient_prompt = args.ambient
        ambient_audio = generate_sound_effect(
            client, 
            ambient_prompt, 
            min(args.ambient_duration, 30.0)
        )
    elif args.mood and os.environ.get("MOODCAST_AUTO_AMBIENT"):
        print(f"Generating mood-matched ambient...")
        ambient_prompt = AMBIENT_PRESETS.get(args.mood, AMBIENT_PRESETS["default"])
        ambient_audio = generate_sound_effect(client, ambient_prompt, 15.0)
    
    if args.output:
        with open(args.output, "wb") as f:
            f.write(all_audio)
        print(f"Speech saved to: {args.output}")
        
        if ambient_audio:
            ambient_path = args.output.replace(".mp3", "_ambient.mp3")
            with open(ambient_path, "wb") as f:
                f.write(ambient_audio)
            print(f"Ambient saved to: {ambient_path}")
    else:
        print("Playing audio...")
        play_audio(all_audio)
        
        if ambient_audio:
            print("Playing ambient...")
            play_audio(ambient_audio)
    
    print("\nMoodCast complete!")


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)