v2.0.0: Modularize project into single-responsibility packages

Replaced monolithic modules/ package with a clean architecture:

- core/       config.py, utils.py
- ai/         models.py (ResilientModel), setup.py (init_models)
- story/      planner.py, writer.py, editor.py, style_persona.py, bible_tracker.py
- marketing/  cover.py, blurb.py, fonts.py, assets.py
- export/     exporter.py
- web/        app.py (Flask factory), db.py, helpers.py, tasks.py, routes/{auth,project,run,persona,admin}.py
- cli/        engine.py (run_generation), wizard.py (BookWizard)

Flask routes split into 5 Blueprints; all templates updated with blueprint-
prefixed url_for() calls. Dockerfile and docker-compose updated to use
web.app entry point and new package paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 22:20:53 -05:00
parent edabc4d4fa
commit f7099cc3e4
52 changed files with 3984 additions and 3798 deletions

75
export/exporter.py Normal file
View File

@@ -0,0 +1,75 @@
import os
import markdown
from docx import Document
from ebooklib import epub
from core import utils
def create_readme(folder, bp):
meta = bp['book_metadata']
ls = bp['length_settings']
content = f"""# {meta['title']}\n**Generated by BookApp**\n\n## Stats Used\n- **Type:** {ls.get('label', 'Custom')}\n- **Planned Chapters:** {ls['chapters']}\n- **Logic Depth:** {ls['depth']}\n- **Target Words:** {ls.get('words', 'Unknown')}"""
with open(os.path.join(folder, "README.md"), "w") as f: f.write(content)
def compile_files(bp, ms, folder):
utils.log("SYSTEM", "Compiling EPUB and DOCX...")
meta = bp.get('book_metadata', {})
title = meta.get('title', 'Untitled')
if meta.get('filename'):
safe = meta['filename']
else:
safe = utils.sanitize_filename(title)
doc = Document(); doc.add_heading(title, 0)
book = epub.EpubBook(); book.set_title(title); spine = ['nav']
cover_path = os.path.join(folder, "cover.png")
if os.path.exists(cover_path):
with open(cover_path, 'rb') as f:
book.set_cover("cover.png", f.read())
ms.sort(key=utils.chapter_sort_key)
for c in ms:
num_str = str(c['num']).lower()
if num_str == '0' or 'prologue' in num_str:
filename = "prologue.xhtml"
default_header = f"Prologue: {c['title']}"
elif 'epilogue' in num_str:
filename = "epilogue.xhtml"
default_header = f"Epilogue: {c['title']}"
else:
filename = f"ch_{c['num']}.xhtml"
default_header = f"Ch {c['num']}: {c['title']}"
content = c['content'].strip()
clean_content = content.replace("```markdown", "").replace("```", "").strip()
lines = clean_content.split('\n')
ai_header = None
body_content = clean_content
if lines and lines[0].strip().startswith('# '):
ai_header = lines[0].strip().replace('#', '').strip()
header = ai_header
body_content = "\n".join(lines[1:]).strip()
else:
header = default_header
doc.add_heading(header, 1)
doc.add_paragraph(body_content)
ch = epub.EpubHtml(title=header, file_name=filename)
clean_content = clean_content.replace(f"{folder}\\", "").replace(f"{folder}/", "")
html_content = markdown.markdown(clean_content)
ch.content = html_content if ai_header else f"<h1>{header}</h1>{html_content}"
book.add_item(ch); spine.append(ch)
doc.save(os.path.join(folder, f"{safe}.docx"))
book.spine = spine; book.add_item(epub.EpubNcx()); book.add_item(epub.EpubNav())
epub.write_epub(os.path.join(folder, f"{safe}.epub"), book, {})
create_readme(folder, bp)