Feat: set language in conversation (#757)
* may set language in conversation * fix locale * fix warning * fix
This commit is contained in:
@@ -88,6 +88,7 @@
|
||||
"ttsEngine": "TTS engine",
|
||||
"ttsModel": "TTS model",
|
||||
"ttsVoice": "TTS voice",
|
||||
"ttsLanguage": "TTS language",
|
||||
"ttsBaseUrl": "TTS base URL",
|
||||
"ttsBaseUrlDescription": "leave it blank if you don't have one",
|
||||
"notFound": "Conversation not found",
|
||||
@@ -419,6 +420,8 @@
|
||||
"selectTtsEngine": "Select TTS engine",
|
||||
"selectTtsModel": "Select TTS model",
|
||||
"selectTtsVoice": "Select TTS voice",
|
||||
"selectTtsLanguage": "Select TTS Language",
|
||||
"pleaseSelectTtsVoice": "Please select TTS voice",
|
||||
"youNeedToSetupApiKeyBeforeUsingOpenAI": "You need to setup API key before using OpenAI",
|
||||
"ensureYouHaveOllamaRunningLocallyAndHasAtLeastOneModel": "Ensure you have Ollama running locally and has at least one model",
|
||||
"creatingSpeech": "Speech is creating",
|
||||
|
||||
@@ -87,7 +87,8 @@
|
||||
"numberOfChoicesDescription": "大于 1 时,将每次生成多版本的文本",
|
||||
"ttsEngine": "TTS 引擎",
|
||||
"ttsModel": "TTS 模型",
|
||||
"ttsVoice": "TTS 声音",
|
||||
"ttsVoice": "TTS 音色",
|
||||
"ttsLanguage": "TTS 语言",
|
||||
"ttsBaseUrl": "TTS 接口地址",
|
||||
"ttsBaseUrlDescription": "留空则使用默认值",
|
||||
"notFound": "未找到对话",
|
||||
@@ -418,7 +419,9 @@
|
||||
"selectAiModel": "选择 AI 模型",
|
||||
"selectTtsEngine": "选择 TTS 引擎",
|
||||
"selectTtsModel": "选择 TTS 模型",
|
||||
"selectTtsVoice": "选择 TTS 角色",
|
||||
"selectTtsVoice": "选择 TTS 音色",
|
||||
"selectTtsLanguage": "选择 TTS 语言",
|
||||
"pleaseSelectTtsVoice": "请选择 TTS 音色",
|
||||
"youNeedToSetupApiKeyBeforeUsingOpenAI": "在使用 OpenAI 之前您需要设置 API 密钥",
|
||||
"ensureYouHaveOllamaRunningLocallyAndHasAtLeastOneModel": "确保您已经在本地运行 Ollama 并且至少有一个模型",
|
||||
"creatingSpeech": "正在生成语音",
|
||||
|
||||
@@ -63,6 +63,11 @@ export class Conversation extends Model<Conversation> {
|
||||
return this.getDataValue("configuration").roleDefinition;
|
||||
}
|
||||
|
||||
@Column(DataType.VIRTUAL)
|
||||
get language(): string {
|
||||
return this.getDataValue("configuration").tts?.language;
|
||||
}
|
||||
|
||||
@HasMany(() => Message)
|
||||
messages: Message[];
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { MessageCircleIcon, SpeechIcon } from "lucide-react";
|
||||
import dayjs from "@renderer/lib/dayjs";
|
||||
import { useContext } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
|
||||
export const ConversationCard = (props: { conversation: ConversationType }) => {
|
||||
const { conversation } = props;
|
||||
const { learningLanguage } = useContext(AppSettingsProviderContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -21,10 +24,11 @@ export const ConversationCard = (props: { conversation: ConversationType }) => {
|
||||
<div className="">
|
||||
<div className="line-clamp-1 text-sm">{conversation.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{conversation.engine} /{" "}
|
||||
{conversation.engine} |{" "}
|
||||
{conversation.type === "tts"
|
||||
? conversation.configuration?.tts?.model
|
||||
: conversation.model}
|
||||
: conversation.model}{" "}
|
||||
| {conversation.language || learningLanguage}
|
||||
</div>
|
||||
</div>
|
||||
<span className="min-w-fit text-sm text-muted-foreground">
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "@renderer/components/ui";
|
||||
import { useContext } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { LANGUAGES } from "@/constants";
|
||||
|
||||
export const ConversationFormTTS = (props: {
|
||||
form: ReturnType<typeof useForm>;
|
||||
@@ -90,6 +91,39 @@ export const ConversationFormTTS = (props: {
|
||||
/>
|
||||
)}
|
||||
|
||||
{ttsProviders[
|
||||
form.watch("configuration.tts.engine")
|
||||
]?.configurable?.includes("language") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="configuration.tts.language"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("models.conversation.ttsLanguage")}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("selectTtsLanguage")} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{LANGUAGES.map((lang) => (
|
||||
<SelectItem key={lang.code} value={lang.code}>
|
||||
{lang.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{ttsProviders[
|
||||
form.watch("configuration.tts.engine")
|
||||
]?.configurable?.includes("voice") && (
|
||||
@@ -124,7 +158,10 @@ export const ConversationFormTTS = (props: {
|
||||
<span className="capitalize">{voice}</span>
|
||||
</SelectItem>
|
||||
);
|
||||
} else if (voice.language === learningLanguage) {
|
||||
} else if (
|
||||
voice.language ===
|
||||
form.watch("configuration.tts.language")
|
||||
) {
|
||||
return (
|
||||
<SelectItem key={voice.value} value={voice.value}>
|
||||
<span className="capitalize">{voice.label}</span>
|
||||
|
||||
@@ -43,31 +43,6 @@ import {
|
||||
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;
|
||||
@@ -76,10 +51,38 @@ export const ConversationForm = (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 { EnjoyApp, webApi, learningLanguage } = useContext(
|
||||
AppSettingsProviderContext
|
||||
);
|
||||
const { openai } = useContext(AISettingsProviderContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
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({
|
||||
language: z.string().default(learningLanguage).optional(),
|
||||
engine: z.enum(["openai", "enjoyai"]).default("enjoyai"),
|
||||
model: z.string().default("openai/tts-1"),
|
||||
voice: z.string(),
|
||||
baseUrl: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const refreshGptProviders = async () => {
|
||||
let providers = GPT_PROVIDERS;
|
||||
|
||||
@@ -148,6 +151,14 @@ export const ConversationForm = (props: {
|
||||
}
|
||||
}
|
||||
|
||||
if (!defaultConfig.configuration.tts) {
|
||||
defaultConfig.configuration.tts = {};
|
||||
}
|
||||
|
||||
if (!defaultConfig.configuration.tts.language) {
|
||||
defaultConfig.configuration.tts.language = learningLanguage;
|
||||
}
|
||||
|
||||
const form = useForm<z.infer<typeof conversationFormSchema>>({
|
||||
resolver: zodResolver(conversationFormSchema),
|
||||
// @ts-ignore
|
||||
@@ -245,6 +256,34 @@ export const ConversationForm = (props: {
|
||||
configuration.tts.baseUrl = gptProviders[engine]?.baseUrl;
|
||||
}
|
||||
|
||||
// validates tts voice
|
||||
const ttsEngine = configuration.tts.engine;
|
||||
const voice = configuration.tts.voice;
|
||||
const language = configuration.tts.language;
|
||||
if (!language) {
|
||||
configuration.tts.language === learningLanguage;
|
||||
}
|
||||
if (ttsEngine === "openai") {
|
||||
const options = ttsProviders["openai"].voices;
|
||||
if (!options.includes(voice)) {
|
||||
throw new Error(t("pleaseSelectTtsVoice"));
|
||||
}
|
||||
}
|
||||
if (ttsEngine === "enjoyai") {
|
||||
const model = configuration.tts.model.split("/")[0];
|
||||
const options = ttsProviders.enjoyai.voices[model];
|
||||
if (model === "openai" && !options.includes(voice)) {
|
||||
throw new Error(t("pleaseSelectTtsVoice"));
|
||||
} else if (
|
||||
model === "azure" &&
|
||||
options.findIndex(
|
||||
(o: any) => o.language === language && o.value === voice
|
||||
) < 0
|
||||
) {
|
||||
throw new Error(t("pleaseSelectTtsVoice"));
|
||||
}
|
||||
}
|
||||
|
||||
return configuration;
|
||||
};
|
||||
|
||||
|
||||
@@ -671,13 +671,13 @@ export const TTS_PROVIDERS: { [key: string]: any } = {
|
||||
},
|
||||
],
|
||||
},
|
||||
configurable: ["model", "voice"],
|
||||
configurable: ["model", "language", "voice"],
|
||||
},
|
||||
openai: {
|
||||
name: "OpenAI",
|
||||
description: t("youNeedToSetupApiKeyBeforeUsingOpenAI"),
|
||||
models: ["tts-1", "tts-1-hd"],
|
||||
voices: ["alloy", "echo", "fable", "onyx", "nova", "shimmer"],
|
||||
configurable: ["model", "voice", "baseUrl"],
|
||||
configurable: ["model", "language", "voice", "baseUrl"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
SheetContent,
|
||||
SheetTrigger,
|
||||
toast,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@renderer/components/ui";
|
||||
import { MessageComponent, ConversationForm } from "@renderer/components";
|
||||
import { SendIcon, BotIcon, LoaderIcon, SettingsIcon } from "lucide-react";
|
||||
@@ -248,6 +250,11 @@ export default () => {
|
||||
</SheetTrigger>
|
||||
|
||||
<SheetContent className="p-0">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="sr-only">
|
||||
{t("editConversation")}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="h-screen">
|
||||
<ConversationForm
|
||||
conversation={conversation}
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
SheetContent,
|
||||
ScrollArea,
|
||||
toast,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@renderer/components/ui";
|
||||
import {
|
||||
ConversationCard,
|
||||
@@ -300,7 +302,12 @@ export default () => {
|
||||
|
||||
<Sheet open={creating} onOpenChange={(value) => setCreating(value)}>
|
||||
<SheetContent className="p-0">
|
||||
<div className="h-screen">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="sr-only">
|
||||
{t("startConversation")}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="h-screen relative">
|
||||
<ConversationForm
|
||||
conversation={preset}
|
||||
onFinish={() => setCreating(false)}
|
||||
|
||||
1
enjoy/src/types/conversation.d.ts
vendored
1
enjoy/src/types/conversation.d.ts
vendored
@@ -5,6 +5,7 @@ type ConversationType = {
|
||||
name: string;
|
||||
configuration: { [key: string]: any };
|
||||
model: string;
|
||||
language?: string;
|
||||
messages?: MessageType[];
|
||||
createdAt?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user