Skip to content

Commit 4dd76c4

Browse files
committed
Release v2.3.0: API improvements and service enhancements
1 parent 41e158a commit 4dd76c4

31 files changed

Lines changed: 505 additions & 102 deletions

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# Gemini Subtitle Pro
22

3-
**Gemini Subtitle Pro** 是一款基于 AI 的字幕创建、翻译和润色工具。它利用 Google 的 Gemini 模型进行高质量的翻译和校对,并使用 OpenAI 的 Whisper 进行精准的语音转写。
3+
**Gemini Subtitle Pro** 是一款基于 AI 的字幕创建、翻译和润色工具。它利用 Google 的 Gemini 模型进行高质量的翻译和润色,并使用 OpenAI 的 Whisper 进行精准的语音转写。
44

55
## ✨ 功能特性
66

77
### 核心 AI 功能
88
- **🤖 AI 转写**: 支持 **OpenAI Whisper API** (在线) 或 **Local Whisper** (离线,仅限桌面版) 转写视频/音频
99
- **🌍 智能翻译**: 使用 **Gemini 2.5 Flash** 将字幕翻译为简体中文
10-
- **🧐 深度校对**: 使用 **Gemini 2.5 Flash****Gemini 3.0 Pro Preview** 润色和校正字幕,确保措辞自然准确
10+
- **🧐 翻译润色**: 使用 **Gemini 2.5 Flash****Gemini 3.0 Pro Preview** 润色和校正字幕,确保措辞自然准确
1111
- **🎯 智能分割**: 使用 Silero VAD 进行智能音频分割,优化字幕时间轴
1212

1313
### 术语管理
@@ -22,8 +22,8 @@
2222
- **⏱️ 请求超时配置**: 可自定义 API 请求超时时间,适应不同网络环境
2323

2424
### 批量操作
25-
- **⏱️ 修复时间轴**: 使用 AI 自动对齐字幕时间轴与音频
26-
- **✏️ 润色**: 结合上下文对选中片段进行批量润色
25+
- **⏱️ 校对时间轴**: 使用 AI 自动对齐字幕时间轴与音频
26+
- **✏️ 润色翻译**: 结合上下文对选中片段进行批量润色
2727

2828
### 工作流功能
2929
- **📸 版本控制**: 内置快照系统,可保存和恢复不同版本的工作
@@ -66,7 +66,7 @@
6666

6767
### 1. 常规 (General)
6868
- **API 配置**:
69-
- `Gemini API 密钥`: 必填。用于翻译 (Gemini 2.5 Flash) 和校对 (Gemini 3 Pro)。
69+
- `Gemini API 密钥`: 必填。用于翻译 (Gemini 2.5 Flash) 和润色 (Gemini 3 Pro)。
7070
- `Gemini 端点`: 可选。自定义 Google Gemini API 的 Base URL。
7171
- `OpenAI API 密钥`: 必填(使用本地 Whisper 时不需要)。用于 Whisper 转写。
7272
- `OpenAI 端点`: 可选(使用本地 Whisper 时不需要)。自定义 OpenAI API 的 Base URL。
@@ -75,7 +75,7 @@
7575

7676
### 2. 性能 (Performance)
7777
- **批处理**:
78-
- `校对批次大小`: 单次 API 调用校对的行数
78+
- `润色批次大小`: 单次 API 调用润色的行数
7979
- `翻译批次大小`: 单次 API 调用翻译的行数。
8080
- **并发控制**:
8181
- `并发数 (Flash)`: Gemini 2.5 Flash 的并发请求限制。

electron/main.ts

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ ipcMain.handle('transcribe-local', async (_event, { audioData, modelPath, langua
3939
}
4040
});
4141

42+
// IPC Handler: Abort Local Whisper
43+
ipcMain.handle('local-whisper-abort', async () => {
44+
console.log('[Main] Aborting all local whisper processes');
45+
localWhisperService.abort();
46+
return { success: true };
47+
});
48+
4249
// IPC Handler: Select Whisper Model
4350
ipcMain.handle('select-whisper-model', async () => {
4451
try {
@@ -70,6 +77,32 @@ ipcMain.handle('select-whisper-model', async () => {
7077
}
7178
});
7279

80+
// IPC Handler: Save Subtitle Dialog
81+
ipcMain.handle('save-subtitle-dialog', async (_event, defaultName: string, content: string, format: 'srt' | 'ass') => {
82+
try {
83+
const result = await dialog.showSaveDialog({
84+
title: '保存字幕文件',
85+
defaultPath: defaultName,
86+
filters: [
87+
{ name: format.toUpperCase() + ' 字幕', extensions: [format] },
88+
{ name: '所有文件', extensions: ['*'] }
89+
]
90+
});
91+
92+
if (!result.canceled && result.filePath) {
93+
// Ensure Windows line endings
94+
const windowsContent = content.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
95+
const bom = '\uFEFF'; // UTF-8 BOM
96+
await fs.promises.writeFile(result.filePath, bom + windowsContent, 'utf-8');
97+
return { success: true, path: result.filePath };
98+
}
99+
return { success: false, canceled: true };
100+
} catch (error: any) {
101+
console.error('[Main] Save subtitle failed:', error);
102+
return { success: false, error: error.message };
103+
}
104+
});
105+
73106
// IPC Handler: Save Logs Dialog
74107
ipcMain.handle('save-logs-dialog', async (_event, content: string) => {
75108
try {
@@ -129,6 +162,17 @@ ipcMain.handle('read-extracted-audio', async (_event, audioPath: string) => {
129162
}
130163
});
131164

165+
// IPC Handler: 读取音频文件 (Fallback)
166+
ipcMain.handle('read-audio-file', async (_event, filePath: string) => {
167+
try {
168+
const buffer = await fs.promises.readFile(filePath);
169+
return buffer.buffer;
170+
} catch (error: any) {
171+
console.error('[Main] Failed to read audio file:', error);
172+
return { success: false, error: error.message };
173+
}
174+
});
175+
132176
// IPC Handler: 清理临时音频文件
133177
ipcMain.handle('cleanup-temp-audio', async (_event, audioPath: string) => {
134178
try {
@@ -178,8 +222,39 @@ const originalConsoleError = console.error;
178222

179223
function addLog(message: string) {
180224
const timestamp = new Date().toLocaleTimeString();
181-
const logLine = `[${timestamp}] ${message}`;
182-
originalConsoleLog(logLine); // Use original console log to avoid recursion
225+
226+
// Parse log level from message prefix
227+
let level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' = 'INFO';
228+
let cleanMessage = message;
229+
230+
if (message.startsWith('[DEBUG]')) {
231+
level = 'DEBUG';
232+
cleanMessage = message.substring(7).trim(); // Remove '[DEBUG]'
233+
} else if (message.startsWith('[INFO]')) {
234+
level = 'INFO';
235+
cleanMessage = message.substring(6).trim(); // Remove '[INFO]'
236+
} else if (message.startsWith('[WARN]')) {
237+
level = 'WARN';
238+
cleanMessage = message.substring(6).trim(); // Remove '[WARN]'
239+
} else if (message.startsWith('[ERROR]')) {
240+
level = 'ERROR';
241+
cleanMessage = message.substring(7).trim(); // Remove '[ERROR]'
242+
}
243+
244+
// Format with level prefix for UI
245+
const logLine = `[${timestamp}] [${level}] ${cleanMessage}`;
246+
247+
// Use original console to avoid recursion, but use appropriate level
248+
if (level === 'DEBUG') {
249+
originalConsoleLog(logLine);
250+
} else if (level === 'INFO') {
251+
originalConsoleLog(logLine);
252+
} else if (level === 'WARN') {
253+
originalConsoleWarn(logLine);
254+
} else if (level === 'ERROR') {
255+
originalConsoleError(logLine);
256+
}
257+
183258
BrowserWindow.getAllWindows().forEach((win) => {
184259
win.webContents.send('new-log', logLine);
185260
});
@@ -208,6 +283,13 @@ const createWindow = () => {
208283
width: 1200,
209284
height: 800,
210285
icon: path.join(__dirname, '../resources/icon.png'),
286+
backgroundColor: '#1a0f2e', // 深紫色背景,与主界面协调
287+
titleBarStyle: 'hidden', // 必须设置为hidden才能使用titleBarOverlay
288+
titleBarOverlay: {
289+
color: '#1a0f2e', // 深紫色标题栏
290+
symbolColor: '#ffffff', // 白色控制按钮
291+
height: 33
292+
},
211293
webPreferences: {
212294
preload: path.join(__dirname, '../dist-electron/preload.cjs'),
213295
nodeIntegration: false,
@@ -286,7 +368,12 @@ const createMenu = () => {
286368
}
287369

288370
const menu = Menu.buildFromTemplate(template);
289-
Menu.setApplicationMenu(menu);
371+
// 只在开发环境显示菜单栏,生产环境隐藏以避免白色菜单栏的视觉违和感
372+
if (!app.isPackaged) {
373+
Menu.setApplicationMenu(menu);
374+
} else {
375+
Menu.setApplicationMenu(null);
376+
}
290377
};
291378

292379
app.on('ready', async () => {

electron/preload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
1616
selectWhisperModel: () => ipcRenderer.invoke('select-whisper-model'),
1717
transcribeLocal: (data: { audioData: ArrayBuffer, modelPath: string, language?: string, threads?: number }) =>
1818
ipcRenderer.invoke('transcribe-local', data),
19+
abortLocalWhisper: () => ipcRenderer.invoke('local-whisper-abort'),
1920

2021
// FFmpeg APIs
2122
extractAudioFFmpeg: (videoPath: string, options?: any) =>

electron/services/ffmpegAudioExtractor.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ import os from 'os';
66
import fs from 'fs';
77

88
// 设置 FFmpeg 路径
9-
ffmpeg.setFfmpegPath(ffmpegPath.path);
10-
ffmpeg.setFfprobePath(ffprobePath.path);
9+
const fixPathForAsar = (pathStr: string) => {
10+
return pathStr.replace('app.asar', 'app.asar.unpacked');
11+
};
12+
13+
ffmpeg.setFfmpegPath(fixPathForAsar(ffmpegPath.path));
14+
ffmpeg.setFfprobePath(fixPathForAsar(ffprobePath.path));
1115

1216
export interface AudioExtractionOptions {
1317
format?: 'wav' | 'mp3' | 'flac';

electron/services/localWhisper.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export class LocalWhisperService {
116116
'-oj', // Output JSON
117117
'-l', language,
118118
'-t', threads.toString(),
119-
'-np' // No print
119+
// '-np' // No print - Removed to capture stdout
120120
];
121121

122122
if (onLog) onLog(`[LocalWhisper] Spawning (Job ${jobId}): ${binaryPath} ${args.join(' ')}`);
@@ -126,12 +126,41 @@ export class LocalWhisperService {
126126
const process = spawn(binaryPath, args);
127127
this.activeProcesses.set(jobId, process);
128128

129+
let stdout = '';
129130
let stderr = '';
131+
let stdoutBuffer = '';
132+
let stderrBuffer = '';
133+
134+
process.stdout?.on('data', (data) => {
135+
const chunk = data.toString();
136+
stdout += chunk;
137+
stdoutBuffer += chunk;
138+
139+
const lines = stdoutBuffer.split('\n');
140+
stdoutBuffer = lines.pop() || ''; // Keep the last incomplete line
141+
142+
lines.forEach(line => {
143+
if (line.trim()) {
144+
// Intermediate output from stdout (if any) -> DEBUG
145+
if (onLog) onLog(`[DEBUG] [Whisper CLI] ${line}`);
146+
}
147+
});
148+
});
130149

131150
process.stderr?.on('data', (data) => {
132-
const msg = data.toString();
133-
stderr += msg;
134-
if (onLog) onLog(`[Whisper CLI] ${msg}`);
151+
const chunk = data.toString();
152+
stderr += chunk;
153+
stderrBuffer += chunk;
154+
155+
const lines = stderrBuffer.split('\n');
156+
stderrBuffer = lines.pop() || ''; // Keep the last incomplete line
157+
158+
lines.forEach(line => {
159+
if (line.trim()) {
160+
// Intermediate output from stderr (progress) -> DEBUG
161+
if (onLog) onLog(`[DEBUG] [Whisper CLI Info] ${line}`);
162+
}
163+
});
135164
});
136165

137166
process.on('close', async (code) => {
@@ -157,6 +186,11 @@ export class LocalWhisperService {
157186
}
158187

159188
const jsonContent = await fs.promises.readFile(outputPath, 'utf-8');
189+
190+
// Log the raw JSON content (or a summary if too large, but user asked for "all")
191+
console.log(`[LocalWhisper] JSON Output: ${jsonContent}`);
192+
if (onLog) onLog(`[LocalWhisper] JSON Output: ${jsonContent}`);
193+
160194
const result = JSON.parse(jsonContent);
161195

162196
const subtitles: SubtitleItem[] = (result.transcription || []).map((item: any) => ({

electron/services/storage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { app } from 'electron';
22
import path from 'path';
33
import fs from 'fs';
44

5-
const SETTINGS_FILE = 'settings.json';
5+
const SETTINGS_FILE = 'gemini-subtitle-pro-settings.json';
66

77
export class StorageService {
88
private filePath: string;

package.json

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "AI-powered subtitle generation tool",
44
"author": "Corvo007",
55
"private": true,
6-
"version": "2.2.2",
6+
"version": "2.3.0",
77
"type": "module",
88
"main": "dist-electron/main.cjs",
99
"scripts": {
@@ -67,11 +67,6 @@
6767
"node_modules/@ffprobe-installer/**/*"
6868
],
6969
"extraResources": [],
70-
"nsis": {
71-
"artifactName": "Gemini-Subtitle-Pro-Setup-${version}.${ext}",
72-
"oneClick": false,
73-
"perMachine": false
74-
},
7570
"portable": {
7671
"artifactName": "Gemini-Subtitle-Pro-${version}.${ext}"
7772
},
@@ -89,15 +84,15 @@
8984
},
9085
"win": {
9186
"target": [
92-
"nsis",
9387
"portable"
9488
],
9589
"extraResources": [
9690
{
97-
"from": "resources/whisper-cli.exe",
98-
"to": "whisper-cli.exe",
91+
"from": "resources",
92+
"to": ".",
9993
"filter": [
100-
"**/*"
94+
"whisper-cli.exe",
95+
"*.dll"
10196
]
10297
}
10398
]

resources/icon.png

-885 KB
Loading

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ export default function App() {
266266
chunkProgress={workspace.chunkProgress}
267267
status={workspace.status}
268268
startTime={workspace.startTime || 0}
269+
onShowLogs={() => setShowLogs(true)}
270+
onCancel={workspace.cancelOperation}
269271
/>
270272
<ToastContainer toasts={toasts} removeToast={removeToast} />
271273
<SimpleConfirmationModal

src/components/editor/BatchHeader.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@ export const BatchHeader: React.FC<BatchHeaderProps> = ({
4040
<div className="flex items-center space-x-2">
4141
<div className="text-xs text-slate-500 font-mono mr-2 hidden sm:block">已选 {selectedBatches.size}</div>
4242
{file && (
43-
<button onClick={() => handleBatchAction('fix_timestamps')} disabled={selectedBatches.size === 0} title="修复时间轴 (保留翻译)" className={`flex items-center space-x-1.5 px-3 py-1.5 rounded-md text-xs font-bold transition-all shadow-sm border ${selectedBatches.size > 0 ? 'bg-slate-700 border-slate-600 text-emerald-400 hover:bg-slate-600 hover:border-emerald-400/50' : 'bg-slate-800 border-slate-800 text-slate-600 cursor-not-allowed'}`}>
44-
<Clock className="w-3 h-3" /><span className="hidden sm:inline">修复时间</span>
43+
<button onClick={() => handleBatchAction('fix_timestamps')} disabled={selectedBatches.size === 0} title="校对时间轴 (保留翻译)" className={`flex items-center space-x-1.5 px-3 py-1.5 rounded-md text-xs font-bold transition-all shadow-sm border ${selectedBatches.size > 0 ? 'bg-slate-700 border-slate-600 text-emerald-400 hover:bg-slate-600 hover:border-emerald-400/50' : 'bg-slate-800 border-slate-800 text-slate-600 cursor-not-allowed'}`}>
44+
<Clock className="w-3 h-3" /><span className="hidden sm:inline">校对时间轴</span>
4545
</button>
4646
)}
4747

48-
<button onClick={() => handleBatchAction('proofread')} disabled={selectedBatches.size === 0} title="校对翻译 (保留时间轴)" className={`flex items-center space-x-1.5 px-3 py-1.5 rounded-md text-xs font-bold transition-all shadow-sm border ${selectedBatches.size > 0 ? 'bg-indigo-600 border-indigo-500 text-white hover:bg-indigo-500' : 'bg-slate-800 border-slate-800 text-slate-600 cursor-not-allowed'}`}>
49-
<Sparkles className="w-3 h-3" /><span className="hidden sm:inline">校对</span>
48+
<button onClick={() => handleBatchAction('proofread')} disabled={selectedBatches.size === 0} title="润色翻译 (保留时间轴)" className={`flex items-center space-x-1.5 px-3 py-1.5 rounded-md text-xs font-bold transition-all shadow-sm border ${selectedBatches.size > 0 ? 'bg-indigo-600 border-indigo-500 text-white hover:bg-indigo-500' : 'bg-slate-800 border-slate-800 text-slate-600 cursor-not-allowed'}`}>
49+
<Sparkles className="w-3 h-3" /><span className="hidden sm:inline">润色翻译</span>
5050
</button>
5151
</div>
5252
</div>

0 commit comments

Comments
 (0)