This commit is contained in:
2026-02-25 06:59:29 +00:00
parent 88fff9efae
commit acd7713c86
37 changed files with 1581 additions and 69 deletions
+30 -12
View File
@@ -9,6 +9,17 @@ import { readTtsConfig } from '@@/api/server-settings/tts';
import { readSttConfig } from '@@/api/server-settings/stt';
import { readOcrConfig } from '@@/api/server-settings/ocr';
async function getUserTtsVoice(email: string): Promise<string | null> {
try {
const settingsFile = Bun.file(getUserSettingsFile(email));
if (await settingsFile.exists()) {
const settings = (await settingsFile.json()) as { tts?: { voice?: string | null } };
return settings.tts?.voice ?? null;
}
} catch {}
return null;
}
const DEFAULT_HOME_DIRS = ['Downloads', 'Documents', 'Music', 'Video', 'Pictures', 'Onboarding'];
const OLD_CACHE_DIRS = ['ocr', 'tts', 'transcriptions', 'audio', 'video'];
const ONBOARDING_SEED = join(DATA_PATH, 'Onboarding');
@@ -373,18 +384,22 @@ router.post('/tts', async (ctx) => {
const s = await stat(absPath);
if (s.isDirectory()) throw errors.BAD_REQUEST('Cannot read aloud a directory');
const ttsConfig = await readTtsConfig();
if (!ttsConfig) throw errors.BAD_REQUEST('TTS not configured — set it up in Settings → Text to Speech');
const userVoice = await getUserTtsVoice(user.email);
const voice = userVoice ?? ttsConfig.voice;
const userDataDir = getUserDataDir(user.email);
const { dir, name } = parsePath(filePath.replace(/^\/+/, ''));
const cacheRel = dir ? `cache/tts/${dir}/${name}.mp3` : `cache/tts/${name}.mp3`;
const voicePrefix = `${voice}/`;
const cacheRel = dir ? `cache/tts/${voicePrefix}${dir}/${name}.mp3` : `cache/tts/${voicePrefix}${name}.mp3`;
const cacheAbs = resolve(userDataDir, cacheRel);
if (existsSync(cacheAbs)) {
return ctx.json({ audioPath: cacheRel, audioRoot: 'user-data' });
}
const ttsConfig = await readTtsConfig();
if (!ttsConfig) throw errors.BAD_REQUEST('TTS not configured — set it up in Settings → Text to Speech');
const content = await readFile(absPath, 'utf-8');
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
@@ -392,7 +407,7 @@ router.post('/tts', async (ctx) => {
if (ttsConfig.provider === 'elevenlabs') {
if (!ttsConfig.apiKey) throw errors.BAD_REQUEST('ElevenLabs API key not configured');
res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(ttsConfig.voice)}`, {
res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(voice)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'xi-api-key': ttsConfig.apiKey },
body: JSON.stringify({ text: content, model_id: ttsConfig.model }),
@@ -402,7 +417,7 @@ router.post('/tts', async (ctx) => {
res = await fetch(`${ttsConfig.url.replace(/\/+$/, '')}/v1/audio/speech`, {
method: 'POST',
headers,
body: JSON.stringify({ model: ttsConfig.model, input: content, voice: ttsConfig.voice, response_format: 'mp3' }),
body: JSON.stringify({ model: ttsConfig.model, input: content, voice, response_format: 'mp3' }),
});
}
if (!res.ok) throw errors.BAD_REQUEST('TTS request failed');
@@ -421,23 +436,26 @@ router.post('/tts-text', async (ctx) => {
if (!text) throw errors.BAD_REQUEST('text is required');
if (!id) throw errors.BAD_REQUEST('id is required');
const ttsConfig = await readTtsConfig();
if (!ttsConfig) throw errors.BAD_REQUEST('TTS not configured — set it up in Settings → Text to Speech');
const userVoice = await getUserTtsVoice(user.email);
const voice = userVoice ?? ttsConfig.voice;
const userDataDir = getUserDataDir(user.email);
const cacheRel = `cache/tts/chat/${id}.mp3`;
const cacheRel = `cache/tts/chat/${voice}/${id}.mp3`;
const cacheAbs = resolve(userDataDir, cacheRel);
if (existsSync(cacheAbs)) {
return ctx.json({ audioPath: cacheRel, audioRoot: 'user-data' });
}
const ttsConfig = await readTtsConfig();
if (!ttsConfig) throw errors.BAD_REQUEST('TTS not configured — set it up in Settings → Text to Speech');
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
let res: Response;
if (ttsConfig.provider === 'elevenlabs') {
if (!ttsConfig.apiKey) throw errors.BAD_REQUEST('ElevenLabs API key not configured');
res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(ttsConfig.voice)}`, {
res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(voice)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'xi-api-key': ttsConfig.apiKey },
body: JSON.stringify({ text, model_id: ttsConfig.model }),
@@ -447,7 +465,7 @@ router.post('/tts-text', async (ctx) => {
res = await fetch(`${ttsConfig.url.replace(/\/+$/, '')}/v1/audio/speech`, {
method: 'POST',
headers,
body: JSON.stringify({ model: ttsConfig.model, input: text, voice: ttsConfig.voice, response_format: 'mp3' }),
body: JSON.stringify({ model: ttsConfig.model, input: text, voice, response_format: 'mp3' }),
});
}
if (!res.ok) throw errors.BAD_REQUEST('TTS request failed');