A little RAG + GPT-2 project that writes original poems, using the Gutenberg Poetry Corpus as inspiration. Give it a topic, it digs up a stylistically similar passage from thousands of chunks of old public-domain verse, and uses that as a style primer for a fine-tuned GPT-2 model to riff on.
It's not going to win any poetry prizes, and it definitely won't always make sense — but on a good run it produces something genuinely poem-shaped, in an old, slightly melancholic, 19th-century voice.
- Retrieval — your prompt gets embedded (TF-IDF → SVD) and compared against a corpus of ~thousands of poetry chunks pulled from Project Gutenberg. The closest match gets pulled out as a style reference.
- Generation — a couple of clean lines from that reference get used to "prime"
andreipb/gpt2-poetry-model-crpo, followed by your topic. The model then just continues in that style. - Cleanup — this is the part that does most of the actual work. The model isn't instruction-tuned, so it can't just be told not to copy its source or ramble into prose — instead there's a chunk of post-processing that strips leaked training tokens, drops lines that are copied (even partially) from the reference, trims truncated fragments, and gets rid of stray punctuation-only lines.
If you're curious how the sausage is made, it's all in poetry_engine.py.
uv syncYou'll need the corpus and the retrieval models built once before anything will run:
uv run download_data.py # pulls + cleans the Gutenberg Poetry Corpus -> data/gutenberg.csv
uv run train.py # fits TF-IDF + SVD -> models/tfidf.pkl, models/svd.pklThis can take a little while the first time (it's downloading a decent-sized dataset from Hugging Face).
Three ways to use it, depending on what you're doing:
Quick CLI test
uv run generate.pyWeb UI
uv run streamlit run app.pyAPI
uv run uvicorn api:app --reload→ http://localhost:8000/docs for the interactive Swagger UI, or:
curl -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \
-d '{"prompt": "a lonely moon over the ocean"}'If you edit
poetry_engine.pywhile the Streamlit app or API is already running, you need to fully restart the process (not just save the file). It's an already-imported module, so a hot reload ofapp.pywon't re-run its top-level code.
.
├── data/
│ └── gutenberg.csv # cleaned poetry corpus (generated)
├── models/
│ ├── tfidf.pkl # generated by train.py
│ └── svd.pkl # generated by train.py
├── download_data.py # pulls + cleans the raw Gutenberg corpus
├── train.py # fits the TF-IDF/SVD retrieval models
├── poetry_engine.py # retrieval + generation + cleanup logic
├── generate.py # CLI entry point
├── app.py # Streamlit web UI
└── api.py # FastAPI REST API
- The base model is a plain fine-tuned GPT-2, not an instruction-following model, so quality is inherently inconsistent — some prompts land, some come back stilted or half-finished.
- Style skews heavily toward 19th-century / classical diction, since that's mostly what's in the Gutenberg corpus.
- Retrieval is TF-IDF based, so it matches on vocabulary overlap rather than deeper semantic meaning — an odd or very modern prompt may pull an unrelated reference passage.
- No persistence — nothing you generate gets saved anywhere unless you add that yourself.
Python, scikit-learn (TF-IDF + SVD), Hugging Face Transformers, Streamlit, FastAPI, pandas.
- Corpus: biglam/gutenberg-poetry-corpus on Hugging Face
- Generator model: andreipb/gpt2-poetry-model-crpo