Feat: set language in conversation (#757)

* may set language in conversation

* fix locale

* fix warning

* fix
This commit is contained in:
an-lee
2024-07-02 11:23:11 +08:00
committed by GitHub
parent 41932fb253
commit d9534bcae8
10 changed files with 140 additions and 34 deletions

View File

@@ -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",

View File

@@ -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": "正在生成语音",

View File

@@ -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[];

View File

@@ -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">

View File

@@ -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>

View File

@@ -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;
};

View File

@@ -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"],
},
};

View File

@@ -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}

View File

@@ -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)}

View File

@@ -5,6 +5,7 @@ type ConversationType = {
name: string;
configuration: { [key: string]: any };
model: string;
language?: string;
messages?: MessageType[];
createdAt?: string;
};