Skill: onlyloveher/edge-tts-cn (version 1.0.0)

--- name: edge-tts-cn description: | Text-to-speech conversion using node-edge-tts npm package for generating audio from text. Supports multiple voices, languages, speed adjustment, pitch control, and subtitle generation. Use when: (1) User requests audio/voice output with the "tts" trigger or keyword. (2) Content needs to be spoken rather than read (multitasking, accessibility, driving, cooking). (3) User wants a specific voice, speed, pitch, or format for TTS output. --- # Edge-TTS Skill ## Overview Generate high-quality text-to-speech audio using Microsoft Edge's neural TTS service via the node-edge-tts npm package. Supports multiple languages, voices, adjustable speed/pitch, and subtitle generation. ## Quick Start When you detect TTS intent from triggers or user request: 1. **Call the tts tool** (Clawdbot built-in) to convert text to speech 2. The tool returns a MEDIA: path 3. Clawdbot routes the audio to the current channel ```javascript // Example: Built-in tts tool usage tts("Your text to convert to speech") // Returns: MEDIA: /path/to/audio.mp3 ``` ## Trigger Detection Recognize "tts" keyword as TTS requests. The skill automatically filters out TTS-related keywords from text before conversion to avoid converting the trigger words themselves to audio. ## Advanced Customization ### Using the Node.js Scripts For more control, use the bundled scripts directly: #### TTS Converter ```bash cd scripts npm install node tts-converter.js "Your text" --voice en-US-AriaNeural --rate +10% --output output.mp3 ``` **Options:** - `--voice, -v`: Voice name (default: en-US-AriaNeural) - `--lang, -l`: Language code (e.g., en-US, es-ES) - `--format, -o`: Output format (default: audio-24khz-48kbitrate-mono-mp3) - `--pitch`: Pitch adjustment (e.g., +10%, -20%, default) - `--rate, -r`: Rate adjustment (e.g., +10%, -20%, default) - `--volume`: Volume adjustment (e.g., +0%, -10%, default) - `--save-subtitles, -s`: Save subtitles as JSON file - `--output, -f`: Output file path (default: tts_output.mp3) - `--proxy, -p`: Proxy URL (e.g., http://localhost:7890) - `--timeout`: Request timeout in milliseconds (default: 10000) - `--list-voices, -L`: List available voices #### Configuration Manager ```bash cd scripts npm install node config-manager.js --set-voice en-US-AriaNeural node config-manager.js --set-rate +10% node config-manager.js --get node config-manager.js --reset ``` ### Voice Selection Common voices (use `--list-voices` for full list): **English:** - `en-US-MichelleNeural` (female, natural, **default**) - `en-US-AriaNeural` (female, natural) - `en-US-GuyNeural` (male, natural) - `en-GB-SoniaNeural` (female, British) - `en-GB-RyanNeural` (male, British) **Other Languages:** - `es-ES-ElviraNeural` (Spanish, Spain) - `fr-FR-DeniseNeural` (French) - `de-DE-KatjaNeural` (German) - `ja-JP-NanamiNeural` (Japanese) - `zh-CN-XiaoxiaoNeural` (Chinese) - `ar-SA-ZariyahNeural` (Arabic) ### Rate Guidelines Rate values use percentage format: - `"default"`: Normal speed - `"-20%"` to `"-10%"`: Slow, clear (tutorials, stories, accessibility) - `"+10%"` to `"+20%"`: Slightly fast (summaries) - `"+30%"` to `"+50%"`: Fast (news, efficiency) ### Output Formats Choose audio quality based on use case: - `audio-24khz-48kbitrate-mono-mp3`: Standard quality (voice notes, messages) - `audio-24khz-96kbitrate-mono-mp3`: High quality (presentations, content) - [REDACTED_SECRET]: Highest quality (professional audio, music) ## Resources ### scripts/tts-converter.js Main TTS conversion script using node-edge-tts. Generates audio files with customizable voice, rate, volume, pitch, and format. Supports subtitle generation and voice listing. ### scripts/config-manager.js Manages persistent user preferences for TTS settings (voice, language, format, pitch, rate, volume). Stores config in `~/.tts-config.json`. ### scripts/package.json NPM package configuration with node-edge-tts dependency. ### references/node_edge_tts_guide.md Complete documentation for node-edge-tts npm package including: - Full voice list by language - Prosody options (rate, pitch, volume) - Usage examples (CLI and Module) - Subtitle generation - Output formats - Best practices and limitations ### Voice Testing Test different voices and preview audio quality at: https://tts.travisvn.com/ Refer to this when you need specific voice details or advanced features. ## Installation To use the bundled scripts: ```bash cd /home/user/clawd/skills/public/tts-skill/scripts npm install ``` This installs: - `node-edge-tts` - TTS library - `commander` - CLI argument parsing ## Workflow 1. **Detect intent**: Check for "tts" trigger or keyword in user message 2. **Choose method**: Use built-in `tts` tool for simple requests, or `scripts/tts-converter.js` for customization 3. **Generate audio**: Convert the target text (message, search results, summary) 4. **Return to user**: The tts tool returns a MEDIA: path; Clawdbot handles delivery ## Testing ### Basic Test Run the test script to verify TTS functionality: ```bash cd /home/user/clawd/skills/public/edge-tts/scripts npm test ``` This generates a test audio file and verifies the TTS service is working. ### Voice Testing Test different voices and preview audio quality at: https://tts.travisvn.com/ ### Integration Test Use the built-in `tts` tool for quick testing: ```javascript // Example: Test TTS with default settings tts("This is a test of the TTS functionality.") ``` ### Configuration Test Verify configuration persistence: ```bash cd /home/user/clawd/skills/public/edge-tts/scripts node config-manager.js --get node config-manager.js --set-voice en-US-GuyNeural node config-manager.js --get ``` ## Troubleshooting - **Test connectivity**: Run `npm test` to check if TTS service is accessible - **Check voice availability**: Use `node tts-converter.js --list-voices` to see available voices - **Verify proxy settings**: If using proxy, test with `node tts-converter.js "test" --proxy http://localhost:7890` - **Check audio output**: The test should generate `test-output.mp3` in the scripts directory ## Notes - node-edge-tts uses Microsoft Edge's online TTS service (updated, working authentication) - No API key needed (free service) - Output is MP3 format by default - Requires internet connection - Supports subtitle generation (JSON format with word-level timing) - **Temporary File Handling**: By default, audio files are saved to the system's temporary directory (`/tmp/edge-tts-temp/` on Unix, `C:\Users\<user>\AppData\Local\Temp\edge-tts-temp\` on Windows) with unique filenames (e.g., `tts_1234567890_abc123.mp3`). Files are not automatically deleted - the calling application (Clawdbot) should handle cleanup after use. You can specify a custom output path with the `--output` option if permanent storage is needed. - **TTS keyword filtering**: The skill automatically filters out TTS-related keywords (tts, TTS, text-to-speech) from text before conversion to avoid converting the trigger words themselves to audio - For repeated preferences, use `config-manager.js` to set defaults - **Default voice**: `en-US-MichelleNeural` (female, natural) - Neural voices (ending in `Neural`) provide higher quality than Standard voices

path: DISTRIBUTION.md
content: # Edge-TTS Skill Distribution

## Overview

This document provides information for distributing the Edge-TTS skill package.

## Package Contents

The distributable package includes:

- `SKILL.md` - Complete skill documentation
- `scripts/` - Node.js scripts for TTS conversion and configuration
- `references/` - Additional documentation and guides
- `dist/` - Distribution artifacts and installation scripts

## Installation Requirements

### System Requirements
- Node.js (v14.0.0 or higher)
- npm (v6.0.0 or higher)
- Internet connection for TTS service
- Approximately 50MB disk space

### Dependencies
The package includes all required dependencies:
- `node-edge-tts` - Microsoft Edge TTS service wrapper
- `commander` - CLI argument parsing

## Installation Instructions

### Method 1: Direct Installation (Recommended)
```bash
# Clone or download the package
git clone https://github.com/clawdbot/edge-tts-skill.git
cd edge-tts-skill

# Install dependencies
npm install

# Test the installation
npm test
```

### Method 2: Manual Installation
```bash
# Create installation directory
mkdir -p /home/user/clawd/skills/public/edge-tts
cd /home/user/clawd/skills/public/edge-tts

# Copy package contents
cp -r /path/to/edge-tts-package/* .

# Install dependencies
npm install

# Run tests
npm test
```

## Usage

### Basic TTS
```javascript
// Simple TTS conversion
tts("Hello, world!")
```

### Advanced Usage
```bash
# Convert text with custom voice
node scripts/tts-converter.js "Hello, world!" --voice en-US-GuyNeural

# List available voices
node scripts/tts-converter.js --list-voices

# Configure default settings
node scripts/config-manager.js --set-voice en-US-AriaNeural
```

## Testing

### Package Validation
```bash
# Run package tests
npm test

# Verify voice list
node scripts/tts-converter.js --list-voices

# Test TTS conversion
node scripts/tts-converter.js "This is a test." --output test.mp3
```

## Voice Testing

Test different voices and preview audio quality at: https://tts.travisvn.com/

## Support

For issues and support, please contact the skill maintainer or visit the official documentation.

## License

MIT License - See LICENSE file for details.

path: _meta.json
content: ownerId: kn7ay2qcqva8ek62e2yrgnjevn7ztee6
slug: edge-tts
version: 2.0.0
publishedAt: 1769456031010

path: install.sh
content: #!/bin/bash
# Edge-TTS Skill Installation Script

set -e

echo "Installing Edge-TTS Skill..."
echo ""

# Install Node.js dependencies
echo "Installing Node.js dependencies..."
cd scripts
npm install --production
cd ..

echo ""
echo "✓ Edge-TTS Skill installed successfully!"
echo ""
echo "To test the installation:"
echo "  cd scripts"
echo "  npm test"
echo ""
echo "For configuration:"
echo "  cd scripts"
echo "  node config-manager.js --help"


path: references/node_edge_tts_guide.md
content: # node-edge-tts Reference

node-edge-tts is a Node.js module that utilizes Microsoft Edge's online TTS (Text-to-Speech) service. It supports multiple voices, languages, and audio formats.

## Installation

```bash
npm install node-edge-tts
# or
npx node-edge-tts -t "Hello world"
```

## Core Concepts

### Voices

node-edge-tts provides access to Microsoft Edge's neural voices. Voice names follow this pattern:
- `en-US-AriaNeural` - English (US), female voice
- `en-US-GuyNeural` - English (US), male voice
- `es-ES-ElviraNeural` - Spanish (Spain), female voice

Format: `{language_code}-{region_code}-{Name}{VoiceType}`

### Output Formats

node-edge-tts supports various output formats for audio quality:
- `audio-24khz-48kbitrate-mono-mp3` - 24kHz, 48kbps, mono MP3 (default)
- `audio-24khz-96kbitrate-mono-mp3` - 24kHz, 96kbps, mono MP3 (higher quality)
- [REDACTED_SECRET] - 48kHz, 96kbps, stereo MP3 (highest quality)

## Common Voice Names

### English
- `en-US-AriaNeural` (female, natural, recommended)
- `en-US-GuyNeural` (male, natural)
- `en-GB-SoniaNeural` (female, British)
- `en-GB-RyanNeural` (male, British)

### Spanish
- `es-ES-ElviraNeural` (female, Spain)
- `es-MX-DaliaNeural` (female, Mexico)

### French
- `fr-FR-DeniseNeural` (female)
- `fr-FR-HenriNeural` (male)

### German
- `de-DE-KatjaNeural` (female)
- `de-DE-ConradNeural` (male)

### Asian Languages
- `ja-JP-NanamiNeural` (Japanese, female)
- `zh-CN-XiaoxiaoNeural` (Chinese, female)
- `ko-KR-SunHiNeural` (Korean, female)

### Arabic
- `ar-SA-ZariyahNeural` (female)
- `ar-SA-HamedNeural` (male)

## Usage Patterns

### Command Line Usage

```bash
# Basic usage
npx node-edge-tts -t "Hello world" -f output.mp3

# With voice and language
npx node-edge-tts -t "Hello world" -v en-US-AriaNeural -l en-US -f output.mp3

# With pitch and rate
npx node-edge-tts -t "Hello world" --rate "+10%" --pitch "-10%" -f output.mp3

# With subtitles
npx node-edge-tts -t "Hello world" -f output.mp3 -s
```

### Module Usage

```javascript
const { EdgeTTS } = require('node-edge-tts');

async function generateSpeech(text, outputFile) {
  const tts = new EdgeTTS({
    voice: 'en-US-AriaNeural',
    lang: 'en-US',
    outputFormat: 'audio-24khz-48kbitrate-mono-mp3',
    pitch: 'default',
    rate: 'default',
    volume: 'default',
  });

  await tts.ttsPromise(text, outputFile);
}

generateSpeech('Hello world!', 'output.mp3');
```

### With Options

```javascript
const { EdgeTTS } = require('node-edge-tts');

async function generateSpeech(text, outputFile) {
  const tts = new EdgeTTS({
    voice: 'en-US-AriaNeural',
    lang: 'en-US',
    outputFormat: 'audio-24khz-96kbitrate-mono-mp3',
    saveSubtitles: true,
    proxy: 'http://localhost:7890',
    timeout: 10000,
    pitch: '+10%',
    rate: '+20%',
    volume: '-10%',
  });

  await tts.ttsPromise(text, outputFile);
  // Subtitles saved to output.json
}
```

## Prosody Options

### Rate (Speed)
Control speech speed with percentage adjustments:
- `"default"` - Normal speed (default)
- `"+10%"` - 10% faster
- `"-20%"` - 20% slower
- `"+50%"` - 50% faster

### Pitch
Adjust voice pitch in percentages:
- `"default"` - Normal pitch (default)
- `"+10%"` - Higher pitch
- `"-10%"` - Lower pitch

### Volume
Adjust volume level in percentages:
- `"default"` - Normal volume (default)
- `"+10%"` - 10% louder
- `"-20%"` - 20% quieter

## Subtitles

node-edge-tts supports JSON subtitle generation with word-level timing:

```bash
npx node-edge-tts -t "Hello world" -f output.mp3 -s
```

This generates:
- `output.mp3` - Audio file
- `output.json` - Subtitle file with timing data

**Subtitle format:**
```json
[
  {
    "part": "Hello ",
    "start": 100,
    "end": 500
  },
  {
    "part": "world",
    "start": 500,
    "end": 900
  }
]
```

- `part`: Word or phrase
- `start`: Start time in milliseconds
- `end`: End time in milliseconds

## Best Practices

1. **Choose appropriate voices**: Neural voices (ending in `Neural`) are higher quality
2. **Adjust rate for content**: Faster for news, slower for stories
3. **Use natural phrasing**: Punctuation affects speech rhythm
4. **Test voices**: Different voices work better for different content types
5. **Consider audience**: Choose voices that match your target audience
6. **Use subtitles for accessibility**: Generate subtitles for videos and presentations
7. **Optimize output format**: Higher bitrate for music/pro use, lower for voice notes

## CLI Options Reference

| Option | Short | Description | Default |
|--------|---------|-------------|----------|
| --help | -h | Show help | - |
| --version | | Show version number | - |
| --text | -t | Text to convert (required) | - |
| --filepath | -f | Output file path | "./output.mp3" |
| --voice | -v | Voice name | "zh-CN-XiaoyiNeural" |
| --lang | -l | Language code | "zh-CN" |
| --outputFormat | -o | Output format | "audio-24khz-48kbitrate-mono-mp3" |
| --pitch | | Pitch of voice | "default" |
| --rate | -r | Rate of voice | "default" |
| --volume | | Volume of voice | "default" |
| --saveSubtitles | -s | Save subtitles | false |
| --proxy | -p | Proxy URL | - |
| --timeout | | Request timeout (ms) | 10000 |

## Limitations

- Requires internet connection (uses Microsoft Edge online service)
- Maximum text length depends on the service
- Rate limiting may apply for excessive use
- Voice availability depends on Microsoft Edge service
- Not all prosody options work with all voices

## Integration with Clawdbot

When using with the `tts` tool:
1. Generate audio using node-edge-tts
2. Save the audio file to the workspace
3. Return the MEDIA: path to Clawdbot
4. Clawdbot routes the audio to the appropriate channel

Example flow:
```javascript
// Generate audio
const outputPath = await textToSpeech("Your text here", {
  voice: "en-US-AriaNeural",
  lang: "en-US",
});

// Return to Clawdbot via tts tool
// (handled internally by Clawdbot when you call the tts tool)
```

## Version Information

- Current version: 1.2.9
- Last published: 3 days ago
- License: MIT
- Homepage: github.com/SchneeHertz/node-edge-tts


path: scripts/config-manager.js
content: #!/usr/bin/env node
/**
 * TTS Configuration Manager
 *
 * Manages user preferences for TTS including voice, language, rate, pitch, and volume.
 * Stores preferences in a JSON file for persistence.
 *
 * Usage:
 *   node config-manager.js --set-voice en-US-AriaNeural
 *   node config-manager.js --get
 *   node config-manager.js --reset
 */

const fs = require('fs/promises');
const path = require('path');
const { program } = require('commander');

const DEFAULT_CONFIG_PATH = path.join(require('os').homedir(), '.tts-config.json');

const DEFAULT_CONFIG = {
  voice: 'en-US-MichelleNeural',
  lang: 'en-US',
  outputFormat: 'audio-24khz-48kbitrate-mono-mp3',
  pitch: 'default',
  rate: 'default',
  volume: 'default',
  saveSubtitles: false,
  proxy: '',
  timeout: 10000,
};

/**
 * Load TTS configuration from file
 * @param {string} configPath - Optional custom config path
 * @returns {Promise<object>} Configuration object
 */
async function loadConfig(configPath = DEFAULT_CONFIG_PATH) {
  try {
    const data = await fs.readFile(configPath, 'utf-8');
    const loaded = JSON.parse(data);
    // Merge with defaults to ensure all fields exist
    return { ...DEFAULT_CONFIG, ...loaded };
  } catch (error) {
    // File doesn't exist or is invalid - return defaults
    console.log(`ℹ  Config file not found or invalid, using defaults`);
    return { ...DEFAULT_CONFIG };
  }
}

/**
 * Save TTS configuration to file
 * @param {object} config - Configuration object to save
 * @param {string} configPath - Optional custom config path
 * @returns {Promise<boolean>} Success status
 */
async function saveConfig(config, configPath = DEFAULT_CONFIG_PATH) {
  try {
    const configDir = path.dirname(configPath);
    await fs.mkdir(configDir, { recursive: true });
    await fs.writeFile(configPath, JSON.stringify(config, null, 2));
    return true;
  } catch (error) {
    console.error('Error saving config:', error.message);
    return false;
  }
}

/**
 * Get configuration value(s)
 * @param {string|null} key - Optional key to get (null for all)
 * @param {string} configPath - Optional custom config path
 * @returns {Promise<any>} Configuration value or entire config
 */
async function getConfig(key = null, configPath = DEFAULT_CONFIG_PATH) {
  const config = await loadConfig(configPath);
  return key ? config[key] : config;
}

/**
 * Set configuration value
 * @param {string} key - Configuration key
 * @param {any} value - Value to set
 * @param {string} configPath - Optional custom config path
 * @returns {Promise<boolean>} Success status
 */
async function setConfig(key, value, configPath = DEFAULT_CONFIG_PATH) {
  const config = await loadConfig(configPath);
  config[key] = value;
  return saveConfig(config, configPath);
}

/**
 * Reset configuration to defaults
 * @param {string} configPath - Optional custom config path
 * @returns {Promise<object>} Default configuration
 */
async function resetConfig(configPath = DEFAULT_CONFIG_PATH) {
  try {
    await fs.unlink(configPath);
  } catch (error) {
    // File doesn't exist - that's fine
  }
  return { ...DEFAULT_CONFIG };
}

/**
 * Convert config to CLI arguments for tts-converter
 * @param {object} config - Configuration object
 * @returns {Array<string>} CLI arguments
 */
function configToArgs(config) {
  const args = [];

  if (config.voice) {
    args.push(`--voice`, config.voice);
  }
  if (config.lang) {
    args.push(`--lang`, config.lang);
  }
  if (config.outputFormat) {
    args.push(`--format`, config.outputFormat);
  }
  if (config.pitch && config.pitch !== 'default') {
    args.push(`--pitch`, config.pitch);
  }
  if (config.rate && config.rate !== 'default') {
    args.push(`--rate`, config.rate);
  }
  if (config.volume && config.volume !== 'default') {
    args.push(`--volume`, config.volume);
  }
  if (config.saveSubtitles) {
    args.push(`--save-subtitles`);
  }
  if (config.proxy) {
    args.push(`--proxy`, config.proxy);
  }
  if (config.timeout !== 10000) {
    args.push(`--timeout`, config.timeout.toString());
  }

  return args;
}

// CLI setup
program
  .option('--config-path <path>', 'Path to config file')
  .option('-g, --get [key]', 'Get config value (or all if no key specified)')
  .option('-s, --set <key> <value>', 'Set config value')
  .option('--set-voice <voice>', 'Set default voice')
  .option('--set-lang <lang>', 'Set default language code')
  .option('--set-format <format>', 'Set default output format')
  .option('--set-pitch <pitch>', 'Set default pitch')
  .option('--set-rate <rate>', 'Set default rate')
  .option('--set-volume <volume>', 'Set default volume')
  .option('--toggle-subtitles', 'Toggle subtitle saving')
  .option('--set-proxy <proxy>', 'Set proxy URL')
  .option('--set-timeout <ms>', 'Set timeout in milliseconds')
  .option('--reset', 'Reset to defaults')
  .option('--export', 'Export config as JSON')
  .option('--to-cli', 'Convert config to CLI arguments')
  .description('Manage TTS configuration')
  .version('2.0.0');

program.parse(process.argv);
const options = program.opts();
const configPath = options.configPath || DEFAULT_CONFIG_PATH;

async function main() {
  if (options.reset) {
    const config = await resetConfig(configPath);
    console.log('✓ Configuration reset to defaults');
    console.log(JSON.stringify(config, null, 2));
    return;
  }

  if (options.get !== undefined) {
    const config = await getConfig(options.get || null, configPath);
    console.log(JSON.stringify(config, null, 2));
    return;
  }

  if (options.toCli) {
    const config = await getConfig(null, configPath);
    const args = configToArgs(config);
    console.log(args.join(' '));
    return;
  }

  if (options.set) {
    const [key, value] = options.set;
    const success = await setConfig(key, value, configPath);
    if (success) {
      console.log(`✓ Set ${key} = ${value}`);
    }
    return;
  }

  if (options.setVoice) {
    await setConfig('voice', options.setVoice, configPath);
    console.log(`✓ Set voice = ${options.setVoice}`);
    return;
  }

  if (options.setLang) {
    await setConfig('lang', options.setLang, configPath);
    console.log(`✓ Set lang = ${options.setLang}`);
    return;
  }

  if (options.setFormat) {
    await setConfig('outputFormat', options.setFormat, configPath);
    console.log(`✓ Set format = ${options.setFormat}`);
    return;
  }

  if (options.setPitch) {
    await setConfig('pitch', options.setPitch, configPath);
    console.log(`✓ Set pitch = ${options.setPitch}`);
    return;
  }

  if (options.setRate) {
    await setConfig('rate', options.setRate, configPath);
    console.log(`✓ Set rate = ${options.setRate}`);
    return;
  }

  if (options.setVolume) {
    await setConfig('volume', options.setVolume, configPath);
    console.log(`✓ Set volume = ${options.setVolume}`);
    return;
  }

  if (options.toggleSubtitles) {
    const config = await getConfig(null, configPath);
    await setConfig('saveSubtitles', !config.saveSubtitles, configPath);
    console.log(`✓ Toggled subtitles: ${config.saveSubtitles} -> ${!config.saveSubtitles}`);
    return;
  }

  if (options.setProxy) {
    await setConfig('proxy', options.setProxy, configPath);
    console.log(`✓ Set proxy = ${options.setProxy}`);
    return;
  }

  if (options.setTimeout) {
    await setConfig('timeout', parseInt(options.setTimeout), configPath);
    console.log(`✓ Set timeout = ${options.setTimeout}ms`);
    return;
  }

  if (options.export) {
    const config = await getConfig(null, configPath);
    console.log(JSON.stringify(config, null, 2));
    return;
  }

  // Show current config if no action specified
  const config = await getConfig(null, configPath);
  console.log(JSON.stringify(config, null, 2));
}

main().catch(error => {
  console.error('Error:', error.message);
  process.exit(1);
});

module.exports = {
  loadConfig,
  saveConfig,
  getConfig,
  setConfig,
  resetConfig,
  configToArgs,
};


path: scripts/package-lock.json
content: {
  "name": "edge-tts-scripts",
  "version": "2.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "edge-tts-scripts",
      "version": "2.0.0",
      "license": "MIT",
      "dependencies": {
        "commander": "^11.0.0",
        "node-edge-tts": "^1.2.9"
      }
    },
    "node_modules/agent-base": {
      "version": "7.1.4",
      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 14"
      }
    },
    "node_modules/ansi-regex": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/cliui": {
      "version": "8.0.1",
      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "string-width": "^4.2.0",
        "strip-ansi": "^6.0.1",
        "wrap-ansi": "^7.0.0"
      },
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/commander": {
      "version": "11.1.0",
      "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=16"
      }
    },
    "node_modules/debug": {
      "version": "4.4.3",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ms": "^2.1.3"
      },
      "engines": {
        "node": ">=6.0"
      },
      "peerDependenciesMeta": {
        "supports-color": {
          "optional": true
        }
      }
    },
    "node_modules/emoji-regex": {
      "version": "8.0.0",
      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/escalade": {
      "version": "3.2.0",
      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/get-caller-file": {
      "version": "2.0.5",
      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "engines": {
        "node": "6.* || 8.* || >= 10.*"
      }
    },
    "node_modules/https-proxy-agent": {
      "version": "7.0.6",
      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "agent-base": "^7.1.2",
        "debug": "4"
      },
      "engines": {
        "node": ">= 14"
      }
    },
    "node_modules/is-fullwidth-code-point": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/node-edge-tts": {
      "version": "1.2.9",
      "resolved": "https://registry.npmjs.org/node-edge-tts/-/node-edge-tts-1.2.9.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "https-proxy-agent": "^7.0.1",
        "ws": "^8.13.0",
        "yargs": "^17.7.2"
      },
      "bin": {
        "node-edge-tts": "bin.js"
      }
    },
    "node_modules/require-directory": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/string-width": {
      "version": "4.2.3",
      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "emoji-regex": "^8.0.0",
        "is-fullwidth-code-point": "^3.0.0",
        "strip-ansi": "^6.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/strip-ansi": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ansi-regex": "^5.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/wrap-ansi": {
      "version": "7.0.0",
      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ansi-styles": "^4.0.0",
        "string-width": "^4.1.0",
        "strip-ansi": "^6.0.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
      }
    },
    "node_modules/ws": {
      "version": "8.19.0",
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=10.0.0"
      },
      "peerDependencies": {
        "bufferutil": "^4.0.1",
        "utf-8-validate": ">=5.0.2"
      },
      "peerDependenciesMeta": {
        "bufferutil": {
          "optional": true
        },
        "utf-8-validate": {
          "optional": true
        }
      }
    },
    "node_modules/y18n": {
      "version": "5.0.8",
      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/yargs": {
      "version": "17.7.2",
      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "cliui": "^8.0.1",
        "escalade": "^3.1.1",
        "get-caller-file": "^2.0.5",
        "require-directory": "^2.1.1",
        "string-width": "^4.2.3",
        "y18n": "^5.0.5",
        "yargs-parser": "^21.1.1"
      },
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/yargs-parser": {
      "version": "21.1.1",
      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "engines": {
        "node": ">=12"
      }
    }
  }
}


path: scripts/package.json
content: {
  "name": "edge-tts-scripts",
  "version": "2.0.0",
  "description": "Edge-TTS converter and configuration management scripts using node-edge-tts",
  "type": "commonjs",
  "scripts": {
    "tts": "node tts-converter.js",
    "config": "node config-manager.js",
    "test": "npx node-edge-tts -t \"Hello, world! This is a test of the Edge TTS service.\" -f test-output.mp3 && echo \"Test completed successfully!\""
  },
  "dependencies": {
    "node-edge-tts": "^1.2.9",
    "commander": "^11.0.0"
  },
  "author": "Clawdbot",
  "license": "MIT"
}

path: scripts/tts-converter.js
content: #!/usr/bin/env node
/**
 * TTS Converter using node-edge-tts
 *
 * Converts text to speech using Microsoft Edge's online TTS service.
 * Supports multiple voices, languages, speeds, and output formats.
 *
 * Usage:
 *   node tts-converter.js "Your text here" --voice en-US-AriaNeural --rate +10% --output audio.mp3
 */

const { EdgeTTS } = require('node-edge-tts');
const { program } = require('commander');
const fs = require('fs/promises');
const path = require('path');
const os = require('os');

// Constants
const DEFAULT_TIMEOUT_MS = 10000;
const MAX_TEXT_LENGTH = 10000;
const TEMP_DIR = path.join(os.tmpdir(), 'edge-tts-temp');

// Default voice configurations
const DEFAULT_VOICES = {
  en: 'en-US-MichelleNeural',
  es: 'es-ES-ElviraNeural',
  fr: 'fr-FR-DeniseNeural',
  de: 'de-DE-KatjaNeural',
  it: 'it-IT-ElsaNeural',
  ja: 'ja-JP-NanamiNeural',
  zh: 'zh-CN-XiaoxiaoNeural',
  ar: 'ar-SA-ZariyahNeural',
};

/**
 * Validate prosody value (pitch, rate, volume)
 * @param {string} value - Value to validate
 * @returns {boolean} True if valid
 */
function validateProsodyValue(value) {
  if (value === 'default') return true;
  if (typeof value === 'string' && value.endsWith('%')) {
    const num = parseInt(value);
    return !isNaN(num) && num >= -100 && num <= 100;
  }
  return false;
}

/**
 * Ensure temp directory exists
 * @returns {Promise<void>}
 */
async function ensureTempDir() {
  try {
    await fs.access(TEMP_DIR);
  } catch (error) {
    await fs.mkdir(TEMP_DIR, { recursive: true });
  }
}

/**
 * Generate unique temporary file path
 * @param {string} extension - File extension (e.g., '.mp3')
 * @returns {string} Temporary file path
 */
function generateTempPath(extension = '.mp3') {
  const timestamp = Date.now();
  const random = Math.random().toString(36).substring(2, 8);
  const filename = `tts_${timestamp}_${random}${extension}`;
  return path.join(TEMP_DIR, filename);
}

/**
 * Convert text to speech
 * @param {string} text - Text to convert
 * @param {object} options - TTS options
 * @returns {Promise<string>} Path to generated audio file
 */
async function textToSpeech(text, options = {}) {
  const {
    voice,
    lang = 'en-US',
    outputFormat = 'audio-24khz-48kbitrate-mono-mp3',
    pitch = 'default',
    rate = 'default',
    volume = 'default',
    saveSubtitles = false,
    outputPath = null, // If null, use temp file
    proxy,
    timeout = DEFAULT_TIMEOUT_MS,
  } = options;

  // Validate input text
  if (!text || typeof text !== 'string' || text.trim().length === 0) {
    throw new Error('Text cannot be empty');
  }
  
  // Warn about very long text
  if (text.length > MAX_TEXT_LENGTH) {
    console.warn(`⚠  Warning: Text is very long (${text.length} characters), may cause issues`);
  }
  
  // Validate prosody values
  if (!validateProsodyValue(pitch)) {
    throw new Error(`Invalid pitch value: "${pitch}". Must be "default" or a percentage (e.g., "+10%", "-20%")`);
  }
  if (!validateProsodyValue(rate)) {
    throw new Error(`Invalid rate value: "${rate}". Must be "default" or a percentage (e.g., "+10%", "-20%")`);
  }
  if (!validateProsodyValue(volume)) {
    throw new Error(`Invalid volume value: "${volume}". Must be "default" or a percentage (e.g., "+10%", "-20%")`);
  }
  
  // Validate timeout
  if (timeout && timeout <= 0) {
    throw new Error(`Invalid timeout: ${timeout}. Must be a positive number`);
  }

  const finalVoice = voice || DEFAULT_VOICES[lang.split('-')[0]] || DEFAULT_VOICES.en;

  // Ensure temp directory exists and use temp file if no output path specified
  await ensureTempDir();
  const finalOutputPath = outputPath || generateTempPath('.mp3');

  // Filter out TTS-related words from text to avoid converting them to audio
  const ttsKeywords = ['tts', 'text-to-speech', 'text to speech'];
  const filteredText = text.split(/\s+/).filter(word => {
    const lowerWord = word.toLowerCase().replace(/[^\w\s-]/g, '');
    return !ttsKeywords.includes(lowerWord);
  }).join(' ');

  if (filteredText !== text.trim()) {
    console.log(`ℹ  Filtered TTS keywords from text: "${text}" -> "${filteredText}"`);
  }

  console.log(`Converting text to speech...`);
  console.log(`  Text: ${filteredText.substring(0, 50)}${filteredText.length > 50 ? '...' : ''}`);
  console.log(`  Voice: ${finalVoice}`);
  console.log(`  Language: ${lang}`);
  console.log(`  Rate: ${rate}`);
  console.log(`  Pitch: ${pitch}`);
  console.log(`  Volume: ${volume}`);

  try {
    // Create TTS instance with configuration
    const tts = new EdgeTTS({
      voice: finalVoice,
      lang,
      outputFormat,
      saveSubtitles,
      proxy,
      timeout,
      pitch,
      rate,
      volume,
    });

    // Synthesize speech
    await tts.ttsPromise(filteredText, finalOutputPath);

    // Check if file was created
    const stats = await fs.stat(finalOutputPath);
    console.log(`\n✓ Audio saved to: ${finalOutputPath}`);
    console.log(`✓ File size: ${stats.size} bytes`);

    if (saveSubtitles) {
      const subtitlePath = finalOutputPath.replace(/\.[^/.]+$/, '.json');
      try {
        const subStats = await fs.stat(subtitlePath);
        console.log(`✓ Subtitles saved to: ${subtitlePath}`);
        console.log(`✓ Subtitle size: ${subStats.size} bytes`);
      } catch (e) {
        console.log(`ℹ  No subtitles generated`);
      }
    }

    return finalOutputPath;
  } catch (error) {
    console.error('\n✗ Conversion failed:', error.message);
    throw error;
  }
}

/**
 * List available voices (shows common ones - full list available from Microsoft Edge service)
 */
function listVoices() {
  console.log('Common voices by language:\n');

  const voicesByLang = {
    'en': ['en-US-MichelleNeural', 'en-US-AriaNeural', 'en-US-GuyNeural', 'en-GB-SoniaNeural', 'en-GB-RyanNeural'],
    'es': ['es-ES-ElviraNeural', 'es-MX-DaliaNeural'],
    'fr': ['fr-FR-DeniseNeural', 'fr-FR-HenriNeural'],
    'de': ['de-DE-KatjaNeural', 'de-DE-ConradNeural'],
    'it': ['it-IT-ElsaNeural'],
    'ja': ['ja-JP-NanamiNeural'],
    'zh': ['zh-CN-XiaoxiaoNeural', 'zh-CN-YunyangNeural'],
    'ar': ['ar-SA-ZariyahNeural', 'ar-SA-HamedNeural'],
  };

  for (const [lang, voices] of Object.entries(voicesByLang)) {
    console.log(`${lang}:`);
    voices.forEach(v => console.log(`  ${v}`));
  }

  console.log('\nVoice name format: {lang}-{region}-{Name}{VoiceType}');
  console.log('Example: en-US-AriaNeural = English (US), Aria, Neural voice');
}

// CLI setup
program
  .argument('<text>', 'Text to convert to speech')
  .option('-v, --voice <voice>', 'Voice name (e.g., en-US-MichelleNeural)')
  .option('-l, --lang <language>', 'Language code (e.g., en-US, es-ES)', 'en-US')
  .option('-o, --format <format>', 'Output format (e.g., audio-24khz-48kbitrate-mono-mp3)', 'audio-24khz-48kbitrate-mono-mp3')
  .option('--pitch <pitch>', 'Pitch adjustment (e.g., +10%, -20%, default)', 'default')
  .option('-r, --rate <rate>', 'Rate adjustment (e.g., +10%, -20%, default)', 'default')
  .option('--volume <volume>', 'Volume adjustment (e.g., +0%, -50%, default)', 'default')
  .option('-s, --save-subtitles', 'Save subtitles as JSON file', false)
  .option('-f, --output <path>', 'Output file path (default: temp file in system temp dir)')
  .option('-p, --proxy <proxy>', 'Proxy URL (e.g., http://localhost:7890)')
  .option('--timeout <ms>', 'Request timeout in milliseconds', '10000')
  .option('-L, --list-voices', 'List available voices')
  .description('Convert text to speech using node-edge-tts')
  .version('2.0.0');

program.parse(process.argv);
const options = program.opts();
const text = program.args[0];

if (options.listVoices) {
  listVoices();
  process.exit(0);
}

if (!text) {
  console.error('Error: No text provided');
  console.log('Usage: node tts-converter.js "Your text" [options]');
  console.log('Run: node tts-converter.js --list-voices to see available voices');
  process.exit(1);
}

textToSpeech(text, {
  voice: options.voice,
  lang: options.lang,
  outputFormat: options.format,
  pitch: options.pitch,
  rate: options.rate,
  volume: options.volume,
  saveSubtitles: options.saveSubtitles,
  outputPath: options.output,
  proxy: options.proxy,
  timeout: parseInt(options.timeout),
}).catch(error => {
  console.error('Error:', error.message);
  process.exit(1);
});

module.exports = { textToSpeech, listVoices };

path: skill-info.json
content: {
  "name": "edge-tts",
  "version": "2.0.0",
  "description": "Text-to-speech conversion using node-edge-tts npm package for generating audio from text. Supports multiple voices, languages, speed adjustment, pitch control, and subtitle generation.",
  "author": "Clawdbot",
  "license": "MIT",
  "main": "scripts/tts-converter.js",
  "dependencies": {
    "node-edge-tts": "^1.2.9",
    "commander": "^11.0.0"
  },
  "install": {
    "path": "scripts",
    "command": "npm install"
  },
  "files": [
    "SKILL.md",
    "DISTRIBUTION.md",
    "scripts/",
    "references/"
  ]
}


path: skill.yaml
content: name: edge-tts
version: 1.0.0
description: 文字转语音工具，使用edge-tts，支持多种语言和声音，可调节语速和音调。
author: clawd
homepage: https://clawhub.com/skills/edge-tts

triggers:
  - 语音合成
  - 文字转语音
  - TTS
  - 朗读
  - text to speech
  - tts

features:
  - 多语言多声音支持
  - 语速音调调节
  - 字幕生成
  - 无需API密钥

compatibility:
  platforms:
    - openclaw
    - clawdbot


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