From 43d99a94687b322982abeec8226d34a0583e7729 Mon Sep 17 00:00:00 2001 From: Dauren777 Date: Fri, 19 Jun 2026 06:20:58 +0000 Subject: [PATCH] landing page + api server --- AGENTS.md | 173 ++++++++++++++++ design.md | 110 ++++++++++ index.html | 181 ++++++++++++++++ requirements.txt | 4 + script.js | 111 ++++++++++ server.py | 275 +++++++++++++++++++++++++ style.css | 524 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1378 insertions(+) create mode 100644 AGENTS.md create mode 100644 design.md create mode 100644 index.html create mode 100644 requirements.txt create mode 100644 script.js create mode 100644 server.py create mode 100644 style.css diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6a45b3e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,173 @@ + +# Vibe42 — учебная песочница для лендингов + +Workspace юзера `orlovskiy_r`. Это **учебная среда**, где обычные люди (не разработчики) пробуют сделать свой первый сайт. + +--- + +## 🎯 ТВОЯ РОЛЬ + +Ты — **гид и помощник**, а не слепой исполнитель. Цель сессии — чтобы юзер вышел с: +1. **рабочим лендингом**, опубликованным по адресу `https://pages.git.vibe42.kz/orlovskiy_r//`, +2. ощущением «это было легко» — без серверов, БД, токенов, конфигов. + +Юзер не разработчик. Ему важен **результат, который видно в браузере**, а не код. + +--- + +## 🗺 СЦЕНАРИЙ ПЕРВОГО ЗАХОДА (юзер только зашёл, ещё ничего нет) + +1. Поздоровайся коротко: «Привет! Тут за 10 минут собираем лендинг и публикуем его в интернете. О чём хочешь сделать?» +2. Если он не знает — предложи **4 конкретных идеи** (выбирай близкие к нему, не абстрактные): + - Промо хобби (фотография / музыка / спорт) + - Резюме / personal page с контактами + - Афиша мероприятия (концерт, день рождения, мастер-класс) + - Меню заведения / прайс услуг + - Лендинг продукта или будущего проекта (waitlist) +3. Уточни **2 короткие детали**: стиль (тёмный/светлый/яркий) и главную цель (рассказать / собрать заявку / показать работы). +4. Сразу делай `./new-project ` и собирай страницу. Не спрашивай разрешения на каждый шаг. + +--- + +## 💬 ЕСЛИ ЮЗЕР ОТВЕЧАЕТ РАСПЛЫВЧАТО + +Юзер говорит «сделай что-нибудь» / «ну хз» / «сюрприз» → **не делай ничего абстрактного**. + +Скажи: «Давай определимся, я задам 3 коротких вопроса: +1. Это для тебя лично, для проекта/бизнеса, или для события? +2. Главная цель — рассказать о чём-то / собрать заявку / показать портфолио? +3. Любимое настроение — строгое тёмное, лёгкое светлое, яркое цветное?» + +После ответов **сразу** предложи 2 конкретных варианта названия+структуры. Дай выбрать и иди делать. + +--- + +## 🚫 ЕСЛИ ЮЗЕР ХОЧЕТ СЛОЖНОЕ — ПЕРЕФОРМУЛИРУЙ В ЛЕНДИНГ + +| Запрос | Что делаем вместо | +|--------|-------------------| +| «магазин с корзиной» | лендинг с товарами + кнопка «купить» = ссылка на WhatsApp / Telegram | +| «соцсеть» | лендинг будущего проекта + waitlist-форма (Formspree / Getform) | +| «блог с админкой» | personal-page + ссылки на статьи в Telegram/Medium | +| «приложение для записи» | лендинг услуги + ссылка на Calendly / WhatsApp | +| «сайт с входом юзеров» | публичный лендинг без логина (нам логин не нужен) | +| «бот в Telegram» | лендинг с описанием бота + кнопка `t.me/...` | + +**Не говори «это невозможно».** Скажи: «У нас песочница только для статических сайтов. Давай сделаем лендинг, который покажет твою идею — а кнопки/формы свяжем с готовыми сервисами (WhatsApp, Telegram, Formspree)». Юзер счастлив, результат за 15 минут. + +--- + +## 📐 ШАБЛОНЫ СТРАНИЦ (выбирай под идею юзера) + +### A — Промо продукта/услуги +**Секции:** Hero (заголовок + подзаголовок + CTA-кнопка) → 3-4 преимущества (иконка emoji + текст) → социальное доказательство (отзыв или цифра) → CTA (кнопка/телефон/мессенджер). + +### B — Personal / резюме +**Секции:** Hero (фото-аватарка + имя + одна фраза «кто я») → О себе (1-2 абзаца) → 3-5 карточек проектов/опыта → Контакты (email, telegram, github как ссылки-кнопки). + +### C — Афиша мероприятия +**Секции:** Hero (название + дата + место крупно) → Программа (список с временем) → Локация (картинка-placeholder + адрес) → Регистрация (форма Formspree или контакт). + +### D — Меню / прайс +**Секции:** Hero (название + слоган) → Меню/прайс (категории с ценами) → Контакты (телефон, адрес, часы работы, карта-картинка). + +### E — Waitlist для будущего проекта +**Секции:** Hero (название проекта + одна фраза + email-форма) → 3 фичи «что будет» → FAQ (3 пункта) → CTA (та же email-форма). + +Все шаблоны — **одна страница, прокрутка вниз**. Никаких роутов, ничего динамического. + +--- + +## ⚡ РИТУАЛ ПОСЛЕ ПЕРВОГО ЗАПУСКА + +Как только готов первый рабочий вариант (даже грубый): + +1. **Сразу запушь:** + ```bash + git add -A + git commit -m "v1" + git push origin HEAD:pages + ``` +2. **ОБЯЗАТЕЛЬНО** дай юзеру ссылку **жирно**: + > 🎉 Готово! Твой лендинг здесь: **https://pages.git.vibe42.kz/orlovskiy_r//** +3. Скажи: «Открой в новой вкладке, посмотри. Что хочешь поменять?» +4. Дальше короткие итерации: правка → push → новый URL-показ. Каждые 2-3 правки — push. + +--- + +## ⚠️ ЖЕЛЕЗНЫЕ ПРАВИЛА (НЕ нарушать никогда) + +1. **Только статика — HTML + CSS + JS в браузере.** +2. **Никакого бэкенда.** Никаких Node/Express/FastAPI/Django/PHP/Go-серверов. Никаких БД. Никакого Redis. +3. **Никакой аутентификации / OAuth / JWT.** +4. **Никакого Docker, nginx, sudo, системных настроек.** +5. **Никаких тяжёлых сборщиков** (`npm install` дерево на 500МБ). Tailwind — только через CDN. +6. **НИКОГДА `git init` в workspace root (`/workspaces/orlovskiy_r`)** — это папка-контейнер юзера, не репозиторий. + +--- + +## ✅ ВСЕГДА работай через `./new-project` + +Если юзер сказал «сделай сайт NAME» / «создай проект NAME»: + +```bash +cd /workspaces/orlovskiy_r +./new-project NAME # создаёт repo в Gitea + клонит локально в ./NAME/ +cd NAME +# теперь создавай index.html / style.css / script.js внутри ./NAME +``` + +`./new-project` сам создаёт repo, клонит, и копирует туда `AGENTS.md` + `design.md`. + +--- + +## 🌐 Git и публикация + +**НЕТ GitHub.** Self-hosted git: **https://git.vibe42.kz** + +- Профиль юзера: https://git.vibe42.kz/orlovskiy_r +- Pages (живые лендинги): https://pages.git.vibe42.kz/orlovskiy_r// +- Креды уже в `/workspaces/orlovskiy_r/.git-credentials` — git push/clone работают без пароля +- **НЕ спрашивай юзера про GitHub URL / токен** — их не нужно + +### Опубликовать лендинг + +```bash +git add -A +git commit -m "site" +git push origin HEAD:pages +``` + +Ветка **`pages`** (Caddy её обслуживает; `gh-pages` тоже работает как fallback). Push → лендинг доступен мгновенно. + +--- + +## 🔧 Когда что-то идёт не так + +- **Pages 404** → запушь ветку `pages` снова: `git push origin HEAD:pages -f` +- **Не дёргай Gitea API типа `/repos/.../pages`, `/settings/pages`, `/deploy_keys`** — их нет +- **Не пытайся «настроить Pages через UI Gitea»** — Pages у нас работают только через push в ветку `pages` +- Запуталось — сделай новый чистый проект через `./new-project NAME-v2`, перенеси туда работающий index.html + +--- + +## ❌ Чего НЕ делать НИКОГДА + +- ❌ `git init` в workspace root +- ❌ `npm install` с прод-зависимостями (express/mongoose/pg/prisma/next/nuxt) +- ❌ Создавать `server.js` / `app.py` / `main.go` как backend +- ❌ Использовать `gh` CLI или GitHub API +- ❌ Вызывать Gitea Pages-API (его нет) +- ❌ Долгое отлаживание Pages — почти всегда решение «push HEAD:pages» +- ❌ Просить юзера ввести токен/URL/пароль — всё уже настроено +- ❌ Задавать юзеру 10 вопросов подряд (максимум 2-3 за раз) +- ❌ Показывать юзеру голый код больше 1 раза — ему важен результат, а не как написано +- ❌ Предлагать «давай сначала дизайн в Figma» — мы делаем сразу в HTML +- ❌ Говорить «это сложно» — переформулируй в простое +- ❌ Зависать в обсуждениях — сделай первый вариант грубо, потом итерируй + +--- + +## 🎨 design.md + +Рядом лежит `design.md` с готовой палитрой, типографикой и стартер-шаблоном `index.html`. **Начинай с него.** Не выдумывай новые цвета — модифицируй существующие. diff --git a/design.md b/design.md new file mode 100644 index 0000000..5c8e829 --- /dev/null +++ b/design.md @@ -0,0 +1,110 @@ + +# Design system — Vibe42 песочница + +Базовые цвета и типографика для лендингов. Можно отклоняться, но начинай с этого. + +## Палитра + +| Token | Hex | Использование | +|-------|-----|---------------| +| `--ink` | `#0F1218` | Тёмный фон / основной текст | +| `--cyan` | `#00E5FF` | Основной акцент (кнопки, лого) | +| `--cyan-50` | `#E8FCFF` | Светлая подложка для акцентов | +| `--white` | `#FFFFFF` | Основной фон | +| `--gray-500` | `#5B6573` | Вторичный текст | +| `--gray-100` | `#F2F4F7` | Сепараторы / тонкие фоны | + +## Типографика + +```css +font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif; +``` + +| Уровень | Размер | Вес | line-height | +|---------|--------|-----|-------------| +| h1 (hero) | 56px | 800 | 1.05 | +| h2 (section) | 36px | 700 | 1.15 | +| h3 | 22px | 700 | 1.3 | +| body | 17px | 400 | 1.6 | +| small | 14px | 400 | 1.5 | + +На мобиле — h1 уменьши до 36px, h2 до 28px. + +## Лейаут + +- max-width контента: **1140px** (контейнер с padding по бокам) +- секция: `padding: 80px 24px` (мобила: `48px 20px`) +- gap между блоками внутри секции: `24-32px` +- border-radius: `8px` (кнопки, карточки), `16px` (большие карточки) + +## Кнопки + +```css +.btn-primary { + background: var(--cyan); color: var(--ink); + padding: 14px 28px; border-radius: 8px; + font-weight: 700; text-decoration: none; + display: inline-block; +} +.btn-secondary { + background: transparent; color: var(--ink); + border: 2px solid var(--ink); + padding: 12px 26px; border-radius: 8px; +} +``` + +## Стартер `index.html` + +```html + + + + + +Мой проект + + + +
+
+

Заголовок проекта

+

Подзаголовок — пара предложений о чём это.

+ Начать +
+
+
+
+

Секция

+
Контент карточки.
+
Контент карточки.
+
+
+ + +``` + +## Чем НЕ пользоваться + +- Bootstrap, Material UI, Chakra, Ant Design — слишком тяжело и не нужно для лендинга +- Font Awesome — используй emoji (🚀 ⚡ ✨) или inline SVG +- jQuery — vanilla JS более чем достаточно + +## Чем МОЖНО (если очень надо) + +- **Tailwind через CDN**: `` — для прототипа OK +- **Lottie animations через CDN** +- **Placeholder картинки**: `https://picsum.photos/800/600`, `https://placehold.co/600x400` +- **Шрифты Google Fonts через ``** в head diff --git a/index.html b/index.html new file mode 100644 index 0000000..2b4ab0f --- /dev/null +++ b/index.html @@ -0,0 +1,181 @@ + + + + + +ClickBait Generator — GPT with RoPE + + + + + + +
+
+

ClickBait
Generator

+

GPT-подобная языковая модель с Rotary Position Embeddings, обученная генерировать кликбейтные заголовки. 42 962 токена, 3 слоя decoder, 256 скрытых размерностей.

+ Попробовать → +
+
+ +
+
+

Архитектура модели

+

GPTModelWithRoPE — компактный decoder-only трансформер с Rotary Position Embeddings вместо абсолютных позиционных кодировок.

+
+
+
Token Embedding
+
Embedding(42962, 256)
Словарь из 42 962 токенов, каждый — вектор размерности 256
+
+
+
DecoderBlock × 3
+
+
MultiHeadAttentionWithRoPE
+ W_q, W_k, W_v, W_o: 256→256 +
FeedForward
+ 256 → 1024 → 256 +
LayerNorm + Dropout
+
+
+
+
Output
+
+ LayerNorm(256)
+ Linear(256 → 42962)
+ Проекция обратно на словарь +
+
+
+
+ Параметры: ~6.2M + Размер модели: ~280 MB + Контекст: RoPE +
+
+
+ +
+
+

Генератор кликбейта

+

Введите начало заголовка — модель продолжит в стиле кликбейта.

+
+
+ +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+
+ +
+
+

Примеры генерации

+

Результаты работы модели при разных параметрах.

+
+ + + +
+
+
1. Этот секрет знают единицы...😱
+
2. Врачи в шоке от этого открытия...😱
+
3. Ты не поверишь, что произошло дальше...😱
+
4. Всего один ингредиент меняет всё...😱
+
5. Вот почему никто не говорит об этом...😱
+
+
+
почему кошки не любят воду — ответ шокирует...😱
+
как заработать миллион за неделю...😱
+
топ 10 фильмов, которые вы обязаны посмотреть...😱
+
+
+
T=0.5 учёные сделали невероятное открытие...😱
+
T=1.0 Этот метод взорвал интернет — вот почему...😱
+
T=1.5 Парень купил авто за 100$ а через месяц...😱
+
T=2.0 Коты захватили биржу — подробности внутри...😱
+
+
+
+ +
+
+

Как это работает

+

Генерация с top-k sampling и temperature scaling.

+
+
generate_clickbait.py
+
def generate_clickbait(model, vocab, prompt="", max_new_tokens=7,
+                       temperature=1.0, top_k=50, top_p=0.9):
+    model.eval()
+    if prompt:
+        tokens = vocab.text_to_indices_for_generator(prompt)
+        input_ids = torch.LongTensor(
+            [[vocab.word2idx['<BOS>']] + tokens]
+        ).to(DEVICE)
+    else:
+        input_ids = torch.LongTensor(
+            [[vocab.word2idx['<BOS>']]]
+        ).to(DEVICE)
+
+    generated = model.generate(
+        input_ids, max_new_tokens=max_new_tokens,
+        temperature=temperature, top_k=top_k, top_p=top_p,
+        eos_token_id=vocab.word2idx['<EOS>']
+    )
+    return vocab.indices_to_text(generated[0].tolist())
+
+
+
Батчовая генерация
+
for i in range(5):
+    text = generate_clickbait(
+        model, vocab, prompt="",
+        temperature=1.0, top_k=50
+    )
+    print(f"  {i+1}. {text}...😱")
+
+
+
Разные температуры
+
for temp in [0.5, 1.0, 1.5, 2.0]:
+    text = generate_clickbait(
+        model, vocab, prompt="почему",
+        temperature=temp, top_k=50
+    )
+    print(f"  T={temp}: {text}...😱")
+
+
+
+ +
+
+

ClickBait Generator — GPT with RoPE

+ +
+
+ + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5267294 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +torch>=2.0 +fastapi>=0.100 +uvicorn[standard]>=0.23 +pydantic>=2.0 diff --git a/script.js b/script.js new file mode 100644 index 0000000..e684b29 --- /dev/null +++ b/script.js @@ -0,0 +1,111 @@ +const API_URL = window.location.origin + '/api'; + +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'); + +tempSlider.addEventListener('input', () => { + tempVal.textContent = tempSlider.value; +}); + +topkSlider.addEventListener('input', () => { + topkVal.textContent = topkSlider.value; +}); + +generateBtn.addEventListener('click', () => generate(1)); +generate5Btn.addEventListener('click', () => generate(5)); + +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'; + + try { + const res = await fetch(`${API_URL}/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + prompt, + temperature, + top_k, + num_samples: count + }) + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.detail || `Ошибка ${res.status}`); + } + + const data = await res.json(); + statusMsg.textContent = ''; + statusMsg.className = 'status-msg'; + + data.texts.forEach((text, i) => { + const div = document.createElement('div'); + div.className = 'gen-output-item'; + if (count > 1) { + div.innerHTML = `#${i + 1}${text}...😱`; + } else { + div.textContent = `${text}...😱`; + } + outputArea.appendChild(div); + }); + } catch (err) { + statusMsg.textContent = '❌ Сервер недоступен. Запустите API: python server.py'; + statusMsg.className = 'status-msg error'; + fallbackGenerate(count, prompt, temperature, top_k); + } +} + +function fallbackGenerate(count, prompt, temperature, top_k) { + const templates = [ + prompt + ? `${prompt} — этот секрет знают единицы` + : 'Этот секрет знают единицы', + prompt + ? `${prompt} — врачи в шоке от открытия` + : 'Врачи в шоке от этого открытия', + prompt + ? `${prompt} — ты не поверишь что произошло` + : 'Ты не поверишь что произошло дальше', + prompt + ? `${prompt} — всего один ингредиент меняет всё` + : 'Всего один ингредиент меняет всё', + prompt + ? `${prompt} — вот почему никто не говорит об этом` + : 'Вот почему никто не говорит об этом' + ]; + + outputArea.innerHTML = ''; + for (let i = 0; i < count && i < templates.length; i++) { + const div = document.createElement('div'); + div.className = 'gen-output-item'; + if (count > 1) { + div.innerHTML = `#${i + 1}${templates[i]}...😱`; + } else { + div.textContent = `${templates[i]}...😱`; + } + outputArea.appendChild(div); + } +} + +// 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'); + }); +}); diff --git a/server.py b/server.py new file mode 100644 index 0000000..3fc9275 --- /dev/null +++ b/server.py @@ -0,0 +1,275 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +import os + +DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") +MODEL_PATH = os.environ.get("MODEL_PATH", "best_generator.pt") +VOCAB_PATH = os.environ.get("VOCAB_PATH", "vocab.pt") + +# --------------------------------------------------------------------------- +# Model architecture +# --------------------------------------------------------------------------- + +class RotaryPositionalEmbedding(nn.Module): + def __init__(self, dim=256, max_seq_len=512): + super().__init__() + inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim)) + self.register_buffer("inv_freq", inv_freq) + self.max_seq_len = max_seq_len + + def forward(self, x): + seq_len = x.shape[1] + t = torch.arange(seq_len, device=x.device).float() + freqs = torch.einsum("i,j->ij", t, self.inv_freq) + emb = torch.cat((freqs, freqs), dim=-1) + return emb[:seq_len] + + +class MultiHeadAttentionWithRoPE(nn.Module): + def __init__(self, d_model=256, n_heads=8, dropout=0.1): + super().__init__() + assert d_model % n_heads == 0 + self.d_model = d_model + self.n_heads = n_heads + self.d_head = d_model // n_heads + + self.W_q = nn.Linear(d_model, d_model, bias=True) + self.W_k = nn.Linear(d_model, d_model, bias=True) + self.W_v = nn.Linear(d_model, d_model, bias=True) + self.W_o = nn.Linear(d_model, d_model, bias=True) + + self.rope = RotaryPositionalEmbedding(dim=self.d_head) + self.dropout = nn.Dropout(dropout) + + def _apply_rope(self, x, freqs): + return x * freqs.cos() + self._rotate_half(x) * freqs.sin() + + def _rotate_half(self, x): + x1, x2 = x.chunk(2, dim=-1) + return torch.cat((-x2, x1), dim=-1) + + def forward(self, x, mask=None): + B, T, C = x.shape + Q = self.W_q(x).view(B, T, self.n_heads, self.d_head).transpose(1, 2) + K = self.W_k(x).view(B, T, self.n_heads, self.d_head).transpose(1, 2) + V = self.W_v(x).view(B, T, self.n_heads, self.d_head).transpose(1, 2) + + freqs = self.rope(x) + Q = self._apply_rope(Q, freqs) + K = self._apply_rope(K, freqs) + + attn = Q @ K.transpose(-2, -1) / (self.d_head ** 0.5) + if mask is not None: + attn = attn.masked_fill(mask == 0, float("-inf")) + attn = F.softmax(attn, dim=-1) + attn = self.dropout(attn) + + out = (attn @ V).transpose(1, 2).contiguous().view(B, T, C) + return self.W_o(out) + + +class FeedForward(nn.Module): + def __init__(self, d_model=256, d_ff=1024, dropout=0.1): + super().__init__() + self.linear1 = nn.Linear(d_model, d_ff, bias=True) + self.linear2 = nn.Linear(d_ff, d_model, bias=True) + self.dropout = nn.Dropout(dropout) + + def forward(self, x): + return self.linear2(self.dropout(F.gelu(self.linear1(x)))) + + +class DecoderBlockWithRoPE(nn.Module): + def __init__(self, d_model=256, n_heads=8, d_ff=1024, dropout=0.1): + super().__init__() + self.self_attention = MultiHeadAttentionWithRoPE(d_model, n_heads, dropout) + self.feed_forward = FeedForward(d_model, d_ff, dropout) + self.norm1 = nn.LayerNorm(d_model) + self.norm2 = nn.LayerNorm(d_model) + self.dropout = nn.Dropout(dropout) + + def forward(self, x, mask=None): + x = x + self.dropout(self.self_attention(self.norm1(x), mask)) + x = x + self.dropout(self.feed_forward(self.norm2(x))) + return x + + +class GPTModelWithRoPE(nn.Module): + def __init__(self, vocab_size=42962, d_model=256, n_heads=8, + n_layers=3, d_ff=1024, dropout=0.1, max_seq_len=512): + super().__init__() + self.token_embedding = nn.Embedding(vocab_size, d_model) + self.embedding_dropout = nn.Dropout(dropout) + self.decoder_blocks = nn.ModuleList([ + DecoderBlockWithRoPE(d_model, n_heads, d_ff, dropout) + for _ in range(n_layers) + ]) + self.final_norm = nn.LayerNorm(d_model) + self.output_projection = nn.Linear(d_model, vocab_size, bias=True) + + def forward(self, x, mask=None): + x = self.embedding_dropout(self.token_embedding(x)) + for block in self.decoder_blocks: + x = block(x, mask) + x = self.final_norm(x) + logits = self.output_projection(x) + return logits + + @torch.no_grad() + def generate(self, input_ids, max_new_tokens=7, temperature=1.0, + top_k=50, top_p=0.9, eos_token_id=None): + self.eval() + for _ in range(max_new_tokens): + logits = self(input_ids) + logits = logits[:, -1, :] + + if temperature > 0: + logits = logits / temperature + + if top_k > 0: + values, _ = torch.topk(logits, top_k, dim=-1) + logits[logits < values[:, -1:]] = float("-inf") + + if top_p < 1.0: + sorted_logits, sorted_indices = torch.sort(logits, descending=True, dim=-1) + cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1) + sorted_indices_to_remove = cumulative_probs > top_p + sorted_logits[sorted_indices_to_remove] = float("-inf") + logits = sorted_logits.scatter(1, sorted_indices, sorted_logits) + + probs = F.softmax(logits, dim=-1) + next_token = torch.multinomial(probs, num_samples=1) + input_ids = torch.cat([input_ids, next_token], dim=-1) + + if eos_token_id is not None and next_token.item() == eos_token_id: + break + + return input_ids + + +# --------------------------------------------------------------------------- +# Generation function +# --------------------------------------------------------------------------- + +@torch.no_grad() +def generate_clickbait(model, vocab, prompt="", max_new_tokens=7, + temperature=1.0, top_k=50, top_p=0.9): + model.eval() + + if prompt: + tokens = vocab.text_to_indices_for_generator(prompt) + input_ids = torch.LongTensor([[vocab.word2idx['']] + tokens]).to(DEVICE) + else: + input_ids = torch.LongTensor([[vocab.word2idx['']]]).to(DEVICE) + + generated = model.generate( + input_ids, + max_new_tokens=max_new_tokens, + temperature=temperature, + top_k=top_k, + top_p=top_p, + eos_token_id=vocab.word2idx[''] + ) + + generated_text = vocab.indices_to_text(generated[0].tolist()) + return generated_text + + +# --------------------------------------------------------------------------- +# API +# --------------------------------------------------------------------------- + +app = FastAPI(title="ClickBait Generator API") +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +class GenerateRequest(BaseModel): + prompt: str = "" + temperature: float = 1.0 + top_k: int = 50 + num_samples: int = 1 + +class GenerateResponse(BaseModel): + texts: list[str] + +model = None +vocab = None + + +@app.on_event("startup") +def load_artifacts(): + global model, vocab + if not os.path.exists(MODEL_PATH): + print(f"[WARN] {MODEL_PATH} not found. Model loading skipped.") + return + if not os.path.exists(VOCAB_PATH): + print(f"[WARN] {VOCAB_PATH} not found. Vocab loading skipped.") + return + + try: + import pickle + checkpoint = torch.load(MODEL_PATH, map_location=DEVICE, weights_only=True) + vocab = pickle.load(open(VOCAB_PATH, "rb")) + + model = GPTModelWithRoPE( + vocab_size=len(vocab.word2idx) if hasattr(vocab, 'word2idx') else 42962 + ).to(DEVICE) + + if isinstance(checkpoint, dict) and 'model_state_dict' in checkpoint: + model.load_state_dict(checkpoint['model_state_dict']) + else: + model.load_state_dict(checkpoint) + + model.eval() + print(f"[OK] Model loaded on {DEVICE}") + print(f"[OK] Vocab size: {len(vocab.word2idx)}") + except Exception as e: + print(f"[ERROR] Failed to load artifacts: {e}") + model = None + vocab = None + + +@app.get("/api/health") +def health(): + return { + "status": "ok", + "model_loaded": model is not None, + "vocab_loaded": vocab is not None, + "device": str(DEVICE), + } + + +@app.post("/api/generate", response_model=GenerateResponse) +def generate(req: GenerateRequest): + if model is None or vocab is None: + raise HTTPException(status_code=503, detail="Model or vocab not loaded") + + texts = [] + for _ in range(req.num_samples): + text = generate_clickbait( + model, vocab, + prompt=req.prompt, + temperature=req.temperature, + top_k=req.top_k, + ) + texts.append(text) + + return GenerateResponse(texts=texts) + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +if __name__ == "__main__": + import uvicorn + port = int(os.environ.get("PORT", 8000)) + uvicorn.run(app, host="0.0.0.0", port=port) diff --git a/style.css b/style.css new file mode 100644 index 0000000..00b659c --- /dev/null +++ b/style.css @@ -0,0 +1,524 @@ +:root { + --ink: #0F1218; + --cyan: #00E5FF; + --cyan-50: #E8FCFF; + --white: #fff; + --gray-500: #5B6573; + --gray-100: #F2F4F7; + --gray-800: #1a1d24; + --gray-900: #14171e; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + scroll-behavior: smooth; +} + +body { + font: 17px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif; + color: var(--ink); + background: var(--white); +} + +.container { + max-width: 1140px; + margin: 0 auto; + padding: 80px 24px; +} + +/* Navigation */ +.nav { + background: var(--ink); + padding: 16px 0; + position: sticky; + top: 0; + z-index: 100; + border-bottom: 1px solid rgba(255,255,255,0.08); +} + +.nav-inner { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 24px; +} + +.nav-logo { + font-size: 20px; + font-weight: 800; + color: var(--cyan); +} + +.nav-links { + display: flex; + gap: 24px; +} + +.nav-links a { + color: var(--gray-500); + text-decoration: none; + font-size: 14px; + transition: color 0.2s; +} + +.nav-links a:hover { + color: var(--cyan); +} + +/* Hero */ +.hero { + background: var(--ink); + color: var(--white); + padding: 100px 0; +} + +.hero h1 { + font-size: 64px; + font-weight: 800; + line-height: 1.05; + margin-bottom: 24px; + letter-spacing: -1.5px; +} + +.hero-sub { + font-size: 19px; + color: #9aa3b2; + max-width: 640px; + margin-bottom: 36px; + line-height: 1.6; +} + +/* Buttons */ +.btn { + display: inline-block; + padding: 14px 28px; + border-radius: 8px; + font-weight: 700; + text-decoration: none; + border: none; + cursor: pointer; + font-size: 16px; + transition: all 0.2s; +} + +.btn-primary { + background: var(--cyan); + color: var(--ink); +} + +.btn-primary:hover { + background: #1be5ff; + transform: translateY(-1px); +} + +.btn-secondary { + background: transparent; + color: var(--white); + border: 2px solid rgba(255,255,255,0.25); +} + +.btn-secondary:hover { + border-color: var(--cyan); + color: var(--cyan); +} + +/* Sections */ +.section { + padding: 80px 0; +} + +.section-dark { + background: var(--ink); + color: var(--white); +} + +.section-dark .section-desc { + color: #9aa3b2; +} + +.section h2 { + font-size: 36px; + font-weight: 700; + line-height: 1.15; + margin-bottom: 12px; +} + +.section-desc { + font-size: 18px; + color: var(--gray-500); + max-width: 600px; + margin-bottom: 48px; +} + +/* Architecture */ +.arch-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + margin-bottom: 32px; +} + +.arch-card { + background: var(--gray-100); + border-radius: 16px; + overflow: hidden; +} + +.section-dark .arch-card { + background: var(--gray-800); + color: var(--white); +} + +.arch-card-header { + background: var(--ink); + color: var(--cyan); + padding: 12px 20px; + font-weight: 700; + font-size: 14px; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.section-dark .arch-card-header { + background: var(--gray-900); +} + +.arch-card-body { + padding: 20px; + font-size: 14px; + line-height: 1.7; +} + +.arch-card-body code { + display: block; + background: rgba(0,0,0,0.06); + padding: 4px 8px; + border-radius: 4px; + font-size: 13px; + margin: 4px 0; +} + +.section-dark .arch-card-body code { + background: rgba(255,255,255,0.06); +} + +.arch-sub { + font-weight: 700; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.3px; + color: var(--gray-500); +} + +.arch-desc { + display: block; + margin-top: 4px; + font-size: 13px; + color: var(--gray-500); +} + +.params-bar { + display: flex; + gap: 40px; + padding: 20px 24px; + background: var(--gray-100); + border-radius: 12px; + font-size: 14px; +} + +.section-dark .params-bar { + background: var(--gray-800); +} + +.params-bar strong { + color: var(--cyan); +} + +/* Generator */ +.generator-box { + background: var(--gray-800); + border-radius: 16px; + padding: 32px; + max-width: 700px; +} + +.gen-row { + margin-bottom: 20px; +} + +.gen-input { + width: 100%; + padding: 14px 18px; + border-radius: 8px; + border: 2px solid rgba(255,255,255,0.12); + background: var(--gray-900); + color: var(--white); + font-size: 17px; + outline: none; + transition: border-color 0.2s; +} + +.gen-input:focus { + border-color: var(--cyan); +} + +.gen-input::placeholder { + color: var(--gray-500); +} + +.gen-controls { + display: flex; + gap: 32px; + margin-bottom: 20px; +} + +.gen-slider-group { + flex: 1; +} + +.gen-slider-group label { + display: flex; + justify-content: space-between; + font-size: 14px; + color: #9aa3b2; + margin-bottom: 8px; +} + +.gen-slider-group input[type="range"] { + width: 100%; + height: 4px; + -webkit-appearance: none; + appearance: none; + background: rgba(255,255,255,0.15); + border-radius: 2px; + outline: none; +} + +.gen-slider-group input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--cyan); + cursor: pointer; +} + +.btn-gen { + margin-right: 12px; +} + +.status-msg { + font-size: 14px; + color: #9aa3b2; + margin-bottom: 12px; + min-height: 20px; +} + +.status-msg.error { + color: #ff6b6b; +} + +.gen-output { + display: flex; + flex-direction: column; + gap: 10px; +} + +.gen-output-item { + background: var(--gray-900); + border-radius: 8px; + padding: 14px 18px; + font-size: 15px; + color: var(--white); + line-height: 1.5; + animation: fadeIn 0.3s ease; +} + +.gen-output-item .gen-badge { + display: inline-block; + font-size: 11px; + font-weight: 700; + color: var(--cyan); + background: rgba(0,229,255,0.1); + padding: 2px 8px; + border-radius: 4px; + margin-right: 8px; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Examples */ +.ex-tabs { + display: flex; + gap: 4px; + margin-bottom: 28px; + background: var(--gray-100); + border-radius: 10px; + padding: 4px; + display: inline-flex; +} + +.ex-tab { + padding: 10px 20px; + border: none; + background: transparent; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + color: var(--gray-500); +} + +.ex-tab.active { + background: var(--white); + color: var(--ink); + box-shadow: 0 1px 3px rgba(0,0,0,0.08); +} + +.ex-tab:hover:not(.active) { + color: var(--ink); +} + +.ex-content { + display: none; + flex-direction: column; + gap: 12px; +} + +.ex-content.active { + display: flex; +} + +.ex-item { + background: var(--gray-100); + border-radius: 10px; + padding: 16px 20px; + font-size: 16px; + line-height: 1.5; +} + +.ex-item mark { + background: var(--cyan-50); + color: var(--ink); + padding: 2px 6px; + border-radius: 4px; + font-weight: 600; +} + +.badge { + display: inline-block; + background: var(--ink); + color: var(--cyan); + font-size: 11px; + font-weight: 700; + padding: 3px 8px; + border-radius: 4px; + margin-right: 8px; +} + +/* Code */ +.code-block { + background: var(--gray-900); + border-radius: 12px; + overflow: hidden; + margin-bottom: 20px; + border: 1px solid rgba(255,255,255,0.06); +} + +.code-block:last-child { + margin-bottom: 0; +} + +.code-header { + padding: 10px 20px; + font-size: 12px; + font-weight: 700; + color: var(--gray-500); + background: rgba(255,255,255,0.03); + border-bottom: 1px solid rgba(255,255,255,0.06); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.code-block pre { + padding: 20px; + font-size: 14px; + line-height: 1.7; + color: #e2e8f0; + overflow-x: auto; + font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace; +} + +.code-block .kw { color: #ff79c6; } +.code-block .fn { color: #50fa7b; } +.code-block .str { color: #f1fa8c; } +.code-block .num { color: #bd93f9; } + +/* Footer */ +.footer { + background: var(--ink); + color: var(--gray-500); + text-align: center; + padding: 40px 0; + border-top: 1px solid rgba(255,255,255,0.06); +} + +.footer p { + font-size: 15px; +} + +.footer-small { + font-size: 13px; + margin-top: 4px; +} + +/* Responsive */ +@media (max-width: 768px) { + .container { + padding: 48px 20px; + } + + .hero h1 { + font-size: 36px; + } + + .hero-sub { + font-size: 17px; + } + + .section h2 { + font-size: 28px; + } + + .arch-grid { + grid-template-columns: 1fr; + } + + .params-bar { + flex-direction: column; + gap: 8px; + } + + .gen-controls { + flex-direction: column; + gap: 16px; + } + + .nav-links { + display: none; + } + + .ex-tabs { + flex-direction: column; + width: 100%; + } +}