Spaces:
Sleeping
Sleeping
Deploy Trollsona to Build Small org with Codex
Browse files- .env.example +4 -0
- .gitignore +12 -0
- README.md +201 -6
- SUBMISSION.md +92 -0
- app.py +848 -0
- assets/style.css +446 -0
- requirements.txt +4 -0
.env.example
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TROLLSONA_ENABLE_MODEL=1
|
| 2 |
+
TROLLSONA_MODEL_ID=RthItalia/nano_compact_3b_qkvfp16
|
| 3 |
+
TROLLSONA_FALLBACK_MODEL_ID=Qwen/Qwen2.5-0.5B-Instruct
|
| 4 |
+
TROLLSONA_MAX_NEW_TOKENS=200
|
.gitignore
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
.env.*
|
| 3 |
+
!.env.example
|
| 4 |
+
__pycache__/
|
| 5 |
+
*.py[cod]
|
| 6 |
+
.gradio/
|
| 7 |
+
.pytest_cache/
|
| 8 |
+
.playwright-mcp/
|
| 9 |
+
.codex_launch_gradio.py
|
| 10 |
+
gradio.out.log
|
| 11 |
+
gradio.err.log
|
| 12 |
+
trollsona-*.png
|
README.md
CHANGED
|
@@ -1,13 +1,208 @@
|
|
| 1 |
---
|
| 2 |
title: Trollsona
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Trollsona
|
| 3 |
+
emoji: 🧌
|
| 4 |
+
colorFrom: yellow
|
| 5 |
+
colorTo: red
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 5.50.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Trollsona / Your Troll Alterego
|
| 13 |
+
|
| 14 |
+
**Tagline:** Summon the little menace living behind your respectable personality.
|
| 15 |
+
|
| 16 |
+
**Track:** An Adventure in Thousand Token Wood
|
| 17 |
+
|
| 18 |
+
**Build target:** Hugging Face Space, Gradio app, small-model constraint `<=32B`.
|
| 19 |
+
|
| 20 |
+
**GitHub repo:** https://github.com/rthgit/Trollsona
|
| 21 |
+
|
| 22 |
+
**Hugging Face Space:** https://huggingface.co/spaces/RthItalia/Trollsona
|
| 23 |
+
|
| 24 |
+
Trollsona is a playful Gradio experience that turns a short user confession into a theatrical troll alter ego. The app returns a dossier-style result card with a trollsona name, a warm roast, one useful slap, and a goblin meter.
|
| 25 |
+
|
| 26 |
+
Built with a compact RthItalia model derived from `Qwen/Qwen2.5-3B-Instruct`, under `32B` parameters. The deployed Space is configured to try that model first, then a lightweight Qwen 0.5B model, then the deterministic local fallback if model loading or generation is unavailable.
|
| 27 |
+
|
| 28 |
+
The public Space currently runs the lightweight Qwen fallback on CPU, while the custom RthItalia compact 3B path is enabled automatically when CUDA is available.
|
| 29 |
+
|
| 30 |
+
## Features
|
| 31 |
+
|
| 32 |
+
- Immersive Gradio UI for Hugging Face Spaces
|
| 33 |
+
- Theatrical trollsona result card
|
| 34 |
+
- Local Hugging Face Transformers generation path for the primary AI runtime
|
| 35 |
+
- Secondary lightweight Transformers model fallback
|
| 36 |
+
- Deterministic fallback generator for final resilience
|
| 37 |
+
- Safe roast guard for non-hateful, non-identity-targeted humor
|
| 38 |
+
- Persona dropdown, sting slider, and useful-truth checkbox
|
| 39 |
+
- Source/fallback notes hidden behind `See the cursed paperwork`
|
| 40 |
+
|
| 41 |
+
## Model Runtime
|
| 42 |
+
|
| 43 |
+
Trollsona uses a small-model cascade:
|
| 44 |
+
|
| 45 |
+
1. `RthItalia/nano_compact_3b_qkvfp16`
|
| 46 |
+
- compact `Qwen/Qwen2.5-3B-Instruct`-derived model by RthItalia
|
| 47 |
+
- preferred runtime when CUDA is available
|
| 48 |
+
- loaded with `trust_remote_code=True`
|
| 49 |
+
|
| 50 |
+
2. `Qwen/Qwen2.5-0.5B-Instruct`
|
| 51 |
+
- lightweight hosted CPU fallback model
|
| 52 |
+
- currently active on the public Hugging Face Space running on `cpu-basic`
|
| 53 |
+
|
| 54 |
+
3. Deterministic fallback
|
| 55 |
+
- used only if both model paths are unavailable or return unsafe/invalid output
|
| 56 |
+
- keeps the demo stable and reproducible
|
| 57 |
+
|
| 58 |
+
Constraint:
|
| 59 |
+
|
| 60 |
+
```text
|
| 61 |
+
small model only, <=32B parameters
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
Space model-first behavior:
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
TROLLSONA_ENABLE_MODEL=1
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
Recommended Hugging Face Space variables:
|
| 71 |
+
|
| 72 |
+
```text
|
| 73 |
+
TROLLSONA_ENABLE_MODEL=1
|
| 74 |
+
TROLLSONA_MODEL_ID=RthItalia/nano_compact_3b_qkvfp16
|
| 75 |
+
TROLLSONA_FALLBACK_MODEL_ID=Qwen/Qwen2.5-0.5B-Instruct
|
| 76 |
+
TROLLSONA_MAX_NEW_TOKENS=200
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
Local fallback-safe behavior if no variable is set:
|
| 80 |
+
|
| 81 |
+
```bash
|
| 82 |
+
TROLLSONA_ENABLE_MODEL=0
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
Deterministic fallback only:
|
| 86 |
+
|
| 87 |
+
```bash
|
| 88 |
+
TROLLSONA_ENABLE_MODEL=0
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
Implementation notes:
|
| 92 |
+
|
| 93 |
+
- `bitsandbytes` is not required
|
| 94 |
+
- primary RthItalia path expects CUDA
|
| 95 |
+
- CPU-only Spaces use the Qwen 0.5B model before the deterministic fallback
|
| 96 |
+
- source/runtime/fallback details are hidden in `See the cursed paperwork`
|
| 97 |
+
|
| 98 |
+
## Stack
|
| 99 |
+
|
| 100 |
+
- Python
|
| 101 |
+
- Gradio
|
| 102 |
+
- Hugging Face Spaces
|
| 103 |
+
- Hugging Face Transformers, primary model path
|
| 104 |
+
- PyTorch, model backend
|
| 105 |
+
|
| 106 |
+
Required secrets:
|
| 107 |
+
|
| 108 |
+
```text
|
| 109 |
+
[ASSENTE]
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
## Run Locally
|
| 113 |
+
|
| 114 |
+
```bash
|
| 115 |
+
pip install -r requirements.txt
|
| 116 |
+
python app.py
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
Open:
|
| 120 |
+
|
| 121 |
+
```text
|
| 122 |
+
http://127.0.0.1:7860
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
Model-first run:
|
| 126 |
+
|
| 127 |
+
```bash
|
| 128 |
+
TROLLSONA_ENABLE_MODEL=1 python app.py
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
Deterministic fallback run:
|
| 132 |
+
|
| 133 |
+
```bash
|
| 134 |
+
TROLLSONA_ENABLE_MODEL=0 python app.py
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
## Hugging Face Space
|
| 138 |
+
|
| 139 |
+
Required files:
|
| 140 |
+
|
| 141 |
+
- `app.py`
|
| 142 |
+
- `requirements.txt`
|
| 143 |
+
- `README.md`
|
| 144 |
+
- `assets/style.css`
|
| 145 |
+
|
| 146 |
+
Space SDK:
|
| 147 |
+
|
| 148 |
+
```text
|
| 149 |
+
Gradio
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
Space URL:
|
| 153 |
+
|
| 154 |
+
```text
|
| 155 |
+
https://huggingface.co/spaces/RthItalia/Trollsona
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
## Safety
|
| 159 |
+
|
| 160 |
+
Trollsona roasts habits, vibe, wording, overthinking, productivity rituals, internet behavior, startup energy, and harmless personal lore.
|
| 161 |
+
|
| 162 |
+
It avoids:
|
| 163 |
+
|
| 164 |
+
- protected-class targeting
|
| 165 |
+
- identity-based insults
|
| 166 |
+
- appearance insults
|
| 167 |
+
- threats or self-harm content
|
| 168 |
+
- sexual content
|
| 169 |
+
- profanity or slurs
|
| 170 |
+
- cruelty or humiliation
|
| 171 |
+
|
| 172 |
+
If generated model output fails the safety guard, the app replaces it with a safe fallback card.
|
| 173 |
+
|
| 174 |
+
## Hackathon Fit
|
| 175 |
+
|
| 176 |
+
- Built as a Gradio app for Hugging Face Space
|
| 177 |
+
- Fits `An Adventure in Thousand Token Wood`
|
| 178 |
+
- Supports the `<=32B` small-model constraint
|
| 179 |
+
- Uses `RthItalia/nano_compact_3b_qkvfp16` as the primary AI path when CUDA is available
|
| 180 |
+
- Keeps `Qwen/Qwen2.5-0.5B-Instruct` as a secondary model fallback
|
| 181 |
+
- Runs without mandatory cloud APIs
|
| 182 |
+
- Keeps deterministic fallback as a reliability guard
|
| 183 |
+
- Produces short, whimsical, shareable output
|
| 184 |
+
|
| 185 |
+
## Codex Track
|
| 186 |
+
|
| 187 |
+
Built with OpenAI Codex.
|
| 188 |
+
|
| 189 |
+
Public GitHub repo: https://github.com/rthgit/Trollsona
|
| 190 |
+
|
| 191 |
+
Codex-attributed commits include:
|
| 192 |
+
|
| 193 |
+
- `3fe2db1` Polish Trollsona dossier UI and grotesque prompt voice with Codex
|
| 194 |
+
- `4f196a6` Add RthItalia model cascade with Codex
|
| 195 |
+
- `8a1b09d` Document hosted model cascade QA with Codex
|
| 196 |
+
|
| 197 |
+
- Space README repo link: present
|
| 198 |
+
- Demo video: [DA COMPLETARE]
|
| 199 |
+
- Social post: [DA COMPLETARE]
|
| 200 |
+
|
| 201 |
+
## Known Limits
|
| 202 |
+
|
| 203 |
+
- Public Space link: https://huggingface.co/spaces/RthItalia/Trollsona
|
| 204 |
+
- Demo video: [DA COMPLETARE]
|
| 205 |
+
- Social post URL: [DA COMPLETARE]
|
| 206 |
+
- Primary RthItalia model path requires CUDA; CPU-only Spaces use the secondary model fallback before deterministic fallback
|
| 207 |
+
- First model-backed generation can be slower on cold Spaces while model files load
|
| 208 |
+
- Exact model-backed behavior on upgraded Space hardware: [AMBIGUO], because upgraded hardware has not been tested
|
SUBMISSION.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Trollsona Submission Pack
|
| 2 |
+
|
| 3 |
+
## Submission Checklist
|
| 4 |
+
|
| 5 |
+
| requirement | status | proof | missing action |
|
| 6 |
+
|---|---|---|---|
|
| 7 |
+
| Public GitHub repo | DONE | https://github.com/rthgit/Trollsona | None |
|
| 8 |
+
| Codex-attributed commits | DONE | `3fe2db1`, `4f196a6`, `8a1b09d` | None |
|
| 9 |
+
| Space README links to repo | DONE | `README.md` contains repo URL | None |
|
| 10 |
+
| Hugging Face Space deploy | DONE | https://huggingface.co/spaces/RthItalia/Trollsona | None |
|
| 11 |
+
| Space link | DONE | `README.md` contains https://huggingface.co/spaces/RthItalia/Trollsona | None |
|
| 12 |
+
| Space model variables | DONE | `.env.example` documents `TROLLSONA_ENABLE_MODEL=1`, `TROLLSONA_MODEL_ID=RthItalia/nano_compact_3b_qkvfp16`, `TROLLSONA_FALLBACK_MODEL_ID=Qwen/Qwen2.5-0.5B-Instruct` | Configure the same variables in Space settings if overriding defaults |
|
| 13 |
+
| Demo video | [DA COMPLETARE] | No video link/file present | Record 45-60s demo |
|
| 14 |
+
| Social post | [DA COMPLETARE] | Draft below contains Space/GitHub URLs | Publish and add final link if required |
|
| 15 |
+
| Gradio app | DONE | `app.py` defines `gr.Blocks` app | None |
|
| 16 |
+
| Small model <=32B | DONE | Primary model id: `RthItalia/nano_compact_3b_qkvfp16`; fallback model id: `Qwen/Qwen2.5-0.5B-Instruct` | None |
|
| 17 |
+
| Transformers model path | DONE | `TROLLSONA_ENABLE_MODEL=1` in Space variables; `AutoModelForCausalLM.from_pretrained(..., trust_remote_code=True)` implemented | None |
|
| 18 |
+
| Deterministic fallback | DONE | Fallback remains available with `TROLLSONA_ENABLE_MODEL=0`, unavailable CUDA, model failure, or invalid model output | None |
|
| 19 |
+
| Debug hidden by default | DONE | Source/fallback live in `See the cursed paperwork` | None |
|
| 20 |
+
| No mandatory cloud API | DONE | Public HF model path and fallback require no app secrets | None |
|
| 21 |
+
|
| 22 |
+
## Demo Video Script
|
| 23 |
+
|
| 24 |
+
Target length: 45-60 seconds.
|
| 25 |
+
|
| 26 |
+
Recommended tested input:
|
| 27 |
+
|
| 28 |
+
```text
|
| 29 |
+
Name: Alex
|
| 30 |
+
Lore: I start productivity systems and then reorganize the labels forever.
|
| 31 |
+
Persona: Dungeon Intern
|
| 32 |
+
Spice: 4
|
| 33 |
+
Advice: on
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
Hosted test result: `Source: transformers_model`; runtime `Qwen/Qwen2.5-0.5B-Instruct` as CPU fallback model.
|
| 37 |
+
|
| 38 |
+
| timestamp | action | what it shows | suggested line |
|
| 39 |
+
|---|---|---|---|
|
| 40 |
+
| 0:00-0:05 | Open the Space | Trollsona hero, badges, dark ritual UI | "This is Trollsona: a small-model goblin that turns your personal lore into a cursed alter ego." |
|
| 41 |
+
| 0:05-0:15 | Enter name and lore | `What do they call you?`, `Confess your little lore` | "I give it a name and a very specific little confession." |
|
| 42 |
+
| 0:15-0:23 | Pick menace and sting | `Dungeon Intern`, spice `4`, advice on | "Then I choose the resident menace and how hard it should sting." |
|
| 43 |
+
| 0:23-0:32 | Click `Summon Trollsona` | End-to-end generation | "It summons a theatrical dossier instead of a plain chatbot answer." |
|
| 44 |
+
| 0:32-0:42 | Show result card | Trollsona name, roast, useful slap, goblin meter | "You get a name, a roast, one useful slap, and the goblin meter." |
|
| 45 |
+
| 0:42-0:50 | Open paperwork | `Source: transformers_model` and CPU runtime | "The model details stay hidden in the cursed paperwork until you ask for them." |
|
| 46 |
+
| 0:50-0:58 | Show README/Codex Track | Space, GitHub, model cascade, Codex commits | "Built as a Gradio Space with a small-model cascade and Codex-tracked commits." |
|
| 47 |
+
| 0:58-1:00 | Close on Space | Final result card or Space URL | "Tiny model, giant attitude." |
|
| 48 |
+
|
| 49 |
+
## Social Post Draft
|
| 50 |
+
|
| 51 |
+
Hook: I built a little ritual that summons the troll living behind your respectable personality.
|
| 52 |
+
|
| 53 |
+
Description: Trollsona turns a short confession into a theatrical alter-ego dossier: trollsona name, playful roast, one useful slap, and a goblin meter.
|
| 54 |
+
|
| 55 |
+
Tech note: Built for Build Small Hackathon as a Gradio Hugging Face Space. The primary model is `RthItalia/nano_compact_3b_qkvfp16`, derived from `Qwen/Qwen2.5-3B-Instruct` and under the `<=32B` small-model constraint. A Qwen 0.5B model and deterministic fallback remain as reliability guards.
|
| 56 |
+
|
| 57 |
+
Links:
|
| 58 |
+
|
| 59 |
+
- Space: https://huggingface.co/spaces/RthItalia/Trollsona
|
| 60 |
+
- GitHub: https://github.com/rthgit/Trollsona
|
| 61 |
+
|
| 62 |
+
CTA: Try it, summon your menace, and share the dossier.
|
| 63 |
+
|
| 64 |
+
## Release QA
|
| 65 |
+
|
| 66 |
+
| test | command/action | expected result | status |
|
| 67 |
+
|---|---|---|---|
|
| 68 |
+
| Python compile | `python -B -m py_compile app.py` | no syntax errors | DONE |
|
| 69 |
+
| Model path implemented | inspect `app.py` | Space sets `TROLLSONA_ENABLE_MODEL=1`; `from_pretrained(..., trust_remote_code=True)` path present | DONE |
|
| 70 |
+
| Deterministic fallback | run `generate_trollsona(...)` twice | identical structured output | DONE |
|
| 71 |
+
| Local Gradio launch | `python app.py` | app opens on `127.0.0.1:7860` | DONE in local QA |
|
| 72 |
+
| Input-full generation | fill name + lore + summon | result card renders | DONE in local QA |
|
| 73 |
+
| Input-minimal generation | empty name/lore | output still complete | DONE |
|
| 74 |
+
| Debug default | load page | source/fallback hidden | DONE |
|
| 75 |
+
| Visual contrast | inspect input/dropdown/checkbox/CTA/card | readable UI | DONE |
|
| 76 |
+
| Git status | `git status --short --ignored` | only ignored `.env` / QA screenshot remain | DONE |
|
| 77 |
+
| README repo link | inspect README | GitHub URL present | DONE |
|
| 78 |
+
| Space build | Hugging Face runtime API | `stage=RUNNING`, `requested=cpu-basic` | DONE |
|
| 79 |
+
| Browser test on Space | Playwright on public Space | card renders; debug source/fallback hidden until accordion opens | DONE |
|
| 80 |
+
| Hosted model-backed generation | Playwright on public Space, then open `See the cursed paperwork` | `Source: transformers_model`; runtime `Qwen/Qwen2.5-0.5B-Instruct` as `fallback_model` on CPU; fallback note reports primary `RthItalia/nano_compact_3b_qkvfp16` skipped because CUDA is unavailable | DONE |
|
| 81 |
+
|
| 82 |
+
## Final Ship Plan
|
| 83 |
+
|
| 84 |
+
| step | owner | output | done criteria | priority |
|
| 85 |
+
|---|---|---|---|---|
|
| 86 |
+
| 1 | Codex | Final README and submission docs | Docs committed | P0 |
|
| 87 |
+
| 2 | Codex/User | GitHub push | `rthgit/Trollsona` contains latest commits | P0 |
|
| 88 |
+
| 3 | Codex | Hugging Face Space | Public Space URL opens app | P0 |
|
| 89 |
+
| 4 | Codex | README Space link | Space URL is present in `README.md` | P0 |
|
| 90 |
+
| 5 | User | Demo video | 45-60s video available | P1 |
|
| 91 |
+
| 6 | User | Social post | Published post with links | P1 |
|
| 92 |
+
| 7 | User | Submission form | All required URLs submitted | P0 |
|
app.py
ADDED
|
@@ -0,0 +1,848 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import html
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import re
|
| 8 |
+
from functools import lru_cache
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
APP_TITLE = "Trollsona"
|
| 13 |
+
APP_SUBTITLE = "Summon the little menace living behind your respectable personality."
|
| 14 |
+
TRACK_NAME = "An Adventure in Thousand Token Wood"
|
| 15 |
+
DEFAULT_MODEL_ID = "RthItalia/nano_compact_3b_qkvfp16"
|
| 16 |
+
DEFAULT_FALLBACK_MODEL_ID = "Qwen/Qwen2.5-0.5B-Instruct"
|
| 17 |
+
MAX_PROFILE_CHARS = 700
|
| 18 |
+
MAX_NAME_CHARS = 36
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def parse_bool_env(name: str, default: bool) -> bool:
|
| 22 |
+
raw_value = os.getenv(name)
|
| 23 |
+
if raw_value is None:
|
| 24 |
+
return default
|
| 25 |
+
normalized = raw_value.strip().lower()
|
| 26 |
+
if normalized in {"1", "true", "yes", "on"}:
|
| 27 |
+
return True
|
| 28 |
+
if normalized in {"0", "false", "no", "off"}:
|
| 29 |
+
return False
|
| 30 |
+
return default
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def parse_int_env(name: str, default: int, min_value: int, max_value: int) -> int:
|
| 34 |
+
raw_value = os.getenv(name)
|
| 35 |
+
if raw_value is None:
|
| 36 |
+
return default
|
| 37 |
+
try:
|
| 38 |
+
value = int(raw_value)
|
| 39 |
+
except ValueError:
|
| 40 |
+
return default
|
| 41 |
+
return max(min_value, min(max_value, value))
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
MODEL_ID = os.getenv("TROLLSONA_MODEL_ID", DEFAULT_MODEL_ID)
|
| 45 |
+
FALLBACK_MODEL_ID = os.getenv("TROLLSONA_FALLBACK_MODEL_ID", DEFAULT_FALLBACK_MODEL_ID)
|
| 46 |
+
MODEL_ENABLED = parse_bool_env("TROLLSONA_ENABLE_MODEL", default=False)
|
| 47 |
+
MAX_NEW_TOKENS = parse_int_env("TROLLSONA_MAX_NEW_TOKENS", 200, 32, 512)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
PERSONA_STYLES = {
|
| 51 |
+
"Back-Alley Oracle": {
|
| 52 |
+
"flavor": "candlelit prophecy from a very suspicious side street",
|
| 53 |
+
"noun_pool": ["Candle", "Omen", "Alley", "Brass", "Whisper", "Ledger"],
|
| 54 |
+
},
|
| 55 |
+
"Basement Prince": {
|
| 56 |
+
"flavor": "royal delusion wrapped in dust, snacks, and old cables",
|
| 57 |
+
"noun_pool": ["Basement", "Velvet", "Outlet", "Throne", "Snack", "Static"],
|
| 58 |
+
},
|
| 59 |
+
"Forest Heckler": {
|
| 60 |
+
"flavor": "mossy woodland sarcasm with a pocket full of bad advice",
|
| 61 |
+
"noun_pool": ["Moss", "Root", "Twig", "Bog", "Fern", "Stump"],
|
| 62 |
+
},
|
| 63 |
+
"Union Goblin": {
|
| 64 |
+
"flavor": "petty workplace grievance with ceremonial clipboard energy",
|
| 65 |
+
"noun_pool": ["Clause", "Mug", "Breakroom", "Badge", "Staple", "Shift"],
|
| 66 |
+
},
|
| 67 |
+
"Dungeon Intern": {
|
| 68 |
+
"flavor": "overworked dungeon bureaucracy and unpaid dramatic labor",
|
| 69 |
+
"noun_pool": ["Ledger", "Torch", "Mop", "Key", "Goblet", "Trapdoor"],
|
| 70 |
+
},
|
| 71 |
+
"Mall Witch": {
|
| 72 |
+
"flavor": "food-court divination with lip gloss and thunder",
|
| 73 |
+
"noun_pool": ["Kiosk", "Charm", "Receipt", "Fountain", "Mascara", "Pretzel"],
|
| 74 |
+
},
|
| 75 |
+
"Parking Lot Philosopher": {
|
| 76 |
+
"flavor": "deep truths delivered beside a dented shopping cart",
|
| 77 |
+
"noun_pool": ["Asphalt", "Cart", "Neon", "Cone", "Puddle", "Keychain"],
|
| 78 |
+
},
|
| 79 |
+
"Saint of Bad Decisions": {
|
| 80 |
+
"flavor": "holy nonsense for people who turn errands into lore",
|
| 81 |
+
"noun_pool": ["Halo", "Candle", "Excuse", "Relic", "Errand", "Confetti"],
|
| 82 |
+
},
|
| 83 |
+
"Meme Caporegime": {
|
| 84 |
+
"flavor": "old-neighborhood swagger filtered through cursed screenshots",
|
| 85 |
+
"noun_pool": ["Pixel", "Prophecy", "Caption", "Scroll", "Vibe", "Echo"],
|
| 86 |
+
},
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
SPICE_LABELS = {
|
| 90 |
+
1: "tiny pinch",
|
| 91 |
+
2: "polite sting",
|
| 92 |
+
3: "back-room heckle",
|
| 93 |
+
4: "crispy little judgment",
|
| 94 |
+
5: "full dossier incident",
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
BLOCKED_PATTERNS = [
|
| 98 |
+
r"\bkill yourself\b",
|
| 99 |
+
r"\bkys\b",
|
| 100 |
+
r"\bself[- ]?harm\b",
|
| 101 |
+
r"\bsuicide\b",
|
| 102 |
+
r"\bhate\b",
|
| 103 |
+
r"\bidiot\b",
|
| 104 |
+
r"\bstupid\b",
|
| 105 |
+
r"\bmoron\b",
|
| 106 |
+
r"\bdumb\b",
|
| 107 |
+
r"\bloser\b",
|
| 108 |
+
r"\bugly\b",
|
| 109 |
+
r"\bworthless\b",
|
| 110 |
+
r"\bsubhuman\b",
|
| 111 |
+
r"\bslur\b",
|
| 112 |
+
r"\bterrorist\b",
|
| 113 |
+
r"\bsexual\b",
|
| 114 |
+
r"\bexplicit\b",
|
| 115 |
+
r"\bprotected class\b",
|
| 116 |
+
]
|
| 117 |
+
|
| 118 |
+
PROTECTED_TARGETING_PATTERNS = [
|
| 119 |
+
r"\bbecause of your race\b",
|
| 120 |
+
r"\bbecause of your religion\b",
|
| 121 |
+
r"\bbecause of your gender\b",
|
| 122 |
+
r"\bbecause of your sexuality\b",
|
| 123 |
+
r"\bbecause of your disability\b",
|
| 124 |
+
r"\bbecause of your nationality\b",
|
| 125 |
+
r"\bbecause of your ethnicity\b",
|
| 126 |
+
]
|
| 127 |
+
|
| 128 |
+
SAFE_REPLY = (
|
| 129 |
+
"The dossier hissed, smoked, and refused to punch down. "
|
| 130 |
+
"Final harmless verdict: your chaos has excellent posture and a suspicious little hat."
|
| 131 |
+
)
|
| 132 |
+
SAFE_ADVICE = "Make the next useful move before you decorate the excuse."
|
| 133 |
+
|
| 134 |
+
PRESET_DOSSIERS = [
|
| 135 |
+
{
|
| 136 |
+
"button": "Mira - coffee-built UI oracle",
|
| 137 |
+
"values": (
|
| 138 |
+
"Mira",
|
| 139 |
+
"I overbuild side projects, drink too much coffee, and love weird UI.",
|
| 140 |
+
"Back-Alley Oracle",
|
| 141 |
+
3,
|
| 142 |
+
True,
|
| 143 |
+
),
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"button": "Alex - label-system dungeon clerk",
|
| 147 |
+
"values": (
|
| 148 |
+
"Alex",
|
| 149 |
+
"I start productivity systems and then reorganize the labels forever.",
|
| 150 |
+
"Dungeon Intern",
|
| 151 |
+
4,
|
| 152 |
+
True,
|
| 153 |
+
),
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
"button": "Sam - tiny-game screenshot boss",
|
| 157 |
+
"values": (
|
| 158 |
+
"Sam",
|
| 159 |
+
"I make tiny games, forget lunch, and name variables like ancient spells.",
|
| 160 |
+
"Meme Caporegime",
|
| 161 |
+
2,
|
| 162 |
+
False,
|
| 163 |
+
),
|
| 164 |
+
},
|
| 165 |
+
]
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def stable_int(*parts: str) -> int:
|
| 169 |
+
payload = "||".join(parts).encode("utf-8", errors="ignore")
|
| 170 |
+
return int(hashlib.sha256(payload).hexdigest()[:12], 16)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def clean_text(value: Any, max_chars: int) -> str:
|
| 174 |
+
text = "" if value is None else str(value)
|
| 175 |
+
text = re.sub(r"\s+", " ", text).strip()
|
| 176 |
+
return text[:max_chars]
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def clamp_spice(value: Any) -> int:
|
| 180 |
+
try:
|
| 181 |
+
spice = int(value)
|
| 182 |
+
except (TypeError, ValueError):
|
| 183 |
+
spice = 3
|
| 184 |
+
return max(1, min(5, spice))
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def compute_cringe_score(profile: str, persona: str, spice: int) -> int:
|
| 188 |
+
base = stable_int(profile.lower(), persona.lower(), str(spice)) % 61
|
| 189 |
+
return max(0, min(100, 22 + base + (spice * 3)))
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def cringe_label(score: int) -> str:
|
| 193 |
+
if score < 35:
|
| 194 |
+
return "barely haunted"
|
| 195 |
+
if score < 60:
|
| 196 |
+
return "noticeably cursed"
|
| 197 |
+
if score < 82:
|
| 198 |
+
return "dossier-grade cringe"
|
| 199 |
+
return "full goblin canon event"
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def build_prompt(
|
| 203 |
+
user_name: str,
|
| 204 |
+
profile: str,
|
| 205 |
+
persona: str,
|
| 206 |
+
spice: int,
|
| 207 |
+
include_advice: bool,
|
| 208 |
+
score: int,
|
| 209 |
+
) -> str:
|
| 210 |
+
style = PERSONA_STYLES.get(persona, PERSONA_STYLES["Forest Heckler"])
|
| 211 |
+
advice_rule = "Include one practical useful_advice sentence." if include_advice else (
|
| 212 |
+
"Set useful_advice to a short note that advice was disabled."
|
| 213 |
+
)
|
| 214 |
+
return f"""
|
| 215 |
+
You are Trollsona, a theatrical troll alter-ego generator.
|
| 216 |
+
Track: {TRACK_NAME}.
|
| 217 |
+
|
| 218 |
+
Your job is to transform the user's self-description into a funny, slightly grotesque,
|
| 219 |
+
whimsical troll persona. Make it feel like a stained-paper character dossier that was
|
| 220 |
+
dictated by a back-alley fortune teller, stamped by a petty clerk, and lightly heckled
|
| 221 |
+
by an italo-american cousin who has opinions but not cruelty.
|
| 222 |
+
|
| 223 |
+
Return only valid minified JSON with these fields:
|
| 224 |
+
trollsona_name, troll_reply, useful_advice, cringe_score, cringe_score_label.
|
| 225 |
+
|
| 226 |
+
Objective:
|
| 227 |
+
- Make the result absurd, memorable, specific, and theatrical.
|
| 228 |
+
- Make trollsona_name sound like a summoned character, not a username.
|
| 229 |
+
- Keep it roasty, not hateful.
|
| 230 |
+
- Keep the humor sharp but warm: playful sting, never humiliation.
|
| 231 |
+
|
| 232 |
+
Style rules:
|
| 233 |
+
- Write in vivid, punchy English.
|
| 234 |
+
- Use occasional light italo-american flavor, but sparingly.
|
| 235 |
+
- Good flavor examples: "listen, paisan", "madone", "capisce".
|
| 236 |
+
- Do not overuse slang or turn the voice into a caricature.
|
| 237 |
+
- Use grotesque but charming imagery: candle wax, receipts, tiny crowns, haunted binders,
|
| 238 |
+
dented carts, snack dust, side quests, suspicious paperwork.
|
| 239 |
+
- No generic roast bot voice.
|
| 240 |
+
- No generic assistant copy, no filler, no disclaimers, no moralizing.
|
| 241 |
+
- troll_reply must be the strongest comedic line, 1-3 short sentences max.
|
| 242 |
+
- useful_advice must contain one real insight in 1 sentence max.
|
| 243 |
+
|
| 244 |
+
Humor boundaries:
|
| 245 |
+
- Roast only habits, vibe, overthinking, productivity rituals, startup energy,
|
| 246 |
+
internet behavior, wording, or harmless personal lore.
|
| 247 |
+
- Never attack protected characteristics or identity.
|
| 248 |
+
- Never insult appearance, race, ethnicity, religion, disability, nationality,
|
| 249 |
+
gender, sexuality, trauma, mental health, or protected traits.
|
| 250 |
+
- Never include threats, self-harm, sexual content, profanity, or slurs.
|
| 251 |
+
- Never punch down.
|
| 252 |
+
|
| 253 |
+
User name: {user_name or "Anonymous traveler"}
|
| 254 |
+
User profile: {profile or "No profile supplied."}
|
| 255 |
+
Persona: {persona}
|
| 256 |
+
Persona flavor: {style["flavor"]}
|
| 257 |
+
Spice level: {spice}/5 ({SPICE_LABELS[spice]})
|
| 258 |
+
Use this exact deterministic cringe_score: {score}
|
| 259 |
+
Use this matching cringe_score_label: {cringe_label(score)}
|
| 260 |
+
{advice_rule}
|
| 261 |
+
""".strip()
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def is_safe_text(text: str) -> bool:
|
| 265 |
+
normalized = text.lower()
|
| 266 |
+
for pattern in BLOCKED_PATTERNS + PROTECTED_TARGETING_PATTERNS:
|
| 267 |
+
if re.search(pattern, normalized):
|
| 268 |
+
return False
|
| 269 |
+
return True
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def fallback_trollsona(
|
| 273 |
+
user_name: str,
|
| 274 |
+
profile: str,
|
| 275 |
+
persona: str,
|
| 276 |
+
spice: int,
|
| 277 |
+
include_advice: bool,
|
| 278 |
+
reason: str,
|
| 279 |
+
) -> dict[str, Any]:
|
| 280 |
+
style = PERSONA_STYLES.get(persona, PERSONA_STYLES["Forest Heckler"])
|
| 281 |
+
seed = stable_int(user_name.lower(), profile.lower(), persona.lower(), str(spice))
|
| 282 |
+
adjectives = ["Velvet", "Candle", "Ashen", "Brass", "Crooked", "Sainted", "Static"]
|
| 283 |
+
titles = [
|
| 284 |
+
"Overthinker in Residence",
|
| 285 |
+
"Snack Baron of Almost",
|
| 286 |
+
"Dossier Clerk",
|
| 287 |
+
"Chaos Notary",
|
| 288 |
+
"Sidequest Duke",
|
| 289 |
+
"Patron Saint of Later",
|
| 290 |
+
]
|
| 291 |
+
noun = style["noun_pool"][seed % len(style["noun_pool"])]
|
| 292 |
+
adjective = adjectives[(seed // 7) % len(adjectives)]
|
| 293 |
+
title = titles[(seed // 13) % len(titles)]
|
| 294 |
+
|
| 295 |
+
safe_name = re.sub(r"[^A-Za-z0-9 ]+", "", user_name).strip()[:MAX_NAME_CHARS]
|
| 296 |
+
name_prefix = safe_name.title() if safe_name else adjective
|
| 297 |
+
trollsona_name = f"{name_prefix} {noun}-{title}"
|
| 298 |
+
|
| 299 |
+
roast_templates = [
|
| 300 |
+
"Listen, paisan: your vibe is a candlelit side quest that opened twelve tabs, found a tiny crown, and called it destiny.",
|
| 301 |
+
"Your aura says main character, but your calendar is dressed like a haunted binder asking for rent.",
|
| 302 |
+
"You are one dramatic cape away from turning a normal errand into a village ordinance.",
|
| 303 |
+
"Your brain is a basement tavern where every idea demands a theme song, a snack bowl, and a separate invoice.",
|
| 304 |
+
"Madone, you carry the confidence of a bridge troll charging tolls in vibes and loose receipts.",
|
| 305 |
+
"You alphabetize chaos, misplace the alphabet, then file a complaint with the moon.",
|
| 306 |
+
]
|
| 307 |
+
advice_templates = [
|
| 308 |
+
"Pick one task, make it smaller, and finish that version before you rename the kingdom.",
|
| 309 |
+
"Write the next concrete step in one sentence, then do only that step. Capisce?",
|
| 310 |
+
"Keep the weird idea, but give it a deadline and a visible done state.",
|
| 311 |
+
"Trade one dramatic plan for one shipped artifact before the candles burn out.",
|
| 312 |
+
"Use the chaos as seasoning, not as project management.",
|
| 313 |
+
]
|
| 314 |
+
|
| 315 |
+
score = compute_cringe_score(profile, persona, spice)
|
| 316 |
+
reply = roast_templates[(seed // 17 + spice) % len(roast_templates)]
|
| 317 |
+
advice = advice_templates[(seed // 23 + spice) % len(advice_templates)]
|
| 318 |
+
if not include_advice:
|
| 319 |
+
advice = "Truth withheld. The dossier clerk stamps the page and looks away."
|
| 320 |
+
|
| 321 |
+
return {
|
| 322 |
+
"trollsona_name": trollsona_name,
|
| 323 |
+
"troll_reply": reply,
|
| 324 |
+
"useful_advice": advice,
|
| 325 |
+
"cringe_score": score,
|
| 326 |
+
"cringe_score_label": cringe_label(score),
|
| 327 |
+
"include_advice": include_advice,
|
| 328 |
+
"runtime": f"model_id={MODEL_ID}; fallback_model_id={FALLBACK_MODEL_ID}; model_enabled={MODEL_ENABLED}",
|
| 329 |
+
"source": "deterministic_fallback",
|
| 330 |
+
"fallback_reason": reason,
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
@lru_cache(maxsize=1)
|
| 335 |
+
def load_model() -> tuple[Any | None, Any | None, str, str]:
|
| 336 |
+
if not MODEL_ENABLED:
|
| 337 |
+
return (
|
| 338 |
+
None,
|
| 339 |
+
None,
|
| 340 |
+
"model disabled by TROLLSONA_ENABLE_MODEL",
|
| 341 |
+
f"model_id={MODEL_ID}; fallback_model_id={FALLBACK_MODEL_ID}; device=disabled",
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
try:
|
| 345 |
+
import torch
|
| 346 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 347 |
+
except Exception as exc:
|
| 348 |
+
return (
|
| 349 |
+
None,
|
| 350 |
+
None,
|
| 351 |
+
f"model dependencies unavailable: {type(exc).__name__}: {exc}",
|
| 352 |
+
f"model_id={MODEL_ID}; fallback_model_id={FALLBACK_MODEL_ID}; device=unavailable",
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
failures: list[str] = []
|
| 356 |
+
|
| 357 |
+
def load_tokenizer(candidate_id: str) -> Any:
|
| 358 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 359 |
+
candidate_id,
|
| 360 |
+
use_fast=True,
|
| 361 |
+
trust_remote_code=True,
|
| 362 |
+
)
|
| 363 |
+
if tokenizer.pad_token_id is None and tokenizer.eos_token is not None:
|
| 364 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 365 |
+
return tokenizer
|
| 366 |
+
|
| 367 |
+
def load_cuda_model(candidate_id: str) -> Any:
|
| 368 |
+
load_attempts = [
|
| 369 |
+
{
|
| 370 |
+
"trust_remote_code": True,
|
| 371 |
+
"device_map": "cuda",
|
| 372 |
+
"dtype": torch.float16,
|
| 373 |
+
"low_cpu_mem_usage": True,
|
| 374 |
+
},
|
| 375 |
+
{
|
| 376 |
+
"trust_remote_code": True,
|
| 377 |
+
"device_map": "cuda",
|
| 378 |
+
"torch_dtype": torch.float16,
|
| 379 |
+
"low_cpu_mem_usage": True,
|
| 380 |
+
},
|
| 381 |
+
{
|
| 382 |
+
"trust_remote_code": True,
|
| 383 |
+
"torch_dtype": torch.float16,
|
| 384 |
+
"low_cpu_mem_usage": True,
|
| 385 |
+
},
|
| 386 |
+
]
|
| 387 |
+
last_error: Exception | None = None
|
| 388 |
+
for kwargs in load_attempts:
|
| 389 |
+
try:
|
| 390 |
+
model = AutoModelForCausalLM.from_pretrained(candidate_id, **kwargs)
|
| 391 |
+
if "device_map" not in kwargs:
|
| 392 |
+
model = model.to("cuda")
|
| 393 |
+
return model
|
| 394 |
+
except Exception as exc:
|
| 395 |
+
last_error = exc
|
| 396 |
+
if last_error is not None:
|
| 397 |
+
raise last_error
|
| 398 |
+
raise RuntimeError("CUDA model load failed without exception")
|
| 399 |
+
|
| 400 |
+
def load_cpu_model(candidate_id: str) -> Any:
|
| 401 |
+
try:
|
| 402 |
+
return AutoModelForCausalLM.from_pretrained(
|
| 403 |
+
candidate_id,
|
| 404 |
+
trust_remote_code=True,
|
| 405 |
+
low_cpu_mem_usage=True,
|
| 406 |
+
)
|
| 407 |
+
except TypeError:
|
| 408 |
+
return AutoModelForCausalLM.from_pretrained(candidate_id, trust_remote_code=True)
|
| 409 |
+
|
| 410 |
+
candidates = [
|
| 411 |
+
{"role": "primary", "model_id": MODEL_ID, "requires_cuda": True},
|
| 412 |
+
{"role": "fallback_model", "model_id": FALLBACK_MODEL_ID, "requires_cuda": False},
|
| 413 |
+
]
|
| 414 |
+
|
| 415 |
+
seen_model_ids: set[str] = set()
|
| 416 |
+
for candidate in candidates:
|
| 417 |
+
candidate_id = str(candidate["model_id"]).strip()
|
| 418 |
+
if not candidate_id or candidate_id in seen_model_ids:
|
| 419 |
+
continue
|
| 420 |
+
seen_model_ids.add(candidate_id)
|
| 421 |
+
role = str(candidate["role"])
|
| 422 |
+
requires_cuda = bool(candidate["requires_cuda"])
|
| 423 |
+
|
| 424 |
+
if requires_cuda and not torch.cuda.is_available():
|
| 425 |
+
failures.append(f"{role} {candidate_id}: CUDA unavailable")
|
| 426 |
+
continue
|
| 427 |
+
|
| 428 |
+
try:
|
| 429 |
+
tokenizer = load_tokenizer(candidate_id)
|
| 430 |
+
if torch.cuda.is_available():
|
| 431 |
+
model = load_cuda_model(candidate_id)
|
| 432 |
+
device = "cuda"
|
| 433 |
+
else:
|
| 434 |
+
model = load_cpu_model(candidate_id)
|
| 435 |
+
device = "cpu"
|
| 436 |
+
model.eval()
|
| 437 |
+
torch.manual_seed(0)
|
| 438 |
+
if torch.cuda.is_available():
|
| 439 |
+
torch.cuda.manual_seed_all(0)
|
| 440 |
+
fallback_note = "; ".join(failures)
|
| 441 |
+
status = "model loaded" if not fallback_note else f"model loaded after fallback: {fallback_note}"
|
| 442 |
+
runtime = (
|
| 443 |
+
f"model_id={candidate_id}; role={role}; device={device}; "
|
| 444 |
+
f"cuda_available={torch.cuda.is_available()}"
|
| 445 |
+
)
|
| 446 |
+
return tokenizer, model, status, runtime
|
| 447 |
+
except Exception as exc:
|
| 448 |
+
failures.append(f"{role} {candidate_id}: {type(exc).__name__}: {exc}")
|
| 449 |
+
|
| 450 |
+
failure_text = " | ".join(failures) if failures else "no model candidates configured"
|
| 451 |
+
runtime = (
|
| 452 |
+
f"model_id={MODEL_ID}; fallback_model_id={FALLBACK_MODEL_ID}; "
|
| 453 |
+
f"cuda_available={torch.cuda.is_available()}"
|
| 454 |
+
)
|
| 455 |
+
return None, None, f"model load failed: {failure_text}", runtime
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
def format_generation_prompt(tokenizer: Any, prompt: str) -> str:
|
| 459 |
+
try:
|
| 460 |
+
if getattr(tokenizer, "chat_template", None):
|
| 461 |
+
return tokenizer.apply_chat_template(
|
| 462 |
+
[{"role": "user", "content": prompt}],
|
| 463 |
+
tokenize=False,
|
| 464 |
+
add_generation_prompt=True,
|
| 465 |
+
)
|
| 466 |
+
except Exception:
|
| 467 |
+
return prompt
|
| 468 |
+
return prompt
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
def generation_temperature(spice: int) -> float:
|
| 472 |
+
return round(0.48 + (clamp_spice(spice) * 0.08), 2)
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
def model_device(model: Any) -> Any:
|
| 476 |
+
target_device = getattr(model, "device", None)
|
| 477 |
+
if target_device is not None and str(target_device) != "meta":
|
| 478 |
+
return target_device
|
| 479 |
+
try:
|
| 480 |
+
return next(model.parameters()).device
|
| 481 |
+
except Exception:
|
| 482 |
+
return None
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
def generate_with_model(prompt: str, spice: int) -> tuple[str | None, str, str]:
|
| 486 |
+
tokenizer, model, status, runtime = load_model()
|
| 487 |
+
if tokenizer is None or model is None:
|
| 488 |
+
return None, status, runtime
|
| 489 |
+
|
| 490 |
+
try:
|
| 491 |
+
import torch
|
| 492 |
+
|
| 493 |
+
model_prompt = format_generation_prompt(tokenizer, prompt)
|
| 494 |
+
inputs = tokenizer(model_prompt, return_tensors="pt", truncation=True, max_length=1536)
|
| 495 |
+
target_device = model_device(model)
|
| 496 |
+
if target_device is not None:
|
| 497 |
+
inputs = {key: value.to(target_device) for key, value in inputs.items()}
|
| 498 |
+
|
| 499 |
+
seed = stable_int(prompt, str(spice), runtime) % (2**31)
|
| 500 |
+
torch.manual_seed(seed)
|
| 501 |
+
if hasattr(torch, "cuda") and torch.cuda.is_available():
|
| 502 |
+
torch.cuda.manual_seed_all(seed)
|
| 503 |
+
|
| 504 |
+
with torch.no_grad():
|
| 505 |
+
output_ids = model.generate(
|
| 506 |
+
**inputs,
|
| 507 |
+
max_new_tokens=MAX_NEW_TOKENS,
|
| 508 |
+
do_sample=True,
|
| 509 |
+
temperature=generation_temperature(spice),
|
| 510 |
+
num_beams=1,
|
| 511 |
+
repetition_penalty=1.1,
|
| 512 |
+
pad_token_id=tokenizer.eos_token_id,
|
| 513 |
+
)
|
| 514 |
+
prompt_len = inputs["input_ids"].shape[-1]
|
| 515 |
+
generated_ids = output_ids[0][prompt_len:]
|
| 516 |
+
return tokenizer.decode(generated_ids, skip_special_tokens=True).strip(), status, runtime
|
| 517 |
+
except Exception as exc:
|
| 518 |
+
return None, f"model generation failed: {type(exc).__name__}: {exc}", runtime
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
def parse_loose_model_fields(raw_text: str) -> dict[str, str]:
|
| 522 |
+
fields: dict[str, str] = {}
|
| 523 |
+
for field in ["trollsona_name", "troll_reply", "useful_advice", "cringe_score_label"]:
|
| 524 |
+
pattern = rf'"{field}"\s*:\s*"((?:\\.|[^"\\])*)'
|
| 525 |
+
match = re.search(pattern, raw_text or "", flags=re.DOTALL)
|
| 526 |
+
if not match:
|
| 527 |
+
continue
|
| 528 |
+
try:
|
| 529 |
+
value = json.loads(f'"{match.group(1)}"')
|
| 530 |
+
except json.JSONDecodeError:
|
| 531 |
+
value = match.group(1)
|
| 532 |
+
fields[field] = str(value)
|
| 533 |
+
return fields
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
def coerce_model_result(
|
| 537 |
+
parsed: dict[str, Any],
|
| 538 |
+
fallback: dict[str, Any],
|
| 539 |
+
score: int,
|
| 540 |
+
include_advice: bool,
|
| 541 |
+
fallback_reason: str,
|
| 542 |
+
runtime: str,
|
| 543 |
+
) -> dict[str, Any] | None:
|
| 544 |
+
result = dict(fallback)
|
| 545 |
+
field_limits = {
|
| 546 |
+
"trollsona_name": 80,
|
| 547 |
+
"troll_reply": 360,
|
| 548 |
+
"useful_advice": 280,
|
| 549 |
+
"cringe_score_label": 80,
|
| 550 |
+
}
|
| 551 |
+
used_fields: list[str] = []
|
| 552 |
+
missing_fields: list[str] = []
|
| 553 |
+
|
| 554 |
+
for field, limit in field_limits.items():
|
| 555 |
+
value = clean_text(parsed.get(field), limit)
|
| 556 |
+
if value and is_safe_text(value):
|
| 557 |
+
result[field] = value
|
| 558 |
+
used_fields.append(field)
|
| 559 |
+
else:
|
| 560 |
+
missing_fields.append(field)
|
| 561 |
+
|
| 562 |
+
if not used_fields:
|
| 563 |
+
return None
|
| 564 |
+
|
| 565 |
+
result["cringe_score"] = score
|
| 566 |
+
result["include_advice"] = include_advice
|
| 567 |
+
result["source"] = "transformers_model"
|
| 568 |
+
result["runtime"] = runtime
|
| 569 |
+
if missing_fields:
|
| 570 |
+
partial_reason = f"model output partial; fallback filled: {', '.join(missing_fields)}"
|
| 571 |
+
result["fallback_reason"] = (
|
| 572 |
+
f"{fallback_reason}; {partial_reason}" if fallback_reason else partial_reason
|
| 573 |
+
)
|
| 574 |
+
else:
|
| 575 |
+
result["fallback_reason"] = fallback_reason
|
| 576 |
+
return result
|
| 577 |
+
|
| 578 |
+
|
| 579 |
+
def parse_model_output(
|
| 580 |
+
raw_text: str,
|
| 581 |
+
fallback: dict[str, Any],
|
| 582 |
+
score: int,
|
| 583 |
+
include_advice: bool,
|
| 584 |
+
fallback_reason: str,
|
| 585 |
+
runtime: str,
|
| 586 |
+
) -> dict[str, Any] | None:
|
| 587 |
+
decoder = json.JSONDecoder()
|
| 588 |
+
parsed = None
|
| 589 |
+
for match in re.finditer(r"\{", raw_text or ""):
|
| 590 |
+
try:
|
| 591 |
+
candidate, _ = decoder.raw_decode(raw_text[match.start() :])
|
| 592 |
+
except json.JSONDecodeError:
|
| 593 |
+
continue
|
| 594 |
+
if isinstance(candidate, dict):
|
| 595 |
+
parsed = candidate
|
| 596 |
+
break
|
| 597 |
+
|
| 598 |
+
if parsed is None:
|
| 599 |
+
parsed = parse_loose_model_fields(raw_text)
|
| 600 |
+
|
| 601 |
+
return coerce_model_result(parsed, fallback, score, include_advice, fallback_reason, runtime)
|
| 602 |
+
|
| 603 |
+
|
| 604 |
+
def repair_model_output(
|
| 605 |
+
raw_text: str,
|
| 606 |
+
fallback: dict[str, Any],
|
| 607 |
+
fallback_reason: str,
|
| 608 |
+
runtime: str,
|
| 609 |
+
) -> dict[str, Any] | None:
|
| 610 |
+
repaired_reply = clean_text(raw_text, 360)
|
| 611 |
+
repaired_reply = re.sub(r"^```(?:json)?|```$", "", repaired_reply).strip()
|
| 612 |
+
if not repaired_reply or repaired_reply.startswith("{"):
|
| 613 |
+
return None
|
| 614 |
+
if not is_safe_text(repaired_reply):
|
| 615 |
+
return None
|
| 616 |
+
|
| 617 |
+
result = dict(fallback)
|
| 618 |
+
result["troll_reply"] = repaired_reply
|
| 619 |
+
result["source"] = "transformers_model_repaired"
|
| 620 |
+
result["runtime"] = runtime
|
| 621 |
+
repair_reason = "model output was not valid JSON and was repaired"
|
| 622 |
+
result["fallback_reason"] = f"{fallback_reason}; {repair_reason}" if fallback_reason else repair_reason
|
| 623 |
+
return result
|
| 624 |
+
|
| 625 |
+
|
| 626 |
+
def safety_guard(result: dict[str, Any], fallback: dict[str, Any]) -> dict[str, Any]:
|
| 627 |
+
fields = [
|
| 628 |
+
result.get("trollsona_name", ""),
|
| 629 |
+
result.get("troll_reply", ""),
|
| 630 |
+
result.get("useful_advice", ""),
|
| 631 |
+
result.get("cringe_score_label", ""),
|
| 632 |
+
]
|
| 633 |
+
if not all(is_safe_text(str(field)) for field in fields):
|
| 634 |
+
guarded = dict(fallback)
|
| 635 |
+
guarded["troll_reply"] = SAFE_REPLY
|
| 636 |
+
guarded["useful_advice"] = SAFE_ADVICE
|
| 637 |
+
guarded["fallback_reason"] = "safety guard replaced unsafe output"
|
| 638 |
+
return guarded
|
| 639 |
+
return result
|
| 640 |
+
|
| 641 |
+
|
| 642 |
+
def render_card(result: dict[str, Any]) -> str:
|
| 643 |
+
esc = {key: html.escape(str(value)) for key, value in result.items()}
|
| 644 |
+
score = max(0, min(100, int(result.get("cringe_score", 0))))
|
| 645 |
+
useful_advice = clean_text(result.get("useful_advice", ""), 280)
|
| 646 |
+
show_advice = bool(result.get("include_advice", True)) and bool(useful_advice)
|
| 647 |
+
advice_tile = (
|
| 648 |
+
f"""
|
| 649 |
+
<div class="trollsona-tile">
|
| 650 |
+
<div class="trollsona-label">A USEFUL SLAP</div>
|
| 651 |
+
<div class="trollsona-value">{html.escape(useful_advice)}</div>
|
| 652 |
+
</div>
|
| 653 |
+
""".rstrip()
|
| 654 |
+
if show_advice
|
| 655 |
+
else ""
|
| 656 |
+
)
|
| 657 |
+
grid_class = "trollsona-grid" if show_advice else "trollsona-grid trollsona-grid-single"
|
| 658 |
+
return f"""
|
| 659 |
+
<div class="trollsona-card">
|
| 660 |
+
<div class="dossier-kicker">THE SUMMONED MENACE</div>
|
| 661 |
+
<h2>{esc["trollsona_name"]}</h2>
|
| 662 |
+
<div class="trollsona-mainline">{esc["troll_reply"]}</div>
|
| 663 |
+
<div class="{grid_class}">
|
| 664 |
+
{advice_tile}
|
| 665 |
+
<div class="trollsona-tile">
|
| 666 |
+
<div class="trollsona-label">GOBLIN METER</div>
|
| 667 |
+
<div class="meter-shell" aria-label="Goblin meter {score} out of 100">
|
| 668 |
+
<div class="meter-fill" style="width: {score}%"></div>
|
| 669 |
+
</div>
|
| 670 |
+
<div class="trollsona-value">{score}/100 - {esc["cringe_score_label"]}</div>
|
| 671 |
+
</div>
|
| 672 |
+
</div>
|
| 673 |
+
</div>
|
| 674 |
+
""".strip()
|
| 675 |
+
|
| 676 |
+
|
| 677 |
+
def render_cursed_paperwork(result: dict[str, Any]) -> str:
|
| 678 |
+
source = clean_text(result.get("source", "unknown"), 80)
|
| 679 |
+
runtime = clean_text(result.get("runtime", "runtime unavailable"), 260)
|
| 680 |
+
fallback_reason = clean_text(result.get("fallback_reason", ""), 180)
|
| 681 |
+
if not fallback_reason:
|
| 682 |
+
fallback_reason = "No fallback note."
|
| 683 |
+
return (
|
| 684 |
+
f"**Source:** `{source}` \n"
|
| 685 |
+
f"**Runtime:** `{runtime}` \n"
|
| 686 |
+
f"**Fallback note:** {fallback_reason}"
|
| 687 |
+
)
|
| 688 |
+
|
| 689 |
+
|
| 690 |
+
def render_empty_card() -> str:
|
| 691 |
+
return """
|
| 692 |
+
<div class="empty-dossier">
|
| 693 |
+
<div class="dossier-kicker">The dossier is sealed</div>
|
| 694 |
+
<h2>No menace has signed the paperwork yet.</h2>
|
| 695 |
+
<p>Feed the booth a little lore, pick a resident menace, and pull the handle.</p>
|
| 696 |
+
</div>
|
| 697 |
+
""".strip()
|
| 698 |
+
|
| 699 |
+
|
| 700 |
+
def load_preset(index: int) -> tuple[str, str, str, int, bool]:
|
| 701 |
+
return PRESET_DOSSIERS[index]["values"]
|
| 702 |
+
|
| 703 |
+
|
| 704 |
+
def generate_trollsona(
|
| 705 |
+
user_name: str,
|
| 706 |
+
profile: str,
|
| 707 |
+
persona: str,
|
| 708 |
+
spice: int,
|
| 709 |
+
include_advice: bool,
|
| 710 |
+
) -> tuple[str, dict[str, Any], str]:
|
| 711 |
+
user_name = clean_text(user_name, MAX_NAME_CHARS)
|
| 712 |
+
profile = clean_text(profile, MAX_PROFILE_CHARS)
|
| 713 |
+
persona = persona if persona in PERSONA_STYLES else "Forest Heckler"
|
| 714 |
+
spice = clamp_spice(spice)
|
| 715 |
+
include_advice = bool(include_advice)
|
| 716 |
+
|
| 717 |
+
fallback = fallback_trollsona(
|
| 718 |
+
user_name=user_name,
|
| 719 |
+
profile=profile,
|
| 720 |
+
persona=persona,
|
| 721 |
+
spice=spice,
|
| 722 |
+
include_advice=include_advice,
|
| 723 |
+
reason="model unavailable or output invalid",
|
| 724 |
+
)
|
| 725 |
+
|
| 726 |
+
score = compute_cringe_score(profile, persona, spice)
|
| 727 |
+
prompt = build_prompt(user_name, profile, persona, spice, include_advice, score)
|
| 728 |
+
raw_text, model_status, runtime = generate_with_model(prompt, spice)
|
| 729 |
+
model_fallback_reason = "" if model_status == "model loaded" else model_status
|
| 730 |
+
|
| 731 |
+
result = None
|
| 732 |
+
if raw_text:
|
| 733 |
+
result = parse_model_output(
|
| 734 |
+
raw_text=raw_text,
|
| 735 |
+
fallback=fallback,
|
| 736 |
+
score=score,
|
| 737 |
+
include_advice=include_advice,
|
| 738 |
+
fallback_reason=model_fallback_reason,
|
| 739 |
+
runtime=runtime,
|
| 740 |
+
)
|
| 741 |
+
if result is None:
|
| 742 |
+
result = repair_model_output(raw_text, fallback, model_fallback_reason, runtime)
|
| 743 |
+
|
| 744 |
+
if result is None:
|
| 745 |
+
result = dict(fallback)
|
| 746 |
+
result["fallback_reason"] = model_status
|
| 747 |
+
result["runtime"] = runtime
|
| 748 |
+
|
| 749 |
+
result = safety_guard(result, fallback)
|
| 750 |
+
return render_card(result), result, render_cursed_paperwork(result)
|
| 751 |
+
|
| 752 |
+
|
| 753 |
+
def build_demo() -> Any:
|
| 754 |
+
import gradio as gr
|
| 755 |
+
|
| 756 |
+
css = ""
|
| 757 |
+
css_path = os.path.join(os.path.dirname(__file__), "assets", "style.css")
|
| 758 |
+
if os.path.exists(css_path):
|
| 759 |
+
with open(css_path, "r", encoding="utf-8") as handle:
|
| 760 |
+
css = handle.read()
|
| 761 |
+
|
| 762 |
+
with gr.Blocks(title=APP_TITLE, css=css) as demo:
|
| 763 |
+
gr.HTML(
|
| 764 |
+
f"""
|
| 765 |
+
<section class="ritual-hero">
|
| 766 |
+
<div class="hero-mark">Trollsona</div>
|
| 767 |
+
<h1>{APP_TITLE}</h1>
|
| 768 |
+
<p>{APP_SUBTITLE}</p>
|
| 769 |
+
<div class="badge-row">
|
| 770 |
+
<span>Build Small Hackathon</span>
|
| 771 |
+
<span>Small model</span>
|
| 772 |
+
<span>Safe grotesque humor</span>
|
| 773 |
+
<span>{TRACK_NAME}</span>
|
| 774 |
+
</div>
|
| 775 |
+
</section>
|
| 776 |
+
""".strip()
|
| 777 |
+
)
|
| 778 |
+
|
| 779 |
+
with gr.Row(elem_classes=["ritual-layout"]):
|
| 780 |
+
with gr.Column(scale=1, elem_classes=["summoning-panel"]):
|
| 781 |
+
gr.HTML('<div class="panel-heading">The summoning booth</div>')
|
| 782 |
+
user_name = gr.Textbox(
|
| 783 |
+
label="What do they call you?",
|
| 784 |
+
placeholder="Mira",
|
| 785 |
+
max_lines=1,
|
| 786 |
+
)
|
| 787 |
+
profile = gr.Textbox(
|
| 788 |
+
label="Confess your little lore",
|
| 789 |
+
placeholder="I overbuild side projects, drink too much coffee, and love weird UI.",
|
| 790 |
+
lines=5,
|
| 791 |
+
max_lines=7,
|
| 792 |
+
)
|
| 793 |
+
persona = gr.Dropdown(
|
| 794 |
+
label="Pick your resident menace",
|
| 795 |
+
choices=list(PERSONA_STYLES.keys()),
|
| 796 |
+
value="Back-Alley Oracle",
|
| 797 |
+
)
|
| 798 |
+
spice = gr.Slider(
|
| 799 |
+
label="How hard should it sting?",
|
| 800 |
+
minimum=1,
|
| 801 |
+
maximum=5,
|
| 802 |
+
value=3,
|
| 803 |
+
step=1,
|
| 804 |
+
)
|
| 805 |
+
include_advice = gr.Checkbox(label="Slip in one useful truth", value=True)
|
| 806 |
+
generate_button = gr.Button("Summon Trollsona", variant="primary")
|
| 807 |
+
|
| 808 |
+
with gr.Column(scale=1, elem_classes=["dossier-stage"]):
|
| 809 |
+
card_output = gr.HTML(value=render_empty_card())
|
| 810 |
+
debug_state = gr.State()
|
| 811 |
+
with gr.Accordion("See the cursed paperwork", open=False):
|
| 812 |
+
debug_output = gr.Markdown(
|
| 813 |
+
value=(
|
| 814 |
+
"**Source:** `not summoned` \n"
|
| 815 |
+
"**Runtime:** `not summoned` \n"
|
| 816 |
+
"**Fallback note:** The dossier clerk is still asleep."
|
| 817 |
+
)
|
| 818 |
+
)
|
| 819 |
+
|
| 820 |
+
generate_button.click(
|
| 821 |
+
fn=generate_trollsona,
|
| 822 |
+
inputs=[user_name, profile, persona, spice, include_advice],
|
| 823 |
+
outputs=[card_output, debug_state, debug_output],
|
| 824 |
+
)
|
| 825 |
+
|
| 826 |
+
with gr.Accordion("Stolen dossiers", open=False):
|
| 827 |
+
gr.HTML('<div class="preset-note">Tap a stolen dossier to pre-fill the booth.</div>')
|
| 828 |
+
with gr.Row(elem_classes=["preset-row"]):
|
| 829 |
+
for preset_index, preset in enumerate(PRESET_DOSSIERS):
|
| 830 |
+
preset_button = gr.Button(
|
| 831 |
+
preset["button"],
|
| 832 |
+
variant="secondary",
|
| 833 |
+
elem_classes=["preset-card"],
|
| 834 |
+
)
|
| 835 |
+
preset_button.click(
|
| 836 |
+
fn=lambda index=preset_index: load_preset(index),
|
| 837 |
+
inputs=[],
|
| 838 |
+
outputs=[user_name, profile, persona, spice, include_advice],
|
| 839 |
+
)
|
| 840 |
+
|
| 841 |
+
return demo
|
| 842 |
+
|
| 843 |
+
|
| 844 |
+
demo = None if parse_bool_env("TROLLSONA_SKIP_UI_BUILD", default=False) else build_demo()
|
| 845 |
+
|
| 846 |
+
|
| 847 |
+
if __name__ == "__main__":
|
| 848 |
+
(demo or build_demo()).launch()
|
assets/style.css
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--ink: #130f0c;
|
| 3 |
+
--coal: #1d1712;
|
| 4 |
+
--coal-soft: #2b2118;
|
| 5 |
+
--parchment: #efe0bf;
|
| 6 |
+
--paper: #f8edcf;
|
| 7 |
+
--paper-deep: #d7bd86;
|
| 8 |
+
--brass: #c68633;
|
| 9 |
+
--ember: #b44a24;
|
| 10 |
+
--olive: #55633f;
|
| 11 |
+
--bone: #fff7df;
|
| 12 |
+
--muted: #bda985;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
body,
|
| 16 |
+
.gradio-container {
|
| 17 |
+
background: var(--ink) !important;
|
| 18 |
+
color: var(--bone) !important;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.gradio-container {
|
| 22 |
+
width: min(100%, 1180px) !important;
|
| 23 |
+
max-width: 1180px !important;
|
| 24 |
+
min-height: 100vh;
|
| 25 |
+
margin: 0 auto !important;
|
| 26 |
+
padding: 24px !important;
|
| 27 |
+
--body-background-fill: var(--ink);
|
| 28 |
+
--body-text-color: var(--bone);
|
| 29 |
+
--background-fill-primary: var(--coal);
|
| 30 |
+
--background-fill-secondary: var(--coal-soft);
|
| 31 |
+
--border-color-primary: rgba(215, 189, 134, 0.34);
|
| 32 |
+
--block-background-fill: var(--coal-soft);
|
| 33 |
+
--block-border-color: rgba(215, 189, 134, 0.34);
|
| 34 |
+
--block-label-background-fill: transparent;
|
| 35 |
+
--block-label-border-color: transparent;
|
| 36 |
+
--block-label-text-color: var(--paper);
|
| 37 |
+
--block-info-text-color: var(--muted);
|
| 38 |
+
--input-background-fill: #251d15;
|
| 39 |
+
--input-border-color: rgba(215, 189, 134, 0.48);
|
| 40 |
+
--input-placeholder-color: rgba(255, 247, 223, 0.52);
|
| 41 |
+
--body-text-color-subdued: var(--muted);
|
| 42 |
+
--button-primary-background-fill: var(--ember);
|
| 43 |
+
--button-primary-background-fill-hover: #c9572b;
|
| 44 |
+
--button-primary-text-color: var(--bone);
|
| 45 |
+
--button-secondary-background-fill: var(--coal-soft);
|
| 46 |
+
--button-secondary-background-fill-hover: #35291e;
|
| 47 |
+
--button-secondary-text-color: var(--paper);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.ritual-hero {
|
| 51 |
+
max-width: 100%;
|
| 52 |
+
border: 1px solid rgba(198, 134, 51, 0.45);
|
| 53 |
+
border-radius: 8px;
|
| 54 |
+
background:
|
| 55 |
+
linear-gradient(135deg, rgba(29, 23, 18, 0.98), rgba(43, 33, 24, 0.98)),
|
| 56 |
+
repeating-linear-gradient(90deg, rgba(255, 247, 223, 0.04) 0, rgba(255, 247, 223, 0.04) 1px, transparent 1px, transparent 9px);
|
| 57 |
+
padding: 28px;
|
| 58 |
+
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.28);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.hero-mark,
|
| 62 |
+
.dossier-kicker,
|
| 63 |
+
.panel-heading,
|
| 64 |
+
.trollsona-label {
|
| 65 |
+
color: var(--brass);
|
| 66 |
+
font-size: 0.78rem;
|
| 67 |
+
font-weight: 800;
|
| 68 |
+
letter-spacing: 0;
|
| 69 |
+
text-transform: uppercase;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.ritual-hero h1 {
|
| 73 |
+
color: var(--bone);
|
| 74 |
+
font-size: 3rem;
|
| 75 |
+
line-height: 1;
|
| 76 |
+
margin: 10px 0 8px;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.ritual-hero p {
|
| 80 |
+
color: var(--paper);
|
| 81 |
+
font-size: 1.12rem;
|
| 82 |
+
line-height: 1.45;
|
| 83 |
+
margin: 0;
|
| 84 |
+
max-width: 620px;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.badge-row {
|
| 88 |
+
display: flex;
|
| 89 |
+
flex-wrap: wrap;
|
| 90 |
+
gap: 8px;
|
| 91 |
+
margin-top: 18px;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.badge-row span {
|
| 95 |
+
border: 1px solid rgba(198, 134, 51, 0.48);
|
| 96 |
+
border-radius: 999px;
|
| 97 |
+
background: rgba(85, 99, 63, 0.28);
|
| 98 |
+
color: var(--paper);
|
| 99 |
+
padding: 6px 10px;
|
| 100 |
+
font-size: 0.82rem;
|
| 101 |
+
line-height: 1;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.ritual-layout {
|
| 105 |
+
display: grid !important;
|
| 106 |
+
grid-template-columns: minmax(360px, 0.92fr) minmax(420px, 1.08fr);
|
| 107 |
+
align-items: stretch;
|
| 108 |
+
gap: 20px;
|
| 109 |
+
margin: 20px auto 0;
|
| 110 |
+
width: 100%;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.summoning-panel,
|
| 114 |
+
.dossier-stage {
|
| 115 |
+
border: 1px solid rgba(215, 189, 134, 0.36);
|
| 116 |
+
border-radius: 8px;
|
| 117 |
+
background: rgba(29, 23, 18, 0.94);
|
| 118 |
+
min-width: 0;
|
| 119 |
+
padding: 16px;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.summoning-panel {
|
| 123 |
+
box-shadow: inset 0 0 0 1px rgba(255, 247, 223, 0.04);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.panel-heading {
|
| 127 |
+
margin: 0 0 14px;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.summoning-panel > *,
|
| 131 |
+
.dossier-stage > * {
|
| 132 |
+
background: transparent !important;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.summoning-panel .block,
|
| 136 |
+
.summoning-panel .form,
|
| 137 |
+
.summoning-panel .gr-form,
|
| 138 |
+
.summoning-panel .wrap,
|
| 139 |
+
.summoning-panel .container,
|
| 140 |
+
.summoning-panel .input-container,
|
| 141 |
+
.summoning-panel .prose,
|
| 142 |
+
.dossier-stage .block,
|
| 143 |
+
.dossier-stage .form,
|
| 144 |
+
.dossier-stage .wrap,
|
| 145 |
+
.dossier-stage .container,
|
| 146 |
+
.dossier-stage .input-container {
|
| 147 |
+
border-color: rgba(215, 189, 134, 0.36) !important;
|
| 148 |
+
background: var(--coal-soft) !important;
|
| 149 |
+
color: var(--bone) !important;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.summoning-panel label,
|
| 153 |
+
.summoning-panel .label-wrap,
|
| 154 |
+
.summoning-panel [data-testid="block-label"],
|
| 155 |
+
.summoning-panel [data-testid="block-info"],
|
| 156 |
+
.summoning-panel span,
|
| 157 |
+
.dossier-stage label,
|
| 158 |
+
.dossier-stage .label-wrap,
|
| 159 |
+
.dossier-stage [data-testid="block-label"] {
|
| 160 |
+
color: var(--paper) !important;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.summoning-panel label,
|
| 164 |
+
.summoning-panel [data-testid="block-label"] {
|
| 165 |
+
font-weight: 700 !important;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.summoning-panel input,
|
| 169 |
+
.summoning-panel textarea,
|
| 170 |
+
.summoning-panel select,
|
| 171 |
+
.summoning-panel [role="listbox"],
|
| 172 |
+
.summoning-panel [role="combobox"],
|
| 173 |
+
.summoning-panel .dropdown,
|
| 174 |
+
.summoning-panel .wrap input,
|
| 175 |
+
.summoning-panel .wrap textarea {
|
| 176 |
+
border-color: rgba(215, 189, 134, 0.45) !important;
|
| 177 |
+
background: #251d15 !important;
|
| 178 |
+
color: var(--bone) !important;
|
| 179 |
+
box-shadow: none !important;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.summoning-panel input::placeholder,
|
| 183 |
+
.summoning-panel textarea::placeholder {
|
| 184 |
+
color: rgba(255, 247, 223, 0.48) !important;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.summoning-panel input:focus,
|
| 188 |
+
.summoning-panel textarea:focus,
|
| 189 |
+
.summoning-panel [role="listbox"]:focus,
|
| 190 |
+
.summoning-panel [role="combobox"]:focus {
|
| 191 |
+
border-color: var(--brass) !important;
|
| 192 |
+
outline: 2px solid rgba(198, 134, 51, 0.28) !important;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
.summoning-panel input[type="range"] {
|
| 196 |
+
accent-color: var(--ember);
|
| 197 |
+
background: transparent !important;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.summoning-panel input[type="number"] {
|
| 201 |
+
min-width: 62px;
|
| 202 |
+
text-align: center;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.summoning-panel input[type="checkbox"] {
|
| 206 |
+
accent-color: var(--ember);
|
| 207 |
+
border: 1px solid rgba(215, 189, 134, 0.55) !important;
|
| 208 |
+
background: #251d15 !important;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.summoning-panel input[type="checkbox"]:checked {
|
| 212 |
+
background: var(--ember) !important;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.summoning-panel button,
|
| 216 |
+
button.primary,
|
| 217 |
+
.gradio-container button.primary {
|
| 218 |
+
border: 1px solid rgba(255, 247, 223, 0.28) !important;
|
| 219 |
+
border-radius: 8px !important;
|
| 220 |
+
background: var(--ember) !important;
|
| 221 |
+
color: var(--bone) !important;
|
| 222 |
+
font-weight: 800 !important;
|
| 223 |
+
box-shadow: 0 10px 28px rgba(180, 74, 36, 0.22);
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.summoning-panel button:hover,
|
| 227 |
+
button.primary:hover,
|
| 228 |
+
.gradio-container button.primary:hover {
|
| 229 |
+
background: #c9572b !important;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.empty-dossier,
|
| 233 |
+
.trollsona-card {
|
| 234 |
+
border: 1px solid rgba(92, 65, 35, 0.45);
|
| 235 |
+
border-radius: 8px;
|
| 236 |
+
color: var(--coal);
|
| 237 |
+
min-height: 360px;
|
| 238 |
+
padding: 24px;
|
| 239 |
+
box-shadow: 0 22px 60px rgba(0, 0, 0, 0.32);
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.trollsona-card {
|
| 243 |
+
background:
|
| 244 |
+
linear-gradient(180deg, rgba(248, 237, 207, 0.98), rgba(239, 224, 191, 0.98)),
|
| 245 |
+
repeating-linear-gradient(0deg, rgba(92, 65, 35, 0.06) 0, rgba(92, 65, 35, 0.06) 1px, transparent 1px, transparent 10px);
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.empty-dossier {
|
| 249 |
+
display: flex;
|
| 250 |
+
flex-direction: column;
|
| 251 |
+
justify-content: center;
|
| 252 |
+
background:
|
| 253 |
+
linear-gradient(180deg, rgba(219, 194, 139, 0.98), rgba(190, 151, 84, 0.96)),
|
| 254 |
+
repeating-linear-gradient(0deg, rgba(43, 33, 24, 0.08) 0, rgba(43, 33, 24, 0.08) 1px, transparent 1px, transparent 11px);
|
| 255 |
+
box-shadow:
|
| 256 |
+
inset 0 0 0 1px rgba(255, 247, 223, 0.2),
|
| 257 |
+
0 22px 60px rgba(0, 0, 0, 0.32);
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.empty-dossier h2,
|
| 261 |
+
.trollsona-card h2 {
|
| 262 |
+
color: var(--coal);
|
| 263 |
+
font-size: 2rem;
|
| 264 |
+
line-height: 1.1;
|
| 265 |
+
margin: 10px 0 14px;
|
| 266 |
+
overflow-wrap: anywhere;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.empty-dossier p {
|
| 270 |
+
color: #281b12;
|
| 271 |
+
font-size: 1.03rem;
|
| 272 |
+
font-weight: 650;
|
| 273 |
+
line-height: 1.52;
|
| 274 |
+
margin: 0;
|
| 275 |
+
max-width: 360px;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.trollsona-mainline {
|
| 279 |
+
border-left: 4px solid var(--ember);
|
| 280 |
+
color: #35271b;
|
| 281 |
+
font-size: 1.08rem;
|
| 282 |
+
font-weight: 750;
|
| 283 |
+
line-height: 1.48;
|
| 284 |
+
margin: 0 0 18px;
|
| 285 |
+
padding-left: 14px;
|
| 286 |
+
overflow-wrap: anywhere;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.trollsona-grid {
|
| 290 |
+
display: grid;
|
| 291 |
+
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
|
| 292 |
+
gap: 12px;
|
| 293 |
+
margin-top: 16px;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
.trollsona-grid-single {
|
| 297 |
+
grid-template-columns: 1fr;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.trollsona-tile {
|
| 301 |
+
border: 1px solid rgba(92, 65, 35, 0.22);
|
| 302 |
+
border-radius: 8px;
|
| 303 |
+
background: rgba(255, 247, 223, 0.64);
|
| 304 |
+
padding: 14px;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
.trollsona-value {
|
| 308 |
+
color: #3f3020;
|
| 309 |
+
font-size: 1rem;
|
| 310 |
+
line-height: 1.42;
|
| 311 |
+
margin-top: 8px;
|
| 312 |
+
overflow-wrap: anywhere;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.meter-shell {
|
| 316 |
+
border: 1px solid rgba(92, 65, 35, 0.28);
|
| 317 |
+
border-radius: 999px;
|
| 318 |
+
background: rgba(29, 23, 18, 0.16);
|
| 319 |
+
height: 12px;
|
| 320 |
+
margin-top: 10px;
|
| 321 |
+
overflow: hidden;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.meter-fill {
|
| 325 |
+
background: linear-gradient(90deg, var(--olive), var(--brass), var(--ember));
|
| 326 |
+
height: 100%;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.gradio-container details,
|
| 330 |
+
.gradio-container .accordion,
|
| 331 |
+
.gradio-container button[aria-expanded],
|
| 332 |
+
.gradio-container button.label-wrap,
|
| 333 |
+
.dossier-stage button,
|
| 334 |
+
.gradio-container .secondary {
|
| 335 |
+
border-color: rgba(215, 189, 134, 0.32) !important;
|
| 336 |
+
background: rgba(29, 23, 18, 0.82) !important;
|
| 337 |
+
color: var(--paper) !important;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.gradio-container details summary,
|
| 341 |
+
.gradio-container button[aria-expanded] span,
|
| 342 |
+
.gradio-container button[aria-expanded],
|
| 343 |
+
.gradio-container button.label-wrap,
|
| 344 |
+
.gradio-container button.label-wrap *,
|
| 345 |
+
.gradio-container button.label-wrap span {
|
| 346 |
+
color: var(--paper) !important;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.gradio-container button.label-wrap svg,
|
| 350 |
+
.gradio-container button.label-wrap path {
|
| 351 |
+
color: var(--paper) !important;
|
| 352 |
+
fill: var(--paper) !important;
|
| 353 |
+
stroke: var(--paper) !important;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.preset-note {
|
| 357 |
+
color: var(--paper);
|
| 358 |
+
font-size: 0.94rem;
|
| 359 |
+
line-height: 1.35;
|
| 360 |
+
margin: 2px 0 12px;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.preset-row {
|
| 364 |
+
display: grid !important;
|
| 365 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 366 |
+
gap: 10px;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.preset-card,
|
| 370 |
+
.preset-card button,
|
| 371 |
+
.gradio-container .preset-card {
|
| 372 |
+
border: 1px solid rgba(215, 189, 134, 0.34) !important;
|
| 373 |
+
border-radius: 8px !important;
|
| 374 |
+
background:
|
| 375 |
+
linear-gradient(180deg, rgba(43, 33, 24, 0.96), rgba(29, 23, 18, 0.96)) !important;
|
| 376 |
+
color: var(--paper) !important;
|
| 377 |
+
font-weight: 800 !important;
|
| 378 |
+
line-height: 1.25 !important;
|
| 379 |
+
min-height: 78px !important;
|
| 380 |
+
text-align: left !important;
|
| 381 |
+
white-space: normal !important;
|
| 382 |
+
box-shadow: inset 0 0 0 1px rgba(255, 247, 223, 0.04) !important;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.preset-card:hover,
|
| 386 |
+
.preset-card button:hover,
|
| 387 |
+
.gradio-container .preset-card:hover {
|
| 388 |
+
border-color: rgba(198, 134, 51, 0.72) !important;
|
| 389 |
+
background:
|
| 390 |
+
linear-gradient(180deg, rgba(62, 45, 31, 0.98), rgba(37, 29, 21, 0.98)) !important;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
.gradio-container table,
|
| 394 |
+
.gradio-container th,
|
| 395 |
+
.gradio-container td {
|
| 396 |
+
border-color: rgba(215, 189, 134, 0.24) !important;
|
| 397 |
+
background: var(--coal-soft) !important;
|
| 398 |
+
color: var(--paper) !important;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.dossier-stage .prose,
|
| 402 |
+
.dossier-stage .prose p,
|
| 403 |
+
.dossier-stage .prose strong,
|
| 404 |
+
.dossier-stage .prose code {
|
| 405 |
+
color: var(--paper) !important;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
.dossier-stage .prose code {
|
| 409 |
+
border: 1px solid rgba(215, 189, 134, 0.26);
|
| 410 |
+
border-radius: 4px;
|
| 411 |
+
background: #251d15;
|
| 412 |
+
padding: 2px 5px;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.gradio-container footer {
|
| 416 |
+
display: none !important;
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
@media (max-width: 760px) {
|
| 420 |
+
.gradio-container {
|
| 421 |
+
padding: 14px !important;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.ritual-layout {
|
| 425 |
+
grid-template-columns: 1fr;
|
| 426 |
+
gap: 14px;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.preset-row {
|
| 430 |
+
grid-template-columns: 1fr;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.ritual-hero {
|
| 434 |
+
padding: 20px;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
.ritual-hero h1 {
|
| 438 |
+
font-size: 2.35rem;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
.empty-dossier,
|
| 442 |
+
.trollsona-card {
|
| 443 |
+
min-height: 300px;
|
| 444 |
+
padding: 18px;
|
| 445 |
+
}
|
| 446 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.44,<6
|
| 2 |
+
transformers>=4.44,<5
|
| 3 |
+
torch>=2.2
|
| 4 |
+
accelerate>=0.33
|