Skill: vshen009/minimax-tts-plus (version 1.2.3)

--- name: MiniMax TTS Plus description: | MiniMax TTS skill (enhanced). Multi-agent voice support (each agent can select a unique voice written in SOUL.md), native voice message for Telegram (MP3) and Feishu (OGG/Opus). Activate when user explicitly requests text-to-speech. Say "文字模式" or "关闭语音" to stop voice generation. Note: This skill uses the MiniMax secret key for API authentication. The API endpoint is https://api.minimaxi.com (not platform.minimax.io). See setup section for details. homepage: https://platform.minimax.io/docs/api-reference/speech-t2a-http metadata: openclaw: emoji: 🎙️ requires: bins: [python3, ffmpeg] env: [MINIMAX_API_KEY, TELEGRAM_BOT_TOKEN, TELEGRAM_TARGET] pip: [requests] primaryEnv: MINIMAX_API_KEY envHelp: MINIMAX_API_KEY: required: true description: MiniMax API Key (secret key for API authentication) howToGet: | 1. Open https://platform.minimax.io 2. Register and log in 3. Get your API Secret Key (Account Management → API Keys) url: https://platform.minimax.io TELEGRAM_BOT_TOKEN: required: false description: Telegram Bot Token (from @BotFather) — only needed for sending voice to Telegram. Use --generate-only or direct Python call if you only need audio generation. TELEGRAM_TARGET: required: false description: Telegram chat ID — only needed together with TELEGRAM_BOT_TOKEN --- # MiniMax TTS Plus Multi-agent + multi-channel native voice message TTS skill. ## Core Script All operations go through `tts-xiaoye.sh` (TTS generation + channel delivery). ## Quick Start ```bash bash tts-xiaoye.sh "Text to speak" ``` ## Multi-Channel Usage | Channel | Command | Format | Notes | |---------|---------|--------|-------| | Telegram | `tts-xiaoye.sh "Text"` | MP3 | Direct send, no transcoding | | Feishu | `tts-xiaoye.sh --feishu "Text"` | OGG/Opus | Auto-transcode to native voice bubble | | Generate only | `tts-xiaoye.sh --generate-only "Text"` | MP3 | Generate file without sending | ### Send Feishu Native Voice Message (Full Flow) ```bash OPUS=$(bash tts-xiaoye.sh --feishu "Feishu voice content" 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin)['audio_file'])") openclaw message send --channel feishu --account personal --target <FeishuUserID> --media "$OPUS" ``` ## Multi-Agent Voice Configuration Each agent can choose a unique voice and write it into their `SOUL.md` Voice Identity section: ```markdown ## Voice Identity - TTS model: speech-2.8-hd - TTS voice: Chinese (Mandarin)_Warm_Girl - TTS script: scripts/tts-xiaoye.sh ``` Recommended voices (verified): | Voice ID | Style | Use Case | |----------|-------|----------| | Chinese (Mandarin)_Warm_Girl | Warm Girl | Personal Assistant | | female-shaonv | Sweet Girl | Default / General | | female-tianmei | Sweet Female | Gentle style | | male-qn-qingse | Youthful Male | Male voice scenario | | Chinese (Mandarin)_Sweet_Lady | Sweet Lady | Formal场合 | ## List Available Voices ```bash python3 tts-xiaoye.sh --list-voices # or directly: python3 scripts/tts.py --list-voices ``` This calls the MiniMax API and prints all available voices organized by category (System Voices, Cloned Voices, Generated Voices). ## Available Models | Model | Characteristic | |-------|---------------| | speech-2.8-hd | Highest quality (recommended) | | speech-2.8-turbo | Faster, slightly lower quality | ## Full Parameters ```bash tts-xiaoye.sh --text "Text" [--voice VoiceID] [--model Model] [--caption Caption] ``` ## Technical Notes - TTS outputs MP3 natively. Telegram sends directly via Bot API `sendVoice` (MP3 supported natively). - Feishu native voice messages require OGG/Opus format. FFmpeg handles conversion (~25ms per audio, negligible). - FFmpeg installation: `brew install ffmpeg` (Linuxbrew/macOS) or `apt install ffmpeg` (Linux). ## Setup 1. Copy `setup.txt` to `.env` and fill in your credentials: ```bash cp skills/minimax-tts-cn/setup.txt skills/minimax-tts-cn/.env # Then edit .env with your real values ``` 2. The script loads credentials from `.env` at runtime — **no hardcoded tokens in scripts**. **Required env vars:** | Variable | Required | Description | |----------|----------|-------------| | `MINIMAX_API_KEY` | ✅ Yes | MiniMax API secret key (from platform.minimax.io) | | `TELEGRAM_BOT_TOKEN` | ❌ No | Telegram bot token — only needed for sending | | `TELEGRAM_TARGET` | ❌ No | Telegram chat ID — only needed together with bot token | > ⚠️ **Security note:** Credentials are loaded from `.env` only — no tokens are hardcoded in shell scripts. The `.env` file is gitignored and never published. > 💡 **API endpoint:** The TTS API uses `https://api.minimaxi.com` (MiniMax's official API server), which is separate from the developer portal at `platform.minimax.io`. --- # MiniMax TTS Plus（多语言增强版） 多 Agent + 多渠道原生语音条增强版 TTS 技能。 ## 核心脚本 所有操作通过 `tts-xiaoye.sh` 完成（TTS 生成 + 渠道发送）。 ## 快速使用 ```bash bash tts-xiaoye.sh "要转语音的文字" ``` ## 多渠道用法 | 渠道 | 命令 | 格式 | 说明 | |------|------|------|------| | Telegram | `tts-xiaoye.sh "文字"` | MP3 | 直接发送语音条，无需转码 | | 飞书 | `tts-xiaoye.sh --feishu "文字"` | OGG/Opus | 自动转码，发原生语音条 | | 仅生成 | `tts-xiaoye.sh --generate-only "文字"` | MP3 | 只生成文件，不发送 | ### 发送飞书原生语音条（完整流程） ```bash OPUS=$(bash tts-xiaoye.sh --feishu "飞书语音内容" 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin)['audio_file'])") openclaw message send --channel feishu --account personal --target <飞书用户ID> --media "$OPUS" ``` ## 多 Agent 音色配置 每个 Agent 可以选择不同音色，写入各自的 `SOUL.md` 的 Voice Identity 节即可： ```markdown ## Voice Identity - TTS model: speech-2.8-hd - TTS voice: Chinese (Mandarin)_Warm_Girl - TTS script: scripts/tts-xiaoye.sh ``` 推荐音色（已验证）： | 音色ID | 风格 | 适用场景 | |--------|------|---------| | Chinese (Mandarin)_Warm_Girl | 温暖少女 | 个人助理 | | female-shaonv | 甜美少女 | 默认/通用 | | female-tianmei | 甜美女性 | 温柔风格 | | male-qn-qingse | 青涩青年男 | 男声场景 | | Chinese (Mandarin)_Sweet_Lady | 甜美女声 | 正式场合 | ## 可用模型 | 模型 | 特点 | |------|------| | speech-2.8-hd | 最高质量（推荐） | | speech-2.8-turbo | 快速，质量略低 | ## 完整参数 ```bash tts-xiaoye.sh --text "文字" [--voice 音色ID] [--model 模型] [--caption 文字] ``` ## 技术说明 - TTS 原生输出 MP3，Telegram 直接发送（Bot API sendVoice 支持 MP3） - 飞书原生语音条需要 OGG/Opus 格式，通过 FFmpeg 转换（耗时约 25ms/音频，可忽略） - FFmpeg 安装方式： - macOS/Linuxbrew: `brew install ffmpeg` - Ubuntu/Debian: `apt install ffmpeg`

path: scripts/tts-xiaoye.sh
content: #!/bin/bash
set -euo pipefail

# MiniMax TTS Voice Message Script (Trinity XiaoYe Edition)
#
# Usage:
#   tts-xiaoye.sh "text to speak"
#   tts-xiaoye.sh "text" [voiceID] [target]
#   tts-xiaoye.sh --text "text" [--voice VoiceID] [--target TelegramChatId] [--caption "caption text"]
#   tts-xiaoye.sh --text "text" --feishu [--caption "caption text"]
#   tts-xiaoye.sh --list-voices
#
# Channels:
#   - Default / Telegram: TTS MP3 -> sendVoice (native voice bubble, no transcoding)
#   - --feishu: TTS MP3 -> FFmpeg convert to OGG/Opus -> Feishu native voice bubble
#   - --generate-only: TTS MP3 only, no sending

WORKSPACE="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}"
SKILL_DIR="$WORKSPACE/skills/minimax-tts-cn"
TTS_PY="$SKILL_DIR/scripts/tts.py"
AUDIO_DIR="$WORKSPACE/generated/tts-audio"
FFMPEG="$(command -v ffmpeg 2>/dev/null || echo "")"

# Load env vars from .env file (if exists) — required: MINIMAX_API_KEY; optional: TELEGRAM_BOT_TOKEN, TELEGRAM_TARGET
ENV_FILE="$SKILL_DIR/.env"
if [[ -f "$ENV_FILE" ]]; then
  while IFS='=' read -r key value; do
    [[ -z "$key" || "$key" == \#* ]] && continue
    value=$(echo "$value" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")
    [[ -n "$value" ]] && export "$key=$value"
  done < "$ENV_FILE"
fi

DEFAULT_TARGET="${TELEGRAM_TARGET:-}"
DEFAULT_MODEL="${TTS_MODEL:-speech-2.8-hd}"
BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}"

TEXT=""
VOICE="Chinese (Mandarin)_Warm_Girl"
TARGET=""
CAPTION=""
GENERATE_ONLY="0"
FEISHU_MODE="0"
POSITIONAL=()

usage() {
  cat <<'EOF'
Usage:
  tts-xiaoye.sh "text to speak"
  tts-xiaoye.sh "text" [voiceID] [target]
  tts-xiaoye.sh --text "text" [--voice VoiceID] [--target TelegramChatId] [--caption "caption text"]
  tts-xiaoye.sh --text "text" --feishu [--caption "caption text"]

Examples:
  # Telegram voice message (MP3, no transcoding)
  bash tts-xiaoye.sh "Voice content"

  # Telegram voice with caption
  bash tts-xiaoye.sh --text "Voice content" --caption "Voice content"

  # Feishu native voice bubble (auto-transcode to OGG/Opus)
  bash tts-xiaoye.sh --text "Feishu voice content" --feishu

  # Generate MP3 only, no sending
  bash tts-xiaoye.sh --text "Voice content" --generate-only
EOF
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --text)
      TEXT="${2:-}"
      shift 2
      ;;
    --voice)
      VOICE="${2:-}"
      shift 2
      ;;
    --target)
      TARGET="${2:-}"
      shift 2
      ;;
    --caption)
      CAPTION="${2:-}"
      shift 2
      ;;
    --feishu)
      FEISHU_MODE="1"
      shift
      ;;
    --list-voices)
      python3 "$TTS_PY" --list-voices
      exit 0
      ;;
    --generate-only)
      GENERATE_ONLY="1"
      shift
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    --)
      shift
      while [[ $# -gt 0 ]]; do
        POSITIONAL+=("$1")
        shift
      done
      ;;
    *)
      POSITIONAL+=("$1")
      shift
      ;;
  esac
done

# Positional args: text, voice, target
if [[ -z "$TEXT" && ${#POSITIONAL[@]} -gt 0 ]]; then
  TEXT="${POSITIONAL[0]}"
fi
if [[ ${#POSITIONAL[@]} -gt 1 ]]; then
  VOICE="${POSITIONAL[1]}"
fi
if [[ ${#POSITIONAL[@]} -gt 2 ]]; then
  TARGET="${POSITIONAL[2]}"
fi

if [[ -z "$TEXT" ]]; then
  usage
  exit 1
fi

if [[ ! -f "$TTS_PY" ]]; then
  echo "❌ TTS script not found: $TTS_PY" >&2
  exit 1
fi

mkdir -p "$AUDIO_DIR"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
AUDIO_FILE="$AUDIO_DIR/tts-xiaoye-${TIMESTAMP}.mp3"

# Generate TTS MP3
python3 "$TTS_PY" \
  "$TEXT" \
  --voice "$VOICE" \
  --model "$DEFAULT_MODEL" \
  --output "$AUDIO_FILE" >/dev/null

# --feishu: transcode to OGG/Opus, return path for openclaw message send
if [[ "$FEISHU_MODE" == "1" ]]; then
  if [[ -z "$FFMPEG" ]]; then
    echo "❌ ffmpeg not found, cannot convert to OGG format" >&2
    exit 1
  fi
  OPUS_FILE="${AUDIO_FILE%.mp3}.opus"
  # Convert MP3 -> OGG/Opus (Feishu native voice bubble format)
  "$FFMPEG" -y -i "$AUDIO_FILE" -acodec libopus -vn "$OPUS_FILE" 2>/dev/null
  printf '{"ok":true,"mode":"feishu","audio_file":"%s","voice":"%s","caption":"%s"}\n' \
    "$OPUS_FILE" "$VOICE" "$CAPTION"
  exit 0
fi

# --generate-only: return MP3 path only
if [[ "$GENERATE_ONLY" == "1" ]]; then
  printf '{"ok":true,"mode":"generate","audio_file":"%s","voice":"%s","caption":"%s"}\n' \
    "$AUDIO_FILE" "$VOICE" "$CAPTION"
  exit 0
fi

# Default: Telegram voice message (MP3 direct send, no transcoding)
TARGET="${TARGET:-$DEFAULT_TARGET}"
if [[ -z "$TARGET" ]]; then
  echo "❌ Telegram target is empty" >&2
  exit 1
fi

# sendVoice: plain voice bubble; with caption = voice + text display
if [[ -n "$CAPTION" ]]; then
  RESPONSE=$(curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendVoice" \
    -F "chat_id=${TARGET}" \
    -F "voice=@${AUDIO_FILE}" \
    -F "caption=${CAPTION}")
else
  RESPONSE=$(curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendVoice" \
    -F "chat_id=${TARGET}" \
    -F "voice=@${AUDIO_FILE}")
fi

printf '{"ok":true,"mode":"send-voice","target":"%s","audio_file":"%s","voice":"%s","caption":"%s"}\n' \
  "$TARGET" "$AUDIO_FILE" "$VOICE" "$CAPTION"


path: scripts/tts.py
content: #!/usr/bin/env python3
"""
MiniMax TTS - Text-to-Speech via MiniMax API
"""

import os
import sys
import json
import argparse
import requests
import subprocess

API_BASE = "https://api.minimaxi.com"
DEFAULT_VOICE = "Chinese (Mandarin)_Warm_Girl"
DEFAULT_MODEL = "speech-2.8-turbo"
WORKSPACE_DIR = os.path.expanduser("~/.openclaw/workspace")
SKILL_DIR = os.path.join(WORKSPACE_DIR, "skills", "minimax-tts-cn")
DEFAULT_OUTPUT_DIR = os.path.join(WORKSPACE_DIR, "generated", "tmp")
ENV_FILE = os.path.join(SKILL_DIR, ".env")

def load_env_file(path):
    """Load KEY=VALUE pairs from .env file (simple format only)."""
    env = {}
    if not os.path.exists(path):
        return env

    try:
        with open(path, "r", encoding="utf-8") as f:
            for raw_line in f:
                line = raw_line.strip()
                if not line or line.startswith("#") or "=" not in line:
                    continue
                key, value = line.split("=", 1)
                key = key.strip()
                value = value.strip()
                if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")):
                    value = value[1:-1]
                env[key] = value
    except Exception as e:
        print(f"Warning: Failed to read .env file: {e}", file=sys.stderr)

    return env


def get_api_key():
    """Get API Key: check env var first, then skill .env file."""
    key = os.environ.get("MINIMAX_API_KEY")
    if key:
        return key

    file_env = load_env_file(ENV_FILE)
    key = file_env.get("MINIMAX_API_KEY")
    if key:
        return key

    print("Error: MINIMAX_API_KEY not set", file=sys.stderr)
    print(f"Set the environment variable, or write MINIMAX_API_KEY=your-key in {ENV_FILE}", file=sys.stderr)
    sys.exit(1)

def get_voice_list(api_key):
    """Fetch and print available voice list."""
    url = f"{API_BASE}/v1/get_voice"
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }
    data = {"voice_type": "all"}

    try:
        resp = requests.post(url, headers=headers, json=data, timeout=30)
        resp.raise_for_status()
        result = resp.json()

        print("Available voices:\n")

        if "system_voice" in result and result["system_voice"]:
            print("=== System Voices ===")
            for v in result["system_voice"]:
                print(f"  {v.get('voice_id')}: {v.get('voice_name')}")

        if "voice_cloning" in result and result["voice_cloning"]:
            print("\n=== Cloned Voices ===")
            for v in result["voice_cloning"]:
                print(f"  {v.get('voice_id')}")

        if "voice_generation" in result and result["voice_generation"]:
            print("\n=== Generated Voices ===")
            for v in result["voice_generation"]:
                print(f"  {v.get('voice_id')}")

    except Exception as e:
        print(f"Failed to fetch voice list: {e}", file=sys.stderr)
        sys.exit(1)

def text_to_speech(text, api_key, model=DEFAULT_MODEL, voice=DEFAULT_VOICE,
                   speed=1.0, format="mp3", output_file=None, pause=0):
    """Call TTS API and save audio to file."""
    url = f"{API_BASE}/v1/t2a_v2"
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }

    # Handle pause: insert silence markers between sentences
    if pause > 0:
        import re
        text = re.sub(r'([。！？\.])([^<])', r'\1<#' + str(pause) + '#>\2', text)

    data = {
        "model": model,
        "text": text,
        "stream": False,
        "voice_setting": {
            "voice_id": voice,
            "speed": speed,
            "vol": 1,
            "pitch": 0
        },
        "audio_setting": {
            "format": format,
            "sample_rate": 32000,
            "bitrate": 128000,
            "channel": 1
        }
    }

    try:
        print(f"Generating speech...", file=sys.stderr)
        resp = requests.post(url, headers=headers, json=data, timeout=120)
        resp.raise_for_status()
        result = resp.json()

        if result.get("base_resp", {}).get("status_code") != 0:
            error_msg = result.get("base_resp", {}).get("status_msg", "Unknown error")
            print(f"API error: {error_msg}", file=sys.stderr)
            sys.exit(1)

        audio_data = result.get("data", {}).get("audio")
        if not audio_data:
            print("Error: No audio data received", file=sys.stderr)
            sys.exit(1)

        # Decode hex-encoded audio bytes
        audio_bytes = bytes.fromhex(audio_data)

        # Determine output file path
        if not output_file:
            os.makedirs(DEFAULT_OUTPUT_DIR, exist_ok=True)
            output_file = os.path.join(DEFAULT_OUTPUT_DIR, f"tts_output.{format}")
        else:
            output_file = os.path.expanduser(output_file)
            output_dir = os.path.dirname(output_file)
            if output_dir:
                os.makedirs(output_dir, exist_ok=True)

        with open(output_file, "wb") as f:
            f.write(audio_bytes)

        # Get audio info
        extra = result.get("extra_info", {})
        duration_ms = extra.get("audio_length", 0)
        duration_sec = duration_ms / 1000 if duration_ms else 0

        print(f"✅ Audio saved to: {output_file}", file=sys.stderr)
        print(f"   Duration: {duration_sec:.2f}s", file=sys.stderr)
        print(f"   Format: {format}", file=sys.stderr)

        return output_file

    except requests.exceptions.RequestException as e:
        print(f"Request failed: {e}", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)

def main():
    parser = argparse.ArgumentParser(description="MiniMax TTS - Text to Speech")
    parser.add_argument("text", nargs="?", help="Text to synthesize")
    parser.add_argument("--list-voices", "-l", action="store_true", help="List all available voices")
    parser.add_argument("--model", "-m", default=DEFAULT_MODEL, help=f"Model (default: {DEFAULT_MODEL})")
    parser.add_argument("--voice", "-v", default=DEFAULT_VOICE, help=f"Voice ID (default: {DEFAULT_VOICE})")
    parser.add_argument("--speed", "-s", type=float, default=1.0, help="Speech speed (default: 1.0)")
    parser.add_argument("--pause", "-p", type=float, default=0, help="Pause between sentences in seconds (default: 0)")
    parser.add_argument("--format", "-f", default="mp3", help="Audio format (default: mp3)")
    parser.add_argument("--output", "-o", help="Output file path")

    args = parser.parse_args()

    [REDACTED_SECRET])

    if args.list_voices:
        get_voice_list(api_key)
        return

    if not args.text:
        parser.print_help()
        print("\nExamples:")
        print(f"  {sys.argv[0]} \"Hello world\"")
        print(f"  {sys.argv[0]} \"Test\" --voice Chinese_Female_Adult --output test.mp3")
        print(f"  {sys.argv[0]} -l  # List all voices")
        sys.exit(1)

    output_file = text_to_speech(
        text=args.text,
        [REDACTED_SECRET],
        model=args.model,
        voice=args.voice,
        speed=args.speed,
        format=args.format,
        output_file=args.output,
        pause=args.pause
    )

    # Output file path (for use by other tools)
    print(output_file)

if __name__ == "__main__":
    main()


path: setup.txt
content: # MiniMax TTS Plus - Environment Variables
# Copy this file to .env and fill in your values.
# NEVER commit .env to version control.

# === Required: MiniMax API (https://api.minimaxi.com) ===
# Get your API Secret Key from: https://platform.minimax.io → Account → API Keys
MINIMAX_API_KEY=your-minimax-secret-key-here

# === Optional: Telegram sending ===
# Only needed if you want to send voice messages to Telegram.
# Get bot token from: https://t.me/BotFather
# Get your chat ID from: https://t.me/useridbot or check @userinfobot
TELEGRAM_BOT_TOKEN=your-telegram-bot-token-here
TELEGRAM_TARGET=your-telegram-chat-id-here

# === Optional: TTS model override ===
# Default: speech-2.8-hd (highest quality)
# TTS_MODEL=speech-2.8-hd


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