Files
2025-07-23 21:14:31 +09:00

665 lines
22 KiB
JavaScript

/**
* ComfyUI Prompt Translator Frontend Extension
* Adds custom translation buttons with direct execution
*/
import { app } from "../../scripts/app.js";
import { ComfyWidgets } from "../../scripts/widgets.js";
// Extension name
const EXTENSION_NAME = "BuimenLabo_PromptTranslator";
// Custom button widget creation
function createTranslationButton(node, name, label, callback) {
const button = document.createElement("button");
button.textContent = label;
button.className = "comfy-button translate-button";
button.style.cssText = `
width: 190px;
height: 35px;
background: #4a90e2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
margin: 2px;
transition: background-color 0.2s;
`;
button.addEventListener("click", callback);
button.addEventListener("mouseenter", () => {
if (!button.disabled) {
button.style.background = "#5aa0f2";
}
});
button.addEventListener("mouseleave", () => {
if (!button.disabled) {
button.style.background = "#4a90e2";
}
});
return button;
}
// Enhanced node creation with custom buttons
function enhancePromptTranslatorNode(node) {
// Set node properties
node.properties = node.properties || {};
node.properties.translating = false;
// Add input text change listener for real-time token counting
const originalOnInputChange = node.onInputChange;
node.onInputChange = function() {
if (originalOnInputChange) {
originalOnInputChange.call(this);
}
updateTokenCountDisplay(this);
};
// Watch for widget value changes
const originalOnWidgetChange = node.onWidgetChange;
node.onWidgetChange = function(widget) {
if (originalOnWidgetChange) {
originalOnWidgetChange.call(this, widget);
}
if (widget.name === "input_text") {
updateTokenCountDisplay(this);
}
};
// Add single translation button
const buttonContainer = document.createElement("div");
buttonContainer.className = "translation-button-container";
buttonContainer.style.cssText = `
display: flex;
margin: 5px 0;
justify-content: center;
`;
// Single Translation button
const translateButton = createTranslationButton(
node,
"execute_translation",
"Translation",
() => executeTranslation(node)
);
translateButton.style.width = "390px"; // Full width
buttonContainer.appendChild(translateButton);
// Add button to node
node.addDOMWidget("translation_button", "div", buttonContainer);
// Add token counter display
const tokenContainer = document.createElement("div");
tokenContainer.className = "token-counter-container";
tokenContainer.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
background: #1a1a1a;
border-radius: 4px;
margin: 5px 0;
border: 1px solid #555;
`;
const tokenLabel = document.createElement("span");
tokenLabel.textContent = "Tokens: 0";
tokenLabel.style.cssText = `
font-family: monospace;
font-size: 12px;
color: white;
font-weight: bold;
`;
const statusLabel = document.createElement("span");
statusLabel.textContent = "Ready";
statusLabel.style.cssText = `
font-family: monospace;
font-size: 12px;
color: #ccc;
`;
tokenContainer.appendChild(tokenLabel);
tokenContainer.appendChild(statusLabel);
node.addDOMWidget("token_counter", "div", tokenContainer);
// Store references for easy access
node.translateButton = translateButton;
node.tokenLabel = tokenLabel;
node.statusLabel = statusLabel;
// Add input text monitoring
setTimeout(() => {
const inputWidget = node.widgets.find(w => w.name === "input_text");
if (inputWidget && inputWidget.element) {
inputWidget.element.addEventListener('input', () => {
updateTokenCountDisplay(node);
});
}
}, 100);
// Initial token count update
updateTokenCountDisplay(node);
}
// Execute translation directly
async function executeTranslation(node) {
// Get input text
const inputWidget = node.widgets.find(w => w.name === "input_text");
const inputText = inputWidget ? inputWidget.value : "";
if (!inputText.trim()) {
alert("Please enter text to translate");
return;
}
// Get translation settings
const providerWidget = node.widgets.find(w => w.name === "provider");
const translationModeWidget = node.widgets.find(w => w.name === "translation_mode");
const otherLanguageWidget = node.widgets.find(w => w.name === "other_language");
const apiKeyWidget = node.widgets.find(w => w.name === "api_key");
const translatedTextWidget = node.widgets.find(w => w.name === "translated_text");
const provider = providerWidget ? providerWidget.value : "Google";
const translationMode = translationModeWidget ? translationModeWidget.value : "Other Language → English";
const otherLanguage = otherLanguageWidget ? otherLanguageWidget.value : "Japanese";
const apiKey = apiKeyWidget ? apiKeyWidget.value : "";
console.log(`🔄 Translation: "${inputText}" (${translationMode}, ${otherLanguage})`);
console.log(`📡 Provider: ${provider}, API key: ${apiKey ? 'provided' : 'none'}`);
// Update UI state
disableButtons(node, true);
try {
// Show translating status
if (translatedTextWidget) {
translatedTextWidget.value = "Translating...";
}
// Set execute_translation widget to true
const executeWidget = node.widgets.find(w => w.name === "execute_translation");
if (executeWidget) {
executeWidget.value = true;
}
// Call translation function directly
let result;
try {
console.log(`🚀 Calling translation API...`);
result = await callTranslationAPINew(provider, inputText, apiKey, translationMode, otherLanguage);
console.log(`✅ Translation result: "${result}"`);
// Update translated text widget
if (translatedTextWidget) {
translatedTextWidget.value = result;
}
} catch (error) {
console.error(`❌ Translation error:`, error);
throw error;
}
// Calculate and display token count
const originalTokens = countTokensSimple(inputText);
const translatedTokens = countTokensSimple(result);
const totalTokens = originalTokens + translatedTokens;
if (node.tokenLabel) {
node.tokenLabel.textContent = `Tokens: ${totalTokens} (${originalTokens}+${translatedTokens})`;
}
if (node.statusLabel) {
node.statusLabel.textContent = "Complete";
node.statusLabel.style.color = "#5cb85c";
}
// Reset button states
setTimeout(() => {
if (jaToEnWidget) jaToEnWidget.value = false;
if (enToJaWidget) enToJaWidget.value = false;
}, 100);
} catch (error) {
console.error("Translation failed:", error);
const translatedTextWidget = node.widgets.find(w => w.name === "translated_text");
if (translatedTextWidget) {
translatedTextWidget.value = `Error: ${error.message}`;
}
if (node.statusLabel) {
node.statusLabel.textContent = "Error";
node.statusLabel.style.color = "#d9534f";
}
} finally {
disableButtons(node, false);
}
}
// New API call for unified translation
async function callTranslationAPINew(provider, text, apiKey, translationMode, otherLanguage) {
// For Google free translation, use direct API
if (provider === "Google" && !apiKey.trim()) {
// Convert mode to direction for Google API
const direction = translationMode === "Other Language → English" ?
(otherLanguage === "Japanese" ? "日→英" : "other→英") :
(otherLanguage === "Japanese" ? "英→日" : "英→other");
return await translateGoogle(text, direction);
} else {
// Use ComfyUI backend API for all other providers
return await translateViaBackendNew(provider, text, apiKey, translationMode, otherLanguage);
}
}
// Legacy API call (kept for compatibility)
async function callTranslationAPI(provider, text, apiKey, direction) {
// For Google free translation, use direct API
if (provider === "Google" && !apiKey.trim()) {
return await translateGoogle(text, direction);
} else {
// Use ComfyUI backend API for all other providers
return await translateViaBackend(provider, text, apiKey, direction);
}
}
// Translate via ComfyUI backend API
async function translateViaBackend(provider, text, apiKey, direction) {
try {
console.log(`🌐 Backend API call: ${provider} "${text}" ${direction}`);
// Use FormData as required by ComfyUI
const formData = new FormData();
formData.append('provider', provider);
formData.append('input_text', text);
formData.append('api_key', apiKey);
formData.append('direction', direction);
const response = await fetch('/prompt_translator/translate', {
method: 'POST',
body: formData
});
console.log(`📡 Backend response status: ${response.status}`);
if (!response.ok) {
const errorText = await response.text();
console.error(`❌ Backend error response:`, errorText);
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const result = await response.json();
console.log(`📋 Backend response:`, result);
if (!result.success) {
throw new Error(result.error || 'Translation failed');
}
return result.translated_text;
} catch (error) {
console.error(`💥 Backend API error:`, error);
if (error.name === 'TypeError' && error.message.includes('fetch')) {
throw new Error(`Backend API not available. Please restart ComfyUI.`);
}
throw error;
}
}
// New backend API call for unified translation
async function translateViaBackendNew(provider, text, apiKey, translationMode, otherLanguage) {
try {
console.log(`🌐 Backend API call: ${provider} "${text}" (${translationMode}, ${otherLanguage})`);
// Use FormData as required by ComfyUI
const formData = new FormData();
formData.append('provider', provider);
formData.append('input_text', text);
formData.append('api_key', apiKey);
formData.append('translation_mode', translationMode);
formData.append('other_language', otherLanguage);
const response = await fetch('/prompt_translator/translate_new', {
method: 'POST',
body: formData
});
console.log(`📡 Backend response status: ${response.status}`);
if (!response.ok) {
const errorText = await response.text();
console.error(`❌ Backend error response:`, errorText);
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const result = await response.json();
console.log(`📋 Backend response:`, result);
if (!result.success) {
throw new Error(result.error || 'Translation failed');
}
return result.translated_text;
} catch (error) {
console.error(`💥 Backend API error:`, error);
if (error.name === 'TypeError' && error.message.includes('fetch')) {
throw new Error(`Backend API not available. Please restart ComfyUI.`);
}
throw error;
}
}
// Google Translate free API
async function translateGoogle(text, direction) {
try {
const sourceLang = direction === "日→英" ? "ja" : "en";
const targetLang = direction === "日→英" ? "en" : "ja";
const encodedText = encodeURIComponent(text);
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodedText}`;
const response = await fetch(url);
const result = await response.json();
if (result && result[0] && result[0][0]) {
return result[0][0][0];
} else {
throw new Error("No translation result");
}
} catch (error) {
throw new Error(`Google translation failed: ${error.message}`);
}
}
// DeepL API requires backend processing due to CORS
// This function is no longer used - DeepL calls go through Python backend
async function translateDeepL(text, apiKey, direction) {
throw new Error("DeepL translation must use backend (Run button) due to CORS restrictions");
}
// Simple token counting function
function countTokensSimple(text) {
if (!text || !text.trim()) {
return 0;
}
// Remove extra whitespace and split
const words = text.trim().split(/\s+/);
// Count tokens: words + punctuation
let tokenCount = 0;
for (const word of words) {
// Count word tokens
tokenCount += 1;
// Add extra tokens for punctuation
const punctuationCount = (word.match(/[.,!?;:"()\[\]{}\/-]/g) || []).length;
tokenCount += punctuationCount;
}
return Math.max(1, tokenCount); // Ensure at least 1 token for non-empty text
}
// Disable/enable buttons
function disableButtons(node, disabled) {
if (node.translateButton) {
node.translateButton.disabled = disabled;
node.translateButton.style.background = disabled ? "#666" : "#4a90e2";
node.translateButton.textContent = disabled ? "Translating..." : "Translation";
}
if (node.statusLabel) {
if (disabled) {
node.statusLabel.textContent = "Translating...";
node.statusLabel.style.color = "#4a90e2";
} else {
node.statusLabel.textContent = "Ready";
node.statusLabel.style.color = "#ccc";
}
}
}
// Register extension
app.registerExtension({
name: EXTENSION_NAME,
async nodeCreated(node) {
console.log("🔍 Prompt Translator - Node created:", node.comfyClass);
// Support both original and BuimenLabo versions
if (node.comfyClass === "PromptTranslator" || node.comfyClass === "BuimenLabo_PromptTranslator") {
console.log("🚀 Enhancing Prompt Translator node:", node.comfyClass);
enhancePromptTranslatorNode(node);
}
},
async setup() {
// Add custom CSS
const style = document.createElement("style");
style.textContent = `
.translation-buttons-container {
display: flex;
gap: 10px;
margin: 5px 0;
justify-content: center;
}
.translate-button {
width: 190px;
height: 35px;
background: #4a90e2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
margin: 2px;
transition: background-color 0.2s;
}
.translate-button:hover:not(:disabled) {
background: #5aa0f2;
}
.translate-button:disabled {
background: #666;
cursor: not-allowed;
}
.translate-button:active {
background: #3a7bc8;
transform: translateY(1px);
}
.token-counter-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
background: #1a1a1a;
border-radius: 4px;
margin: 5px 0;
border: 1px solid #555;
}
.token-counter-container span {
font-family: monospace;
font-size: 12px;
}
`;
document.head.appendChild(style);
}
});
// Show translation preview and confirmation dialog
function showTranslationPreview(node, provider, inputText, direction, apiKey) {
// Create modal dialog
const modal = document.createElement("div");
modal.className = "translation-preview-modal";
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
font-family: Arial, sans-serif;
`;
const dialog = document.createElement("div");
dialog.style.cssText = `
background: #2a2a2a;
border-radius: 8px;
padding: 20px;
max-width: 500px;
width: 90%;
color: white;
border: 2px solid #4a90e2;
`;
const apiKeyDisplay = apiKey ? apiKey.substring(0, 8) + "..." : "None";
dialog.innerHTML = `
<h3 style="margin: 0 0 15px 0; color: #4a90e2;">🔄 Translation Preview</h3>
<div style="margin-bottom: 15px;">
<strong>Provider:</strong> ${provider}<br>
<strong>Direction:</strong> ${direction}<br>
<strong>API Key:</strong> ${apiKeyDisplay}
</div>
<div style="margin-bottom: 20px;">
<strong>Text to translate:</strong><br>
<div style="background: #1a1a1a; padding: 10px; border-radius: 4px; margin-top: 5px; border-left: 3px solid #4a90e2;">
"${inputText}"
</div>
</div>
<div style="text-align: center;">
<button id="confirm-translate" style="
background: #5cb85c;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
margin-right: 10px;
cursor: pointer;
font-weight: bold;
">✅ Execute Run Button</button>
<button id="cancel-translate" style="
background: #d9534f;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
">❌ Cancel</button>
</div>
<div style="margin-top: 15px; padding: 10px; background: #333; border-radius: 4px; font-size: 12px; color: #ccc;">
💡 <strong>Instructions:</strong><br>
1. Review the translation settings above<br>
2. Click "Execute Run Button" to proceed<br>
3. The Run button will be automatically triggered<br>
4. Translation result will appear in the output
</div>
`;
modal.appendChild(dialog);
document.body.appendChild(modal);
// Add event listeners
const confirmBtn = dialog.querySelector("#confirm-translate");
const cancelBtn = dialog.querySelector("#cancel-translate");
confirmBtn.addEventListener("click", async () => {
document.body.removeChild(modal);
// Update status
if (node.statusLabel) {
node.statusLabel.textContent = "Executing...";
node.statusLabel.style.color = "#4a90e2";
}
// Auto-trigger the run button
await autoExecuteRunButton();
});
cancelBtn.addEventListener("click", () => {
document.body.removeChild(modal);
// Reset status
if (node.statusLabel) {
node.statusLabel.textContent = "Cancelled";
node.statusLabel.style.color = "#ccc";
}
const translatedTextWidget = node.widgets.find(w => w.name === "translated_text");
if (translatedTextWidget) {
translatedTextWidget.value = "";
}
});
// Close on outside click
modal.addEventListener("click", (e) => {
if (e.target === modal) {
cancelBtn.click();
}
});
}
// Auto-trigger the Run button
async function autoExecuteRunButton() {
try {
// Find and click the queue/run button
const queueButton = document.querySelector("#queue-button") ||
document.querySelector(".queue-button") ||
document.querySelector("[title*='Queue']") ||
document.querySelector("button:contains('Queue')");
if (queueButton) {
queueButton.click();
console.log("Run button triggered successfully");
} else {
// Try ComfyUI API methods
if (app.queuePrompt) {
const workflow = app.graph.serialize();
await app.queuePrompt(-1, workflow);
} else {
throw new Error("Could not find Run button to execute");
}
}
} catch (error) {
console.error("Auto-execution failed:", error);
alert(`Failed to execute Run button: ${error.message}\nPlease click the Run button manually.`);
}
}
// Update token count display
function updateTokenCountDisplay(node) {
const inputWidget = node.widgets.find(w => w.name === "input_text");
const translatedTextWidget = node.widgets.find(w => w.name === "translated_text");
if (node.tokenLabel) {
const inputText = inputWidget ? inputWidget.value : "";
const translatedText = translatedTextWidget ? translatedTextWidget.value : "";
const inputTokens = countTokensSimple(inputText);
const translatedTokens = countTokensSimple(translatedText);
const totalTokens = inputTokens + translatedTokens;
if (translatedText && translatedText.trim()) {
node.tokenLabel.textContent = `Tokens: ${totalTokens} (${inputTokens}+${translatedTokens})`;
} else {
node.tokenLabel.textContent = `Tokens: ${inputTokens}`;
}
}
}