Skip to content

Commit

Permalink
feat: support i18n (#46)
Browse files Browse the repository at this point in the history
Co-authored-by: linxiaodong <[email protected]>
  • Loading branch information
buxuku and linxiaodong authored Oct 9, 2024
1 parent 0aee0ad commit a6a0000
Show file tree
Hide file tree
Showing 43 changed files with 1,087 additions and 182 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# video-subtitle-master

[English](./README_EN.md) | 中文

批量为视频生成字幕,并可将字幕翻译成其它语言。这是在之前的一个开源项目 [VideoSubtitleGenerator](https://github.com/buxuku/VideoSubtitleGenerator) 的基础上,制作成的一个客户端工具,以方便更多朋友们的使用。

![preview](./resources/preview.png)
Expand Down
104 changes: 104 additions & 0 deletions README_EN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Video Subtitle Master

English | [中文](./README.md)

Video Subtitle Master is a powerful desktop application for batch generating subtitles for videos and translating them into other languages. This project is an enhanced version of the open-source [VideoSubtitleGenerator](https://github.com/buxuku/VideoSubtitleGenerator), redesigned as a user-friendly client tool.

![preview](./resources/preview-en.png)

> [!NOTE]
> The current release has been tested on macOS. Windows testing was done in a virtual environment. If you encounter any issues, please feel free to open an Issue for feedback.
## 💥 Features

This application retains all the features of the original [VideoSubtitleGenerator](https://github.com/buxuku/VideoSubtitleGenerator) command-line tool, with the following enhancements:

- Graphical user interface for easier operation
- Source and target language subtitle files are saved in the same directory as the video for convenient subtitle attachment during playback
- Batch processing of video/audio/subtitle files
- Support for generating subtitles from video or audio files
- Ability to translate generated or imported subtitles
- Multiple translation services supported:
- Volcano Engine Translation
- Baidu Translation
- DeepLX Translation (Note: Batch translation may be rate-limited)
- Local Ollama model translation
- Support for OpenAI-style API translations (e.g., DeepSpeed)
- Customizable subtitle file naming for compatibility with various media players
- Flexible translated subtitle content: choose between pure translation or original + translated subtitles
- Integrated `whisper.cpp` with optimization for Apple Silicon, offering faster generation speeds
- Built-in `fluent-ffmpeg`, eliminating the need for separate `ffmpeg` installation
- Support for running locally installed `whisper` command
- Option to choose model download source (domestic mirror or official source)
- Customizable number of concurrent tasks

## Translation Services

This project supports various translation services, including Baidu Translation, Volcano Engine Translation, DeepLX, local Ollama models, and OpenAI-style APIs. Using these services requires the appropriate API keys or configurations.

For information on obtaining API keys for services like Baidu Translation and Volcano Engine, please refer to https://bobtranslate.com/service/. We appreciate the information provided by [Bob](https://bobtranslate.com/), an excellent software tool.

## 🔦 Usage (For End Users)

1. Go to the [releases](https://github.com/buxuku/video-subtitle-master/releases) page and download the appropriate package for your operating system
2. Install and run the program
3. Configure the desired translation services within the application
4. Select the video or subtitle files you want to process
5. Set relevant parameters (e.g., source language, target language, model)
6. Start the processing task

## 🔦 Usage (For Developers)

1️⃣ Clone the project locally

```shell
git clone https://github.com/buxuku/video-subtitle-master.git
```

2️⃣ Install dependencies using `yarn install` or `npm install`

```shell
cd video-subtitle-master
yarn install
```

3️⃣ After installing dependencies, run `yarn start` or `npm start` to launch the project

```shell
yarn start
```

## Manually Downloading and Importing Models

Due to the large size of model files, downloading them through the software may be challenging. You can manually download models and import them into the application. Here are two links for downloading models:

1. Domestic mirror (faster download speeds):
https://hf-mirror.com/ggerganov/whisper.cpp/tree/main

2. Hugging Face official source:
https://huggingface.co/ggerganov/whisper.cpp/tree/main

After downloading, you can import the model files into the application using the "Import Model" feature on the "Model Management" page.

Import steps:
1. On the "Model Management" page, click the "Import Model" button.
2. In the file selector that appears, choose your downloaded model file.
3. After confirming the import, the model will be added to your list of installed models.

## Common Issues

##### 1. "The application is damaged and can't be opened" message
Execute the following command in the terminal:

```shell
sudo xattr -dr com.apple.quarantine /Applications/Video\ Subtitle\ Master.app
```
Then try running the application again.

## Contributing

Issues and Pull Requests are welcome to help improve this project!

## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
15 changes: 10 additions & 5 deletions main/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createWindow } from "./helpers/create-window";
import { setupIpcHandlers } from './helpers/ipcHandler';
import { setupTaskProcessor } from './helpers/taskProcessor';
import { setupSystemInfoManager } from './helpers/systemInfoManager';
import { setupStoreHandlers } from './helpers/storeManager';
import { setupStoreHandlers, store } from './helpers/storeManager';
import { setupTaskManager } from "./helpers/taskManager";

const isProd = process.env.NODE_ENV === "production";
Expand All @@ -19,22 +19,27 @@ if (isProd) {
(async () => {
await app.whenReady();

setupStoreHandlers();

const settings = store.get('settings');
const userLanguage = settings?.language || 'zh'; // 默认为中文

const mainWindow = createWindow("main", {
width: 1400,
height: 1040,
height: 1020,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
},
});

if (isProd) {
await mainWindow.loadURL("app://./home");
await mainWindow.loadURL(`app://./${userLanguage}/home`);
} else {
const port = process.argv[2];
await mainWindow.loadURL(`http://localhost:${port}/home`);
await mainWindow.loadURL(`http://localhost:${port}/${userLanguage}/home`);
mainWindow.webContents.openDevTools();
}
setupStoreHandlers();

setupIpcHandlers(mainWindow);
setupTaskProcessor(mainWindow);
setupSystemInfoManager(mainWindow);
Expand Down
16 changes: 15 additions & 1 deletion main/helpers/storeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { defaultUserConfig } from './utils';
type StoreType = {
translationProviders: Record<string, any>[],
userConfig: Record<string, any>,
settings: {
language: string;
},
[key: string]: any
}

Expand All @@ -25,7 +28,10 @@ const defaultTranslationProviders = [
export const store = new Store<StoreType>({
defaults: {
userConfig: defaultUserConfig,
translationProviders: defaultTranslationProviders
translationProviders: defaultTranslationProviders,
settings: {
language: 'zh'
}
}
});

Expand Down Expand Up @@ -68,4 +74,12 @@ export function setupStoreHandlers() {
ipcMain.handle('getUserConfig', async () => {
return store.get('userConfig');
});

ipcMain.handle('setSettings', async (event, settings) => {
store.set('settings', settings);
});

ipcMain.handle('getSettings', async () => {
return store.get('settings');
});
}
2 changes: 1 addition & 1 deletion main/helpers/translate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export default async function translate(
try {
targetContent = await translator(sourceContent, proof);
} catch (translationError) {
throw new Error(`翻译失败: ${translationError.message}`);
throw new Error(`${translationError.message}`);
}
items.push({
id: data[i],
Expand Down
3 changes: 2 additions & 1 deletion main/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ export const defaultUserConfig = {
sourceSrtSaveFileName: '${fileName}.${sourceLanguage}',
model: 'tiny',
translateProvider: 'baidu',
saveSourceSrt: false,
translateContent: 'onlyTranslate',
maxConcurrentTasks: 1,
sourceSrtSaveOption: 'noSave',
targetSrtSaveOption: 'fileNameWithLang',
}

export function getSrtFileName(
Expand Down
2 changes: 1 addition & 1 deletion main/service/baidu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default async function baidu(query, proof) {
const { apiKey: appid, apiSecret: key } = proof || {};
if (!appid || !key) {
console.log("请先配置 API KEY 和 API SECRET");
throw new Error("请先配置 API KEY 和 API SECRET");
throw new Error("missingKeyOrSecret");
}
const salt = new Date().getTime();
const str1 = appid + query + salt + key;
Expand Down
2 changes: 1 addition & 1 deletion main/service/volc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default async function translate(query, proof) {
const { apiKey: accessKeyId, apiSecret: secretKey } = proof || {};
if (!accessKeyId || !secretKey) {
console.log("请先配置 API KEY 和 API SECRET");
throw new Error("请先配置 API KEY 和 API SECRET");
throw new Error("missingKeyOrSecret");
}
if (!service || !fetchApi) {
service = new Service({
Expand Down
14 changes: 14 additions & 0 deletions next-i18next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/** @type {import('next-i18next').UserConfig} */
module.exports = {
i18n: {
defaultLocale: 'zh',
locales: ['zh', 'en'],
},
debug: process.env.NODE_ENV === 'development',
reloadOnPrerender: process.env.NODE_ENV === 'development',
localePath:
typeof window === 'undefined'
? // eslint-disable-next-line @typescript-eslint/no-var-requires
require('path').resolve('./renderer/public/locales')
: '/locales',
}
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"private": true,
"name": "video-subtitle-master",
"description": "视频转字幕,字幕翻译软件",
"version": "1.0.20",
"version": "1.1.0",
"author": "buxuku <[email protected]>",
"main": "app/background.js",
"scripts": {
Expand All @@ -22,6 +22,7 @@
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
Expand All @@ -34,12 +35,15 @@
"electron-store": "^8.2.0",
"ffmpeg-static": "^5.2.0",
"fluent-ffmpeg": "^2.1.2",
"i18next": "^23.15.2",
"isomorphic-git": "^1.25.10",
"lodash": "^4.17.21",
"lucide-react": "^0.378.0",
"next-i18next": "^15.3.1",
"next-themes": "^0.3.0",
"openai": "^4.0.0",
"react-hook-form": "^7.51.4",
"react-i18next": "^15.0.2",
"regenerator-runtime": "^0.14.1",
"sonner": "^1.4.41",
"tailwind-merge": "^2.3.0",
Expand Down
10 changes: 6 additions & 4 deletions renderer/components/DeleteModel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { useTranslation } from 'next-i18next';

const DeleteModel = ({children, modelName, callBack}) => {
const { t } = useTranslation('common');
const [visibility, setVisibility] = React.useState(false);
const handleDelete = async (e) => {
e.preventDefault();
Expand All @@ -26,14 +28,14 @@ const DeleteModel = ({children, modelName, callBack}) => {
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认删除该模型?</AlertDialogTitle>
<AlertDialogTitle>{t('confirmDeleteModel')}</AlertDialogTitle>
<AlertDialogDescription>
删除之后,如果你需要再次使用该模型,需要重新下载。
{t('deleteModelDesc')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setVisibility(false)}>取消</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete}>删除</AlertDialogAction>
<AlertDialogCancel onClick={() => setVisibility(false)}>{t('cancel')}</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete}>{t('delete')}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
Expand Down
4 changes: 3 additions & 1 deletion renderer/components/DownModelButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { FC } from "react";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
import { useTranslation } from "next-i18next";

interface IProps {
loading?: boolean;
Expand All @@ -13,14 +14,15 @@ const DownModelButton: FC<IProps> = ({
progress,
handleDownModel,
}) => {
const { t } = useTranslation('common');
return (
<Button disabled={loading} onClick={() => handleDownModel()}>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> {progress} %
</>
) : (
"下载"
t('download')
)}
</Button>
);
Expand Down
12 changes: 7 additions & 5 deletions renderer/components/DownModelDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
import { useTranslation } from "next-i18next";

interface IProps {
loading?: boolean;
Expand All @@ -27,6 +28,7 @@ const DownModelDropdown: FC<IProps> = ({
installComplete,
whisperLoading,
}) => {
const { t } = useTranslation("common");
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
Expand All @@ -36,30 +38,30 @@ const DownModelDropdown: FC<IProps> = ({
className="w-24"
>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{loading ? `${progress}%` : "下载"}
{loading ? `${progress}%` : t("download")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[225px]">
<DropdownMenuLabel>请选择下载源</DropdownMenuLabel>
<DropdownMenuLabel>{t("pleaseSelectSource")}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer hover:bg-gray-100"
onClick={() => handleDownModel("hf-mirror")}
>
国内镜像源(较快)
{t("domesticMirrorSource")}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer hover:bg-gray-100"
onClick={() => handleDownModel("huggingface")}
>
huggingface官方源(较慢)
{t("officialHuggingFaceSource")}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer hover:bg-gray-100"
onClick={() => setShowGuide(false)}
>
稍后下载
{t("downloadLater")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Expand Down
Loading

0 comments on commit a6a0000

Please sign in to comment.