Feat: recorder settings (#1004)

* add recorder settings

* update locales

* add description
This commit is contained in:
an-lee
2024-08-21 16:30:42 +08:00
committed by GitHub
parent fc2db81a7c
commit 9cfd058671
9 changed files with 226 additions and 5 deletions

View File

@@ -739,5 +739,8 @@
"logs": "Contains some logs helpful for debugging.",
"cache": "Contains cached files. They will be cleaned up automatically."
},
"recordingIsTooLongToAssess": "Recording is too long to assess. The maximum duration is 60 seconds."
"recordingIsTooLongToAssess": "Recording is too long to assess. The maximum duration is 60 seconds.",
"recorderConfig": "Recorder config",
"recorderConfigSaved": "Recorder config saved",
"recorderConfigDescription": "Advanced settings for recorder"
}

View File

@@ -739,5 +739,8 @@
"logs": "日志文件,帮助开发者调试问题",
"cache": "缓存文件,会自动清理。"
},
"recordingIsTooLongToAssess": "录音时长过长,无法评估。最长支持 1 分钟。"
"recordingIsTooLongToAssess": "录音时长过长,无法评估。最长支持 1 分钟。",
"recorderConfig": "录音设置",
"recorderConfigSaved": "录音设置已保存",
"recorderConfigDescription": "调整录音高级设置"
}

View File

@@ -182,7 +182,7 @@ export default {
return settings.setSync("defaultHotkeys", records);
});
ipcMain.handle("settings-get-api-url", (_event, url) => {
ipcMain.handle("settings-get-api-url", (_event) => {
return settings.getSync("apiUrl");
});

View File

@@ -29,3 +29,5 @@ export * from "./proxy-settings";
export * from "./whisper-model-options";
export * from "./network-state";
export * from "./recorder-settings";

View File

@@ -17,6 +17,7 @@ import {
NativeLanguageSettings,
LearningLanguageSettings,
NetworkState,
RecorderSettings,
} from "@renderer/components";
import { useState } from "react";
import { Tooltip } from "react-tooltip";
@@ -59,6 +60,8 @@ export const Preferences = () => {
<Separator />
<NetworkState />
<Separator />
<RecorderSettings />
<Separator />
<ResetSettings />
<Separator />
<ResetAllSettings />

View File

@@ -0,0 +1,172 @@
import { t } from "i18next";
import {
Button,
Form,
FormControl,
FormField,
FormItem,
FormLabel,
Input,
Switch,
toast,
} from "@renderer/components/ui";
import { AppSettingsProviderContext } from "@renderer/context";
import { useContext, useState } from "react";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
export const RecorderSettings = () => {
const [editing, setEditing] = useState(false);
const { recorderConfig, setRecorderConfig } = useContext(
AppSettingsProviderContext
);
const recorderConfigSchema = z.object({
autoGainControl: z.boolean(),
echoCancellation: z.boolean(),
noiseSuppression: z.boolean(),
sampleRate: z.number(),
sampleSize: z.number(),
});
const form = useForm<z.infer<typeof recorderConfigSchema>>({
resolver: zodResolver(recorderConfigSchema),
values: recorderConfig,
});
const onSubmit = async (data: z.infer<typeof recorderConfigSchema>) => {
setRecorderConfig({
...recorderConfig,
...data,
})
.then(() => toast.success(t("recorderConfigSaved")))
.finally(() => setEditing(false));
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="flex items-start justify-between py-4">
<div className="">
<div className="mb-4">{t("recorderConfig")}</div>
<div className="text-sm text-muted-foreground mb-3">
{t("recorderConfigDescription")}
</div>
<div
className={`text-sm text-muted-foreground space-y-3 ${
editing ? "" : "hidden"
}`}
>
<FormField
control={form.control}
name="autoGainControl"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<FormLabel className="min-w-max">
autoGainControl
</FormLabel>
<FormControl>
<Switch
disabled={!editing}
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="echoCancellation"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<FormLabel className="min-w-max">
echoCancellation
</FormLabel>
<FormControl>
<Switch
disabled={!editing}
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="noiseSuppression"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<FormLabel className="min-w-max">
noiseSuppression
</FormLabel>
<FormControl>
<Switch
disabled={!editing}
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="sampleRate"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<FormLabel className="min-w-max">sampleRate</FormLabel>
<FormControl>
<Input disabled={!editing} {...field} />
</FormControl>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="sampleSize"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<FormLabel className="min-w-max">sampleSize</FormLabel>
<FormControl>
<Input disabled={!editing} {...field} />
</FormControl>
</div>
</FormItem>
)}
/>
</div>
</div>
<div className="flex items-center space-x-2">
<Button
variant={editing ? "outline" : "secondary"}
size="sm"
type="reset"
onClick={(event) => {
event.preventDefault();
form.reset();
setEditing(!editing);
}}
>
{editing ? t("cancel") : t("edit")}
</Button>
<Button className={editing ? "" : "hidden"} size="sm" type="submit">
{t("save")}
</Button>
</div>
</div>
</form>
</Form>
);
};

View File

@@ -29,6 +29,8 @@ type AppSettingsProviderState = {
setProxy?: (config: ProxyConfigType) => Promise<void>;
cable?: Consumer;
ahoy?: typeof ahoy;
recorderConfig?: RecorderConfigType;
setRecorderConfig?: (config: RecorderConfigType) => Promise<void>;
// remote config
ipaMappings?: { [key: string]: string };
};
@@ -58,6 +60,7 @@ export const AppSettingsProvider = ({
const [learningLanguage, setLearningLanguage] = useState<string>("en-US");
const [proxy, setProxy] = useState<ProxyConfigType>();
const EnjoyApp = window.__ENJOY_APP__;
const [recorderConfig, setRecorderConfig] = useState<RecorderConfigType>();
const [ipaMappings, setIpaMappings] = useState<{ [key: string]: string }>(
IPA_MAPPINGS
);
@@ -177,6 +180,28 @@ export const AppSettingsProvider = ({
setCable(consumer);
};
const fetchRecorderConfig = async () => {
const config = await EnjoyApp.settings.get("recorderConfig");
if (config) {
setRecorderConfig(config);
} else {
const defaultConfig: RecorderConfigType = {
autoGainControl: true,
echoCancellation: true,
noiseSuppression: true,
sampleRate: 16000,
sampleSize: 16,
};
setRecorderConfigHandler(defaultConfig);
}
};
const setRecorderConfigHandler = async (config: RecorderConfigType) => {
return EnjoyApp.settings.set("recorderConfig", config).then(() => {
setRecorderConfig(config);
});
};
useEffect(() => {
fetchVersion();
fetchUser();
@@ -184,6 +209,7 @@ export const AppSettingsProvider = ({
fetchLanguages();
fetchProxyConfig();
initSentry();
fetchRecorderConfig();
}, []);
useEffect(() => {
@@ -238,6 +264,8 @@ export const AppSettingsProvider = ({
initialized: Boolean(user && libraryPath),
ahoy,
cable,
recorderConfig,
setRecorderConfig: setRecorderConfigHandler,
ipaMappings,
}}
>

View File

@@ -138,7 +138,7 @@ export const MediaPlayerProvider = ({
children: React.ReactNode;
}) => {
const minPxPerSec = 150;
const { EnjoyApp, webApi, learningLanguage } = useContext(
const { EnjoyApp, learningLanguage, recorderConfig } = useContext(
AppSettingsProviderContext
);
@@ -207,7 +207,9 @@ export const MediaPlayerProvider = ({
isPaused,
recordingTime,
mediaRecorder,
} = useAudioRecorder();
} = useAudioRecorder(recorderConfig, (exception) => {
toast.error(exception.message);
});
const { segment, createSegment } = useSegments({
targetId: media?.id,

View File

@@ -189,3 +189,11 @@ type DiskUsageType = {
path: string;
size: number;
}[];
type RecorderConfigType = {
autoGainControl: boolean;
echoCancellation: boolean;
noiseSuppression: boolean;
sampleRate: number;
sampleSize: number;
};