<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-GB">
	<id>https://yusupov.cloud/index.php?action=history&amp;feed=atom&amp;title=A_Cabinet_of_Brief_Curiosities</id>
	<title>A Cabinet of Brief Curiosities - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://yusupov.cloud/index.php?action=history&amp;feed=atom&amp;title=A_Cabinet_of_Brief_Curiosities"/>
	<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=A_Cabinet_of_Brief_Curiosities&amp;action=history"/>
	<updated>2026-05-16T11:51:02Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.44.0</generator>
	<entry>
		<id>https://yusupov.cloud/index.php?title=A_Cabinet_of_Brief_Curiosities&amp;diff=415&amp;oldid=prev</id>
		<title>Mvuijlst at 15:24, 23 April 2026</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=A_Cabinet_of_Brief_Curiosities&amp;diff=415&amp;oldid=prev"/>
		<updated>2026-04-23T15:24:51Z</updated>

		<summary type="html">&lt;p&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #fff; color: #202122;&quot; data-mw=&quot;interface&quot;&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;tr class=&quot;diff-title&quot; lang=&quot;en-GB&quot;&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;← Older revision&lt;/td&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;Revision as of 15:24, 23 April 2026&lt;/td&gt;
				&lt;/tr&gt;&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l14&quot;&gt;Line 14:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 14:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;== Technology stack ==&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;== Technology stack ==&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;The application is built on Flask 3.0 with [[SQLite]] as its database, accessed through [[SQLAlchemy]] &lt;del style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;(Flask-SQLAlchemy 3.1)&lt;/del&gt;.&amp;lt;ref name=&quot;requirements&quot;&amp;gt;requirements.txt in the project repository &lt;del style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;pins Flask 3.0.3, Flask-Login 0.6.3, Flask-SQLAlchemy 3.1.1, Flask-WTF ≥1.2, python-dotenv 1.0.1, openai ≥1.50, httpx 0.27.2, and Pillow ≥10.&lt;/del&gt;&amp;lt;/ref&amp;gt; Authentication is handled by Flask-Login and cross-site request forgery protection by Flask-WTF. It is deployed on an Ubuntu VPS behind [[Nginx]] with [[Gunicorn]] running over a Unix socket under a dedicated &amp;lt;code&amp;gt;django&amp;lt;/code&amp;gt; system user, supervised by systemd. Additional dependencies include the [[OpenAI]] Python client for both text and image generation, [[Pillow (imaging library)|Pillow]] for image post-processing, [[python-dotenv]] for environment configuration, and [[httpx]] as the underlying HTTP transport. The front end is rendered from Jinja templates using a small Bootstrap-derived stylesheet (&amp;lt;code&amp;gt;static/style.css&amp;lt;/code&amp;gt;) and is registered as an installable progressive web app via &amp;lt;code&amp;gt;static/manifest.json&amp;lt;/code&amp;gt; and a service worker (&amp;lt;code&amp;gt;static/sw.js&amp;lt;/code&amp;gt;).&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;The application is built on Flask 3.0 with [[SQLite]] as its database, accessed through [[SQLAlchemy]].&amp;lt;ref name=&quot;requirements&quot;&amp;gt;requirements.txt in the project repository &amp;lt;/ref&amp;gt; Authentication is handled by Flask-Login and cross-site request forgery protection by Flask-WTF. It is deployed on an Ubuntu VPS behind [[Nginx]] with [[Gunicorn]] running over a Unix socket under a dedicated &amp;lt;code&amp;gt;django&amp;lt;/code&amp;gt; system user, supervised by systemd. Additional dependencies include the [[OpenAI]] Python client for both text and image generation, [[Pillow (imaging library)|Pillow]] for image post-processing, [[python-dotenv]] for environment configuration, and [[httpx]] as the underlying HTTP transport. The front end is rendered from Jinja templates using a small Bootstrap-derived stylesheet (&amp;lt;code&amp;gt;static/style.css&amp;lt;/code&amp;gt;) and is registered as an installable progressive web app via &amp;lt;code&amp;gt;static/manifest.json&amp;lt;/code&amp;gt; and a service worker (&amp;lt;code&amp;gt;static/sw.js&amp;lt;/code&amp;gt;).&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;== Data model ==&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;== Data model ==&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;

&lt;!-- diff cache key yusupov:diff:1.41:old-414:rev-415:php=table --&gt;
&lt;/table&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=A_Cabinet_of_Brief_Curiosities&amp;diff=414&amp;oldid=prev</id>
		<title>Mvuijlst at 15:23, 23 April 2026</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=A_Cabinet_of_Brief_Curiosities&amp;diff=414&amp;oldid=prev"/>
		<updated>2026-04-23T15:23:51Z</updated>

		<summary type="html">&lt;p&gt;&lt;/p&gt;
&lt;a href=&quot;https://yusupov.cloud/index.php?title=A_Cabinet_of_Brief_Curiosities&amp;amp;diff=414&amp;amp;oldid=413&quot;&gt;Show changes&lt;/a&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=A_Cabinet_of_Brief_Curiosities&amp;diff=413&amp;oldid=prev</id>
		<title>Mvuijlst: Created page with &quot;{{Infobox | 01_name         = A Cabinet of Brief Curiosities | 02_url          = https://acbc.yusupov.cloud | 03_developer    = Michel Vuijlsteke | 04_released     = 2025 | 05_genre        = AI-generated short-fiction application | 06_language     = Python | 07_framework    = Flask 3.0 | 08_license      = MIT }}  &#039;&#039;&#039;A Cabinet of Brief Curiosities&#039;&#039;&#039; (abbreviated &#039;&#039;&#039;acbc&#039;&#039;&#039;) is a web application hosted at &lt;code&gt;acbc.yusupov.cloud&lt;/code&gt; that generates illustrated thre...&quot;</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=A_Cabinet_of_Brief_Curiosities&amp;diff=413&amp;oldid=prev"/>
		<updated>2026-04-23T14:50:46Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;{{Infobox | 01_name         = A Cabinet of Brief Curiosities | 02_url          = https://acbc.yusupov.cloud | 03_developer    = Michel Vuijlsteke | 04_released     = 2025 | 05_genre        = AI-generated short-fiction application | 06_language     = Python | 07_framework    = &lt;a href=&quot;/index.php?title=Flask&amp;amp;action=edit&amp;amp;redlink=1&quot; class=&quot;new&quot; title=&quot;Flask (page does not exist)&quot;&gt;Flask&lt;/a&gt; 3.0 | 08_license      = MIT }}  &amp;#039;&amp;#039;&amp;#039;A Cabinet of Brief Curiosities&amp;#039;&amp;#039;&amp;#039; (abbreviated &amp;#039;&amp;#039;&amp;#039;acbc&amp;#039;&amp;#039;&amp;#039;) is a web application hosted at &amp;lt;code&amp;gt;acbc.yusupov.cloud&amp;lt;/code&amp;gt; that generates illustrated thre...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;{{Infobox&lt;br /&gt;
| 01_name         = A Cabinet of Brief Curiosities&lt;br /&gt;
| 02_url          = https://acbc.yusupov.cloud&lt;br /&gt;
| 03_developer    = Michel Vuijlsteke&lt;br /&gt;
| 04_released     = 2025&lt;br /&gt;
| 05_genre        = AI-generated short-fiction application&lt;br /&gt;
| 06_language     = Python&lt;br /&gt;
| 07_framework    = [[Flask]] 3.0&lt;br /&gt;
| 08_license      = MIT&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;A Cabinet of Brief Curiosities&amp;#039;&amp;#039;&amp;#039; (abbreviated &amp;#039;&amp;#039;&amp;#039;acbc&amp;#039;&amp;#039;&amp;#039;) is a web application hosted at &amp;lt;code&amp;gt;acbc.yusupov.cloud&amp;lt;/code&amp;gt; that generates illustrated three-sentence short stories in the style of [[H. P. Lovecraft]]. Each story is composed by a large language model from a structured set of randomised &amp;quot;knobs&amp;quot; and optional user-supplied seed words, and is paired with a black-and-white illustration designed to resemble a 19th-century engraved book plate. The site&amp;#039;s rotating tagline — drawn every thirty minutes from a pool of forty-five variants — frames the act of generation in deliberately archaic terms; the canonical opening reads &amp;quot;Dredge slivers of impossible worlds from the black gulfs of imagination and press them into trembling mortal words.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Flask 3.0 with [[SQLite]] as its database, accessed through [[SQLAlchemy]] (Flask-SQLAlchemy 3.1).&amp;lt;ref name=&amp;quot;requirements&amp;quot;&amp;gt;requirements.txt in the project repository pins Flask 3.0.3, Flask-Login 0.6.3, Flask-SQLAlchemy 3.1.1, python-dotenv 1.0.1, openai ≥1.50, httpx 0.27.2, and Pillow ≥10.&amp;lt;/ref&amp;gt; Authentication is handled by Flask-Login. It is deployed on an Ubuntu VPS behind [[Nginx]] with [[Gunicorn]] running over a Unix socket under a dedicated &amp;lt;code&amp;gt;django&amp;lt;/code&amp;gt; system user, supervised by systemd. Additional dependencies include the [[OpenAI]] Python client for both text and image generation, [[Pillow (imaging library)|Pillow]] for image post-processing, [[python-dotenv]] for environment configuration, and [[httpx]] as the underlying HTTP transport. The front end is rendered from Jinja templates using a small Bootstrap-derived stylesheet (&amp;lt;code&amp;gt;static/style.css&amp;lt;/code&amp;gt;) and is registered as an installable progressive web app via &amp;lt;code&amp;gt;static/manifest.json&amp;lt;/code&amp;gt; and a service worker (&amp;lt;code&amp;gt;static/sw.js&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
The database contains two tables.&lt;br /&gt;
&lt;br /&gt;
=== User ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; stores a single bootstrapped administrator account with an e-mail address, a Werkzeug password hash, and a creation timestamp. Sign-ups are disabled at the route level: the &amp;lt;code&amp;gt;/signup&amp;lt;/code&amp;gt; endpoint flashes a notice and redirects to login. The administrator is created on application start from the &amp;lt;code&amp;gt;ADMIN_EMAIL&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ADMIN_PASSWORD&amp;lt;/code&amp;gt; environment variables if no matching row exists.&lt;br /&gt;
&lt;br /&gt;
=== Story ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;Story&amp;lt;/code&amp;gt; stores the generated short fiction:&lt;br /&gt;
&lt;br /&gt;
* &amp;#039;&amp;#039;id&amp;#039;&amp;#039; (primary key), an optional &amp;#039;&amp;#039;user_id&amp;#039;&amp;#039; foreign key (null for guest submissions), the three-sentence &amp;#039;&amp;#039;story_text&amp;#039;&amp;#039;, the optional &amp;#039;&amp;#039;mood&amp;#039;&amp;#039;, &amp;#039;&amp;#039;nouns&amp;#039;&amp;#039; and &amp;#039;&amp;#039;verbs&amp;#039;&amp;#039; seeds supplied by the requester, an optional &amp;#039;&amp;#039;image_path&amp;#039;&amp;#039; (relative to the static folder, e.g. &amp;lt;code&amp;gt;images/story_42.png&amp;lt;/code&amp;gt;), the originating &amp;#039;&amp;#039;ip_address&amp;#039;&amp;#039; (indexed), and an indexed &amp;#039;&amp;#039;created_at&amp;#039;&amp;#039; timestamp.&lt;br /&gt;
&lt;br /&gt;
== Story generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Story generation is initiated by a POST to &amp;lt;code&amp;gt;/generate&amp;lt;/code&amp;gt; and proceeds through several deterministic-and-random stages before the OpenAI call.&lt;br /&gt;
&lt;br /&gt;
=== Seeds and knobs ===&lt;br /&gt;
&lt;br /&gt;
A request may carry up to three optional free-text fields — &amp;#039;&amp;#039;noun&amp;#039;&amp;#039;, &amp;#039;&amp;#039;verb&amp;#039;&amp;#039;, and &amp;#039;&amp;#039;mood&amp;#039;&amp;#039; — which are passed to the model as &amp;quot;seeds&amp;quot; and validated for presence in the output. Around the seeds, the application constructs a compact JSON &amp;#039;&amp;#039;knobs&amp;#039;&amp;#039; object that nudges the model along several axes:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Dimension !! Pool size !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| Perspective || 4 || first, second, third, omniscient&lt;br /&gt;
|-&lt;br /&gt;
| Structure || 8 || Discovery → Investigation → Revelation; Object → Rumor → Catastrophe; Signal → Interpretation → Realization; …&lt;br /&gt;
|-&lt;br /&gt;
| Time || 8 (forced &amp;#039;&amp;#039;ambiguous&amp;#039;&amp;#039;) || Victorian era, distant past, mythic period, interwar, near-future of obsolete technology&lt;br /&gt;
|-&lt;br /&gt;
| Location || 50 || lighthouse, salt marsh, foundry, signal box, scriptorium, observatory, shipbreaker&amp;#039;s yard, …&lt;br /&gt;
|-&lt;br /&gt;
| Situation || 32 || during a storm, at low tide, during a blackout, on the eve of demolition, while the clock refuses to strike, …&lt;br /&gt;
|-&lt;br /&gt;
| Lexical palette || 53 || nautical, horological, astronomical, archival, cartographic, mycological, glaciological, heraldic, …&lt;br /&gt;
|-&lt;br /&gt;
| Style constraint || 5 || include exactly one short line of dialogue; include a question; avoid the words &amp;quot;shadow&amp;quot; and &amp;quot;dim&amp;quot;; …&lt;br /&gt;
|-&lt;br /&gt;
| Sentence pattern || 3 || short → long → medium; medium → short → long; long → medium → short&lt;br /&gt;
|-&lt;br /&gt;
| Theme (only when no seeds) || 18 || forbidden knowledge, cosmic entities, inherited curses, mathematical theorems, astronomical observations, …&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The knobs object always carries the seeds and at most three additional dimensions. Three of the knobs — &amp;#039;&amp;#039;perspective&amp;#039;&amp;#039;, &amp;#039;&amp;#039;structure&amp;#039;&amp;#039;, and &amp;#039;&amp;#039;time&amp;#039;&amp;#039; — are selected &amp;#039;&amp;#039;deterministically&amp;#039;&amp;#039; from a SHA-256 hash of a salt composed of the current 30-minute time bucket, the requester&amp;#039;s IP address, and the seed words; this guarantees that the same client requesting the same seeds inside the same half-hour window draws the same priority knobs. The previous story&amp;#039;s knobs for that IP are read from the choices log and avoided where possible. &amp;#039;&amp;#039;Time&amp;#039;&amp;#039; is hard-pinned to &amp;lt;code&amp;gt;ambiguous&amp;lt;/code&amp;gt; to suppress dated or era-specific references in the output. Lexicon and constraint values are sampled with the regular pseudo-random generator. Once all priority knobs are forced into the object, any non-priority extras are dropped at random until at most three non-seed knobs remain.&lt;br /&gt;
&lt;br /&gt;
=== System rules ===&lt;br /&gt;
&lt;br /&gt;
The system prompt instructs the model to produce a coherent short story of exactly three sentences in the style of H. P. Lovecraft, with no titles, lists, numbering, or blank lines. The tone must be grave, ominous, and unsettling, and never humorous. The model is forbidden to use the words &amp;quot;eldritch&amp;quot;, &amp;quot;cyclopean&amp;quot;, or &amp;quot;loathsome&amp;quot;, and is forbidden to begin any line with one of six banned openings — &amp;quot;In the&amp;quot;, &amp;quot;Beneath&amp;quot;, &amp;quot;Under the&amp;quot;, &amp;quot;Within the&amp;quot;, &amp;quot;In the dim&amp;quot;, and &amp;quot;In the shadow&amp;quot;. Whimsical seeds are to be adapted with synonyms that preserve tone.&lt;br /&gt;
&lt;br /&gt;
=== Model selection ===&lt;br /&gt;
&lt;br /&gt;
The default text model is GPT-4o (configurable via &amp;lt;code&amp;gt;OPENAI_MODEL&amp;lt;/code&amp;gt;) with a configurable fallback (&amp;lt;code&amp;gt;OPENAI_FALLBACK_MODEL&amp;lt;/code&amp;gt;, also GPT-4o by default). A helper function inspects the installed OpenAI SDK at request time: if the configured model is in the &amp;lt;code&amp;gt;gpt-5&amp;lt;/code&amp;gt; family but the SDK does not expose the Responses API, the call silently falls back to a chat-compatible model. When the Responses API is available it is preferred for all models, with the system rules supplied via the explicit &amp;lt;code&amp;gt;instructions&amp;lt;/code&amp;gt; field. For &amp;lt;code&amp;gt;gpt-5&amp;lt;/code&amp;gt; family models the call additionally injects a &amp;lt;code&amp;gt;reasoning&amp;lt;/code&amp;gt; object (defaulting to &amp;#039;&amp;#039;effort: low&amp;#039;&amp;#039; but configurable via &amp;lt;code&amp;gt;OPENAI_REASONING_EFFORT&amp;lt;/code&amp;gt;), drops the &amp;lt;code&amp;gt;temperature&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;top_p&amp;lt;/code&amp;gt; parameters (which those models reject), and grants a higher &amp;lt;code&amp;gt;max_output_tokens&amp;lt;/code&amp;gt; budget (1600 by default, raised to 2400 on retry) to absorb reasoning-token consumption. For non-reasoning models, &amp;#039;&amp;#039;temperature&amp;#039;&amp;#039; is sampled uniformly from [0.88, 1.05] and &amp;#039;&amp;#039;top_p&amp;#039;&amp;#039; is fixed at 0.92. Where the Responses API is unavailable, the call degrades to Chat Completions.&lt;br /&gt;
&lt;br /&gt;
=== Validation and retry ===&lt;br /&gt;
&lt;br /&gt;
After the call returns, the output is validated by a lightweight checker that enforces:&lt;br /&gt;
&lt;br /&gt;
* Exactly three non-empty lines.&lt;br /&gt;
* No line beginning with any of the banned opening prefixes.&lt;br /&gt;
* Absence of the banned words.&lt;br /&gt;
* One terminal punctuation mark per line (no missing terminator, no more than two strong delimiters).&lt;br /&gt;
&lt;br /&gt;
If the request supplied seeds, a second pass verifies that the noun appears in the text and that the verb (or a simple inflection thereof) appears as a whole word.&lt;br /&gt;
&lt;br /&gt;
When validation fails, a single targeted retry is issued with a corrective prompt that names the specific failures, lowers the temperature slightly, and reuses the same knobs JSON. The retry result is accepted if it passes, or if it fails with strictly fewer issues than the original. If the model call itself fails — including the specific case where a &amp;lt;code&amp;gt;gpt-5&amp;lt;/code&amp;gt; response is marked &amp;#039;&amp;#039;incomplete&amp;#039;&amp;#039; due to &amp;lt;code&amp;gt;max_output_tokens&amp;lt;/code&amp;gt; — the pipeline retries once with a higher token budget, then attempts the configured fallback model, and finally falls back to a fixed three-sentence placeholder (&amp;quot;The moon borrowed a suitcase from a bewildered pigeon. …&amp;quot;). All failures surface to the user as Flask flash messages with appropriate severity.&lt;br /&gt;
&lt;br /&gt;
=== Persistence and logging ===&lt;br /&gt;
&lt;br /&gt;
The validated story text is written to a new &amp;lt;code&amp;gt;Story&amp;lt;/code&amp;gt; row together with the seeds and originating IP. Image generation is then dispatched in a background daemon thread so the HTTP response returns immediately. Two structured logs are appended in JSON-Lines format under the Flask instance folder:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;instance/choices.log.jsonl&amp;lt;/code&amp;gt; records, per story, the seeds, the knobs JSON, the selected temperature and top_p, the model actually used, the configured model, and the validation outcome (including reasons and whether a retry was attempted).&lt;br /&gt;
* &amp;lt;code&amp;gt;instance/image.log.jsonl&amp;lt;/code&amp;gt; records each image generation attempt with status (&amp;#039;&amp;#039;success&amp;#039;&amp;#039;, &amp;#039;&amp;#039;retry&amp;#039;&amp;#039;, &amp;#039;&amp;#039;failed&amp;#039;&amp;#039;, &amp;#039;&amp;#039;skipped&amp;#039;&amp;#039;), error text, attempt count, image size, and image model.&lt;br /&gt;
&lt;br /&gt;
== Image generation ==&lt;br /&gt;
&lt;br /&gt;
Each story is paired with a square 1024×1024 illustration generated through the OpenAI image API.&lt;br /&gt;
&lt;br /&gt;
=== Style cycling ===&lt;br /&gt;
&lt;br /&gt;
The application maintains a counter file at &amp;lt;code&amp;gt;instance/image_style_cycle.txt&amp;lt;/code&amp;gt; that walks deterministically through a library of fifteen monochrome engraving styles, including Victorian wood engraving, antique grimoire pen-and-ink, penny-dreadful frontispiece, steel engraving, copperplate etching, drypoint, mezzotint, aquatint-grained etching, scratchboard, Victorian scientific plate, woodcut, scanned 1860s book plate, fin-de-siècle symbolism, and Victorian reportage sketch. A threading lock guards the read-modify-write of the counter so that concurrent generations do not collide on the same style. If the counter file is missing or unreadable, a random style is chosen instead.&lt;br /&gt;
&lt;br /&gt;
=== Prompt construction ===&lt;br /&gt;
&lt;br /&gt;
The image prompt is assembled from four blocks:&lt;br /&gt;
&lt;br /&gt;
# A composition directive specifying a square 1:1 aspect ratio, full-bleed framing with no borders or margins, and a centred subject.&lt;br /&gt;
# The selected style sentence from the cycling library.&lt;br /&gt;
# A &amp;quot;scanned 1860s book plate&amp;quot; line that asks for slight ink unevenness and faint paper texture.&lt;br /&gt;
# The story text itself, truncated to 900 characters with an ellipsis if longer.&lt;br /&gt;
# A trailing &amp;quot;Avoid:&amp;quot; clause that explicitly forbids colour, grayscale wash, painterly shading, photorealism, 3D rendering, borders, frames, mats, vignettes, modern comic or anime style, halftone dots, captions, readable text, watermarks, and signatures.&lt;br /&gt;
&lt;br /&gt;
=== Image API and retries ===&lt;br /&gt;
&lt;br /&gt;
The configured image model defaults to &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt;; common typos such as &amp;lt;code&amp;gt;gpt-image-1.5&amp;lt;/code&amp;gt; are normalised back to &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt; at start-up. The output size is fixed at 1024×1024. Each generation tolerates up to three attempts (configurable via &amp;lt;code&amp;gt;IMAGE_RETRIES&amp;lt;/code&amp;gt;) with exponential backoff starting at 1.5 seconds and capped at 10 seconds. If all attempts fail, a tiny placeholder GIF is written to disk under the name &amp;lt;code&amp;gt;story_&amp;lt;id&amp;gt;_placeholder.gif&amp;lt;/code&amp;gt; and recorded as the story&amp;#039;s image path so the front end always has something to display.&lt;br /&gt;
&lt;br /&gt;
=== Post-processing ===&lt;br /&gt;
&lt;br /&gt;
Successfully returned PNG bytes are passed through a Pillow-based post-processor that:&lt;br /&gt;
&lt;br /&gt;
* Estimates the background colour from the four corner patches.&lt;br /&gt;
* Computes a difference image against a uniform background of that colour, raises its contrast, and finds the bounding box of meaningful content.&lt;br /&gt;
* Crops away any uniform border of more than ten pixels on any side, padded by two pixels to avoid clipping ink.&lt;br /&gt;
* Resizes the result back to 1024×1024 using Lanczos resampling and re-encodes as optimised PNG.&lt;br /&gt;
&lt;br /&gt;
If Pillow is unavailable or any step fails, the original bytes are written through unchanged.&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Home ===&lt;br /&gt;
&lt;br /&gt;
The home page (&amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt;) renders the generation form together with a gallery of the five most recent stories that successfully produced an image. For unauthenticated visitors, the form is hidden and replaced by a rotating notice (one of twenty-two phrasings, selected from a SHA-256 hash of the visitor&amp;#039;s IP and a six-hour bucket) when the visitor has already generated a story in the last 24 hours or when the site-wide guest cap has been reached. A footer-level statistics block displays the number of guest stories in the last 24 hours, the number of stories created by signed-in users in the same window, the all-time total, and the time elapsed since the most recent story.&lt;br /&gt;
&lt;br /&gt;
=== Story detail ===&lt;br /&gt;
&lt;br /&gt;
Each story is reachable at &amp;lt;code&amp;gt;/story/&amp;lt;id&amp;gt;&amp;lt;/code&amp;gt; and is publicly viewable regardless of authorship. The page presents the three sentences alongside the illustration; for the administrator, controls are exposed to regenerate the image (&amp;lt;code&amp;gt;/regenerate-image/&amp;lt;id&amp;gt;&amp;lt;/code&amp;gt;) or delete the story (&amp;lt;code&amp;gt;/story/&amp;lt;id&amp;gt;/delete&amp;lt;/code&amp;gt;). A polling endpoint at &amp;lt;code&amp;gt;/api/story/&amp;lt;id&amp;gt;/status&amp;lt;/code&amp;gt; returns a JSON document indicating whether a real image has yet been written, the URL of the image (or placeholder), and a flag distinguishing the two; the front end uses this to swap a placeholder GIF for the final image once background generation completes.&lt;br /&gt;
&lt;br /&gt;
=== Archive ===&lt;br /&gt;
&lt;br /&gt;
The archive (&amp;lt;code&amp;gt;/archive&amp;lt;/code&amp;gt;) is a paginated chronological listing of all stories, nine per page, with previous/next navigation. There is no per-user filter — all stories are visible.&lt;br /&gt;
&lt;br /&gt;
=== Authentication ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;/login&amp;lt;/code&amp;gt; accepts only the e-mail address configured in &amp;lt;code&amp;gt;ADMIN_EMAIL&amp;lt;/code&amp;gt; and verifies the password against the stored Werkzeug hash. &amp;lt;code&amp;gt;/logout&amp;lt;/code&amp;gt; ends the session. &amp;lt;code&amp;gt;/signup&amp;lt;/code&amp;gt; is intentionally disabled.&lt;br /&gt;
&lt;br /&gt;
=== Tagline rotation ===&lt;br /&gt;
&lt;br /&gt;
The base template injects a &amp;quot;current tagline&amp;quot; string into every response. The selection is deterministic on the current 30-minute interval since the Unix epoch: the interval timestamp is used as a seed for the standard library random module, which then picks one of forty-five variant phrasings. All visitors served within the same half-hour see the same tagline; the tagline rotates without any database state.&lt;br /&gt;
&lt;br /&gt;
== Rate limiting ==&lt;br /&gt;
&lt;br /&gt;
Rate limiting is enforced for unauthenticated visitors only:&lt;br /&gt;
&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Per-IP daily limit&amp;#039;&amp;#039;&amp;#039;: any IP that has produced a story within the last 24 hours is blocked from generating another (one story per visitor per day).&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Site-wide guest cap&amp;#039;&amp;#039;&amp;#039;: the total number of guest-authored stories in the last 24 hours must not exceed &amp;lt;code&amp;gt;GUEST_DAILY_CAP&amp;lt;/code&amp;gt; (default 24). When the cap is reached, the form is hidden for all guests until older stories age out.&lt;br /&gt;
&lt;br /&gt;
Both limits are evaluated at form render time (to hide the form) and at form submission time (to short-circuit the POST with a flashed warning and a redirect). Authenticated users have no rate limits and no per-IP enforcement.&lt;br /&gt;
&lt;br /&gt;
The originating IP for limit accounting and for the &amp;lt;code&amp;gt;Story.ip_address&amp;lt;/code&amp;gt; column is read from &amp;lt;code&amp;gt;X-Forwarded-For&amp;lt;/code&amp;gt; when the request carries it (taking the first comma-separated value), and from &amp;lt;code&amp;gt;request.remote_addr&amp;lt;/code&amp;gt; otherwise.&lt;br /&gt;
&lt;br /&gt;
The application reads its configuration from environment variables loaded via python-dotenv:&lt;br /&gt;
&lt;br /&gt;
== Deployment ==&lt;br /&gt;
&lt;br /&gt;
The production deployment is an Ubuntu VPS running a systemd unit (&amp;lt;code&amp;gt;acbc.service&amp;lt;/code&amp;gt;) that launches Gunicorn with three workers, bound to a Unix domain socket under the project directory. Nginx terminates HTTPS (provisioned by [[Let&amp;#039;s Encrypt]] via Certbot), serves the &amp;lt;code&amp;gt;static/&amp;lt;/code&amp;gt; directory directly with a one-year immutable cache header, and proxies all other requests to the Gunicorn socket. The service runs as the &amp;lt;code&amp;gt;django&amp;lt;/code&amp;gt; system user out of &amp;lt;code&amp;gt;/home/django/acbc&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Security and authorisation ==&lt;br /&gt;
&lt;br /&gt;
* Sign-ups are disabled; only the bootstrapped administrator account can authenticate.&lt;br /&gt;
* Login attempts for any e-mail other than &amp;lt;code&amp;gt;ADMIN_EMAIL&amp;lt;/code&amp;gt; are rejected without a database query.&lt;br /&gt;
* The image-deletion helper refuses to remove any path that does not begin with &amp;lt;code&amp;gt;images/&amp;lt;/code&amp;gt; or that, after canonicalisation, falls outside the configured upload folder, providing protection against path-traversal in stored data.&lt;br /&gt;
* Story deletion requires either the administrator session or ownership of the row; image regeneration requires the administrator session.&lt;br /&gt;
* The story-status JSON endpoint returns no user-identifying information beyond the public fields rendered on the story page.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
* [[Flask (web framework)]]&lt;br /&gt;
* [[H. P. Lovecraft]]&lt;br /&gt;
* [[Computational creativity]]&lt;br /&gt;
* [[Generative artificial intelligence]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
</feed>