improve(tts-voice): isolate per-request LuxTTS cookie jar

This commit is contained in:
2026-03-16 12:03:00 +08:00
parent c1dab26f66
commit 92d41d33cd

View File

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