Initial commit: OpenClaw Skill Collection

6 custom skills (assign-task, dispatch-webhook, daily-briefing,
task-capture, qmd-brain, tts-voice) with technical documentation.
Compatible with Claude Code, OpenClaw, Codex CLI, and OpenCode.
This commit is contained in:
2026-03-13 10:58:30 +08:00
commit 4c966a3ad2
884 changed files with 140761 additions and 0 deletions

56
skills/tts-voice/SKILL.md Normal file
View File

@@ -0,0 +1,56 @@
---
name: tts-voice
description: 文字轉語音LuxTTS Voice Cloning。將文字合成為 48kHz 高品質語音,支援語音克隆。
triggers:
- "文字轉語音"
- "tts"
- "語音合成"
- "唸出來"
- "說出來"
- "轉語音"
- "voice"
tools:
- exec
---
# tts-voice Skill
## 功能說明
透過本機 LuxTTS 服務port 7860將文字合成為語音。支援
- 預設參考音訊快速合成
- 自訂語速speed、步數steps、溫度t_shift
## 觸發範例
```
使用者「tts 你好,歡迎來到我的頻道」
→ 使用預設參考音訊合成語音,回傳 wav 檔
使用者:「文字轉語音 Hello world」
→ 合成英文語音
使用者:「語音合成 慢速 這是一段測試」
→ speed=0.8 慢速合成
使用者:「唸出來 快速 今天天氣真好」
→ speed=1.3 快速合成
```
## 參數
| 修飾詞 | 效果 |
|--------|------|
| 慢速 / slow | speed=0.8 |
| 快速 / fast | speed=1.3 |
| 高品質 / hq | num_steps=6, t_shift=0.95 |
| (無修飾)| speed=1.0, num_steps=4, t_shift=0.9 |
## 技術細節
- 服務LuxTTSFastAPIport 7860systemd user service
- 認證session cookie自動取得
- 參考音訊:`~/LuxTTS/ref_speech.wav`
- 輸出48kHz wav存放 `/tmp/tts_output_*.wav`
- CPU 模式合成約 15-20 秒

192
skills/tts-voice/handler.ts Normal file
View File

@@ -0,0 +1,192 @@
/**
* tts-voice skill
* 文字轉語音:透過本機 LuxTTS API 合成語音
*
* 依賴:
* - LuxTTS server running on port 7860 (systemd user service)
* - ~/LuxTTS/.env (LUXTTS_USER, LUXTTS_PASS)
* - curl CLI
*/
import { execSync } from 'child_process';
import { readFileSync, existsSync, unlinkSync } from 'fs';
const LUXTTS_BASE = 'http://localhost:7860';
const REF_AUDIO = '/home/selig/LuxTTS/ref_speech.wav';
const ENV_PATH = '/home/selig/LuxTTS/.env';
const COOKIE_JAR = '/tmp/luxtts-skill-cookie';
// Trigger keywords to strip from user message
const TRIGGER_WORDS = [
'文字轉語音', '語音合成', '唸出來', '說出來', '轉語音',
'tts', 'voice',
];
// Speed/quality modifiers
const MODIFIERS: Record<string, Partial<TtsParams>> = {
'慢速': { speed: 0.8 },
'slow': { speed: 0.8 },
'快速': { speed: 1.3 },
'fast': { speed: 1.3 },
'高品質': { numSteps: 6, tShift: 0.95 },
'hq': { numSteps: 6, tShift: 0.95 },
};
interface TtsParams {
speed: number;
numSteps: number;
tShift: number;
}
/** Read credentials from .env */
function loadCredentials(): { user: string; pass: string } {
try {
const content = readFileSync(ENV_PATH, 'utf-8');
const user = content.match(/^LUXTTS_USER=(.+)$/m)?.[1]?.trim() || 'admin';
const pass = content.match(/^LUXTTS_PASS=(.+)$/m)?.[1]?.trim() || '';
return { user, pass };
} catch {
return { user: 'admin', pass: '' };
}
}
/** Ensure we have a valid session cookie */
function ensureCookie(): boolean {
const { user, pass } = loadCredentials();
if (!pass) return false;
try {
execSync(
`curl -s -o /dev/null -w "%{http_code}" -c ${COOKIE_JAR} ` +
`-d "username=${user}&password=${pass}" ` +
`${LUXTTS_BASE}/luxtts/login`,
{ timeout: 10000 }
);
return existsSync(COOKIE_JAR);
} catch {
return false;
}
}
/** Check if LuxTTS service is alive */
function healthCheck(): boolean {
try {
const result = execSync(
`curl -s -o /dev/null -w "%{http_code}" ${LUXTTS_BASE}/luxtts/api/health`,
{ timeout: 5000 }
).toString().trim();
return result === '200';
} catch {
return false;
}
}
/** Extract text and modifiers from user message */
function parseMessage(message: string): { text: string; params: TtsParams } {
let cleaned = message;
const params: TtsParams = { speed: 1.0, numSteps: 4, tShift: 0.9 };
// Remove trigger words
for (const trigger of TRIGGER_WORDS) {
const re = new RegExp(trigger, 'gi');
cleaned = cleaned.replace(re, '');
}
// Extract and apply modifiers
for (const [mod, overrides] of Object.entries(MODIFIERS)) {
const re = new RegExp(mod, 'gi');
if (re.test(cleaned)) {
cleaned = cleaned.replace(re, '');
Object.assign(params, overrides);
}
}
// Clean up
cleaned = cleaned.replace(/^[\s:,、]+/, '').replace(/[\s:,、]+$/, '').trim();
return { text: cleaned, params };
}
/** Call LuxTTS API to generate speech */
function generateSpeech(text: string, params: TtsParams): string | null {
const timestamp = Date.now();
const outPath = `/tmp/tts_output_${timestamp}.wav`;
try {
const httpCode = execSync(
`curl -s -o ${outPath} -w "%{http_code}" ` +
`-b ${COOKIE_JAR} ` +
`-X POST ${LUXTTS_BASE}/luxtts/api/tts ` +
`-F "ref_audio=@${REF_AUDIO}" ` +
`-F "text=${text.replace(/"/g, '\\"')}" ` +
`-F "num_steps=${params.numSteps}" ` +
`-F "t_shift=${params.tShift}" ` +
`-F "speed=${params.speed}"`,
{ timeout: 120000 } // 2 min timeout for CPU synthesis
).toString().trim();
if (httpCode === '200' && existsSync(outPath)) {
return outPath;
}
// Clean up failed output
if (existsSync(outPath)) unlinkSync(outPath);
return null;
} catch {
if (existsSync(outPath)) unlinkSync(outPath);
return null;
}
}
// ─── 主 handler ─────────────────────────────────────────────────────────────
export async function handler(ctx: any) {
const message = ctx.message?.text || ctx.message?.content || '';
if (!message.trim()) {
return { reply: '請提供要合成的文字例如「tts 你好,歡迎來到我的頻道」' };
}
// Parse user input
const { text, params } = parseMessage(message);
if (!text) {
return { reply: '請提供要合成的文字例如「tts 你好,歡迎來到我的頻道」' };
}
// Health check
if (!healthCheck()) {
return { reply: '❌ LuxTTS 服務未啟動,請先執行:`systemctl --user start luxtts`' };
}
// Ensure authentication
if (!ensureCookie()) {
return { reply: '❌ LuxTTS 認證失敗,請檢查 ~/LuxTTS/.env 的帳密設定' };
}
// Build parameter description
const paramDesc: string[] = [];
if (params.speed !== 1.0) paramDesc.push(`語速 ${params.speed}x`);
if (params.numSteps !== 4) paramDesc.push(`步數 ${params.numSteps}`);
const paramStr = paramDesc.length ? `${paramDesc.join('、')}` : '';
// Generate
const wavPath = generateSpeech(text, params);
if (!wavPath) {
return {
reply: `❌ 語音合成失敗,請稍後再試。`,
metadata: { text, params, error: true },
};
}
return {
reply: `🔊 語音合成完成${paramStr}\n\n📝 文字:${text}\n📂 檔案:\`${wavPath}\``,
metadata: {
text,
params,
output: wavPath,
},
files: [wavPath],
};
}