We are building a creative writing assistant that turns a rough premise into a structured outline, character profiles, and a revised scene. The pipeline runs against Oxlo.ai's API using flat per-request pricing, so you can iterate with long context prompts without watching token costs climb. I will walk through the Python script I actually use to unblock myself on short fiction.
What you'll need
Python 3.10 or newer, the OpenAI SDK (pip install openai), and an API key from the Oxlo.ai portal. Oxlo.ai is fully OpenAI SDK compatible, so the client setup is a one-line base URL change.
Step 1: Configure the client and system prompt
First, I set up the client pointing at Oxlo.ai and define the system prompt that governs tone and output format. I use llama-3.3-70b as the general-purpose workhorse, but you can swap in kimi-k2.6 or qwen-3-32b for different stylistic flavors.
from openai import OpenAI
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
SYSTEM_PROMPT = """You are a fiction editor and writing coach. You write in concise, sensory prose. When asked for an outline, return a three-act structure with bullet points. When asked for characters, return name, motivation, and a verbal tic. When asked for a scene, write 300 to 400 words in third-person limited, heavy on dialogue and sensory detail. When critiquing, list two strengths and two specific fixes."""
# quick connectivity check
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": "Confirm you are ready to help with creative writing."},
],
)
print(response.choices[0].message.content)
Step 2: Generate a three-act outline
I feed the premise into a dedicated function that asks for a three-act structure with clear turning points. Because Oxlo.ai charges per request rather than per token, I can stuff a detailed style guide into the user prompt without worrying about input length.
def generate_outline(premise):
user_msg = f"""Premise: {premise}
Return a three-act outline with the following beats:
- Act 1: Setup and inciting incident
- Act 2: Rising action and midpoint reversal
- Act 3: Climax and resolution
Keep each beat to one or two sentences."""
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_msg},
],
)
return response.choices[0].message.content
premise = "A retired cryptographer discovers her old cipher machine is receiving messages from the future."
outline = generate_outline(premise)
print(outline)
Step 3: Create character profiles
Next, I pass the outline back into the model and ask for two characters whose motivations naturally conflict. Reusing the same context window in a new request keeps the logic stateless and easy to debug.
def generate_characters(outline):
user_msg = f"""Given this outline:
{outline}
Create two characters:
1. Protagonist: name, motivation, verbal tic
2. Antagonist or foil: name, motivation, verbal tic
Explain why their goals conflict in one sentence."""
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_msg},
],
)
return response.choices[0].message.content
characters = generate_characters(outline)
print(characters)
Step 4: Draft the opening scene
Now I combine the outline and characters into a single prompt and ask for a 350-word opening scene. I explicitly request third-person limited perspective and sensory details to avoid generic exposition.
def generate_scene(outline, characters):
user_msg = f"""Outline:
{outline}
Characters:
{characters}
Write the opening scene, 300 to 400 words, third-person limited from the protagonist. Include:
- One distinct sensory detail about the setting
- One line of dialogue that reveals motivation
- One moment of physical action"""
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_msg},
],
)
return response.choices[0].message.content
scene = generate_scene(outline, characters)
print(scene)
Step 5: Critique and refine the scene
Finally, I run a critique pass. I pipe the scene back into the model with instructions to flag weak adverbs and suggest one concrete rewrite. This extra request is cheap on Oxlo.ai because the cost is flat per call, not per word.
def critique_scene(scene):
user_msg = f"""Scene:
{scene}
List two strengths of the prose. Then list two specific fixes:
1. One weak adverb or cliché to cut
2. One sentence to rewrite for stronger rhythm"""
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_msg},
],
)
return response.choices[0].message.content
critique = critique_scene(scene)
print(critique)
Run it
Here is the full script wired together. Executing it end-to-end takes about ten seconds on Oxlo.ai with no cold starts on llama-3.3-70b.
from openai import OpenAI
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
SYSTEM_PROMPT = """You are a fiction editor and writing coach. You write in concise, sensory prose. When asked for an outline, return a three-act structure with bullet points. When asked for characters, return name, motivation, and a verbal tic. When asked for a scene, write 300 to 400 words in third-person limited, heavy on dialogue and sensory detail. When critiquing, list two strengths and two specific fixes."""
def generate_outline(premise):
user_msg = f"""Premise: {premise}
Return a three-act outline with Setup, Rising Action, and Climax beats. One or two sentences per beat."""
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_msg},
],
)
return response.choices[0].message.content
def generate_characters(outline):
user_msg = f"""Given this outline:\n{outline}\n\nCreate two characters with name, motivation, and verbal tic. Explain the conflict in one sentence."""
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_msg},
],
)
return response.choices[0].message.content
def generate_scene(outline, characters):
user_msg = f"""Outline:\n{outline}\n\nCharacters:\n{characters}\n\nWrite the opening scene, 300 to 400 words, third-person limited. Include a sensory detail, a dialogue line revealing motivation, and a physical action."""
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_msg},
],
)
return response.choices[0].message.content
def critique_scene(scene):
user_msg = f"""Scene:\n{scene}\n\nList two strengths. Then list two fixes: one weak adverb to cut, one sentence to rewrite for rhythm."""
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_msg},
],
)
return response.choices[0].message.content
if __name__ == "__main__":
premise = "A retired cryptographer discovers her old cipher machine is receiving messages from the future."
print("=== OUTLINE ===")
outline = generate_outline(premise)
print(outline)
print("\n=== CHARACTERS ===")
characters = generate_characters(outline)
print(characters)
print("\n=== SCENE ===")
scene = generate_scene(outline, characters)
print(scene)
print("\n=== CRITIQUE ===")
print(critique_scene(scene))
Example output excerpt:
=== OUTLINE ===
Act 1: Elena Voss, a retired cryptographer living in a coastal Oregon town, dusts off her Cold War-era Fialka cipher machine for a museum donation. Late that night, the machine clatters to life and prints a message dated thirty years in the future.
Act 2: Elena decodes the messages and realizes they are warnings about an impending local disaster...
=== SCENE ===
The salt air had turned the brass dials green. Elena ran her thumb across the Fialka's keyboard, feeling the cold metal teeth beneath each key. She had not touched it since the Berlin Wall fell...
"I did not spend forty years breaking ciphers," she said, not looking up, "just to ignore one that begs for my help."
Her fingers moved. The rotors spun. And then, impossibly, the paper tape began to move.
Next steps
Swap llama-3.3-70b for kimi-k2.6 or deepseek-v3.2 to see how different models handle tone and dialogue. If you want to scale this into a web app, consider adding a JSON mode schema to parse the outline into structured data before rendering it in a frontend. You can explore Oxlo.ai's flat request pricing for this kind of iterative workflow at oxlo.ai/pricing.
Top comments (0)