Compare commits
1 Commits
main
...
improve/tt
| Author | SHA1 | Date | |
|---|---|---|---|
| 92d41d33cd |
@@ -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],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user