feat(imggen): авто-перевод промпта на английский перед FLUX

FLUX лучше понимает английский. Если в промпте есть кириллица — прогоняем
через тот же LLM-провайдер ассистента (callLLMFailover, с failover) и
отправляем перевод. При сбое перевода — исходный текст. callLLMFailover
теперь экспортируется из assistantController.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-12 11:20:05 +03:00
parent c75e331c02
commit 4e8c0841db
2 changed files with 21 additions and 2 deletions
@@ -616,4 +616,4 @@ async function flashcardsFromText(req, res) {
res.json({ title, cards }); res.json({ title, cards });
} }
module.exports = { getContext, markSeen, dismiss, setSettings, ask, flashcardsFromText, feedback, getMemory, clearMemory, getStudentProfile, llmConfig, pingLLM, clearFailover: _clearFailover }; module.exports = { getContext, markSeen, dismiss, setSettings, ask, flashcardsFromText, feedback, getMemory, clearMemory, getStudentProfile, llmConfig, pingLLM, clearFailover: _clearFailover, callLLMFailover };
+20 -1
View File
@@ -17,6 +17,23 @@ function _cfg() {
} }
function _enabled() { const c = _cfg(); return !!(c && c.provider === 'cloudflare' && c.accountId && c.token); } function _enabled() { const c = _cfg(); return !!(c && c.provider === 'cloudflare' && c.accountId && c.token); }
/* FLUX лучше понимает английский. Если в промпте есть кириллица — переводим
* через тот же LLM-провайдер, что и ассистент (с failover). При сбое — исходный текст. */
async function _toEnglish(prompt) {
if (!/[Ѐ-ӿ]/.test(prompt)) return prompt;
try {
const { callLLMFailover } = require('./assistantController');
const sys = 'You translate image-generation prompts into concise, vivid English. ' +
'Output ONLY the English prompt — no quotes, no notes, no preamble. Keep it short and descriptive.';
const r = await callLLMFailover([{ role: 'system', content: sys }, { role: 'user', content: prompt }], 120);
if (r && r.text) {
const en = String(r.text).replace(/^["'«»\s]+|["'«»\s]+$/g, '').replace(/\s+/g, ' ').trim();
if (en && !/[Ѐ-ӿ]/.test(en)) return en.slice(0, 500);
}
} catch (e) {}
return prompt; // перевод не удался — отправим как есть
}
/* GET /api/imggen/status — для UI (показывать кнопки или нет) */ /* GET /api/imggen/status — для UI (показывать кнопки или нет) */
function status(req, res) { res.json({ enabled: _enabled() }); } function status(req, res) { res.json({ enabled: _enabled() }); }
@@ -35,13 +52,15 @@ async function generate(req, res) {
if (d && d.day === today && d.count >= DAILY_CAP) return res.status(429).json({ error: 'Дневной лимит генераций исчерпан, попробуй завтра' }); if (d && d.day === today && d.count >= DAILY_CAP) return res.status(429).json({ error: 'Дневной лимит генераций исчерпан, попробуй завтра' });
_cooldown.set(uid, now); _cooldown.set(uid, now);
const enPrompt = await _toEnglish(prompt);
const ctrl = new AbortController(); const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 30000); const timer = setTimeout(() => ctrl.abort(), 30000);
try { try {
const r = await fetch(`https://api.cloudflare.com/client/v4/accounts/${cfg.accountId}/ai/run/${cfg.model}`, { const r = await fetch(`https://api.cloudflare.com/client/v4/accounts/${cfg.accountId}/ai/run/${cfg.model}`, {
method: 'POST', method: 'POST',
headers: { Authorization: 'Bearer ' + cfg.token, 'Content-Type': 'application/json' }, headers: { Authorization: 'Bearer ' + cfg.token, 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, steps: 4 }), body: JSON.stringify({ prompt: enPrompt, steps: 4 }),
signal: ctrl.signal, signal: ctrl.signal,
}); });
if (!r.ok) { const t = await r.text(); return res.status(502).json({ error: 'Сервис картинок ответил ошибкой (' + r.status + ')', detail: t.slice(0, 120) }); } if (!r.ok) { const t = await r.text(); return res.status(502).json({ error: 'Сервис картинок ответил ошибкой (' + r.status + ')', detail: t.slice(0, 120) }); }