Skip to content

Commit

Permalink
feat: 开发字幕文件翻译后端(2/3)
Browse files Browse the repository at this point in the history
  • Loading branch information
QiuYeDx committed Feb 15, 2025
1 parent 86b6711 commit 71eeaed
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 93 deletions.
202 changes: 148 additions & 54 deletions electron/main/translation/class/base-translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { promises as fs } from "fs";
import path from "path";
import { DEFAULT_SLICE_LENGTH_MAP } from "../contants";
import { SubtitleSliceType, SubtitleTranslatorTask } from "../typing";
import { ipcMain } from "electron";
import { ipcMain, BrowserWindow } from "electron";
import axios from 'axios';

export abstract class BaseTranslator {
protected abstract splitContent(content: string, maxTokens: number): string[];
Expand All @@ -16,20 +17,53 @@ export abstract class BaseTranslator {
protected retryDelay = 1000;

async translate(task: SubtitleTranslatorTask, signal?: AbortSignal) {
const content = await this.readFile(task.originFileURL);
const maxTokens = this.getMaxTokens(task.sliceType);
const fragments = this.splitContent(content, maxTokens);
try {
console.log("开始处理文件:", task.fileName);
const content = task.fileContent;
console.log("文件内容长度:", content.length);

const maxTokens = this.getMaxTokens(task.sliceType);
console.log("使用的最大 token 数:", maxTokens);

const fragments = this.splitContent(content, maxTokens);
console.log("分片数量:", fragments.length);

// 初始化进度
this.updateProgress(task, 0, fragments.length);

const translatedFragments: string[] = [];

for (const [index, fragment] of fragments.entries()) {
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");

for (const [index, fragment] of fragments.entries()) {
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
const result = await this.translateFragment(
fragment,
index > 0 ? fragments[index - 1] : "",
task.apiKey,
task.apiModel,
);

const result = await this.translateFragment(
fragment,
index > 0 ? fragments[index - 1] : "",
task.extraInfo?.apiKey
);
if (!result) {
throw new Error("Translation result is undefined");
}

this.updateProgress(task, index + 1, fragments.length);
translatedFragments.push(result);
console.log("result", result);

// 更新当前分片进度
this.updateProgress(task, index + 1, fragments.length);
}

// 将翻译后的内容写入目标文件
const translatedContent = translatedFragments.join("\n");
await this.writeFile(task.targetFileURL, translatedContent, task.fileName);

// 通知任务完成
this.updateProgress(task, fragments.length, fragments.length);

} catch (error) {
console.error("翻译过程出错:", error);
throw error;
}
}

Expand All @@ -38,28 +72,72 @@ export abstract class BaseTranslator {
current: number,
total: number
) {
// 发送进度到渲染进程
ipcMain.emit("update-progress", {
fileName: task.fileName,
current,
total,
progress: (current / total) * 100,
});
// 更新任务对象
task.resolvedFragments = current;
task.totalFragments = total;
task.progress = Math.round((current / total) * 100);

// 获取主窗口并发送消息
const mainWindow = BrowserWindow.getAllWindows()[0];
if (mainWindow) {
console.log("发送进度更新:", {
fileName: task.fileName,
resolvedFragments: current,
totalFragments: total,
progress: task.progress
});

mainWindow.webContents.send("update-progress", {
fileName: task.fileName,
resolvedFragments: current,
totalFragments: total,
progress: task.progress
});
} else {
console.error("未找到主窗口,无法发送进度更新");
}
}

private async readFile(fileURL: string) {
// private async readFile(fileURL: string) {
// try {
// // 确保路径是绝对路径
// const absolutePath = path.resolve(fileURL);

// // 读取文件内容
// const fileContent = await fs.readFile(absolutePath, "utf-8");

// // 返回文件内容
// return fileContent;
// } catch (error) {
// console.error("读取文件时出错:", error);
// throw new Error("无法读取文件");
// }
// }

// // 从 blobURL 中获取文件内容, 返回实际字符串
// private async readFile(blobURL: string) {
// const response = await fetch(blobURL);
// const blob = await response.blob();
// // return new File([blob], "file.txt", { type: "text/plain" });
// return blob.toString();
// }

private async writeFile(fileURL: string, content: string, fileName: string) {
try {
const newFileURL = path.join(fileURL, fileName);
// 确保路径是绝对路径
const absolutePath = path.resolve(fileURL);

// 读取文件内容
const fileContent = await fs.readFile(absolutePath, "utf-8");

// 返回文件内容
return fileContent;

// 确保目标目录存在
const targetDir = path.dirname(absolutePath);
await fs.mkdir(targetDir, { recursive: true });

// 写入文件内容
await fs.writeFile(newFileURL, content, 'utf-8');
console.log("文件已成功写入:", fileName);
} catch (error) {
console.error("读取文件时出错:", error);
throw new Error("无法读取文件");
console.error("写入文件时出错:", error);
throw new Error("无法写入文件");
}
}

Expand All @@ -71,46 +149,62 @@ export abstract class BaseTranslator {
content: string,
context: string,
apiKey: string,
signal?: AbortSignal
) {
apiModel: string,
): Promise<string> {
const prompt = this.formatPrompt(content, context);
const headers = this.createHeaders(apiKey);

for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
const controller = new AbortController();
signal?.addEventListener("abort", () => controller.abort());

const response = await fetch(this.getApiEndpoint(), {
method: "POST",
headers,
body: this.buildRequestBody(prompt),
signal: controller.signal,
});

if (response.status === 429) {
await this.handleRateLimit(response);
continue;
const response = await axios.post(
this.getApiEndpoint(),
{
model: apiModel,
messages: [
{
role: "user",
content: prompt
}
],
max_tokens: 3500,
temperature: 0.3
},
{
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
}
);

if (!response.data) {
throw new Error('翻译返回结果为空');
}

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
console.log("翻译响应数据:", response.data);
return this.parseResponse(response.data);

return await this.parseResponse(await response.json());
} catch (error) {
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
if (attempt === this.maxRetries) throw this.normalizeError(error);
await new Promise((r) => setTimeout(r, this.retryDelay * attempt));
if (signal?.aborted) {
throw new DOMException("Aborted", "AbortError");
}

console.error(`第 ${attempt} 次翻译尝试失败:`, error);

if (attempt === this.maxRetries) {
throw this.normalizeError(error);
}

// 重试延迟
await new Promise(r => setTimeout(r, this.retryDelay * attempt));
}
}

throw new Error('所有翻译尝试都失败了');
}

// 以下为需要子类实现的抽象方法
protected abstract getApiEndpoint(): string;
protected abstract createHeaders(apiKey: string): Record<string, string>;
protected abstract buildRequestBody(prompt: string): BodyInit;
protected abstract parseResponse(response: any): Promise<string>;
protected abstract parseResponse(responseData: any): Promise<string>;
protected abstract normalizeError(error: unknown): Error;

// 通用的速率限制处理
Expand Down
2 changes: 1 addition & 1 deletion electron/main/translation/typing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export enum TaskStatus {

export type SubtitleTranslatorTask = {
fileName: string;
// fileType: SubtitleFileType;
fileContent: string;
sliceType: SubtitleSliceType;
originFileURL: string; // 源文件路径
targetFileURL: string; // 输出文件路径
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"dependencies": {
"@heroicons/react": "^2.2.0",
"@react-spring/web": "^9.7.5",
"axios": "^1.7.9",
"electron-updater": "^6.3.9",
"gpt-3-encoder": "^1.1.4",
"html-to-image": "^1.11.11",
Expand Down
9 changes: 8 additions & 1 deletion src/locales/en/subtitle.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,15 @@
"resolved": "Completed",
"failed": "Failed"
},
"infos": {
"output_path_selected": "Output path is set",
"total_fragments": "Total fragments: {count}"
},
"errors": {
"invalid_file_type": "Unsupported file type: {types}"
"invalid_file_type": "Unsupported file type: {types}",
"duplicate_file": "File {file} already exists, skipping duplicate addition",
"path_selection_failed": "Path selection failed, please try again",
"please_select_output_url": "Please select the output URL first"
},
"upload_section": "File Upload",
"task_management": "Task Queue"
Expand Down
9 changes: 8 additions & 1 deletion src/locales/ja/subtitle.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,15 @@
"resolved": "完了",
"failed": "失敗"
},
"infos": {
"output_path_selected": "出力パスが設定されています",
"total_fragments": "総分片数:{count}"
},
"errors": {
"invalid_file_type": "非対応ファイル形式: {types}"
"invalid_file_type": "非対応ファイル形式: {types}",
"duplicate_file": "ファイル {file} は既に存在します。",
"path_selection_failed": "パス選択に失敗しました。再試行してください。",
"please_select_output_url": "出力URLを先に選択してください。"
},
"upload_section": "ファイルアップロード",
"task_management": "タスク管理"
Expand Down
3 changes: 2 additions & 1 deletion src/locales/zh/subtitle.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"failed": "翻译失败"
},
"infos": {
"output_path_selected": "输出路径已设置"
"output_path_selected": "输出路径已设置",
"total_fragments": "总分片数:{count}"
},
"errors": {
"invalid_file_type": "不支持的文件类型:{types}",
Expand Down
Loading

0 comments on commit 71eeaed

Please sign in to comment.