NOTE: This repository likely contains security issues. It was never launched into production and I was far more focused with building, there are areas that should be assessed if you are consdiering moving it into produciton, such as: markdown report generation / downloads, SQLi, celery tasks, XSS, Authorization logic, and validating the pricing methodology for tokens is accurate. Other issues may include prompt injection and a lack of controls on model output, which could be harmful or potentially cost you a lot of money!
This project was one of my first attempts at learning how to integrate AI into web applications and taking a crack at prompt engineering. It's pretty basic, but it was fun to build. No license, if you find it useful in part or in full feel free to use it. It isn't perfect, it could use some tighter engineering. For a minute I considered launching it and improving it, which is why it has complete systems for pricing, but hey, I'm bored of it now.
This repository contains a Flask application that generates long‑form fiction by moving through a structured pipeline: metadata → outlines → arcs → chapter guides → prose → images. It combines Flask + SQLAlchemy + Celery + Redis + Socket.IO on the backend with OpenAI models for text and image generation, and stores assets in S3‑compatible object storage.
Users create a Story (title, details, tags, inspirations, writing_style, chapters_count). This yields empty Chapter rows and initializes the authoring flow (see models/Story.py, views/story.py).
- Prompt builder:
build_meta_prompt(...)inprompt_templates.pywraps inputs in XML‑like scaffolding and asks for strict JSON with two arrays:charactersandlocations(each character includesname,description, andexample_dialogue). - Generation:
generate_meta_from_prompt(...)inopenai_handler.pycallsgpt-4o-mini. - Persistence: Results are parsed into
CharacterandLocationrows; aGenerationLogentry captures predicted vs. real cost + tokens. - Why this helps coherence: consistent entity memory (names, voices, example dialogue) and setting memory (location descriptions) are fed into every later prompt.
- Prompt builder:
build_chapter_summaries_prompt(...)requests a JSON array of chapters (title,summary) using the metadata above, overall arcs (if any), and targettotal_chapters. - Generation:
generate_chapter_summaries_from_prompt(...)with modelo1-mini. - Persistence: Populates
Chapter.titleandChapter.summaryacross the book. - Why this helps coherence: forces a top‑down outline so later steps keep consistent pacing and plot logic.
- Prompt builder:
build_story_arcs_prompt(...)converts chapter list + summaries + metadata into high‑level arcs for the whole book. - Generation:
generate_story_arcs_from_prompt(...)witho1-mini. - Persistence: Saves
StoryArcrows (arc_text,arc_order), giving you a spine that the chapter guides can reference. - Why this helps coherence: injects an across‑chapters throughline that keeps stakes and themes progressing.
- Prompt builder:
build_chapter_guide_prompt(...)expands each chapter into a list of arc parts (beats). Each part can be annotated with characters and locations used. - Generation:
generate_chapter_guide_from_prompt(...)witho1-mini. - Persistence:
ChapterGuiderows (story_id,chapter_title,part_index,part_text,characters,locations). - Why this helps coherence: gives the prose step a scene‑level plan, preventing meandering chapters.
- Prompt builder:
build_chapter_content_prompt(...)includes:- Chapter title and current chapter summary
- Previous and next chapter summaries (a small sliding window for continuity)
- Global details/tags/inspirations/writing_style
- Character and location mappings (including example dialogue snippets)
- The chapter’s arc parts from the guide
- Generation:
generate_chapter_content_from_prompt(...)witho1-mini; returns Markdown chapter text. - Why this helps coherence: the prose always sees what came before/after and stays faithful to the character voices and the beat sheet.
- Generation:
generate_image_from_prompt(...)usesdall-e-3and stores keys in S3 (seehelpers.get_image_url). - Why this helps coherence: chapter prompts can echo the scene/characters in the guide so visuals reflect the same canon.
- Background work: Celery tasks in
tasks.pydo the heavy lifting so the web thread stays responsive. - WebSocket events: Flask‑Socket.IO emits updates like
notification, generation_error, meta_generated, summaries_generated, arcs_generated, chapter_guide_generated, chapter_generated, image_generatedso the UI can show progress/errors live. - Single in‑flight task per user: a Redis lock (
generation_lock:<user_id>) prevents clobbering when a user clicks generate multiple times (seeapi/generation.py).
- Structured I/O: Prompts ask for strict JSON (for metadata, summaries, guides) and use XML‑like wrappers. This reduces parsing errors and keeps later steps machine‑readable.
- Entity memory: Characters include example_dialogue, and locations include rich descriptions. These are merged into chapter prompts to stabilize voice and setting.
- Hierarchical planning: The system moves from global → local: summaries → arcs → per‑chapter beat sheets → prose.
- Local continuity window: Chapter generation includes the previous/next summaries to smooth transitions and avoid plot amnesia.
- Beat‑driven drafting: The
ChapterGuidebreaks a chapter into parts with explicit characters/locations, which discourages repetition and keeps scenes purposeful.
The app uses a credit abstraction for text and images, backed by per‑million token prices and action‑specific modifiers stored in the database.
TokenCostConfig(one row):- Fields: cost_per_credit, cost_per_1m_input, cost_per_1m_output, o1_cost_per_credit, o1_cost_per_1m_input, o1_cost_per_1m_output, dall_e_price_per_image
- These determine how many tokens each credit buys for input vs output tokens for standard text models and for the
o1-minifamily, plus the price perdall-e-3image.
CreditConfig(per action):- Defaults (from
app.py):image(image): modifier = 50meta_input(text): modifier = 50meta_output(text): modifier = 50summary_input(text): modifier = 2summary_output(text): modifier = 2arcs_input(text): modifier = 2arcs_output(text): modifier = 2chapter_guide_input(text): modifier = 2chapter_guide_output(text): modifier = 2chapter_input(text): modifier = 2chapter_output(text): modifier = 2
- Defaults (from
For each generation step the app:
- Estimates input tokens using
tiktokenon the composed prompt. - Uses
TokenCostConfigto compute:input_tokens_per_credit = (credit_cost_dollar * 1_000_000) / cost_per_1m_inputoutput_tokens_per_credit = (credit_cost_dollar * 1_000_000) / cost_per_1m_output
- Converts tokens → base credits (rounding and minimum 1):
base_credit_cost_input = max(1, round(input_tokens / input_tokens_per_credit))base_credit_cost_output = max(1, round(predicted_output_tokens / output_tokens_per_credit))
- Applies
CreditConfig.modifierfor the step (e.g.summary_input,chapter_output) to get modified costs. - Predicted cost is shown before enqueueing work; after completion, the app recomputes actual cost using the real output tokens and charges the user (
helpers.spend_credits).
Predicted output tokens used by default:
- meta: 200
- summaries: 250
- story_arcs: 250
- chapter_guide: 250
- chapter: 300
Tracking: Every task writes a GenerationLog with predicted_cost, real_cost, input_tokens, output_tokens, model, and status.
- Framework: Flask (
app.py,views/,api/) - DB: SQLAlchemy models (
models/) incl.Story,Chapter,Character,Location,StoryArc,ChapterGuide,GenerationLog,TokenCostConfig,CreditConfig - Background: Celery (
tasks.py) + Redis broker/result backend - Realtime: Flask‑Socket.IO events to user‑scoped rooms
- AI:
openai_handler.py(text viao1-minifamily, images viadall-e-3) - Prompts:
prompt_templates.py(XML‑style wrappers; JSON outputs) - Assets: S3‑compatible storage via
helpers.get_image_url() - Payments/roles:
Role,CreditPackage, and Stripe keys inconfig.py
You’ll need: Python 3.10+, Redis, and environment variables for Stripe, OpenAI, and S3‑compatible object storage.
- Install dependencies
python3 -m venv novelapp
source novelapp/bin/activate
pip install -r requirements.txt- Configure
config.py
Set secrets/keys and infra URLs (OpenAI, Stripe, Redis, S3). Example fields:
ADMIN_USERNAME = "admin"
ADMIN_PASSWORD = "SET_SECURE_PASSWORD_HERE"
SECRET_KEY = "FLASK_SECRET_KEY"
SQLALCHEMY_DATABASE_URI = "sqlite:///novelapp.db"
SQLALCHEMY_TRACK_MODIFICATIONS = False
JWT_SECRET_KEY = "JWT_SECRET_KEY"
JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=2)
CELERY_BROKER_URL = "redis://localhost:6379/0"
CELERY_RESULT_BACKEND = "redis://localhost:6379/0"
REGISTRATION_DISABLED = False
MAINTENANCE_MODE = False
# Stripe (dev/prod blocks provided in config.py)
# OpenAI
OPENAI_API_KEY = "OPEN_API_KEY"
# S3 / compatible object storage
S3_IMAGE_BUCKET = "BUCKET_NAME"
S3_ENDPOINT = "BUCKET_URL"
S3_REGION = "BUCKET_REGION"
S3_IMAGE_ACCESS_KEY_ID = "BUCKET_ACCESS_KEY_ID"
S3_IMAGE_SECRET_KEY = "BUCKET_SECRET_KEY"- Run the web app
env=development python app.py- Run the worker
celery -A tasks.celery_app worker --loglevel=info- Verify Redis
redis-cli ping # -> PONG- Login
Open http://localhost:5000 and sign in with the admin credentials you set inconfig.py.
- The app seeds
CreditConfigdefaults and a minimal role set on first launch. - User credit checks happen before queueing tasks; actual spend occurs after generation using measured tokens.
- To prevent duplicate tasks, a Redis lock is set per user while a generation is in progress.
- Socket.IO events (e.g.,
"meta_generated","summaries_generated","chapter_generated") let the UI update in real time.
navigate to http://localhost:5000 and login with creds from `app.py`