mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
speech: replace preprocess parsing with strict zod schemas
This commit is contained in:
@@ -32,44 +32,29 @@ export type { LocalSpeechModelId, LocalSttModelId, LocalTtsModelId };
|
||||
|
||||
const DEFAULT_LOCAL_MODELS_SUBDIR = path.join("models", "local-speech");
|
||||
|
||||
const OptionalBooleanFlagSchema = z.preprocess((value) => {
|
||||
if (typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
return value;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === "1" || normalized === "true" || normalized === "yes") {
|
||||
return true;
|
||||
}
|
||||
if (normalized === "0" || normalized === "false" || normalized === "no") {
|
||||
return false;
|
||||
}
|
||||
return undefined;
|
||||
}, z.boolean().optional());
|
||||
const BooleanStringSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.pipe(z.enum(["1", "0", "true", "false", "yes", "no"]))
|
||||
.transform((value) => value === "1" || value === "true" || value === "yes");
|
||||
|
||||
const OptionalFiniteNumberSchema = z.preprocess((value) => {
|
||||
if (typeof value === "number") {
|
||||
return Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
return value;
|
||||
}
|
||||
const parsed = Number.parseFloat(value);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}, z.number().optional());
|
||||
const OptionalBooleanFlagSchema = z
|
||||
.union([z.boolean(), BooleanStringSchema])
|
||||
.optional();
|
||||
|
||||
const OptionalIntegerSchema = z.preprocess((value) => {
|
||||
if (typeof value === "number") {
|
||||
return Number.isInteger(value) ? value : undefined;
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
return value;
|
||||
}
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}, z.number().int().optional());
|
||||
const NumberLikeSchema = z.union([
|
||||
z.number(),
|
||||
z.string().trim().min(1),
|
||||
]);
|
||||
|
||||
const OptionalFiniteNumberSchema = NumberLikeSchema
|
||||
.pipe(z.coerce.number().finite())
|
||||
.optional();
|
||||
|
||||
const OptionalIntegerSchema = NumberLikeSchema
|
||||
.pipe(z.coerce.number().int())
|
||||
.optional();
|
||||
|
||||
const LocalSpeechResolutionSchema = z.object({
|
||||
includeProviderConfig: z.boolean(),
|
||||
|
||||
@@ -166,18 +166,20 @@ function buildAliasMap<T extends string>(modelIds: readonly T[]): Record<string,
|
||||
function createAliasedModelIdSchema<T extends string>(params: {
|
||||
modelIds: readonly T[];
|
||||
aliases: Record<string, T>;
|
||||
}): z.ZodType<T> {
|
||||
}): z.ZodType<T, z.ZodTypeDef, string> {
|
||||
const validIds = new Set(params.modelIds);
|
||||
return z.preprocess((value) => {
|
||||
if (typeof value !== "string") {
|
||||
return value;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return value;
|
||||
}
|
||||
return params.aliases[normalized] ?? normalized;
|
||||
}, z.string().refine((value): value is T => validIds.has(value as T))) as z.ZodType<T>;
|
||||
return z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.refine(
|
||||
(value): value is T =>
|
||||
validIds.has(value as T) || Object.prototype.hasOwnProperty.call(params.aliases, value),
|
||||
{
|
||||
message: "Invalid model id",
|
||||
}
|
||||
)
|
||||
.transform((value) => params.aliases[value] ?? (value as T));
|
||||
}
|
||||
|
||||
const STT_MODEL_ALIASES = buildAliasMap(LOCAL_STT_MODEL_IDS);
|
||||
|
||||
@@ -26,43 +26,33 @@ const OpenAiTtsVoiceSchema = z.enum([
|
||||
|
||||
const OpenAiTtsModelSchema = z.enum(["tts-1", "tts-1-hd"]);
|
||||
|
||||
const OptionalFiniteNumberSchema = z.preprocess((value) => {
|
||||
if (typeof value === "number") {
|
||||
return Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
return value;
|
||||
}
|
||||
const parsed = Number.parseFloat(value);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}, z.number().optional());
|
||||
const NumberLikeSchema = z.union([
|
||||
z.number(),
|
||||
z.string().trim().min(1),
|
||||
]);
|
||||
|
||||
const OptionalTrimmedStringSchema = z.preprocess((value) => {
|
||||
if (typeof value !== "string") {
|
||||
return value;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
}, z.string().optional());
|
||||
const OptionalFiniteNumberSchema = NumberLikeSchema
|
||||
.pipe(z.coerce.number().finite())
|
||||
.optional();
|
||||
|
||||
const OptionalTrimmedStringSchema = z.string().trim().min(1).optional();
|
||||
|
||||
const OpenAiSpeechResolutionSchema = z.object({
|
||||
apiKey: OptionalTrimmedStringSchema,
|
||||
sttConfidenceThreshold: OptionalFiniteNumberSchema,
|
||||
sttModel: OptionalTrimmedStringSchema,
|
||||
ttsVoice: z.preprocess((value) => {
|
||||
if (typeof value !== "string") {
|
||||
return value;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}, OpenAiTtsVoiceSchema.default("alloy")),
|
||||
ttsModel: z.preprocess((value) => {
|
||||
if (typeof value !== "string") {
|
||||
return value;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}, OpenAiTtsModelSchema.default(DEFAULT_OPENAI_TTS_MODEL)),
|
||||
ttsVoice: z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.pipe(OpenAiTtsVoiceSchema)
|
||||
.default("alloy"),
|
||||
ttsModel: z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.pipe(OpenAiTtsModelSchema)
|
||||
.default(DEFAULT_OPENAI_TTS_MODEL),
|
||||
realtimeTranscriptionModel: OptionalTrimmedStringSchema.default(
|
||||
DEFAULT_OPENAI_REALTIME_TRANSCRIPTION_MODEL
|
||||
),
|
||||
|
||||
@@ -9,13 +9,12 @@ import {
|
||||
type RequestedSpeechProviders,
|
||||
} from "./speech-types.js";
|
||||
|
||||
const OptionalSpeechProviderSchema = z.preprocess((value) => {
|
||||
if (typeof value !== "string") {
|
||||
return value;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}, SpeechProviderIdSchema.optional());
|
||||
const OptionalSpeechProviderSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.pipe(SpeechProviderIdSchema)
|
||||
.optional();
|
||||
|
||||
const RequestedSpeechProvidersSchema = z.object({
|
||||
dictationSttProvider: OptionalSpeechProviderSchema.default("local"),
|
||||
|
||||
Reference in New Issue
Block a user