looks interesting, I had some time during the holidays and used it as a excuse to learn marimo for the front end
I think you should use structured outputs as you won’t need to write regex to parse out the premise, opening paragraph, etc. I have a example here
from __future__ import annotations
from typing import Optional, List
from pydantic import BaseModel, Field
class StoryStart(BaseModel):
"""
You are a sharp, imaginative fiction writer.
Task:
- Produce (1) a concise, compelling story premise and (2) the opening paragraph that launches the story.
Rules:
- If the user provides ideas, weave them in organically (don’t just repeat them).
- If the user provides no ideas, invent something fresh with a surprising combination of
genre, setting, protagonist, conflict, and twist.
- Premise: 2–5 sentences, stakes + hook, no spoilers.
- Opening paragraph: 120–180 words, vivid and concrete, minimal clichés, clear POV,
grounded scene, ends with a soft hook.
- Tone should follow user preferences; default to PG-13 if none are given.
- Avoid copying user phrasing verbatim; enrich and reframe.
- If user ideas conflict, choose one coherent direction and proceed.
Output only fields that match this schema.
"""
premise: str = Field(..., description="2–5 sentences. Stakes + hook, no spoilers.")
opening_paragraph: str = Field(..., description="120–180 words. Vivid, grounded, ends with a soft hook.")
class StoryContinue(BaseModel):
"""
You are a skilled novelist. Write the next paragraph only.
Inputs:
- You will be given the story premise and the story-so-far (either the opening paragraph + latest paragraph,
or a compact analysis summary). Use them to preserve continuity.
Rules:
- Output exactly one paragraph of story prose (no headings, no bullets, no analysis).
- Preserve continuity: characters, tone, POV, tense, world rules.
- Length target: ~120–200 words unless told otherwise.
- Concrete detail, strong verbs, show > tell; minimal clichés.
- Dialogue (if any) should reveal motive or conflict; avoid summary dumps.
- End with a soft hook/turn that invites the next paragraph.
Output only fields that match this schema.
"""
next_paragraph: str = Field(..., description="Exactly one paragraph of continuation prose.")
class StoryAnalysis(BaseModel):
"""
You are a story analyst. Produce a succinct “Story-So-Far” handoff so another model can write
the next paragraph without breaking continuity. Do not write new story prose.
Inputs:
- You will be given the story premise and the story text so far (opening paragraph + one or more continuation paragraphs).
Rules:
- Extract ground truth only from the provided text/premise. No inventions.
- Capture continuity anchors: cast, goals, stakes, conflicts, setting rules, POV/tense,
tone/style markers, motifs, and notable objects.
- Map causality and current situation.
- List active threads/hazards: open questions, ticking clocks, contradictions to avoid.
- Provide 3–5 next-paragraph seeds as beats only (no prose paragraphs).
Output only fields that match this schema.
"""
logline: str
cast: List[str] = Field(default_factory=list, description="Bullets: Name — role/goal/conflict; ties.")
world_rules: List[str] = Field(default_factory=list, description="Bullets: constraints/rules implied by text.")
pov_tense_tone: str = Field(..., description="Compact string for POV, tense, and tone/style markers.")
timeline: List[str] = Field(default_factory=list, description="Bullets: key event → consequence.")
current_situation: str
active_threads: List[str] = Field(default_factory=list)
continuity_landmines: List[str] = Field(default_factory=list)
ambiguities: List[str] = Field(default_factory=list)
next_paragraph_seeds: List[str] = Field(..., min_length=3, max_length=5, description="Beats-only options, no prose.")
Using the pydantic models
# -------------------------
# Example calls (Structured)
# -------------------------
# 1) Start
user_prompt = "let's create a new story no preferences just give me a idea"
start = client.beta.chat.completions.parse(
model=model,
response_format=StoryStart,
messages=[
{"role": "system", "content": "You are a helpful assistant. Follow the response model docstring."},
{"role": "user", "content": user_prompt},
],
temperature=2,
).choices[0].message.parsed
start = StoryStart.model_validate(start)
# 2) Continue (using premise + approved story-so-far text)
story_so_far = f"Premise:\n{start.premise}\n\nOpening paragraph:\n{start.opening_paragraph}"
cont = client.beta.chat.completions.parse(
model=model,
response_format=StoryContinue,
messages=[
{"role": "system", "content": "You are a helpful assistant. Follow the response model docstring."},
{"role": "user", "content": story_so_far},
],
temperature=0.2,
).choices[0].message.parsed
cont = StoryContinue.model_validate(cont)
# (User edits cont.next_paragraph if desired)
# 3) Analyze (premise + opening + approved next paragraph)
story_text = f"{start.opening_paragraph}\n\n{cont.next_paragraph}"
analysis_input = f"Premise:\n{start.premise}\n\nStory text:\n{story_text}"
analysis = client.beta.chat.completions.parse(
model=model,
response_format=StoryAnalysis,
messages=[
{"role": "system", "content": "You are a helpful assistant. Follow the response model docstring."},
{"role": "user", "content": analysis_input},
],
temperature=0.2,
).choices[0].message.parsed
analysis = StoryAnalysis.model_validate(analysis)
# 4) Continue again (using analysis summary instead of full text)
cont2_input = (
f"Premise:\n{start.premise}\n\n"
f"Story analysis summary:\n{analysis.model_dump_json(indent=2)}"
)
cont2 = client.beta.chat.completions.parse(
model=model,
response_format=StoryContinue,
messages=[
{"role": "system", "content": "You are a helpful assistant. Follow the response model docstring."},
{"role": "user", "content": cont2_input},
],
temperature=0.2,
).choices[0].message.parsed
cont2 = StoryContinue.model_validate(cont2)
After doing some exploration with structured outputs, I made a marimo app. You can see a live demo here
and tried out the light house premise
A reclusive lighthouse keeper discovers a cryptic message in a bottle, leading him on a perilous quest to uncover a generations-old secret buried beneath the churning sea. But the deeper he delves, the more he realizes the truth might be too dangerous to unearth.
If you want to look at the app, I forked your repo and reorganized it to my repo template.
if you want the full script checkout the app file
just run this after you build the dependencies
uv run marimo run 03-app/app.py
