adding export checklists and script to do the export

This commit is contained in:
2026-03-12 10:54:19 -05:00
parent 87ede0d2c8
commit d633daa8e5
8 changed files with 590 additions and 5 deletions

1
.gitignore vendored
View File

@@ -22,3 +22,4 @@ exports/
# Claude Code
.claude/
.sneakycode/

View File

@@ -6,7 +6,7 @@
## 1. PROJECT OVERVIEW
**Series Title (working):** The Shade Series
**Author:** Phillip (writing as [pen name TBD])
**Author:** Phillip Tarrant
**Publishing Target:** Amazon Kindle Direct Publishing (KDP) — eBook + Print on Demand
**Enrollment:** Kindle Unlimited (KU) eligible — exclusive to Amazon
**Format:** Episodic fantasy series, 3+ books planned

446
export_kindle.py Normal file
View File

@@ -0,0 +1,446 @@
#!/usr/bin/env python3
"""
Export Book chapters to a Kindle-ready EPUB file.
Usage:
venv/bin/python export_kindle.py --book 1
# or: source venv/bin/activate && python export_kindle.py --book 1
Requires: pip install -r requirements.txt (ebooklib, markdown)
"""
import argparse
import re
import sys
from pathlib import Path
import markdown
from ebooklib import epub
ROOT = Path(__file__).parent
KINDLE_CSS = """\
body {
font-family: Georgia, "Times New Roman", serif;
font-size: 1em;
line-height: 1.6;
margin: 1em;
}
h1 {
font-size: 1.8em;
text-align: center;
margin-top: 2em;
margin-bottom: 1.5em;
page-break-before: always;
}
p {
margin: 0 0 0.8em 0;
text-indent: 1.5em;
}
p.first-para {
text-indent: 0;
}
p.scene-break {
text-align: center;
text-indent: 0;
margin: 1.5em 0;
letter-spacing: 0.5em;
font-size: 0.85em;
}
.title-page {
text-align: center;
margin-top: 4em;
margin-bottom: 4em;
}
.title-page h1 {
font-size: 2.2em;
margin-bottom: 0.3em;
page-break-before: avoid;
}
.title-page p {
text-indent: 0;
font-size: 1.1em;
font-style: italic;
}
em { font-style: italic; }
strong { font-weight: bold; }
"""
# ---------------------------------------------------------------------------
# Markdown → HTML conversion
# ---------------------------------------------------------------------------
def convert_md_to_html(text: str) -> str:
"""Convert markdown text to HTML with smart quotes and scene breaks."""
md = markdown.Markdown(extensions=["smarty"])
html = md.convert(text)
html = convert_scene_breaks(html)
html = inject_first_para_class(html)
return html
def convert_scene_breaks(html: str) -> str:
"""Replace bare '* * *' paragraphs with styled scene breaks."""
return html.replace("<p>* * *</p>", '<p class="scene-break">* * *</p>')
def inject_first_para_class(html: str) -> str:
"""Add class='first-para' to paragraphs following h1 and scene breaks."""
# After </h1>
html = re.sub(
r"(</h1>\s*)<p>",
r'\1<p class="first-para">',
html,
)
# After scene break
html = re.sub(
r'(<p class="scene-break">.*?</p>\s*)<p>',
r'\1<p class="first-para">',
html,
)
return html
def extract_title(text: str) -> str:
"""Extract the chapter title from the first # heading."""
match = re.match(r"^#\s+(.+)$", text.strip(), re.MULTILINE)
return match.group(1) if match else None
# ---------------------------------------------------------------------------
# Chapter discovery
# ---------------------------------------------------------------------------
def find_chapter_file(chapter_dir: Path, num: int) -> Path | None:
"""Find the best file for a chapter number (prefer final, fall back to draft)."""
for suffix in ("final", "draft"):
path = chapter_dir / f"ch{num:02d}-{suffix}.md"
if path.exists():
return path
return None
def find_special_chapter(chapter_dir: Path, name: str) -> Path | None:
"""Find prologue or epilogue (prefer final, fall back to draft)."""
for suffix in ("final", "draft"):
path = chapter_dir / f"{name}-{suffix}.md"
if path.exists():
return path
return None
def discover_chapters(chapter_dir: Path) -> list[tuple[str, Path]]:
"""Discover all chapter files in order: prologue, ch01..chNN, epilogue."""
chapters = []
# Prologue
prologue = find_special_chapter(chapter_dir, "prologue")
if prologue:
chapters.append(("prologue", prologue))
# Numbered chapters
nums = set()
for p in chapter_dir.glob("ch*-final.md"):
m = re.match(r"ch(\d+)", p.name)
if m:
nums.add(int(m.group(1)))
for p in chapter_dir.glob("ch*-draft.md"):
m = re.match(r"ch(\d+)", p.name)
if m:
nums.add(int(m.group(1)))
for num in sorted(nums):
path = find_chapter_file(chapter_dir, num)
if path:
chapters.append((f"ch{num:02d}", path))
# Epilogue
epilogue = find_special_chapter(chapter_dir, "epilogue")
if epilogue:
chapters.append(("epilogue", epilogue))
return chapters
# ---------------------------------------------------------------------------
# Template handling
# ---------------------------------------------------------------------------
def is_stub(text: str) -> bool:
"""Check if a template file is a stub (placeholder with no real content)."""
# Strip headings, HTML comments, and whitespace
content = re.sub(r"^#+\s+.*$", "", text, flags=re.MULTILINE)
content = re.sub(r"<!--.*?-->", "", content, flags=re.DOTALL)
content = content.strip()
return len(content.split()) < 50
def load_template(path: Path) -> str | None:
"""Load a template file, returning None if it doesn't exist or is a stub."""
if not path.exists():
return None
text = path.read_text(encoding="utf-8")
if is_stub(text):
print(f" Skipping stub template: {path.name}", file=sys.stderr)
return None
return text
# ---------------------------------------------------------------------------
# EPUB construction
# ---------------------------------------------------------------------------
def make_epub_chapter(filename: str, title: str, body_html: str) -> epub.EpubHtml:
"""Create an EPUB chapter item from HTML body content."""
chapter = epub.EpubHtml(
title=title,
file_name=filename,
lang="en",
)
chapter.set_content(body_html)
chapter.add_link(href="style/kindle.css", rel="stylesheet", type="text/css")
return chapter
def build_title_page(title: str, subtitle: str, author: str) -> epub.EpubHtml:
"""Generate a title page chapter."""
body = f"""\
<div class="title-page">
<h1>{title}</h1>
<p>{subtitle}</p>
<p>by {author}</p>
</div>"""
return make_epub_chapter("title.xhtml", "Title Page", body)
def build_epub(args) -> Path:
"""Build the EPUB file and return its path."""
book_num = args.book
title = args.title or "The Unbreakable Curse"
author = args.author or "Phillip Tarrant"
subtitle = f"Book {book_num} of The Shade Series"
chapter_dir = ROOT / "chapters" / f"book{book_num}"
if not chapter_dir.exists():
print(f"Error: Chapter directory not found: {chapter_dir}", file=sys.stderr)
sys.exit(1)
template_dir = ROOT / "kindle" / "templates"
export_dir = ROOT / "kindle" / "exports"
export_dir.mkdir(parents=True, exist_ok=True)
# --- Create the EPUB book ---
book = epub.EpubBook()
book.set_identifier(f"shade-series-book{book_num}")
book.set_title(f"{title}: {subtitle}")
book.set_language("en")
book.add_author(author)
# Cover image
if args.cover:
cover_path = Path(args.cover)
if cover_path.exists():
cover_data = cover_path.read_bytes()
ext = cover_path.suffix.lower()
media_type = {
".jpg": "image/jpeg", ".jpeg": "image/jpeg",
".png": "image/png",
}.get(ext, "image/jpeg")
book.set_cover("cover" + ext, cover_data)
print(f" Cover: {cover_path.name}")
else:
print(f" Warning: Cover image not found: {args.cover}", file=sys.stderr)
# CSS
css = epub.EpubItem(
uid="kindle_css",
file_name="style/kindle.css",
media_type="text/css",
content=KINDLE_CSS,
)
book.add_item(css)
spine = ["nav"]
toc = []
# --- Title page ---
title_page = build_title_page(title, subtitle, author)
book.add_item(title_page)
spine.append(title_page)
# --- Copyright page ---
copyright_md = load_template(template_dir / "copyright-page.md")
if copyright_md:
html = convert_md_to_html(copyright_md)
ch = make_epub_chapter("copyright.xhtml", "Copyright", html)
book.add_item(ch)
spine.append(ch)
# --- Front matter ---
front_md = load_template(template_dir / "front-matter.md")
if front_md:
html = convert_md_to_html(front_md)
ch = make_epub_chapter("front-matter.xhtml", "Front Matter", html)
book.add_item(ch)
spine.append(ch)
# --- Chapters ---
chapters = discover_chapters(chapter_dir)
if not chapters:
print("Error: No chapters found.", file=sys.stderr)
sys.exit(1)
print(f" Found {len(chapters)} chapter(s)")
for slug, path in chapters:
text = path.read_text(encoding="utf-8")
ch_title = extract_title(text) or slug.title()
html = convert_md_to_html(text)
filename = f"{slug}.xhtml"
ch = make_epub_chapter(filename, ch_title, html)
book.add_item(ch)
spine.append(ch)
toc.append(ch)
print(f" Added: {path.name}{ch_title}")
# --- Back matter templates ---
for template_name, epub_title in [
("back-matter.md", "Back Matter"),
("also-by.md", "Also by the Author"),
]:
back_md = load_template(template_dir / template_name)
if back_md:
html = convert_md_to_html(back_md)
fname = template_name.replace(".md", ".xhtml")
ch = make_epub_chapter(fname, epub_title, html)
book.add_item(ch)
spine.append(ch)
# --- Finalize ---
book.toc = toc
book.spine = spine
book.add_item(epub.EpubNcx())
book.add_item(epub.EpubNav())
# Output
if args.output:
out_path = Path(args.output)
else:
out_path = export_dir / f"book{book_num}-kindle.epub"
epub.write_epub(str(out_path), book, {})
return out_path
# ---------------------------------------------------------------------------
# Checklist generation
# ---------------------------------------------------------------------------
def generate_checklist(book_num: int, epub_path: Path) -> Path:
"""Write a publishing checklist to notes/kindle-publish-checklist.md."""
notes_dir = ROOT / "notes"
notes_dir.mkdir(parents=True, exist_ok=True)
checklist_path = notes_dir / "kindle-publish-checklist.md"
checklist = f"""\
# Kindle Publishing Checklist — Book {book_num}
Generated from `export_kindle.py`. Review and complete before uploading.
EPUB: `{epub_path.relative_to(ROOT)}`
---
## Front & Back Matter
- [ ] Copyright page — fill in year, pen name, legal text (`kindle/templates/copyright-page.md`)
- [ ] Dedication (optional) — add to front matter if desired
- [ ] Author\u2019s Note (optional) — add to back matter if desired
- [ ] \u201cAlso by the Author\u201d page — create `kindle/templates/also-by.md` when applicable
- [ ] Next book preview — append preview chapter to back matter when available
## Metadata
- [ ] Confirm pen name / author name
- [ ] Confirm book title and subtitle
- [ ] Write book description / blurb (for KDP listing)
- [ ] Choose KDP categories (2 allowed)
- [ ] Select 7 keywords for KDP search
- [ ] Set price (consider KU 70% royalty tier: $2.99\u2013$9.99)
## Cover Image
- [ ] Cover image ready (2560\u00d71600px recommended for Kindle)
- [ ] Cover passes KDP image quality checks
- [ ] Run `export_kindle.py --cover path/to/cover.jpg` to embed cover in EPUB
## Quality Checks
- [ ] Open EPUB in Kindle Previewer \u2014 check rendering on multiple devices
- [ ] Verify Table of Contents links work
- [ ] Verify chapter breaks (each chapter starts on new page)
- [ ] Verify scene breaks (`* * *`) are centered and styled
- [ ] Check smart quotes, em dashes, ellipses throughout
- [ ] Check first paragraphs are not indented after headings/scene breaks
- [ ] Proofread front and back matter
- [ ] Confirm no `[CONTINUITY FLAG]` markers remain in text
## Upload
- [ ] Log in to KDP (kdp.amazon.com)
- [ ] Create new Kindle eBook
- [ ] Upload EPUB manuscript
- [ ] Upload cover image
- [ ] Fill in metadata (title, description, categories, keywords)
- [ ] Set pricing and royalty
- [ ] Enroll in Kindle Unlimited (KDP Select)
- [ ] Preview and publish
"""
checklist_path.write_text(checklist, encoding="utf-8")
return checklist_path
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Export chapter markdown to a Kindle-ready EPUB file.",
)
parser.add_argument(
"--book", type=int, default=1,
help="Book number (default: 1)",
)
parser.add_argument(
"--output", "-o",
help="Output EPUB path (default: kindle/exports/bookN-kindle.epub)",
)
parser.add_argument(
"--title",
help="Book title (default: The Unbreakable Curse)",
)
parser.add_argument(
"--author",
help="Author name (default: [Author Name])",
)
parser.add_argument(
"--cover",
help="Path to cover image (JPG or PNG)",
)
args = parser.parse_args()
print(f"Building EPUB for Book {args.book}...")
epub_path = build_epub(args)
print(f"\nEPUB written: {epub_path}")
checklist_path = generate_checklist(args.book, epub_path)
print(f"Checklist written: {checklist_path}")
if __name__ == "__main__":
main()

View File

@@ -1,3 +1,11 @@
# Copyright Page Template
# Copyright
Standard copyright page for KDP publication.
Copyright © 2026 by Phillip Tarrant. All rights reserved.
No part of this publication may be reproduced, distributed, or transmitted in any form or by any means, including photocopying, recording, or other electronic or mechanical methods, without the prior written permission of the author, except in the case of brief quotations embodied in critical reviews and certain other noncommercial uses permitted by copyright law.
This is a work of fiction. Names, characters, places, and incidents either are the product of the author's imagination or are used fictitiously. Any resemblance to actual persons, living or dead, events, or locales is entirely coincidental.
<!-- TODO: Add ISBN if applicable -->
<!-- TODO: Add edition info (First Edition, [Month] [Year]) -->
<!-- TODO: Add cover art credit if applicable -->

View File

@@ -1,3 +1,3 @@
# Front Matter Template
# Dedication
Title page and front matter for KDP publication.
*For my family — who navigated my lack of emotion, my autistic brain, and my ADD with more grace and patience than I deserved. You believed in me when I couldn't. This one's yours.*

View File

@@ -0,0 +1,76 @@
# Kindle Publishing Checklist — Book 1
Generated from `export_kindle.py`. Review and complete before uploading.
EPUB: `kindle/exports/book1-kindle.epub`
---
## Front & Back Matter
- [X] Copyright page — fill in year, pen name, legal text (`kindle/templates/copyright-page.md`)
- [X] Dedication (optional) — add to front matter if desired
- [-] Authors Note (optional) — add to back matter if desired
- [-] “Also by the Author” page — create `kindle/templates/also-by.md` when applicable
- [-] Next book preview — append preview chapter to back matter when available
## Metadata
- [X] Confirm pen name / author name — **Phillip Tarrant**
- [X] Confirm book title and subtitle — **The Unbreakable Curse: The Shade Series, Book One**
- [X] Write book description / blurb (for KDP listing) — see below
- [X] Choose KDP categories (2 allowed) — Fantasy > Humorous; Fantasy > Mystery & Detective
- [X] Select 7 keywords for KDP search — see below
- [X] Set price (consider KU 70% royalty tier: $2.99$9.99) — **$2.99** (launch price, raise later)
### Book Description
Phelan Varrant sees what nobody else can — the flaws in magic that everyone else calls perfect.
Broke, antisocial, and operating out of a rented shack on Drenwick's dockside, Phelan makes his living through the Guild of Necessary Services, solving problems other guilds won't touch. When a desperate client arrives with a dying family member cursed by a working that two registered curse-breakers have already declared unbreakable, Phelan takes the case. The fee is significant. The problem is interesting. In that order.
The curse is named unbreakable by the Arcane Compact, but the curse isn't what it seems.
To save a man he barely knows, Phelan will have to do the one thing he's worst at: trust other people.
Phelan has a gift for hacking magic and seeing flaws. Can he find a way to crack this curse? Can he learn to work well with others? Will the effort be worth the cost?
*The Unbreakable Curse* is the first book in The Shade Series — a fantasy adventure for readers who love Jim Butcher's Dresden Files, Terry Pratchett's Discworld, and protagonists who are more clever than they are kind.
### KDP Keywords
1. fantasy series first person
2. sarcastic protagonist fantasy
3. magic system fantasy
4. guild fantasy adventure
5. fantasy mystery curse
6. Dresden Files similar
7. funny fantasy novel
## Cover Image
- [ ] Cover image ready (2560×1600px recommended for Kindle)
- [ ] Cover passes KDP image quality checks
- [ ] Run `export_kindle.py --cover path/to/cover.jpg` to embed cover in EPUB
## Quality Checks
- [ ] Open EPUB in Kindle Previewer — check rendering on multiple devices
- [ ] Verify Table of Contents links work
- [ ] Verify chapter breaks (each chapter starts on new page)
- [ ] Verify scene breaks (`* * *`) are centered and styled
- [ ] Check smart quotes, em dashes, ellipses throughout
- [ ] Check first paragraphs are not indented after headings/scene breaks
- [ ] Proofread front and back matter
- [ ] Confirm no `[CONTINUITY FLAG]` markers remain in text
## Upload
- [ ] Log in to KDP (kdp.amazon.com)
- [ ] Create new Kindle eBook
- [ ] Upload EPUB manuscript
- [ ] Upload cover image
- [ ] Fill in metadata (title, description, categories, keywords)
- [ ] Set pricing and royalty
- [ ] Enroll in Kindle Unlimited (KDP Select)
- [ ] Preview and publish

View File

@@ -0,0 +1,52 @@
# Kindle Publishing Checklist — Book 1
Generated from `export_kindle.py`. Review and complete before uploading.
EPUB: `kindle/exports/book1-kindle.epub`
---
## Front & Back Matter
- [ ] Copyright page — fill in year, pen name, legal text (`kindle/templates/copyright-page.md`)
- [ ] Dedication (optional) — add to front matter if desired
- [ ] Authors Note (optional) — add to back matter if desired
- [ ] “Also by the Author” page — create `kindle/templates/also-by.md` when applicable
- [ ] Next book preview — append preview chapter to back matter when available
## Metadata
- [ ] Confirm pen name / author name
- [ ] Confirm book title and subtitle
- [ ] Write book description / blurb (for KDP listing)
- [ ] Choose KDP categories (2 allowed)
- [ ] Select 7 keywords for KDP search
- [ ] Set price (consider KU 70% royalty tier: $2.99$9.99)
## Cover Image
- [ ] Cover image ready (2560×1600px recommended for Kindle)
- [ ] Cover passes KDP image quality checks
- [ ] Run `export_kindle.py --cover path/to/cover.jpg` to embed cover in EPUB
## Quality Checks
- [ ] Open EPUB in Kindle Previewer — check rendering on multiple devices
- [ ] Verify Table of Contents links work
- [ ] Verify chapter breaks (each chapter starts on new page)
- [ ] Verify scene breaks (`* * *`) are centered and styled
- [ ] Check smart quotes, em dashes, ellipses throughout
- [ ] Check first paragraphs are not indented after headings/scene breaks
- [ ] Proofread front and back matter
- [ ] Confirm no `[CONTINUITY FLAG]` markers remain in text
## Upload
- [ ] Log in to KDP (kdp.amazon.com)
- [ ] Create new Kindle eBook
- [ ] Upload EPUB manuscript
- [ ] Upload cover image
- [ ] Fill in metadata (title, description, categories, keywords)
- [ ] Set pricing and royalty
- [ ] Enroll in Kindle Unlimited (KDP Select)
- [ ] Preview and publish

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
ebooklib
markdown