665 lines
22 KiB
JavaScript
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}`;
|
|
}
|
|
}
|
|
} |