Chapter Argument

For Pratchett-like material, this prompt aims for an old-fashioned, mock-solemn chapter argument rather than asking the model to imitate Pratchett’s prose style. That contrast—grand bibliographical dignity applied to absurd events—is exactly what makes the device work.

import streamlit as st
import os
from dataclasses import dataclass

Page banner

st.set_page_config(
    page_title="Chapter Argument",
)

@st.cache_data
def print_banner():
    print("### Chapter Argument")
    return 1

print_banner()
st.write("### Chapter Argument")

💰 OpenAI Model Pricing

https://platform.openai.com/docs/pricing#latest-models

💰 Gemini Model Pricing

https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing


Select LLM.

Prices are USD per 1 million input/output tokens.

llm_prices = {
    "gemini-3.8-flash": (0.75, 3.75),
    "gpt-6-luna": (0.10, 0.50),
    "gpt-6.1-sol": (2.00, 10.00),
    # "gpt-5.6-sol": (5.00, 30.00),
    # "gpt-6-astra": (10.00, 50.00),
}

llm_models = list(llm_prices.keys())

llm_model =  st.radio("Model", llm_models, horizontal=True, label_visibility="collapsed")
# model_name = st.radio("Model", ["luna", "terra"], horizontal=True, label_visibility="collapsed")
# llm_model = llm_models[2 if model_name == "terra" else 1]

st.write(f"`{llm_model}`")

EN prompt

chapter_argument_prompt = """
You are given the full text of a chapter from a comic fantasy novel.

Write a **traditional descriptive chapter argument** for the chapter:
an old-fashioned miniature synopsis of the kind historically printed
beneath or in place of a chapter title.

The argument must begin:

**“In which…”**

### Style

Write as though an excessively dignified old editor is cataloguing
the contents of the chapter for the reader.

The tone should be:
- formal, dry, concise, and mock-solemn;
- slightly archaic, but immediately readable;
- amused without explaining the joke;
- disproportionately serious about trivial, ridiculous, or mundane events.

The humour should arise primarily from **contrast**: treat a minor inconvenience,
foolish remark, unfortunate meal, bureaucratic procedure, peculiar object,
or embarrassing misunderstanding with the same narrative importance as a murder,
magical catastrophe, political crisis, or threat to civilization.

Prefer understatement to punchlines.

Do **not** imitate the prose style or characteristic phrasing of the novel's author.
The voice should instead resemble an old-fashioned eighteenth- or nineteenth-century
chapter heading applied to comic fantasy.

### Content

Identify roughly **3–6 of the chapter's most characteristic events, discoveries, reversals, conversations, or absurdities**.

Do not merely summarize the plot. Select details that make an amusing sequence when juxtaposed.

Whenever possible:
- begin with something apparently important;
- include at least one wonderfully trivial or mundane detail;
- mention a character by name where useful;
- hint at consequences rather than explaining them;
- preserve surprises that the argument does not need to reveal;
- finish on an intriguing, ominous, or absurdly understated note.

### Form

Produce **one sentence only**.

Begin with **“In which…”**

Separate the miniature topics with semicolons, commas, or constructions such as **“and in which…”**.

Aim for **35–70 words**.

Do not add a title, explanation, quotation marks, bullet points, or commentary.

### Examples of the desired mechanism

“In which a matter of considerable importance is entrusted to entirely the wrong person;
a wizard discovers that theoretical knowledge has certain practical disadvantages;
supper is delayed by circumstances beyond supper’s control;
and a perfectly ordinary door proves regrettably less ordinary upon inspection.”

“In which the Watch investigates a disturbance; several citizens demonstrate
the flexible nature of the word ‘innocent’; a question of municipal procedure
assumes unexpected urgency; and a cheese becomes relevant.”

These examples demonstrate the **form and comic mechanism only**.
Do not reuse their wording or events.
"""

RU prompt

chapter_argument_prompt_ru = """
Вам дан полный текст главы из юмористического фэнтезийного романа.

Напишите для этой главы **традиционное описательное краткое содержание** —
старомодную миниатюрную аннотацию в духе тех, что исторически печатались
под заголовком главы или вместо него.

Текст должен начинаться словами:

**«В которой…»**

### Стиль

Пишите так, словно чрезмерно степенный старинный редактор обстоятельно
перечисляет читателю содержание главы.

Тон должен быть:
- формальным, сухим, лаконичным и шутливо-торжественным;
- слегка архаичным, но сразу понятным современному читателю;
- насмешливым, но без объяснения шуток;
- несоразмерно серьёзным по отношению к пустяковым, нелепым или обыденным событиям.

Юмор должен возникать прежде всего из **контраста**: мелкое неудобство, глупое замечание,
неудачную трапезу, бюрократическую процедуру, странный предмет или неловкое недоразумение
следует описывать с той же повествовательной значительностью, что и
убийство, магическую катастрофу, политический кризис или угрозу цивилизации.

Предпочитайте сдержанность явным остротам и панчлайнам.

**Не** подражайте стилю прозы или характерным оборотам автора романа. Голос должен
скорее напоминать старомодный заголовок главы XVIII или XIX века,
применённый к юмористическому фэнтези.

### Содержание

Выберите примерно **3–6 наиболее характерных событий, открытий, поворотов, разговоров или нелепостей главы**.

Не пересказывайте сюжет целиком. Отбирайте такие детали, которые при сопоставлении
образуют забавную последовательность.

По возможности:
- начинайте с чего-либо, кажущегося важным;
- включайте хотя бы одну восхитительно мелкую или бытовую подробность;
- при необходимости называйте персонажа по имени;
- намекайте на последствия, а не объясняйте их;
- сохраняйте те сюрпризы, раскрывать которые для этой аннотации необязательно;
- заканчивайте интригующей, зловещей или нелепо сдержанной нотой.

### Форма

Напишите **ровно одно предложение**.

Начните со слов **«В которой…»**

Разделяйте миниатюрные темы точками с запятой, запятыми или конструкциями вроде **«и в которой…»**.

Ориентируйтесь на объём **35–70 слов**.

Не добавляйте заголовок, пояснения, кавычки, маркированные списки или комментарии.

### Примеры требуемого приёма

«В которой дело чрезвычайной важности доверяется совершенно неподходящему человеку;
волшебник обнаруживает, что теоретические познания имеют некоторые практические недостатки;
ужин задерживается по обстоятельствам, от ужина не зависящим; а самая обыкновенная дверь
при ближайшем рассмотрении оказывается прискорбно менее обыкновенной».

«В которой Стража расследует происшествие; несколько горожан демонстрируют весьма гибкое
понимание слова „невиновен“; вопрос муниципальной процедуры неожиданно приобретает неотложность;
а сыр оказывается небезразличен к происходящему».

Эти примеры демонстрируют **только форму и комический механизм**.
Не повторяйте их формулировки или события.
"""

#ru = st.toggle("en : ru")
lang = st.radio("Language", ["en", "ru"], horizontal=True, label_visibility="collapsed")

prompt = chapter_argument_prompt_ru if lang == "ru" else chapter_argument_prompt

Find Obsidian folder

def find_obsidian_folder():
    current_folder = os.getcwd()
    home_folders = os.listdir(current_folder)
    book_folders = [item for item in home_folders if os.path.isdir(os.path.join(current_folder, item)) and item.endswith(" Book")]

    if (len(book_folders)==0):
        raise ValueError('The folder should contain a subfolder with a name that ends with " Book".')

    return book_folders[0]

Process the supplied text with OpenAI or Gemini client.

There should be OPENAI_API_KEY and GEMINI_API_KEY environment variables defined.

@dataclass(frozen=True)
class LLMResult:
    text: str
    input_tokens: int
    output_tokens: int
count_tokens(value: str) → int
def count_tokens(value: str) -> int:
    import tiktoken

    try:
        encoding = tiktoken.encoding_for_model(llm_model)
    except KeyError:
        # Use the current general-purpose OpenAI encoding for new or non-OpenAI
        # model names. For Gemini this count is an estimate only.
        encoding = tiktoken.get_encoding("o200k_base")
    return len(encoding.encode(value))
process_with_openai(text: str, prompt: str) → LLMResult
def process_with_openai(text: str, prompt: str) -> LLMResult:
    from openai import OpenAI

    client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
    response = client.responses.create(
        model=llm_model,
        instructions=prompt.strip(),
        input=text,
    )
    return LLMResult(
        text=response.output_text,
        input_tokens=response.usage.input_tokens,
        output_tokens=response.usage.output_tokens,
    )
process_with_gemini(text: str, prompt: str) → LLMResult
def process_with_gemini(text: str, prompt: str) -> LLMResult:
    from google import genai
    from google.genai import types

    client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
    response = client.models.generate_content(
        model=llm_model,
        contents=text,
        config=types.GenerateContentConfig(system_instruction=prompt.strip()),
    )
    usage = response.usage_metadata
    # Gemini bills response and reasoning tokens at the output-token rate.
    output_tokens = (usage.candidates_token_count or 0) + (
        usage.thoughts_token_count or 0
    )
    return LLMResult(
        text=response.text or "",
        input_tokens=usage.prompt_token_count or 0,
        output_tokens=output_tokens,
    )
process_text(text: str, prompt: str) → LLMResult
def process_text(text: str, prompt: str) -> LLMResult:
    if llm_model.startswith("gpt-"):
        return process_with_openai(text, prompt)
    if llm_model.startswith("gemini-"):
        return process_with_gemini(text, prompt)
    raise ValueError(f"Unsupported model: {llm_model}")
calculate_cost(result: LLMResult) → float
def calculate_cost(result: LLMResult) -> float:
    input_price, output_price = llm_prices[llm_model]
    return (
        result.input_tokens * input_price + result.output_tokens * output_price
    ) / 1_000_000
get_newest_files(directory, num_files)

Get num_files newest files from the provided directory.

def get_newest_files(directory, num_files):
    # Check if the directory exists
    if not os.path.isdir(directory):
        raise ValueError(f"The directory {directory} does not exist.")

    # Get a list of files in the directory with their full paths and modification times
    files_with_paths = []
    for file_name in os.listdir(directory):
        file_path = os.path.join(directory, file_name)
        if os.path.isfile(file_path) and file_path.endswith(".md"):
            files_with_paths.append((file_path, os.path.getmtime(file_path)))

    # Sort files by modification time in descending order (newest first)
    sorted_files = sorted(files_with_paths, key=lambda x: x[1], reverse=True)

    # Extract the num_files newest file names
    newest_files = [os.path.basename(file_with_path[0]) for file_with_path in sorted_files[:num_files]]

    return newest_files

Select note_name from 5 newest notes.

note_home = find_obsidian_folder()

newest_files = get_newest_files(note_home, 5)
note_name = st.selectbox(
   "Note",
   newest_files,
)

Load Obsidian note

file_path = os.path.join(note_home, note_name)
with open(file_path, 'r', encoding='utf-8') as file:
    text = file.read()

Write truncated input text in container

c_input = st.container(border=True)

def max_len(text, k):
    if len(text) <= k:
        return text
    return text[:k] + '...'

c_input.write(f"""

{max_len(text, 250)}

""")

st.write(f"Prompt: `{count_tokens(prompt)}` tokens, Text: `{count_tokens(text)}` tokens")

Process button

if st.button("Process"):
    if not text.strip():
        st.warning("Enter some text to process.")
    else:
        try:
            with st.spinner("Processing..."):
                result = process_text(text, prompt)
            st.write(result.text)
            st.write(
                f"Cost: ${calculate_cost(result):.8f} "
                f"({result.input_tokens:,} input tokens, "
                f"{result.output_tokens:,} output tokens)"
            )
        except KeyError as error:
            st.error(f"Missing environment variable: {error.args[0]}")
        except Exception as error:
            st.error(f"Processing failed: {error}")