Feat: support azure tts (#651)

* upgrade deps

* update tts providers

* refactor conversation form

* update tts providers config

* add azure tts api

* fix azure sdk usage

* consume/revoke token

* upgrade deps
This commit is contained in:
an-lee
2024-06-04 10:09:04 +08:00
committed by GitHub
parent 6d687b2d53
commit a3208d876f
12 changed files with 3093 additions and 3406 deletions

View File

@@ -1,777 +0,0 @@
import * as z from "zod";
import { t } from "i18next";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
Button,
FormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
Input,
ScrollArea,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
Textarea,
toast,
} from "@renderer/components/ui";
import { useState, useEffect, useContext } from "react";
import {
AppSettingsProviderContext,
AISettingsProviderContext,
} from "@renderer/context";
import { LoaderIcon } from "lucide-react";
import { useNavigate } from "react-router-dom";
import {
GPT_PROVIDERS,
TTS_PROVIDERS,
GPTShareButton,
} from "@renderer/components";
const conversationFormSchema = z.object({
name: z.string().optional(),
engine: z
.enum(["enjoyai", "openai", "ollama", "googleGenerativeAi"])
.default("openai"),
configuration: z.object({
type: z.enum(["gpt", "tts"]),
model: z.string().optional(),
baseUrl: z.string().optional(),
roleDefinition: z.string().optional(),
temperature: z.number().min(0).max(1).default(0.2),
numberOfChoices: z.number().min(1).default(1),
maxTokens: z.number().min(-1).default(2000),
presencePenalty: z.number().min(-2).max(2).default(0),
frequencyPenalty: z.number().min(-2).max(2).default(0),
historyBufferSize: z.number().min(0).default(10),
tts: z.object({
engine: z.enum(["openai", "enjoyai"]).default("enjoyai"),
model: z.string().default("tts-1"),
voice: z.string(),
baseUrl: z.string().optional(),
}),
}),
});
export const ConversationForm = (props: {
conversation: Partial<ConversationType>;
onFinish?: () => void;
}) => {
const { conversation, onFinish } = props;
const [submitting, setSubmitting] = useState<boolean>(false);
const [gptProviders, setGptProviders] = useState<any>(GPT_PROVIDERS);
const [ttsProviders, setTtsProviders] = useState<any>(TTS_PROVIDERS);
const { EnjoyApp, webApi } = useContext(AppSettingsProviderContext);
const { openai } = useContext(AISettingsProviderContext);
const navigate = useNavigate();
const refreshGptProviders = async () => {
let providers = GPT_PROVIDERS;
try {
const config = await webApi.config("gpt_providers");
providers = Object.assign(providers, config);
} catch (e) {
console.warn(`Failed to fetch remote GPT config: ${e.message}`);
}
try {
const response = await fetch(providers["ollama"]?.baseUrl + "/api/tags");
providers["ollama"].models = (await response.json()).models.map(
(m: any) => m.name
);
} catch (e) {
console.warn(`No ollama server found: ${e.message}`);
}
setGptProviders({ ...providers });
};
const destroyConversation = async () => {
if (!conversation.id) return;
EnjoyApp.conversations.destroy(conversation.id).then(() => {
navigate(`/conversations`);
});
};
const refreshTtsProviders = async () => {
let providers = TTS_PROVIDERS;
try {
const config = await webApi.config("tts_providers");
providers = Object.assign(providers, config);
} catch (e) {
console.warn(`Failed to fetch remote TTS config: ${e.message}`);
}
setTtsProviders({ ...providers });
};
useEffect(() => {
refreshGptProviders();
refreshTtsProviders();
}, []);
const defaultConfig = JSON.parse(JSON.stringify(conversation || {}));
if (defaultConfig.engine === "openai" && openai) {
if (!defaultConfig.configuration) {
defaultConfig.configuration = {};
}
if (!defaultConfig.configuration.model) {
defaultConfig.configuration.model = openai.model;
}
if (!defaultConfig.configuration.baseUrl) {
defaultConfig.configuration.baseUrl = openai.baseUrl;
}
}
if (defaultConfig.configuration.tts?.engine === "openai" && openai) {
if (!defaultConfig.configuration.tts?.baseUrl) {
defaultConfig.configuration.tts.baseUrl = openai.baseUrl;
}
}
const form = useForm<z.infer<typeof conversationFormSchema>>({
resolver: zodResolver(conversationFormSchema),
// @ts-ignore
values: conversation?.id
? {
name: conversation.name,
engine: conversation.engine,
configuration: {
type: conversation.configuration.type || "gpt",
...conversation.configuration,
},
}
: {
name: defaultConfig.name,
engine: defaultConfig.engine,
configuration: {
...defaultConfig.configuration,
},
},
});
const onSubmit = async (data: z.infer<typeof conversationFormSchema>) => {
let { name, engine, configuration } = data;
setSubmitting(true);
try {
configuration = validateConfiguration(data);
} catch (e) {
toast.error(e.message);
setSubmitting(false);
return;
}
if (conversation?.id) {
EnjoyApp.conversations
.update(conversation.id, {
name,
configuration,
})
.then(() => {
onFinish && onFinish();
})
.finally(() => {
setSubmitting(false);
});
} else {
EnjoyApp.conversations
.create({
name,
engine,
configuration,
})
.then(() => {
onFinish && onFinish();
})
.finally(() => {
setSubmitting(false);
});
}
};
const validateConfiguration = (
data: z.infer<typeof conversationFormSchema>
) => {
const { engine, configuration } = data;
Object.keys(configuration).forEach((key) => {
if (key === "type") return;
if (
configuration.type === "gpt" &&
!gptProviders[engine]?.configurable.includes(key)
) {
// @ts-ignore
delete configuration[key];
}
if (
configuration.type === "tts" &&
!ttsProviders[engine]?.configurable.includes(key)
) {
// @ts-ignore
delete configuration.tts[key];
}
});
// use default base url if not set
if (!configuration.baseUrl) {
configuration.baseUrl = gptProviders[engine]?.baseUrl;
}
// use default base url if not set
if (!configuration?.tts?.baseUrl) {
configuration.tts ||= {};
configuration.tts.baseUrl = gptProviders[engine]?.baseUrl;
}
return configuration;
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="h-full flex flex-col pt-6"
data-testid="conversation-form"
>
<div className="mb-4 px-6 flex items-center space-x-4">
<div className="text-lg font-bold">
{conversation.id ? t("editConversation") : t("startConversation")}
</div>
<GPTShareButton conversation={conversation} />
</div>
<ScrollArea className="flex-1 px-4">
<div className="space-y-4 px-2 mb-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.name")}</FormLabel>
<Input value={field.value} onChange={field.onChange} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="configuration.type"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.type")}</FormLabel>
<Select
disabled={Boolean(conversation?.id)}
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectAiType")} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem key="gpt" value="gpt">
GPT
</SelectItem>
<SelectItem key="tts" value="tts">
TTS
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{form.watch("configuration.type") === "gpt" && (
<>
<FormField
control={form.control}
name="engine"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.engine")}</FormLabel>
<Select
disabled={Boolean(conversation?.id)}
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectAiEngine")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.keys(gptProviders).map((key) => (
<SelectItem key={key} value={key}>
{gptProviders[key].name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
{gptProviders[form.watch("engine")]?.description}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="configuration.model"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.model")}</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectAiModel")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{(
gptProviders[form.watch("engine")]?.models || []
).map((option: string) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="configuration.roleDefinition"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.roleDefinition")}
</FormLabel>
<Textarea
placeholder={t(
"models.conversation.roleDefinitionPlaceholder"
)}
className="h-64"
{...field}
/>
<FormMessage />
</FormItem>
)}
/>
{gptProviders[form.watch("engine")]?.configurable.includes(
"temperature"
) && (
<FormField
control={form.control}
name="configuration.temperature"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.temperature")}
</FormLabel>
<Input
type="number"
min="0"
max="1.0"
step="0.1"
value={field.value}
onChange={(event) => {
field.onChange(
event.target.value
? parseFloat(event.target.value)
: 0.0
);
}}
/>
<FormDescription>
{t("models.conversation.temperatureDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{gptProviders[form.watch("engine")]?.configurable.includes(
"maxTokens"
) && (
<FormField
control={form.control}
name="configuration.maxTokens"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.maxTokens")}
</FormLabel>
<Input
type="number"
min="0"
value={field.value}
onChange={(event) => {
if (!event.target.value) return;
field.onChange(parseInt(event.target.value));
}}
/>
<FormDescription>
{t("models.conversation.maxTokensDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{gptProviders[form.watch("engine")]?.configurable.includes(
"presencePenalty"
) && (
<FormField
control={form.control}
name="configuration.presencePenalty"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.presencePenalty")}
</FormLabel>
<Input
type="number"
min="-2"
step="0.1"
max="2"
value={field.value}
onChange={(event) => {
if (!event.target.value) return;
field.onChange(parseInt(event.target.value));
}}
/>
<FormDescription>
{t("models.conversation.presencePenaltyDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{gptProviders[form.watch("engine")]?.configurable.includes(
"frequencyPenalty"
) && (
<FormField
control={form.control}
name="configuration.frequencyPenalty"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.frequencyPenalty")}
</FormLabel>
<Input
type="number"
min="-2"
step="0.1"
max="2"
value={field.value}
onChange={(event) => {
if (!event.target.value) return;
field.onChange(parseInt(event.target.value));
}}
/>
<FormDescription>
{t("models.conversation.frequencyPenaltyDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{gptProviders[form.watch("engine")]?.configurable.includes(
"numberOfChoices"
) && (
<FormField
control={form.control}
name="configuration.numberOfChoices"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.numberOfChoices")}
</FormLabel>
<Input
type="number"
min="1"
step="1.0"
value={field.value}
onChange={(event) => {
field.onChange(
event.target.value
? parseInt(event.target.value)
: 1.0
);
}}
/>
<FormDescription>
{t("models.conversation.numberOfChoicesDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="configuration.historyBufferSize"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.historyBufferSize")}
</FormLabel>
<Input
type="number"
min="0"
step="1"
max="100"
value={field.value}
onChange={(event) => {
field.onChange(
event.target.value
? parseInt(event.target.value)
: 0
);
}}
/>
<FormDescription>
{t("models.conversation.historyBufferSizeDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{gptProviders[form.watch("engine")]?.configurable.includes(
"baseUrl"
) && (
<FormField
control={form.control}
name="configuration.baseUrl"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.baseUrl")}
</FormLabel>
<Input
{...field}
placeholder={t(
"models.conversation.baseUrlDescription"
)}
/>
<FormMessage />
</FormItem>
)}
/>
)}
</>
)}
<FormField
control={form.control}
name="configuration.tts.engine"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.ttsEngine")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectTtsEngine")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.keys(ttsProviders).map((key) => (
<SelectItem key={key} value={key}>
{ttsProviders[key].name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{ttsProviders[
form.watch("configuration.tts.engine")
]?.configurable?.includes("model") && (
<FormField
control={form.control}
name="configuration.tts.model"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.ttsModel")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectTtsModel")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{(
ttsProviders[form.watch("configuration.tts.engine")]
?.models || []
).map((model: string) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
{ttsProviders[
form.watch("configuration.tts.engine")
]?.configurable?.includes("voice") && (
<FormField
control={form.control}
name="configuration.tts.voice"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.ttsVoice")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectTtsVoice")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{(
ttsProviders[form.watch("configuration.tts.engine")]
?.voices || []
).map((voice: string) => (
<SelectItem key={voice} value={voice}>
<span className="capitalize">{voice}</span>
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
{ttsProviders[
form.watch("configuration.tts.engine")
]?.configurable.includes("baseUrl") && (
<FormField
control={form.control}
name="configuration.tts.baseUrl"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.ttsBaseUrl")}</FormLabel>
<Input
{...field}
placeholder={t(
"models.conversation.ttsBaseUrlDescription"
)}
/>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
</ScrollArea>
<div className="flex justify-center space-x-4 py-6 px-6 border-t shadow">
{conversation.id && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
className="w-full h-12 text-destructive"
size="lg"
variant="secondary"
>
{t("delete")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("deleteConversation")}</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
{t("deleteConversationConfirmation")}
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive hover:bg-destructive-hover"
onClick={destroyConversation}
>
{t("delete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
<Button
disabled={
submitting || (conversation.id && !form.formState.isDirty)
}
className="w-full h-12"
data-testid="conversation-form-submit"
size="lg"
type="submit"
>
{submitting && <LoaderIcon className="mr-2 animate-spin" />}
{t("confirm")}
</Button>
</div>
</form>
</Form>
);
};

View File

@@ -0,0 +1,291 @@
import { t } from "i18next";
import { useForm } from "react-hook-form";
import {
FormField,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
Input,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
Textarea,
} from "@renderer/components/ui";
export const ConversationFormGPT = (props: {
conversation: Partial<ConversationType>;
form: ReturnType<typeof useForm>;
gptProviders: any;
}) => {
const { form, gptProviders, conversation } = props;
return (
<>
<FormField
control={form.control}
name="engine"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.engine")}</FormLabel>
<Select
disabled={Boolean(conversation?.id)}
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectAiEngine")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.keys(gptProviders).map((key) => (
<SelectItem key={key} value={key}>
{gptProviders[key].name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
{gptProviders[form.watch("engine")]?.description}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="configuration.model"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.model")}</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectAiModel")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{(gptProviders[form.watch("engine")]?.models || []).map(
(option: string) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="configuration.roleDefinition"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.roleDefinition")}</FormLabel>
<Textarea
placeholder={t("models.conversation.roleDefinitionPlaceholder")}
className="h-64"
{...field}
/>
<FormMessage />
</FormItem>
)}
/>
{gptProviders[form.watch("engine")]?.configurable.includes(
"temperature"
) && (
<FormField
control={form.control}
name="configuration.temperature"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.temperature")}</FormLabel>
<Input
type="number"
min="0"
max="1.0"
step="0.1"
value={field.value}
onChange={(event) => {
field.onChange(
event.target.value ? parseFloat(event.target.value) : 0.0
);
}}
/>
<FormDescription>
{t("models.conversation.temperatureDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{gptProviders[form.watch("engine")]?.configurable.includes(
"maxTokens"
) && (
<FormField
control={form.control}
name="configuration.maxTokens"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.maxTokens")}</FormLabel>
<Input
type="number"
min="0"
value={field.value}
onChange={(event) => {
if (!event.target.value) return;
field.onChange(parseInt(event.target.value));
}}
/>
<FormDescription>
{t("models.conversation.maxTokensDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{gptProviders[form.watch("engine")]?.configurable.includes(
"presencePenalty"
) && (
<FormField
control={form.control}
name="configuration.presencePenalty"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.presencePenalty")}</FormLabel>
<Input
type="number"
min="-2"
step="0.1"
max="2"
value={field.value}
onChange={(event) => {
if (!event.target.value) return;
field.onChange(parseInt(event.target.value));
}}
/>
<FormDescription>
{t("models.conversation.presencePenaltyDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{gptProviders[form.watch("engine")]?.configurable.includes(
"frequencyPenalty"
) && (
<FormField
control={form.control}
name="configuration.frequencyPenalty"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.frequencyPenalty")}</FormLabel>
<Input
type="number"
min="-2"
step="0.1"
max="2"
value={field.value}
onChange={(event) => {
if (!event.target.value) return;
field.onChange(parseInt(event.target.value));
}}
/>
<FormDescription>
{t("models.conversation.frequencyPenaltyDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{gptProviders[form.watch("engine")]?.configurable.includes(
"numberOfChoices"
) && (
<FormField
control={form.control}
name="configuration.numberOfChoices"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.numberOfChoices")}</FormLabel>
<Input
type="number"
min="1"
step="1.0"
value={field.value}
onChange={(event) => {
field.onChange(
event.target.value ? parseInt(event.target.value) : 1.0
);
}}
/>
<FormDescription>
{t("models.conversation.numberOfChoicesDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="configuration.historyBufferSize"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.historyBufferSize")}</FormLabel>
<Input
type="number"
min="0"
step="1"
max="100"
value={field.value}
onChange={(event) => {
field.onChange(
event.target.value ? parseInt(event.target.value) : 0
);
}}
/>
<FormDescription>
{t("models.conversation.historyBufferSizeDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{gptProviders[form.watch("engine")]?.configurable.includes("baseUrl") && (
<FormField
control={form.control}
name="configuration.baseUrl"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.baseUrl")}</FormLabel>
<Input
{...field}
placeholder={t("models.conversation.baseUrlDescription")}
/>
<FormMessage />
</FormItem>
)}
/>
)}
</>
);
};

View File

@@ -0,0 +1,163 @@
import { t } from "i18next";
import { useForm } from "react-hook-form";
import {
FormField,
FormItem,
FormLabel,
FormControl,
FormMessage,
Input,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@renderer/components/ui";
import { useContext } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
export const ConversationFormTTS = (props: {
form: ReturnType<typeof useForm>;
ttsProviders: any;
}) => {
const { form, ttsProviders } = props;
const { learningLanguage } = useContext(AppSettingsProviderContext);
return (
<>
<FormField
control={form.control}
name="configuration.tts.engine"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.ttsEngine")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectTtsEngine")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.keys(ttsProviders).map((key) => (
<SelectItem key={key} value={key}>
{ttsProviders[key].name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{ttsProviders[
form.watch("configuration.tts.engine")
]?.configurable?.includes("model") && (
<FormField
control={form.control}
name="configuration.tts.model"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.ttsModel")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectTtsModel")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{(
ttsProviders[form.watch("configuration.tts.engine")]
?.models || []
).map((model: string) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
{ttsProviders[
form.watch("configuration.tts.engine")
]?.configurable?.includes("voice") && (
<FormField
control={form.control}
name="configuration.tts.voice"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.ttsVoice")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectTtsVoice")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{(
(form.watch("configuration.tts.engine") === "enjoyai"
? ttsProviders.enjoyai.voices[
form.watch("configuration.tts.model").split("/")[0]
]
: ttsProviders[form.watch("configuration.tts.engine")]
.voices) || []
).map((voice: any) => {
if (typeof voice === "string") {
return (
<SelectItem key={voice} value={voice}>
<span className="capitalize">{voice}</span>
</SelectItem>
);
} else if (voice.language === learningLanguage) {
return (
<SelectItem key={voice.value} value={voice.value}>
<span className="capitalize">{voice.label}</span>
</SelectItem>
);
}
})}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
{ttsProviders[
form.watch("configuration.tts.engine")
]?.configurable.includes("baseUrl") && (
<FormField
control={form.control}
name="configuration.tts.baseUrl"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.ttsBaseUrl")}</FormLabel>
<Input
{...field}
placeholder={t("models.conversation.ttsBaseUrlDescription")}
/>
<FormMessage />
</FormItem>
)}
/>
)}
</>
);
};

View File

@@ -0,0 +1,369 @@
import * as z from "zod";
import { t } from "i18next";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
Button,
FormField,
Form,
FormItem,
FormLabel,
FormControl,
FormMessage,
Input,
ScrollArea,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
toast,
} from "@renderer/components/ui";
import { useState, useEffect, useContext } from "react";
import {
AppSettingsProviderContext,
AISettingsProviderContext,
} from "@renderer/context";
import { LoaderIcon } from "lucide-react";
import { useNavigate } from "react-router-dom";
import {
GPT_PROVIDERS,
TTS_PROVIDERS,
GPTShareButton,
ConversationFormGPT,
ConversationFormTTS,
} from "@renderer/components";
const conversationFormSchema = z.object({
name: z.string().optional(),
engine: z
.enum(["enjoyai", "openai", "ollama", "googleGenerativeAi"])
.default("openai"),
configuration: z.object({
type: z.enum(["gpt", "tts"]),
model: z.string().optional(),
baseUrl: z.string().optional(),
roleDefinition: z.string().optional(),
temperature: z.number().min(0).max(1).default(0.2),
numberOfChoices: z.number().min(1).default(1),
maxTokens: z.number().min(-1).default(2000),
presencePenalty: z.number().min(-2).max(2).default(0),
frequencyPenalty: z.number().min(-2).max(2).default(0),
historyBufferSize: z.number().min(0).default(10),
tts: z.object({
engine: z.enum(["openai", "enjoyai"]).default("enjoyai"),
model: z.string().default("openai/tts-1"),
voice: z.string(),
baseUrl: z.string().optional(),
}),
}),
});
export const ConversationForm = (props: {
conversation: Partial<ConversationType>;
onFinish?: () => void;
}) => {
const { conversation, onFinish } = props;
const [submitting, setSubmitting] = useState<boolean>(false);
const [gptProviders, setGptProviders] = useState<any>(GPT_PROVIDERS);
const [ttsProviders, setTtsProviders] = useState<any>(TTS_PROVIDERS);
const { EnjoyApp, webApi } = useContext(AppSettingsProviderContext);
const { openai } = useContext(AISettingsProviderContext);
const navigate = useNavigate();
const refreshGptProviders = async () => {
let providers = GPT_PROVIDERS;
try {
const config = await webApi.config("gpt_providers");
providers = Object.assign(providers, config);
} catch (e) {
console.warn(`Failed to fetch remote GPT config: ${e.message}`);
}
try {
const response = await fetch(providers["ollama"]?.baseUrl + "/api/tags");
providers["ollama"].models = (await response.json()).models.map(
(m: any) => m.name
);
} catch (e) {
console.warn(`No ollama server found: ${e.message}`);
}
setGptProviders({ ...providers });
};
const destroyConversation = async () => {
if (!conversation.id) return;
EnjoyApp.conversations.destroy(conversation.id).then(() => {
navigate(`/conversations`);
});
};
const refreshTtsProviders = async () => {
let providers = TTS_PROVIDERS;
try {
const config = await webApi.config("tts_providers_v2");
providers = Object.assign(providers, config);
} catch (e) {
console.warn(`Failed to fetch remote TTS config: ${e.message}`);
}
setTtsProviders({ ...providers });
};
useEffect(() => {
refreshGptProviders();
refreshTtsProviders();
}, []);
const defaultConfig = JSON.parse(JSON.stringify(conversation || {}));
if (defaultConfig.engine === "openai" && openai) {
if (!defaultConfig.configuration) {
defaultConfig.configuration = {};
}
if (!defaultConfig.configuration.model) {
defaultConfig.configuration.model = openai.model;
}
if (!defaultConfig.configuration.baseUrl) {
defaultConfig.configuration.baseUrl = openai.baseUrl;
}
}
if (defaultConfig.configuration.tts?.engine === "openai" && openai) {
if (!defaultConfig.configuration.tts?.baseUrl) {
defaultConfig.configuration.tts.baseUrl = openai.baseUrl;
}
}
const form = useForm<z.infer<typeof conversationFormSchema>>({
resolver: zodResolver(conversationFormSchema),
// @ts-ignore
values: conversation?.id
? {
name: conversation.name,
engine: conversation.engine,
configuration: {
type: conversation.configuration.type || "gpt",
...conversation.configuration,
},
}
: {
name: defaultConfig.name,
engine: defaultConfig.engine,
configuration: {
...defaultConfig.configuration,
},
},
});
const onSubmit = async (data: z.infer<typeof conversationFormSchema>) => {
let { name, engine, configuration } = data;
setSubmitting(true);
try {
configuration = validateConfiguration(data);
} catch (e) {
toast.error(e.message);
setSubmitting(false);
return;
}
if (conversation?.id) {
EnjoyApp.conversations
.update(conversation.id, {
name,
configuration,
})
.then(() => {
onFinish && onFinish();
})
.finally(() => {
setSubmitting(false);
});
} else {
EnjoyApp.conversations
.create({
name,
engine,
configuration,
})
.then(() => {
onFinish && onFinish();
})
.finally(() => {
setSubmitting(false);
});
}
};
const validateConfiguration = (
data: z.infer<typeof conversationFormSchema>
) => {
const { engine, configuration } = data;
Object.keys(configuration).forEach((key) => {
if (key === "type") return;
if (
configuration.type === "gpt" &&
!gptProviders[engine]?.configurable.includes(key)
) {
// @ts-ignore
delete configuration[key];
}
if (
configuration.type === "tts" &&
!ttsProviders[engine]?.configurable.includes(key)
) {
// @ts-ignore
delete configuration.tts[key];
}
});
// use default base url if not set
if (!configuration.baseUrl) {
configuration.baseUrl = gptProviders[engine]?.baseUrl;
}
// use default base url if not set
if (!configuration?.tts?.baseUrl) {
configuration.tts ||= {};
configuration.tts.baseUrl = gptProviders[engine]?.baseUrl;
}
return configuration;
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="h-full flex flex-col pt-6"
data-testid="conversation-form"
>
<div className="mb-4 px-6 flex items-center space-x-4">
<div className="text-lg font-bold">
{conversation.id ? t("editConversation") : t("startConversation")}
</div>
<GPTShareButton conversation={conversation} />
</div>
<ScrollArea className="flex-1 px-4">
<div className="space-y-4 px-2 mb-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.name")}</FormLabel>
<Input value={field.value} onChange={field.onChange} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="configuration.type"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.type")}</FormLabel>
<Select
disabled={Boolean(conversation?.id)}
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectAiType")} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem key="gpt" value="gpt">
GPT
</SelectItem>
<SelectItem key="tts" value="tts">
TTS
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{form.watch("configuration.type") === "gpt" && (
<ConversationFormGPT
form={form}
gptProviders={gptProviders}
conversation={conversation}
/>
)}
<ConversationFormTTS form={form} ttsProviders={ttsProviders} />
</div>
</ScrollArea>
<div className="flex justify-center space-x-4 py-6 px-6 border-t shadow">
{conversation.id && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
className="w-full h-12 text-destructive"
size="lg"
variant="secondary"
>
{t("delete")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("deleteConversation")}</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
{t("deleteConversationConfirmation")}
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive hover:bg-destructive-hover"
onClick={destroyConversation}
>
{t("delete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
<Button
disabled={
submitting || (conversation.id && !form.formState.isDirty)
}
className="w-full h-12"
data-testid="conversation-form-submit"
size="lg"
type="submit"
>
{submitting && <LoaderIcon className="mr-2 animate-spin" />}
{t("confirm")}
</Button>
</div>
</form>
</Form>
);
};

View File

@@ -1,5 +1,7 @@
export * from "./conversation-card";
export * from "./conversation-form";
export * from "./conversation-form/index";
export * from "./conversation-form/conversation-form-gpt";
export * from "./conversation-form/conversation-form-tts";
export * from "./conversation-shortcuts";
export * from "./speech-player";

View File

@@ -3,9 +3,675 @@ import { t } from "i18next";
export const TTS_PROVIDERS: { [key: string]: any } = {
enjoyai: {
name: "EnjoyAI",
models: ["tts-1", "tts-1-hd"],
voices: ["alloy", "echo", "fable", "onyx", "nova", "shimmer"],
configurable: ["voice"],
models: ["openai/tts-1", "openai/tts-1-hd", "azure/speech"],
voices: {
openai: ["alloy", "echo", "fable", "onyx", "nova", "shimmer"],
azure: [
{
label: "Katja (Female)",
value: "de-DE-KatjaNeural",
language: "de-DE",
},
{
label: "Conradl (Male)",
value: "de-DE-ConradNeural",
language: "de-DE",
},
{
label: "Amala (Female)",
value: "de-DE-AmalaNeural",
language: "de-DE",
},
{
label: "Bernd (Male)",
value: "de-DE-BerndNeural",
language: "de-DE",
},
{
label: "Christoph (Male)",
value: "de-DE-ChristophNeural",
language: "de-DE",
},
{
label: "Elke (Female)",
value: "de-DE-ElkeNeural",
language: "de-DE",
},
{
label: "Gisela (Female, Child)",
value: "de-DE-GiselaNeural",
language: "de-DE",
},
{
label: "Kasper (Male)",
value: "de-DE-KasperNeural",
language: "de-DE",
},
{
label: "Killian (Male)",
value: "de-DE-KillianNeural",
language: "de-DE",
},
{
label: "Klarissa (Female)",
value: "de-DE-KlarissaNeural",
language: "de-DE",
},
{
label: "Klaus (Male)",
value: "de-DE-KlausNeural",
language: "de-DE",
},
{
label: "Louisa (Female)",
value: "de-DE-LouisaNeural",
language: "de-DE",
},
{
label: "Maja (Female)",
value: "de-DE-MajaNeural",
language: "de-DE",
},
{ label: "Ralf (Male)", value: "de-DE-RalfNeural", language: "de-DE" },
{
label: "Tanja (Female)",
value: "de-DE-TanjaNeural",
language: "de-DE",
},
{
label: "Sonia (Female)",
value: "en-GB-SoniaNeural",
language: "en-GB",
},
{ label: "Ryan (Male)", value: "en-GB-RyanNeural", language: "en-GB" },
{
label: "Libby (Female)",
value: "en-GB-LibbyNeural",
language: "en-GB",
},
{
label: "Abbi (Female)",
value: "en-GB-AbbiNeural",
language: "en-GB",
},
{
label: "Alfie (Male)",
value: "en-GB-AlfieNeural",
language: "en-GB",
},
{
label: "Bella (Female)",
value: "en-GB-BellaNeural",
language: "en-GB",
},
{
label: "Elliot (Male)",
value: "en-GB-ElliotNeural",
language: "en-GB",
},
{
label: "Ethan (Male)",
value: "en-GB-EthanNeural",
language: "en-GB",
},
{
label: "Hollie (Female)",
value: "en-GB-HollieNeural",
language: "en-GB",
},
{
label: "Maisie (Female, Child)",
value: "en-GB-MaisieNeural",
language: "en-GB",
},
{ label: "Noah (Male)", value: "en-GB-NoahNeural", language: "en-GB" },
{
label: "Oliver (Male)",
value: "en-GB-OliverNeural",
language: "en-GB",
},
{
label: "Olivia (Female)",
value: "en-GB-OliviaNeural",
language: "en-GB",
},
{
label: "Thomas (Male)",
value: "en-GB-ThomasNeural",
language: "en-GB",
},
{ label: "Ava (Female)", value: "en-US-AvaNeural", language: "en-US" },
{
label: "Andrew (Male)",
value: "en-US-AndrewNeural",
language: "en-US",
},
{
label: "Emma (Female)",
value: "en-US-EmmaNeural",
language: "en-US",
},
{
label: "Brian (Male)",
value: "en-US-BrianNeural",
language: "en-US",
},
{
label: "Jenny (Female)",
value: "en-US-JennyNeural",
language: "en-US",
},
{ label: "Guy (Male)", value: "en-US-GuyNeural", language: "en-US" },
{
label: "Aria (Female)",
value: "en-US-AriaNeural",
language: "en-US",
},
{
label: "Davis (Male)",
value: "en-US-DavisNeural",
language: "en-US",
},
{
label: "Jane (Female)",
value: "en-US-JaneNeural",
language: "en-US",
},
{
label: "Jason (Male)",
value: "en-US-JasonNeural",
language: "en-US",
},
{
label: "Sara (Female)",
value: "en-US-SaraNeural",
language: "en-US",
},
{ label: "Tony (Male)", value: "en-US-TonyNeural", language: "en-US" },
{
label: "Nancy (Female)",
value: "en-US-NancyNeural",
language: "en-US",
},
{
label: "Amber (Female)",
value: "en-US-AmberNeural",
language: "en-US",
},
{
label: "Ana (Female, Child)",
value: "en-US-AnaNeural",
language: "en-US",
},
{
label: "Ashley (Female)",
value: "en-US-AshleyNeural",
language: "en-US",
},
{
label: "Brandon (Male)",
value: "en-US-BrandonNeural",
language: "en-US",
},
{
label: "Christopher (Male)",
value: "en-US-ChristopherNeural",
language: "en-US",
},
{
label: "Cora (Female)",
value: "en-US-CoraNeural",
language: "en-US",
},
{
label: "Elizabeth (Female)",
value: "en-US-ElizabethNeural",
language: "en-US",
},
{ label: "Eric (Male)", value: "en-US-EricNeural", language: "en-US" },
{
label: "Jacob (Male)",
value: "en-US-JacobNeural",
language: "en-US",
},
{
label: "Michelle (Female)",
value: "en-US-MichelleNeural",
language: "en-US",
},
{
label: "Monica (Female)",
value: "en-US-MonicaNeural",
language: "en-US",
},
{
label: "Roger (Male)",
value: "en-US-RogerNeural",
language: "en-US",
},
{
label: "Steffan (Male)",
value: "en-US-SteffanNeural",
language: "en-US",
},
{
label: "AIGenerate1 (Male)",
value: "en-US-AIGenerate1Neural",
language: "en-US",
},
{
label: "AIGenerate2 (Female)",
value: "en-US-AIGenerate2Neural",
language: "en-US",
},
{
label: "Elvira (Female)",
value: "es-ES-ElviraNeural",
language: "es-ES",
},
{
label: "Alvaro (Male)",
value: "es-ES-AlvaroNeural",
language: "es-ES",
},
{
label: "Abril (Female)",
value: "es-ES-AbrilNeural",
language: "es-ES",
},
{
label: "Arnau (Male)",
value: "es-ES-ArnauNeural",
language: "es-ES",
},
{
label: "Dario (Male)",
value: "es-ES-DarioNeural",
language: "es-ES",
},
{
label: "Elias (Male)",
value: "es-ES-EliasNeural",
language: "es-ES",
},
{
label: "Estrella (Female)",
value: "es-ES-EstrellaNeural",
language: "es-ES",
},
{
label: "Irene (Female)",
value: "es-ES-IreneNeural",
language: "es-ES",
},
{
label: "Laia (Female)",
value: "es-ES-LaiaNeural",
language: "es-ES",
},
{ label: "Lia (Female)", value: "es-ES-LiaNeural", language: "es-ES" },
{ label: "Nil (Male)", value: "es-ES-NilNeural", language: "es-ES" },
{ label: "Saul (Male)", value: "es-ES-SaulNeural", language: "es-ES" },
{ label: "Teo (Male)", value: "es-ES-TeoNeural", language: "es-ES" },
{
label: "Triana (Female)",
value: "es-ES-TrianaNeural",
language: "es-ES",
},
{
label: "Vera (Female)",
value: "es-ES-VeraNeural",
language: "es-ES",
},
{
label: "Ximena (Female)",
value: "es-ES-XimenaNeural",
language: "es-ES",
},
{
label: "Denise (Female)",
value: "fr-FR-DeniseNeural",
language: "fr-FR",
},
{
label: "Henri (Male)",
value: "fr-FR-HenriNeural",
language: "fr-FR",
},
{
label: "Alain (Male)",
value: "fr-FR-AlainNeural",
language: "fr-FR",
},
{
label: "Brigitte (Female)",
value: "fr-FR-BrigitteNeural",
language: "fr-FR",
},
{
label: "Celeste (Female)",
value: "fr-FR-CelesteNeural",
language: "fr-FR",
},
{
label: "Claude (Male)",
value: "fr-FR-ClaudeNeural",
language: "fr-FR",
},
{
label: "Coralie (Female)",
value: "fr-FR-CoralieNeural",
language: "fr-FR",
},
{
label: "Eloise (Female, Child)",
value: "fr-FR-EloiseNeural",
language: "fr-FR",
},
{
label: "Jacqueline (Female)",
value: "fr-FR-JacquelineNeural",
language: "fr-FR",
},
{
label: "Jerome (Male)",
value: "fr-FR-JeromeNeural",
language: "fr-FR",
},
{
label: "Josephine (Female)",
value: "fr-FR-JosephineNeural",
language: "fr-FR",
},
{
label: "Maurice (Male)",
value: "fr-FR-MauriceNeural",
language: "fr-FR",
},
{ label: "Yves (Male)", value: "fr-FR-YvesNeural", language: "fr-FR" },
{
label: "Yvette (Female)",
value: "fr-FR-YvetteNeural",
language: "fr-FR",
},
{
label: "Elsa (Female)",
value: "it-IT-ElsaNeural",
language: "it-IT",
},
{
label: "Isabella (Female)",
value: "it-IT-IsabellaNeural",
language: "it-IT",
},
{
label: "Diego (Male)",
value: "it-IT-DiegoNeural",
language: "it-IT",
},
{
label: "Benigno (Male)",
value: "it-IT-BenignoNeural",
language: "it-IT",
},
{
label: "Calimero (Male)",
value: "it-IT-CalimeroNeural",
language: "it-IT",
},
{
label: "Cataldo (Male)",
value: "it-IT-CataldoNeural",
language: "it-IT",
},
{
label: "Fabiola (Female)",
value: "it-IT-FabiolaNeural",
language: "it-IT",
},
{
label: "Fiamma (Female)",
value: "it-IT-FiammaNeural",
language: "it-IT",
},
{
label: "Gianni (Male)",
value: "it-IT-GianniNeural",
language: "it-IT",
},
{
label: "Imelda (Female)",
value: "it-IT-ImeldaNeural",
language: "it-IT",
},
{
label: "Irma (Female)",
value: "it-IT-IrmaNeural",
language: "it-IT",
},
{
label: "Lisandro (Male)",
value: "it-IT-LisandroNeural",
language: "it-IT",
},
{
label: "Palmira (Female)",
value: "it-IT-PalmiraNeural",
language: "it-IT",
},
{
label: "Pierina (Female)",
value: "it-IT-PierinaNeural",
language: "it-IT",
},
{
label: "Rinaldo (Male)",
value: "it-IT-RinaldoNeural",
language: "it-IT",
},
{
label: "Giuseppe (Male)",
value: "it-IT-GiuseppeNeural",
language: "it-IT",
},
{
label: "NanamiNeural (Female)",
value: "ja-JP-NanamiNeural",
language: "ja-JP",
},
{
label: "KeitaNeural (Male)",
value: "ja-JP-KeitaNeural",
language: "ja-JP",
},
{
label: "AoiNeural (Female)",
value: "ja-JP-AoiNeural",
language: "ja-JP",
},
{
label: "DaichiNeural (Male)",
value: "ja-JP-DaichiNeural",
language: "ja-JP",
},
{
label: "MayuNeural (Female)",
value: "ja-JP-MayuNeural",
language: "ja-JP",
},
{
label: "NaokiNeural (Male)",
value: "ja-JP-NaokiNeural",
language: "ja-JP",
},
{
label: "ShioriNeural (Female)",
value: "ja-JP-ShioriNeural",
language: "ja-JP",
},
{
label: "SunHi (Female)",
value: "ko-KR-SunHiNeural",
language: "ko-KR",
},
{
label: "InJoon (Male)",
value: "ko-KR-InJoonNeural",
language: "ko-KR",
},
{
label: "BongJin (Male)",
value: "ko-KR-BongJinNeural",
language: "ko-KR",
},
{
label: "GookMin (Male)",
value: "ko-KR-GookMinNeural",
language: "ko-KR",
},
{
label: "JiMin (Female)",
value: "ko-KR-JiMinNeural",
language: "ko-KR",
},
{
label: "SeoHyeon (Female)",
value: "ko-KR-SeoHyeonNeural",
language: "ko-KR",
},
{
label: "SoonBok (Female)",
value: "ko-KR-SoonBokNeural",
language: "ko-KR",
},
{
label: "YuJin (Female)",
value: "ko-KR-YuJinNeural",
language: "ko-KR",
},
{
label: "Hyunsu (Male)",
value: "ko-KR-HyunsuNeural1",
language: "ko-KR",
},
{
label: "Xiaoxiao (Female)",
value: "zh-CN-XiaoxiaoNeural",
language: "zh-CN",
},
{
label: "Yunxi (Male)",
value: "zh-CN-YunxiNeural",
language: "zh-CN",
},
{
label: "Yunjian (Male)",
value: "zh-CN-YunjianNeural",
language: "zh-CN",
},
{
label: "Xiaoyi (Female)",
value: "zh-CN-XiaoyiNeural",
language: "zh-CN",
},
{
label: "Yunyang (Male)",
value: "zh-CN-YunyangNeural",
language: "zh-CN",
},
{
label: "Xiaochen (Female)",
value: "zh-CN-XiaochenNeural",
language: "zh-CN",
},
{
label: "Xiaohan (Female)",
value: "zh-CN-XiaohanNeural",
language: "zh-CN",
},
{
label: "Xiaomeng (Female)",
value: "zh-CN-XiaomengNeural",
language: "zh-CN",
},
{
label: "Xiaomo (Female)",
value: "zh-CN-XiaomoNeural",
language: "zh-CN",
},
{
label: "Xiaoqiu (Female)",
value: "zh-CN-XiaoqiuNeural",
language: "zh-CN",
},
{
label: "Xiaorui (Female)",
value: "zh-CN-XiaoruiNeural",
language: "zh-CN",
},
{
label: "Xiaoshuang (Female, Child)",
value: "zh-CN-XiaoshuangNeural",
language: "zh-CN",
},
{
label: "Xiaoyan (Female)",
value: "zh-CN-XiaoyanNeural",
language: "zh-CN",
},
{
label: "Xiaoyou (Female, Child)",
value: "zh-CN-XiaoyouNeural",
language: "zh-CN",
},
{
label: "Xiaozhen (Female)",
value: "zh-CN-XiaozhenNeural",
language: "zh-CN",
},
{
label: "Yunfeng (Male)",
value: "zh-CN-YunfengNeural",
language: "zh-CN",
},
{
label: "Yunhao (Male)",
value: "zh-CN-YunhaoNeural",
language: "zh-CN",
},
{
label: "Yunxia (Male)",
value: "zh-CN-YunxiaNeural",
language: "zh-CN",
},
{
label: "Yunye (Male)",
value: "zh-CN-YunyeNeural",
language: "zh-CN",
},
{
label: "Yunze (Male)",
value: "zh-CN-YunzeNeural",
language: "zh-CN",
},
{
label: "Xiaorou (Female)",
value: "zh-CN-XiaorouNeural",
language: "zh-CN",
},
{
label: "XiaoxiaoDialects (Female)",
value: "zh-CN-XiaoxiaoDialectsNeural",
language: "zh-CN",
},
{
label: "Yunjie (Male)",
value: "zh-CN-YunjieNeural",
language: "zh-CN",
},
],
},
configurable: ["model", "voice"],
},
openai: {
name: "OpenAI",