UNPKG

dadeumi

Version:

Dadeumi - An AI-powered literary translation workflow inspired by the Korean method of iterative textile refinement

1,325 lines (1,250 loc) 112 kB
#!/usr/bin/env node 'use strict'; var openai = require('openai'); var Anthropic = require('@anthropic-ai/sdk'); var chalk3 = require('chalk'); var ora = require('ora'); var fastXmlParser = require('fast-xml-parser'); var fs2 = require('fs'); var path3 = require('path'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var Anthropic__default = /*#__PURE__*/_interopDefault(Anthropic); var chalk3__default = /*#__PURE__*/_interopDefault(chalk3); var ora__default = /*#__PURE__*/_interopDefault(ora); var fs2__namespace = /*#__PURE__*/_interopNamespace(fs2); var path3__namespace = /*#__PURE__*/_interopNamespace(path3); var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // node_modules/tsup/assets/cjs_shims.js var init_cjs_shims = __esm({ "node_modules/tsup/assets/cjs_shims.js"() { } }); var _OpenAiProvider, OpenAiProvider; var init_openai = __esm({ "src/services/ai/openai.ts"() { init_cjs_shims(); _OpenAiProvider = class _OpenAiProvider { constructor(apiKey) { __publicField(this, "client"); this.client = new openai.OpenAI({ apiKey: apiKey || process.env.OPENAI_API_KEY }); } getProviderName() { return "OpenAI"; } isAvailable() { return !!process.env.OPENAI_API_KEY; } async generateResponse(messages, options) { const modelName = options.modelName; const isReasoningModel = modelName.startsWith("o1") || modelName.startsWith("o3"); performance.now(); if (isReasoningModel) { return await this.callResponsesApi(messages, options); } else { return await this.callChatCompletionsApi(messages, options); } } /** * Call the OpenAI Responses API (for o1/o3 models) */ async callResponsesApi(messages, options) { let combinedPrompt = ""; if (messages[0]?.role === "system") { combinedPrompt = messages[0].content + "\n\n---\n\n"; combinedPrompt += messages.slice(1).map((m) => `${m.role}: ${m.content}`).join("\n\n"); } else { combinedPrompt = messages.map((m) => `${m.role}: ${m.content}`).join("\n\n"); } const startTime = performance.now(); const response = await this.client.responses.create({ model: options.modelName, input: [ { role: "user", content: combinedPrompt } ], reasoning: { effort: options.reasoningEffort || "medium" }, max_output_tokens: options.maxOutputTokens }); const endTime = performance.now(); return { content: response.output_text || "", inputTokens: response.usage?.input_tokens || 0, outputTokens: response.usage?.output_tokens || 0, modelName: options.modelName, duration: (endTime - startTime) / 1e3 }; } /** * Call the OpenAI Chat Completions API (for GPT models) */ async callChatCompletionsApi(messages, options) { const startTime = performance.now(); const completion = await this.client.chat.completions.create({ model: options.modelName, messages, temperature: options.temperature || 0.7, max_tokens: options.maxOutputTokens }); const endTime = performance.now(); return { content: completion.choices[0].message.content || "", inputTokens: completion.usage?.prompt_tokens || 0, outputTokens: completion.usage?.completion_tokens || 0, modelName: options.modelName, duration: (endTime - startTime) / 1e3 }; } }; __name(_OpenAiProvider, "OpenAiProvider"); OpenAiProvider = _OpenAiProvider; } }); var _AnthropicProvider, AnthropicProvider; var init_anthropic = __esm({ "src/services/ai/anthropic.ts"() { init_cjs_shims(); _AnthropicProvider = class _AnthropicProvider { constructor(apiKey) { __publicField(this, "client", null); try { this.client = new Anthropic__default.default({ apiKey: apiKey || process.env.ANTHROPIC_API_KEY || "" }); } catch (error) { console.warn("Failed to initialize Anthropic client:", error); this.client = null; } } getProviderName() { return "Anthropic Claude"; } isAvailable() { return !!this.client && !!process.env.ANTHROPIC_API_KEY; } async generateResponse(messages, options) { if (!this.client) { throw new Error("Anthropic client not initialized"); } const formattedMessages = []; let systemMessage; let startIndex = 0; if (messages[0]?.role === "system") { systemMessage = messages[0].content; startIndex = 1; } for (let i = startIndex; i < messages.length; i++) { const msg = messages[i]; formattedMessages.push({ role: msg.role === "assistant" ? "assistant" : "user", content: msg.content }); } const startTime = performance.now(); try { const response = await this.client.messages.create({ model: options.modelName || "claude-3-7-sonnet-latest", max_tokens: options.maxOutputTokens || 1e5, temperature: options.temperature || 0.7, system: systemMessage, messages: formattedMessages }); const endTime = performance.now(); let content = ""; if (response.content && response.content.length > 0 && "text" in response.content[0]) { content = response.content[0].text; } return { content, inputTokens: response.usage?.input_tokens || 0, outputTokens: response.usage?.output_tokens || 0, modelName: options.modelName || "claude-3-7-sonnet-latest", duration: (endTime - startTime) / 1e3 }; } catch (error) { console.error("Anthropic API error:", error?.message || error); if (error?.status) { console.error(`Status: ${error.status}, Type: ${error?.error?.type || "unknown"}`); } throw error; } } }; __name(_AnthropicProvider, "AnthropicProvider"); AnthropicProvider = _AnthropicProvider; } }); // src/config/pricing.ts var pricingData; var init_pricing = __esm({ "src/config/pricing.ts"() { init_cjs_shims(); pricingData = /* @__PURE__ */ new Map([ // OpenAI Models [ "gpt-4o", { inputCostPerMillion: 2.5, outputCostPerMillion: 10 } ], [ "gpt-4o-mini", { inputCostPerMillion: 0.15, outputCostPerMillion: 0.6 } ], [ "o1", { inputCostPerMillion: 15, outputCostPerMillion: 60 } ], [ "o3-mini", { inputCostPerMillion: 1.1, outputCostPerMillion: 4.4 } ], [ "gpt-4.5-preview", { inputCostPerMillion: 75, outputCostPerMillion: 150 } ], // Anthropic Models [ "claude-3-7-sonnet-latest", { inputCostPerMillion: 3, outputCostPerMillion: 15 } ] ]); } }); // src/services/ai/index.ts var _AiService; exports.AiService = void 0; var init_ai = __esm({ "src/services/ai/index.ts"() { init_cjs_shims(); init_openai(); init_anthropic(); init_pricing(); _AiService = class _AiService { constructor(openAiApiKey, anthropicApiKey) { __publicField(this, "openAiProvider"); __publicField(this, "anthropicProvider"); this.openAiProvider = new OpenAiProvider(openAiApiKey); this.anthropicProvider = new AnthropicProvider(anthropicApiKey); } /** * Check which providers are available */ getAvailableProviders() { return { openai: this.openAiProvider.isAvailable(), anthropic: this.anthropicProvider.isAvailable() }; } /** * Generate a response from a conversation history using the appropriate provider */ async generateResponse(messages, options) { const modelName = options.modelName; const supportedOpenAiModels = [ "gpt-4o", "gpt-4o-mini", "o1", "o3-mini", "gpt-4.5-preview" ]; const supportedAnthropicModels = [ "claude-3-7-sonnet-latest", "claude-3-7-sonnet", "claude-3-sonnet", "claude-3-5-sonnet" ]; const isAnthropicModel = modelName.startsWith("claude") || modelName.includes("claude-") || modelName.includes("anthropic"); if (isAnthropicModel) { const isSupported = supportedAnthropicModels.some((model) => modelName === model || modelName.startsWith(model)); if (!isSupported) { throw new Error(`Unsupported Anthropic model: ${modelName}. Supported models are: ${supportedAnthropicModels.join(", ")}.`); } if (!this.anthropicProvider.isAvailable()) { throw new Error("Anthropic provider not available. Please set ANTHROPIC_API_KEY environment variable."); } return this.anthropicProvider.generateResponse(messages, options); } else { const isSupported = supportedOpenAiModels.some((model) => modelName === model || modelName.startsWith(model)); if (!isSupported) { throw new Error(`Unsupported OpenAI model: ${modelName}. Supported models are: ${supportedOpenAiModels.join(", ")}.`); } if (!this.openAiProvider.isAvailable()) { throw new Error("OpenAI provider not available. Please set OPENAI_API_KEY environment variable."); } return this.openAiProvider.generateResponse(messages, options); } } /** * Calculate cost based on token usage */ calculateCost(model, inputTokens, outputTokens) { let pricing = pricingData.get(model); if (!pricing) { const baseModelMatch = model.match(/^(gpt-4o|gpt-4o-mini|o1|o3-mini|claude-3-opus|claude-3-sonnet|claude-3-haiku|claude-3-5-sonnet|claude-3-7-sonnet)/); const baseModel = baseModelMatch ? baseModelMatch[0] : model; if (baseModel !== model) { pricing = pricingData.get(baseModel); } if (!pricing && model.includes("claude-3-7-sonnet")) { pricing = pricingData.get("claude-3-7-sonnet-latest"); } } if (!pricing) { console.warn(`No pricing data found for model: ${model}`); return { inputCost: 0, outputCost: 0, totalCost: 0 }; } const inputCost = inputTokens / 1e6 * pricing.inputCostPerMillion; const outputCost = outputTokens / 1e6 * pricing.outputCostPerMillion; const totalCost = inputCost + outputCost; return { inputCost, outputCost, totalCost }; } }; __name(_AiService, "AiService"); exports.AiService = _AiService; } }); var _Logger; exports.Logger = void 0; var init_logger = __esm({ "src/utils/logger.ts"() { init_cjs_shims(); _Logger = class _Logger { constructor(verbose = true) { __publicField(this, "verbose"); __publicField(this, "spinner", ora__default.default()); this.verbose = verbose; } /** * Log a message if verbose mode is enabled */ log(message) { if (this.verbose) { console.log(message); } } /** * Log a section header */ logHeader(title) { if (this.verbose) { console.log("\n" + chalk3__default.default.bgBlue.white(` ${title} `) + "\n"); } } /** * Log an info message with blue color */ info(message) { this.log(chalk3__default.default.blue(message)); } /** * Log a success message with green color */ success(message) { this.log(chalk3__default.default.green(message)); } /** * Log a warning message with yellow color */ warn(message) { this.log(chalk3__default.default.yellow(message)); } /** * Log an error message with red color */ error(message) { console.error(chalk3__default.default.red(message)); } /** * Start a spinner with a message */ startSpinner(message) { if (this.verbose) { this.spinner.start(message); } } /** * Stop the spinner with a success message */ succeedSpinner(message) { if (this.verbose) { this.spinner.succeed(message); } } /** * Stop the spinner with a warning message */ warnSpinner(message) { if (this.verbose) { this.spinner.warn(message); } } /** * Stop the spinner with an error message */ failSpinner(message) { if (this.verbose) { this.spinner.fail(message); } } /** * Log data metrics in a formatted way */ logMetrics(label, metrics) { this.log(chalk3__default.default.cyan(`\u{1F4CF} ${label} metrics:`)); Object.entries(metrics).forEach(([key, value]) => { this.log(chalk3__default.default.cyan(` ${key}: ${value}`)); }); } }; __name(_Logger, "Logger"); exports.Logger = _Logger; } }); var _XmlProcessor; exports.XmlProcessor = void 0; var init_xml = __esm({ "src/utils/xml.ts"() { init_cjs_shims(); _XmlProcessor = class _XmlProcessor { constructor() { __publicField(this, "parser"); __publicField(this, "builder"); this.parser = new fastXmlParser.XMLParser({ ignoreAttributes: false, preserveOrder: true }); this.builder = new fastXmlParser.XMLBuilder({ ignoreAttributes: false, format: true, preserveOrder: true }); } /** * Extract content from XML tags * @param text Text containing XML tags * @param tagName Name of the tag to extract (without angle brackets) * @returns The content of the tag, or empty string if tag not found */ extractTagContent(text, tagName) { const regex = new RegExp(`<${tagName}>(.*?)</${tagName}>`, "s"); const match = regex.exec(text); if (match && match[1]) { return match[1].trim(); } const openTagRegex = new RegExp(`<${tagName}>(.*)$`, "s"); const openTagMatch = openTagRegex.exec(text); if (openTagMatch && openTagMatch[1]) { console.warn(`Warning: Found opening <${tagName}> tag but no closing tag. The response might be truncated.`); return openTagMatch[1].trim(); } console.warn(`Warning: Tag <${tagName}> not found in the text.`); return ""; } /** * Extract all matching tags from a text * @param text Text containing XML tags * @param pattern Regular expression pattern to match * @returns Array of tag contents */ extractAllTags(text, pattern) { const matches = text.match(pattern); if (!matches) return []; return matches.map((match) => { const contentMatch = match.match(/>(.*?)</); if (contentMatch && contentMatch[1]) { return contentMatch[1].trim(); } return match.trim(); }); } /** * Parse XML string to object */ parseXml(xmlString) { try { return this.parser.parse(`<root>${xmlString}</root>`); } catch (error) { console.error("Error parsing XML:", error); return null; } } /** * Build XML string from object */ buildXml(obj) { try { return this.builder.build(obj); } catch (error) { console.error("Error building XML:", error); return ""; } } /** * Wrap content in XML tags */ wrapInTags(content, tagName) { return `<${tagName}>${content}</${tagName}>`; } }; __name(_XmlProcessor, "XmlProcessor"); exports.XmlProcessor = _XmlProcessor; } }); async function checkTranslationCompletion(sourceText, translationText, verbose = false, apiKey) { try { const openai$1 = new openai.OpenAI({ apiKey: apiKey || process.env.OPENAI_API_KEY }); if (verbose) { console.log(chalk3__default.default.cyan("\u{1F50D} Checking if translation is complete...")); } const response = await openai$1.chat.completions.create({ model: "gpt-4o-mini", response_format: { type: "json_object" }, messages: [ { role: "system", content: `You are a translation verification assistant. You'll be given a source text and its translation. Your job is to determine if the translation is complete or if it got cut off. If incomplete, identify the last line in the translation and the corresponding source line to continue from. Output JSON with these fields: - continue: boolean indicating if translation needs to continue (true = incomplete) - targetLastLine: the last line of the translation (required if continue=true) - sourceLine: the corresponding source line (required if continue=true) Requirements: 1. Match the last meaningful line in the translation and the corresponding source line 2. Ignore differences in formatting (newlines, spaces) when determining completeness 3. Verify that all major content sections exist in both source and translation` }, { role: "user", content: `Check if this translation is complete: <source_text> ${sourceText} </source_text> <translation> ${translationText} </translation>` } ], temperature: 0, max_tokens: 1024 }); const content = response.choices[0]?.message?.content; if (!content) { throw new Error("No content in response from OpenAI"); } const result = JSON.parse(content); if (verbose) { if (result.continue) { console.log(chalk3__default.default.yellow("\u26A0\uFE0F Translation is incomplete")); console.log(chalk3__default.default.yellow(` Last translated line: "${result.targetLastLine}"`)); console.log(chalk3__default.default.yellow(` Corresponding source line: "${result.sourceLine}"`)); } else { console.log(chalk3__default.default.green("\u2705 Translation is complete")); } } return result; } catch (error) { console.error(chalk3__default.default.red("\u274C Error checking translation completion:"), error); return null; } } function backupPartialTranslation(filePath, backupDir) { try { if (!fs2__namespace.existsSync(filePath)) { console.error(chalk3__default.default.red(`\u274C File not found: ${filePath}`)); return null; } const backupDirectory = backupDir || path3__namespace.join(path3__namespace.dirname(filePath), ".backups"); if (!fs2__namespace.existsSync(backupDirectory)) { fs2__namespace.mkdirSync(backupDirectory, { recursive: true }); } const filename = path3__namespace.basename(filePath); const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); const backupPath = path3__namespace.join(backupDirectory, `${filename}.${timestamp}.bak`); fs2__namespace.copyFileSync(filePath, backupPath); return backupPath; } catch (error) { console.error(chalk3__default.default.red("\u274C Error creating backup:"), error); return null; } } function combineTranslation(partialTranslation, continuationText, targetLastLine) { try { let continued = cleanContinuationText(continuationText); if (!continued || continued.trim().length === 0) { console.log(chalk3__default.default.yellow("\u26A0\uFE0F No usable continuation content found after cleanup")); return partialTranslation; } let cleanedPartialTranslation = removeUnpairedXmlTags(partialTranslation); cleanedPartialTranslation = removeContinuationMarkers(cleanedPartialTranslation); const matchResult = findBestMatchPoint(cleanedPartialTranslation, continued, targetLastLine); if (!matchResult) { console.error(chalk3__default.default.red(`\u274C Could not find a good match point in the partial translation using: "${targetLastLine}"`)); console.log(chalk3__default.default.yellow("\u26A0\uFE0F Falling back to simple append method")); return cleanedPartialTranslation + "\n\n" + continued; } const { matchedText, matchIndex } = matchResult; const textBefore = cleanedPartialTranslation.substring(0, matchIndex + matchedText.length); let continuationAfterMatch = continued; if (continued.includes(matchedText)) { const matchStartInContinuation = continued.indexOf(matchedText); continuationAfterMatch = continued.substring(matchStartInContinuation + matchedText.length); } let combinedText = textBefore; if (!combinedText.endsWith("\n") && !continuationAfterMatch.startsWith("\n")) { continuationAfterMatch = "\n" + continuationAfterMatch; } combinedText += continuationAfterMatch; console.log(chalk3__default.default.green(`\u2705 Successfully combined translation (${textBefore.length} chars + ${continuationAfterMatch.length} chars)`)); return combinedText; } catch (error) { console.error(chalk3__default.default.red("\u274C Error combining translation:"), error); return null; } } function cleanContinuationText(text) { if (!text) return ""; let cleaned = text; const continuedMatch = text.match(/<continued_translation>([\s\S]*?)<\/continued_translation>/); if (continuedMatch && continuedMatch[1]?.trim()) { cleaned = continuedMatch[1].trim(); } else { cleaned = text.trim().replace(/<continued_translation>/g, "").replace(/<\/continued_translation>/g, ""); } cleaned = removeUnpairedXmlTags(cleaned); cleaned = removeContinuationMarkers(cleaned); return cleaned; } function removeUnpairedXmlTags(text) { if (!text) return ""; const openingTags = []; const regex = /<([^\/\s>]+)[^>]*>|<\/([^>]+)>/g; let match; let result = text; const tagsToRemove = []; while ((match = regex.exec(text)) !== null) { const openTag = match[1]; const closeTag = match[2]; if (openTag) { openingTags.push(openTag); } else if (closeTag) { if (openingTags.length > 0 && openingTags[openingTags.length - 1] === closeTag) { openingTags.pop(); } else { tagsToRemove.push(`</${closeTag}>`); } } } for (const tag of openingTags) { const openTagRegex = new RegExp(`<${tag}[^>]*>`, "g"); let tagMatch; while ((tagMatch = openTagRegex.exec(text)) !== null) { if (!tagsToRemove.includes(tagMatch[0])) { tagsToRemove.push(tagMatch[0]); } } } for (const tag of tagsToRemove) { result = result.replace(tag, ""); } return result; } function removeContinuationMarkers(text) { if (!text) return ""; const markers = [ /\(to be continued\.?\.?\.?\)$/i, /\(continued\.?\.?\.?\)$/i, /\.\.\.$/, /\(계속\.?\.?\.?\)$/i, /\(다음 편에 계속\.?\.?\.?\)$/i, /\(fortsetzung folgt\.?\.?\.?\)$/i, /\(continuará\.?\.?\.?\)$/i, /\(à suivre\.?\.?\.?\)$/i, /\(続く\.?\.?\.?\)$/i, /\(未完成\.?\.?\.?\)$/i, /\(未完待续\.?\.?\.?\)$/i ]; let cleaned = text; for (const marker of markers) { cleaned = cleaned.replace(marker, ""); } return cleaned.trim(); } function findBestMatchPoint(partialTranslation, continuation, targetLastLine) { if (!partialTranslation || !continuation) return null; const exactMatch = partialTranslation.lastIndexOf(targetLastLine); if (exactMatch !== -1 && targetLastLine.trim().length > 0) { return { matchedText: targetLastLine, matchIndex: exactMatch }; } const minMatchLength = Math.min(5, targetLastLine.length); let bestMatch = ""; let bestMatchIndex = -1; const endChunk = partialTranslation.substring(Math.max(0, partialTranslation.length - 200)); const startChunk = continuation.substring(0, Math.min(continuation.length, 200)); for (let length = endChunk.length; length >= minMatchLength; length--) { const endPart = endChunk.substring(endChunk.length - length); if (startChunk.includes(endPart)) { bestMatch = endPart; bestMatchIndex = partialTranslation.lastIndexOf(endPart); break; } } if (bestMatchIndex !== -1 && bestMatch.length >= minMatchLength) { return { matchedText: bestMatch, matchIndex: bestMatchIndex }; } const paragraphs = partialTranslation.split(/\n\n+/); if (paragraphs.length > 0) { const lastParagraph = paragraphs[paragraphs.length - 1].trim(); const paragraphIndex = partialTranslation.lastIndexOf(lastParagraph); if (paragraphIndex !== -1 && lastParagraph.length >= minMatchLength) { return { matchedText: lastParagraph, matchIndex: paragraphIndex }; } } return null; } function createSimpleContinuationPrompt(targetLastLine) { return `Your response appeared to be cut off. Please continue your translation from where you left off, starting at this point: "${targetLastLine.trim()}" Please continue in the same style, tone, and approach as you were using before. Don't repeat what you've already translated - just continue from exactly where you left off.`; } var init_continuation = __esm({ "src/utils/continuation.ts"() { init_cjs_shims(); __name(checkTranslationCompletion, "checkTranslationCompletion"); __name(backupPartialTranslation, "backupPartialTranslation"); __name(combineTranslation, "combineTranslation"); __name(cleanContinuationText, "cleanContinuationText"); __name(removeUnpairedXmlTags, "removeUnpairedXmlTags"); __name(removeContinuationMarkers, "removeContinuationMarkers"); __name(findBestMatchPoint, "findBestMatchPoint"); __name(createSimpleContinuationPrompt, "createSimpleContinuationPrompt"); } }); function ensureDirectoryExists(dirPath) { if (!fs2__namespace.existsSync(dirPath)) { fs2__namespace.mkdirSync(dirPath, { recursive: true }); } } function saveJson(filePath, data) { try { fs2__namespace.writeFileSync(filePath, JSON.stringify(data, null, 2)); } catch (error) { console.error(`Error saving JSON to ${filePath}:`, error); throw error; } } function loadJson(filePath, defaultValue) { try { if (fs2__namespace.existsSync(filePath)) { const data = fs2__namespace.readFileSync(filePath, "utf-8"); return JSON.parse(data); } return defaultValue; } catch (error) { console.error(`Error loading JSON from ${filePath}:`, error); return defaultValue; } } function saveText(filePath, text) { try { ensureDirectoryExists(path3__namespace.dirname(filePath)); fs2__namespace.writeFileSync(filePath, text); } catch (error) { console.error(`Error saving text to ${filePath}:`, error); throw error; } } function loadText(filePath, defaultValue = "") { try { if (fs2__namespace.existsSync(filePath)) { return fs2__namespace.readFileSync(filePath, "utf-8"); } return defaultValue; } catch (error) { console.error(`Error loading text from ${filePath}:`, error); return defaultValue; } } function findLatestFile(directory, fileNames) { for (const fileName of fileNames) { const filePath = path3__namespace.join(directory, fileName); if (fs2__namespace.existsSync(filePath)) { return filePath; } } return null; } function findFilesInDirectory(directory, pattern) { try { if (!fs2__namespace.existsSync(directory)) { return []; } const files = fs2__namespace.readdirSync(directory); return files.filter((file) => pattern.test(file)).map((file) => path3__namespace.join(directory, file)); } catch (error) { console.error(`Error finding files in ${directory}:`, error); return []; } } var init_filesystem = __esm({ "src/utils/filesystem.ts"() { init_cjs_shims(); __name(ensureDirectoryExists, "ensureDirectoryExists"); __name(saveJson, "saveJson"); __name(loadJson, "loadJson"); __name(saveText, "saveText"); __name(loadText, "loadText"); __name(findLatestFile, "findLatestFile"); __name(findFilesInDirectory, "findFilesInDirectory"); } }); // src/utils/formatters.ts function formatTime(timeInMinutes) { if (timeInMinutes < 1) { return `${Math.round(timeInMinutes * 60)} seconds`; } else { const minutes = Math.floor(timeInMinutes); const seconds = Math.round((timeInMinutes - minutes) * 60); return `${minutes} min ${seconds} sec`; } } function formatDuration(seconds) { if (seconds < 60) { return `${Math.round(seconds)} seconds`; } else if (seconds < 3600) { const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.round(seconds % 60); return `${minutes} min ${remainingSeconds} sec`; } else { const hours = Math.floor(seconds / 3600); const minutes = Math.floor(seconds % 3600 / 60); const remainingSeconds = Math.round(seconds % 60); return `${hours} hr ${minutes} min ${remainingSeconds} sec`; } } function createProgressBar(ratio, validSource = true) { if (!validSource) { return "N/A (source metrics unavailable)"; } const width = 20; const filled = Math.round(ratio * width); const empty = width - filled; let bar = ""; if (ratio > 1) { bar = "\u2593".repeat(width) + " +" + Math.round((ratio - 1) * 100) + "%"; } else if (ratio >= 0) { bar = "\u2593".repeat(filled) + "\u2591".repeat(empty); } else { bar = "Invalid ratio"; } return bar; } var init_formatters = __esm({ "src/utils/formatters.ts"() { init_cjs_shims(); __name(formatTime, "formatTime"); __name(formatDuration, "formatDuration"); __name(createProgressBar, "createProgressBar"); } }); // src/utils/metrics.ts function calculateMetrics(text, isSourceText = false, sourceMetrics) { if (!text) { return { sourceWordCount: 0, targetWordCount: 0, sourceCharCount: 0, targetCharCount: 0, ratio: 0, estimatedReadingTime: 0 }; } const charCount = text.replace(/\s/g, "").length; let wordCount = 0; wordCount = text.split(/\s+/).filter((word) => word.length > 0).length; const readingTime = wordCount / 200; const sourceW = isSourceText ? wordCount : sourceMetrics?.sourceWordCount ?? 0; const sourceC = isSourceText ? charCount : sourceMetrics?.sourceCharCount ?? 0; return { sourceWordCount: sourceW, targetWordCount: wordCount, sourceCharCount: sourceC, targetCharCount: charCount, ratio: sourceW > 0 ? wordCount / sourceW : 0, estimatedReadingTime: readingTime }; } function calculateMetricsForLanguage(text, language, isSourceText = false, sourceMetrics) { if (!text) { return { sourceWordCount: 0, targetWordCount: 0, sourceCharCount: 0, targetCharCount: 0, ratio: 0, estimatedReadingTime: 0 }; } const charCount = text.replace(/\s/g, "").length; let wordCount = 0; const effectiveLanguage = language?.toLowerCase() || ""; if (effectiveLanguage === "korean" || effectiveLanguage === "japanese" || effectiveLanguage === "chinese") { wordCount = Math.round(charCount / 2); } else { wordCount = text.split(/\s+/).filter((word) => word.length > 0).length; } const readingTime = wordCount / 200; const sourceW = isSourceText ? wordCount : sourceMetrics?.sourceWordCount ?? 0; const sourceC = isSourceText ? charCount : sourceMetrics?.sourceCharCount ?? 0; return { sourceWordCount: sourceW, targetWordCount: wordCount, sourceCharCount: sourceC, targetCharCount: charCount, ratio: sourceW > 0 ? wordCount / sourceW : 0, estimatedReadingTime: readingTime }; } var init_metrics = __esm({ "src/utils/metrics.ts"() { init_cjs_shims(); __name(calculateMetrics, "calculateMetrics"); __name(calculateMetricsForLanguage, "calculateMetricsForLanguage"); } }); // src/utils/constants.ts var MODEL_CONTEXT_LIMITS; var init_constants = __esm({ "src/utils/constants.ts"() { init_cjs_shims(); MODEL_CONTEXT_LIMITS = { // OpenAI models "gpt-4.5-preview": 128e3, "gpt-4o": 128e3, "gpt-4-turbo": 128e3, "gpt-4-32k": 32e3, "gpt-4": 8192, "gpt-3.5-turbo-16k": 16384, "gpt-3.5-turbo": 4096, // Claude models "claude-3-opus-20240229": 2e5, "claude-3-sonnet-20240229": 2e5, "claude-3-haiku-20240307": 2e5, "claude-3-5-sonnet-20240620": 2e5, "claude-3-7-sonnet-latest": 2e5, // Default fallback (conservative estimate) default: 16384 }; } }); // src/utils/index.ts var init_utils = __esm({ "src/utils/index.ts"() { init_cjs_shims(); init_continuation(); init_filesystem(); init_formatters(); init_metrics(); init_logger(); init_xml(); init_constants(); } }); // src/prompts/translation.ts var prompts; var init_translation = __esm({ "src/prompts/translation.ts"() { init_cjs_shims(); prompts = { system: /* @__PURE__ */ __name((targetLanguage, sourceLanguage, customInstructions, currentStep) => { let systemPrompt = `You are an expert literary translator with deep fluency in ${targetLanguage}${sourceLanguage ? ` and ${sourceLanguage}` : ""}. Your goal is to create a high-quality translation that preserves the original's tone, style, literary devices, cultural nuances, and overall impact. You prioritize readability and naturalness in the target language while staying faithful to the source text's meaning and intention${sourceLanguage ? "" : " (you may need to infer the source language from the text provided)"}. CRITICAL: You must ALWAYS translate the COMPLETE text without omitting any content. Ensure your translations include EVERY part of the source text from beginning to end. If the text is lengthy, you must still translate it in its entirety. `; if (currentStep) { systemPrompt += `For this step, place your response inside the following XML tag(s) for easy extraction: `; switch (currentStep) { case "initial_analysis": systemPrompt += `- <analysis>your analysis here</analysis> `; break; case "expression_exploration": systemPrompt += `- <expression_exploration>your exploration here</expression_exploration> `; break; case "cultural_discussion": systemPrompt += `- <cultural_discussion>your discussion here</cultural_discussion> `; break; case "title_options": systemPrompt += `- <title_options>your title suggestions here</title_options> `; break; case "first_translation": systemPrompt += `- <first_translation>your COMPLETE translation here</first_translation> `; break; case "self_critique": systemPrompt += `- <critique>your critique here</critique> `; systemPrompt += `- <improved_translation>your COMPLETE improved translation here</improved_translation> `; break; case "further_refinement": systemPrompt += `- <second_critique>your second critique here</second_critique> `; systemPrompt += `- <further_improved_translation>your COMPLETE further improved translation here</further_improved_translation> `; break; case "final_translation": systemPrompt += `- <final_translation>your COMPLETE final translation here</final_translation> `; break; case "external_review": systemPrompt += `- <external_review>your external review here</external_review> `; break; case "apply_feedback": systemPrompt += `- <refined_final_translation>your COMPLETE refined final translation here</refined_final_translation> `; break; case "continue_translation": systemPrompt += `- <continued_translation>your continuation of the translation here</continued_translation> `; systemPrompt += `You are specifically tasked with continuing a translation that was cut off or incomplete. Follow the instructions carefully about where to continue from. `; break; default: systemPrompt += `Always place your translations inside appropriate XML tags for easy extraction: - Initial analysis: <analysis>your analysis here</analysis> - Expression exploration: <expression_exploration>your exploration here</expression_exploration> - Cultural discussion: <cultural_discussion>your discussion here</cultural_discussion> - Title options: <title_options>your title suggestions here</title_options> - First draft translation: <first_translation>your COMPLETE translation here</first_translation> - Critique: <critique>your critique here</critique> - Improved translation: <improved_translation>your COMPLETE improved translation here</improved_translation> - Second critique: <second_critique>your second critique here</second_critique> - Further improved translation: <further_improved_translation>your COMPLETE further improved translation here</further_improved_translation> - Comprehensive review: <review>your comprehensive review here</review> - Final translation: <final_translation>your COMPLETE final translation here</final_translation> - External review: <external_review>your external review here</external_review> - Refined final translation: <refined_final_translation>your COMPLETE refined final translation here</refined_final_translation> - Continuation: <continued_translation>your continuation of the translation here</continued_translation> `; } } else { systemPrompt += `Always place your translations inside appropriate XML tags for easy extraction: - Initial analysis: <analysis>your analysis here</analysis> - Expression exploration: <expression_exploration>your exploration here</expression_exploration> - Cultural discussion: <cultural_discussion>your discussion here</cultural_discussion> - Title options: <title_options>your title suggestions here</title_options> - First draft translation: <first_translation>your COMPLETE translation here</first_translation> - Critique: <critique>your critique here</critique> - Improved translation: <improved_translation>your COMPLETE improved translation here</improved_translation> - Second critique: <second_critique>your second critique here</second_critique> - Further improved translation: <further_improved_translation>your COMPLETE further improved translation here</further_improved_translation> - Comprehensive review: <review>your comprehensive review here</review> - Final translation: <final_translation>your COMPLETE final translation here</final_translation> - External review: <external_review>your external review here</external_review> - Refined final translation: <refined_final_translation>your COMPLETE refined final translation here</refined_final_translation> - Continuation: <continued_translation>your continuation of the translation here</continued_translation> `; } systemPrompt += `Your tone should be conversational and thoughtful, as if you're discussing the translation process with a colleague. Think deeply about cultural context, idiomatic expressions, and literary devices that would resonate with native ${targetLanguage} speakers. Work through the translation step by step, maintaining the voice and essence of the original while making it feel naturally written in ${targetLanguage}. Your output length is unlocked so you can produce at least 30K tokens in your output if needed - ensure you NEVER truncate or omit any part of the text.`; if (customInstructions) { systemPrompt += ` Additional instructions for this translation: ${customInstructions}`; } return systemPrompt; }, "system"), initialAnalysis: /* @__PURE__ */ __name((sourceText, targetLanguage, sourceLanguage) => `I'd like your help translating a text into ${targetLanguage}${sourceLanguage ? ` from ${sourceLanguage}` : ""}. Before we start, could you analyze what we'll need to preserve in terms of tone, style, meaning, and cultural nuances? Here's the text to be translated: <source_text> ${sourceText} </source_text> Please analyze this text thoughtfully. What are the key elements that make this text distinctive? What tone, voice, argument structure, rhetorical devices, and cultural references should we be careful to preserve in translation? NOTE: Do not translate the text yet. This is just an analysis step to understand the text's distinctive elements. Remember to put your analysis in <analysis> tags.`, "initialAnalysis"), expressionExploration: /* @__PURE__ */ __name((sourceText, targetLanguage) => `Now that we've analyzed the text, let's explore how we might express key elements in ${targetLanguage}. Here's the original text for reference: <source_text> ${sourceText} </source_text> Could you identify 5-10 key phrases, idioms, cultural references, or stylistic elements from this text that might be challenging to translate? For each one, could you suggest how you might express it in ${targetLanguage} to preserve the intended meaning, tone, and effect? NOTE: This is not a full translation yet - we're just exploring key expressions to understand how to approach them. Please format your exploration within <expression_exploration> tags.`, "expressionExploration"), toneAndCulturalDiscussion: /* @__PURE__ */ __name((sourceText, targetLanguage) => `Let's discuss the cultural adaptation aspects of this translation. Here's the original text again for reference: <source_text> ${sourceText} </source_text> What cultural adaptations might be necessary for ${targetLanguage} readers? Are there any references, analogies, or concepts that would need special consideration for the target audience? How should we handle the overall tone and voice to ensure it resonates with ${targetLanguage} readers while staying faithful to the original? Are there any cultural sensitivities to be aware of? Remember, we're still not translating the full text yet - just discussing how we'll approach cultural elements. Please put your discussion within <cultural_discussion> tags.`, "toneAndCulturalDiscussion"), titleAndInspirationExploration: /* @__PURE__ */ __name((sourceText, targetLanguage) => `Let's now consider how to translate the title and any literary inspirations we might draw from to create a compelling translation. Here's the original text for reference: <source_text> ${sourceText} </source_text> 1. How should we translate the title to capture its essence in ${targetLanguage}? Please provide a few options with your reasoning. 2. Are there any well-known ${targetLanguage} literary works, authors, or stylistic traditions that might provide inspiration for our approach? How might these influences help us create a translation that feels natural and resonant to native speakers? Please share your thoughts within <title_options> tags.`, "titleAndInspirationExploration"), firstTranslationAttempt: /* @__PURE__ */ __name((sourceText, targetLanguage) => `Now that we've analyzed the text and discussed key considerations, I'd like you to create a first draft translation into ${targetLanguage}. Here is the source text: <source_text> ${sourceText} </source_text> Based on our previous discussions about tone, style, cultural nuances, and specific expressions, please translate the COMPLETE text. Aim to preserve the original's meaning, impact, and feeling while making it sound natural in ${targetLanguage}. IMPORTANT: You must translate the ENTIRE text without omitting any paragraphs, sentences, or content. Ensure your translation is complete from beginning to end. If the text is lengthy, make sure you include everything. Please place your complete translation within <first_translation> tags.`, "firstTranslationAttempt"), selfCritiqueAndRefinement: /* @__PURE__ */ __name((targetLanguage, sourceLanguage, sourceText, previousTranslation) => `Now that we have our first draft, I'd love for you to review it critically. What do you think are the strengths and weaknesses of this translation? Here is the original text for reference: <source_text> ${sourceText} </source_text> And here is the first draft translation: <previous_translation> ${previousTranslation} </previous_translation> Could you analyze aspects like: - Sentence structure and flow - Word choice and terminology - How well cultural elements were adapted - The preservation of the original's tone and voice - Poetic quality and literary devices - Overall readability and naturalness in ${targetLanguage} - COMPLETENESS: Check if any sentences or paragraphs were omitted from the original text After providing your critique, please offer an improved version of the translation that addresses the issues you identified. Providing the complete improved translation allows for easier comparison and usability. IMPORTANT: Ensure your improved translation includes EVERY part of the source text with nothing omitted. Double-check that no content is missing. Please put your critique in <critique> tags and your complete improved translation in <improved_translation> tags.`, "selfCritiqueAndRefinement"), furtherRefinement: /* @__PURE__ */ __name((targetLanguage, sourceLanguage, sourceText, previousTranslation) => `Let's take a fresh look at our translation with new eyes. Here is the original text: <source_text> ${sourceText} </source_text> And here is our current translation: <previous_translation> ${previousTranslation} </previous_translation> I'd like you to review this translation again, but this time paying special attention to: 1. Naturalness: Does it flow as if it were originally written in ${targetLanguage}? 2. Fidelity: Does it capture the full meaning and nuance of the original? 3. Completeness: Is EVERY part of the source text included in the translation? 4. Impact: Does it have the same emotional and rhetorical effect as the original? 5. Consistency: Are terms, tone, and style consistent throughout? 6. Cultural resonance: Would it connect with native ${targetLanguage} speakers on a cultural level? After your critique, please provide a further improved version that addresses any issues you find. IMPORTANT: Verify that no content has been omitted. Your translation must include EVERY part of the original text from start to finish. Please put your critique in <second_critique> tags and your improved translation in <further_improved_translation> tags.`, "furtherRefinement"), finalTranslation: /* @__PURE__ */ __name((targetLanguage, sourceLanguage, sourceText, previousTranslation) => `Now it's time for our final comprehensive review and translation. Here's the original text: <source_text> ${sourceText} </source_text> And our current translation: <previous_translation> ${previousTranslation} </previous_translation> Let's do one final pass to perfect this translation. Consider all the aspects we've discussed: - Overall flow and readability - Accuracy and fidelity to the original - Cultural adaptation - Natural expression in ${targetLanguage} - Preservation of tone, style, and voice - Literary quality - COMPLETENESS: Ensure that EVERY part of the source text is translated After your review, please provide what you consider to be the final, polished translation. CRITICAL: Your final translation MUST include the ENTIRE source text with no omissions whatsoever. Double-check that you have translated everything completely from beginning to end. Please put your final translation in <final_translation> tags.`, "finalTranslation"), externalReviewSystem: /* @__PURE__ */ __name((targetLanguage, sourceLanguage) => `You are an expert literary translator and critic with deep fluency in ${targetLanguage}${sourceLanguage ? ` and ${sourceLanguage}` : ""}. Your task is to critically review a translation ${sourceLanguage ? `from ${sourceLanguage} ` : ""}to ${targetLanguage}, providing detailed, constructive feedback on how well it captures