Merge branch 'main' into sounds-of-american-english-2
@@ -25,9 +25,26 @@ Enjoy 支持添加本地的音频资源,和在线资源。在音频页面,
|
||||
如果第二步语音转文本失败,请检查是否在正在使用本地 whisper 组件进行语音转文本,在某些电脑上可能因为兼容性和未知问题导致无法使用。如果出现这种情况,请在 [语音转文本服务设置](./settings#语音转文本服务) 中改用其他语音转文本的云服务。
|
||||
:::
|
||||
|
||||
## 播放
|
||||
|
||||
点击播放键(或者快捷键 <kbd>Space</kbd>)即可播放或者暂停音频。
|
||||
|
||||
Enjoy 会将音频按照句子切分,默认播放模式为“播放单句”,以便逐句反复听练。
|
||||
|
||||
其他可选的播放模式有:
|
||||
|
||||
- 单句循环
|
||||
- 播放所有
|
||||
|
||||
## 智能断句
|
||||
|
||||
Enjoy 按照原音的停顿和标点符号,将当前句子分成几个断句,以便逐一反复练习。
|
||||
|
||||
你也可以通过点击当前句子的任意词语(或同时按下 <kbd>Shift</kbd> 进行多选)进行选中单词或者短语,进行听练。
|
||||
|
||||
## 录音
|
||||
|
||||
Enjoy 会将音频按照句子切分,用户以句子为单位进行跟读练习。在激活的音频句子下,点击下方的录音按钮,即可开始录音,用户可以模仿音频朗读当前句子作为练习。
|
||||
Enjoy 会将音频按照句子切分,用户以句子为单位进行跟读练习。在激活的音频句子下,点击红色的录音按钮(或者快捷键 <kbd>r</kbd>),即可开始录音,用户可以模仿音频朗读当前句子作为练习。
|
||||
|
||||

|
||||
_\* 音频播放页面_
|
||||
@@ -36,9 +53,16 @@ _\* 音频播放页面_
|
||||
在 Mac 电脑上,首次使用录音功能时,会弹窗请求麦克风的使用权限,请务必点允许,否则无法使用录音功能。
|
||||
:::
|
||||
|
||||
## 录音对比
|
||||
|
||||
将录音的 Pitch contour 与原音对比,以便自纠发音。对比状态下,按下播放键,会同时播放录音和原音。
|
||||
|
||||

|
||||
_\* 录音于原音对比_
|
||||
|
||||
## 发音评估
|
||||
|
||||
Enjoy 集成了微软 Azure 的发音评估功能,作为自我的发音检查参考。点击录音右下方的仪表盘图标,即可打开发音评估弹窗。
|
||||
Enjoy 集成了微软 Azure 的发音评估功能,作为自我的发音检查参考。
|
||||
|
||||
该功能会以**录音时的句子文本作为参考**,评估录音的发音情况,各指标的详细说明可以参考微软的 [官方文档](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-pronunciation-assessment?pivots=programming-language-javascript#scripted-assessment-results)。
|
||||
|
||||
@@ -48,9 +72,7 @@ _\* 发音评估示例_
|
||||
::: warning 发音评估的使用建议
|
||||
该功能是收费功能,每次使用均会在 Enjoy 账户的余额扣费,如果余额不足则需要 [充值](./settings#充值) 后才可继续使用。
|
||||
|
||||
根据实测,该功能对提高学习者的发音作用水平有限,其评估结果仅做参考,不应该过度依赖。
|
||||
|
||||
另外,我们正在开发一套更有效的发音评估体系,敬请期待。
|
||||
值得注意的是,发音评估更侧重于单词的发音是否正确,无法评估音调变化是否正确。
|
||||
:::
|
||||
|
||||
## 修改音频信息
|
||||
|
||||
23
1000-hours/enjoy-app/faq.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# 常见问题
|
||||
|
||||
## 为什么本地语音转文本服务无法使用?
|
||||
|
||||
Enjoy 集成了 [whipser.cpp](https://github.com/ggerganov/whisper.cpp) 作为本地的语音转文本(STT)服务,但是由于兼容性的问题,某些配置较低或者操作系统版本较低的电脑无法使用。
|
||||
|
||||
如果您遇到这种情况,Enjoy 提供了其他 STT 的云服务,可以前往 [软件设置](./settings#语音转文本服务) 进行配置。推荐优先使用 Azure AI。
|
||||
|
||||
## 403 Insufficient balance
|
||||
|
||||
遇到这个报错,说明您正在使用 Enjoy 的付费功能,但是账户余额不足了。
|
||||
|
||||
Enjoy 内有很多功能都由 AI 驱动,比如[智能助手](./ai-assistant)、智能翻译、句子分析等。如果您在 [软件设置](./settings#默认-ai-引擎) 中的配置了 `OpenAI` 作为默认 AI 引擎,在使用这些功能时候,会使用您配置的 OpenAI 信息进行实现,不会涉及 Enjoy 的扣费。
|
||||
|
||||
(需要注意的是,[智能助手](./ai-assistant) 的对话一旦创建,AI 引擎无法修改。如果需要切换,比如由 Enjoy AI 换成 Open AI,则需要新建一个对话)
|
||||
|
||||
另外,[发音评估](./audios#发音评估) 是收费功能,并非 OpenAI 提供,所以无论 [默认 AI 引擎](./settings#默认-ai-引擎) 选了什么,使用发音评估时,总是会在 Enjoy 账户中扣费。
|
||||
|
||||
如果需要充值,请参考[充值](./settings#充值)。
|
||||
|
||||
## 如何下载音频、录音
|
||||
|
||||
Enjoy 提供了音频、视频、录音的下载功能,以便可以在其他设备使用。
|
||||
@@ -4,13 +4,13 @@ Enjoy App 是一个跨平台的桌面应用,可以在 Windows、Mac 和 Linux
|
||||
|
||||
请根据电脑设备的操作系统,下载相应的版本安装使用。
|
||||
|
||||
当前最新版本:[v0.1.0-alpha.13](https://github.com/xiaolai/everyone-can-use-english/releases/latest)
|
||||
当前最新版本:[v0.2.0](https://github.com/xiaolai/everyone-can-use-english/releases/latest)
|
||||
|
||||
## Windows
|
||||
|
||||
支持 Windows 10 以上版本。
|
||||
|
||||
[点击下载](https://github.com/xiaolai/everyone-can-use-english/releases/download/v0.1.0-alpha.13/Enjoy-0.1.0-alpha.13-Setup.exe)
|
||||
[点击下载](https://github.com/xiaolai/everyone-can-use-english/releases/download/v0.2.0/Enjoy-0.2.0.Setup.exe)
|
||||
|
||||
下载后,双击即可安装。
|
||||
|
||||
@@ -18,8 +18,8 @@ Enjoy App 是一个跨平台的桌面应用,可以在 Windows、Mac 和 Linux
|
||||
|
||||
根据使用 Mac 电脑的芯片不同,需要下载不同的版本。
|
||||
|
||||
- [Silicon 芯片版本(arm64)](https://github.com/xiaolai/everyone-can-use-english/releases/download/v0.1.0-alpha.13/Enjoy-0.1.0-alpha.13-arm64.dmg)
|
||||
- [Intel 芯片版本(x64)](https://github.com/xiaolai/everyone-can-use-english/releases/download/v0.1.0-alpha.13/Enjoy-0.1.0-alpha.13-x64.dmg)
|
||||
- [Silicon 芯片版本(arm64)](https://github.com/xiaolai/everyone-can-use-english/releases/download/v0.2.0/Enjoy-0.2.0-arm64.dmg)
|
||||
- [Intel 芯片版本(x64)](https://github.com/xiaolai/everyone-can-use-english/releases/download/v0.2.0/Enjoy-0.2.0-x64.dmg)
|
||||
|
||||
::: info 如何查看本机配置
|
||||
M1 以后的 Mac 电脑型号(M1、M2、M3),均为 Silicon 芯片。
|
||||
@@ -37,8 +37,8 @@ M1 以后的 Mac 电脑型号(M1、M2、M3),均为 Silicon 芯片。
|
||||
|
||||
请根据不同发行版本选用安装文件。
|
||||
|
||||
- [下载 deb 版本](https://github.com/xiaolai/everyone-can-use-english/releases/download/v0.1.0-alpha.13/enjoy_0.1.0-alpha.13_amd64.deb)
|
||||
- [下载 rpm 版本](https://github.com/xiaolai/everyone-can-use-english/releases/download/v0.1.0-alpha.13/enjoy-0.1.0.alpha.13-1.x86_64.rpm)
|
||||
- [下载 deb 版本](https://github.com/xiaolai/everyone-can-use-english/releases/download/v0.2.0/enjoy_0.2.0_amd64.deb)
|
||||
- [下载 rpm 版本](https://github.com/xiaolai/everyone-can-use-english/releases/download/v0.2.0/Enjoy-linux-x64-0.2.0.zip)
|
||||
|
||||
## 历史版本
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 利用 AI 生成训练材料
|
||||
# 使用案例:利用 AI 生成训练材料
|
||||
|
||||
用外语说我们自己想说的话是学习外语的其中一个重要目的。市面上的口语书实际上并不实用,因为那些话大都不是我们想要说的,我们需要创建专属自己的口语书。使用 Enjoy 可以很容易做到这一点。
|
||||
|
||||
|
||||
@@ -5,10 +5,6 @@
|
||||

|
||||
_\* 视频播放页面_
|
||||
|
||||
## 录音
|
||||
|
||||
在视频播放页面,需要录音时,可以点播放控制栏上的录音图标,视频将会被缩小至小窗,主界面上会显示录音按钮。
|
||||
|
||||
## 视频大小的限制
|
||||
|
||||
过大的视频文件会导致加载卡死而无法使用,目前 Enjoy 将添加视频的大小限制在 100 Mb,超过则会提示添加失败。
|
||||
|
||||
|
Before Width: | Height: | Size: 425 KiB After Width: | Height: | Size: 443 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 238 KiB |
BIN
1000-hours/public/images/enjoy/recording-comparing.png
Normal file
|
After Width: | Height: | Size: 402 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 669 KiB |
@@ -1,6 +1,6 @@
|
||||
# 2. 训练方法
|
||||
|
||||
请后继反复认真阅读《[语音塑造](/sounds-of-english/01-basics)》中的每一个章节。
|
||||
请后续反复认真阅读《[语音塑造](/sounds-of-english/01-basics)》中的每一个章节。
|
||||
|
||||
## 2.1 搞清每个单词的读音
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
自然语音中有**可换气停顿**的地方,就相当于是乐谱里的小节分界线。分段练习,实在难的小节,还可以继续拆分……
|
||||
|
||||
在跟读的时候,有必要**夸张**一点。声音也要正常地**大** —— 确实不用喊,嗓子的确需要保护。实际上,我们主要需要练的是嘴唇、舌头、气流震动的配合,主要练的还真不是声带。
|
||||
在跟读的时候,有必要**夸张**一点。声音也要正常地**大** —— 确实不用喊,嗓子的确需要保护。实际上,我们主要需要练的是嘴唇、舌头、气流振动的配合,主要练的还真不是声带。
|
||||
|
||||
跟读的时候,可以戴着耳机。一个比较好的方法是戴单只耳机 —— 这样,一方面录音听得更清楚,另外一方面也不妨碍听清自己的声音,还可以换着耳朵戴。
|
||||
|
||||
@@ -119,4 +119,4 @@ TED 上有一个讲座,Benjamin Zander 讲 [The transformative power of classi
|
||||
|
||||
一天至少三个小时,每周最多可以中止一天。
|
||||
|
||||
每天结束之前,要花 5~10 分钟做一下**复盘**,回忆并记录一下自己今天遇到的困难、已经克服的困难、尚未克服的困难、以及面对那些困难时所采用的方法…… 想一想还有什么方法可以试试?这些都要写下来。相信我,记录这个东西 ,总是会以想象不到的方式起想象不到的作用。
|
||||
每天结束之前,要花 5~10 分钟做一下**复盘**,回忆并记录一下自己今天遇到的困难、已经克服的困难、尚未克服的困难、以及面对那些困难时所采用的方法…… 想一想还有什么方法可以试试?这些都要写下来。相信我,记录这个东西 ,总是会以想象不到的方式起想象不到的作用。
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
* [GRE的Issue题目](files/GRE-Analytical-Writing-Argument-Task-Topics.md)
|
||||
* [GRE的Argument题目](files/GRE-Analytical-Writing-Issue-Task-Topics)
|
||||
|
||||
在相当长一段时间里,我以为自己 “发现” 了一条真正的捷径,后来才发现 “我并不孤独”。在国内,卖得最好的口语教材,实际上是北京外国语大学的专业教材,作者是北外教授吴桢福老师:一共三本,《英语初级口语》、《英语中级口语》、《英语高级口语》,多年来多次再版,印数均超过 100 万本。如果读者有机会,不妨去书店翻翻这套教材──你会发现其中的大多数课文话题,实际上也许都是从 TOEFL 作文题脱胎而来的。
|
||||
在相当长一段时间里,我以为自己 “发现” 了一条真正的捷径,后来才发现 “我并不孤独”。在国内,卖得最好的口语教材,实际上是北京外国语大学的专业教材,作者是北外教授吴祯福老师:一共三本,《英语初级口语》、《英语中级口语》、《英语高级口语》,多年来多次再版,印数均超过 100 万本。如果读者有机会,不妨去书店翻翻这套教材──你会发现其中的大多数课文话题,实际上也许都是从 TOEFL 作文题脱胎而来的。
|
||||
|
||||
请允许我重复一遍:**你的问题也许不在于你不会说,而在于你没什么话可说。**
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"name": "enjoy",
|
||||
"productName": "Enjoy",
|
||||
"version": "0.2.0-preview",
|
||||
"version": "0.2.0",
|
||||
"description": "Enjoy desktop app",
|
||||
"main": ".vite/build/main.js",
|
||||
"types": "./src/types.d.ts",
|
||||
@@ -110,8 +110,11 @@
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@sentry/electron": "^4.21.0",
|
||||
"@types/ahoy.js": "^0.4.2",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@vidstack/react": "^1.10.9",
|
||||
"ahoy.js": "^0.4.3",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.6.8",
|
||||
"camelcase": "^8.0.0",
|
||||
|
||||
@@ -22,32 +22,26 @@ export const analyzeCommand = async (
|
||||
},
|
||||
cache: false,
|
||||
verbose: true,
|
||||
maxRetries: 2,
|
||||
});
|
||||
|
||||
const prompt = ChatPromptTemplate.fromMessages([
|
||||
["system", SYSTEM_PROMPT],
|
||||
["human", TRANSLATION_PROMPT],
|
||||
["human", text],
|
||||
]);
|
||||
|
||||
const response = await prompt.pipe(chatModel).invoke({
|
||||
native_language: "Chinese",
|
||||
text,
|
||||
});
|
||||
const response = await prompt.pipe(chatModel).invoke({});
|
||||
|
||||
return response.text;
|
||||
};
|
||||
|
||||
const SYSTEM_PROMPT = `You are a language coach of English, and you are helping a student to learn {native_language}.`;
|
||||
const TRANSLATION_PROMPT = `
|
||||
{text}
|
||||
const SYSTEM_PROMPT = `你是我的英语教练,我将提供英语文本,你将帮助我分析文本的句子结构、语法和词汇/短语,并对文本进行详细解释。请用中文回答,并按以下格式返回结果:
|
||||
|
||||
Please analyze the text above, including sentence structure, grammar, and vocabulary/phrases, and provide a detailed explanation of the text. Please reply in {native_language} and return the result only in the following format:
|
||||
### 句子结构
|
||||
(解释句子的每个元素)
|
||||
|
||||
### Sentence structure
|
||||
(explain every element of the sentence)
|
||||
### 语法
|
||||
(解释句子的语法)
|
||||
|
||||
### Grammar
|
||||
(explain the grammar of the sentence)
|
||||
|
||||
### Vocabulary/phrases
|
||||
(explain the key vocabulary and phrases used)`;
|
||||
### 词汇/短语
|
||||
(解释使用的关键词汇和短语)`;
|
||||
|
||||
@@ -39,6 +39,7 @@ export const extractStoryCommand = async (
|
||||
},
|
||||
cache: true,
|
||||
verbose: true,
|
||||
maxRetries: 2,
|
||||
}).bind({
|
||||
tools: [
|
||||
{
|
||||
|
||||
@@ -59,6 +59,7 @@ export const ipaCommand = async (
|
||||
},
|
||||
cache: true,
|
||||
verbose: true,
|
||||
maxRetries: 2,
|
||||
});
|
||||
|
||||
const prompt = ChatPromptTemplate.fromMessages([
|
||||
|
||||
@@ -71,6 +71,7 @@ export const lookupCommand = async (
|
||||
},
|
||||
cache: true,
|
||||
verbose: true,
|
||||
maxRetries: 2,
|
||||
});
|
||||
|
||||
const prompt = ChatPromptTemplate.fromMessages([
|
||||
|
||||
@@ -22,6 +22,7 @@ export const translateCommand = async (
|
||||
},
|
||||
cache: false,
|
||||
verbose: true,
|
||||
maxRetries: 2,
|
||||
});
|
||||
|
||||
const prompt = ChatPromptTemplate.fromMessages([
|
||||
|
||||
@@ -7,6 +7,8 @@ export const WEB_API_URL = "https://enjoy-web.fly.dev";
|
||||
|
||||
export const REPO_URL = "https://github.com/xiaolai/everyone-can-use-english";
|
||||
|
||||
export const SENTRY_DSN = "https://d51056d7af7d14eae446c0c15b4f3d31@o1168905.ingest.us.sentry.io/4506969353289728"
|
||||
|
||||
export const MAGIC_TOKEN_REGEX =
|
||||
/\b(Mrs|Ms|Mr|Dr|Prof|St|[a-zA-Z]{1,2}|\d{1,2})\.\b/g;
|
||||
export const END_OF_SENTENCE_REGEX = /[^\.!,\?][\.!\?]/g;
|
||||
|
||||
@@ -6,6 +6,12 @@ import mainWindow from "@main/window";
|
||||
import ElectronSquirrelStartup from "electron-squirrel-startup";
|
||||
import contextMenu from "electron-context-menu";
|
||||
import { t } from "i18next";
|
||||
import * as Sentry from "@sentry/electron";
|
||||
import { SENTRY_DSN } from "@/constants";
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
});
|
||||
|
||||
app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer");
|
||||
|
||||
|
||||
@@ -28,6 +28,12 @@
|
||||
|
||||
import "./index.css";
|
||||
import "./renderer/index";
|
||||
import * as Sentry from "@sentry/electron";
|
||||
import { SENTRY_DSN } from "@/constants";
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
||||
@@ -348,7 +348,7 @@ const TranslationTabContent = (props: { text: string }) => {
|
||||
setTranslation(result);
|
||||
}
|
||||
})
|
||||
.catch((err) => t("translationFailed", { error: err.message }))
|
||||
.catch((err) => toast.error(err.message))
|
||||
.finally(() => {
|
||||
setTranslating(false);
|
||||
});
|
||||
@@ -422,7 +422,7 @@ const AnalysisTabContent = (props: { text: string }) => {
|
||||
setAnalysisResult(result);
|
||||
}
|
||||
})
|
||||
.catch((err) => t("analysisFailed", { error: err.message }))
|
||||
.catch((err) => toast.error(err.message))
|
||||
.finally(() => {
|
||||
setAnalyzing(false);
|
||||
});
|
||||
|
||||
@@ -222,7 +222,7 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
|
||||
|
||||
const calContainerWidth = () => {
|
||||
const w = document
|
||||
.querySelector(".media-recording-container")
|
||||
.querySelector(".media-recording-wrapper")
|
||||
?.getBoundingClientRect()?.width;
|
||||
if (!w) return;
|
||||
|
||||
@@ -371,10 +371,15 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref?.current) return;
|
||||
if (!width) return;
|
||||
|
||||
ref.current.style.width = `${width}px`;
|
||||
}, [width]);
|
||||
const container: HTMLDivElement = document.querySelector(
|
||||
".media-recording-container"
|
||||
);
|
||||
if (!container) return;
|
||||
|
||||
container.style.width = `${width}px`;
|
||||
}, [width, currentRecording, isRecording]);
|
||||
|
||||
useEffect(() => {
|
||||
calContainerWidth();
|
||||
@@ -387,7 +392,7 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
|
||||
calContainerWidth();
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
}, [currentRecording, isRecording]);
|
||||
|
||||
useHotkeys(
|
||||
["Ctrl+R", "Meta+R"],
|
||||
@@ -428,9 +433,9 @@ export const MediaCurrentRecording = (props: { height?: number }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex space-x-4 media-recording-container">
|
||||
<div className="border rounded-xl shadow-lg flex-1 relative">
|
||||
<div ref={ref}></div>
|
||||
<div className="flex space-x-4 media-recording-wrapper">
|
||||
<div className="border rounded-xl shadow-lg flex-1 relative media-recording-container">
|
||||
<div className="w-full" ref={ref}></div>
|
||||
|
||||
<div className="absolute right-2 top-1">
|
||||
<span className="text-sm">{formatDuration(currentTime || 0)}</span>
|
||||
|
||||
@@ -87,7 +87,7 @@ export const MediaPlayer = () => {
|
||||
|
||||
const calContainerWidth = () => {
|
||||
const w = document
|
||||
.querySelector(".media-player-container")
|
||||
.querySelector(".media-player-wrapper")
|
||||
?.getBoundingClientRect()?.width;
|
||||
if (!w) return;
|
||||
|
||||
@@ -122,7 +122,10 @@ export const MediaPlayer = () => {
|
||||
}, [ref]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref?.current) return;
|
||||
const container: HTMLDivElement = document.querySelector(
|
||||
".media-player-container"
|
||||
);
|
||||
if (!container) return;
|
||||
|
||||
ref.current.style.width = `${width}px`;
|
||||
}, [width]);
|
||||
@@ -141,11 +144,11 @@ export const MediaPlayer = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="media-player-container"
|
||||
className="flex space-x-4 media-player-container"
|
||||
>
|
||||
<div className="flex-1 border rounded-xl shadow-lg relative">
|
||||
<div className="flex space-x-4 media-player-wrapper">
|
||||
<div
|
||||
data-testid="media-player-container"
|
||||
className="flex-1 border rounded-xl shadow-lg relative media-player-container"
|
||||
>
|
||||
<div ref={ref} />
|
||||
<div className="absolute right-2 top-1">
|
||||
<span className="text-sm">{formatDuration(currentTime || 0)}</span>
|
||||
|
||||
@@ -109,10 +109,10 @@ export const MediaRecorder = (props: { height?: number }) => {
|
||||
setDuration(0);
|
||||
interval = setInterval(() => {
|
||||
setDuration((duration) => {
|
||||
if (duration >= 300) {
|
||||
if (duration >= 3000) {
|
||||
setIsRecording(false);
|
||||
}
|
||||
return duration + 1;
|
||||
return duration + 0.1;
|
||||
});
|
||||
}, 100);
|
||||
} else {
|
||||
@@ -135,7 +135,7 @@ export const MediaRecorder = (props: { height?: number }) => {
|
||||
<div className="h-full w-full flex items-center space-x-4">
|
||||
<div className="flex-1 h-full border rounded-xl shadow-lg relative">
|
||||
<span className="absolute bottom-2 right-2 serif">
|
||||
{duration / 10}
|
||||
{duration.toFixed(1)}
|
||||
<span className="text-xs"> / 300</span>
|
||||
</span>
|
||||
<div className="h-full" ref={ref}></div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Client } from "@/api";
|
||||
import i18n from "@renderer/i18n";
|
||||
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
||||
import { toBlobURL } from "@ffmpeg/util";
|
||||
import ahoy from "ahoy.js";
|
||||
|
||||
type AppSettingsProviderState = {
|
||||
webApi: Client;
|
||||
@@ -23,6 +24,7 @@ type AppSettingsProviderState = {
|
||||
switchLanguage?: (language: "en" | "zh-CN") => void;
|
||||
proxy?: ProxyConfigType;
|
||||
setProxy?: (config: ProxyConfigType) => Promise<void>;
|
||||
ahoy?: typeof ahoy;
|
||||
};
|
||||
|
||||
const initialState: AppSettingsProviderState = {
|
||||
@@ -71,6 +73,13 @@ export const AppSettingsProvider = ({
|
||||
locale: language,
|
||||
})
|
||||
);
|
||||
|
||||
if (user) {
|
||||
ahoy.configure({
|
||||
urlPrefix: apiUrl,
|
||||
});
|
||||
ahoy.track("logged in", { user: user.id });
|
||||
}
|
||||
}, [user, apiUrl, language]);
|
||||
|
||||
const prepareFfmpeg = async () => {
|
||||
@@ -206,6 +215,7 @@ export const AppSettingsProvider = ({
|
||||
proxy,
|
||||
setProxy: setProxyConfigHandler,
|
||||
initialized: Boolean(user && libraryPath),
|
||||
ahoy,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -39,7 +39,7 @@ export const useConversation = () => {
|
||||
configuration: {
|
||||
baseURL: `${apiUrl}/api/ai`,
|
||||
},
|
||||
maxRetries: 3,
|
||||
maxRetries: 2,
|
||||
modelName: model,
|
||||
temperature,
|
||||
maxTokens,
|
||||
@@ -55,6 +55,7 @@ export const useConversation = () => {
|
||||
configuration: {
|
||||
baseURL: baseUrl || openai.baseUrl,
|
||||
},
|
||||
maxRetries: 2,
|
||||
modelName: model,
|
||||
temperature,
|
||||
maxTokens,
|
||||
@@ -69,6 +70,7 @@ export const useConversation = () => {
|
||||
temperature,
|
||||
frequencyPenalty,
|
||||
presencePenalty,
|
||||
maxRetries: 2,
|
||||
});
|
||||
} else if (conversation.engine === "googleGenerativeAi") {
|
||||
if (!googleGenerativeAi) throw new Error("Google Generative AI API key is required");
|
||||
@@ -78,6 +80,7 @@ export const useConversation = () => {
|
||||
modelName: model,
|
||||
temperature: temperature,
|
||||
maxOutputTokens: maxTokens,
|
||||
maxRetries: 2,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"start:enjoy": "echo 'Please use `yarn enjoy:start` instead'",
|
||||
"enjoy:add": "yarn workspace enjoy add",
|
||||
"enjoy:dev": "yarn workspace enjoy dev",
|
||||
"enjoy:start": "yarn workspace enjoy start",
|
||||
"enjoy:test": "yarn workspace enjoy test",
|
||||
|
||||
146
yarn.lock
@@ -4678,10 +4678,124 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@shikijs/core@npm:1.2.1, @shikijs/core@npm:^1.2.0":
|
||||
version: 1.2.1
|
||||
resolution: "@shikijs/core@npm:1.2.1"
|
||||
checksum: 10c0/60b5a1cdaf4389c1377b1527657377dac67173d5fd79fe4d9775392c037d6b0fe46c42bb3e3fbe42a88f4f62295f20f5cf3e43cdf1f070f4ccfedf9077379ecd
|
||||
"@sentry-internal/feedback@npm:7.107.0":
|
||||
version: 7.107.0
|
||||
resolution: "@sentry-internal/feedback@npm:7.107.0"
|
||||
dependencies:
|
||||
"@sentry/core": "npm:7.107.0"
|
||||
"@sentry/types": "npm:7.107.0"
|
||||
"@sentry/utils": "npm:7.107.0"
|
||||
checksum: 10c0/b79a68eaefef8fb83f0d423a7aa1e80cf83d82be95396e79170452ed7fe82b0c3b75f6ffa2cef525127c5fe0caf64bdffdab5e374d8912b2496e9aa379e6d500
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry-internal/replay-canvas@npm:7.107.0":
|
||||
version: 7.107.0
|
||||
resolution: "@sentry-internal/replay-canvas@npm:7.107.0"
|
||||
dependencies:
|
||||
"@sentry/core": "npm:7.107.0"
|
||||
"@sentry/replay": "npm:7.107.0"
|
||||
"@sentry/types": "npm:7.107.0"
|
||||
"@sentry/utils": "npm:7.107.0"
|
||||
checksum: 10c0/5223d66bb69cde2ba73049cacd55e5712dda6187c87df8514e8642a26ff13b8c3e34a8ec8f6c59437cb3f52a8785c3dc960eb9e046ca3bb2a74573ca99f8b77f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry-internal/tracing@npm:7.107.0":
|
||||
version: 7.107.0
|
||||
resolution: "@sentry-internal/tracing@npm:7.107.0"
|
||||
dependencies:
|
||||
"@sentry/core": "npm:7.107.0"
|
||||
"@sentry/types": "npm:7.107.0"
|
||||
"@sentry/utils": "npm:7.107.0"
|
||||
checksum: 10c0/d87143c548659ce018ba320406b098cc04429904b6194ae03e4626ccc71f2838256cee3390a1c8cd9b0ca945e8349456cf6da460772164545199682962218564
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/browser@npm:7.107.0":
|
||||
version: 7.107.0
|
||||
resolution: "@sentry/browser@npm:7.107.0"
|
||||
dependencies:
|
||||
"@sentry-internal/feedback": "npm:7.107.0"
|
||||
"@sentry-internal/replay-canvas": "npm:7.107.0"
|
||||
"@sentry-internal/tracing": "npm:7.107.0"
|
||||
"@sentry/core": "npm:7.107.0"
|
||||
"@sentry/replay": "npm:7.107.0"
|
||||
"@sentry/types": "npm:7.107.0"
|
||||
"@sentry/utils": "npm:7.107.0"
|
||||
checksum: 10c0/6cefdb519815f14419135274da22d1748e28d01f9d85d4a77ae8d999dcde08d998b4a7bc417a55c46aa45cd07d5a1abba82701f4f662be15679e6f97b0a26343
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/core@npm:7.107.0":
|
||||
version: 7.107.0
|
||||
resolution: "@sentry/core@npm:7.107.0"
|
||||
dependencies:
|
||||
"@sentry/types": "npm:7.107.0"
|
||||
"@sentry/utils": "npm:7.107.0"
|
||||
checksum: 10c0/4d28df638aec9f8b88700a09d6a5d71e05b627a4bd6e75e062a523a51d0aed3dc080909859a2a72fc49e1d7637b732ae616468a3e14482900df7c4b55739f537
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/electron@npm:^4.21.0":
|
||||
version: 4.21.0
|
||||
resolution: "@sentry/electron@npm:4.21.0"
|
||||
dependencies:
|
||||
"@sentry/browser": "npm:7.107.0"
|
||||
"@sentry/core": "npm:7.107.0"
|
||||
"@sentry/node": "npm:7.107.0"
|
||||
"@sentry/types": "npm:7.107.0"
|
||||
"@sentry/utils": "npm:7.107.0"
|
||||
deepmerge: "npm:4.3.0"
|
||||
tslib: "npm:^2.5.0"
|
||||
checksum: 10c0/596fd402d96c26ae4af7b016010231f9748b3c801022529472e25cfc7caa334fce65f5b5cf44055c10232e35676644e53f96afd0d54b162ac171605919ee26b2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/node@npm:7.107.0":
|
||||
version: 7.107.0
|
||||
resolution: "@sentry/node@npm:7.107.0"
|
||||
dependencies:
|
||||
"@sentry-internal/tracing": "npm:7.107.0"
|
||||
"@sentry/core": "npm:7.107.0"
|
||||
"@sentry/types": "npm:7.107.0"
|
||||
"@sentry/utils": "npm:7.107.0"
|
||||
checksum: 10c0/a2992e2c54a17cd323c7f7362c7742417b4fdff800f104f8de0506ca7c0c225b6709b4580bb82cebfbb28e2c53642f663d9e2ad7a68f5ef71f7b4bc8e1564501
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/replay@npm:7.107.0":
|
||||
version: 7.107.0
|
||||
resolution: "@sentry/replay@npm:7.107.0"
|
||||
dependencies:
|
||||
"@sentry-internal/tracing": "npm:7.107.0"
|
||||
"@sentry/core": "npm:7.107.0"
|
||||
"@sentry/types": "npm:7.107.0"
|
||||
"@sentry/utils": "npm:7.107.0"
|
||||
checksum: 10c0/2bb4b0c05bed77a4ad4f7b6b5b653714d02cb4892d403115bddadb1c88ccbe7d2433a7a097973204b89da1587bc9d74e2b4b402dd2e2cff27c558fc762d9b0b9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/types@npm:7.107.0":
|
||||
version: 7.107.0
|
||||
resolution: "@sentry/types@npm:7.107.0"
|
||||
checksum: 10c0/08a2b95a27106a974a42a1e2462c979b8d61d9ae63cef0d7bd36eb37603009e1e8e9bb181586d1a449b7fe52a4b696b01a4228ac13083a7c4088774f16c89421
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/utils@npm:7.107.0":
|
||||
version: 7.107.0
|
||||
resolution: "@sentry/utils@npm:7.107.0"
|
||||
dependencies:
|
||||
"@sentry/types": "npm:7.107.0"
|
||||
checksum: 10c0/c0a0cd612f4db3d12853311ba8437e0315dee2f842d8ad741afeef75ab030965fc4288d8751c42f0b263d409792388d51257c3d665fba7efdf175527ef8891c4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@shikijs/core@npm:1.1.7, @shikijs/core@npm:^1.1.5":
|
||||
version: 1.1.7
|
||||
resolution: "@shikijs/core@npm:1.1.7"
|
||||
checksum: 10c0/ee59b88d4c81422792651c0ca52ff378c3035f9d1e4907b58c1d6da06fad02d530775a2e6f43bb033832cd3d2a5f69c9aa4eb5b6b05311396acc503f15442f37
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5274,6 +5388,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ahoy.js@npm:^0.4.2":
|
||||
version: 0.4.2
|
||||
resolution: "@types/ahoy.js@npm:0.4.2"
|
||||
checksum: 10c0/2be990ab96b57e749e35bf3f80c37ce375e0f7b58bd0cc75848a70e22adb0f63c5b657c82a1915d40f4272b8e973103e993ac1b125e28e24e208002621010a15
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/argparse@npm:1.0.38":
|
||||
version: 1.0.38
|
||||
resolution: "@types/argparse@npm:1.0.38"
|
||||
@@ -6297,6 +6418,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ahoy.js@npm:^0.4.3":
|
||||
version: 0.4.3
|
||||
resolution: "ahoy.js@npm:0.4.3"
|
||||
checksum: 10c0/eaacbf267d2094cb80651485af0d58034ec89d48f3755159ec850fb75d62b776222e6e9aa333aeb54d6ba1f0bac58f03653294244f1b01e69458a76fae3c2855
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ajv@npm:^6.12.4":
|
||||
version: 6.12.6
|
||||
resolution: "ajv@npm:6.12.6"
|
||||
@@ -8435,6 +8563,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"deepmerge@npm:4.3.0":
|
||||
version: 4.3.0
|
||||
resolution: "deepmerge@npm:4.3.0"
|
||||
checksum: 10c0/7ff5c6294b3316c1bc6bca9d3ef2193c1d7beec4e62252db8bcb8a6366d85b924850492eb1a746a5f33d609862e03dfb907ce9fa8769583300f65f20a337cec5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"deepmerge@npm:^4.3.1":
|
||||
version: 4.3.1
|
||||
resolution: "deepmerge@npm:4.3.1"
|
||||
@@ -9151,7 +9286,9 @@ __metadata:
|
||||
"@radix-ui/react-toast": "npm:^1.1.5"
|
||||
"@radix-ui/react-toggle": "npm:^1.0.3"
|
||||
"@radix-ui/react-tooltip": "npm:^1.0.7"
|
||||
"@sentry/electron": "npm:^4.21.0"
|
||||
"@tailwindcss/typography": "npm:^0.5.10"
|
||||
"@types/ahoy.js": "npm:^0.4.2"
|
||||
"@types/autosize": "npm:^4.0.3"
|
||||
"@types/command-exists": "npm:^1.2.3"
|
||||
"@types/electron-squirrel-startup": "npm:^1.0.2"
|
||||
@@ -9170,6 +9307,7 @@ __metadata:
|
||||
"@uidotdev/usehooks": "npm:^2.4.1"
|
||||
"@vidstack/react": "npm:^1.10.9"
|
||||
"@vitejs/plugin-react": "npm:^4.2.1"
|
||||
ahoy.js: "npm:^0.4.3"
|
||||
autoprefixer: "npm:^10.4.19"
|
||||
autosize: "npm:^6.0.1"
|
||||
axios: "npm:^1.6.8"
|
||||
|
||||