Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Hanjie Deng

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ curl https://obsidian.daydreamer.net.cn/videos/demo.mp4 --output demo.mp4

VoxBridge 是一款面向 Windows 桌面的 AI 同声传译助手。它用于观看英语或中文演讲、技术分享、国际会议、网课和会议软件音频时,将单向音频流实时识别、翻译为中文字幕,并可在用户显式开启时播放中文语音。

完整的安装、课堂语境配置和故障排查说明见 [中文使用教程](docs/USER_GUIDE.zh-CN.md)。

本项目当前实现聚焦 Windows MVP:采集 Windows 系统音频或指定应用音频,接入火山引擎 AST 同声传译服务,使用浮动字幕窗口呈现实时译文,并通过可变字幕缓冲区支持流式 partial/final 替换与近期字幕修正。

## 核心交互功能
Expand Down Expand Up @@ -164,3 +166,7 @@ Rust 测试:
```bash
cargo test --manifest-path src-tauri/Cargo.toml
```

## 许可证

本项目采用 [MIT License](LICENSE)。
156 changes: 156 additions & 0 deletions docs/USER_GUIDE.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# VoxBridge 中文使用教程

本教程适用于 Windows 10/11。VoxBridge 是 Tauri 桌面应用;仅启动网页开发服务器时,Windows 音频采集、系统托盘和悬浮字幕等原生能力可能不可用。

## 1. 准备运行环境

安装以下软件:

- Bun
- Rust stable toolchain
- Microsoft C++ Build Tools
- Microsoft Edge WebView2 Runtime

真实同传还需要可用的火山引擎 AST API Key。API 服务的价格、额度和可用区域由服务商决定,VoxBridge 本身不附带 API 额度。

## 2. 下载与安装依赖

```powershell
git clone https://github.com/floatDreamWithSong/VoxBridge.git
cd VoxBridge
bun install
```

如需试用尚未合并的课堂语境功能,可从贡献者 fork 获取对应分支:

```powershell
git remote add lecture-fork https://github.com/879568078-commits/VoxBridge.git
git fetch lecture-fork
git switch --track lecture-fork/feature/context-aware-lecture-profile
bun install
```

## 3. 启动桌面应用

```powershell
bun run tauri dev
```

首次启动需要编译 Rust 依赖,耗时会比后续启动更长。

仅查看前端界面时可运行:

```powershell
bun run dev
```

然后访问 `http://localhost:1420`。此方式不代表完整桌面同传链路可用。

## 4. 配置 AST 服务

1. 打开 **Settings**。
2. 在 **Volcengine AST** 中填写 API Key。
3. 不要把 API Key 写入源码、提交到 GitHub 或发送给他人。

配置保存在本机活动 Profile 中。

## 5. 选择课堂音频

打开 **Audio Source**:

- **System audio**:识别 Windows 默认输出设备播放的全部声音。
- **Application audio**:只采集指定应用,适合 Chrome、Edge、Teams 或 Zoom 中的课程。

课堂使用推荐 **Application audio**,它能减少通知音和其他软件声音的干扰。英语课程建议将源语言固定为 **English**;只有确实存在中英文混合时再使用 **Auto**。

## 6. 配置中文字幕

打开 **Subtitles**:

1. 字幕语言选择中文。
2. 如需同时查看原文,开启双语字幕。
3. 打开字幕预览窗口并拖到屏幕底部。
4. 保存窗口位置。

建议从以下参数开始调整:

- 字体大小:28–34 px
- 最大字幕块:2–3
- 背景透明度:0.80–0.90
- 行高:1.25–1.35

## 7. 配置 EU 条约改革课堂语境

打开 **Settings → 课堂语境**:

- 语境模式:**自然课堂**
- 课程主题:**Treaty Reform in the EU**
- 可回溯修订字幕段数:建议 **3–5**
- 自动术语修正:开启

可在自定义术语中每行填写一个词组:

```text
Treaty of Lisbon
Treaty on European Union
Treaty on the Functioning of the European Union
constitutional convention
intergovernmental conference
acquis communautaire
subsidiarity
proportionality
qualified majority voting
ordinary legislative procedure
European Council
European Commission
```

通用模式会关闭 EU 课程预设。自动术语修正只处理已经稳定的字幕,不会改写仍在生成的临时字幕。当前实现属于识别后的保守规范化,并不会把术语提示发送给 AST Provider。

## 8. 开始同传

1. 打开课程视频或会议并暂时暂停播放。
2. 回到 VoxBridge 首页。
3. 点击开始实时同传。
4. 等待状态变为运行中。
5. 继续播放课程并观察悬浮中文字幕。
6. 结束后点击停止按钮。

## 9. 常见问题

### 没有字幕

- 检查 AST API Key 和服务开通状态。
- 确认课程正在播放声音。
- 刷新应用列表并重新选择课程播放器。
- Application audio 无结果时尝试 System audio。

### 有英文但没有中文

- 检查 AST 账号额度、目标语言和网络连接。
- 查看首页显示的运行时错误。

### 口音识别不准

- 将源语言固定为 English。
- 使用 Application audio 减少回声与环境噪声。
- 将课程高频词加入自定义术语。
- 避免多个应用同时播放声音。

### 翻译过于法律化

- 删除不必要的自定义术语。
- 切换到通用模式以关闭 EU 课程预设。

## 10. 验证开发环境

```powershell
bun run test
bun run typecheck
bun run lint
bun run build
```

## 11. 许可证

VoxBridge 使用 MIT License。允许使用、复制、修改、合并、发布、分发、再许可和销售软件副本,但必须在软件副本或重要部分中保留版权声明和许可声明。软件按“原样”提供,不附带保证。完整条款见仓库根目录 `LICENSE`。
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"width": 1400,
"height": 918,
"center": true,
"visible": false
"visible": true
}
],
"security": {
Expand Down
14 changes: 14 additions & 0 deletions src/features/configuration/SettingsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ describe("SettingsPage", () => {
expect(inputValue("托盘点击行为")).toBe("toggle-runtime")
})

test("configures natural EU treaty reform lecture context", () => {
renderSettingsPage()

expect(inputValue("语境模式")).toBe("lecture")
expect(inputValue("课程主题")).toBe("eu-treaty-reform")
fireEvent.change(screen.getByLabelText("自定义术语(每行一个)"), {
target: { value: "constitutional convention\nacquis communautaire" },
})

expect(
screen.getByLabelText<HTMLTextAreaElement>("自定义术语(每行一个)").value
).toContain("acquis communautaire")
})

test("switches UI language independently from AST API key settings", () => {
renderSettingsPage()

Expand Down
131 changes: 126 additions & 5 deletions src/features/configuration/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,7 @@ export function SettingsPage() {
app: {
...profile.app,
closeBehavior: event.target.value as
| "exit"
| "minimize"
| "minimize-to-tray",
"exit" | "minimize" | "minimize-to-tray",
},
}))
}
Expand Down Expand Up @@ -193,8 +191,7 @@ export function SettingsPage() {
app: {
...profile.app,
trayClickBehavior: event.target.value as
| "open-main-window"
| "toggle-runtime",
"open-main-window" | "toggle-runtime",
},
}))
}
Expand All @@ -210,6 +207,130 @@ export function SettingsPage() {
</div>
</section>

<section className="rounded-md border border-zinc-200 bg-white p-4 lg:col-span-2">
<h2 className="text-base font-semibold">{t("settings.context")}</h2>
<p className="mt-1 text-sm text-zinc-500">
{t("settings.context.description")}
</p>
<div className="mt-4 grid gap-4 md:grid-cols-2">
<label className="block">
<span className="text-sm font-medium">
{t("settings.context.mode")}
</span>
<select
className="mt-2 h-10 w-full rounded-md border border-zinc-300 px-3 text-sm"
value={activeProfile.interpretationContext.mode}
onChange={(event) =>
updateActiveProfile((profile) => ({
...profile,
interpretationContext: {
...profile.interpretationContext,
mode: event.target.value as "general" | "lecture",
},
}))
}
>
<option value="lecture">
{t("settings.context.mode.lecture")}
</option>
<option value="general">
{t("settings.context.mode.general")}
</option>
</select>
</label>

<label className="block">
<span className="text-sm font-medium">
{t("settings.context.topic")}
</span>
<select
className="mt-2 h-10 w-full rounded-md border border-zinc-300 px-3 text-sm"
value={activeProfile.interpretationContext.topicPreset}
onChange={(event) =>
updateActiveProfile((profile) => ({
...profile,
interpretationContext: {
...profile.interpretationContext,
topicPreset: event.target.value as
"none" | "eu-treaty-reform",
},
}))
}
>
<option value="eu-treaty-reform">
{t("settings.context.topic.euTreatyReform")}
</option>
<option value="none">
{t("settings.context.topic.none")}
</option>
</select>
</label>

<label className="block">
<span className="text-sm font-medium">
{t("settings.context.depth")}
</span>
<input
className="mt-2 h-10 w-full rounded-md border border-zinc-300 px-3 text-sm"
type="number"
min={1}
max={8}
value={activeProfile.interpretationContext.contextDepth}
onChange={(event) =>
updateActiveProfile((profile) => ({
...profile,
interpretationContext: {
...profile.interpretationContext,
contextDepth: Number(event.target.value),
},
}))
}
/>
</label>

<label className="flex items-center gap-3 self-end pb-2 text-sm font-medium">
<input
type="checkbox"
checked={
activeProfile.interpretationContext.automaticCorrection
}
onChange={(event) =>
updateActiveProfile((profile) => ({
...profile,
interpretationContext: {
...profile.interpretationContext,
automaticCorrection: event.target.checked,
},
}))
}
/>
{t("settings.context.automaticCorrection")}
</label>

<label className="block md:col-span-2">
<span className="text-sm font-medium">
{t("settings.context.customTerms")}
</span>
<textarea
className="mt-2 min-h-24 w-full rounded-md border border-zinc-300 p-3 text-sm"
placeholder={t("settings.context.customTermsPlaceholder")}
value={activeProfile.interpretationContext.customTerms.join(
"\n"
)}
onChange={(event) =>
updateActiveProfile((profile) => ({
...profile,
interpretationContext: {
...profile.interpretationContext,
customTerms: event.target.value.split(/\r?\n/),
},
}))
}
/>
</label>
</div>
</section>

<section className="rounded-md border border-zinc-200 bg-white p-4 lg:col-span-2">
<h2 className="text-base font-semibold">{t("settings.ast")}</h2>
<div className="mt-4">
Expand Down
Loading