技能备份 - 2026-04-15 (40个技能)
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: voice-message
|
||||
version: 1.0.4
|
||||
description: Send voice messages across chat channels (Telegram, Discord, Feishu/Lark, Signal, WhatsApp, and others) using edge-tts for text-to-speech and ffmpeg for audio conversion. IMPORTANT - Feishu/Lark does NOT support asVoice=true via the message tool; you MUST use this skill to send voice messages on Feishu, otherwise it will send a file attachment instead of a voice bubble. | 通过 edge-tts 文字转语音和 ffmpeg 音频转换,在各聊天渠道(Telegram、Discord、飞书、Signal、WhatsApp 等)发送语音消息。飞书不支持 message 工具的 asVoice=true,必须使用本 skill 才能发送语音气泡而非文件附件。
|
||||
metadata:
|
||||
openclaw:
|
||||
emoji: "🎤"
|
||||
---
|
||||
|
||||
# Voice Message
|
||||
|
||||
Send text as voice messages to any chat channel.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `edge-tts` — Microsoft Edge TTS (`pip install edge-tts`)
|
||||
- `ffmpeg` / `ffprobe` — audio conversion and duration detection
|
||||
|
||||
## Default Voices
|
||||
|
||||
- Chinese: `zh-CN-XiaoxiaoNeural`
|
||||
- English: `en-US-JennyNeural`
|
||||
- Other languages: see [references/voices.md](references/voices.md)
|
||||
|
||||
## Step 1: Generate Voice File
|
||||
|
||||
Use `scripts/gen_voice.sh` to convert text to an ogg/opus file:
|
||||
|
||||
```bash
|
||||
scripts/gen_voice.sh "你好" /tmp/voice.ogg
|
||||
scripts/gen_voice.sh "Hello" /tmp/voice.ogg en-US-JennyNeural
|
||||
```
|
||||
|
||||
Arguments: `<text> <output.ogg> [voice]`
|
||||
- If voice is omitted, defaults to `zh-CN-XiaoxiaoNeural`.
|
||||
|
||||
## Step 2: Send by Channel
|
||||
|
||||
### Generic (Telegram, Signal, WhatsApp, etc.)
|
||||
|
||||
Use the message tool directly:
|
||||
|
||||
```
|
||||
action=send, asVoice=true, filePath=/tmp/voice.ogg
|
||||
```
|
||||
|
||||
This works for most channels. Telegram confirmed working.
|
||||
|
||||
### Feishu/Lark
|
||||
|
||||
⚠️ Feishu does NOT support `asVoice=true` via the message tool. You must use the dedicated script.
|
||||
|
||||
Use `scripts/send_feishu_voice.sh`:
|
||||
|
||||
```bash
|
||||
scripts/send_feishu_voice.sh /tmp/voice.ogg <receive_id> <tenant_access_token> [receive_id_type]
|
||||
```
|
||||
|
||||
- `receive_id_type`: `open_id` (default), `chat_id`, `user_id`, `union_id`, `email`
|
||||
- The script handles upload (as opus with duration) and sends as audio message type to produce a voice bubble.
|
||||
- To get `tenant_access_token`, use the Feishu tenant token API with your app credentials.
|
||||
|
||||
### Discord
|
||||
|
||||
Discord voice messages require a waveform and special flags.
|
||||
|
||||
1. Generate ogg with `scripts/gen_voice.sh`
|
||||
2. Generate waveform: `python3 scripts/gen_waveform.py /tmp/voice.ogg`
|
||||
- Outputs JSON: `{"duration_secs": 4.2, "waveform": "base64..."}`
|
||||
3. Send via Discord API with `flags: 8192` (IS_VOICE_MESSAGE) and the waveform/duration in attachments metadata.
|
||||
- Missing waveform/duration causes error 50161.
|
||||
|
||||
### Fallback
|
||||
|
||||
If `asVoice=true` does not produce a voice bubble on a channel:
|
||||
1. Try sending via the platform's native API
|
||||
2. If native API unavailable, send as audio file attachment
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn723j18ymp7d11sm0dvgz24s981ndwd",
|
||||
"slug": "voice-message",
|
||||
"version": "1.0.4",
|
||||
"publishedAt": 1772175873705
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Voice Reference
|
||||
|
||||
Recommended edge-tts voices by language.
|
||||
|
||||
| Language | Voice | Type |
|
||||
|----------|-------|------|
|
||||
| Chinese (Mandarin) | zh-CN-XiaoxiaoNeural | Female, warm |
|
||||
| Chinese (Mandarin) | zh-CN-YunjianNeural | Male, steady |
|
||||
| Chinese (Cantonese) | zh-HK-HiuGaaiNeural | Female |
|
||||
| Chinese (Taiwan) | zh-TW-HsiaoChenNeural | Female |
|
||||
| English (US) | en-US-JennyNeural | Female, natural |
|
||||
| English (US) | en-US-GuyNeural | Male |
|
||||
| English (UK) | en-GB-SoniaNeural | Female |
|
||||
| Japanese | ja-JP-NanamiNeural | Female |
|
||||
| Korean | ko-KR-SunHiNeural | Female |
|
||||
| French | fr-FR-DeniseNeural | Female |
|
||||
| German | de-DE-KatjaNeural | Female |
|
||||
| Spanish | es-ES-ElviraNeural | Female |
|
||||
| Portuguese (BR) | pt-BR-FranciscaNeural | Female |
|
||||
| Russian | ru-RU-SvetlanaNeural | Female |
|
||||
| Thai | th-TH-PremwadeeNeural | Female |
|
||||
| Vietnamese | vi-VN-HoaiMyNeural | Female |
|
||||
|
||||
Full list: `edge-tts --list-voices`
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# Generate ogg/opus voice file from text using edge-tts + ffmpeg
|
||||
# Usage: gen_voice.sh <text> <output.ogg> [voice]
|
||||
# Default voice: zh-CN-XiaoxiaoNeural
|
||||
|
||||
set -e
|
||||
|
||||
TEXT="$1"
|
||||
OUTPUT="$2"
|
||||
VOICE="${3:-zh-CN-XiaoxiaoNeural}"
|
||||
|
||||
if [ -z "$TEXT" ] || [ -z "$OUTPUT" ]; then
|
||||
echo "Usage: gen_voice.sh <text> <output.ogg> [voice]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMP_MP3=$(mktemp /tmp/voice_XXXXXX.mp3)
|
||||
trap "rm -f $TMP_MP3" EXIT
|
||||
|
||||
edge-tts --voice "$VOICE" --text "$TEXT" --write-media "$TMP_MP3" 2>&1
|
||||
ffmpeg -y -i "$TMP_MP3" -c:a libopus -b:a 64k "$OUTPUT" 2>&1 | tail -1
|
||||
|
||||
echo "Generated: $OUTPUT"
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate Discord voice message waveform from an ogg file.
|
||||
Usage: gen_waveform.py <ogg_file>
|
||||
Output: JSON with duration_secs and base64-encoded waveform (256 points, 0-255).
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import struct
|
||||
import base64
|
||||
import json
|
||||
import sys
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: gen_waveform.py <ogg_file>")
|
||||
sys.exit(1)
|
||||
|
||||
ogg_file = sys.argv[1]
|
||||
|
||||
# Get duration
|
||||
result = subprocess.run(
|
||||
["ffprobe", "-v", "error", "-show_entries", "format=duration",
|
||||
"-of", "csv=p=0", ogg_file],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
duration_secs = float(result.stdout.strip())
|
||||
|
||||
# Extract raw PCM (mono, 16-bit, 48kHz)
|
||||
result = subprocess.run(
|
||||
["ffmpeg", "-i", ogg_file, "-f", "s16le", "-ac", "1",
|
||||
"-ar", "48000", "-"],
|
||||
capture_output=True
|
||||
)
|
||||
pcm_data = result.stdout
|
||||
|
||||
# Parse samples
|
||||
num_samples = len(pcm_data) // 2
|
||||
samples = struct.unpack(f"<{num_samples}h", pcm_data[:num_samples * 2])
|
||||
|
||||
# Generate 256-point waveform
|
||||
points = 256
|
||||
chunk_size = max(1, num_samples // points)
|
||||
waveform = []
|
||||
|
||||
for i in range(points):
|
||||
start = i * chunk_size
|
||||
end = min(start + chunk_size, num_samples)
|
||||
if start >= num_samples:
|
||||
waveform.append(0)
|
||||
continue
|
||||
chunk = samples[start:end]
|
||||
avg = sum(abs(s) for s in chunk) / len(chunk)
|
||||
# Map to 0-255
|
||||
val = min(255, int(avg / 32768 * 255))
|
||||
waveform.append(val)
|
||||
|
||||
waveform_b64 = base64.b64encode(bytes(waveform)).decode()
|
||||
|
||||
print(json.dumps({
|
||||
"duration_secs": round(duration_secs, 2),
|
||||
"waveform": waveform_b64
|
||||
}))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# Send voice message to Feishu/Lark
|
||||
# Usage: send_feishu_voice.sh <ogg_file> <receive_id> <tenant_access_token> [receive_id_type]
|
||||
# receive_id_type: open_id (default), chat_id, user_id, union_id, email
|
||||
|
||||
set -e
|
||||
|
||||
OGG_FILE="$1"
|
||||
RECEIVE_ID="$2"
|
||||
TOKEN="$3"
|
||||
ID_TYPE="${4:-open_id}"
|
||||
API_BASE="https://open.feishu.cn/open-apis"
|
||||
|
||||
if [ -z "$OGG_FILE" ] || [ -z "$RECEIVE_ID" ] || [ -z "$TOKEN" ]; then
|
||||
echo "Usage: send_feishu_voice.sh <ogg_file> <receive_id> <token> [receive_id_type]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. Get duration in milliseconds
|
||||
DURATION_SEC=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$OGG_FILE")
|
||||
DURATION_MS=$(python3 -c "print(int(float('$DURATION_SEC') * 1000))")
|
||||
|
||||
# 2. Upload file to Feishu
|
||||
UPLOAD_RESP=$(curl -s -X POST "$API_BASE/im/v1/files" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-F "file_type=opus" \
|
||||
-F "file_name=voice.ogg" \
|
||||
-F "duration=$DURATION_MS" \
|
||||
-F "file=@$OGG_FILE")
|
||||
|
||||
FILE_KEY=$(echo "$UPLOAD_RESP" | python3 -c "import json,sys; print(json.load(sys.stdin).get('data',{}).get('file_key',''))")
|
||||
|
||||
if [ -z "$FILE_KEY" ]; then
|
||||
echo "Upload failed: $UPLOAD_RESP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Uploaded: file_key=$FILE_KEY"
|
||||
|
||||
# 3. Send audio message
|
||||
SEND_RESP=$(curl -s -X POST "$API_BASE/im/v1/messages?receive_id_type=$ID_TYPE" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"receive_id\":\"$RECEIVE_ID\",\"msg_type\":\"audio\",\"content\":\"{\\\"file_key\\\":\\\"$FILE_KEY\\\"}\"}")
|
||||
|
||||
echo "Send result: $SEND_RESP"
|
||||
Reference in New Issue
Block a user