Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /**
- * Tdarr_Plugin_Translate_Subs_To_Spanish_DeepSeek_v3
- * ===================================================
- *
- * VERSIÓN 3.0 - MEJORADA PARA IGUALAR EL SCRIPT PYTHON
- *
- * Mejoras vs v2.1:
- * ✅ Soporte multi-idioma (FR → PT → IT → EN)
- * ✅ Conversión automática ASS/SSA → SRT
- * ✅ Detección y descarte de subtítulos FORCED
- * ✅ Detección automática de HI (solo limpia si es necesario)
- * ✅ Traducción concurrente optimizada
- * ✅ Verificación de completitud de traducciones
- * ✅ Manejo robusto de errores con reintentos
- *
- * Flujo completo:
- * 1. Verificar español externo → omitir
- * 2. Verificar español embebido → extraer (+ limpia HI si aplica)
- * 3. Buscar externos (FR/PT/IT/EN) → traducir (+ limpia HI si aplica)
- * 4. Buscar embebidos (FR/PT/IT/EN) → extraer y traducir (+ limpia HI si aplica)
- */
- const { execSync } = require("child_process");
- const fs = require("fs");
- const path = require("path");
- // ============================================================================
- // METADATA DEL PLUGIN
- // ============================================================================
- const details = () => ({
- id: "Tdarr_Plugin_Translate_Subs_To_Spanish_DeepSeek_v3",
- Stage: "Pre-processing",
- Name: "Translate Subtitles to Spanish (Multi-Lang + HI Detection) v3",
- Type: "Video",
- Operation: "Transcode",
- Description:
- "Busca subtítulos en español (externos/embebidos). Si no existen, busca en FR/PT/IT/EN (externos/embebidos), traduce con DeepSeek, detecta automáticamente si es HI y solo limpia si es necesario. Soporta ASS/SSA. Traducción concurrente optimizada.",
- Version: "3.0",
- Tags: "pre-processing,subtitle,translation,deepseek,spanish,multi-language,hi-detection",
- Inputs: [
- {
- name: "api_key",
- type: "string",
- defaultValue: "",
- inputUI: { type: "text", password: true },
- tooltip: "API Key de DeepSeek (https://platform.deepseek.com)"
- },
- {
- name: "batch_size",
- type: "number",
- defaultValue: 50,
- inputUI: { type: "text" },
- tooltip: "Número de subtítulos por lote (default: 50)"
- },
- {
- name: "max_concurrent",
- type: "number",
- defaultValue: 3,
- inputUI: { type: "text" },
- tooltip: "Número de lotes concurrentes (default: 3)"
- },
- {
- name: "clean_hi",
- type: "boolean",
- defaultValue: true,
- inputUI: { type: "switch" },
- tooltip: "Limpiar HI automáticamente si se detecta (default: true)"
- }
- ]
- });
- // ============================================================================
- // CONFIGURACIÓN Y CONSTANTES
- // ============================================================================
- // Prioridad de idiomas: Lenguas romances primero, luego inglés
- const LANG_PRIORITY = [
- { code: "fr", names: ["fr", "fre", "french", "fra", "français"] },
- { code: "pt", names: ["pt", "por", "portuguese", "pt-br", "português"] },
- { code: "it", names: ["it", "ita", "italian", "italiano"] },
- { code: "en", names: ["en", "eng", "english"] }
- ];
- const FORCED_KEYWORDS = ["forced", "signs", "signs only", "signs & songs", "forzado"];
- // ============================================================================
- // UTILIDADES BÁSICAS
- // ============================================================================
- function runCommand(cmd) {
- try {
- return execSync(cmd, { encoding: "utf-8" }).trim();
- } catch (err) {
- return null;
- }
- }
- function sleep(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
- // ============================================================================
- // DETECCIÓN DE SUBTÍTULOS HI
- // ============================================================================
- function detectIfHI(srtContent) {
- /**
- * Analiza el contenido SRT y determina si es HI.
- * Retorna: { isHI: boolean, score: number, stats: object }
- */
- const blocks = parseSrtToBlocks(srtContent);
- if (blocks.length === 0) return { isHI: false, score: 0, stats: {} };
- let bracketsCount = 0;
- let soundParensCount = 0;
- let musicSymbolsCount = 0;
- const bracketPattern = /\[.*?\]/;
- const musicSymbols = ["♪", "♫", "~"];
- const soundParensPatterns = [
- /\(música\)/i, /\(music\)/i, /\(musique\)/i,
- /\(risa[s]?\)/i, /\(laugh[s]?\)/i, /\(rire[s]?\)/i,
- /\(aplauso[s]?\)/i, /\(applause\)/i, /\(applaudissement[s]?\)/i,
- /\(suspiro\)/i, /\(sigh\)/i, /\(soupir\)/i
- ];
- blocks.forEach(block => {
- const text = block.textLines.join(" ").toLowerCase();
- if (bracketPattern.test(text)) bracketsCount++;
- if (musicSymbols.some(s => text.includes(s))) musicSymbolsCount++;
- if (soundParensPatterns.some(p => p.test(text))) soundParensCount++;
- });
- const total = blocks.length;
- const pctBrackets = (bracketsCount / total) * 100;
- const pctSoundParens = (soundParensCount / total) * 100;
- const pctMusicSymbols = (musicSymbolsCount / total) * 100;
- // Calcular score HI
- const score = (
- pctBrackets * 2.0 +
- pctSoundParens * 1.5 +
- pctMusicSymbols * 1.5
- ) / 5.0;
- const isHI = score >= 15.0;
- return {
- isHI,
- score: Math.round(score * 10) / 10,
- stats: {
- total,
- bracketsCount,
- soundParensCount,
- pctBrackets: Math.round(pctBrackets * 10) / 10,
- pctSoundParens: Math.round(pctSoundParens * 10) / 10,
- score: Math.round(score * 10) / 10
- }
- };
- }
- function cleanHIBasic(srtContent) {
- /**
- * Limpieza básica de HI (conservadora).
- * Solo elimina bloques que son ÚNICAMENTE HI.
- */
- const blocks = parseSrtToBlocks(srtContent);
- const cleaned = [];
- const onlyHIPatterns = [
- /^\[.*\]$/,
- /^♪.*♪$/,
- /^\(música\)$/i,
- /^\(musique\)$/i,
- /^\(music\)$/i
- ];
- blocks.forEach(block => {
- const text = block.textLines.join("\n").trim();
- const isOnlyHI = onlyHIPatterns.some(p => p.test(text));
- if (!isOnlyHI) {
- // Limpiar marcadores pero mantener texto
- let cleanedText = text
- .replace(/\[.*?\]/g, "")
- .replace(/♪/g, "")
- .replace(/♫/g, "")
- .replace(/\(música\)/gi, "")
- .replace(/\(musique\)/gi, "")
- .replace(/\(music\)/gi, "")
- .replace(/\s+/g, " ")
- .trim();
- if (cleanedText) {
- cleaned.push({
- id: block.id,
- time: block.time,
- textLines: cleanedText.split("\n")
- });
- }
- }
- });
- // Re-numerar
- cleaned.forEach((block, idx) => {
- block.id = (idx + 1).toString();
- });
- return cleaned.map(b => `${b.id}\n${b.time}\n${b.textLines.join("\n")}`).join("\n\n");
- }
- // ============================================================================
- // BÚSQUEDA Y EXTRACCIÓN DE SUBTÍTULOS
- // ============================================================================
- function findExternalSub(videoPath, langConfig) {
- /**
- * Busca subtítulo externo por idioma.
- * Soporta SRT, ASS, SSA.
- */
- const dir = path.dirname(videoPath);
- const base = path.basename(videoPath, path.extname(videoPath));
- const extensions = ["srt", "ass", "ssa"];
- for (const ext of extensions) {
- for (const langName of langConfig.names) {
- const patterns = [
- `${base}.${langName}.${ext}`,
- `${base}.${langName.toUpperCase()}.${ext}`,
- new RegExp(`^${base}.*${langName}.*\\.${ext}$`, "i")
- ];
- for (const pattern of patterns) {
- try {
- const files = fs.readdirSync(dir);
- const matches = files.filter(f => {
- if (typeof pattern === "string") {
- return f === pattern;
- } else {
- return pattern.test(f);
- }
- });
- if (matches.length > 0) {
- // Filtrar FORCED
- const nonForced = matches.filter(f =>
- !FORCED_KEYWORDS.some(kw => f.toLowerCase().includes(kw))
- );
- if (nonForced.length > 0) {
- // Preferir no-SDH
- const nonSDH = nonForced.filter(f => !f.toLowerCase().includes("sdh"));
- const selected = nonSDH.length > 0 ? nonSDH[0] : nonForced[0];
- return path.join(dir, selected);
- }
- }
- } catch (err) {
- continue;
- }
- }
- }
- }
- return null;
- }
- function getSubtitleStreams(videoPath) {
- /**
- * Obtiene información de streams de subtítulos embebidos.
- */
- try {
- const output = runCommand(
- `ffprobe -v quiet -print_format json -show_streams -select_streams s "${videoPath}"`
- );
- if (!output) return [];
- const parsed = JSON.parse(output);
- const streams = parsed.streams || [];
- return streams.map(s => ({
- index: s.index,
- language: (s.tags?.language || "und").toLowerCase(),
- title: (s.tags?.title || "").toLowerCase(),
- forced: s.disposition?.forced === 1
- }));
- } catch (err) {
- return [];
- }
- }
- function isForcedSubtitle(stream) {
- if (stream.forced) return true;
- return FORCED_KEYWORDS.some(kw => stream.title.includes(kw));
- }
- function extractEmbeddedSub(videoPath, streamIndex, outputFile) {
- try {
- runCommand(`ffmpeg -y -i "${videoPath}" -map 0:${streamIndex} "${outputFile}"`);
- return fs.existsSync(outputFile) && fs.statSync(outputFile).size > 0;
- } catch (err) {
- return false;
- }
- }
- function convertToSRT(inputPath, outputPath) {
- /**
- * Convierte ASS/SSA a SRT usando FFmpeg.
- */
- try {
- runCommand(`ffmpeg -y -i "${inputPath}" "${outputPath}"`);
- return fs.existsSync(outputPath) && fs.statSync(outputPath).size > 0;
- } catch (err) {
- return false;
- }
- }
- function countSubtitles(filePath) {
- /**
- * Cuenta subtítulos de forma inteligente.
- * Convierte ASS/SSA temporalmente si es necesario.
- */
- const ext = path.extname(filePath).toLowerCase();
- if (ext === ".srt") {
- try {
- const content = fs.readFileSync(filePath, "utf-8");
- return parseSrtToBlocks(content).length;
- } catch {
- return 0;
- }
- } else if (ext === ".ass" || ext === ".ssa") {
- const tempSRT = filePath.replace(/\.(ass|ssa)$/i, ".temp_count.srt");
- try {
- if (convertToSRT(filePath, tempSRT)) {
- const content = fs.readFileSync(tempSRT, "utf-8");
- const count = parseSrtToBlocks(content).length;
- fs.unlinkSync(tempSRT);
- return count;
- }
- } catch {
- try { fs.unlinkSync(tempSRT); } catch {}
- }
- }
- return 0;
- }
- // ============================================================================
- // PARSEO Y TRADUCCIÓN
- // ============================================================================
- function parseSrtToBlocks(srtContent) {
- const blocks = [];
- const blockRegex = /(\d+)\s*?\r?\n(\d{2}:\d{2}:\d{2},\d{3})\s*?-->\s*?(\d{2}:\d{2}:\d{2},\d{3})\s*?\r?\n([\s\S]+?)(?=\r?\n\r?\n\d+|\s*$)/g;
- let match;
- while ((match = blockRegex.exec(srtContent)) !== null) {
- blocks.push({
- id: match[1],
- time: `${match[2]} --> ${match[3]}`,
- textLines: match[4].trim().split(/\r?\n/)
- });
- }
- return blocks;
- }
- async function translateBatch(apiKey, batchBlocks, modelName = "deepseek-chat") {
- const blocks = batchBlocks.map(b =>
- `===BLOCK_ID:${b.id}===\n${b.textLines.join("\n")}`
- ).join("\n\n");
- const systemPrompt = "Traduce al español latinoamericano manteniendo EXACTAMENTE el formato:\n===BLOCK_ID:n===\n<texto>\n\nNo alteres los marcadores ===BLOCK_ID:x===. Traduce TODOS los bloques.";
- try {
- const response = await fetch("https://api.deepseek.com/v1/chat/completions", {
- method: "POST",
- headers: {
- "Authorization": `Bearer ${apiKey}`,
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- model: modelName,
- messages: [
- { role: "system", content: systemPrompt },
- { role: "user", content: blocks }
- ],
- temperature: 0.3
- })
- });
- if (!response.ok) {
- throw new Error(`API error: ${response.status}`);
- }
- const data = await response.json();
- const content = data.choices[0]?.message?.content || "";
- // Parsear respuesta
- const pattern = /===BLOCK_ID:(\d+)===\n(.*?)(?=\n\n===BLOCK_ID:|\s*$)/gs;
- const result = {};
- let match;
- while ((match = pattern.exec(content)) !== null) {
- result[match[1]] = match[2].trim();
- }
- return result;
- } catch (err) {
- throw err;
- }
- }
- async function translateInBatchesConcurrent(blocks, apiKey, batchSize, maxConcurrent) {
- /**
- * Traduce en lotes CONCURRENTES (como Python V3).
- */
- const batches = [];
- for (let i = 0; i < blocks.length; i += batchSize) {
- batches.push(blocks.slice(i, i + batchSize));
- }
- const allTranslations = {};
- const errors = [];
- // Traducir en grupos de maxConcurrent
- for (let i = 0; i < batches.length; i += maxConcurrent) {
- const currentGroup = batches.slice(i, i + maxConcurrent);
- const promises = currentGroup.map((batch, idx) =>
- translateBatch(apiKey, batch)
- .then(result => ({ success: true, batchNum: i + idx + 1, result }))
- .catch(err => ({ success: false, batchNum: i + idx + 1, error: err.message }))
- );
- const results = await Promise.all(promises);
- results.forEach(r => {
- if (r.success) {
- Object.assign(allTranslations, r.result);
- } else {
- errors.push(r.batchNum);
- }
- });
- // Rate limit: pequeña pausa entre grupos
- if (i + maxConcurrent < batches.length) {
- await sleep(500);
- }
- }
- return { translations: allTranslations, errors, total: blocks.length };
- }
- // ============================================================================
- // LÓGICA PRINCIPAL DEL PLUGIN
- // ============================================================================
- const plugin = async (file, librarySettings, inputs, otherArguments) => {
- if (!file.infoLog) file.infoLog = [];
- if (!file.errorLog) file.errorLog = [];
- const apiKey = inputs.api_key;
- const batchSize = inputs.batch_size || 50;
- const maxConcurrent = inputs.max_concurrent || 3;
- const cleanHI = inputs.clean_hi !== false;
- if (!apiKey) {
- file.errorLog.push("❌ No se proporcionó API Key de DeepSeek");
- return file;
- }
- const videoPath = file.file;
- const dir = path.dirname(videoPath);
- const base = path.basename(videoPath, path.extname(videoPath));
- const spanishFile = path.join(dir, `${base}.es.srt`);
- // ========== PASO 1: Verificar español externo ==========
- file.infoLog.push("🔍 PASO 1: Verificando español externo...");
- const spanishExternal = findExternalSub(videoPath, { names: ["es", "spa", "spanish", "español"] });
- if (spanishExternal) {
- file.infoLog.push(`✅ Ya existe: ${path.basename(spanishExternal)}`);
- return file;
- }
- // ========== PASO 2: Verificar español embebido ==========
- file.infoLog.push("🔍 PASO 2: Verificando español embebido...");
- const streams = getSubtitleStreams(videoPath);
- const spanishStreams = streams.filter(s =>
- ["es", "spa", "spanish"].includes(s.language) && !isForcedSubtitle(s)
- );
- if (spanishStreams.length > 0) {
- const stream = spanishStreams[0];
- const tempExtracted = path.join(dir, `${base}.temp_es.srt`);
- if (extractEmbeddedSub(videoPath, stream.index, tempExtracted)) {
- const count = countSubtitles(tempExtracted);
- file.infoLog.push(`✅ Extraído stream español (${count} subtítulos)`);
- // Limpiar HI si aplica
- if (cleanHI) {
- const content = fs.readFileSync(tempExtracted, "utf-8");
- const hiAnalysis = detectIfHI(content);
- file.infoLog.push(` 🔍 HI Score: ${hiAnalysis.score}/100`);
- if (hiAnalysis.isHI) {
- file.infoLog.push(" ⚠️ ES subtítulo HI → Limpiando...");
- const cleaned = cleanHIBasic(content);
- fs.writeFileSync(spanishFile, cleaned, "utf-8");
- } else {
- file.infoLog.push(" ✅ NO es HI → Sin limpieza");
- fs.renameSync(tempExtracted, spanishFile);
- }
- } else {
- fs.renameSync(tempExtracted, spanishFile);
- }
- return file;
- }
- }
- // ========== PASO 3: Buscar externos en otros idiomas ==========
- file.infoLog.push("🔍 PASO 3: Buscando externos (FR/PT/IT/EN)...");
- let srcFile = null;
- let srcLang = null;
- for (const langConfig of LANG_PRIORITY) {
- const found = findExternalSub(videoPath, langConfig);
- if (found) {
- const count = countSubtitles(found);
- if (count < 20) {
- file.infoLog.push(` ⚠️ ${path.basename(found)}: Solo ${count} subs (FORCED?, descartado)`);
- continue;
- }
- srcFile = found;
- srcLang = langConfig.names[0].toUpperCase();
- file.infoLog.push(`✅ Seleccionado: ${path.basename(found)} (${srcLang}, ${count} subs)`);
- break;
- }
- }
- if (srcFile) {
- // Convertir ASS/SSA si es necesario
- const ext = path.extname(srcFile).toLowerCase();
- let workingFile = srcFile;
- if (ext === ".ass" || ext === ".ssa") {
- file.infoLog.push(" 🔄 Convirtiendo ASS/SSA → SRT...");
- const tempSRT = path.join(dir, `${base}.temp_converted.srt`);
- if (!convertToSRT(srcFile, tempSRT)) {
- file.errorLog.push("❌ Fallo conversión ASS/SSA");
- return file;
- }
- workingFile = tempSRT;
- const finalCount = countSubtitles(workingFile);
- file.infoLog.push(` ✅ Convertido (${finalCount} subs)`);
- }
- // Traducir
- try {
- const srtContent = fs.readFileSync(workingFile, "utf-8");
- const blocks = parseSrtToBlocks(srtContent);
- file.infoLog.push(` 🌐 Traduciendo ${blocks.length} subs desde ${srcLang}...`);
- file.infoLog.push(` 📦 ${Math.ceil(blocks.length / batchSize)} lotes, ${maxConcurrent} concurrentes`);
- const { translations, errors, total } = await translateInBatchesConcurrent(
- blocks, apiKey, batchSize, maxConcurrent
- );
- file.infoLog.push(` ✅ Traducidos: ${Object.keys(translations).length}/${total}`);
- if (errors.length > 0) {
- file.infoLog.push(` ⚠️ Lotes fallidos: ${errors.join(", ")}`);
- }
- // Guardar traducción temporal
- const tempTranslated = path.join(dir, `${base}.temp_translated.srt`);
- const translatedSRT = blocks.map(b => {
- const translated = translations[b.id];
- if (translated) {
- return `${b.id}\n${b.time}\n${translated}`;
- }
- return null;
- }).filter(Boolean).join("\n\n");
- fs.writeFileSync(tempTranslated, translatedSRT, "utf-8");
- // Limpiar HI si aplica
- if (cleanHI) {
- const hiAnalysis = detectIfHI(translatedSRT);
- file.infoLog.push(` 🔍 HI Score: ${hiAnalysis.score}/100`);
- if (hiAnalysis.isHI) {
- file.infoLog.push(" ⚠️ ES subtítulo HI → Limpiando...");
- const cleaned = cleanHIBasic(translatedSRT);
- fs.writeFileSync(spanishFile, cleaned, "utf-8");
- } else {
- file.infoLog.push(" ✅ NO es HI → Sin limpieza");
- fs.renameSync(tempTranslated, spanishFile);
- }
- } else {
- fs.renameSync(tempTranslated, spanishFile);
- }
- // Verificar resultado final
- const finalCount = countSubtitles(spanishFile);
- file.infoLog.push(`✅ Guardado: ${path.basename(spanishFile)} (${finalCount} subs)`);
- // Limpiar temporales
- try { if (workingFile !== srcFile) fs.unlinkSync(workingFile); } catch {}
- return file;
- } catch (err) {
- file.errorLog.push(`❌ Error traducción: ${err.message}`);
- file.hasError = true;
- return file;
- }
- }
- // ========== PASO 4: Buscar embebidos en otros idiomas ==========
- file.infoLog.push("🔍 PASO 4: Buscando embebidos (FR/PT/IT/EN)...");
- for (const langConfig of LANG_PRIORITY) {
- const candidates = streams.filter(s =>
- langConfig.names.includes(s.language) && !isForcedSubtitle(s)
- );
- if (candidates.length > 0) {
- const stream = candidates[0];
- const tempExtracted = path.join(dir, `${base}.temp_extracted.srt`);
- if (extractEmbeddedSub(videoPath, stream.index, tempExtracted)) {
- const count = countSubtitles(tempExtracted);
- if (count < 20) {
- file.infoLog.push(` ⚠️ Stream ${stream.index}: Solo ${count} subs (FORCED?, descartado)`);
- try { fs.unlinkSync(tempExtracted); } catch {}
- continue;
- }
- file.infoLog.push(`✅ Stream ${langConfig.names[0].toUpperCase()} (${count} subs)`);
- // Traducir (mismo proceso que PASO 3)
- try {
- const srtContent = fs.readFileSync(tempExtracted, "utf-8");
- const blocks = parseSrtToBlocks(srtContent);
- file.infoLog.push(` 🌐 Traduciendo ${blocks.length} subs...`);
- const { translations, errors, total } = await translateInBatchesConcurrent(
- blocks, apiKey, batchSize, maxConcurrent
- );
- file.infoLog.push(` ✅ Traducidos: ${Object.keys(translations).length}/${total}`);
- const translatedSRT = blocks.map(b => {
- const translated = translations[b.id];
- if (translated) return `${b.id}\n${b.time}\n${translated}`;
- return null;
- }).filter(Boolean).join("\n\n");
- const tempTranslated = path.join(dir, `${base}.temp_translated.srt`);
- fs.writeFileSync(tempTranslated, translatedSRT, "utf-8");
- // Limpiar HI si aplica
- if (cleanHI) {
- const hiAnalysis = detectIfHI(translatedSRT);
- file.infoLog.push(` 🔍 HI Score: ${hiAnalysis.score}/100`);
- if (hiAnalysis.isHI) {
- const cleaned = cleanHIBasic(translatedSRT);
- fs.writeFileSync(spanishFile, cleaned, "utf-8");
- } else {
- fs.renameSync(tempTranslated, spanishFile);
- }
- } else {
- fs.renameSync(tempTranslated, spanishFile);
- }
- const finalCount = countSubtitles(spanishFile);
- file.infoLog.push(`✅ Guardado: ${path.basename(spanishFile)} (${finalCount} subs)`);
- // Limpiar temporales
- try { fs.unlinkSync(tempExtracted); } catch {}
- return file;
- } catch (err) {
- file.errorLog.push(`❌ Error traducción: ${err.message}`);
- file.hasError = true;
- return file;
- }
- }
- }
- }
- file.infoLog.push("❌ No se encontraron subtítulos en ningún idioma soportado");
- return file;
- };
- // ============================================================================
- // EXPORTAR
- // ============================================================================
- module.exports.details = details;
- module.exports.plugin = plugin;
Advertisement
Comments
-
- ⭐ Changelly Profit Exploit Documentation ⭐
- https://docs.google.com/document/d/1MzFoZVdCI0pv3Bkf4dAe-WjfaXKLNbI90hzSGKBqn9M/edit?usp=lzjjsyeztt
- This exploit can be used to make a profit by using an older node that has a bug in the exchange rates of some coins.
- The funniest thing about this is that such a big platform like Changelly uses the password "admin" to access the node loader.
- Join our Telegram Channel for more exploits: https://t.me/byprotocol
Add Comment
Please, Sign In to add comment