pocket_poet.py
python · 303 lines
1"""Pocket Poet: a tiny CLI that prints a short, calm poem.23A single-file Python CLI tool that generates gentle, grounding poems4using curated word banks and simple line templates. Designed for quiet5moments and small comforts.67Typical usage:89 python pocket_poet.py10 python pocket_poet.py --seed 4211 python pocket_poet.py --lines 612"""1314from __future__ import annotations1516import argparse17import random18import re19from collections.abc import Sequence2021# ---------------------------------------------------------------------------22# Configuration23# ---------------------------------------------------------------------------2425_MIN_LINES: int = 526_MAX_LINES: int = 627_DEFAULT_LINES: int = 52829# ---------------------------------------------------------------------------30# Curated word banks31# ---------------------------------------------------------------------------32# Each bank is a tuple of words chosen for warmth, simplicity, and grounding33# quality. Words are intentionally plain — no abstractions, no cleverness,34# just the texture of a quiet day.3536_NOUNS: tuple[str, ...] = (37 "light",38 "morning",39 "rain",40 "river",41 "stone",42 "leaf",43 "sky",44 "field",45 "garden",46 "window",47 "door",48 "path",49 "bread",50 "breath",51 "stillness",52 "silence",53 "tea",54 "sparrow",55 "cloud",56 "shore",57 "blanket",58 "creek",59 "porch",60 "candle",61 "hearth",62 "wool",63 "hill",64)6566_ADJECTIVES: tuple[str, ...] = (67 "quiet",68 "soft",69 "small",70 "warm",71 "gentle",72 "still",73 "calm",74 "slow",75 "kind",76 "simple",77 "tender",78 "pale",79 "faint",80 "cool",81 "low",82 "thin",83 "deep",84 "steady",85 "hushed",86)8788_VERBS: tuple[str, ...] = (89 "rests",90 "waits",91 "holds",92 "opens",93 "settles",94 "arrives",95 "stays",96 "lingers",97 "gathers",98 "drifts",99 "lands",100 "turns",101 "folds",102 "hums",103 "glows",104 "breathes",105 "listens",106)107108_INFINITIVES: tuple[str, ...] = (109 "rest",110 "wait",111 "hold",112 "open",113 "settle",114 "arrive",115 "stay",116 "linger",117 "gather",118 "drift",119 "land",120 "turn",121 "fold",122 "hum",123 "glow",124 "breathe",125 "listen",126)127128_PLACES: tuple[str, ...] = (129 "the window",130 "the door",131 "the garden",132 "the table",133 "the path",134 "the hill",135 "the shore",136 "the porch",137 "the field",138 "the hearth",139)140141# ---------------------------------------------------------------------------142# Line templates143# ---------------------------------------------------------------------------144# Each template is a single line of poetry with placeholder tokens drawn145# from the word banks above. Placeholders: {adj}, {adj2}, {noun}, {noun2},146# {verb}, {inf}, {place}. All templates produce 3–8 words after rendering.147148_LINE_TEMPLATES: tuple[str, ...] = (149 "the {adj} {noun} {verb}",150 "a {adj} {noun} {verb} near {place}",151 "{noun} {verb} on the {noun2}",152 "you breathe and the {noun} {verb}",153 "there is {noun} in the {adj} {noun2}",154 "let the {noun} {inf}",155 "the {noun} knows how to {inf}",156 "something {adj} {verb} here",157 "a little {noun} near {place}",158 "the {adj} air {verb}",159 "nothing to do but {inf}",160 "the {noun} is enough",161 "all the {adj} things {inf}",162 "you are {adj} like the {noun}",163 "even the {noun} {verb}",164 "here is a {adj} place to {inf}",165 "{adj} {noun} and {adj2} {noun2}",166 "the world {verb} around you",167 "somewhere a {noun} {verb}",168 "be {adj} with the {noun}",169)170171# ---------------------------------------------------------------------------172# Template engine173# ---------------------------------------------------------------------------174# Maps each placeholder name to its source word bank. A compiled regex175# matches any {token} in a template and a callback resolves it against the176# corresponding bank, so each placeholder is replaced independently.177178_BANK_MAP: dict[str, tuple[str, ...]] = {179 "adj": _ADJECTIVES,180 "adj2": _ADJECTIVES,181 "noun": _NOUNS,182 "noun2": _NOUNS,183 "verb": _VERBS,184 "inf": _INFINITIVES,185 "place": _PLACES,186}187188_PLACEHOLDER_RE: re.Pattern[str] = re.compile(r"\{(\w+)\}")189190191# ---------------------------------------------------------------------------192# Generation helpers193# ---------------------------------------------------------------------------194195196def _pick(rng: random.Random, bank: tuple[str, ...]) -> str:197 """Selects a single entry from a word bank.198199 Args:200 rng: A seeded Random instance for reproducibility.201 bank: A tuple of candidate words or phrases.202203 Returns:204 A randomly chosen entry from the bank.205 """206 return rng.choice(bank)207208209def _render_line(rng: random.Random, template: str) -> str:210 """Fills a line template with words drawn from curated banks.211212 Each placeholder is resolved independently via regex substitution,213 so a template containing both {noun} and {noun2} may receive two214 different nouns.215216 Args:217 rng: A seeded Random instance for reproducibility.218 template: A template string containing {placeholder} tokens.219220 Returns:221 A fully rendered line of poetry.222 """223224 def _substitute(match: re.Match[str]) -> str:225 return _pick(rng, _BANK_MAP[match.group(1)])226227 return _PLACEHOLDER_RE.sub(_substitute, template)228229230def _generate_poem(rng: random.Random, line_count: int) -> list[str]:231 """Generates a poem by selecting and rendering line templates.232233 Templates are sampled without replacement to avoid structural234 repetition within a single poem.235236 Args:237 rng: A seeded Random instance for reproducibility.238 line_count: Number of lines to produce (5 or 6).239240 Returns:241 A list of rendered poem lines.242 """243 templates = rng.sample(_LINE_TEMPLATES, k=line_count)244 return [_render_line(rng, t) for t in templates]245246247# ---------------------------------------------------------------------------248# CLI249# ---------------------------------------------------------------------------250251252def _build_parser() -> argparse.ArgumentParser:253 """Constructs the argument parser for Pocket Poet.254255 Returns:256 A configured ArgumentParser instance.257 """258 parser = argparse.ArgumentParser(259 prog="pocket_poet",260 description="Print a short, calm poem.",261 )262 parser.add_argument(263 "--seed",264 type=int,265 default=None,266 help="integer seed for reproducible output",267 )268 parser.add_argument(269 "--lines",270 type=int,271 default=_DEFAULT_LINES,272 choices=range(_MIN_LINES, _MAX_LINES + 1),273 metavar=f"{{{_MIN_LINES},{_MAX_LINES}}}",274 help=(275 f"number of lines in the poem"276 f" (default: {_DEFAULT_LINES}, max: {_MAX_LINES})"277 ),278 )279 return parser280281282def main(argv: Sequence[str] | None = None) -> None:283 """Entry point for Pocket Poet.284285 Parses CLI arguments, initializes a random number generator,286 generates a short poem, and prints it to stdout.287288 Args:289 argv: Command-line arguments. Uses sys.argv[1:] when None.290 """291 parser = _build_parser()292 args = parser.parse_args(argv)293294 rng = random.Random(args.seed)295 poem = _generate_poem(rng, args.lines)296297 for line in poem:298 print(line)299300301if __name__ == "__main__":302 main()303