s / subtitle_v2.ts
stnh70's picture
Rename subtitle.ts to subtitle_v2.ts
a553f05 verified
import {WebSocketServer} from "https://deno.land/x/websocket@v0.1.4/mod.ts";
import {LRU} from "https://deno.land/x/lru@1.0.2/mod.ts";
import {franc} from 'https://esm.sh/franc-min@6.1.0';
// 全局配置 (确保这些环境变量都已设置)
const DEEPLX_TOKEN = Deno.env.get("DEEPLX_TOKEN");
const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");
const OPENAI_API_URL = Deno.env.get("OPENAI_API_URL");
const OPENAI_MODEL = Deno.env.get("OPENAI_MODEL");
if (!DEEPLX_TOKEN) {
throw new Error("缺少DEEPLX_TOKEN环境变量");
}
if (!OPENAI_API_KEY) {
throw new Error("缺少OPENAI_API_KEY环境变量");
}
if (!OPENAI_API_URL) {
throw new Error("缺少OPENAI_API_URL环境变量");
}
if (!OPENAI_MODEL) {
throw new Error("缺少OPENAI_MODEL环境变量");
}
// 全局配置
const config = {
// DeepLX 配置
DEEPLX_API_URL: "https://api.deeplx.org/" + DEEPLX_TOKEN + "/translate",
// OpenAI 配置
OPENAI_API_URL: OPENAI_API_URL + "/v1/chat/completions",
OPENAI_MODEL: OPENAI_MODEL,
BATCH_SIZE: 15,
SUBTITLE_SEPARATOR: "\n",
SUBTITLE_MARKER: "‖",
OPTIMAL_TEXT_LENGTH: 1000,
DELAY_BETWEEN_REQUESTS: 1000,
INITIAL_BATCH_SIZE: 20,
SEND_REPORTS: false,
ALERT_THRESHOLD: 1000, // 毫秒
NTFY_TOPIC: "aston",
NTFY_URL: "https://ntfy.sh/aston",
LANGUAGE_DETECTION_SAMPLE_SIZE: 10,
LANGUAGE_DETECTION_THRESHOLD: 7,
USE_ADAPTIVE_RATE_LIMITER: true, // 新增:控制是否使用AdaptiveRateLimiter
// !!! OpenAI 的专属高效配置 !!!
//OpenAI翻译参数配置
MAXWORKERS: 3,
CHATGPT_REQUEST_INTERVAL: 300, // 新增:请求间隔,
CHATGPT_MAX_LINES_PER_REQUEST: 25, // <--- 一次请求最多处理行字幕
CHATGPT_MAX_CHARS_PER_REQUEST: 1500, // 安全上限1500字符
CHATGPT_SAFE_SEPARATOR: "‖", // 使用一个自然、健壮的分隔符
};
// 语言代码映射
const languageCodeMapping: {
[key: string]: string
} = {
'cmn': 'ZH', // 简体中文
'zho': 'ZH', // 中文(通用)
'yue': 'ZH-TW', // 粤语,映射到繁体中文
'eng': 'EN', // 英语
'jpn': 'JA', // 日语
'kor': 'KO', // 韩语
'fra': 'FR', // 法语
'deu': 'DE', // 德语
'spa': 'ES', // 西班牙语
'rus': 'RU', // 俄语
'por': 'PT', // 葡萄牙语
'ita': 'IT', // 意大利语
'nld': 'NL', // 荷兰语
'pol': 'PL', // 波兰语
'bul': 'BG', // 保加利亚语
'ces': 'CS', // 捷克语
'dan': 'DA', // 丹麦语
'ell': 'EL', // 希腊语
'est': 'ET', // 爱沙尼亚语
'fin': 'FI', // 芬兰语
'hun': 'HU', // 匈牙利语
'ind': 'ID', // 印度尼西亚语
'lit': 'LT', // 立陶宛语
'lav': 'LV', // 拉脱维亚语
'nob': 'NB', // 挪威语(博克马尔语)
'nno': 'NB', // 挪威语(尼诺斯克语)
'ron': 'RO', // 罗马尼亚语
'slk': 'SK', // 斯洛伐克语
'slv': 'SL', // 斯洛文尼亚语
'swe': 'SV', // 瑞典语
'tur': 'TR', // 土耳其语
'ukr': 'UK', // 乌克兰语
};
// 缓存配置
const translationCache = new LRU<string,
string>(1000);
// 接口定义
interface SubtitleEntry {
id: string;
startTime: number;
endTime: number;
text: string;
translatedText?: string;
originalSubtitles?: SubtitleEntry[];
}
class AdaptiveRateLimiter {
private queue: Array<{
fn: () => Promise<any>,
resolve: (value: any) => void,
reject: (reason?: any) => void
}> = [];
private running = 0;
private maxConcurrent = 8;
private minInterval = 1000; // ms
private lastRunTime = 0;
async schedule<T>(fn: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push({
fn,
resolve,
reject
});
this.runNext();
});
}
private async runNext() {
if (this.running >= this.maxConcurrent || this.queue.length === 0) return;
const now = Date.now();
if (now - this.lastRunTime < this.minInterval) {
setTimeout(() => this.runNext(), this.minInterval - (now - this.lastRunTime));
return;
}
this.running++;
this.lastRunTime = now;
const next = this.queue.shift();
if (next) {
try {
console.log(`[AdaptiveRateLimiter] 开始执行请求`);
const result = await next.fn();
console.log(`[AdaptiveRateLimiter] 完成请求`);
next.resolve(result);
} catch (err) {
if (err.status === 429) { // Too Many Requests
this.adjustLimits();
}
next.reject(err);
} finally {
this.running--;
this.runNext();
}
}
}
private adjustLimits() {
this.maxConcurrent = Math.max(1, this.maxConcurrent - 1);
this.minInterval += 500;
console.log(`[AdaptiveRateLimiter] Adjusted limits: maxConcurrent=${this.maxConcurrent}, minInterval=${this.minInterval}ms`);
}
}
const rateLimiter = new AdaptiveRateLimiter();
// 性能监控模块
class PerformanceMonitor {
private samples: Map<string, number[]> = new Map();
private readonly sampleSize = 10;
private dailyData: number[] = [];
private updateMetric(metric: string, value: number) {
if (!this.samples.has(metric)) {
this.samples.set(metric, []);
}
const samples = this.samples.get(metric)!;
samples.push(value);
if (samples.length > this.sampleSize) {
samples.shift();
}
}
private getAverageMetric(metric: string): number {
const samples = this.samples.get(metric) || [];
if (samples.length === 0) return 0;
return samples.reduce((a, b) => a + b, 0) / samples.length;
}
updateApiResponseTime(responseTime: number) {
this.updateMetric('apiResponseTime', responseTime);
this.dailyData.push(responseTime);
}
getAverageApiResponseTime(): number {
return this.getAverageMetric('apiResponseTime');
}
getDailyData(): number[] {
return [...this.dailyData];
}
clearDailyData() {
this.dailyData = [];
}
logPerformanceMetrics() {
console.log(`[Performance] Average API Response Time: ${this.getAverageApiResponseTime().toFixed(2)}ms`);
}
}
const performanceMonitor = new PerformanceMonitor();
// 性能分析器
class PerformanceAnalyzer {
static async analyzeDailyPerformance(performanceData: number[]) {
const avgResponseTime = performanceData.reduce((a, b) => a + b, 0) / performanceData.length;
const maxResponseTime = Math.max(...performanceData);
console.log(`每日性能报告:`);
console.log(`平均响应时间:${avgResponseTime.toFixed(2)}ms`);
console.log(`最大响应时间:${maxResponseTime}ms`);
if (config.SEND_REPORTS) {
await this.sendAlert(`每日性能报告:平均响应时间:${avgResponseTime.toFixed(2)}ms,最大响应时间:${maxResponseTime}ms`);
}
if (avgResponseTime > config.ALERT_THRESHOLD) {
await this.sendAlert(`警告:平均响应时间 (${avgResponseTime.toFixed(2)}ms) 超过阈值`);
}
}
private static async sendAlert(message: string) {
const title = "字幕翻译服务性能报告";
try {
const response = await fetch(config.NTFY_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
topic: config.NTFY_TOPIC,
title: title,
message: message,
priority: 3,
}),
});
if (response.ok) {
console.log("警报发送成功");
} else {
console.error("发送警报失败:", response.statusText);
}
} catch (error) {
console.error("发送警报时出错:", error);
}
}
}
//新增
type TranslationEngine = 'deeplx' | 'chatgpt';
async function translateText(
text: string,
sourceLanguage: string,
targetLanguage: string,
subtitleId: string,
signal: AbortSignal,
engine: TranslationEngine = 'deeplx' // 默认使用 deeplx
): Promise<string> {
switch (engine) {
case 'chatgpt':
console.log(`[Dispatcher] 使用 ChatGPT 引擎翻译 (ID: ${subtitleId})`);
return await translateWithChatGPT(text, sourceLanguage, targetLanguage, subtitleId, signal, 0);
case 'deeplx':
default:
console.log(`[Dispatcher] 使用 DeepLX 引擎翻译 (ID: ${subtitleId})`);
return await translateWithDeepLX(text, sourceLanguage, targetLanguage, subtitleId, signal);
}
}
// ========================================================================
// 最终的、完整的、带 minInterval 的、绝对正确的 WorkerPool
// ========================================================================
class ChatGPT_WorkerPool {
private queue: Array<{
task: { text: string; sourceLanguage: string; targetLanguage: string; subtitleId: string; signal: AbortSignal };
resolve: (value: string) => void;
reject: (reason?: any) => void;
priority: number;
}> = [];
// --- 我们的两个核心控制旋钮 ---
private maxWorkers: number;
private minInterval: number; // 两个请求启动之间的最小间隔(毫秒)
private runningWorkers = 0;
private lastRunTime = 0; // 记录上一个任务的启动时间
constructor() {
// 在构造函数中,从 config 初始化参数
if (config.OPENAI_MODEL.includes('glm-')) {
config.MAXWORKERS = 2;
}
this.maxWorkers = config.MAXWORKERS || 3; // 如果未定义,默认为3
this.minInterval = config.CHATGPT_REQUEST_INTERVAL || 300; // 默认为300ms
console.log(`[WorkerPool] 初始化完成,最大并发数: ${this.maxWorkers}, 最小请求间隔: ${this.minInterval}ms`);
}
public translate(task: { text: string; sourceLanguage: string; targetLanguage: string; subtitleId: string; signal: AbortSignal }, priority: number = 0): Promise<string> {
return new Promise((resolve, reject) => {
// this.queue.push({ task, resolve, reject });
const queueItem = { task, resolve, reject, priority };
this.insertByPriority(queueItem);
this.tryStartWorker();
});
}
private insertByPriority(item: any) {
let insertIndex = this.queue.length;
for (let i = 0; i < this.queue.length; i++) {
if (this.queue[i].priority < item.priority) {
insertIndex = i;
break;
}
}
this.queue.splice(insertIndex, 0, item);
}
// private insertByPriority(item: any) {
// let insertIndex = this.queue.length;
// for (let i = 0; i < this.queue.length; i++) {
// if (this.queue[i].priority < item.priority) {
// insertIndex = i;
// break;
// }
// }
// this.queue.splice(insertIndex, 0, item);
// }
private tryStartWorker() {
// 如果工人已满或队列为空,则不执行
if (this.runningWorkers >= this.maxWorkers || this.queue.length === 0) {
return;
}
const now = Date.now();
const elapsed = now - this.lastRunTime;
// 检查时间间隔
if (elapsed < this.minInterval) {
// 如果间隔不够,则安排一个延时器再次尝试,然后退出
setTimeout(() => this.tryStartWorker(), this.minInterval - elapsed);
return;
}
this.runningWorkers++;
this.lastRunTime = now; // 记录本次启动时间
const { task, resolve, reject } = this.queue.shift()!;
console.log(`[WorkerPool] 🚀 翻译启动, ID: ${task.subtitleId}. (正在运行: ${this.runningWorkers}, 队列剩余: ${this.queue.length})`);
// 异步地执行这个工人的任务
this.singleTranslationTask(task)
.then(resolve)
.catch(reject)
.finally(() => {
this.runningWorkers--;
// 一个工人完成后,立刻尝试启动下一个
console.log(`[WorkerPool] ✅ 翻译完成, ID: ${task.subtitleId}. (正在运行: ${this.runningWorkers})`);
this.tryStartWorker();
});
}
// singleTranslationTask 负责单次翻译,包含了超时和重试逻辑
private async singleTranslationTask(
task: { text: string; sourceLanguage: string; targetLanguage: string; subtitleId: string; signal: AbortSignal },
retryCount = 0
): Promise<string> {
const MAX_RETRIES = 5;
const REQUEST_TIMEOUT = 30000;
if (task.signal.aborted) throw new Error("Aborted by user signal");
// === 添加缓存逻辑 ===
try {
const textHash = await sha256(task.text.trim());
const cacheKey = `chatgpt-worker-${task.sourceLanguage}-${task.targetLanguage}-${textHash}`;
const cachedResult = translationCache.get(cacheKey);
if (cachedResult) {
console.log(`[WorkerPool-Cache] HIT for task ${task.subtitleId}`);
return cachedResult;
}
console.log(`[WorkerPool-Cache] MISS for task ${task.subtitleId}`);
} catch (cacheError) {
console.warn(`[WorkerPool-Cache] Cache check failed: ${cacheError.message}`);
}
// === 缓存逻辑结束 ===
let systemPrompt = '';
if (config.OPENAI_MODEL.includes('glm-')) {
systemPrompt = `You are a text translation API. Your task is to translate the user's text from ${task.sourceLanguage} to ${task.targetLanguage}.
RULES:
1. Translate the text inside the square brackets for each numbered item.
2. Your response MUST be a numbered list with the exact same structure and the exact same number of items.
3. Preserve the separator "${config.CHATGPT_SAFE_SEPARATOR}" between each translated item.
4. Your output must ONLY be the translated numbered list.
`;
} else {
systemPrompt = `You are a text translation API. Your task is to translate the user's text from ${task.sourceLanguage} to ${task.targetLanguage}.
CRITICAL RULES:
1. Each item is strictly defined as "number. [content]". Treat every item as atomic and indivisible.
2. Translate ONLY the text inside [ ], keep numbers, brackets, punctuation, and the separator "${config.CHATGPT_SAFE_SEPARATOR}" EXACTLY unchanged.
3. Even if an item is a sentence fragment or looks incomplete, you MUST translate it literally and output it as a separate item. NEVER merge or combine with other items.
4. Input line N → Output line N. The number of items in the output MUST match the input EXACTLY.
5. Before finalizing, COUNT your output. If input has 25 items, output must also have 25 items. If not, FIX it.
WARNING: Merging, skipping, or changing item count will cause system failure.
EXAMPLE:
Input:
6. [dooming us to]‖7. [spend our lives in search of the other half.]
Output:
6. [注定我们要]‖7. [用一生去寻找另一半。]
`;
}
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(new Error("Request timed out after 30s")), REQUEST_TIMEOUT);
const onAbort = () => {
controller.abort();
task.signal.removeEventListener('abort', onAbort);
};
task.signal.addEventListener('abort', onAbort, { once: true });
if (task.signal.aborted) throw new Error("Aborted before fetch");
const response = await fetch(config.OPENAI_API_URL, {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${OPENAI_API_KEY}` },
body: JSON.stringify({
model: config.OPENAI_MODEL,
messages: [{ role: "system", content: systemPrompt }, { role: "user", content: task.text }],
temperature: 0.2,
stream: false,
}),
signal: controller.signal,
});
clearTimeout(timeoutId);
task.signal.removeEventListener('abort', onAbort);
if (!response.ok) {
if ([429, 500, 502, 503, 504].includes(response.status)) {
throw new Error("RETRYABLE_PROXY_ERROR");
}
const errorBody = await response.text();
throw new Error(`API Error: ${response.status}, Body: ${errorBody.substring(0, 100)}`);
}
const result = await response.json();
const translation = result.choices[0]?.message?.content?.trim();
// === 缓存成功结果 ===
if (translation && task.text.trim().length > 0) {
try {
const textHash = await sha256(task.text.trim());
const cacheKey = `chatgpt-worker-${task.sourceLanguage}-${task.targetLanguage}-${textHash}`;
translationCache.set(cacheKey, translation);
console.log(`[WorkerPool-Cache] STORED result for task ${task.subtitleId}`);
} catch (cacheError) {
console.warn(`[WorkerPool-Cache] Failed to cache result: ${cacheError.message}`);
}
}
// === 缓存逻辑结束 ===
return translation;
} catch (error) {
if (task.signal.aborted) throw error;
if ((error.message === "RETRYABLE_PROXY_ERROR" || error.message === "EMPTY_RESPONSE" || error.message.includes("timed out")||
(error instanceof TypeError && error.message.includes("fetch"))) && retryCount < MAX_RETRIES) {
const delay = Math.pow(2, retryCount) * 1500 + Math.random() * 1000;
console.warn(`[WorkerPool] ⚠️ 重试 ID: ${task.subtitleId} (第 ${retryCount + 1} 次), 原因: ${error.message}. 等待 ${delay.toFixed(0)}ms...`);
await new Promise(r => setTimeout(r, delay));
return this.singleTranslationTask(task, retryCount + 1);
}
// console.error(`[Worker] Final failure for ID: ${task.subtitleId}`, error.message);
console.error(`[WorkerPool] ❌ 最终失败, ID: ${task.subtitleId}. 原因: ${error.message}`);
throw error;
}
}
}
// ========================================================================
// 继承了 ChatGPT_WorkerPool 的新类
// ========================================================================
class DeduplicatedWorkerPool extends ChatGPT_WorkerPool {
private pendingRequests = new Map<string, Promise<string>>();
// 重写父类的公共接口
public async translate(task: {
text: string;
sourceLanguage: string;
targetLanguage: string;
subtitleId: string;
signal: AbortSignal
}, priority = 0): Promise<string> {
const taskKey = `${task.sourceLanguage}-${task.targetLanguage}-${await sha256(task.text.trim())}`;
if (this.pendingRequests.has(taskKey)) {
console.log(`[Dedup] HIT: Reusing in-flight request for ID: ${task.subtitleId}`);
return this.pendingRequests.get(taskKey)!;
}
// console.log(`[Dedup] MISS: Creating new translation task for ID: ${task.subtitleId}`);
// !!! 调用父类的原始方法来执行真正的任务 !!!
const promise = super.translate(task, priority);
this.pendingRequests.set(taskKey, promise);
promise.finally(() => {
// 任务完成后,无论成功失败,都从清单中删除
this.pendingRequests.delete(taskKey);
});
return promise;
}
}
// 实例化部分保持不变
const chatGptWorkerPool = new DeduplicatedWorkerPool();
// 这是一个辅助函数,用来将字符串异步地转换为SHA-256哈希
async function sha256(str: string): Promise<string> {
const buffer = new TextEncoder().encode(str);
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
// 将哈希字节转换为十六进制字符串
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
async function translateWithChatGPT(
text: string,
sourceLanguage: string,
targetLanguage: string,
subtitleId: string, // subtitleId 依然传入,用于日志和传递
signal: AbortSignal,
priority = 0
): Promise<string> {
const translatedText = await chatGptWorkerPool.translate({
text,
sourceLanguage,
targetLanguage,
subtitleId,
signal
}, priority);
return translatedText;
}
// 作为“总指挥”的 batchTranslateWithChatGPT !!!
async function batchTranslateWithChatGPT(
subtitlesToTranslate: SubtitleEntry[],
sourceLanguage: string,
targetLanguage: string,
signal: AbortSignal,
onBatchComplete: (translatedBatch: SubtitleEntry[]) => void
) {
// 1. 智能分批
const batches: SubtitleEntry[][] = [];
let currentBatch: SubtitleEntry[] = [];
let currentCharCount = 0;
if (config.OPENAI_MODEL.includes('glm-')) {
config.CHATGPT_MAX_LINES_PER_REQUEST = 50;
}
for (const subtitle of subtitlesToTranslate) {
if (currentBatch.length > 0 && (
currentBatch.length >= config.CHATGPT_MAX_LINES_PER_REQUEST ||
(config.CHATGPT_MAX_CHARS_PER_REQUEST && currentCharCount + subtitle.text.length > config.CHATGPT_MAX_CHARS_PER_REQUEST)
)) {
batches.push(currentBatch);
currentBatch = [];
currentCharCount = 0;
}
currentBatch.push(subtitle);
currentCharCount += subtitle.text.length;
}
if (currentBatch.length > 0) batches.push(currentBatch);
console.log(`[Director] 字幕被分成了 ${batches.length} 个初始批次。`);
// 2. 并行处理所有顶层批次
await Promise.all(batches.map(async (batch, i) => {
if (signal.aborted) return;
// 为每个批次,都启动一次我们的“智能分形火箭”
const translatedBatch = await processSingleBatchWithSmartFallback(batch, sourceLanguage, targetLanguage, signal);
// 实时发送结果
if (onBatchComplete) { // 确保回调函数存在
onBatchComplete(translatedBatch);
}
}));
}
// ========================================================================
// !!! 优化版"智能急诊室" - 更安全、更可靠的版本 !!!
// ========================================================================
async function processSingleBatchWithSmartFallback_(
batch: SubtitleEntry[],
sourceLanguage: string,
targetLanguage: string,
signal: AbortSignal,
depth = 0
): Promise<SubtitleEntry[]> {
const batchId = batch.length > 0 ? `${batch[0].id}...` : 'empty';
console.log(`[SmartFallback] 正在处理批次大小为 ${batch.length} 行,深度为 ${depth} (ID: ${batchId})`);
// 1. --- 安全阀:防止无限递归(降低到5层) ---
if (depth > 5) {
console.error(`[SmartFallback] Max recursion depth reached for ${batchId}. Switching to final line-by-line.`);
return fallbackLineByLine(batch, sourceLanguage, targetLanguage, signal);
}
// 2. --- 优化的递归终点:批次大小为3或更小 ---
if (batch.length <= 3) {
// 当批次足够小时,直接用最可靠的"按行翻译",避免过度递归
console.log(`[SmartFallback] Small batch (${batch.length} items), using line-by-line translation`);
return fallbackLineByLine(batch, sourceLanguage, targetLanguage, signal);
}
// 3. --- 特殊处理:空批次 ---
if (batch.length === 0) {
console.log(`[SmartFallback] Empty batch, returning empty array`);
return [];
}
// --- 主流程:尝试"合并翻译" ---
try {
const mergedText = batch.map((s, index) =>
`${index + 1}. [${s.text.replace(/\n/g, " ")}]`
).join(config.CHATGPT_SAFE_SEPARATOR);
// 4. --- 调用翻译API ---
const translatedMergedText = await simpleTextTranslateWithChatGPT(
mergedText,
sourceLanguage,
targetLanguage,
batchId,
signal,
depth
);
// --- 5. 开始完善的"分诊"逻辑 ---
// 诊断 A: 是否是【空内容】?
if (translatedMergedText.trim() === "") {
console.warn(`[SmartFallback-Triage] Batch ${batchId} returned empty content. Splitting batch in half...`);
// 添加延迟以避免API限流(深度越深延迟越长)
if (depth > 1) {
const delayMs = 500 * depth;
console.log(`[SmartFallback] Adding ${delayMs}ms delay before splitting (depth: ${depth})`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
// !! 智能拆分逻辑 - 改为串行处理避免并发压力 !!
const mid = Math.ceil(batch.length / 2);
console.log(`[SmartFallback] Splitting batch ${batchId} into ${mid} + ${batch.length - mid} parts`);
// 串行处理,避免API压力过大
const firstHalf = await processSingleBatchWithSmartFallback(
batch.slice(0, mid),
sourceLanguage,
targetLanguage,
signal,
depth + 1
);
// 在处理第二部分前稍作延迟
await new Promise(resolve => setTimeout(resolve, 200));
const secondHalf = await processSingleBatchWithSmartFallback(
batch.slice(mid),
sourceLanguage,
targetLanguage,
signal,
depth + 1
);
return [...firstHalf, ...secondHalf];
}
let translatedParts = translatedMergedText.split(config.CHATGPT_SAFE_SEPARATOR);
const receivedCount = translatedParts.length;
// 诊断 B: 是否是【格式错误】?
if (receivedCount !== batch.length) {
const lineMatches = translatedMergedText.match(/\d+\.\s*\[?([^\]]*)\]?/g);
if (lineMatches && lineMatches.length === batch.length) {
translatedParts = lineMatches.map(match =>
match.replace(/^\d+\.\s*\[?/, '').replace(/\]?\s*$/, '').trim()
);
} else {
console.error(`[SmartFallback-Triage] Mismatch for ${batchId} (received: ${receivedCount}, expected: ${batch.length}). Activating final fallback.`);
console.error(`\n\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`);
console.error(`[CRIME SCENE] BATCH #${batchId} FAILED: Mismatch Detected!`);
console.error(`!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`);
console.error(` - Batch ID: ${batchId}`);
console.error(` - EXPECTED LINES: ${batch.length}`);
console.error(` - RECEIVED PARTS: ${receivedCount}`);
console.error("\n--- MERGED ORIGINAL TEXT (SENT TO API) ---");
console.error(mergedText);
console.error("\n--- RECEIVED TRANSLATED TEXT (FROM API) ---");
console.error(translatedMergedText);
console.error("\n--- SPLIT PARTS (FOR DEBUGGING) ---");
console.error(translatedParts);
console.error(`!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`);
console.error(`[ACTION] Activating line-by-line fallback for this batch...`);
console.error(`!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n\n`);
// 这是确定性的内容错误,必须立刻降级到最可靠的模式
return fallbackLineByLine(batch, sourceLanguage, targetLanguage, signal, 100);
}
}
// 诊断 C: 检查翻译质量(新增)
const hasValidTranslations = translatedParts.every(part => {
const cleaned = part.replace(/^\d+\.\s*\[?/, '').replace(/]?\s*$/, '').trim();
return cleaned.length > 0;
});
if (!hasValidTranslations) {
console.warn(`[SmartFallback-Triage] Poor quality translations detected for ${batchId}. Using line-by-line fallback.`);
return fallbackLineByLine(batch, sourceLanguage, targetLanguage, signal, 100);
}
// 6. --- 如果所有诊断都通过了,就是成功! ---
console.log(`[SmartFallback] ✅ 翻译成功 batch ${batchId} with merged translation.`);
return batch.map((original, index) => ({
...original,
translatedText: translatedParts[index]
.replace(/^\d+\.\s*\[?/, '')
.replace(/]?\s*$/, '')
.trim(),
}));
} catch (error) {
// 1. 首先,检查是不是用户主动中止的
if (error.name === 'AbortError' || error.message.includes('Aborted')) {
console.log(`[SmartFallback] 任务在 ${batchId} 处被中止。`);
// 如果是中止,我们不应该启动耗时的回退
// 而是直接把这个批次的所有字幕都标记为“中止”
return batch.map(sub => ({ ...sub, translatedText: `[翻译中止]` }));
}
// 7. --- 最终的捕获 ---
// 这个 catch 捕捉网络错误、API错误等不可恢复的情况
console.error(`[SmartFallback-Triage] Unrecoverable API error for ${batchId}: ${error.message}. Activating final fallback.`);
// 在降级前添加延迟,避免立即重试造成更多问题
await new Promise(resolve => setTimeout(resolve, 1000));
return fallbackLineByLine(batch, sourceLanguage, targetLanguage, signal, 100);
}
}
async function processSingleBatchWithSmartFallback(
batch: SubtitleEntry[],
sourceLanguage: string,
targetLanguage: string,
signal: AbortSignal,
depth = 0,
retryCount = 0
): Promise<SubtitleEntry[]> {
// 参数验证
if (!batch || !Array.isArray(batch)) {
throw new Error("Invalid batch parameter");
}
if (!sourceLanguage || !targetLanguage) {
throw new Error("Language parameters are required");
}
const batchId = batch.length > 0 ? `${batch[0].id} (size ${batch.length}, depth ${depth})` : 'empty';
console.log(`[SmartFallback] Processing batch: ${batchId}`);
// 动态深度计算 - 结合方案一的改进
const maxDepth = Math.min(4, Math.floor(Math.log2(batch.length)) + 2);
// --- 1. 递归的终点与安全阀 ---
if (batch.length <= 1 || depth > maxDepth || retryCount > 3) {
if (depth > maxDepth) console.error(`[SmartFallback] Max recursion depth reached for ${batchId}.`);
if (retryCount > 3) console.error(`[SmartFallback] Max retries exceeded for ${batchId}.`);
console.log(`[SmartFallback] Using line-by-line for ${batchId}.`);
return fallbackLineByLine(batch, sourceLanguage, targetLanguage, signal);
}
// --- 2. 主流程:永远先尝试最高效的"合并-序号"模式 ---
try {
const mergedText = batch.map((s, index) =>
`${index + 1}. [${s.text.replace(/\n/g, " ")}]`
).join(config.CHATGPT_SAFE_SEPARATOR);
const translatedMergedText = await simpleTextTranslateWithChatGPT(
mergedText, sourceLanguage, targetLanguage, batchId, signal, depth
);
// --- 3. 严格的质检 ---
if (!translatedMergedText || translatedMergedText.trim() === "") {
throw new Error("API returned empty content.");
}
const translatedParts = translatedMergedText.split(config.CHATGPT_SAFE_SEPARATOR);
if (translatedParts.length !== batch.length) {
console.warn(`[SmartFallback] Mismatch for ${batchId} (${translatedParts.length} vs ${batch.length}).`);
throw new Error("Mismatch detected.");
}
const formatRegex = /^\s*\d+\.\s*\[.*]\s*$/;
if (!translatedParts.every(p => formatRegex.test(p.trim()))) {
console.warn(`[SmartFallback] Format error detected in ${batchId}.`);
throw new Error("Format error detected.");
}
// --- 4. 如果所有检查都通过,就是成功! ---
console.log(`[SmartFallback] ✅ SUCCESS for batch: ${batchId}`);
return batch.map((original, index) => ({
...original,
translatedText: translatedParts[index].replace(/^\d+\.\s*\[?/, '').replace(/]?\s*$/, '').trim(),
}));
} catch (error) {
if (signal.aborted) return batch.map(s => ({...s, translatedText: "[翻译中止]"}));
console.warn(`[SmartFallback-Fallback] ⚠️⚠️ Batch ${batchId} failed: ${error.message}. Splitting and retrying with controlled concurrency.`);
// 动态延迟计算,结合深度和重试次数
const delayMs = Math.min(500 * (depth + 1) * (retryCount + 1), 5000);
await new Promise(resolve => setTimeout(resolve, delayMs));
const mid = Math.ceil(batch.length / 2);
// 使用受控的并行处理,添加延迟避免风暴
const firstHalf = await processSingleBatchWithSmartFallback(
batch.slice(0, mid),
sourceLanguage,
targetLanguage,
signal,
depth + 1,
retryCount + 1
);
// 在处理第二部分前,再次检查中止信号
if (signal.aborted) return [...firstHalf, ...batch.slice(mid).map(s => ({...s, translatedText: "[翻译中止]"}))];
// 动态调整延迟,既避免风暴又保持效率
const interBatchDelay = Math.min(1000 * (depth + 1), 3000);
await new Promise(resolve => setTimeout(resolve, interBatchDelay));
const secondHalf = await processSingleBatchWithSmartFallback(
batch.slice(mid),
sourceLanguage,
targetLanguage,
signal,
depth + 1,
retryCount + 1
);
return [...firstHalf, ...secondHalf];
}
}
// // 改进版本
// function improvedCleanup(text) {
// // 更强大的正则表达式,处理更多格式
// return text
// // 处理各种编号格式
// .replace(/^(\d+[.)\s]*|[①②③④⑤⑥⑦⑧⑨⑩]+|[一二三四五六七八九十]+[、.])\s*\[?/, '')
// // 处理结尾
// .replace(/\]?\s*$/, '')
// .trim();
// }
//调用WorkerPool的【纯文本】翻译函数
async function simpleTextTranslateWithChatGPT(
text: string,
sourceLanguage: string,
targetLanguage: string,
subtitleId: string,
signal: AbortSignal,
priority = 0
): Promise<string> {
return chatGptWorkerPool.translate({ text, sourceLanguage, targetLanguage, subtitleId, signal }, priority);
}
// 按行翻译的回退函数
async function fallbackLineByLine_old(
subtitles: SubtitleEntry[],
sourceLanguage: string,
targetLanguage: string,
signal: AbortSignal,
priority = 100
): Promise<SubtitleEntry[]> {
console.log(`[Fallback-Serial] 启动串行按行翻译,处理 ${subtitles.length} 行...`);
const translatedSubtitles: SubtitleEntry[] = [];
// !!! 使用【串行的 for...of 循环】,而不是并行的 Promise.all !!!
for (const subtitle of subtitles) {
// 在处理每一行之前,都检查一次中止信号
if (signal.aborted) {
console.log(`[Fallback-Serial] 翻译在 ${subtitle.id} 处被中止。`);
// 把剩下未翻译的,都标记为中止
translatedSubtitles.push({ ...subtitle, translatedText: `[翻译中止]` });
continue; // 继续循环,以填充完所有剩余的条目
}
try {
// 我们一次只 await 一行字幕的翻译
const translatedText = await simpleTextTranslateWithChatGPT(
subtitle.text,
sourceLanguage,
targetLanguage,
`${subtitle.id}-fallback`,
signal,
priority
);
translatedSubtitles.push({ ...subtitle, translatedText });
} catch (e) {
// 再次检查,因为错误可能就是 AbortError
if (signal.aborted) {
translatedSubtitles.push({ ...subtitle, translatedText: `[翻译中止]` });
} else {
translatedSubtitles.push({ ...subtitle, translatedText: `[翻译失败] ${subtitle.text}` });
}
}
}
return translatedSubtitles;
}
// ========================================================================
// !!! 最终的、兼顾了“并行性能”和“干净中止”的 fallbackLineByLine !!!
// ========================================================================
async function fallbackLineByLine(
subtitles: SubtitleEntry[],
sourceLanguage: string,
targetLanguage: string,
signal: AbortSignal,
priority = 100
): Promise<SubtitleEntry[]> {
console.log(`[Fallback-Parallel] 启动【并行】按行翻译,处理 ${subtitles.length} 行...`);
// 如果在启动前就已经被中止了,直接返回
if (signal.aborted) {
return subtitles.map(sub => ({ ...sub, translatedText: "[翻译中止]" }));
}
// 我们依然使用并行的 Promise.all 来获得最高性能
const translationPromises = subtitles.map(async (subtitle) => {
// 在 map 回调的开头,立刻检查一次
// 这可以防止已经中止后,还创建新的异步任务
if (signal.aborted) {
return { ...subtitle, translatedText: `[翻译中止]` };
}
try {
const translatedText = await simpleTextTranslateWithChatGPT(
subtitle.text,
sourceLanguage,
targetLanguage,
`${subtitle.id}-fallback`,
signal,
priority
);
return { ...subtitle, translatedText };
} catch (error) {
// !!! 核心修改:在这里也检查中止信号 !!!
if (signal.aborted || error.name === 'AbortError') {
return { ...subtitle, translatedText: `[翻译中止]` };
}
// 如果是其他错误,才标记为失败
return { ...subtitle, translatedText: `[翻译失败] ${subtitle.text}` };
}
});
// !!! Promise.all 本身不做任何修改 !!!
return Promise.all(translationPromises);
}
async function translateWithDeepLX(
text: string,
sourceLanguage: string,
targetLanguage: string,
subtitleId: string,
signal: AbortSignal
): Promise<string> {
const cacheKey = `${sourceLanguage}-${targetLanguage}-${subtitleId}-${text}`;
console.log(`[DeepLX] 尝试翻译文本 (subtitleId: ${subtitleId})`);
const cachedTranslation = translationCache.get(cacheKey);
if (cachedTranslation) {
console.log(`[DeepLX] 使用缓存的翻译结果 (subtitleId: ${subtitleId})`);
return cachedTranslation;
}
const translate = async () => {
if (signal.aborted) {
console.log(`[DeepLX] 翻译被中止 (subtitleId: ${subtitleId})`);
throw new Error("Translation aborted");
}
try {
console.log(`[DeepLX] 等待 ${config.DELAY_BETWEEN_REQUESTS}ms 后发送请求 (subtitleId: ${subtitleId})`);
await new Promise(resolve => setTimeout(resolve, config.DELAY_BETWEEN_REQUESTS));
console.log(`[DeepLX] 发送翻译请求到 API (subtitleId: ${subtitleId})`);
const startTime = Date.now();
const response = await fetch(config.DEEPLX_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
text,
source_lang: sourceLanguage,
target_lang: targetLanguage,
}),
signal,
});
const endTime = Date.now();
performanceMonitor.updateApiResponseTime(endTime - startTime);
if (!response.ok) {
if (response.status === 429) {
console.warn(`[DeepLX] 遇到限流,等待后重试 (subtitleId: ${subtitleId})`);
await new Promise(resolve => setTimeout(resolve, 5000));
return translateWithDeepLX(text, sourceLanguage, targetLanguage, subtitleId, signal);
}
throw new Error(`DeepLX API error: ${response.statusText}`);
}
const result = await response.json();
const translation = result.data;
console.log(`[DeepLX] 翻译成功: ${translation.substring(0, 50)}... (subtitleId: ${subtitleId})`);
const markerRegex = new RegExp(`${config.SUBTITLE_MARKER}.*?${config.SUBTITLE_MARKER}`, 'g');
const markers = text.match(markerRegex) || [];
let translatedTextWithMarkers = translation;
markers.forEach((marker, index) => {
translatedTextWithMarkers = translatedTextWithMarkers.replace(
new RegExp(`^(.{${index * marker.length}})(.*)`, 's'),
`$1${marker}$2`
);
});
translationCache.set(cacheKey, translatedTextWithMarkers);
return translatedTextWithMarkers;
} catch (error) {
console.error(`[DeepLX] 翻译失败 (subtitleId: ${subtitleId}):`, error);
throw error;
}
};
if (config.USE_ADAPTIVE_RATE_LIMITER) {
return rateLimiter.schedule(translate);
} else {
return translate();
}
}
//新增
async function translateWithFallback(
text: string,
sourceLanguage: string,
targetLanguage: string,
subtitleId: string,
signal: AbortSignal,
engine: TranslationEngine // 新增 engine 参数
): Promise<string> {
try {
console.log(`[Fallback] 尝试整体翻译 (subtitleId: ${subtitleId})`);
return await translateText(text, sourceLanguage, targetLanguage, subtitleId, signal, engine); // 调用调度器
} catch (error) {
console.error(`[Fallback] 整体翻译失败 (subtitleId: ${subtitleId}):`, error);
if (signal.aborted) {
throw new Error("Translation aborted");
}
const parts = text.split(config.SUBTITLE_SEPARATOR);
console.log(`[Fallback] 尝试单独翻译 ${parts.length} 个部分 (subtitleId: ${subtitleId})`);
const translatedParts = [];
for (let i = 0; i < parts.length; i++) {
if (signal.aborted) {
throw new Error("Translation aborted");
}
try {
const partId = `${subtitleId}-part${i}`;
const translatedPart = await translateText(parts[i], sourceLanguage, targetLanguage, partId, signal, engine); // 调用调度器
translatedParts.push(translatedPart);
} catch (partError) {
console.error(`[Fallback] 部分翻译失败 (subtitleId: ${subtitleId}, part: ${i}):`, partError);
translatedParts.push(`[翻译失败] ${parts[i]}`);
}
}
return translatedParts.join(config.SUBTITLE_SEPARATOR);
}
}
async function translateWithFallback_old(
text: string,
sourceLanguage: string,
targetLanguage: string,
subtitleId: string,
signal: AbortSignal
): Promise<string> {
try {
console.log(`[Fallback] 尝试整体翻译 (subtitleId: ${subtitleId})`);
return await translateWithDeepLX(text, sourceLanguage, targetLanguage, subtitleId, signal);
} catch (error) {
console.error(`[Fallback] 整体翻译失败 (subtitleId: ${subtitleId}):`, error);
if (signal.aborted) {
console.log(`[Fallback] 翻译被中止 (subtitleId: ${subtitleId})`);
throw new Error("Translation aborted");
}
const parts = text.split(config.SUBTITLE_SEPARATOR);
console.log(`[Fallback] 尝试单独翻译 ${parts.length} 个部分 (subtitleId: ${subtitleId})`);
const translatedParts = [];
for (let i = 0; i < parts.length; i++) {
if (signal.aborted) {
console.log(`[Fallback] 翻译过程中被中止 (subtitleId: ${subtitleId})`);
throw new Error("Translation aborted");
}
try {
const partId = `${subtitleId}-part${i}`;
const translatedPart = await translateWithDeepLX(parts[i], sourceLanguage, targetLanguage, partId, signal);
translatedParts.push(translatedPart);
} catch (partError) {
console.error(`[Fallback] 部分翻译失败 (subtitleId: ${subtitleId}, part: ${i}):`, partError);
translatedParts.push(`[翻译失败] ${parts[i]}`);
}
}
console.log(`[Fallback] 单独翻译完成 (subtitleId: ${subtitleId})`);
return translatedParts.join(config.SUBTITLE_SEPARATOR);
}
}
function initializeSubtitles(subtitles: SubtitleEntry[]): SubtitleEntry[] {
return subtitles;
}
function mergeSubtitles(subtitles: SubtitleEntry[]): SubtitleEntry[] {
console.log(`[Merger] 开始合并 ${subtitles.length} 条字幕`);
const mergedSubtitles: SubtitleEntry[] = [];
let currentGroup: SubtitleEntry[] = [];
let currentLength = 0;
for (const subtitle of subtitles) {
if (currentLength + subtitle.text.length > config.OPTIMAL_TEXT_LENGTH && currentGroup.length > 0) {
mergedSubtitles.push(mergeGroup(currentGroup));
currentGroup = [];
currentLength = 0;
}
currentGroup.push(subtitle);
currentLength += subtitle.text.length;
}
if (currentGroup.length > 0) {
mergedSubtitles.push(mergeGroup(currentGroup));
}
console.log(`[Merger] 合并完成,得到 ${mergedSubtitles.length} 个合并组`);
return mergedSubtitles;
}
function mergeGroup(group: SubtitleEntry[]): SubtitleEntry {
const mergedText = group.map(sub => sub.text.replace(/\n/g, '<br>')).join(config.SUBTITLE_SEPARATOR);
return {
id: `merged_${group[0].id}_to_${group[group.length - 1].id}`,
startTime: group[0].startTime,
endTime: group[group.length - 1].endTime,
text: mergedText,
originalSubtitles: group
};
}
function splitMergedSubtitle(mergedSubtitle: SubtitleEntry): SubtitleEntry[] {
console.log(`[Splitter] 拆分合并的字幕: ${mergedSubtitle.id}`);
const translatedText = mergedSubtitle.translatedText || '';
const originalSubtitles = mergedSubtitle.originalSubtitles || [];
const translatedParts = translatedText.split(config.SUBTITLE_SEPARATOR);
return originalSubtitles.map((original, index) => {
const translatedPart = translatedParts[index] || '';
return {
...original,
translatedText: translatedPart || `[翻译失败] ${original.text}`
};
});
}
async function handleWebSocket(ws: WebSocket) {
console.log("[WebSocket] 新的 WebSocket 连接已建立");
let subtitles: SubtitleEntry[] = [];
let heartbeatInterval: number;
let isTranslating = false;
let isConnected = true;
let abortController: AbortController | null = null;
const translatedSubtitleIds = new Set<string>();
let shouldStopTranslation = false;
const heartbeat = () => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
action: "heartbeat"
}));
} else {
clearInterval(heartbeatInterval);
}
};
ws.on("open", () => {
console.log("[WebSocket] 连接已打开");
heartbeatInterval = setInterval(heartbeat, 30000);
console.log("[WebSocket] 心跳机制已启动");
});
async function stopTranslation() {
if (abortController) {
abortController.abort();
abortController = null;
}
isTranslating = false;
shouldStopTranslation = false;
console.log("[WebSocket] 翻译已停止");
if (isConnected) {
ws.send(JSON.stringify({
action: "translationStopped"
}));
}
}
ws.on("message", async (message: string) => {
if (!isConnected) return;
try {
const data = JSON.parse(message);
switch (data.action) {
case "initialize":
subtitles = initializeSubtitles(data.subtitles);
translatedSubtitleIds.clear();
isTranslating = false;
if (!Array.isArray(subtitles) || subtitles.length === 0) {
throw new Error("无效的字幕数组");
}
console.log(`[WebSocket] 初始化字幕数组,长度: ${subtitles.length}`);
ws.send(JSON.stringify({
action: "initialized"
}));
break;
case "translate":
console.log("[WebSocket] 收到翻译请求");
if (isTranslating) {
console.log("[WebSocket] 翻译已在进行中,忽略新的请求");
return;
}
// const { timestamp, sourceLanguage, targetLanguage } = data;
// 从客户端数据中解构出 engine,并提供默认值
const {
timestamp, sourceLanguage, targetLanguage, engine = 'deeplx'
} = data;
const currentTime = typeof timestamp === 'number' ? timestamp : parseFloat(timestamp);
//console.log(`[WebSocket] 开始翻译,时间戳: ${currentTime}, 源语言: ${sourceLanguage}, 目标语言: ${targetLanguage}`);
console.log(`[WebSocket] 开始翻译,引擎: ${engine}, 时间戳: ${currentTime}, 源语言: ${sourceLanguage}, 目标语言: ${targetLanguage}`);
isTranslating = true;
shouldStopTranslation = false;
abortController = new AbortController();
const signal = abortController.signal;
const subtitlesToTranslate = subtitles.filter(sub => sub.startTime >= currentTime && !translatedSubtitleIds.has(sub.id));
console.log(`[Translator] 筛选出 ${subtitlesToTranslate.length} 条字幕需要翻译`);
// Language detection logic
const sampleSize = Math.min(config.LANGUAGE_DETECTION_SAMPLE_SIZE, subtitlesToTranslate.length);
const samples = subtitlesToTranslate.slice(0, sampleSize);
const detectedLanguages = samples.map(sample => {
const detectedCode = franc(sample.text) as string;
return languageCodeMapping[detectedCode] || detectedCode;
});
console.log(`[Translator] 检测到的语言: ${detectedLanguages.join(', ')}`);
const validDetections = detectedLanguages.filter(lang => lang !== 'und');
const targetLangCount = validDetections.filter(lang => lang === targetLanguage).length;
const detectionThreshold = Math.max(1, Math.floor(validDetections.length * 0.6));
const detectedLanguage = validDetections.length > 0 ?
validDetections.reduce((a, b, i, arr) =>
arr.filter(v => v === a).length >= arr.filter(v => v === b).length ? a : b
) :
'unknown';
if (targetLangCount >= detectionThreshold) {
console.log(`[Translator] 检测到字幕主要是目标语言 (${targetLanguage}),跳过翻译`);
ws.send(JSON.stringify({
action: "languageDetected",
language: detectedLanguage,
message: "Source language matches target language, translation skipped"
}));
ws.send(JSON.stringify({
action: "translationComplete"
}));
isTranslating = false;
abortController = null;
} else {
console.log(`[Translator] 检测到字幕不是目标语言,继续翻译`);
ws.send(JSON.stringify({
action: "languageDetected",
language: "different",
message: "Source language differs from target language, proceeding with translation"
}));
try {
if (engine === 'chatgpt') {
await batchTranslateWithChatGPT(
subtitlesToTranslate,
sourceLanguage,
targetLanguage,
signal,
(translatedBatch) => {
if (isConnected && !shouldStopTranslation) {
ws.send(JSON.stringify({
action: "translationResult",
subtitles: translatedBatch,
}));
}
}
);
} else {
// --- DeepLX 的专属“合并-批处理”路径 (你现有的稳定逻辑) ---
console.log(`[DeepLX-Path] 开始合并与批处理翻译...`);
const initialBatch = subtitlesToTranslate.slice(0, config.INITIAL_BATCH_SIZE);
console.log(`[Translator] 开始初始快速翻译,包含 ${initialBatch.length} 条字幕`);
const translatedInitialBatch = await Promise.all(initialBatch.map(async (item) => {
if (shouldStopTranslation) throw new Error("Translation stopped");
const translatedText = await translateWithFallback(
item.text,
sourceLanguage,
targetLanguage,
item.id,
signal,
//新增
engine
);
translatedSubtitleIds.add(item.id);
return {
...item,
translatedText
};
}));
if (isConnected && !shouldStopTranslation) {
console.log(`[WebSocket] 发送初始快速翻译结果,包含 ${translatedInitialBatch.length} 条字幕`);
ws.send(JSON.stringify({
action: "translationResult",
subtitles: translatedInitialBatch
}));
}
console.log(`[Translator] 合并前的字幕数量: ${subtitlesToTranslate.length}`);
const remainingSubtitles = subtitlesToTranslate.slice(config.INITIAL_BATCH_SIZE);
const mergedSubtitles = mergeSubtitles(remainingSubtitles);
console.log(`[Translator] 合并后的剩余字幕数量: ${mergedSubtitles.length}`);
for (let i = 0; i < mergedSubtitles.length; i += config.BATCH_SIZE) {
if (!isConnected || shouldStopTranslation) {
console.log("[Translator] 连接已断开或收到停止命令,停止翻译");
break;
}
const batch = mergedSubtitles.slice(i, i + config.BATCH_SIZE);
console.log(`[Translator] 处理批次 ${i / config.BATCH_SIZE + 1}, 包含 ${batch.length} 条合并字幕`);
const translatedBatch = await Promise.all(batch.map(async (item) => {
if (shouldStopTranslation) throw new Error("Translation stopped");
try {
console.log(`[Translator] 翻译文本: ${item.text.substring(0, 50)}...`);
const translatedText = await translateWithFallback(
item.text,
sourceLanguage,
targetLanguage,
item.id,
signal,
//新增
engine
);
console.log(`[Translator] 翻译完成: ${translatedText.substring(0, 50)}...`);
return {
...item,
translatedText
};
} catch (error) {
console.error(`[Translator] 翻译失败: ${error.message}`);
if (error.name === 'AbortError' || error.message === "Translation stopped") {
throw error;
}
return {
...item,
translatedText: `[翻译失败] ${item.text}`
};
}
}));
if (isConnected && !shouldStopTranslation) {
const distributedResults = translatedBatch.flatMap(splitMergedSubtitle);
distributedResults.forEach(sub => translatedSubtitleIds.add(sub.id));
console.log(`[WebSocket] 发送翻译结果,包含 ${distributedResults.length} 条字幕`);
ws.send(JSON.stringify({
action: "translationResult",
subtitles: distributedResults
}));
}
}
}
if (isConnected && !shouldStopTranslation) {
console.log(`[WebSocket] 翻译完成,共翻译 ${subtitlesToTranslate.length} 条字幕`);
ws.send(JSON.stringify({
action: "translationComplete"
}));
}
} catch (error) {
// console.error("[Translator] 翻译过程中出错:", error);
// if (error.message === "Translation stopped") {
// console.log("[Translator] 翻译被手动停止");
// } else if (error.name !== 'AbortError' && isConnected) {
// ws.send(JSON.stringify({
// action: "error",
// message: error.message
// }));
// }
// --- 1. 首先,判断这是不是一个我们预料之中的“中止”操作 ---
if (error.name === 'AbortError' || error.message.includes('aborted')) {
// 如果是,我们就只打印一条清晰的、表示“任务已按预期停止”的信息
// 这不是一个 ERROR,所以我们用 console.log
console.log(`[Translator] 翻译任务已被用户成功中止。`);
} else {
// --- 2. 如果不是中止,那它就是一个真正的、意料之外的错误 ---
console.error("[Translator] 翻译过程中发生意外错误:", error);
// 我们只在连接仍然存在的情况下,才向客户端发送错误消息
if (isConnected) {
ws.send(JSON.stringify({
action: "error",
// 发送一个更友好的错误消息
message: `An unexpected error occurred: ${error.message}`
}));
}
}
} finally {
isTranslating = false;
abortController = null;
shouldStopTranslation = false;
}
}
performanceMonitor.logPerformanceMetrics();
break;
case "stopTranslation":
console.log("[WebSocket] 收到停止翻译请求");
shouldStopTranslation = true;
await stopTranslation();
break;
case "closeConnection":
console.log("[WebSocket] 收到关闭连接请求");
ws.close();
break;
case "heartbeatResponse":
console.log("[WebSocket] 收到心跳响应");
break;
case "heartbeat":
// console.log("[WebSocket] 收到客户端心跳");
ws.send(JSON.stringify({
action: "heartbeatResponse"
}));
break;
case "setAdaptiveRateLimiter":
config.USE_ADAPTIVE_RATE_LIMITER = data.useAdaptiveRateLimiter;
console.log(`[WebSocket] 设置 AdaptiveRateLimiter: ${config.USE_ADAPTIVE_RATE_LIMITER ? '启用' : '禁用'}`);
ws.send(JSON.stringify({
action: "adaptiveRateLimiterSet",
useAdaptiveRateLimiter: config.USE_ADAPTIVE_RATE_LIMITER
}));
break;
default:
console.warn(`[WebSocket] 收到未知操作: ${data.action}`);
}
} catch (error) {
console.error("[WebSocket] 处理消息时出错:", error);
if (isConnected) {
ws.send(JSON.stringify({
action: "error",
message: error.message
}));
}
}
});
ws.on("close", () => {
console.log("[WebSocket] 连接已关闭");
isConnected = false;
clearInterval(heartbeatInterval);
if (isTranslating) {
stopTranslation();
}
});
ws.on("error", async (error) => {
console.error("[WebSocket] 发生错误:", error);
if (isTranslating) {
await stopTranslation();
}
});
}
// 设置WebSocket服务器
const wss = new WebSocketServer(8000);
wss.on("connection", (ws: WebSocket) => {
handleWebSocket(ws);
});
// Attempt to catch server-level errors to prevent crashes from bad handshakes
wss.on("error", (error: Error) => {
if (error.message.includes("request is not acceptable") ||
error.message.includes("missing or invalid headers")) {
console.warn(`[WebSocketServer LIB] 忽略了无效的握手或非WebSocket请求: ${error.message}`);
// This catch should prevent the unhandled promise rejection.
} else {
console.error("[WebSocketServer LIB] 服务器实例错误:", error);
// For other errors, you might want to decide if the server should exit or try to recover.
}
});
// 添加每日分析的定时器
const dailyAnalysisInterval = setInterval(async () => {
const dailyData = performanceMonitor.getDailyData();
await PerformanceAnalyzer.analyzeDailyPerformance(dailyData);
performanceMonitor.clearDailyData();
}, 24 * 60 * 60 * 1000); // 每24小时运行一次
// 确保在程序退出时清理定时器
Deno.addSignalListener("SIGINT", () => {
clearInterval(dailyAnalysisInterval);
// 其他清理代码...
console.log("正在关闭服务器...");
wss.close(() => {
console.log("WebSocket 服务器已关闭");
Deno.exit(0);
});
});
console.log("WebSocket 字幕翻译服务器正在运行,地址为 ws://localhost:8000");
// // 添加 HTTP 服务器
// const port = 80;
// console.log(`[${new Date().toISOString()}] [INFO] 启动 HTTP 服务器在端口 ${port}`);
// try {
// const server = Deno.listen({ port });
// console.log(`[${new Date().toISOString()}] [INFO] HTTP 服务器已启动成功`);
// // 处理 HTTP 请求
// (async () => {
// for await (const conn of server) {
// (async () => {
// const httpConn = Deno.serveHttp(conn);
// for await (const requestEvent of httpConn) {
// await requestEvent.respondWith(
// new Response("字幕翻译服务正在运行!", {
// status: 200,
// headers: { "content-type": "text/plain; charset=utf-8" }
// })
// );
// }
// })().catch(err => {
// console.error(`[${new Date().toISOString()}] [ERROR] HTTP 请求处理错误:`, err);
// });
// }
// })().catch(err => {
// console.error(`[${new Date().toISOString()}] [ERROR] HTTP 服务器错误:`, err);
// });
// } catch (error) {
// console.error(`[${new Date().toISOString()}] [ERROR] HTTP 服务器启动失败:`, error);
// }