- server.py: FastAPI serves model (best_generator.pt) + static files - /api/generate endpoint with temperature/top_k controls - /api/health endpoint for status - Frontend auto-detects backend, shows connection status - API URL configurable from UI (persisted in localStorage) - NaN/inf safety in generation - Clean output (strips BOS/EOS/PAD/UNK tokens)
184 lines
7.2 KiB
JavaScript
184 lines
7.2 KiB
JavaScript
// API URL: ?api=... в URL, или localStorage, или auto-detect (same host :8000)
|
||
function getApiUrl() {
|
||
const qs = new URLSearchParams(window.location.search).get('api');
|
||
if (qs) return qs.replace(/\/+$/, '');
|
||
const saved = localStorage.getItem('cbgen_api_url');
|
||
if (saved) return saved.replace(/\/+$/, '');
|
||
// Auto-detect: same hostname, port 8000
|
||
return `${location.protocol}//${location.hostname}:8000`;
|
||
}
|
||
|
||
let API_URL = getApiUrl();
|
||
|
||
const promptInput = document.getElementById('promptInput');
|
||
const tempSlider = document.getElementById('tempSlider');
|
||
const tempVal = document.getElementById('tempVal');
|
||
const topkSlider = document.getElementById('topkSlider');
|
||
const topkVal = document.getElementById('topkVal');
|
||
const generateBtn = document.getElementById('generateBtn');
|
||
const generate5Btn = document.getElementById('generate5Btn');
|
||
const outputArea = document.getElementById('outputArea');
|
||
const statusMsg = document.getElementById('statusMsg');
|
||
const apiInput = document.getElementById('apiUrlInput');
|
||
const apiStatus = document.getElementById('apiStatus');
|
||
|
||
let backendOnline = false;
|
||
|
||
async function checkBackend() {
|
||
try {
|
||
const res = await fetch(`${API_URL}/api/health`, { signal: AbortSignal.timeout(3000) });
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
backendOnline = data.model_loaded === true;
|
||
if (backendOnline && data.using_dummy_vocab) {
|
||
apiStatus.textContent = '🟡 Модель загружена (без vocab.pt)';
|
||
apiStatus.className = 'api-status warn';
|
||
} else if (backendOnline) {
|
||
apiStatus.textContent = '🟢 Модель загружена';
|
||
apiStatus.className = 'api-status ok';
|
||
} else {
|
||
apiStatus.textContent = '🟡 Сервер работает, модель не загружена';
|
||
apiStatus.className = 'api-status warn';
|
||
}
|
||
} else {
|
||
backendOnline = false;
|
||
apiStatus.textContent = '🔴 Сервер отвечает с ошибкой';
|
||
apiStatus.className = 'api-status err';
|
||
}
|
||
} catch {
|
||
backendOnline = false;
|
||
apiStatus.textContent = '🔴 Бэкенд недоступен — используются шаблоны';
|
||
apiStatus.className = 'api-status err';
|
||
}
|
||
}
|
||
|
||
if (apiInput) {
|
||
apiInput.value = API_URL;
|
||
apiInput.addEventListener('change', () => {
|
||
API_URL = apiInput.value.replace(/\/+$/, '');
|
||
localStorage.setItem('cbgen_api_url', API_URL);
|
||
checkBackend();
|
||
});
|
||
}
|
||
|
||
checkBackend();
|
||
setInterval(checkBackend, 15000);
|
||
|
||
tempSlider.addEventListener('input', () => { tempVal.textContent = tempSlider.value; });
|
||
topkSlider.addEventListener('input', () => { topkVal.textContent = topkSlider.value; });
|
||
|
||
generateBtn.addEventListener('click', () => generate(1));
|
||
generate5Btn.addEventListener('click', () => generate(5));
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Client-side clickbait generator (fallback / default)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const TEMPLATES = [
|
||
'этот секрет знают единицы',
|
||
'врачи в шоке от этого открытия',
|
||
'ты не поверишь что произошло дальше',
|
||
'всего один ингредиент меняет всё',
|
||
'вот почему никто не говорит об этом',
|
||
'результат превзошёл все ожидания',
|
||
'это должен знать каждый',
|
||
'никто не ожидал такого поворота',
|
||
'учёные озадачены этим феноменом',
|
||
'это изменит вашу жизнь навсегда',
|
||
'вы будете в шоке от правды',
|
||
'самое важное о чём молчат СМИ',
|
||
'это открытие перевернуло науку',
|
||
'простые вещи которые творят чудеса',
|
||
'теперь это знает весь мир',
|
||
'о чём вам не расскажут в школе',
|
||
'главный секрет успешных людей',
|
||
'это работает безотказно',
|
||
'вы упускаете это каждый день',
|
||
'пора узнать правду'
|
||
];
|
||
|
||
const PREFIXES = ['', 'невероятно но ', 'шокирующе но ', 'оказывается ', 'представьте себе '];
|
||
|
||
function pick(arr, seed) {
|
||
const idx = Math.abs(seed * 7 + seed * seed * 31) % arr.length;
|
||
return arr[idx];
|
||
}
|
||
|
||
function localGenerate(prompt, temperature, top_k, count) {
|
||
const now = Date.now();
|
||
const results = [];
|
||
for (let i = 0; i < count; i++) {
|
||
const seed = now + i * 137 + Math.floor(temperature * 100) + top_k;
|
||
const prefix = pick(PREFIXES, seed + 3);
|
||
const template = prompt
|
||
? `${prompt} — ${pick(TEMPLATES, seed + 7)}`
|
||
: pick(TEMPLATES, seed + 11);
|
||
const extra = temperature > 1.3
|
||
? (pick(['невероятно', 'шокирующе', 'фантастически'], seed + 19) + ' ')
|
||
: '';
|
||
results.push(prefix + extra + template);
|
||
}
|
||
return results;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Main generation
|
||
// ---------------------------------------------------------------------------
|
||
|
||
async function generate(count) {
|
||
const prompt = promptInput.value.trim();
|
||
const temperature = parseFloat(tempSlider.value);
|
||
const top_k = parseInt(topkSlider.value);
|
||
|
||
outputArea.innerHTML = '';
|
||
statusMsg.textContent = '⚡ Генерируем...';
|
||
statusMsg.className = 'status-msg';
|
||
|
||
let texts;
|
||
let usedBackend = false;
|
||
|
||
try {
|
||
const res = await fetch(`${API_URL}/api/generate`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ prompt, temperature, top_k, num_samples: count })
|
||
});
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
const data = await res.json();
|
||
texts = data.texts;
|
||
usedBackend = true;
|
||
} catch {
|
||
texts = localGenerate(prompt, temperature, top_k, count);
|
||
usedBackend = false;
|
||
}
|
||
|
||
statusMsg.textContent = usedBackend ? '✅ Модель (PyTorch)' : '⚡ Локальные шаблоны (бэкенд недоступен)';
|
||
statusMsg.className = 'status-msg';
|
||
|
||
texts.forEach((text, i) => {
|
||
const div = document.createElement('div');
|
||
div.className = 'gen-output-item';
|
||
div.style.animationDelay = `${i * 0.08}s`;
|
||
const label = count > 1 ? `<span class="gen-badge">#${i + 1}</span>` : '';
|
||
div.innerHTML = `${label}${text}...😱`;
|
||
outputArea.appendChild(div);
|
||
});
|
||
|
||
// Regenerate button
|
||
const reBtn = document.createElement('button');
|
||
reBtn.className = 'btn btn-secondary btn-regenerate';
|
||
reBtn.textContent = '🔄 Сгенерировать ещё';
|
||
reBtn.addEventListener('click', () => generate(count));
|
||
outputArea.appendChild(reBtn);
|
||
}
|
||
|
||
// Example tabs
|
||
document.querySelectorAll('.ex-tab').forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
document.querySelectorAll('.ex-tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.ex-content').forEach(c => c.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
document.getElementById('ex-' + tab.dataset.tab).classList.add('active');
|
||
});
|
||
});
|