- Each chapter card now has a footer with Prev/Next chapter anchor links
- First chapter shows only Next; last chapter shows 'End of Book'
- Back to Top link on every chapter footer
- Added get_chapter_neighbours() helper in story/bible_tracker.py for
programmatic chapter sequence navigation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Guard checks at task start verify: run exists in DB, project folder exists,
bible.json exists and is parseable, and bible has at least one book
- Any failed check marks the run as 'failed' and returns early, preventing
jobs from writing to the wrong book or orphaned project directories
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added tags VARCHAR(300) column to Run model
- Added startup ALTER TABLE migration in app.py
- New POST /run/<id>/set_tags route saves comma-separated tags
- Tag badges + collapsible edit form in run_details.html header area
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New POST /run/<id>/delete route removes run from DB and deletes run directory
- Only allows deletion of non-active runs (blocks running/queued)
- Delete Run button shown in run_details.html header for non-active runs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New GET /run/<id>/download_bible route serves project bible.json as attachment
- Download Bible button added to run_details.html header toolbar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove ai_blueprint.md from git tracking (already gitignored)
- web/app.py: Unify startup reset — all non-terminal states (running,
queued, interrupted) are reset to 'failed' with per-job logging
- web/routes/project.py: Add active_runs list to view_project() context
- templates/project.html: Add Active Jobs card showing all running/queued
jobs with status badge, start time, progress bar, and View Details link;
Generate button and Stop buttons now driven by active_runs list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- web/db.py: Add last_heartbeat column to Run model
- core/utils.py: Add set_heartbeat_callback() and send_heartbeat()
- web/tasks.py: Add _robust_update_run_status() with 5-retry exponential backoff;
add db_heartbeat_callback(); remove all bare except:pass on DB status updates;
set start_time + last_heartbeat when marking run as 'running'
- web/app.py: Add last_heartbeat column migration; add _stale_job_watcher()
background thread (checks every 5 min, 15-min heartbeat threshold, 2-hr start_time threshold)
- cli/engine.py: Add phase-level logging banners and try/except wrappers in
process_book(); add utils.send_heartbeat() after each chapter save;
add start/finish logging in run_generation()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend (web/routes/run.py): Extended /run/<id>/status JSON response with
server_timestamp, db_log_count, and latest_log_timestamp so clients can
detect whether the DB is being written to independently of the log text.
- Frontend (templates/run_details.html):
• Added Live Status Panel above the System Log card, showing:
- Polling state badge (Initializing / Requesting / Waiting Ns / Error / Idle)
- Last Successful Update timestamp (HH:MM:SS, updated every successful poll)
- DB diagnostics (log count + latest log timestamp from server response)
- Last Error message displayed inline when a poll fails
- Force Refresh button to immediately trigger a new poll
• Refactored JS polling loop: countdown timer with clearCountdown/
startWaitCountdown helpers, forceRefresh() clears pending timers before
re-polling, explicit pollTimer/countdownInterval tracking.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- web/tasks.py: db_log_callback now writes non-OperationalError exceptions to data/app.log for visibility
- web/tasks.py: generate_book_task restructured with try...finally to guarantee final status update — run can never be left in 'running' state if worker crashes
- templates/project.html: added .catch() to fetchLog() with console.error + polling resume on failure; added manual Refresh button to status bar
- templates/run_details.html: improved .catch() in updateLog() with descriptive message + 5s retry; added manual Refresh button to status bar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- web/tasks.py: db_log_callback bare `except: break` replaced with
explicit `except Exception as _e: print(...)` so insertion failures
are visible in Docker logs. Also fixed datetime.utcnow() → .isoformat()
for clean string storage in SQLite.
Same fix applied to db_progress_callback.
- web/routes/run.py (run_status): added db.session.expire_all() to
force fresh reads; raw sqlite3 bypass query when ORM returns no rows;
file fallback wrapped in try/except with stdout error reporting;
secondary check for web_console.log inside the run directory;
utf-8 encoding on all file opens.
- ai_blueprint.md: bumped to v2.11, documented root causes and fixes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docker-compose.yml:
- Add PYTHONIOENCODING=utf-8 env var (guarantees UTF-8 stdout in all
Python environments, including Docker slim images on ARM).
- Add logging driver section: json-file, max-size 10m, max-file 5.
Without this the json-file log on a Raspberry Pi SD card grows
unbounded and eventually kills the container or fills the disk.
web/requirements_web.txt:
- Pin huey==2.6.0 so a future pip upgrade cannot silently change the
Consumer() API and re-introduce the loglevel= TypeError that caused
all tasks to stay queued forever.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- web/app.py: Startup banner to docker logs (Python version, platform,
Huey version, DB paths). All print() calls now flush=True so Docker
captures them immediately. Emoji-free for robust stdout encoding.
Startup now detects orphaned queued runs (queue empty but DB queued)
and resets them to 'failed' so the UI does not stay stuck on reload.
Huey logging configured at INFO level so task pick-up/completion
appears in `docker logs`. Consumer skip reason logged explicitly.
- web/tasks.py: generate_book_task now emits [TASK run=N] lines to
stdout (docker logs) at pick-up, log-file creation, DB status update,
and on error (with full traceback) so failures are always visible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: Consumer(huey, workers=1, worker_type='thread', loglevel=20)
raised TypeError on every app start because Huey 2.6.0 does not accept
a `loglevel` keyword argument. The exception was silently caught and only
printed to stdout, so the consumer never ran and all tasks stayed 'queued'
forever — causing the 'Preparing environment / Waiting for logs' hang.
Fixes:
- web/app.py: Remove invalid `loglevel=20` from Consumer(); configure
Huey logging via logging.basicConfig(WARNING) instead. Add persistent
error logging to data/consumer_error.log for future diagnosis.
- core/config.py: Replace emoji print() calls with ASCII-safe equivalents
to prevent UnicodeEncodeError on Windows cp1252 terminals at import time.
- core/config.py: Update VERSION to 2.9 (was stale at 1.5.0).
- ai_blueprint.md: Bump to v2.10, document root cause and fixes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ai/setup.py: Added threading import; OAuth block now detects background/headless
threads and skips run_local_server to prevent indefinite blocking. Logs a clear
warning and falls back to ADC for Vertex AI. Token file only written when creds
are not None.
- web/tasks.py: All sqlite3.connect() calls now use timeout=30, check_same_thread=False.
OperationalError on the initial status update is caught and logged via utils.log.
generate_book_task now touches initial_log immediately so the UI polling endpoint
always finds an existing file even if the worker crashes on the next line.
- ai_blueprint.md: Bumped to v2.9; Section 12.D sub-items 1-3 marked ✅; item 13
added to summary.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added Section 12 to ai_blueprint.md covering:
- A: API timeout hangs (ai/models.py 180s, ai/setup.py 30s, removed cascading init call)
- B: Huey consumer never started under flask/gunicorn (module-level start + reloader guard)
- C: 'Create new book not showing anything' — 3 root causes fixed:
(4) Jinja2 UndefinedError on s.tropes|join in project_setup.html
(5) Silent redirect when model_logic=None now renders form with defaults
(6) planner.enrich() called with wrong bible structure in create_project_final
Bumped blueprint version from v2.7 → v2.8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. templates/project_setup.html: s.tropes|join and s.formatting_rules|join
raised Jinja2 UndefinedError when AI failed and fallback dict lacked those
keys → 500 blank page. Fixed with (s.tropes or [])|join(', ').
2. web/routes/project.py (project_setup_wizard): Removed silent redirect-to-
dashboard when model_logic is None. Now renders the setup form with a
complete default suggestions dict (all fields present, lists as []) plus a
clear warning flash so the user can fill it in manually.
3. web/routes/project.py (create_project_final): planner.enrich() was called
with the full bible dict — enrich() reads manual_instruction from the top
level (got 'A generic story' fallback) and wrote results into book_metadata
instead of the bible's books[0]. Fixed to build a proper per-book blueprint,
call enrich, and merge characters/plot_beats back into the correct locations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root causes of indefinite spinning during book create/generate:
1. ai/models.py — ResilientModel.generate_content() had no timeout: a
stalled Gemini API call would block the thread forever. Now injects
request_options={"timeout": 180} into every call. Also removed the
dangerous init_models(force=True) call inside the retry handler, which
was making a second network call during an existing API failure.
2. ai/setup.py — genai.list_models() calls in get_optimal_model(),
select_best_models(), and init_models() had no timeout. Added
request_options={"timeout": 30} to all three calls so model init
fails fast rather than hanging indefinitely.
3. web/app.py — Huey task consumer only started inside
`if __name__ == "__main__":`, meaning tasks queued via flask run,
gunicorn, or other WSGI runners were never executed (status stuck at
"queued" forever). Moved consumer start to module level with a
WERKZEUG_RUN_MAIN guard to prevent double-start under the reloader.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sections 1 (RAG for Lore/Locations) and 2 (Thread Tracking) still showed
⏳ despite being fully implemented under Sections 8 and 9 in v2.5.
Updated both to ✅ with accurate implementation notes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- story/planner.py: enrich() and plan_structure() now extract series_metadata
and inject a SERIES_CONTEXT block (Book X of Y in series Z, with position-aware
guidance) into prompts when is_series is true.
- story/writer.py: write_chapter() builds and injects the same SERIES_CONTEXT
into the chapter draft prompt; passes series_context to evaluate_chapter_quality().
- story/editor.py: evaluate_chapter_quality() accepts optional series_context
parameter and injects it into METADATA so arc pacing is evaluated relative to
the book's position in the series.
- ai_blueprint.md: Section 11 marked complete (v2.7), summary updated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- story/style_persona.py: Expanded default ai_isms list with 20+ modern AI phrases
(delved, mined, neon-lit, bustling, a wave of, etched in, etc.) and added
filter_words (wondered, seemed, appeared, watched, observed, sensed)
- story/editor.py: Stricter evaluate_chapter_quality rubric — added
DEEP_POV_ENFORCEMENT block with automatic fail conditions for filter word
density and summary mode; strengthened criterion 5 scoring thresholds
- story/writer.py: Added get_genre_instructions() helper with genre-specific
mandates for Thriller, Romance, Fantasy, Sci-Fi, Horror, Historical, and
General Fiction; added DEEP_POV_MANDATE block banning summary mode and
filter words; expanded AVOID AI-ISMS banned phrase list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Convert form POST to async fetch() in system_status.html
- Spinner + disabled button while request is in-flight
- Bootstrap toast notification on success/error
- Auto-reload page 1.5s after successful refresh
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implement Section 3 of the AI Context Optimization Blueprint: before each
chapter draft, model_logic expands sparse scene_beats into a structured
Director's Treatment covering staging, sensory anchors, emotional shifts,
and subtext per beat. This treatment is injected into the writer prompt,
giving the model a detailed scene blueprint to dramatize rather than infer,
reducing rewrite attempts and improving first-draft quality scores.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- writer.py: Dynamic character injection — only POV + beat-named characters
are sent to the writer prompt, eliminating token waste and hallucinations
from characters unrelated to the current scene.
- writer.py: Smart tail truncation — prev_content trimmed to last 1,000 tokens
(the actual chapter ending) instead of a blind 2,000-token head slice,
preserving the exact hand-off point for continuity.
- writer.py: Scene state injected into char_visuals — current_location,
time_of_day, and held_items now surfaced per relevant character in prompt.
- bible_tracker.py: update_tracking expanded to record current_location,
time_of_day, and held_items per character after each chapter.
- core/config.py: VERSION bumped 1.4.0 → 1.5.0.
- README.md: Story Generation section and tracking_characters.json schema
updated to document new context optimization features.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removed `from huey.contrib.mini import MiniHuey` which caused
`ModuleNotFoundError: No module named 'gevent'` on startup. MiniHuey
was never used; the app correctly uses SqliteHuey via `web.tasks`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
web/app.py was hardcoded to port 7070, causing Docker port forwarding
(5000:5000) and the Dockerfile HEALTHCHECK to fail. Changed to port 5000
to match docker-compose.yml and Dockerfile configuration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Appended entries from ai_blueprint.md guidelines to exclude AI planning
files, context indexes, and assistant directories from version control:
- ai_blueprint.md and plans/
- .claude/, .gemini/, .roo/, .cline/, .cursor/, .cascade/, .windsurfrules
- *.aiindex, ai_workspace_index.json
Also untracks the already-committed .claude/ and ai_blueprint.md files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Organic book quality:
- write_chapter: strip key_events spoilers from character context so the writer
doesn't know planned future events when writing early chapters
- write_chapter: added next_chapter_hint — seeds anticipation for the next scene
in the final paragraphs of each chapter for natural story flow
- write_chapter: added DIALOGUE VOICE instruction referencing CHARACTER TRACKING
speech styles so every character sounds distinctly different
- Lowered SCORE_AUTO_ACCEPT 9→8 to stop over-refining already-professional drafts
Speed improvements:
- check_pacing: reduced from every chapter to every other chapter (~50% fewer calls)
- refine_persona: reduced from every 3 to every 5 chapters (~40% fewer calls)
- Resume summary rebuild: uses first + last-4 chapters instead of all chapters
to avoid massive prompts when resuming mid-book
- Summary context sent to writer capped at 8000 chars (most-recent events)
- update_tracking text cap lowered 500000→20000 (covers any realistic chapter)
Logging and progress bars:
- Progress bar updates at chapter START, not just after completion
- Chapter banner logged before each write so the log shows which chapter is active
- Word count logged after first draft (e.g. "Draft: 2,341 words (target: ~2200)")
- Word count added to chapter completion TIMING line
- Pacing check now logs "Pacing OK" with reason when no intervention needed
- utils: added log_banner() helper for phase separator lines
UI:
- run_details.html: log lines are now phase-coloured (WRITER=cyan, ARCHITECT=green,
TIMING=gray, SYSTEM=yellow, TRACKER=purple, RESUME=orange, etc.)
- Status bar shows current active phase (e.g. "Status: Running — WRITER")
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
story.py — create_chapter_plan():
- TARGET_CHAPTERS is now a guideline (±15%) not a hard constraint; the AI
can produce a count that fits the story rather than forcing a specific number
- Word scaling is now pacing-aware instead of uniform: Very Fast ≈ 60% of avg,
Fast ≈ 80%, Standard ≈ 100%, Slow ≈ 125%, Very Slow ≈ 150%
- Two-pass normalisation: pacing weights applied first, then the total is
nudged to the word target — natural variation preserved throughout
- Variance range tightened to ±8% (was ±10%) for more predictable totals
- Prompt now tells the AI that estimated_words should reflect pacing rhythm
story.py — expand():
- Added event ceiling (target_chapters × 1.5): if the outline already has
enough beats, the pass switches from "add events" to "enrich descriptions"
— prevents over-dense outlines for short stories and flash fiction
- Task instruction is dynamically chosen: add-events vs deepen-descriptions
- Clarified that original user beats must be preserved but new events must
each be distinct and spread evenly (not front-loaded)
story.py — refinement loop:
- Word count constraint softened from hard "do not condense" to
"~N words ±20% acceptable if the scene demands it" so action chapters
can run short and introspective chapters can run long naturally
main.py — bridge chapter insertion:
- Removed hardcoded 1500-word estimate for dynamically inserted bridge
chapters; now computes the average estimated_words from the current
chapter plan so bridge chapters match the book's natural chapter length
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
story.py — write_chapter():
- Added POSITION context ("Chapter N of Total") so the AI calibrates narrative
tension correctly (setup vs escalation vs climax/payoff)
- Moved PACING_GUIDE to sit directly after PACING metadata instead of being
buried after 13 quality criteria items where the AI rarely reads it
- Removed duplicate pacing descriptions that appeared after QUALITY_CRITERIA
story.py — refinement loop:
- Capped critique history to last 2 entries (was accumulating all previous
attempts, wasting tokens and confusing the model on attempt 4-5)
- Added TARGET_WORDS and BEATS constraints to the refinement prompt to prevent
chapters from shrinking or losing plot beats during editing passes
- Restructured refinement prompt with explicit HARD_CONSTRAINTS section
story.py — check_and_propagate():
- Increased chapter context from 5000 to 12000 chars for continuity rewrites
(was asking for a full chapter rewrite but only providing a fragment)
- Added explicit word count target to rewrite so chapters are not truncated
- Added conservative decision bias: only rewrite on genuine contradictions
story.py — plan_structure():
- Now passes TARGET_CHAPTERS, TARGET_WORDS, GENRE, and CHARACTERS to the
structure AI — it was planning blindly without knowing the book's scale
marketing.py — generate_blurb():
- Rewrote prompt with 4-part structure: Hook → Stakes → Tension → Close
- Formats plot beats as a readable list instead of raw JSON array
- Extracts protagonist automatically for personalised blurb copy
- Added genre-tone matching, present-tense voice, and no-spoiler rule
marketing.py — generate_cover():
- Added genre-to-visual-style mapping (thriller → cinematic, fantasy → epic
digital painting, romance → painterly, etc.)
- Art prompt instructions now enforce: no text/letters/watermarks, rule-of-thirds
composition, explicit focal point, lighting description, colour palette
- Replaced generic image evaluation with a 5-criteria book-cover rubric:
visual impact, genre fit, composition, quality, and clean image (no text)
- Score penalties: -3 for visible text/watermarks, -2 for blur/deformed anatomy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Model selection (ai.py):
- get_optimal_model() now scores Gemini 2.5 > 2.0 > 1.5 when ranking candidates
- get_default_models() fallbacks updated to gemini-2.0-pro-exp (logic) and gemini-2.0-flash (writer/artist)
- AI selection prompt rewritten: includes Gemini 2.x pricing context, guidance to avoid 'thinking' models for writer/artist roles, and instructions to prefer 2.x over 1.5
- Added image_model_name and image_model_source globals for UI visibility
- init_models() now reads MODEL_IMAGE_HINT; tries imagen-3.0-generate-001 then imagen-3.0-fast-generate-001 on both Gemini API and Vertex AI paths
Cover generation (marketing.py):
- Fixed display bug: "Attempt X/5" now correctly reads "Attempt X/3"
- Added imagen-3.0-fast-generate-001 as intermediate fallback before legacy Imagen 2
- Quality threshold: images with score < 5 are only kept if nothing better exists
- Smarter prompt refinement on retry: deformity, blur, and watermark critique keywords each append targeted corrections to the art prompt
- Fixed missing sys import (sys.platform check for macOS was silently broken)
Config / Docker:
- config.py: added MODEL_IMAGE_HINT env var, bumped version to 1.2.0
- docker-compose.yml: added MODEL_IMAGE environment variable
- Dockerfile: added libpng-dev and libfreetype6-dev for better font/PNG rendering; added HEALTHCHECK so Portainer detects unhealthy containers
System status UI:
- system_status.html: added Image row showing active Imagen model and provider (Gemini API / Vertex AI)
- Added cache expiry countdown with colour-coded badges
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>