Add 5 missing skills to repo for sync coverage

github-repo-search, gooddays-calendar, luxtts,
openclaw-tavily-search, skill-vetter — previously only
in workspace, now tracked in Gitea for full sync.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 20:36:22 +08:00
parent 6451d73732
commit 8bacc868bd
12 changed files with 1043 additions and 0 deletions

134
skills/luxtts/handler.ts Normal file
View File

@@ -0,0 +1,134 @@
/**
* luxtts skill
* 文字轉語音:透過本機 LuxTTS API 進行 voice clone
*/
import { existsSync, readFileSync } from 'fs';
import { execFileSync } from 'child_process';
const LUXTTS_API = process.env.LUXTTS_API || 'http://127.0.0.1:7861/luxtts/api/tts';
const DEFAULT_REF_AUDIO = process.env.LUXTTS_REF_AUDIO || '/home/selig/.openclaw/workspace/media/refs/ref_from_762.wav';
const OUTPUT_DIR = '/home/selig/.openclaw/workspace/media';
const TRIGGER_WORDS = [
'luxtts', 'lux', '文字轉語音', '語音合成', '唸出來', '說出來', '轉語音', 'voice',
];
const SPEED_MODIFIERS: Record<string, number> = {
'慢速': 0.85,
'slow': 0.85,
'快速': 1.15,
'fast': 1.15,
};
function parseMessage(message: string): { text: string; speed: number } {
let cleaned = message;
let speed = 1.0;
for (const trigger of TRIGGER_WORDS) {
const re = new RegExp(trigger, 'gi');
cleaned = cleaned.replace(re, '');
}
for (const [modifier, value] of Object.entries(SPEED_MODIFIERS)) {
const re = new RegExp(modifier, 'gi');
if (re.test(cleaned)) {
cleaned = cleaned.replace(re, '');
speed = value;
}
}
cleaned = cleaned.replace(/^[\s:,、]+/, '').replace(/[\s:,、]+$/, '').trim();
return { text: cleaned, speed };
}
function ensureDependencies() {
if (!existsSync(DEFAULT_REF_AUDIO)) {
throw new Error(`找不到預設參考音檔:${DEFAULT_REF_AUDIO}`);
}
}
function generateSpeech(text: string, speed: number): string {
const timestamp = Date.now();
const outputPath = `${OUTPUT_DIR}/luxtts_clone_${timestamp}.wav`;
const curlCmd = [
'curl', '-sS', '-o', outputPath,
'-F', `ref_audio=@${DEFAULT_REF_AUDIO}`,
'-F', `text=${text}`,
'-F', 'num_steps=4',
'-F', 't_shift=0.9',
'-F', `speed=${speed}`,
'-F', 'duration=5',
'-F', 'rms=0.01',
LUXTTS_API,
];
execFileSync(curlCmd[0], curlCmd.slice(1), {
timeout: 600000,
stdio: 'pipe',
encoding: 'utf8',
});
if (!existsSync(outputPath)) {
throw new Error('LuxTTS 未產生輸出音檔');
}
const header = readFileSync(outputPath).subarray(0, 16).toString('ascii');
if (!header.includes('RIFF') && !header.includes('WAVE')) {
throw new Error(`LuxTTS 回傳非 WAV 音訊,檔頭:${JSON.stringify(header)}`);
}
return outputPath;
}
export async function handler(ctx: any) {
const message = ctx.message?.text || ctx.message?.content || '';
if (!message.trim()) {
return { reply: '請提供要合成的文字例如「luxtts 这个世界已经改变了」' };
}
const { text, speed } = parseMessage(message);
if (!text) {
return { reply: '請提供要合成的文字例如「luxtts 这个世界已经改变了」' };
}
try {
ensureDependencies();
const outputPath = generateSpeech(text, speed);
return {
reply:
'🔊 luxtts 語音合成完成' +
`\n\n📝 文字:${text}` +
`\n⏩ 語速:${speed}` +
`\n🎙 參考音檔:\`${DEFAULT_REF_AUDIO}\`` +
`\n🌐 API\`${LUXTTS_API}\`` +
`\n📂 檔案:\`${outputPath}\``,
metadata: {
text,
speed,
refAudio: DEFAULT_REF_AUDIO,
output: outputPath,
engine: 'luxtts',
backend: 'luxtts-api',
},
files: [outputPath],
};
} catch (error: any) {
return {
reply:
'❌ luxtts 語音合成失敗,請檢查 luxtts 服務、API 與預設參考音檔是否正常。' +
(error?.message ? `\n\n錯誤${error.message}` : ''),
metadata: {
text,
speed,
refAudio: DEFAULT_REF_AUDIO,
engine: 'luxtts',
backend: 'luxtts-api',
error: error?.message || String(error),
},
};
}
}