- replace custom DB adapter with Drizzle setup (SQLite/Postgres schemas) and add `drizzle.config.ts` plus migrations in `drizzle/` and `drizzle_pg/` - switch better-auth to drizzleAdapter, add auth helpers (`getAuthContext`, `requireAuthContext`, `requireAudiobookOwned`), and make `useAuth`/`useAuthSession` safe no-ops when auth is disabled - persist documents/audiobooks in DB with ownership checks, unclaimed fallback, and ref-counted deletes; add FS scan helper for no-auth mode - gate docx-to-pdf, library, voices, whisper, and migration endpoints behind auth when enabled - add unclaimed data scan/claim flow with `/api/user/claim`, `ClaimDataModal`, and server-side scan/claim helpers - refactor rate limiting and account deletion to use Drizzle tables (`user_tts_chars`, `user`) - run migrations via `scripts/migrate-if-auth.mjs` (auto on `pnpm start` + `pnpm migrate`), remove Docker entrypoint and old better-auth migration file - update README and lockfile for the new migration workflow and dependencies
164 lines
No EOL
6 KiB
TypeScript
164 lines
No EOL
6 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { isKokoroModel } from '@/utils/voice';
|
|
import { auth } from '@/lib/server/auth';
|
|
|
|
const OPENAI_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
|
|
const GPT4O_MINI_VOICES = ['alloy', 'ash', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer'];
|
|
const CUSTOM_OPENAI_VOICES = ['af_sarah', 'af_bella', 'af_nicole', 'am_adam', 'am_michael', 'bf_emma', 'bf_isabella', 'bm_george', 'bm_lewis'];
|
|
|
|
const KOKORO_VOICES = [
|
|
'af_alloy', 'af_aoede', 'af_bella', 'af_heart', 'af_jessica', 'af_kore', 'af_nicole', 'af_nova',
|
|
'af_river', 'af_sarah', 'af_sky', 'am_adam', 'am_echo', 'am_eric', 'am_fenrir', 'am_liam',
|
|
'am_michael', 'am_onyx', 'am_puck', 'am_santa', 'bf_alice', 'bf_emma', 'bf_isabella', 'bf_lily',
|
|
'bm_daniel', 'bm_fable', 'bm_george', 'bm_lewis', 'ef_dora', 'em_alex', 'em_santa', 'ff_siwis',
|
|
'hf_alpha', 'hf_beta', 'hm_omega', 'hm_psi', 'if_sara', 'im_nicola', 'jf_alpha', 'jf_gongitsune',
|
|
'jf_nezumi', 'jf_tebukuro', 'jm_kumo', 'pf_dora', 'pm_alex', 'pm_santa', 'zf_xiaobei', 'zf_xiaoni',
|
|
'zf_xiaoxiao', 'zf_xiaoyi', 'zm_yunjian', 'zm_yunxi', 'zm_yunxia', 'zm_yunyang'
|
|
];
|
|
|
|
const ORPHEUS_VOICES = ['tara', 'leah', 'jess', 'leo', 'dan', 'mia', 'zac'];
|
|
|
|
const SESAME_VOICES = ['conversational_a', 'conversational_b', 'read_speech_a', 'read_speech_b', 'read_speech_c', 'read_speech_d', 'none'];
|
|
|
|
function getDefaultVoices(provider: string, model: string): string[] {
|
|
// For OpenAI provider
|
|
if (provider === 'openai') {
|
|
if (model === 'gpt-4o-mini-tts') {
|
|
return GPT4O_MINI_VOICES;
|
|
}
|
|
return OPENAI_VOICES;
|
|
}
|
|
|
|
// For Custom OpenAI-Like provider
|
|
if (provider === 'custom-openai') {
|
|
// If using Kokoro-FastAPI (model string contains 'kokoro'), expose full Kokoro voices
|
|
if (isKokoroModel(model)) {
|
|
return KOKORO_VOICES;
|
|
}
|
|
return CUSTOM_OPENAI_VOICES;
|
|
}
|
|
|
|
// For Deepinfra provider - model-specific voices
|
|
if (provider === 'deepinfra') {
|
|
if (model === 'hexgrad/Kokoro-82M') {
|
|
return KOKORO_VOICES;
|
|
}
|
|
if (model === 'canopylabs/orpheus-3b-0.1-ft') {
|
|
return ORPHEUS_VOICES;
|
|
}
|
|
if (model === 'sesame/csm-1b') {
|
|
return SESAME_VOICES;
|
|
}
|
|
// For ResembleAI/chatterbox and Zyphra models, return special values
|
|
if (model === 'ResembleAI/chatterbox') {
|
|
return ['None'];
|
|
}
|
|
if (model === 'Zyphra/Zonos-v0.1-hybrid' || model === 'Zyphra/Zonos-v0.1-transformer') {
|
|
return ['random'];
|
|
}
|
|
// Default Deepinfra voices
|
|
return CUSTOM_OPENAI_VOICES;
|
|
}
|
|
|
|
// Default fallback
|
|
return OPENAI_VOICES;
|
|
}
|
|
|
|
async function fetchDeepinfraVoices(apiKey: string): Promise<string[]> {
|
|
try {
|
|
const response = await fetch('https://api.deepinfra.com/v1/voices', {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${apiKey}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch Deepinfra voices');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Extract voice names from the response, excluding preset voices
|
|
if (data.voices && Array.isArray(data.voices)) {
|
|
return data.voices
|
|
.filter((voice: { user_id?: string }) => voice.user_id !== 'preset')
|
|
.map((voice: { name: string }) => voice.name);
|
|
}
|
|
return [];
|
|
} catch (error) {
|
|
console.error('Error fetching Deepinfra voices:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function GET(req: NextRequest) {
|
|
try {
|
|
// Auth check - require session
|
|
const session = await auth?.api.getSession({ headers: req.headers });
|
|
if (auth && !session?.user) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
}
|
|
|
|
const openApiKey = req.headers.get('x-openai-key') || process.env.API_KEY || 'none';
|
|
const openApiBaseUrl = req.headers.get('x-openai-base-url') || process.env.API_BASE;
|
|
const provider = req.headers.get('x-tts-provider') || 'openai';
|
|
const model = req.headers.get('x-tts-model') || 'tts-1';
|
|
|
|
// For OpenAI provider, use default voices (no API call needed)
|
|
if (provider === 'openai') {
|
|
return NextResponse.json({ voices: getDefaultVoices(provider, model) });
|
|
}
|
|
|
|
// For Deepinfra provider with specific models that need API fetching
|
|
if (provider === 'deepinfra') {
|
|
const needsApiFetch = model === 'ResembleAI/chatterbox' ||
|
|
model === 'Zyphra/Zonos-v0.1-hybrid' ||
|
|
model === 'Zyphra/Zonos-v0.1-transformer';
|
|
|
|
if (needsApiFetch) {
|
|
const apiVoices = await fetchDeepinfraVoices(openApiKey);
|
|
// Combine default voice with fetched voices
|
|
const defaultVoice = getDefaultVoices(provider, model);
|
|
if (apiVoices.length > 0) {
|
|
return NextResponse.json({ voices: [...defaultVoice, ...apiVoices] });
|
|
}
|
|
}
|
|
|
|
// For other Deepinfra models, return static defaults
|
|
return NextResponse.json({ voices: getDefaultVoices(provider, model) });
|
|
}
|
|
|
|
// For Custom OpenAI-Like provider, try to fetch voices from custom endpoint
|
|
if (provider === 'custom-openai') {
|
|
try {
|
|
const response = await fetch(`${openApiBaseUrl}/audio/voices`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${openApiKey}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.voices) {
|
|
return NextResponse.json({ voices: data.voices });
|
|
}
|
|
}
|
|
} catch {
|
|
console.log('Custom endpoint does not support voices, using defaults');
|
|
}
|
|
|
|
// Fallback to default voices if API call fails
|
|
return NextResponse.json({ voices: getDefaultVoices(provider, model) });
|
|
}
|
|
|
|
// Default fallback
|
|
return NextResponse.json({ voices: getDefaultVoices(provider, model) });
|
|
} catch (error) {
|
|
console.error('Error in voices endpoint:', error);
|
|
const provider = req.headers.get('x-tts-provider') || 'openai';
|
|
const model = req.headers.get('x-tts-model') || 'tts-1';
|
|
return NextResponse.json({ voices: getDefaultVoices(provider, model) });
|
|
}
|
|
} |