1 Commits

Author SHA1 Message Date
92d41d33cd improve(tts-voice): isolate per-request LuxTTS cookie jar 2026-03-16 12:03:00 +08:00

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,8 +197,11 @@ export async function handler(ctx: any) {
return { reply: '❌ LuxTTS 服務未啟動,請先執行:`systemctl --user start luxtts`' }; return { reply: '❌ LuxTTS 服務未啟動,請先執行:`systemctl --user start luxtts`' };
} }
const cookieJar = createCookieJarPath();
try {
// Ensure authentication // Ensure authentication
if (!ensureCookie()) { if (!ensureCookie(cookieJar)) {
return { reply: '❌ LuxTTS 認證失敗,請檢查 ~/LuxTTS/.env 的帳密設定' }; return { reply: '❌ LuxTTS 認證失敗,請檢查 ~/LuxTTS/.env 的帳密設定' };
} }
@@ -192,11 +212,11 @@ export async function handler(ctx: any) {
const paramStr = paramDesc.length ? `${paramDesc.join('、')}` : ''; const paramStr = paramDesc.length ? `${paramDesc.join('、')}` : '';
// Generate // Generate
const wavPath = generateSpeech(text, params); const wavPath = generateSpeech(text, params, cookieJar);
if (!wavPath) { if (!wavPath) {
return { return {
reply: `❌ 語音合成失敗,請稍後再試。`, reply: '❌ 語音合成失敗,請稍後再試。',
metadata: { text, params, error: true }, metadata: { text, params, error: true },
}; };
} }
@@ -210,4 +230,7 @@ export async function handler(ctx: any) {
}, },
files: [wavPath], files: [wavPath],
}; };
} finally {
safeUnlink(cookieJar);
}
} }