Initial commit
This commit is contained in:
4
skills/npc-voice/.env.example
Normal file
4
skills/npc-voice/.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
# ElevenLabs API Key for Text-to-Speech
|
||||
# Get your API key from: https://elevenlabs.io/app/settings/api-keys
|
||||
# Required for the DM skill's NPC voice feature
|
||||
ELEVENLABS_API_KEY=your_api_key_here
|
||||
141
skills/npc-voice/README.md
Normal file
141
skills/npc-voice/README.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# NPC Voice Skill
|
||||
|
||||
Text-to-Speech for bringing NPCs and narration to life using ElevenLabs AI voices.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Test it out
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "Hello, adventurer!" \
|
||||
--voice merchant \
|
||||
--npc "Shopkeeper"
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Get API Key**: Sign up at [ElevenLabs](https://elevenlabs.io) and get your API key from [settings](https://elevenlabs.io/app/settings/api-keys)
|
||||
|
||||
2. **Configure**: Copy `.env.example` to `.env` and add your API key:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env and add: ELEVENLABS_API_KEY=your_key_here
|
||||
```
|
||||
|
||||
3. **Install Dependencies**:
|
||||
```bash
|
||||
cd .claude/skills/npc-voice && npm install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Command
|
||||
|
||||
```bash
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "Your dialogue here" \
|
||||
--voice <preset> \
|
||||
--npc "Character Name"
|
||||
```
|
||||
|
||||
### Voice Presets
|
||||
|
||||
Run `--list` to see all available voices:
|
||||
|
||||
```bash
|
||||
node .claude/skills/npc-voice/speak-npc.js --list
|
||||
```
|
||||
|
||||
**Popular Presets:**
|
||||
- `narrator` - Storytelling, scene descriptions
|
||||
- `merchant` - Friendly shopkeeper
|
||||
- `villain` - Menacing antagonist
|
||||
- `wizard` - Wise spellcaster
|
||||
- `warrior` - Gruff fighter
|
||||
- `goblin` - Sneaky creature
|
||||
- `dwarf` - Deep, gruff
|
||||
- `elf` - Elegant
|
||||
|
||||
### Examples
|
||||
|
||||
**D&D Session:**
|
||||
```bash
|
||||
# DM narration
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "You enter a dimly lit tavern. The smell of ale and pipe smoke fills the air." \
|
||||
--voice narrator
|
||||
|
||||
# NPC dialogue
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "Welcome to my shop! Looking for potions?" \
|
||||
--voice merchant \
|
||||
--npc "Albus the Alchemist"
|
||||
|
||||
# Villain monologue
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "You fools! You cannot stop me now!" \
|
||||
--voice villain \
|
||||
--npc "Dark Wizard Malakar"
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Text Input**: You provide dialogue/narration text
|
||||
2. **Voice Selection**: Choose from preset character voices
|
||||
3. **AI Generation**: ElevenLabs generates natural-sounding speech
|
||||
4. **Auto-Play**: Audio plays automatically through your system
|
||||
5. **Cleanup**: Temporary files are removed
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Model**: ElevenLabs eleven_flash_v2_5
|
||||
- **Audio Format**: MP3, 44.1kHz, 128kbps
|
||||
- **Audio Player**:
|
||||
- macOS: `afplay`
|
||||
- Linux: `mpg123`
|
||||
- **Dependencies**:
|
||||
- `@elevenlabs/elevenlabs-js`
|
||||
- `dotenv`
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **D&D/TTRPG**: Voice NPCs and narrate scenes
|
||||
- **Storytelling**: Read passages from books with character voices
|
||||
- **Content Creation**: Generate voiceovers for videos/podcasts
|
||||
- **Accessibility**: Convert text to speech for easier consumption
|
||||
- **Game Development**: Prototype character voices
|
||||
|
||||
## Files
|
||||
|
||||
- `speak-npc.js` - Main TTS script
|
||||
- `skill.md` - Skill documentation for Claude
|
||||
- `package.json` - Node.js dependencies
|
||||
- `.env.example` - Environment variable template
|
||||
- `.env` - Your API key (git-ignored)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"API key not configured"**
|
||||
- Make sure `.env` file exists with valid `ELEVENLABS_API_KEY`
|
||||
|
||||
**"Audio player exited with code 1"**
|
||||
- macOS: `afplay` should work by default
|
||||
- Linux: Install `mpg123` with `sudo apt install mpg123`
|
||||
|
||||
**"401 Unauthorized"**
|
||||
- Check your API key is correct and active
|
||||
- Verify you have credits remaining in your ElevenLabs account
|
||||
|
||||
## Cost
|
||||
|
||||
ElevenLabs pricing (as of 2024):
|
||||
- Free tier: 10,000 characters/month
|
||||
- Paid tiers: Starting at $5/month for 30,000 characters
|
||||
|
||||
Short NPC dialogues typically use 50-200 characters each.
|
||||
|
||||
---
|
||||
|
||||
**Created by**: Sahar Carmel
|
||||
**License**: MIT
|
||||
**ElevenLabs**: https://elevenlabs.io
|
||||
129
skills/npc-voice/SKILL.md
Normal file
129
skills/npc-voice/SKILL.md
Normal file
@@ -0,0 +1,129 @@
|
||||
---
|
||||
name: npc-voice
|
||||
description: Generate AI-powered voice acting for D&D NPCs using ElevenLabs TTS. Brings characters to life with distinct voices for important moments and memorable NPCs.
|
||||
---
|
||||
|
||||
# NPC Voice Text-to-Speech Skill
|
||||
|
||||
Use ElevenLabs AI voices to bring NPCs and narration to life with realistic speech synthesis.
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this skill **sparingly** for maximum dramatic impact:
|
||||
|
||||
- **Important NPC introductions**: First time meeting a major NPC
|
||||
- **Dramatic moments**: Villain speeches, emotional reveals, climactic scenes
|
||||
- **Recurring NPCs**: Builds consistency and player attachment
|
||||
- **Boss taunts**: Makes combat more memorable
|
||||
- **Scene narration**: Add atmosphere to key story moments
|
||||
|
||||
**Don't overuse it** - save it for special moments so it remains impactful!
|
||||
|
||||
## Setup Requirements
|
||||
|
||||
**First-time setup:**
|
||||
1. Get an API key from [ElevenLabs](https://elevenlabs.io/app/settings/api-keys) (free tier available)
|
||||
2. Create `.env` file in this skill directory:
|
||||
```
|
||||
ELEVENLABS_API_KEY=your_api_key_here
|
||||
```
|
||||
3. Install dependencies:
|
||||
```bash
|
||||
cd .claude/skills/npc-voice && npm install
|
||||
```
|
||||
|
||||
**If no API key is configured**, simply skip using this tool - the dnd-dm skill works perfectly without it!
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Basic usage - speak as an NPC
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "Welcome to my shop, traveler!" \
|
||||
--voice merchant \
|
||||
--npc "Elmar Barthen"
|
||||
|
||||
# Villain monologue
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "You dare challenge me? Foolish mortals!" \
|
||||
--voice villain \
|
||||
--npc "Dark Lord Karzoth"
|
||||
|
||||
# Scene narration
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "The ancient door creaks open, revealing a dark corridor..." \
|
||||
--voice narrator
|
||||
|
||||
# List all available voices
|
||||
node .claude/skills/npc-voice/speak-npc.js --list
|
||||
```
|
||||
|
||||
## Available Voice Presets
|
||||
|
||||
**Fantasy Character Types:**
|
||||
- `goblin` - Sneaky, nasty creatures
|
||||
- `dwarf` - Deep, gruff voices
|
||||
- `elf` - Elegant, refined speech
|
||||
- `wizard` - Wise, scholarly tone
|
||||
- `warrior` - Gruff, commanding
|
||||
- `rogue` - Sneaky, sly
|
||||
- `cleric` - Gentle, compassionate
|
||||
|
||||
**NPC Archetypes:**
|
||||
- `merchant` - Friendly, talkative
|
||||
- `guard` - Authoritative
|
||||
- `noble` - Refined, aristocratic
|
||||
- `villain` - Menacing, threatening
|
||||
|
||||
**General:**
|
||||
- `narrator` - Storytelling and scene descriptions
|
||||
- `default` - Neutral male voice
|
||||
|
||||
**Age/Gender:**
|
||||
- `oldman` - Elderly male
|
||||
- `youngman` - Young male
|
||||
- `woman` - Female
|
||||
- `girl` - Young female
|
||||
|
||||
## Example in Gameplay
|
||||
|
||||
```
|
||||
DM: As you enter the cave, a hulking bugbear emerges from the shadows.
|
||||
|
||||
[Use TTS for dramatic effect:]
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "You dare enter Klarg's lair? Your bones will join the others!" \
|
||||
--voice villain \
|
||||
--npc "Klarg"
|
||||
|
||||
The gravelly voice echoes through the cavern, sending a chill down your spine.
|
||||
What do you do?
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
- Uses ElevenLabs TTS API (eleven_flash_v2_5 model for speed)
|
||||
- Generates high-quality MP3 audio (44.1kHz, 128kbps)
|
||||
- Auto-plays using system audio player:
|
||||
- macOS: `afplay` (built-in)
|
||||
- Linux: `mpg123` (install via package manager)
|
||||
- Temporary files are cleaned up automatically
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If TTS doesn't work:
|
||||
1. Check that `.env` file exists with valid API key
|
||||
2. Verify audio player is available on your system
|
||||
3. Check ElevenLabs API quota at https://elevenlabs.io
|
||||
4. **Remember: TTS is optional!** The dnd-dm skill works fine without it
|
||||
|
||||
## Cost Information
|
||||
|
||||
**ElevenLabs Pricing** (as of 2024):
|
||||
- Free tier: 10,000 characters/month
|
||||
- Paid tiers: Starting at $5/month for 30,000 characters
|
||||
- Short NPC dialogues typically use 50-200 characters each
|
||||
|
||||
---
|
||||
|
||||
**Ready to bring your NPCs to life with voice acting!**
|
||||
13
skills/npc-voice/package.json
Normal file
13
skills/npc-voice/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "dnd-dm-skill",
|
||||
"version": "1.0.0",
|
||||
"description": "D&D Dungeon Master skill with text-to-speech for NPC voices",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"speak": "node speak-npc.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elevenlabs/elevenlabs-js": "^2.22.0",
|
||||
"dotenv": "^16.4.5"
|
||||
}
|
||||
}
|
||||
285
skills/npc-voice/speak-npc.js
Executable file
285
skills/npc-voice/speak-npc.js
Executable file
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* D&D NPC Voice TTS CLI Tool
|
||||
* Usage: node speak-npc.js --text "NPC dialogue" --voice <voice-name> [--npc "NPC Name"]
|
||||
*
|
||||
* Examples:
|
||||
* node speak-npc.js --text "Welcome, traveler!" --voice goblin --npc "Klarg"
|
||||
* node speak-npc.js --text "I need your help" --voice wizard --npc "Sildar"
|
||||
*/
|
||||
|
||||
import { ElevenLabsClient } from '@elevenlabs/elevenlabs-js';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { spawn } from 'child_process';
|
||||
import { promises as fs } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Load environment variables from .env file in skill directory
|
||||
dotenv.config({ path: join(__dirname, '.env') });
|
||||
|
||||
// Voice presets for different character types
|
||||
const VOICE_PRESETS = {
|
||||
// Default/versatile voices
|
||||
'default': 'JBFqnCBsd6RMkjVDRZzb', // George - neutral male
|
||||
'narrator': 'pNInz6obpgDQGcFmaJgB', // Adam - calm narrator
|
||||
|
||||
// Fantasy character archetypes
|
||||
'goblin': 'EXAVITQu4vr4xnSDxMaL', // Sarah - can sound sneaky/nasty
|
||||
'dwarf': 'VR6AewLTigWG4xSOukaG', // Arnold - deep male
|
||||
'elf': 'ThT5KcBeYPX3keUQqHPh', // Dorothy - elegant female
|
||||
'wizard': 'pNInz6obpgDQGcFmaJgB', // Adam - wise male
|
||||
'warrior': 'VR6AewLTigWG4xSOukaG', // Arnold - gruff male
|
||||
'rogue': 'EXAVITQu4vr4xnSDxMaL', // Sarah - sneaky
|
||||
'cleric': 'ThT5KcBeYPX3keUQqHPh', // Dorothy - gentle female
|
||||
'merchant': 'JBFqnCBsd6RMkjVDRZzb', // George - friendly
|
||||
'guard': 'VR6AewLTigWG4xSOukaG', // Arnold - authoritative
|
||||
'noble': 'pNInz6obpgDQGcFmaJgB', // Adam - refined
|
||||
'villain': 'EXAVITQu4vr4xnSDxMaL', // Sarah - menacing
|
||||
|
||||
// Age/gender variations
|
||||
'oldman': 'pNInz6obpgDQGcFmaJgB', // Adam
|
||||
'youngman': 'JBFqnCBsd6RMkjVDRZzb', // George
|
||||
'woman': 'ThT5KcBeYPX3keUQqHPh', // Dorothy
|
||||
'girl': 'ThT5KcBeYPX3keUQqHPh', // Dorothy
|
||||
};
|
||||
|
||||
// ANSI color codes
|
||||
const colors = {
|
||||
cyan: '\x1b[36m',
|
||||
blue: '\x1b[34m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
reset: '\x1b[0m'
|
||||
};
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const parsed = {
|
||||
text: null,
|
||||
voice: 'default',
|
||||
npc: null,
|
||||
list: false,
|
||||
help: false
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--text':
|
||||
parsed.text = args[++i];
|
||||
break;
|
||||
case '--voice':
|
||||
parsed.voice = args[++i];
|
||||
break;
|
||||
case '--npc':
|
||||
parsed.npc = args[++i];
|
||||
break;
|
||||
case '--list':
|
||||
parsed.list = true;
|
||||
break;
|
||||
case '--help':
|
||||
case '-h':
|
||||
parsed.help = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
${colors.cyan}🎭 D&D NPC Voice TTS Tool${colors.reset}
|
||||
|
||||
Usage:
|
||||
node speak-npc.js --text "dialogue" --voice <preset> [--npc "Name"]
|
||||
node speak-npc.js --list
|
||||
node speak-npc.js --help
|
||||
|
||||
Options:
|
||||
--text <string> The dialogue text to speak (required)
|
||||
--voice <preset> Voice preset (default: "default")
|
||||
--npc <name> NPC name for display (optional)
|
||||
--list List all available voice presets
|
||||
--help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
node speak-npc.js --text "Welcome, traveler!" --voice goblin --npc "Klarg"
|
||||
node speak-npc.js --text "I need your help" --voice wizard --npc "Sildar"
|
||||
node speak-npc.js --text "You dare challenge me?" --voice villain
|
||||
|
||||
Available voice presets:
|
||||
${Object.keys(VOICE_PRESETS).join(', ')}
|
||||
|
||||
Setup:
|
||||
1. Copy .env.example to .env in the skill directory
|
||||
2. Add your ElevenLabs API key to .env
|
||||
3. Get API key from: https://elevenlabs.io/app/settings/api-keys
|
||||
`);
|
||||
}
|
||||
|
||||
function listVoices() {
|
||||
console.log(`\n${colors.cyan}🎭 Available Voice Presets${colors.reset}\n`);
|
||||
|
||||
const categories = {
|
||||
'Default': ['default', 'narrator'],
|
||||
'Fantasy Archetypes': ['goblin', 'dwarf', 'elf', 'wizard', 'warrior', 'rogue', 'cleric'],
|
||||
'NPCs': ['merchant', 'guard', 'noble', 'villain'],
|
||||
'Age/Gender': ['oldman', 'youngman', 'woman', 'girl']
|
||||
};
|
||||
|
||||
for (const [category, voices] of Object.entries(categories)) {
|
||||
console.log(`${colors.yellow}${category}:${colors.reset}`);
|
||||
voices.forEach(voice => {
|
||||
if (VOICE_PRESETS[voice]) {
|
||||
console.log(` ${colors.green}${voice.padEnd(15)}${colors.reset} (ID: ${VOICE_PRESETS[voice]})`);
|
||||
}
|
||||
});
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
async function playAudio(audioPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Try afplay (macOS), then ffplay, then mpg123
|
||||
const players = ['afplay', 'ffplay -nodisp -autoexit', 'mpg123'];
|
||||
|
||||
let player = players[0];
|
||||
if (process.platform === 'darwin') {
|
||||
player = 'afplay';
|
||||
} else if (process.platform === 'linux') {
|
||||
player = 'mpg123';
|
||||
}
|
||||
|
||||
const proc = spawn(player.split(' ')[0], [
|
||||
...player.split(' ').slice(1),
|
||||
audioPath
|
||||
], {
|
||||
stdio: 'ignore'
|
||||
});
|
||||
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Audio player exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function textToSpeech(text, voiceId, npcName = null) {
|
||||
const apiKey = process.env.ELEVENLABS_API_KEY;
|
||||
|
||||
if (!apiKey || apiKey === 'your_api_key_here') {
|
||||
console.error(`${colors.red}❌ Error: ElevenLabs API key not configured${colors.reset}`);
|
||||
console.error(`\n${colors.yellow}Setup instructions:${colors.reset}`);
|
||||
console.error(`1. Copy .env.example to .env in the skill directory`);
|
||||
console.error(`2. Add your API key to .env`);
|
||||
console.error(`3. Get API key from: https://elevenlabs.io/app/settings/api-keys\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`${colors.cyan}🎙️ Generating speech...${colors.reset}`);
|
||||
if (npcName) {
|
||||
console.log(`${colors.blue} NPC: ${npcName}${colors.reset}`);
|
||||
}
|
||||
console.log(`${colors.blue} Text: "${text}"${colors.reset}`);
|
||||
|
||||
const elevenlabs = new ElevenLabsClient({
|
||||
apiKey: apiKey
|
||||
});
|
||||
|
||||
const audio = await elevenlabs.textToSpeech.convert(
|
||||
voiceId,
|
||||
{
|
||||
text: text,
|
||||
model_id: 'eleven_flash_v2_5',
|
||||
output_format: 'mp3_44100_128'
|
||||
}
|
||||
);
|
||||
|
||||
// Save to temporary file
|
||||
const tempFile = join(__dirname, '.temp_voice.mp3');
|
||||
const writeStream = createWriteStream(tempFile);
|
||||
|
||||
// Handle the audio stream
|
||||
if (audio[Symbol.asyncIterator]) {
|
||||
for await (const chunk of audio) {
|
||||
writeStream.write(chunk);
|
||||
}
|
||||
} else {
|
||||
writeStream.write(audio);
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
writeStream.end();
|
||||
writeStream.on('finish', resolve);
|
||||
writeStream.on('error', reject);
|
||||
});
|
||||
|
||||
console.log(`${colors.green}✅ Audio generated${colors.reset}`);
|
||||
console.log(`${colors.cyan}🔊 Playing audio...${colors.reset}`);
|
||||
|
||||
// Play the audio
|
||||
await playAudio(tempFile);
|
||||
|
||||
// Clean up temp file
|
||||
await fs.unlink(tempFile);
|
||||
|
||||
console.log(`${colors.green}✅ Complete!${colors.reset}`);
|
||||
} catch (error) {
|
||||
console.error(`${colors.red}❌ Error: ${error.message}${colors.reset}`);
|
||||
|
||||
if (error.message.includes('401') || error.message.includes('unauthorized')) {
|
||||
console.error(`\n${colors.yellow}Your API key may be invalid. Please check:${colors.reset}`);
|
||||
console.error(`1. API key is correct in .env file`);
|
||||
console.error(`2. Key has not expired`);
|
||||
console.error(`3. Get a new key from: https://elevenlabs.io/app/settings/api-keys\n`);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
async function main() {
|
||||
const args = parseArgs();
|
||||
|
||||
if (args.help) {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (args.list) {
|
||||
listVoices();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!args.text) {
|
||||
console.error(`${colors.red}❌ Error: --text is required${colors.reset}`);
|
||||
console.error(`Run with --help for usage information\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const voiceId = VOICE_PRESETS[args.voice] || args.voice;
|
||||
|
||||
if (!VOICE_PRESETS[args.voice] && args.voice !== 'default') {
|
||||
console.warn(`${colors.yellow}⚠️ Warning: Unknown voice preset "${args.voice}", using voice ID directly${colors.reset}`);
|
||||
}
|
||||
|
||||
await textToSpeech(args.text, voiceId, args.npc);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user