89 lines
2.6 KiB
TypeScript
89 lines
2.6 KiB
TypeScript
import { NextRequest } from 'next/server';
|
|
import { createLogger } from '@/lib/logger';
|
|
import { apiError, apiSuccess } from '@/lib/server/api-response';
|
|
import { fetchPinnedPublicUrl, PublicUrlFetchError } from '@/lib/server/public-url-fetch';
|
|
const log = createLogger('Azure Voices');
|
|
|
|
export const maxDuration = 30;
|
|
export const runtime = 'nodejs';
|
|
|
|
function publicFetchError(error: unknown) {
|
|
if (
|
|
error instanceof PublicUrlFetchError &&
|
|
(error.code === 'INVALID_URL' ||
|
|
error.code === 'BLOCKED_TARGET' ||
|
|
error.code === 'DNS_FAILURE')
|
|
) {
|
|
return apiError('INVALID_URL', 403, 'Azure endpoint is not an allowed public URL');
|
|
}
|
|
return apiError('UPSTREAM_ERROR', 502, 'Unable to connect to Azure Speech Services');
|
|
}
|
|
|
|
/**
|
|
* Azure TTS Voice List API
|
|
* Fetches available voices from Azure Speech Services
|
|
*/
|
|
export async function POST(req: NextRequest) {
|
|
let baseUrl: string | undefined;
|
|
try {
|
|
const body = await req.json();
|
|
const { apiKey } = body;
|
|
baseUrl = body.baseUrl;
|
|
|
|
if (typeof apiKey !== 'string' || !apiKey) {
|
|
return apiError('MISSING_API_KEY', 400, 'API Key is required');
|
|
}
|
|
|
|
if (typeof baseUrl !== 'string' || !baseUrl) {
|
|
return apiError('MISSING_REQUIRED_FIELD', 400, 'Base URL is required');
|
|
}
|
|
|
|
let voicesUrl: string;
|
|
try {
|
|
voicesUrl = new URL(
|
|
'cognitiveservices/voices/list',
|
|
baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`,
|
|
).href;
|
|
} catch {
|
|
return apiError('INVALID_URL', 403, 'Azure endpoint is not an allowed public URL');
|
|
}
|
|
|
|
let pinnedResponse;
|
|
try {
|
|
pinnedResponse = await fetchPinnedPublicUrl(voicesUrl, {
|
|
headers: { 'Ocp-Apim-Subscription-Key': apiKey },
|
|
signal: AbortSignal.timeout(25_000),
|
|
maxResponseBytes: 8 * 1024 * 1024,
|
|
});
|
|
} catch (error) {
|
|
return publicFetchError(error);
|
|
}
|
|
|
|
const { response } = pinnedResponse;
|
|
try {
|
|
if (response.status >= 300 && response.status < 400) {
|
|
return apiError('REDIRECT_NOT_ALLOWED', 403, 'Redirects are not allowed');
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
return apiError(
|
|
'UPSTREAM_ERROR',
|
|
response.status,
|
|
'Failed to fetch voices from Azure',
|
|
errorText || response.statusText,
|
|
);
|
|
}
|
|
|
|
const voices = await response.json();
|
|
|
|
return apiSuccess({ voices });
|
|
} finally {
|
|
await pinnedResponse.dispose();
|
|
}
|
|
} catch (error) {
|
|
log.error(`Azure voices fetch failed [baseUrl="${baseUrl ?? 'unknown'}"]:`, error);
|
|
return apiError('INTERNAL_ERROR', 500, 'Failed to fetch voices');
|
|
}
|
|
}
|