Creative Applications › Code Implementation

Michael BrenndoerferJanuary 14, 202610 min read
Language AI HandbookAll chapters
Part 63 of 67LLM Applications
Chapter 484Creative Applications › Code Implementation
Reading section 6 of 12 · Code Implementation · 10 minSection 6 of 12 · 10 min

Book contents

Section 6 of 12 · 10 min read

Creative Applications › Code ImplementationOne section · Sections in this chapter6 / 12 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let us implement tools for creative generation: a poetry analyzer that measures rhyme and meter, a story beat generator, and a utility for style comparison.

Poetry Analysis Tools

We will build a poetry analyzer that can measure rhyme schemes and approximate metrical patterns.

In[3]:
Code
import re
import string


def get_last_vowel_sound(word):
    """Approximate rhyme detection by extracting the final vowel cluster + consonants."""
    word = word.lower().strip(string.punctuation)
    # Find the last vowel group and everything after it
    match = re.search(r"[aeiou][^aeiou]*$", word)
    if match:
        return match.group()
    return word[-2:]  # fallback


def analyze_rhyme_scheme(poem_lines):
    """
    Analyze the rhyme scheme of a poem.
    Returns a string like 'ABAB' or 'AABB'.
    """
    endings = []
    for line in poem_lines:
        line = line.strip()
        if not line:
            continue
        words = line.split()
        if words:
            endings.append(get_last_vowel_sound(words[-1]))

    # Map each unique sound to a letter
    sound_to_letter = {}
    current_letter = ord("A")
    scheme = []

    for sound in endings:
        # Check if this sound rhymes with any existing category
        matched = False
        for existing_sound, letter in sound_to_letter.items():
            if sound == existing_sound:
                scheme.append(chr(letter))
                matched = True
                break
        if not matched:
            sound_to_letter[sound] = current_letter
            scheme.append(chr(current_letter))
            current_letter += 1

    return "".join(scheme)


# Example: Analyze a Shakespearean sonnet opening
sonnet_opening = [
    "Shall I compare thee to a summer's day?",
    "Thou art more lovely and more temperate:",
    "Rough winds do shake the darling buds of May,",
    "And summer's lease hath all too short a date:",
]

scheme = analyze_rhyme_scheme(sonnet_opening)
Out[4]:
Console
Rhyme scheme: ABAB
Expected: ABAB

  [A] ...day
  [B] ...temperate
  [A] ...May
  [B] ...date

The analyzer extracts the terminal sound of each line and groups lines that share sounds. The Shakespearean sonnet opening correctly gets the ABAB pattern. Notice that "day" and "May" share the terminal sound "ay", and "temperate" and "date" share the terminal sound "ate", which our simple vowel-cluster heuristic captures despite their different spellings.

Syllable Counting

We use the cmudict library, which provides the CMU Pronouncing Dictionary, to count syllables in words. Each phoneme string in CMU contains stress markers (0, 1, or 2) that correspond to vowel sounds, and counting these markers gives the syllable count.

In[5]:
Code
import subprocess

subprocess.run(["uv", "pip", "install", "cmudict"], capture_output=True)

import cmudict as cmudict_lib

# Load CMU Pronouncing Dictionary once at module level
_cmu_dict = cmudict_lib.dict()


def count_syllables(word):
    """Count syllables using the CMU Pronouncing Dictionary."""
    word_clean = word.lower().strip(string.punctuation)
    entries = _cmu_dict.get(word_clean, None)
    if entries:
        # Count stress markers (0, 1, 2) in the first pronunciation
        return sum(1 for phoneme in entries[0] if phoneme[-1].isdigit())
    # Fallback: count vowel groups
    return len(re.findall(r"[aeiou]+", word_clean, re.IGNORECASE)) or 1


def count_line_syllables(line):
    """Count total syllables in a line of poetry."""
    words = line.split()
    return sum(count_syllables(w) for w in words)


# Test on haiku
haiku = [
    "An old silent pond",
    "A frog jumps into the pond",
    "Splash! Silence again",
]

syllable_counts = [count_line_syllables(line) for line in haiku]
Out[6]:
Console
Haiku syllable analysis:
  5 syllables (OK): 'An old silent pond'
  7 syllables (OK): 'A frog jumps into the pond'
  5 syllables (OK): 'Splash! Silence again'

Total syllables: 17 (expected: 17)

The syllable counter provides a ground-truth check we can apply to LLM-generated haiku. CMU phoneme strings use digit-suffixed stress markers (0 for unstressed, 1 for primary stress, 2 for secondary stress) on every vowel sound, so counting the digits gives the syllable count directly. When a language model produces a haiku, we verify the syllable counts and request a revision if they are incorrect. This verification-and-revise loop, where the model generates and a deterministic checker evaluates, is the general pattern for constrained creative generation: let the model handle the creative search and use rule-based verification to enforce the hard constraints.

Story Beat Generator

A story beat is a single unit of narrative action: something happens, a character reacts, the situation changes. Let us implement a simple story structure generator that maps high-level narrative beats to a structured outline.

In[7]:
Code
from dataclasses import dataclass
from typing import List


@dataclass
class StoryBeat:
    act: int
    beat_name: str
    description: str
    questions: List[str]


def generate_three_act_structure(
    protagonist: str, goal: str, obstacle: str, theme: str
) -> List[StoryBeat]:
    """Generate a three-act story structure outline."""

    beats = [
        StoryBeat(
            1,
            "Opening Image",
            f"Establish {protagonist}'s ordinary world before the story begins.",
            [
                f"What does {protagonist}'s normal life look like?",
                "What tone does the story set visually?",
            ],
        ),
        StoryBeat(
            1,
            "Inciting Incident",
            f"{protagonist} encounters a disruption that makes {goal} necessary.",
            [
                "What event forces the story into motion?",
                "Why can't the protagonist ignore this?",
            ],
        ),
        StoryBeat(
            1,
            "First Plot Point",
            f"{protagonist} commits to pursuing {goal}, crossing into a new world.",
            [
                "What decision locks the protagonist into the story?",
                "What is at stake if they fail?",
            ],
        ),
        StoryBeat(
            2,
            "Rising Action",
            f"{protagonist} faces increasingly difficult challenges as {obstacle} intensifies.",
            [
                f"How does {obstacle} escalate?",
                "What does the protagonist learn about themselves?",
            ],
        ),
        StoryBeat(
            2,
            "Midpoint",
            f"A false victory or major revelation reframes {protagonist}'s understanding.",
            [
                "What changes the protagonist's strategy?",
                "What truth do they confront?",
            ],
        ),
        StoryBeat(
            2,
            "Dark Night of the Soul",
            f"{protagonist} appears to have lost everything. The theme of '{theme}' is most sharply tested.",
            [
                "What is the protagonist's lowest point?",
                f"How does '{theme}' challenge them here?",
            ],
        ),
        StoryBeat(
            3,
            "Climax",
            f"{protagonist} confronts {obstacle} directly, armed with new understanding.",
            [
                "How does the protagonist apply what they've learned?",
                "What must they sacrifice or accept?",
            ],
        ),
        StoryBeat(
            3,
            "Resolution",
            f"The consequences of {protagonist}'s choices settle. The theme of '{theme}' is affirmed or subverted.",
            [
                "How has the world changed?",
                f"What does the story say about '{theme}'?",
            ],
        ),
    ]

    return beats


# Example story
beats = generate_three_act_structure(
    protagonist="a linguist who discovers her translation algorithm is being used for surveillance",
    goal="expose the misuse without destroying her life's work",
    obstacle="a powerful corporation and her own complicity in what happened",
    theme="the responsibility of tool-makers for how their tools are used",
)
Out[8]:
Console
THREE-ACT STORY STRUCTURE


--- ACT 1 ---

[Opening Image]
  Establish a linguist who discovers her translation algorithm is being used for surveillance's ordinary world before the story begins.
  Key questions:
    - What does a linguist who discovers her translation algorithm is being used for surveillance's normal life look like?
    - What tone does the story set visually?

[Inciting Incident]
  a linguist who discovers her translation algorithm is being used for surveillance encounters a disruption that makes expose the misuse without destroying her life's work necessary.
  Key questions:
    - What event forces the story into motion?
    - Why can't the protagonist ignore this?

[First Plot Point]
  a linguist who discovers her translation algorithm is being used for surveillance commits to pursuing expose the misuse without destroying her life's work, crossing into a new world.
  Key questions:
    - What decision locks the protagonist into the story?
    - What is at stake if they fail?

--- ACT 2 ---

[Rising Action]
  a linguist who discovers her translation algorithm is being used for surveillance faces increasingly difficult challenges as a powerful corporation and her own complicity in what happened intensifies.
  Key questions:
    - How does a powerful corporation and her own complicity in what happened escalate?
    - What does the protagonist learn about themselves?

[Midpoint]
  A false victory or major revelation reframes a linguist who discovers her translation algorithm is being used for surveillance's understanding.
  Key questions:
    - What changes the protagonist's strategy?
    - What truth do they confront?

[Dark Night of the Soul]
  a linguist who discovers her translation algorithm is being used for surveillance appears to have lost everything. The theme of 'the responsibility of tool-makers for how their tools are used' is most sharply tested.
  Key questions:
    - What is the protagonist's lowest point?
    - How does 'the responsibility of tool-makers for how their tools are used' challenge them here?

--- ACT 3 ---

[Climax]
  a linguist who discovers her translation algorithm is being used for surveillance confronts a powerful corporation and her own complicity in what happened directly, armed with new understanding.
  Key questions:
    - How does the protagonist apply what they've learned?
    - What must they sacrifice or accept?

[Resolution]
  The consequences of a linguist who discovers her translation algorithm is being used for surveillance's choices settle. The theme of 'the responsibility of tool-makers for how their tools are used' is affirmed or subverted.
  Key questions:
    - How has the world changed?
    - What does the story say about 'the responsibility of tool-makers for how their tools are used'?

The story structure generator produces a complete narrative scaffold. Each beat is a creative brief: a focused question the author must answer. This kind of structured prompting, where the model fills in one beat at a time with the full structure visible as context, produces far more coherent long-form narratives than asking the model to write a complete story in one shot. The reason is simple: when you provide the full structural skeleton, the model is not making structural decisions during generation. It is executing locally on the current beat while the global shape is already fixed. All of its generative capacity goes toward prose quality rather than narrative architecture.

Style Comparison

Let us implement a simple stylometric comparison that measures surface-level style features across writing samples.

In[9]:
Code
import numpy as np


def compute_style_features(text: str) -> dict:
    """Extract stylometric features from a text sample."""
    sentences = re.split(r"[.!?]+", text)
    sentences = [s.strip() for s in sentences if s.strip()]
    words = text.split()

    # Basic features
    avg_sentence_length = (
        np.mean([len(s.split()) for s in sentences]) if sentences else 0
    )
    word_lengths = [
        len(w.strip(string.punctuation))
        for w in words
        if w.strip(string.punctuation)
    ]
    avg_word_length = np.mean(word_lengths) if word_lengths else 0

    # Vocabulary richness (type-token ratio)
    unique_words = set(w.lower().strip(string.punctuation) for w in words)
    ttr = len(unique_words) / len(words) if words else 0

    # Punctuation density
    punct_count = sum(1 for c in text if c in string.punctuation)
    punct_density = punct_count / len(text) if text else 0

    # Sentence length variance (consistency vs. variety)
    sent_lengths = [len(s.split()) for s in sentences]
    sent_variance = (
        float(np.var(sent_lengths)) if len(sent_lengths) > 1 else 0.0
    )

    return {
        "avg_sentence_length": round(avg_sentence_length, 1),
        "avg_word_length": round(avg_word_length, 2),
        "type_token_ratio": round(ttr, 3),
        "punct_density": round(punct_density, 3),
        "sentence_length_variance": round(sent_variance, 1),
        "sentence_count": len(sentences),
        "word_count": len(words),
    }


# Compare three distinct prose styles
styles = {
    "Minimalist (Carver-like)": """
        He drove her to work. She didn't say thank you. He didn't expect it.
        Outside, it was raining. He sat in the car for a while after she went in.
        He thought about calling his brother. He didn't.
    """,
    "Maximalist (Proust-like)": """
        The morning light, that particular quality of autumn morning light which falls
        not so much upon the world as through it, transforming even the most ordinary
        surfaces, the kitchen table, the worn linoleum, the steam rising from her cup,
        into something that seemed, in those first moments before thought properly
        established itself, almost unbearably tender and irretrievably past.
    """,
    "Technical/Analytical": """
        The algorithm processes input tokens sequentially. Each token is mapped to an
        embedding vector. These vectors are combined using attention weights. The output
        layer produces probability distributions over the vocabulary. Training minimizes
        cross-entropy loss.
    """,
}

features = {name: compute_style_features(text) for name, text in styles.items()}
Out[10]:
Console
Feature                      Minimalist (Carver  Maximalist (Proust  Technical/Analytic  
----------------------------------------------------------------------------------------
avg_sentence_length          5.4                 58.0                6.8                 
avg_word_length              3.84                5.4                 6.5                 
type_token_ratio             0.763               0.862               0.941               
punct_density                0.05                0.021               0.02                
sentence_length_variance     8.5                 0.0                 3.0

The stylometric features capture real differences between writing styles. The minimalist sample has short sentences with low variance, while the maximalist sample has long, complex sentences with high internal variance. These features can be used to measure how closely an LLM's output matches a target style, and to verify that style transfer prompts are having the intended effect.

These five features represent only a fraction of what professional stylometry uses. Computational stylometry for author attribution often employs hundreds of features: function word frequencies, character nn-gram distributions, syntactic dependency patterns, and more. But even these simple surface features reveal enough signal to detect the broad strokes of stylistic difference that we care about when evaluating style transfer quality.

Key Parameters

The key implementation choices across these creative analysis tools are:

  • get_last_vowel_sound(word): Extracts the terminal vowel cluster and trailing consonants as a rhyme proxy. Uses regex [aeiou][^aeiou]*$ to find the last vowel run. This is a simplification; true rhyme detectors use phonemic transcriptions from dictionaries like CMU.
  • cmudict.dict(): Loads the CMU Pronouncing Dictionary as a Python dictionary mapping lowercase words to lists of phoneme sequences. Each phoneme sequence is one pronunciation variant.
  • syllable_count (manual): Counts the number of phoneme strings ending in a digit (0, 1, or 2) in the first pronunciation entry. These digits are stress markers on vowel phonemes, so counting them gives the syllable count.
  • type_token_ratio: Unique words divided by total words. Values near 1.0 indicate high vocabulary diversity (novelty-seeking prose). Values near 0.3 to 0.5 indicate repetitive or formulaic prose.
  • sentence_length_variance: High variance signals stylistic dynamism, mixing short punchy sentences with long elaborate ones. Low variance signals a more uniform rhythmic style.