111 lines
4.1 KiB
Python
111 lines
4.1 KiB
Python
"""CLI entry point for the Crossword KDP Book Generator."""
|
||||
|
|
|
|||
|
|
import logging
|
|||
|
|
import sys
|
|||
|
|
|
|||
|
|
import click
|
|||
|
|
|
|||
|
|
from csv_reader import CSVValidationError, read_csv
|
|||
|
|
from generator import generate_batch
|
|||
|
|
from kdp_format import render_kdp_book
|
|||
|
|
from models import TRIM_SIZES, PuzzleBook
|
|||
|
|
|
|||
|
|
|
|||
|
|
@click.command()
|
|||
|
|
@click.option("--input", "input_path", required=True,
|
|||
|
|
type=click.Path(exists=True, dir_okay=False),
|
|||
|
|
help="Path to a CSV file with word,clue columns.")
|
|||
|
|
@click.option("--output", "output_path", default="output.pdf",
|
|||
|
|
show_default=True,
|
|||
|
|
help="Output PDF file path.")
|
|||
|
|
@click.option("--puzzles", default=20, show_default=True,
|
|||
|
|
help="Number of crossword puzzles to generate.")
|
|||
|
|
@click.option("--grid-size", default=13, show_default=True,
|
|||
|
|
help="Grid dimensions (e.g. 15 for a 15×15 grid).")
|
|||
|
|
@click.option("--trim-size", default="8.5x11", show_default=True,
|
|||
|
|
type=click.Choice(list(TRIM_SIZES.keys())),
|
|||
|
|
help="KDP page trim size.")
|
|||
|
|
@click.option("--title", default="Crossword Puzzles", show_default=True,
|
|||
|
|
help="Book title for the cover page.")
|
|||
|
|
@click.option("--author", default="", help="Author name.")
|
|||
|
|
@click.option("--words-per-puzzle", default=20, show_default=True,
|
|||
|
|
help="Target number of words per puzzle.")
|
|||
|
|
@click.option("--solutions-at-end", is_flag=True, default=False,
|
|||
|
|
help="Group all solution pages at the back of the book.")
|
|||
|
|
@click.option("--seed", default=None, type=int,
|
|||
|
|
help="Random seed for reproducible output.")
|
|||
|
|
@click.option("--no-toc", is_flag=True, default=False,
|
|||
|
|
help="Omit the table of contents page.")
|
|||
|
|
@click.option("--no-instructions", is_flag=True, default=False,
|
|||
|
|
help="Omit the how-to-solve instructions page.")
|
|||
|
|
@click.option("-v", "--verbose", is_flag=True, default=False,
|
|||
|
|
help="Enable verbose logging.")
|
|||
|
|
def main(input_path: str, output_path: str, puzzles: int, grid_size: int,
|
|||
|
|
trim_size: str, title: str, author: str, words_per_puzzle: int,
|
|||
|
|
solutions_at_end: bool, seed: int | None, no_toc: bool,
|
|||
|
|
no_instructions: bool, verbose: bool) -> None:
|
|||
|
|
"""Generate a KDP-ready crossword puzzle book from a CSV word list."""
|
|||
|
|
|
|||
|
|
logging.basicConfig(
|
|||
|
|
level=logging.DEBUG if verbose else logging.INFO,
|
|||
|
|
format="%(levelname)s: %(message)s",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# -- read & validate CSV -------------------------------------------------
|
|||
|
|
try:
|
|||
|
|
word_clues = read_csv(input_path, min_words=3,
|
|||
|
|
max_word_length=grid_size)
|
|||
|
|
except (CSVValidationError, FileNotFoundError) as exc:
|
|||
|
|
click.echo(f"Error: {exc}", err=True)
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
click.echo(f"Loaded {len(word_clues)} words from {input_path}.")
|
|||
|
|
|
|||
|
|
# -- generate puzzles ----------------------------------------------------
|
|||
|
|
click.echo(f"Generating {puzzles} puzzle(s) "
|
|||
|
|
f"({words_per_puzzle} words each, {grid_size}×{grid_size} grid)…")
|
|||
|
|
|
|||
|
|
generated = generate_batch(
|
|||
|
|
word_clues,
|
|||
|
|
count=puzzles,
|
|||
|
|
words_per_puzzle=words_per_puzzle,
|
|||
|
|
grid_size=grid_size,
|
|||
|
|
seed=seed,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if not generated:
|
|||
|
|
click.echo("Error: could not generate any puzzles. "
|
|||
|
|
"Try a larger word list or smaller grid.", err=True)
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
if len(generated) < puzzles:
|
|||
|
|
click.echo(f"Warning: only {len(generated)}/{puzzles} puzzles met "
|
|||
|
|
f"quality thresholds.", err=True)
|
|||
|
|
|
|||
|
|
click.echo(f"Generated {len(generated)} puzzle(s).")
|
|||
|
|
|
|||
|
|
# -- assemble KDP book ---------------------------------------------------
|
|||
|
|
book = PuzzleBook(
|
|||
|
|
title=title,
|
|||
|
|
author=author,
|
|||
|
|
puzzles=generated,
|
|||
|
|
trim_size=TRIM_SIZES[trim_size],
|
|||
|
|
solutions_at_end=solutions_at_end,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
total_pages = render_kdp_book(
|
|||
|
|
book, output_path,
|
|||
|
|
include_toc=not no_toc,
|
|||
|
|
include_instructions=not no_instructions,
|
|||
|
|
)
|
|||
|
|
except ValueError as exc:
|
|||
|
|
click.echo(f"Error: {exc}", err=True)
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
click.echo(f"Wrote {total_pages}-page KDP book to {output_path}.")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|