Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92d41d33cd |
@@ -8,13 +8,15 @@
|
||||
* - curl CLI
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'crypto';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { readFileSync, existsSync, unlinkSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
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 = [
|
||||
@@ -38,6 +40,18 @@ interface TtsParams {
|
||||
tShift: number;
|
||||
}
|
||||
|
||||
function createCookieJarPath(): string {
|
||||
return join(tmpdir(), `luxtts-skill-cookie-${randomUUID()}.txt`);
|
||||
}
|
||||
|
||||
function safeUnlink(path: string): void {
|
||||
try {
|
||||
if (existsSync(path)) unlinkSync(path);
|
||||
} catch {
|
||||
// ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
/** Read credentials from .env */
|
||||
function loadCredentials(): { user: string; pass: string } {
|
||||
try {
|
||||
@@ -51,10 +65,13 @@ function loadCredentials(): { user: string; pass: string } {
|
||||
}
|
||||
|
||||
/** Ensure we have a valid session cookie */
|
||||
function ensureCookie(): boolean {
|
||||
function ensureCookie(cookieJar: string): boolean {
|
||||
const { user, pass } = loadCredentials();
|
||||
if (!pass) return false;
|
||||
|
||||
// always start fresh to avoid stale session side effects
|
||||
safeUnlink(cookieJar);
|
||||
|
||||
try {
|
||||
const result = spawnSync(
|
||||
'curl',
|
||||
@@ -62,7 +79,7 @@ function ensureCookie(): boolean {
|
||||
'-s',
|
||||
'-o', '/dev/null',
|
||||
'-w', '%{http_code}',
|
||||
'-c', COOKIE_JAR,
|
||||
'-c', cookieJar,
|
||||
'-X', 'POST',
|
||||
'-d', `username=${user}&password=${pass}`,
|
||||
`${LUXTTS_BASE}/luxtts/login`,
|
||||
@@ -71,7 +88,7 @@ function ensureCookie(): boolean {
|
||||
);
|
||||
|
||||
const httpCode = (result.stdout || '').trim();
|
||||
return result.status === 0 && httpCode === '200' && existsSync(COOKIE_JAR);
|
||||
return result.status === 0 && httpCode === '200' && existsSync(cookieJar);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -120,7 +137,7 @@ function parseMessage(message: string): { text: string; params: TtsParams } {
|
||||
}
|
||||
|
||||
/** Call LuxTTS API to generate speech */
|
||||
function generateSpeech(text: string, params: TtsParams): string | null {
|
||||
function generateSpeech(text: string, params: TtsParams, cookieJar: string): string | null {
|
||||
const timestamp = Date.now();
|
||||
const outPath = `/tmp/tts_output_${timestamp}.wav`;
|
||||
|
||||
@@ -129,7 +146,7 @@ function generateSpeech(text: string, params: TtsParams): string | null {
|
||||
'-s',
|
||||
'-o', outPath,
|
||||
'-w', '%{http_code}',
|
||||
'-b', COOKIE_JAR,
|
||||
'-b', cookieJar,
|
||||
'-X', 'POST',
|
||||
`${LUXTTS_BASE}/luxtts/api/tts`,
|
||||
'-F', `ref_audio=@${REF_AUDIO}`,
|
||||
@@ -151,10 +168,10 @@ function generateSpeech(text: string, params: TtsParams): string | null {
|
||||
}
|
||||
|
||||
// Clean up failed output
|
||||
if (existsSync(outPath)) unlinkSync(outPath);
|
||||
safeUnlink(outPath);
|
||||
return null;
|
||||
} catch {
|
||||
if (existsSync(outPath)) unlinkSync(outPath);
|
||||
safeUnlink(outPath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -180,34 +197,40 @@ export async function handler(ctx: any) {
|
||||
return { reply: '❌ LuxTTS 服務未啟動,請先執行:`systemctl --user start luxtts`' };
|
||||
}
|
||||
|
||||
// Ensure authentication
|
||||
if (!ensureCookie()) {
|
||||
return { reply: '❌ LuxTTS 認證失敗,請檢查 ~/LuxTTS/.env 的帳密設定' };
|
||||
}
|
||||
const cookieJar = createCookieJarPath();
|
||||
|
||||
// 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('、')})` : '';
|
||||
try {
|
||||
// Ensure authentication
|
||||
if (!ensureCookie(cookieJar)) {
|
||||
return { reply: '❌ LuxTTS 認證失敗,請檢查 ~/LuxTTS/.env 的帳密設定' };
|
||||
}
|
||||
|
||||
// Generate
|
||||
const wavPath = generateSpeech(text, params);
|
||||
// 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, cookieJar);
|
||||
|
||||
if (!wavPath) {
|
||||
return {
|
||||
reply: '❌ 語音合成失敗,請稍後再試。',
|
||||
metadata: { text, params, error: true },
|
||||
};
|
||||
}
|
||||
|
||||
if (!wavPath) {
|
||||
return {
|
||||
reply: `❌ 語音合成失敗,請稍後再試。`,
|
||||
metadata: { text, params, error: true },
|
||||
reply: `🔊 語音合成完成${paramStr}\n\n📝 文字:${text}\n📂 檔案:\`${wavPath}\``,
|
||||
metadata: {
|
||||
text,
|
||||
params,
|
||||
output: wavPath,
|
||||
},
|
||||
files: [wavPath],
|
||||
};
|
||||
} finally {
|
||||
safeUnlink(cookieJar);
|
||||
}
|
||||
|
||||
return {
|
||||
reply: `🔊 語音合成完成${paramStr}\n\n📝 文字:${text}\n📂 檔案:\`${wavPath}\``,
|
||||
metadata: {
|
||||
text,
|
||||
params,
|
||||
output: wavPath,
|
||||
},
|
||||
files: [wavPath],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user