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 DEFAULT_LOCAL_MODELS_SUBDIR = path.join("models", "local-speech");
|
||||||
|
|
||||||
const OptionalBooleanFlagSchema = z.preprocess((value) => {
|
const BooleanStringSchema = z
|
||||||
if (typeof value === "boolean") {
|
.string()
|
||||||
return value;
|
.trim()
|
||||||
}
|
.toLowerCase()
|
||||||
if (typeof value !== "string") {
|
.pipe(z.enum(["1", "0", "true", "false", "yes", "no"]))
|
||||||
return value;
|
.transform((value) => value === "1" || value === "true" || value === "yes");
|
||||||
}
|
|
||||||
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 OptionalFiniteNumberSchema = z.preprocess((value) => {
|
const OptionalBooleanFlagSchema = z
|
||||||
if (typeof value === "number") {
|
.union([z.boolean(), BooleanStringSchema])
|
||||||
return Number.isFinite(value) ? value : undefined;
|
.optional();
|
||||||
}
|
|
||||||
if (typeof value !== "string") {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
const parsed = Number.parseFloat(value);
|
|
||||||
return Number.isFinite(parsed) ? parsed : undefined;
|
|
||||||
}, z.number().optional());
|
|
||||||
|
|
||||||
const OptionalIntegerSchema = z.preprocess((value) => {
|
const NumberLikeSchema = z.union([
|
||||||
if (typeof value === "number") {
|
z.number(),
|
||||||
return Number.isInteger(value) ? value : undefined;
|
z.string().trim().min(1),
|
||||||
}
|
]);
|
||||||
if (typeof value !== "string") {
|
|
||||||
return value;
|
const OptionalFiniteNumberSchema = NumberLikeSchema
|
||||||
}
|
.pipe(z.coerce.number().finite())
|
||||||
const parsed = Number.parseInt(value, 10);
|
.optional();
|
||||||
return Number.isFinite(parsed) ? parsed : undefined;
|
|
||||||
}, z.number().int().optional());
|
const OptionalIntegerSchema = NumberLikeSchema
|
||||||
|
.pipe(z.coerce.number().int())
|
||||||
|
.optional();
|
||||||
|
|
||||||
const LocalSpeechResolutionSchema = z.object({
|
const LocalSpeechResolutionSchema = z.object({
|
||||||
includeProviderConfig: z.boolean(),
|
includeProviderConfig: z.boolean(),
|
||||||
|
|||||||
@@ -166,18 +166,20 @@ function buildAliasMap<T extends string>(modelIds: readonly T[]): Record<string,
|
|||||||
function createAliasedModelIdSchema<T extends string>(params: {
|
function createAliasedModelIdSchema<T extends string>(params: {
|
||||||
modelIds: readonly T[];
|
modelIds: readonly T[];
|
||||||
aliases: Record<string, T>;
|
aliases: Record<string, T>;
|
||||||
}): z.ZodType<T> {
|
}): z.ZodType<T, z.ZodTypeDef, string> {
|
||||||
const validIds = new Set(params.modelIds);
|
const validIds = new Set(params.modelIds);
|
||||||
return z.preprocess((value) => {
|
return z
|
||||||
if (typeof value !== "string") {
|
.string()
|
||||||
return value;
|
.trim()
|
||||||
}
|
.toLowerCase()
|
||||||
const normalized = value.trim().toLowerCase();
|
.refine(
|
||||||
if (!normalized) {
|
(value): value is T =>
|
||||||
return value;
|
validIds.has(value as T) || Object.prototype.hasOwnProperty.call(params.aliases, value),
|
||||||
}
|
{
|
||||||
return params.aliases[normalized] ?? normalized;
|
message: "Invalid model id",
|
||||||
}, z.string().refine((value): value is T => validIds.has(value as T))) as z.ZodType<T>;
|
}
|
||||||
|
)
|
||||||
|
.transform((value) => params.aliases[value] ?? (value as T));
|
||||||
}
|
}
|
||||||
|
|
||||||
const STT_MODEL_ALIASES = buildAliasMap(LOCAL_STT_MODEL_IDS);
|
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 OpenAiTtsModelSchema = z.enum(["tts-1", "tts-1-hd"]);
|
||||||
|
|
||||||
const OptionalFiniteNumberSchema = z.preprocess((value) => {
|
const NumberLikeSchema = z.union([
|
||||||
if (typeof value === "number") {
|
z.number(),
|
||||||
return Number.isFinite(value) ? value : undefined;
|
z.string().trim().min(1),
|
||||||
}
|
]);
|
||||||
if (typeof value !== "string") {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
const parsed = Number.parseFloat(value);
|
|
||||||
return Number.isFinite(parsed) ? parsed : undefined;
|
|
||||||
}, z.number().optional());
|
|
||||||
|
|
||||||
const OptionalTrimmedStringSchema = z.preprocess((value) => {
|
const OptionalFiniteNumberSchema = NumberLikeSchema
|
||||||
if (typeof value !== "string") {
|
.pipe(z.coerce.number().finite())
|
||||||
return value;
|
.optional();
|
||||||
}
|
|
||||||
const trimmed = value.trim();
|
const OptionalTrimmedStringSchema = z.string().trim().min(1).optional();
|
||||||
return trimmed.length > 0 ? trimmed : undefined;
|
|
||||||
}, z.string().optional());
|
|
||||||
|
|
||||||
const OpenAiSpeechResolutionSchema = z.object({
|
const OpenAiSpeechResolutionSchema = z.object({
|
||||||
apiKey: OptionalTrimmedStringSchema,
|
apiKey: OptionalTrimmedStringSchema,
|
||||||
sttConfidenceThreshold: OptionalFiniteNumberSchema,
|
sttConfidenceThreshold: OptionalFiniteNumberSchema,
|
||||||
sttModel: OptionalTrimmedStringSchema,
|
sttModel: OptionalTrimmedStringSchema,
|
||||||
ttsVoice: z.preprocess((value) => {
|
ttsVoice: z
|
||||||
if (typeof value !== "string") {
|
.string()
|
||||||
return value;
|
.trim()
|
||||||
}
|
.toLowerCase()
|
||||||
const normalized = value.trim().toLowerCase();
|
.pipe(OpenAiTtsVoiceSchema)
|
||||||
return normalized.length > 0 ? normalized : undefined;
|
.default("alloy"),
|
||||||
}, OpenAiTtsVoiceSchema.default("alloy")),
|
ttsModel: z
|
||||||
ttsModel: z.preprocess((value) => {
|
.string()
|
||||||
if (typeof value !== "string") {
|
.trim()
|
||||||
return value;
|
.toLowerCase()
|
||||||
}
|
.pipe(OpenAiTtsModelSchema)
|
||||||
const normalized = value.trim().toLowerCase();
|
.default(DEFAULT_OPENAI_TTS_MODEL),
|
||||||
return normalized.length > 0 ? normalized : undefined;
|
|
||||||
}, OpenAiTtsModelSchema.default(DEFAULT_OPENAI_TTS_MODEL)),
|
|
||||||
realtimeTranscriptionModel: OptionalTrimmedStringSchema.default(
|
realtimeTranscriptionModel: OptionalTrimmedStringSchema.default(
|
||||||
DEFAULT_OPENAI_REALTIME_TRANSCRIPTION_MODEL
|
DEFAULT_OPENAI_REALTIME_TRANSCRIPTION_MODEL
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ import {
|
|||||||
type RequestedSpeechProviders,
|
type RequestedSpeechProviders,
|
||||||
} from "./speech-types.js";
|
} from "./speech-types.js";
|
||||||
|
|
||||||
const OptionalSpeechProviderSchema = z.preprocess((value) => {
|
const OptionalSpeechProviderSchema = z
|
||||||
if (typeof value !== "string") {
|
.string()
|
||||||
return value;
|
.trim()
|
||||||
}
|
.toLowerCase()
|
||||||
const normalized = value.trim().toLowerCase();
|
.pipe(SpeechProviderIdSchema)
|
||||||
return normalized.length > 0 ? normalized : undefined;
|
.optional();
|
||||||
}, SpeechProviderIdSchema.optional());
|
|
||||||
|
|
||||||
const RequestedSpeechProvidersSchema = z.object({
|
const RequestedSpeechProvidersSchema = z.object({
|
||||||
dictationSttProvider: OptionalSpeechProviderSchema.default("local"),
|
dictationSttProvider: OptionalSpeechProviderSchema.default("local"),
|
||||||
|
|||||||
Reference in New Issue
Block a user