项目文件夹

文件
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

154 行
5.2 KiB
TypeScript

/**
* Auto-voice registration API (provider-neutral).
*
* Idempotently ensures an agent's deterministic voice id is registered on the
* selected TTS provider's backend so later TTS can reference it by id (stable
* timbre, lean payload). Dispatches to the provider's VoiceRegistrationAdapter;
* no provider is named here. Folds bootstrap + register + existence-check +
* register-on-invalid into one call:
* - client supplies a cached reference clip → (re)register it under voiceId;
* - else if the voice already exists → no-op;
* - else synthesize the descriptor once, register, and return the clip so the
* client can cache it.
*
* POST /api/generate/voice
*/
import { NextRequest } from 'next/server';
import {
isServerConfiguredProvider,
isServerTTSProviderDisabled,
resolveTTSApiKey,
resolveTTSBaseUrl,
resolveTTSModel,
} from '@/lib/server/provider-config';
import { createLogger } from '@/lib/logger';
import { apiError, apiSuccess } from '@/lib/server/api-response';
import { validateUrlForSSRF } from '@/lib/server/ssrf-guard';
import { normalizeVoiceDesign } from '@/lib/audio/voice-design';
import {
getVoiceRegistrationAdapter,
type VoiceRegistrationConfig,
} from '@/lib/audio/voice-registration';
const log = createLogger('Voice Registration API');
export const maxDuration = 30;
export async function POST(req: NextRequest) {
let providerId: string | undefined;
let voiceId: string | undefined;
try {
const body = (await req.json()) as {
providerId?: string;
voiceId?: string;
descriptor?: unknown;
language?: string;
referenceAudioBase64?: string;
mimeType?: string;
ttsApiKey?: string;
ttsBaseUrl?: string;
ttsModelId?: string;
};
providerId = typeof body.providerId === 'string' ? body.providerId : undefined;
voiceId = typeof body.voiceId === 'string' ? body.voiceId.trim() : undefined;
const design = normalizeVoiceDesign(body.descriptor);
if (!providerId) {
return apiError('MISSING_REQUIRED_FIELD', 400, 'providerId is required');
}
if (!voiceId) {
return apiError('MISSING_REQUIRED_FIELD', 400, 'voiceId is required');
}
if (!design && !body.referenceAudioBase64) {
return apiError(
'MISSING_REQUIRED_FIELD',
400,
'descriptor or referenceAudioBase64 is required',
);
}
// A server-force-disabled provider is off for everyone (#665), same as the TTS route.
if (isServerTTSProviderDisabled(providerId)) {
return apiError('PROVIDER_DISABLED', 403, 'This TTS provider is disabled by the server');
}
const adapter = getVoiceRegistrationAdapter(providerId);
if (!adapter) {
return apiError(
'INVALID_REQUEST',
400,
`Provider "${providerId}" does not support voice registration`,
);
}
// Managed providers are admin-owned: ignore any client-sent key/baseUrl.
const managed = isServerConfiguredProvider('tts', providerId);
const clientBaseUrl = managed ? undefined : body.ttsBaseUrl || undefined;
if (clientBaseUrl) {
const ssrfError = await validateUrlForSSRF(clientBaseUrl);
if (ssrfError) {
return apiError('INVALID_URL', 403, ssrfError);
}
}
const apiKey = resolveTTSApiKey(providerId, managed ? undefined : body.ttsApiKey || undefined);
const baseUrl = resolveTTSBaseUrl(providerId, clientBaseUrl);
if (!baseUrl) {
return apiError('MISSING_REQUIRED_FIELD', 400, 'TTS base URL is required');
}
const cfg: VoiceRegistrationConfig = {
baseUrl,
apiKey,
model: resolveTTSModel(providerId, body.ttsModelId),
};
// Already registered → no-op (also avoids a redundant re-register when the
// client offered a cached clip but the voice is still live on the backend).
if (await adapter.voiceExists(cfg, voiceId)) {
return apiSuccess({ voiceId, registered: true });
}
// Not present, but the client has the cached reference clip → re-register it
// (register-on-invalid; preserves the original timbre instead of re-synthesizing).
if (body.referenceAudioBase64) {
await adapter.registerVoice(cfg, {
voiceId,
referenceAudioBase64: body.referenceAudioBase64,
mimeType: body.mimeType,
});
return apiSuccess({ voiceId, registered: true });
}
// First use → bootstrap-synthesize the descriptor, register, return the clip.
const clip = await adapter.bootstrapReferenceClip(cfg, {
design: design!,
language: body.language,
});
await adapter.registerVoice(cfg, {
voiceId,
referenceAudioBase64: clip.referenceAudioBase64,
mimeType: clip.mimeType,
});
log.info(`Registered auto voice ${voiceId} for provider ${providerId}`);
return apiSuccess({
voiceId,
registered: true,
referenceAudioBase64: clip.referenceAudioBase64,
mimeType: clip.mimeType,
});
} catch (error) {
log.error(
`Voice registration failed [provider=${providerId ?? 'unknown'}, voiceId=${voiceId ?? 'unknown'}]:`,
error,
);
return apiError(
'GENERATION_FAILED',
500,
error instanceof Error ? error.message : String(error),
);
}
}