Compare commits

...
2 Commits
Author SHA1 Message Date
JesseMarkowitzandClaude Opus 5 e5617b86ba Replace clustering with a catalogue, and hand-write the references to judge it against
The pipeline's embed-and-cluster step is dead, and this commit holds both the
evidence for that and the step proposed to replace it.

Predicaments. Scenes are re-described as "what the person is up against", with
no names, jobs or places, then embedded and clustered (redescribe.py,
topic_words.py, topic_share.py). The pilot chose qwen3:14b over 3b by reading
both side by side. Two defects the pilot exposed are fixed: split.py missed
titles in quotes and a contents subtitle after a dash, so three stories had been
merged into their neighbours, and strip_names.py read New York place names as
people. The corrected corpus is probe/v2 (97 stories, 839 scenes);
carry_summaries.py reuses the 829 unchanged v1 summaries. Topic share fell from
20% to 13% at k=60, short of the pre-registered 10%.

Hand references. Three corpora were read scene by scene and written up by hand,
under the same prompt rules the local models get, as a baseline to judge them
against: O. Henry (probe/v2/claude, 839 scenes, 20 situations), Wharton's
Descent of Man (probe/wharton, 262 scenes, 16 groups) and Jacobs's The Lady of
the Barge (probe/jacobs, 157 scenes, 19 groups). Each has its own README and a
readable page. No inference was used for any of them.

Catalogue. probe/catalogue maps every hand group in the three references onto 36
situation entries, with an answer key per corpus and one recurrence rule applied
to all three. classify.py assigns a scene one entry or none, leave-one-corpus-
out; score.py checks it against the key, with a self-test on random labels.

Why clustering is out: hand-written predicaments, embedded and clustered exactly
as the model's were, agree with the hand grouping at ARI 0.05 — no better than
the 14B text's 0.07. Better rewriting cannot rescue it. Embeddings cannot even
shortlist: the hand label is the nearest entry 13% of the time and in the top 8
half the time.

The classification runs are not here. The dev and test runs are pre-registered
in probe/catalogue/README.md with the bar set beforehand, and are blocked on the
inference host, whose GPU has fallen off the PCIe bus three times. The 30-scene
partial output in out/ is not a result.

Review page. The situation review is now a browser page rather than JSON edited
by hand (review_page.py, review_page_logic.cjs with Node tests, format schema
v2). It has never been rendered in a real browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BygvsUXV9eU6oHkTCkKZ1
2026-09-20 17:19:42 -04:00
JesseMarkowitzandClaude Opus 5 fa3769d0fe Add story-to-pack research and a structured situation review
Research toward building a content pack from a story corpus, kept on its own
branch and independent of the game. Records the selection experiments against
blind labels, and settles selection as gate G2 followed by a human review:
review.py writes REVIEW.md and a review.json form, apply_review.py checks the
filled form and writes situations.json for the next stage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6UDQ9o6L6Ey173U7XVou6
2026-09-15 06:59:35 -04:00
234 changed files with 160117 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
# story-to-pack lives on its own branch and is never imported by the game.
# The repo root ignores *.log. Here the run logs are the record of results, so keep them.
!*.log
# Inference-host telemetry: large, and it names the host.
probe/gpu-logs/
# Rebuildable caches — gate2.py, gate3.py and signals.py recreate them on first use.
probe/sim.pkl
probe/null.pkl
probe/null-samples.pkl
probe/coassoc.json
__pycache__/
+275
View File
@@ -0,0 +1,275 @@
# story-to-pack — design note
A tool that builds a playable The Ladder content pack from a corpus of stories.
**Status (2026-09-14): direction agreed, nothing built.** The decisions below were made in
conversation on 2026-09-10 and had until now existed only in that session's transcript. The
architecture rests on one assumption that has so far **failed** its first test (see
[Assumptions](#assumptions-under-test)); nothing past the probes should be built until it passes.
This is a design note, not a specification. It records what was decided and why, so that the next
step is argued from it rather than re-derived.
---
## What it is for
Feed it a body of fiction set in one world — say 20–30 short stories — plus a handful of structural
numbers, and get back a content pack the game can load and that its own conformance checks judge
viable. Then play it, hand-edit it, and retune it from play logs.
The Ladder's founding commitment (Planning/01 §9) is that a new setting is purely data. This tool
tests how far that goes: whether "data" can come from a corpus rather than a designer.
## Decisions
| Area | Decision |
| --- | --- |
| **Inputs** | A thematically coherent corpus (one world), 20–30 stories of 2–15 pages. Plus: number of stages, turns per stage, number of promotions, number of characters, and a one-line player role written by the designer. Early development uses much less text. |
| **Corpus** | Saved permanently beside the pack. Read once, at fixed depth. **Provenance recorded:** which chunks fed which event, even though nothing reads it yet. |
| **Artifact** | **One.** The generated pack is the source of truth from the moment it is written. The designer and the tool both edit that same file in place. Seed text is a seed, not a source: there is no regenerate-and-merge. |
| **Pipeline** | Chunk → embed → cluster → select recurring situations → extract prose → assign state paths and directions → search numbers → verify → repair. |
| **The model's job** | Selection and classification. **It never chooses a number.** Its generative tasks are entity substitution and, only where needed, filling out options. |
| **Inference** | Once, up front, checkpointed and cached. The numeric search and every repair iteration are pure computation and never call a model. This is what makes a small local model viable. |
| **Stat vocabulary** | A fixed mechanical spine (currency, debt, standing, two skills, flags, notable, per-character relationship stats). The corpus supplies the *labels*, not the structure. |
| **Cast** | Recurring **roles** harvested across the corpus (the authority, the rival, the peer), not people from any one story. Characters are minted for the roles and entity names are substituted in extracted prose. |
| **Stages** | Assigned automatically by the protagonist's standing in each scene, binned and balanced across stages. Overridable by hand. |
| **Event count** | Derived from turns per stage, with a sufficiency report *before* inference is spent: "this corpus yields N situations, the pack needs M". |
| **Options** | 3–5 per event: 1–3 fixed archetypes ("say nothing"), 1–3 drawn from other scenes in the cluster, and 1–3 generated only to fill gaps. Deduplicated **mechanically** — two options with the same effects are one option. |
| **Description variants** | An event can carry several descriptions, sharing one general option set. This needs an engine change (see Open questions). |
| **Viability** | Fully automatic — a human does not have to play a pack for it to count as viable. A **graded score**, not pass/fail, so extra time improves it. The search **always keeps the best-scoring candidate**, never the most recent. When it cannot reach viable, it reports precisely which checks it could not satisfy. |
| **Time and quality** | Start fast and iterate; later, overnight runs for higher quality. Time buys **numeric search depth and prose fidelity** — not corpus reach. |
| **Retuning** | Deterministic, driven by the play log `npm run analyze` already reads. Not model-mediated. |
| **Extension** | Pulling more corpus into an existing pack is **out of scope** for now. Provenance is recorded so it stays possible. |
| **Home** | `theladder/tools/`, never imported by `src/`, `content/` or `test/`. Its own dependencies stay out of the game. |
| **Hardware** | Must be able to run on a low-powered local inference host, taking longer if it has to. |
## Why the hard part is numbers, not prose
Counted from the two hand-written packs:
| | Corporate Ladder | The Frontier |
| --- | --- | --- |
| Prose strings | 219 | 211 |
| Numeric decisions | 347 | 427 |
| Stable ids | 125 | 120 |
A pack is roughly 35% prose and 65% numbers, and every defect the project has hit — the debt trap,
the rent retune, the negotiation cliff — lived in the numbers. The corpus supplies the prose; the
tool has to invent the rest, and has to be able to show it is not a trap.
Randomising The Frontier's numbers while keeping its prose and structure: random magnitudes pass the
conformance checks 3% of the time, random directions 0%, and a crude four-rule repair loop 42% (5 of
12 runs converged). The search space is not hopeless, but it is not a broad basin either — hence a
real search against a graded score.
## Why recurrence is the central problem
Hand-written packs are almost entirely **recurring** situations: 29 of 31 events in Corporate Ladder
and 28 of 29 in The Frontier can fire more than once, and a 41-turn playtest saw 12 distinct
situations an average of 3.4 times each. Stories are the opposite: every scene happens once.
So selection is not "pick 30 good scenes". It is **find the situations this corpus keeps returning
to**. Twenty stories in one world should contain eight variations on "a superior asks you to cover
something up"; clustering those is what yields one recurring event with several descriptions. One
long novel is the hard case and is not the target.
## Boundaries
- **Planning/01 §10 is not violated by a hosted model here.** §10 says the *game* must never require
a hosted inference service. The generator is a build-time tool whose output is plain data with no
runtime dependency; a shipped `.html` still plays offline. Record this in theladder's DECISIONS
when the tool lands there.
- **There are two provider slots, not one.** Embeddings and generation are configured separately.
Anthropic offers generation but no embeddings API, so any mix must keep embeddings local.
- **The inference host is shared.** Other work sends requests to the same server. Clients run one
request at a time with a pause between requests, set a short `keep_alive`, and read the host from
the environment (`STP_OLLAMA`) — never from a tracked file. Every run is agreed with its owner
before it starts.
## Assumptions under test
The architecture rests on three assumptions. All three are cheap to test, and the first could
invalidate everything else.
### 1. A coherent corpus clusters into recurring situations — FAILED in naive form
**Test (2026-09-10):** O. Henry's *Four Million* cycle — four Gutenberg volumes, 94 stories, 244,690
words — cut into 838 scenes of ~300 words, embedded with `nomic-embed-text`, clustered with k-means.
**Result:** at the best k, 18 clusters were both tight and drawn from four or more stories. But
reading them showed most were 60–79% **one** story plus stragglers. Measured by concentration, only
**3 of 18** were genuinely recurring, covering **2% of the corpus**. At scene length, embedding
similarity tracks *which story a scene is from* — names, setting, narrator voice — not *what kind of
situation it is*.
**Fix under test:** summarise each scene into one generic sentence with no proper names, embed the
summary, and cluster those instead. This probe is in `probe/`.
**Baseline re-measured (2026-09-14)** on the rebuilt corpus — prose embeddings made on a GPU host,
same model, same 838 chunks. Seed 11 at k = 100 reproduces the 2026-09-10 figures exactly (18 sharp
clusters spanning ≥ 4 stories, 3 genuinely recurring, 2% coverage). Seeds 12 and 13 give 7 and 7,
covering 8% and 10%: the old 2% was one unlucky initialisation, and the true baseline is poor rather
than hopeless.
**Pass mark, fixed before any summary existed.** Judged at **k = 60 and k = 100**, medians over
three seeds, summary embeddings against prose embeddings as reported by `probe/measure.py`:
1. Median dominant-story share of clusters of size ≥ 5 falls to **40% or below** at both k.
2. **Tight-recurring coverage** — recurring clusters (≥ 4 stories, dominant ≤ 40%) whose coherence is
at or above that file's median cluster — reaches **at least 10% of the corpus** at both k. (A
relative bar was written first and dropped once the baseline came in at 2% and 0%, where "double"
means nothing. 10% of 838 scenes is ~84 — enough raw material for a stage's worth of situations.)
3. Reading three sampled tight-recurring clusters, each is recognisably **one kind of situation**,
not a prose register ("men walking through the city") or a vocabulary cluster.
The prose baseline numbers the first two are judged against are in `probe/measure-prose.log`.
**Result (2026-09-14): criteria 1 and 2 pass decisively; criterion 3 passes only in part.**
| k | Median dominant share, prose → summaries | Tight-recurring coverage, prose → summaries |
| --- | --- | --- |
| 60 | 57% → **22%** | 2% → **26%** |
| 100 | 67% → **28%** | 0% → **19%** |
Pipeline as run: `qwen2.5:3b-instruct` summaries with the v2 prompt (no example roles), names removed
by `strip_names.py`, embedded with `nomic-embed-text`. Logs: `probe/measure-summary.log`,
`probe/sample-k60.log`, `probe/sample-k100.log`.
Reading the seeded samples (three at each k; one cluster appeared at both, so five distinct):
- **Clearly one situation (2):** a spouse scheming against or confronting a spouse over infidelity
(15 scenes, 10 stories); a woman made the centre of attention while men compete for her (5 scenes,
5 stories).
- **Broadly one situation (2):** a secret identity or past revealed in a romantic encounter, often over
dinner (15 scenes, 13 stories); hosts and guests jockeying for social rank at a dinner (6, 6).
- **Not a situation (1):** fortune-tellers, adventurers, a found tyre, a graft inquiry — held together
by "seeks", "fortune" and "mysterious" and dominated by three stories. It is the same cluster at
both k.
The rule as written requires every sample to read as one situation, so **strictly this is not a full
pass**, and it is recorded that way rather than rounded up.
**Follow-up: the selection step is designed and tested in [`SELECTION.md`](SELECTION.md).** A
two-signal gate (cross-story cohesion, and no single word dominating) accepted 10 clusters across
development and test, all of them situations; it rejects the vocabulary clusters this result found.
But it keeps only 5 of ~40 candidates — far short of what a pack needs — so recall is the open problem.
Three recall-recovery methods were then tested on a fresh partition and **none met its pre-registered
bar**: re-clustering found nothing new, consensus clustering found only sharp situations (3 of 3) but
too few, and prune-then-grow found 10 distinct situations with too much noise. The finding that
matters is that the gate is size-dependent in both directions, so it has to be made size-invariant
before recall can be recovered.
A second gate, G2, built for that (cohesion against a same-size random baseline, a significance test
for shared words), **failed validation on fresh data**: it accepts three to four times as many
clusters as the first gate at about the same precision (0.79), but still rejects every cluster under
10 members and still lets growth run to the cap. A fixed threshold on a z-score reverses the size bias
rather than removing it.
A third gate, G3 (a percentile against same-size random sets, plus a mandatory per-member floor),
**failed all four validation criteria and did worse than G2**: 17 accepted across fresh partitions,
only 8 of them situations. The random-set baseline turned out to be the wrong comparison — any
k-means cluster beats random scenes — and the per-member floor is noisiest, and so most lenient,
exactly where clusters are smallest.
A fourth gate, G4, had the inference model read each cluster and say which summaries share one
situation. It **failed with both `qwen2.5:3b-instruct` and `qwen3:14b`** (primary precision 0.48 and
0.52 against G2's 0.73): the models answer with abstractions ("power dynamics", "someone wants
something from someone") that most summaries fit. Observed but not pre-registered: the 14B model AND
G2 accepted 10 clusters, all situations — precise, narrow, and untested on fresh data. The substantive conclusion: summarising
before embedding **does** break the story-identity effect that sank the naive design, and most of
what clusters is recognisably situational — the recurrence premise is alive, not proven. The design
needs a **selection step that rejects vocabulary clusters**, not only a clustering step.
Known defects in this run, none large enough to change the verdict:
- `first_sentence` cut 7 of 838 summaries that begin with an initial ("E. Rushmore Coglan…") to
"E." — single-letter initials and "Gen." are not handled.
- Heavy use of "someone" is itself a shared token across summaries with many characters. Whether it
pulls clusters together has not been measured; a replacement by role or a neutral mix would test it.
- A few names that are also common words survive stripping ("Silver", "Cherry", "Grace", "the Kid").
**Why k ≥ 60, and why a tightness filter.** The first version of this pass mark said "median dominant
≤ 40% at k ≥ 40" — and the prose baseline, the approach known to fail, passed it at k = 40 (36%).
Two things were wrong. Small k forces clusters to merge stories whatever the embeddings encode: on
synthetic vectors carrying nothing but story identity, `measure.py` reports 36% at k = 25, 53% at k
= 40 and 100% at k ≥ 60, while vectors with planted cross-story situations report 11–17% at every k
≥ 40. And without a tightness filter, the loose clusters the 2026-09-10 reading identified as prose
register count as recurring. The tightness cutoff is relative to each file's own median because an
absolute cosine threshold does not transfer between 300-word scenes and one-sentence summaries.
If it fails, the recurrence premise needs rethinking before any design goes further.
### 2. A small model can classify against a closed path vocabulary
Roughly 100+ calls per pack deciding which state paths an option moves and in which direction.
Malformed or wrong output at a meaningful rate changes the design (batching, retries, a bigger
model). **Untested.**
### 3. A small model can substitute entity names without wrecking prose
The main generative task, run on every description. **Untested.**
The summarisation probe in assumption 1 incidentally gives the first evidence on 2 and 3: it asks a
small model for constrained, name-free output and the results can be read.
**First evidence (2026-09-14, `qwen2.5:3b-instruct`, two 40-scene pilots):**
- **It copies examples from the prompt into the output.** A prompt that illustrated roles with "a
clerk", "a landlady", "a rich man", "a city park" got those phrases back 18 times — in scenes
containing **none** of them, every one invented. For this probe that is worse than noise: shared
invented phrases make unrelated scenes cluster together and fake exactly the recurrence being
measured. Removing the examples brought it to 1. **Rule: prompts for a small model name no example
of the output vocabulary.**
- **It does not follow "never use a name."** 25 of 40 summaries kept names with the first prompt,
and 27 of 40 with a stricter one plus an automatic retry — the retry fired 35 times, nearly doubled
the run, and fixed almost nothing. Names are therefore removed **deterministically** afterwards
(`probe/strip_names.py`): a lexicon built from the corpus, of words seen capitalised mid-sentence
and almost never in lowercase. It misses names that are also common words ("Bill", "the Kid",
"New" in "New York"). That bears directly on assumption 3: **entity substitution should be done by
code over a lexicon, with the model's output treated as untrusted.**
- **Throughput is not the constraint.** On a GTX 1080 Ti, a scene summary takes ~0.6 s of model time
(about 1.1 s including a deliberate pause between requests), and embedding runs at ~16 texts/s.
A 838-scene corpus summarises in roughly a quarter of an hour.
## Open questions
- **Description variants need an engine change.** Events carry one description today. Scope it
before building, and record it in theladder's DECISIONS — it is the first engine change content
generation would force.
- **The test strategies hardcode the stat spine.** `theladder/test/fixtures/strategies.js` reads the
literal path `money` and the prefix `relationships.`. A pack that named its currency `coin` would
make four of six archetypes stop discriminating while the sweep still ran and reported. The fixed
spine satisfies this by construction; it is also a coupling that should be made explicit.
- **Options versus variants.** A cluster-derived option is tied to the scene it came from and may
read wrongly under the event's other descriptions. The more variants an event has, the more its
options must come from the fixed archetypes.
- **More options, harder search.** Moving from ~3.1 to ~4 options per event adds roughly 90 effect
magnitudes to a search that already converged only 5 times in 12 with crude repair.
- **Prompts may not transfer between models.** Prompts developed against a capable model are
typically under-specified for a 3B one. The small-model question stays open until it is tested on
a small model, however well a larger one does.
- **Story splitting is per-volume fiddly.** Segmentation needs no model, but each source's format
needs its own pattern. A production tool needs a report when a volume does not split cleanly.
The probe's `split.py` yields 94 stories from 98 contents entries: it silently drops titles
containing quotation marks or an em dash ("LITTLE SPECK IN GARNERED FRUIT", "THE THING'S THE PLAY",
"WHAT YOU WANT", "THE GUILTY PARTY — AN EAST SIDE TRAGEDY"). The 2026-09-10 run dropped the same
ones, so it is left alone to keep the before/after comparison exact.
## Layout
```
story-to-pack/
DESIGN.md this note
recovered/ the 2026-09-10 scripts, as recovered from the transcript; not re-run
probe/ the working probe for assumption 1
corpus/ Gutenberg source texts (rebuildable; see recovered/README.md)
split.py volumes -> stories.json
chunk.py stories -> chunks.json (~300-word scenes)
ollama.py the only module that talks to an inference host
embed.py texts -> vectors, checkpointed
summarise.py scenes -> one generic sentence each, checkpointed
strip_names.py summaries -> summaries with names replaced, from a corpus-built lexicon
measure.py clustering concentration report for any embedding file
sample_clusters.py seeded sample of tight-recurring clusters, for reading (criterion 3)
summaries-v1-pilot.json, summaries-v2-pilot-retry.json the two 40-scene pilots, kept as evidence
*.log run and measurement output
gpu-logs/ the inference host's power/link/kernel logs for the 2026-09-14 run
```
+103
View File
@@ -0,0 +1,103 @@
# story-to-pack
Research toward a tool that builds a The Ladder content pack from a corpus of stories.
**Independent of the game.** Nothing in `src/`, `content/` or `test/` imports anything here, and
nothing here is part of `npm test` or `npm run build`. It lives on the `story-to-pack` branch so it is
versioned alongside the pack format it targets, without touching `main`.
- **What was decided and why:** [`DESIGN.md`](DESIGN.md)
- **How situations are selected, and every test of it:** [`SELECTION.md`](SELECTION.md)
- **The 2026-09-10 scripts, recovered from a transcript:** [`recovered/`](recovered/README.md)
## Status
Selection was settled as **gate G2 followed by a structured human review** (see below). Every automated
alternative tried — three more geometric gates and two model-judged gates — was worse on fresh,
blind-labelled data. The first real review then found G2's groups were **topics** (artists, police,
hotels), not situations. **Now under test:** re-describe every scene as its main person's
**predicament** with qwen3:14b, regroup, and read the groups (SELECTION.md, "Predicaments"). Nothing
downstream of selection (paths and directions, numbers, pack output) is built yet.
## Layout
`probe/` holds the v1 data every G1–G4 result was measured on. `probe/v2/` is the corrected corpus
(97 stories, 839 scenes: v1's splitter merged three quoted-title stories into their neighbours), built by
running the same scripts from inside `v2/` (`python3 ../split.py`, and so on).
```
probe/
corpus/ O. Henry's Four Million cycle, Project Gutenberg (public domain)
split.py, chunk.py volumes -> stories.json -> chunks.json (838 scenes in v1, 839 in v2)
carry_summaries.py v1 summaries onto v2 scenes; only new scene text goes to the model
redescribe.py scene -> its main person's predicament, via the model -> predicaments.json
topic_words.py, topic_share.py how much groups are held together by a job, relationship or place word
pilot_page.py side-by-side page for the 3B / 14B predicament pilot -> pilot-compare.html
groups_page.py reading page for regrouped predicaments -> v2/groups-*.html
ollama.py the only module that talks to an inference host (host from STP_OLLAMA)
summarise.py scene -> one sentence, via the model -> summaries.json
strip_names.py names removed deterministically -> summaries_clean.json
embed.py texts -> vectors -> embeddings.json, summary_embeddings.json
measure.py, signals.py, gate2.py, gate3.py, judge.py … the selection experiments
labels-*.json, sheet-*.txt, candidates-*.json, *-map.json blind labels and what they labelled
review.py G2 on a partition -> REVIEW.html and candidates.json
review_page.py renders the page; picks out role words for each group
review_page.html the page template
review_page_logic.cjs the rules the page applies (also run by the Node tests)
review_format.py the saved review's format, checks and conversion
apply_review.py saved review -> situations.json
test_review.py, test_review_page.mjs tests
review/<run>/ REVIEW.html, candidates.json, and situations.json once applied
```
Not committed (see `.gitignore`): the inference host's power and kernel logs, and caches that the
scripts rebuild on first use (`sim.pkl`, `null*.pkl`, `coassoc.json`). The embeddings are committed
(about 16 MB) because recreating them needs the inference host.
## Running
Everything runs from `probe/` with Python 3 and no third-party packages.
Anything that calls a model reads the host from the environment and never from a file:
```sh
export STP_OLLAMA=http://<inference-host>:11434
```
## Reviewing situations
The review happens in a web page. Nobody edits JSON.
```sh
cd probe
python3 review.py # k = 60, seed 24 -> review/k60-s24/REVIEW.html
```
1. **Open `review/k60-s24/REVIEW.html` in a browser** (double-click it; nothing to install). The first
screen explains the job with examples.
2. **One group per screen.** For each: keep it, drop it, or mark it the same as a group already kept;
untick scenes that don't fit; and fill in "A ___ wants ___ from ___", with the people named in its
scenes offered as click-to-fill words. The page shows as you go whether a group still has enough
scenes, and colours each group in the strip across the top. Keys: K keep, D drop, ← →.
3. **Progress saves in the browser** after every change, so you can close it and come back.
4. **"Finish & save"** lists anything unfinished and saves `review-k60-s24.json`, usually to Downloads.
5. **Then:**
```sh
python3 apply_review.py review/k60-s24
```
It finds the saved review in Downloads by itself, checks everything again, and names any problem the
way the page does ("Group 4: kept, but its name is not finished"). Nothing is written until the
review passes; then it writes **`situations.json`**: each situation's sentence and roles, the scenes
that show it with their stories, and which groups it came from.
`candidates.json` in the run directory is the program's record for the checker; leave it alone.
```sh
python3 -m unittest test_review # the saved review's checks, the page renderer, apply_review.py
node --test test_review_page.mjs # the rules the page applies as you review
```
The page's drawing code is only checked for syntax and for starting up; how it looks and behaves in a
real browser has to be tried by a person.
+891
View File
@@ -0,0 +1,891 @@
# story-to-pack — selecting situations
> **Decision (2026-09-15): selection is gate G2 followed by a structured human review.** `probe/review.py`
> writes `REVIEW.html`, a page where the reviewer keeps, drops or merges each group, unticks scenes and
> names the situation; the page saves a review file, and `probe/apply_review.py` checks it and writes
> `situations.json` for the next stage. See the README.
>
> The first version asked the reviewer to read a long Markdown file and edit JSON by hand. The first
> real attempt stopped before a single entry: what to decide was unclear, there was too much to read at
> once, hand-edited JSON broke easily, and the wording was hard to write. The page answers each of those.
> The record below is how the G2 decision was reached: four gate designs and two model-judged gates,
> each tested against blind labels.
>
> **Superseded in part (2026-09-15), see "Predicaments" at the end:** reading the page, the reviewer
> found the groups were topics, not situations.
The step after clustering: from the clusters of summarised scenes, choose the ones that are genuine
**recurring situations** — the raw material of a recurring event — and reject the rest. Companion to
`DESIGN.md`; this is the step its assumption-1 result called for.
**Status (2026-09-14): designed and pre-registered below, before any feature was computed or any
label was read against a feature.** Results are appended at the end, and nothing above the Results
heading is edited after the test runs, except to fix a typo.
---
## The problem, from the assumption-1 result
Summarise-then-embed broke the story-identity effect: clusters now draw from many stories. But of five
sampled tight-recurring clusters, one was a **vocabulary cluster** — fortune-tellers, adventurers, a
found tyre, a graft inquiry — held together by "seeks", "fortune", "mysterious", and mostly from three
stories. The concentration and tightness filters in `measure.py` let it through. A selector has to
reject clusters like that one and keep clusters like "a spouse schemes against or confronts a spouse
over infidelity".
## Constraints
- **No inference.** Selection uses only what is already on disk: the summary embeddings, the prose
embeddings, the summary text, and which story each scene came from. It is pure computation, like the
numeric search, so it can be re-run and tuned for free. (It also keeps the inference host free for
other work.)
- **Deterministic.** Seeded clustering; the same inputs give the same selection.
- **Pure Python**, like the rest of the probe.
## What a selected situation is
A set of scenes, from **at least 4 different stories**, with no story contributing more than 40%,
that a reader can name as **one social transaction**: who wants what from whom. It becomes one
recurring event; its member scenes become that event's description variants and option material.
Rejected: story-dominated clusters (already mechanical), **vocabulary clusters** (shared words, no
shared transaction), **register clusters** (shared tone or setting, e.g. "men walking through the
city"), and **near-duplicates** of an already-selected situation.
## Candidate signals
All computed per cluster, from existing vectors and text. Each carries a pre-registered prediction of
which way it moves for a genuine situation.
| # | Signal | Definition | Prediction for a situation |
| --- | --- | --- | --- |
| S1 | **Cross-story cohesion** | Mean summary-embedding cosine over member pairs from *different* stories | Higher |
| S2 | **Cross/within ratio** | S1 divided by the mean cosine over member pairs from the *same* story (1.0 if there are none) | Closer to 1 — a situation is as similar across stories as within one; a cluster held together by a few stories' scenes is not |
| S3 | **Effective stories** | Inverse Simpson index of the member story counts, divided by cluster size | Higher — members spread evenly rather than a few stories plus stragglers |
| S4 | **Prose agreement** | Mean *prose*-embedding cosine over cross-story member pairs, minus the corpus-wide mean cross-story prose cosine | Higher — a second, independent view: scenes of the same situation should read a little alike even in the original text |
| S5 | **Stability** | Mean pairwise co-association of members across 12 k-means runs (k from 50 to 100, several seeds): the share of runs in which two members land together | Higher |
| S6 | **Word dependence** | Share of members containing the cluster's single most over-represented content word (log-odds against the whole summary set, stopwords and "someone" excluded) | Higher for a **vocabulary** cluster — one word doing the work |
S6 is predicted to indicate the failure, the others to indicate success.
## Protocol
1. **Candidates.** Cluster the summary embeddings with k = 60. A candidate is a cluster with ≥ 5
members, ≥ 4 distinct stories and dominant share ≤ 40% (`measure.py`'s "recurring"; the relative
tightness filter is *not* applied — the selector replaces it).
2. **Two candidate sets from two partitions.** Development: seed 11. Test: seed 12.
3. **Blind labels.** Each set is dumped to a sheet showing, per cluster, only its member summaries in
shuffled order — no story titles, no scores, no sizes beyond the lines themselves. Each cluster is
labelled by reading:
- **2 — one situation:** one nameable social transaction that most members (roughly 70%+)
instantiate. The label records its name.
- **1 — broad:** a shared setting or theme, but the transaction varies across members.
- **0 — not a situation:** vocabulary, register, or nothing nameable.
The test sheet is labelled before any signal is computed for the test set.
4. **Choose the rule on development only.** Compute S1–S6 for the development set. Candidate rules
are single thresholds on one signal, or an AND of two. The chosen rule is the one with the highest
**precision** (share of accepted clusters labelled 2), subject to accepting at least half of the
development set's label-2 clusters; ties go to the simpler rule. Freeze it here, in writing, before
step 5.
*Detail fixed after the labels were written and before any signal was computed:* thresholds are
placed midway between adjacent observed development values, never on one. Ties beyond simplicity
break on more label-2 clusters accepted, then on precision for label ≥ 1. Two reference points
are reported but cannot be chosen: accepting every candidate, and `measure.py`'s tightness filter
(coherence at or above the partition's median sized cluster), called B0.
5. **Test once.** Apply the frozen rule to the test set. Report precision (label 2, and label ≥ 1),
recall of label-2 clusters, and the number accepted. No re-tuning after seeing test results; any
later change is a new rule and needs new data to be tested on.
6. **Near-duplicates.** Among accepted clusters, merge any pair whose centroids have cosine ≥ a
threshold chosen on development by reading the labels' names. Report how many merges happened on
the test set and whether each merged pair's names agree.
## Known limits of this test
- **The labels are one reader's judgement** — the model running the probe. They are written to a
file so the designer can spot-check or overrule them, and the verdict should be read with that in
mind.
- **Development and test come from the same corpus.** Two partitions of the same 838 scenes share
situations, so the test measures stability across clustering, not generalisation to a new corpus.
A genuinely held-out test needs a second corpus, which needs embedding, which needs the inference
host.
- **k is fixed at 60** for the test. The selector's behaviour at other k is not tested.
## Frozen rule (written before any test signal was computed)
Chosen on development (40 candidates, 5 labelled 2; must accept ≥ 3), by `probe/select_rule.py choose`:
> **Accept a candidate when S1 ≥ 0.6119 AND S6 ≤ 0.1603.**
> Cross-story cohesion at least 0.6119, and no single over-represented word in more than 16% of members.
| On development | Accepted | Precision (2) | Precision (≥ 1) | Recall (2) |
| --- | --- | --- | --- | --- |
| Accept every candidate | 40 | 0.12 | 0.62 | 1.00 |
| B0, tightness filter | 17 | 0.24 | 0.76 | 0.80 |
| S1 alone (best single) | 9 | 0.44 | 0.89 | 0.80 |
| **Frozen rule** | **5** | **0.60** | **1.00** | **0.60** |
Accepted on development: D04 (2), D09 (1), D13 (2), D16 (2), D33 (1). Rejected label-2: D15 (S1 0.603),
D30 (S6 0.20). Full leaderboard: `probe/choose-dev.log`; signals: `probe/signals-dev.json`.
Observations recorded now, so they cannot be shaped by the test:
- **This is very likely overfit.** Five positives, fine-grained thresholds, and an AND of two signals
is exactly the setting in which a rule memorises its development set. The test is what decides.
- **S1 does most of the work.** It is the best single signal by a wide margin, and it alone rejects
the vocabulary cluster that motivated this step (D18, S1 0.605).
- **S6 did not behave as predicted.** Its most over-represented word is usually a *story's* word
("juggler", "furs", "eviction"), not generic vocabulary, and D18's is low (0.11). It catches D39
("young", 1.00) — a vocabulary cluster of a different kind. Its contribution to the frozen rule is
a single cut, and may not carry over.
- **S2, S3 and S4 were weak on development**, and S5 middling. None of the predicted directions was
strongly wrong, but only S1 separated well.
**Near-duplicates (step 6): centroid cosine cannot do it, so no automatic merge is applied.** Among
the five clusters the rule accepts on development, no two share a situation, so there was nothing
there to calibrate on. Across all 26 development candidates labelled ≥ 1, the one clear duplicate —
D13 and D30, both "a run-in with the police" — has centroid cosine **0.9026**, while five pairs of
plainly different situations score higher, up to **0.9326** ("money and marriage" against "schemes,
cons and theft"). D05/D23 (artists) sit at 0.9005. Any threshold that merges the duplicate merges
different situations first. Summary embeddings place situations about money, romance and conflict
in one dense region, so closeness between centroids is not identity of situation.
Decision, fixed before the test: the test reports, **by label name**, whether any two accepted
clusters are the same situation, as a measurement of how often duplicates survive selection. A
merge method is an open problem; member overlap across partitions (co-association between two
clusters' members) is the candidate to try next, and is untested.
## Results
**Test run once, 2026-09-14,** on the seed-12 partition (41 candidates, 5 labelled 2), frozen rule
unchanged. Logs: `probe/signals-test.json`, `probe/apply-test.log`.
| On test | Accepted | Precision (2) | Precision (≥ 1) | Recall (2) |
| --- | --- | --- | --- | --- |
| Accept every candidate | 41 | 0.12 | 0.51 | 1.00 |
| B0, tightness filter | 18 | 0.17 | 0.44 | 0.60 |
| **Frozen rule** | **5** | **0.40** | **1.00** | **0.40** |
Accepted: T35 (2) coercion over money; T39 (2) spouse against spouse over infidelity; T05 (1) schemes
and cons over money; T07 (1) courtship; T25 (1) trying to impress or win someone's favour.
Rejected label-2: T16 newcomer seeks acceptance (S1 0.598), T17 proposal or engagement (S1 0.596),
T30 run-in with the police (S1 0.604, S6 0.23).
### Verdict
**The selector is precise and narrow.** Across both sets it accepted 10 clusters, and **all 10 are
situations or broad situational themes; none is a vocabulary, register or grab-bag cluster.**
Accepting everything gives about half that. The tightness filter used in the assumption-1 test is
*worse* than accepting everything on the test set (0.44 against 0.51) — it keeps vocabulary clusters,
which are tight.
**It does not find most of the situations.** Recall of label-2 clusters fell from 0.60 to 0.40, and
precision for label 2 from 0.60 to 0.40 — the expected overfitting. The rejected label-2 clusters sit
just under the S1 cut (0.596–0.604), so the threshold is doing real work at a place where situations
and non-situations overlap. **Five accepted clusters out of 41 is far short of the ~15 situations
per stage a pack needs**, so as it stands selection would make the sufficiency report fail on this
corpus.
**S6 earned its place on the test set, in the way it failed to on development.** Two test clusters are
held together by a single word — T10 "caliph" (S6 0.70) and T18 "young" (1.00) — and both have high
cross-story cohesion (S1 0.628, 0.626). S1 alone would accept them; S6 rejects them. (Observation made
after the test, not a re-tuning: S1 ≥ 0.6119 alone would accept 10 test clusters, 2 labelled 2 and 6 labelled ≥ 1.)
Its original motivation, D18, was caught by S1 instead. Both failure modes exist, and each signal
catches one.
**Duplicates among accepted clusters:** no two carry the same label name. T07 (courtship) and T25
(trying to impress or win someone's favour) overlap in substance; a reader could reasonably call them
one situation. Automatic merging remains unsolved (see step 6).
### What this means for the design
1. **Two-signal selection is a sound first gate, not a complete step.** It should run as a *filter
that is allowed to reject* — its precision is what matters for pack quality — followed by a
second stage that recovers recall.
2. **Recall is the next problem, and clustering is the likely cause, not selection.** The rejected
situations (police run-ins, proposals, social climbing) are recognisable but diluted by
off-situation members at k = 60. Candidates to test, all without inference: re-cluster the members
of rejected clusters at finer k and re-apply the gate; select from the union of several partitions,
using co-association to keep only members that consistently travel together; or grow an accepted
cluster's core by adding nearest cross-story members while S1 stays above the cut.
3. **Merging duplicates needs member overlap, not centroid distance.**
4. **These labels are one reader's.** Every number above rests on them. They are in
`probe/labels-dev.json` and `probe/labels-test.json`, with the sheets they were read from, and are
worth a designer's spot-check — especially the 1-versus-0 boundary, which is the least certain.
5. **The test is within one corpus.** Generalisation to a different corpus is untested and needs
embeddings, which needs the inference host.
---
## Recovering recall (pre-registered 2026-09-14, before any recovery method was run)
The gate is precise and keeps too few. Three ways to find more situations, compared on **fresh data**,
because the test set above is spent. Still no inference.
### Fixed before running
- **The gate does not change.** `S1 ≥ 0.6119 AND S6 ≤ 0.1603`, plus the candidate conditions (≥ 5
members, ≥ 4 stories, dominant ≤ 40%). "Passes the gate" below means all of these.
- **A property of the gate, noted in advance:** S6 counts a word only if at least two members share
it, so a cluster of fewer than 13 members fails whenever any content word repeats (2/12 = 0.167). In
effect the gate requires ~13 members. The methods are built to produce clusters of that size;
anything smaller is expected to fail, and that is the gate's behaviour, not a method's.
- **Fresh partition:** k = 60, **seed 13** — neither development (11) nor test (12).
- **Baseline (B):** the gate applied to seed 13's candidates, as before.
- **M1 — re-cluster the rejected.** Every scene not in a B-accepted cluster is pooled and re-clustered
with k-means, k = round(pool size / 20), seed 13. Result: B's accepted clusters plus pool clusters
that pass the gate.
- **M2 — consensus clustering.** Each scene is represented by its co-association row over the 12 cached
runs (k 50–100, seeds 21 and 22 — no overlap with 11, 12 or 13): how often it landed with every
other scene. Those rows are clustered with k-means, k = 60, seed 13. Result: clusters that pass the gate.
- **M3 — prune, then grow.** For each seed-13 candidate: if it fails the gate, repeatedly remove the
member with the lowest mean similarity to members from *other* stories, until it passes or falls to
13 members (then it is dropped). Then grow every passing cluster: add the non-member scene nearest
its centroid while the result still passes; stop at the first addition that would fail, or at 30
members. Scenes may end up in more than one M3 cluster; the overlap is reported.
### Evaluation
- Every cluster accepted by any method is pooled (identical member sets once), given a shuffled id, and
dumped to a blind sheet showing only member summaries. Which method produced a cluster is written
to a separate map file that is not read until labelling is finished.
- Labels use the same 2 / 1 / 0 scale. Names are reused verbatim from the development and test label
files wherever the situation is the same, so duplicate situations can be counted by name.
- Per method: clusters accepted; precision for label ≥ 1 and for label 2; **distinct situations**
(distinct names among label ≥ 1); scene coverage; overlap between accepted clusters.
- **Success, per method:** at least **twice** B's accepted clusters, **and** precision (≥ 1) of at least
**0.80**, **and** at least twice B's distinct situations. For scale: a two-stage pack wants ~30.
### Known limits
The reader has now read every summary several times; labels are blind to method and signals, not to
content. The methods and the gate are evaluated on one corpus.
### Results (2026-09-14)
Seed 13 gave 42 candidates. 21 distinct accepted clusters were labelled blind (`probe/labels-recover.json`,
3 labelled 2, 13 labelled 1, 5 labelled 0), then scored by `probe/evaluate_recover.py`:
| Method | Accepted | Precision (≥ 1) | Precision (2) | Distinct situations | Scene coverage | Success |
| --- | --- | --- | --- | --- | --- | --- |
| B, gate on candidates | 4 | 1.00 | 0.00 | 4 | 13% | — |
| M1, re-cluster the rejected | 4 | 1.00 | 0.00 | 4 | 13% | **fail** — 0 new |
| M2, consensus clustering | 3 | 1.00 | **1.00** | 3 | 5% | **fail** — too few |
| M3, prune then grow | 16 | 0.69 | 0.00 | 10 | 45% | **fail** — precision |
**No method meets the pre-registered bar.**
- **M1 adds nothing.** Re-clustering the 729 scenes outside B's clusters at k = 36 produced no cluster
that passes the gate. The gate's selectivity does not come from dilution by one bad partition.
- **M2 is the only method that produced sharp situations.** All three of its clusters are labelled 2
— a wealthy suitor weighed against love, a spouse against a spouse over infidelity, coercion by
threat — at 14–17 members, against B's 13–38. Consensus over many partitions isolates the scenes
that genuinely travel together. It produces too few clusters to be the selection step on its own.
- **M3 finds the most, and the gate cannot stop it diluting.** Pruning recovered 12 rejected
candidates and M3 reached 10 distinct situations (B: 4), but 5 of its 16 clusters are not
situations. **Growth was stopped by the 30-member cap in 10 of 16 clusters, not by the gate.**
That exposes the gate's second size effect: S6 is a share, so it gets *easier* to satisfy as a
cluster grows, and S1 moves slowly. The gate calibrated on k = 60 clusters does not constrain a
cluster that is being built up one scene at a time.
- **Duplicates survive.** Across methods, "a spouse against a spouse" appears three times (R006, R015)
and courtship twice within M3 (R008, R010); 68 scenes sit in more than one M3 cluster.
- **The seed-13 baseline itself accepted no label-2 cluster** (0 of 4), where development and test
each had 2–3. The baseline's precision for label 2 is unstable across partitions.
### What this means
The gate, as frozen, is **size-dependent in both directions**: it structurally rejects clusters under
~13 members and barely constrains clusters above ~25. Any method that changes cluster size — pruning,
growing, finer re-clustering — changes what the gate measures. Every recovery method was, in effect,
testing the gate at sizes it was not calibrated for.
Observation only, not pre-registered and not a result: B and M2 together would give 7 clusters,
6 distinct situations, all labelled ≥ 1, and 3 labelled 2.
Directions this points to, none tested:
1. **Make the gate size-invariant before anything else.** Replace S6's share with a count or a
significance test against the corpus, and judge S1 relative to what a random cluster of the same
size scores. Then re-validate — which needs fresh labels on fresh partitions.
2. **Use consensus (M2) to form clusters, not k-means.** It was the only source of sharp situations.
Running more co-association partitions (more seeds, more k) and extracting cores at several
agreement levels may raise its yield without losing precision.
3. **Grow only with a size-invariant stopping rule.** M3's recall is real; its stopping rule is not.
---
## A size-invariant gate, G2 (pre-registered 2026-09-14, before any G2 signal was computed)
### Why each old signal depended on size
- **S6** is a share with a floor of 2 members: 2/size. Under ~13 members any repeated word fails the
cut; above ~25 almost nothing does. A share is not evidence unless the count is improbable.
- **S1**'s *expectation* does not depend on size — it is a mean over pairs — but its *noise* does:
small clusters reach a high S1 by chance, which is part of why k-means' small clusters look tight.
- **Nothing looked at individual members.** Adding one off-situation scene to 30 moves S1 by
roughly a thirtieth of the difference, so growth was never stopped.
### G2 signals
| Signal | Definition | Why it is size-invariant |
| --- | --- | --- |
| **S1** | Unchanged: mean summary cosine over cross-story member pairs | Unbiased at any size |
| **Z1** | (S1 − μₘ) / σₘ, where μₘ and σₘ come from random member sets of the same size m (≥ 4 stories, dominant ≤ 40%): 300 samples for m ≤ 40, 120 above, sizes 5–150, seeded | Requires more evidence where chance does more |
| **MMIN** | The lowest, over members, of a member's mean cosine to members of *other* stories | A per-member quantity; one diluting member fails it at any size |
| **W** | Among words shared by ≥ 2 members, those whose count is significant by a hypergeometric upper-tail test at Bonferroni 0.01 over the corpus vocabulary; W is the largest share among them (0 if none) | Counts only improbable sharing, then measures how dominant it is |
Word extraction, stopwords and the corpus are identical to `signals.py`. Pairwise similarities are
precomputed once. Candidate conditions (≥ 5 members, ≥ 4 stories, dominant ≤ 40%) still apply.
### Choosing thresholds
- **Tuning set:** every labelled cluster so far — development (40), test (41) and the recovery pool
(21): 102 clusters, 63 labelled ≥ 1, sizes 5–39. All have been seen; none is used for validation.
- **Rule form:** S1 ≥ a AND Z1 ≥ b AND MMIN ≥ c AND W < d, where each term may be switched off. Grid:
a and c at the deciles of the tuning set's values or off; b ∈ {off, 1, 2, 3, 4, 5}; d ∈ {off, 0.25,
0.30, 0.35, 0.40, 0.50, 0.60}.
- **Objective:** the most tuning clusters labelled ≥ 1 accepted, subject to precision (≥ 1) of at least
**0.85**; ties on more label-2 accepted, then fewer active terms. The objective changes from the
first gate (which maximised label-2 precision) because the failure now is recall, and label 1 —
a situational theme — is usable material for a pack.
- The tuning report also gives, per size band (≤ 12, 13–25, ≥ 26), how many clusters the old and new
gates accept and at what precision.
### Validation on fresh data (nothing below has been computed)
- **V1, small clusters:** k = 100, seed 15; 30 candidates sampled at random (seed 15).
- **V2, large clusters:** k = 40, seed 14; 30 candidates sampled at random (seed 14).
- **V3, growth:** k = 60, seed 16; every candidate G2 accepts is grown by adding the non-member nearest
its centroid while the result still passes G2, up to 45 members. The stop reason — gate or cap — is
recorded.
- All V1 and V2 samples and all V3 grown clusters are pooled, deduplicated, shuffled and labelled blind,
with the set and method in a separate map file.
- **Success, all three required:**
1. G2's precision (≥ 1) is at least **0.80 on V1 and on V2 separately**, and G2 accepts at least 3
clusters in each.
2. In V1, G2 accepts **more** small clusters than the old gate.
3. In V3, **most growths are stopped by the gate, not the cap**, and grown clusters have precision
(≥ 1) of at least 0.80.
The old gate is scored on V1 and V2 alongside, for comparison.
### Frozen G2 (written after tuning, before any validation cluster was built)
`probe/tune_gate2.py`, log `probe/tune-gate2.log`. (Correction: the tuning set has 62 clusters
labelled ≥ 1, not 63.)
> **Accept a candidate when S1 ≥ 0.6037 AND Z1 ≥ 5 AND W < 0.60.** MMIN is off.
| Tuning set, 102 clusters | Accepted | Label ≥ 1 | Precision (≥ 1) | Label 2 |
| --- | --- | --- | --- | --- |
| Accept all | 102 | 62 | 0.61 | 13 |
| Old gate | 31 | 26 | 0.84 | 8 |
| **G2** | **40** | **34** | **0.85** | **9** |
| Size band | Clusters (≥ 1) | Old gate: accepted / P1+ | G2: accepted / P1+ |
| --- | --- | --- | --- |
| ≤ 12 | 20 (7) | 0 / — | 1 / 1.00 |
| 13–25 | 52 (34) | 13 / 0.92 | 18 / 0.89 |
| ≥ 26 | 30 (21) | 18 / 0.78 | 21 / 0.81 |
**Observations and predictions, recorded before validation:**
- **G2 is not size-invariant at the small end, and the objective chose that.** Z1 ≥ 5 is a fixed bar
on a statistic whose value, for a fixed true cohesion, *grows* with size (σₘ shrinks as m grows).
It replaces S6's structural rejection of small clusters with a milder one: 1 small cluster accepted
instead of 0. **Prediction: criterion 2 passes narrowly or fails, and V1 yields fewer than 3
accepted clusters, failing criterion 1.**
- **MMIN, the signal designed to stop dilution, was not selected**, and W's cut (0.60) is loose enough
to act only on near-total single-word clusters. Growing a cluster raises Z1 and barely moves S1.
**Prediction: most V3 growths run to the cap, failing criterion 3.**
- The gain over the old gate is real but modest, and it is in the mid and large bands: 8 more clusters
labelled ≥ 1 accepted at unchanged precision.
- The thresholds are not changed in light of these predictions. The validation tests the rule the
pre-registered procedure produced.
### Validation results (2026-09-14)
73 fresh clusters labelled blind (`probe/labels-validate.json`: 6 labelled 2, 33 labelled 1, 34
labelled 0), scored by `probe/validate_gate2.py score` (log `probe/validate-score.log`).
| Set | Sampled (≥ 1) | Old gate: accepted / ≥ 1 / P1+ | G2: accepted / ≥ 1 / P1+ |
| --- | --- | --- | --- |
| V1, k = 100 | 30 (13) | 3 / 2 / 0.67 | **10 / 8 / 0.80** (2 labelled 2) |
| V2, k = 40 | 30 (16) | 1 / 1 / 1.00 | 4 / 3 / 0.75 |
| V3, grown | 13 | — | 13 grown: **11 stopped by the cap**, 2 by the gate; 10 ≥ 1, P1+ 0.77 |
| Criterion | Result |
| --- | --- |
| 1. P1+ ≥ 0.80 and ≥ 3 accepted, on V1 and V2 | **fail** — V1 passes (0.80, 10), V2 does not (0.75, 4) |
| 2. G2 accepts more V1 clusters than the old gate | **pass** — 10 against 3 |
| 3. V3 mostly stopped by the gate, P1+ ≥ 0.80 | **fail** — 2 of 13, 0.77 |
| **Overall** | **fail** |
By size, across V1 and V2 together:
| Size | Old gate: accepted (≥ 1) | G2: accepted (≥ 1) |
| --- | --- | --- |
| ≤ 9 | 0 | **0** — including two clusters labelled 2 (5 and 6 members) |
| 10–12 | 0 | 3 (3) |
| 13–25 | 3 (2) | 7 (5) |
| ≥ 26 | 1 (1) | 4 (3) |
| All | 4 (3), P1+ 0.75 | 14 (11), P1+ 0.79 |
### Verdict on G2
**G2 is a real improvement on the old gate, and it is not size-invariant.** It accepts three to four
times as many clusters at slightly better precision, and it opens the 10–25 member range the old gate
all but closed. Both pre-registered predictions of failure held:
- **The small end is still closed below 10 members.** Z1 ≥ 5 is unreachable for a cluster of 5–9
however cohesive it is. The two sharpest small clusters on the sheet — a fare dispute (5 members,
label 2) and a spouse against a spouse (6 members, label 2) — were both rejected.
- **Growth is still unconstrained.** Growing a cluster raises Z1 and barely moves S1, so both gates
accept a 45-member cluster as readily as a 15-member one. The one per-member signal built to stop
this, MMIN, was not selected by the tuning objective.
- **Large clusters are still mostly rejected** (4 of 30 in V2), including clear themes such as the
saloon, dining rooms and illness.
**What was learned about size-invariance itself:** a fixed threshold on a z-score is not size-invariant;
it reverses the old bias rather than removing it. A size-invariant cohesion test needs a threshold
that scales with the null — for example a fixed *percentile* of the same-size random distribution
combined with an effect-size floor on S1 — and a stopping rule for growth has to be a per-member
test that the tuning cannot switch off. Both are untested proposals, and testing them needs another
fresh partition and another round of blind labels, because every cluster labelled so far has now
informed a design decision.
---
## G3: a percentile of the same-size null, and a mandatory member floor (pre-registered 2026-09-14, before any G3 signal was computed)
### Changes from G2
- **Z1 is replaced by PCT**: the fraction of same-size random member sets (≥ 4 stories, dominant ≤ 40%)
whose S1 is below the cluster's. 1,000 samples per size for 5–60 members, 200 for 61–150, seeded.
A percentile threshold asks "is this cluster as unusual as the top x% of random sets *of its own
size*", which a z-score with a fixed bar does not.
- **S1 floor is mandatory.** At large sizes the null is narrow and PCT saturates at 1.0 for any
cluster slightly above average, so an effect-size floor has to carry the weight there.
- **MMIN is mandatory**: every member's mean cosine to members of other stories must reach the floor.
It is the growth stop; tuning chooses its value but cannot switch it off.
- **W** is unchanged from G2 and remains optional.
### Tuning
- **Tuning set: all 175 labelled clusters** — development 40, test 41, recovery 21, G2 validation 73.
- **Rule form:** S1 ≥ a AND PCT ≥ p AND MMIN ≥ c AND W < d. a and c at the tuning set's deciles (no "off");
p ∈ {0.90, 0.95, 0.975, 0.99, 0.999}; d ∈ {off, 0.30, 0.40, 0.50, 0.60}.
- **Objective unchanged from G2:** most clusters labelled ≥ 1 accepted, subject to precision (≥ 1) of at
least 0.85; ties on more label-2 accepted, then fewer active terms.
**Risk named in advance:** MMIN is a minimum over per-member means. Small clusters have noisier
member means but fewer of them; large clusters have steadier means but more chances to contain one
low member. Its net size effect is not predictable, and it could re-introduce a size bias of its own.
### Validation on fresh partitions (seeds never used before)
- **V0, very small:** k = 150, seed 20; 25 candidates sampled (seed 20).
- **V1, small:** k = 100, seed 17; 20 sampled (seed 17).
- **V2, large:** k = 40, seed 18; 20 sampled (seed 18).
- **V3, growth:** k = 60, seed 19; every G3-accepted candidate grown toward its nearest scene while it
passes G3, cap 45, stop reason recorded.
- Pooled, deduplicated, shuffled and labelled blind as before. The old gate and G2 are scored on the
same clusters.
**Success, all four required:**
1. G3 precision (≥ 1) ≥ **0.80** and ≥ 3 accepted, on V1 and on V2 separately.
2. **Small clusters:** among V0 and V1 clusters of ≤ 9 members, G3 accepts **at least 2**, with precision
(≥ 1) of at least **0.67**, and more than G2 accepts.
3. **Growth:** most V3 growths stopped by the gate, and grown clusters have precision (≥ 1) ≥ 0.80.
4. G3 accepts at least as many clusters labelled ≥ 1 across V0–V2 as G2, at precision no lower than G2's.
### Frozen G3 (written after tuning, before any validation cluster was built)
`probe/tune_gate3.py`, log `probe/tune-gate3.log`.
> **Accept a candidate when S1 ≥ 0.6175 AND PCT ≥ 0.90 AND MMIN ≥ 0.5296 AND W < 0.60.**
| Tuning set, 175 clusters (101 ≥ 1, 19 label 2) | Accepted | Label ≥ 1 | Precision (≥ 1) | Label 2 |
| --- | --- | --- | --- | --- |
| Old gate | 47 | 38 | 0.81 | 9 |
| G2 | 67 | 55 | 0.82 | 12 |
| **G3** | **47** | **40** | **0.85** | **13** |
| Size band | Clusters (≥ 1) | Old: accepted/≥ 1 | G2 | G3 |
| --- | --- | --- | --- | --- |
| ≤ 9 | 26 (5) | 0/0 | 0/0 | **4/3** |
| 10–12 | 21 (11) | 0/0 | 4/4 | 4/4 |
| 13–25 | 75 (47) | 17/15 | 26/22 | 17/17 |
| ≥ 26 | 53 (38) | 30/23 | 37/29 | 22/16 |
**Observations and predictions, recorded before validation:**
- **G3 is the first gate to accept any cluster under 10 members** (4 on tuning, 3 of them ≥ 1).
**Prediction: criterion 2 passes.**
- **The percentile threshold settled at its loosest value, 0.90, and the S1 floor rose** from G2's 0.6037
to 0.6175. The floor, not the percentile, is doing most of the work at mid and large sizes, and G3
accepts 15 fewer large clusters than G2 on tuning. **Prediction: criterion 4 fails** — G3 finds fewer
clusters labelled ≥ 1 than G2 (40 against 55 on tuning), though at higher precision. Criterion 1 is
at risk on V2 for the same reason.
- **MMIN at 0.53 is the growth stop.** Whether a nearest-to-centroid scene falls below it is not
predictable from tuning, which contains no grown-under-G3 clusters. No prediction for criterion 3.
- The trade is visible already: G3 exchanges recall at the large end for precision everywhere and for
admission at the small end. Whether that is the right trade for a pack is a design question the
validation cannot answer.
### Validation results (2026-09-14)
75 fresh clusters labelled blind (`probe/labels-validate3.json`: 4 labelled 2, 24 labelled 1, 47
labelled 0), scored by `probe/validate_gate3.py score` (log `probe/validate3-score.log`).
| Set | Sampled (≥ 1) | Old: acc / ≥ 1 / P1+ | G2: acc / ≥ 1 / P1+ | G3: acc / ≥ 1 / P1+ |
| --- | --- | --- | --- | --- |
| V0, k = 150 | 25 (5) | 2 / 1 / 0.50 | 8 / 4 / 0.50 | 11 / 4 / 0.36 |
| V1, k = 100 | 20 (5) | 1 / 0 / 0.00 | 1 / 1 / 1.00 | 4 / 2 / 0.50 |
| V2, k = 40 | 20 (12) | 0 / 0 / — | 4 / 4 / 1.00 | 2 / 2 / 1.00 |
| ≤ 9 members, V0 + V1 | 27 (4) | 1 / 0 / 0.00 | 3 / 1 / 0.33 | 11 / 3 / 0.27 |
| V0–V2 pooled | 65 (22) | 3 / 1 / 0.33 | **13 / 9 / 0.69** | 17 / 8 / 0.47 |
| V3, grown under G3 | 10 | — | — | 7 stopped by the cap, 3 by the gate; P1+ 0.60 |
**All four criteria fail**, and G3 is worse than G2 on fresh data: more clusters accepted, fewer of
them situations. G3's gain on the tuning set (P1+ 0.85, 13 label-2) did not survive a new partition.
### Why G3 failed — measured, not inferred
- **PCT carries no information.** Across all 175 tuning clusters, 173 reach PCT ≥ 0.90 and 141 reach
1.0 — including 72 of the 74 clusters labelled 0. **The random-set null is the wrong null.** k-means
groups each scene with its nearest neighbours, so every cluster it produces is far tighter than a
random set of scenes of the same size, whether or not it is a situation. A cohesion test has to ask
whether a cluster is tighter than *other clusters of its size*, not than random scenes.
- **MMIN barely separates labels and favours small clusters.** Median MMIN is 0.536 for label 0,
0.543 for label 1 and 0.564 for label 2; by size it is 0.547 for ≤ 9 members, 0.537 for 10–25 and
0.552 for ≥ 26. A minimum over few, noisy per-member means is not reliably lower for small clusters,
so the floor admitted 11 small clusters of which 8 are not situations. This is the risk recorded
before tuning.
- **Growth is still not stopped by the gate** (7 of 10 to the cap). A nearest-to-centroid scene
typically clears a member floor calibrated on whole clusters.
### Where this leaves selection
Across three gates and five blind-labelled evaluations, **G2 remains the best**: pooled precision
(≥ 1) of 0.79 on its own validation and 0.69 here, with the fewest non-situations admitted.
None of the three is size-invariant, and none stops growth.
Two conclusions look robust enough to build on:
1. **Cohesion alone does not identify a situation.** Clusters labelled 0, 1 and 2 overlap heavily on
every cohesion statistic tried (S1, Z1, PCT, MMIN). What separates the best-labelled clusters is
visible to a reader — one transaction — and is not a geometric property of summary embeddings at
the resolution tried.
2. **The labelling budget is the binding constraint.** Each gate needs fresh partitions and 70–80 new
blind labels to test honestly, and every labelled cluster so far has informed a design. Continuing
to iterate gate designs on this corpus without a different kind of evidence is unlikely to converge.
Candidate directions, none tested: a null built from **k-means clusters of the same size on
shuffled-story data** rather than random scenes; a selection step that uses the **inference model to
name each cluster's transaction** and checks agreement across members (needs the GPU); or accepting
G2 as the gate and moving the problem downstream to event construction, where a human pass over
~40 candidate clusters is cheap compared with the labelling this probe has already required.
---
## G4: the model reads the cluster (pre-registered 2026-09-14, before any model call)
The finding above is that what makes a cluster a situation is visible to a reader and not to the
geometry. So let a reader decide — the inference model — and measure it against the blind labels.
### Method
- For each cluster, up to **15 member summaries** (a seeded sample when larger), shuffled and numbered,
go to the model in one call with the prompt below. It returns JSON: the single social transaction
most of them clearly show, or NONE, and the numbers of the summaries that clearly show it.
- **FIT** = (number of valid summary numbers listed) / (number shown); 0 when the answer is NONE or
the reply cannot be parsed. Parse failures are counted and reported.
- **Gate: accept when FIT ≥ θ**, plus the usual candidate conditions (≥ 5 members, ≥ 4 stories,
dominant ≤ 40%), which every labelled cluster already meets.
- Deterministic: temperature 0, 2,048-token context, `format: json`. One request at a time, as before.
- **The prompt contains no example of a situation** — the 3B model copied the examples it was given
in the summarisation pilot.
Prompt (system), fixed:
> You read short summaries of scenes from different stories and decide whether they share one kind
> of social situation.
>
> A social situation is a specific transaction between people: who wants what from whom, and what
> is at stake. Describe it in your own words.
>
> Rules:
> - Find the single situation that the largest number of summaries clearly show.
> - It is normal for many summaries not to fit. Include a summary only if it clearly shows that
> situation.
> - A shared place, mood, profession or word is not a situation.
> - If no situation is clearly shown by at least three summaries, the situation is NONE.
>
> Reply with JSON only: {"situation": "<at most 10 words, or NONE>", "fits": [<numbers of the
> summaries that clearly show it>]}
The user message is the numbered list. For `qwen3:14b` it ends with `/no_think`, and any think block
is stripped before parsing.
### Models
- **Primary: `qwen2.5:3b-instruct`** — the design requires a small local model; the verdict rests on it.
- **Secondary: `qwen3:14b`** — reported, to show whether capability is what limits the result.
### Data — no new labels
The model is never fitted to labels; the only fitted quantity is θ.
- **θ is chosen on development (40 clusters)** only, with the objective used for G2 and G3: most
clusters labelled ≥ 1 accepted, subject to precision (≥ 1) ≥ 0.85; ties on more label-2 accepted,
then the higher θ. Grid: θ ∈ {0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}.
- **Primary evaluation: the G2 and G3 validation sets, 148 clusters**, which tuned neither G2 nor G4,
so G2 can be compared fairly on them. **Secondary:** test and recovery, 62 clusters.
- Identical member sets appearing in more than one file are judged once.
- **Pilot, allowed once:** the first 5 development clusters, to check the JSON parses. If more than 1
of 5 fails to parse, one prompt revision is allowed and recorded here before any other call.
### Success, on the 148-cluster primary set, with the 3B model
1. Precision (≥ 1) at least **0.80**.
2. **More** clusters labelled ≥ 1 accepted than G2 accepts on the same set.
3. Among accepted clusters of ≤ 9 members, precision (≥ 1) at least 0.67, if there are at least 3.
Also reported: G4 AND G2; FIT distribution by label; the situations named for accepted clusters
against the label names; and the 14B results against the same bar.
**Pilot (21:23):** 5 of 5 replies parsed with the 3B model, so the prompt is unchanged. About 1.6 s
per cluster. Noted without drawing a conclusion from five: the situations named are often generic
("Confrontation and hidden motives" for D04, a label-2 spouse-against-spouse cluster, FIT 0.47), and
a label-1 argument cluster scored FIT 0.87.
### Results, `qwen2.5:3b-instruct` (21:24–21:29, 245 distinct clusters in 286 s, 1 parse failure)
Log `probe/evaluate-judge-3b.log`. θ chosen on development: **0.6** (9 accepted, P1+ 0.89).
| Primary set, 148 clusters (67 ≥ 1) | Accepted | ≥ 1 | Label 2 | P1+ | Recall |
| --- | --- | --- | --- | --- | --- |
| Accept all | 148 | 67 | 10 | 0.45 | 1.00 |
| **G2** | **49** | **36** | 5 | **0.73** | **0.54** |
| G4, FIT ≥ 0.6 | 50 | 24 | 4 | 0.48 | 0.36 |
| G4 AND G2 | 18 | 11 | 1 | 0.61 | 0.16 |
On the secondary set (62) G4 reaches P1+ 0.71 against G2's 0.82. **All three criteria fail**
(P1+ 0.48; 24 ≥ 1 against G2's 36; 13 accepted small clusters at P1+ 0.23).
**FIT does not separate the labels:** mean 0.39 for label 0, 0.49 for label 1 — and **0.42 for label
2**, lower than for label 1. Precision at the primary set (0.48) is barely above accepting everything
(0.45).
**Why, visible in the replies:** the 3B model names situations at a level of abstraction where almost
anything fits — "confrontation", "power dynamics in social hierarchies", "tension between parties",
"coercion and manipulation" — and then lists most summaries as fitting it. A 5-member cluster labelled
0 scored FIT 1.00 as "power struggle/conflict". When it is specific it is often right ("marital
conflict" for a spouse-against-spouse cluster, "con or scam", "argue over payment"), but specificity
is not what FIT rewards. The rule against shared place, mood and profession was followed; a rule
against *abstraction* was never written, and the model supplies abstraction freely.
### 14B pilot and the one allowed revision
The `qwen3:14b` pilot failed to parse **5 of 5**: every reply's content was empty. The model spent its
whole 200-token budget in a separate thinking channel (Ollama returns it outside `content`), and the
`/no_think` suffix did not stop it. **Revision, the one allowed by the pilot rule, affecting only the
14B run:** requests set Ollama's `think: false` and allow 300 output tokens. The prompt text,
sampling, θ procedure and success criteria are unchanged. The five empty results were deleted before
re-running the pilot.
Re-run pilot: **5 of 5 parsed**, about 2.4 s per cluster. The situations named are more specific than
the 3B model's ("Wealthy person pressures someone into marriage", "Struggling artist facing societal or
personal challenges"), though an abstract one still scored FIT 0.80 ("conflict between individuals with
differing perspectives").
### Results, `qwen3:14b` (21:31–21:45, 245 clusters in 832 s, 0 parse failures)
Log `probe/evaluate-judge-14b.log`. θ chosen on development: **0.6** (8 accepted, P1+ 0.88).
| Primary set, 148 clusters (67 ≥ 1) | Accepted | ≥ 1 | Label 2 | P1+ | Recall |
| --- | --- | --- | --- | --- | --- |
| Accept all | 148 | 67 | 10 | 0.45 | 1.00 |
| **G2** | **49** | **36** | 5 | **0.73** | **0.54** |
| G4 (14B), FIT ≥ 0.6 | 42 | 22 | 4 | 0.52 | 0.33 |
| G4 (14B) AND G2 | 10 | 10 | 1 | 1.00 | 0.15 |
By size, G4 (14B) on the primary set: ≤ 9 members 17 accepted, P1+ 0.29; 10–25, 14 accepted, P1+ 0.50;
≥ 26, 11 accepted, **P1+ 0.91**. Secondary set (62): G4 P1+ 0.60 against G2's 0.82.
**All three criteria fail with the 14B model as well** (P1+ 0.52; 22 ≥ 1 against G2's 36; 17 small
clusters accepted at P1+ 0.29).
FIT now rises with the label — mean 0.40, 0.47, 0.52 for labels 0, 1, 2 — where the 3B model's did
not, so capability helps. But the separation is small next to the spread, and the 14B model still
names abstractions that fit almost anything ("Someone wants something from someone else", "Power
dynamics between individuals", "Someone holds power over someone else"), most often for small
clusters.
GPU host during both runs: no faults logged; peak 284.9 W (1 s), mean 163 W under load, 65 °C, PCIe
Gen 3 x4 throughout. Logs in `probe/gpu-logs/*2122*`.
### Verdict on G4
**Neither model's judgement is a better gate than G2**, and the pre-registered verdict (on the 3B
model) is a fail. The failure is specific and repeatable: asked for "the single situation most
summaries share", a model answers at whatever level of abstraction makes the most summaries fit, and
FIT rewards exactly that. Small clusters make it worse, because with 5–9 summaries an abstraction that
covers four or five of them is always available.
Two observations, recorded as observations because neither was a pre-registered criterion:
- **G4 (14B) AND G2 accepted 10 clusters on the primary set, and all 10 are labelled ≥ 1.** The two
gates fail in different ways — G2 admits cohesive clusters that are not situations, the model admits
abstract "situations" that are not cohesive — and their intersection is precise. It is also
narrow: recall 0.15.
- **For clusters of 26 or more, the 14B model's precision is 0.91.** The abstraction problem is a
small-cluster problem: 15 shown summaries rarely all fit one vague phrase *and* get listed.
What would test the obvious next step, none of it done: a prompt that asks for the situation **in
terms of roles and a concrete want** and rejects answers without both; scoring the *specificity* of
the named situation (for example, requiring the named situation to be closer to the member summaries
than to the corpus average in embedding space); and G2 AND G4 evaluated as a pre-registered gate on a
fresh partition, since every labelled cluster now informs this observation.
---
## Predicaments (recorded 2026-09-15, before any model call)
### What the first real review found
The designer opened `REVIEW.html` for run k60-s24 and read four groups closely:
- **Group 1 is artists.** It holds at least three different situations: an artist in a new place using
the art to get on, being paid for the work against what the work is worth, and plain money trouble.
- **Group 2 is police and court officers.** Inside it: being coerced, with the police watching or
arriving; and a crime that is stopped or found out.
- **Group 6 is flirting and infatuation.**
- **Extra group 1 is room and board**: hotels, dinners and the like.
None of these is "one person wanting something specific from another". Checked across all 20 groups,
most are held together by who is in the scenes or where they happen: *artist* in 44% of group 1's
scenes, *policeman* in 54% of group 3's, *uncle* and *lawyer* in group 4, *marriage* and *wedding* in
group 8, *restaurant* and *customers* in group 12, *hotel* in 41% of extra group 1.
**Why every earlier test missed it.** A topic group is as cohesive and as spread across stories as a
situation group, so cohesion, story spread, word dominance and the model's own judgement all passed it.
Summaries keep jobs and places, and the embeddings group on them. It took a person reading the groups.
### Decisions (designer, 2026-09-15)
1. **The unit is a predicament**, not a two-person transaction: what the main person is up against and
what they want out of it, as The Ladder's existing events are (the rent bouncing, the mail cart).
The sub-themes the designer found inside groups 1 and 2 are this kind of thing.
2. **Re-describe every scene as a predicament, then regroup**, rather than splitting topic groups by hand.
### Method, fixed before any call
- `probe/redescribe.py` gives the model each scene's own prose (not its summary) and the prompt in that
script: one sentence, at most 20 words, no names, jobs, places, businesses or story-specific objects,
saying what the person is up against and what they want. No example of the output vocabulary.
- Names are then removed deterministically (`strip_names.py`), the sentences embedded (`embed.py`), and
the embeddings clustered at k = 60.
- **Pilot first:** the first 40 scenes, with both `qwen2.5:3b-instruct` and `qwen3:14b`, read by the
designer side by side before any full run. The full run uses the model the designer prefers.
### How it is judged
- **Topic share** (`probe/topic_share.py`): for each candidate group, the share of its scenes whose
original summary contains the group's most common job, relationship or place word; median over groups
and seeds 11–13 at k = 60. The baseline on the summary embeddings is measured before any call and
recorded below. **Pass: the median falls to half the baseline or less**, with at least 30 candidate
groups remaining at k = 60 (enough material for a two-stage pack).
- **Leakage:** how many predicament sentences still contain a job, relationship or place word. Reported,
not gated.
- **The real test is the designer reading the regrouped top groups** in the review page: most should
read as one predicament. Numbers alone passed topic groups before.
### Baseline (measured 2026-09-15, before any model call)
A first measurement counted words for a person with no role at all ("man", "woman", "girl"), and scored
one group 90% topic-bound for saying "man". Those words (`GENERIC_PEOPLE` in `probe/topic_words.py`)
were taken out of the measure, and the baseline re-measured, still before any predicament was generated.
`python3 topic_share.py summary_embeddings.json` (`probe/topic-share-baseline.log`):
| Seed | Candidate groups | Median top topic-word share |
| --- | --- | --- |
| 11 | 40 | 16% |
| 12 | 41 | 20% |
| 13 | 42 | 22% |
| **Median** | **41** | **20%** |
Most topic-bound at seed 11: *wife* 47% of 15, *dinner* 47% of 15, *policeman* 40% of 15 and 38% of 16,
*brother* 38% of 8, *artist* 36% of 11. **Pass mark for the predicament groups: median 10% or less, with
at least 30 candidate groups.**
### Pilot (2026-09-15)
First 40 scenes, one request at a time on the GPU host (3B 39 s, 14B 111 s; no GPU faults; logs in
`probe/gpu-logs/*-2026-09-15-1803*`). Read side by side in `probe/pilot-compare.html` (`pilot_page.py`).
| | qwen2.5:3b-instruct | qwen3:14b |
| --- | --- | --- |
| Has a job, relationship or place word | 15 of 40 | 13 of 40 (mostly *wife*, *husband*) |
| Mean words (limit 20) | 14 | 25; 34 of 40 over |
| Most common opening | "The Kid seeks…" ×3 | "A man struggles…" ×6 |
3B mostly retells the plot, keeping names, objects and places. 14B writes something close to a
predicament but ignores the length limit and repeats a few openings. **Designer's read: scenes 1–10,
14B dramatically better in every one; stopped there. The full run uses qwen3:14b.**
Found while reading: `split.py` skips a title written in quotation marks, so three stories were
merged into the story before them. "Little Speck in Garnered Fruit" is inside "Dougherty's
Eye-Opener", "The Guilty Party" inside "A Harlem Tragedy", and "What You Want" inside "The Duel". The
corpus has 97 stories, not 94. Separately, `strip_names.py` treats *York*, *Manhattan* and *Broadway*
as personal names, which gives "New someone" in 29 of 838 summaries.
### Corrections before the full run (2026-09-15, designer's go-ahead)
The v1 data and every G1–G4 record stay as they were, in `probe/`. The corrected corpus is built in
`probe/v2/`, running the same scripts from that directory.
- **Split.** `split.py` accepts a quoted title and matches a contents entry by the part before a dash
(“THE GUILTY PARTY”—AN EAST SIDE TRAGEDY). Result: **97 stories, 839 scenes**. Exactly the three
missing stories were added, nothing was lost, and only their three host stories changed.
- **Summaries carried over.** 829 of the 839 scenes have text identical to a v1 scene and keep that
v1 summary verbatim (`carry_summaries.py`). The other 10 are summarised with the v1 model and prompt.
- **Name stripper.** New York places (New York, Manhattan, Broadway, Harlem, the Bowery, and a few
others) become "the city" before names are looked for, and days and holidays are never names. Months
stay ambiguous ("May" is a person in scene 5) and are left alone.
- **The baseline is re-measured on v2** (the v2 summaries, re-embedded), because both fixes change
what is embedded. It is recorded below before any predicament is read.
- **No trimming to 20 words.** Tried on the 14B pilot: cutting at the last clause break within 20
words removed what the person is up against in most cut sentences ("…with a promise of future money"
without "but his dishonesty sparks a violent confrontation"), and 10 of 40 had no clean break.
The full 14B sentence is embedded.
### Result (2026-09-15)
Full run: 839 scenes re-described by `qwen3:14b`, one request at a time, 37 minutes on the GPU host
(peak 270 W, mean 136 W, 65 °C, no faults; logs in `probe/gpu-logs/*-2026-09-15-1831*`). Sentences
average 25 words. 219 of 839 still contain a job, relationship or place word, most often a relationship
(*wife*, *husband*) — reported, not gated.
`topic_share.py`, k = 60, seeds 11–13, both measured on the v2 corpus:
| | Candidate groups | Median top topic-word share |
| --- | --- | --- |
| v2 summaries (baseline, `topic-share-baseline-v2.log`) | 45 | **20%** |
| Predicaments (`topic-share-predicaments.log`) | 42 | **13%** |
**The pre-registered pass mark is not met.** It asked for half the baseline or less (10%); the measure
fell by about a third, from 20% to 13%. The group-count mark is met (42 ≥ 30). The most topic-bound
groups are also much weaker than the baseline's: the worst predicament group is *room* at 40% of 5
scenes, against the baseline's *bride* at 62% of 8 and *policeman* at 60% of 20.
Not a pass and not a wash, so the decision goes to the reading test that this whole step exists for:
`groups_page.py` builds `v2/groups-predicaments-k60-s24.html`, which lists each group's predicament
sentences with each scene's original summary beneath, most cohesive group first, and asks the designer
to mark each group **one predicament**, **two or three mixed**, or **not a predicament**. The openings
repeat ("A man must" 45 times, "A man struggles" 37), so the risk to watch for is a group held together
by that phrasing rather than by a predicament.
+16
View File
@@ -0,0 +1,16 @@
test: 41 candidates, 5 labelled 2
FROZEN RULE S1 >= 0.6119 AND S6 <= 0.1603
accepted 5 P2 0.40 P1+ 1.00 R2 0.40
baseline accept all candidates accepted 41 P2 0.12 P1+ 0.51 R2 1.00
baseline B0: tightness filter accepted 18 P2 0.17 P1+ 0.44 R2 0.60
accepted:
T05 label 1 schemes, cons and theft over money
T07 label 1 courtship
T25 label 1 trying to impress or win someone's favour
T35 label 2 coercion over money: threats, bribes, demands for payment
T39 label 2 a spouse schemes against or confronts a spouse over infidelity
rejected label-2:
T16 a newcomer seeks acceptance in a social circle
T17 a proposal or engagement, accepted or refused
T30 a run-in with the police
+77
View File
@@ -0,0 +1,77 @@
"""Check a saved review and turn it into situations.json.
python3 apply_review.py review/k60-s24 # finds the saved review itself
python3 apply_review.py review/k60-s24 ~/Downloads/review-k60-s24.json
Without a file named, it uses the newest review-<run>*.json in the run directory, and
otherwise the newest one in ~/Downloads, where a browser saves it. It prints every problem
at once and writes nothing until the review passes; then it writes situations.json into the
run directory.
"""
import datetime, json, os, pathlib, sys
import review_format as rf
def downloads_dir():
return pathlib.Path(os.environ.get('STP_DOWNLOADS', pathlib.Path.home() / 'Downloads'))
def find_review(run_dir, run_id):
for folder in (run_dir, downloads_dir()):
if folder.is_dir():
found = sorted(folder.glob(f'review-{run_id}*.json'), key=lambda p: p.stat().st_mtime, reverse=True)
if found:
return found[0]
return None
def main(argv):
if len(argv) not in (2, 3):
print(__doc__.strip())
return 2
run_dir = pathlib.Path(argv[1])
cand_path = run_dir / 'candidates.json'
if not cand_path.exists():
print(f'{run_dir} has no candidates.json. Check the folder name, or run review.py first.')
return 1
candidates = json.loads(cand_path.read_text(encoding='utf-8'))
run_id = candidates['run']['id']
review_path = pathlib.Path(argv[2]).expanduser() if len(argv) == 3 else find_review(run_dir, run_id)
if review_path is None or not review_path.exists():
print(f'No saved review found for run {run_id}.')
print(f'Open {run_dir / "REVIEW.html"} in a browser, and at the end press "Save review file".')
print(f'It saves review-{run_id}.json into Downloads, where this looks for it.')
return 1
print(f'reading {review_path}')
try:
form = json.loads(review_path.read_text(encoding='utf-8'))
except json.JSONDecodeError:
print('That file is damaged and cannot be read. Save the review again from REVIEW.html; '
'your progress is still in the browser.')
return 1
errors, warnings, result = rf.validate(candidates, form)
for w in warnings:
print(f'note: {w}')
if errors:
print(f'\nThe review is not finished. {len(errors)} thing(s) to sort out in REVIEW.html, '
f'then save the review file again:')
for e in errors:
print(f' - {e}')
return 1
result['applied'] = datetime.datetime.now().astimezone().isoformat(timespec='seconds')
result['review_file'] = str(review_path)
out = run_dir / 'situations.json'
out.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding='utf-8')
c = result['counts']
print(f'\n{out}: {c["situations"]} situations ({c["kept"]} kept, {c["same"]} merged in, '
f'{c["dropped"]} dropped, {c["left_out"]} extras left out).')
for s in result['situations']:
print(f' {s["id"]} {s["situation"]} ({s["size"]} scenes, {s["stories"]} stories)')
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv))
@@ -0,0 +1,824 @@
{
"D01": [
105,
232,
241,
242,
248,
271,
277,
336,
387,
390,
418,
451,
465,
528,
568,
709,
778,
782,
790,
820
],
"D02": [
63,
81,
222,
240,
251,
281,
296,
297,
386,
481,
544,
551,
589,
745
],
"D03": [
20,
57,
65,
125,
135,
178,
196,
198,
326,
353,
359,
361,
364,
373,
375,
380,
381,
459,
519,
540,
543,
639,
640,
642,
692,
739,
779,
799,
802,
819,
833
],
"D04": [
41,
127,
163,
204,
205,
302,
304,
305,
322,
475,
483,
590,
761,
763,
770
],
"D05": [
73,
82,
156,
157,
158,
225,
389,
430,
485,
486,
487,
488,
514,
653,
787
],
"D06": [
84,
85,
86,
87,
88,
106,
141,
162,
255,
290,
306,
314,
319,
325,
383,
384,
385,
394,
413,
450,
523,
658,
707
],
"D07": [
37,
40,
42,
62,
151,
154,
175,
199,
201,
215,
218,
223,
254,
264,
267,
269,
279,
345,
348,
350,
352,
354,
370,
371,
453,
479,
521,
567,
595,
700,
815,
821,
822,
826
],
"D08": [
75,
76,
246,
591,
645,
666,
670,
789
],
"D09": [
133,
134,
144,
168,
192,
207,
276,
376,
379,
399,
461,
517,
545,
619,
625,
649,
731,
805,
809,
810
],
"D10": [
7,
8,
96,
99,
171,
217,
219,
250,
253,
414,
433,
436,
502,
554,
570,
689
],
"D11": [
21,
104,
124,
138,
140,
208,
263,
282,
330,
331,
369,
377,
401,
437,
440,
492,
518,
569,
629,
633,
634,
636,
637,
638,
641,
643,
646,
687,
727,
736,
804,
811,
831,
832,
834,
835
],
"D12": [
160,
480,
495,
627,
628,
706,
729,
730,
750,
807,
816
],
"D13": [
11,
173,
265,
367,
398,
407,
500,
511,
512,
515,
696,
698,
699,
718,
771,
830
],
"D14": [
109,
164,
249,
270,
315,
316,
374,
539,
541,
562,
564,
583,
671,
708,
717,
719,
743,
747
],
"D15": [
9,
103,
110,
111,
112,
228,
259,
288,
312,
448,
469,
477,
478,
557,
616,
620,
621,
623,
624,
711,
737,
786,
827,
829
],
"D16": [
38,
121,
129,
148,
200,
363,
647,
663,
680,
694,
725,
726,
769
],
"D17": [
14,
292,
346,
524,
525,
527,
529,
575,
664,
744
],
"D18": [
172,
224,
236,
237,
243,
247,
323,
412,
417,
420,
443,
445,
447,
449,
695,
775,
777,
818
],
"D19": [
98,
142,
226,
238,
262,
309,
338,
349,
351,
404,
419,
602,
659,
665,
764
],
"D20": [
6,
68,
116,
145,
318,
548,
667,
668,
793,
795
],
"D21": [
28,
67,
136,
177,
185,
260,
272,
278,
494,
598,
600,
604,
632,
688,
704,
705,
713,
746,
783,
796
],
"D22": [
12,
18,
22,
29,
44,
53,
54,
66,
69,
128,
131,
187,
227,
230,
231,
285,
287,
303,
347,
392,
397,
406,
410,
482,
538,
542,
560,
572,
574,
662,
710,
766,
785,
814
],
"D23": [
123,
176,
391,
516,
652,
654,
655,
791,
792,
794,
800
],
"D24": [
27,
74,
77,
150,
165,
180,
229,
234,
235,
252,
273,
308,
321,
355,
358,
382,
415,
452,
460,
508,
520,
558,
566,
577,
650,
660,
672,
679,
728,
733,
773,
774,
776
],
"D25": [
5,
17,
46,
58,
59,
60,
78,
139,
212,
221,
342,
362,
372,
378,
435,
439,
489,
522,
537,
571,
582,
587,
685,
734
],
"D26": [
33,
209,
210,
211,
213,
216,
310,
311,
337,
396,
499,
741,
753
],
"D27": [
23,
174,
261,
266,
298,
299,
360,
368,
509,
576,
603,
607,
644,
735,
806
],
"D28": [
0,
1,
2,
4,
143,
220,
274,
466,
505,
712,
732,
837
],
"D29": [
51,
61,
97,
130,
132,
147,
149,
153,
182,
239,
257,
472,
473,
474,
484,
536,
561,
618,
828
],
"D30": [
3,
56,
101,
214,
408,
409,
411,
441,
471,
476,
510,
597,
599,
701,
702
],
"D31": [
47,
70,
113,
170,
275,
343,
416,
421,
422,
424,
470,
497,
549,
563,
565,
683,
749,
754,
755,
784,
808,
836
],
"D32": [
19,
24,
25,
100,
159,
506,
559,
605,
673,
674,
675,
676,
677,
684,
781
],
"D33": [
137,
186,
301,
356,
425,
427,
596,
606,
608,
631,
651,
657,
691,
738,
803
],
"D34": [
32,
108,
126,
146,
365,
530,
531,
532,
533,
534,
535,
588,
592,
593,
615,
681,
682,
720,
722,
723,
724,
751,
767,
768
],
"D35": [
16,
26,
39,
52,
55,
71,
107,
166,
167,
258,
289,
291,
400,
426,
446,
493,
550,
573,
584,
586,
622,
648,
661,
686,
690,
697,
801,
817
],
"D36": [
102,
152,
203,
284,
327,
797
],
"D37": [
10,
80,
115,
161,
313,
317,
357,
395,
444,
498,
513,
556,
594,
693,
703,
740,
765,
825
],
"D38": [
50,
64,
83,
114,
122,
206,
307,
341,
429,
431,
434,
496,
714,
715,
716,
772
],
"D39": [
13,
43,
45,
48,
49,
120,
244,
339,
340,
344,
464,
578,
579,
580,
581,
610,
611,
612,
613,
614
],
"D40": [
36,
79,
117,
118,
119,
155,
393,
428,
432,
504,
507,
546,
547,
553,
780
]
}
@@ -0,0 +1,805 @@
{
"T01": [
90,
162,
178,
188,
292,
443,
503,
658,
811,
812
],
"T02": [
116,
125,
214,
227,
261,
288,
312,
363,
375,
448,
478,
547,
593,
594,
647,
702,
703,
711,
721,
786,
829,
831,
832
],
"T03": [
240,
277,
279,
280,
281,
342,
447,
685,
782,
820,
821
],
"T04": [
3,
73,
74,
110,
111,
113,
115,
161,
271,
272,
309,
336,
395,
444,
600,
679,
693,
696,
698,
765
],
"T05": [
37,
58,
75,
151,
172,
173,
175,
177,
199,
201,
218,
236,
251,
264,
265,
267,
323,
328,
361,
367,
370,
371,
415,
479,
500,
540,
543,
777,
815,
822
],
"T06": [
140,
146,
183,
184,
186,
190,
195,
197,
402,
403,
404,
405,
410
],
"T07": [
18,
21,
39,
44,
53,
106,
107,
126,
160,
167,
285,
373,
392,
400,
406,
482,
532,
560,
574,
608,
622,
627,
642,
710,
783,
785,
801,
816,
830
],
"T08": [
103,
134,
165,
360,
477,
545,
571,
576,
577,
602,
625,
632,
729,
810
],
"T09": [
9,
71,
114,
132,
133,
144,
166,
182,
192,
238,
239,
256,
257,
259,
289,
303,
341,
356,
419,
480,
501,
550,
557,
561,
617,
618,
619,
620,
621,
623,
624,
648,
651,
660,
661,
665,
691,
730,
750,
751,
805,
807,
809
],
"T10": [
86,
337,
358,
359,
364,
435,
439,
440,
441,
650
],
"T11": [
108,
325,
326,
484,
531,
558,
615,
795,
800
],
"T12": [
27,
168,
180,
207,
378,
399,
452,
461,
506,
517,
598,
603,
659,
674,
677,
688,
731,
742,
749,
806,
808
],
"T13": [
297,
343,
344,
345,
348,
533,
534,
555,
744
],
"T14": [
5,
46,
60,
78,
139,
174,
196,
212,
221,
230,
333,
362,
369,
372,
423,
489,
492,
522,
537,
569,
587,
609,
635,
641,
643,
734,
735,
797
],
"T15": [
25,
77,
245,
252,
275,
276,
306,
317,
322,
355,
416,
421,
422,
424,
490,
520,
538,
544,
549,
551,
559,
566,
605,
649,
683,
728,
754,
774,
776,
825,
826,
836
],
"T16": [
23,
24,
26,
64,
65,
136,
143,
185,
202,
494,
496,
606,
798
],
"T17": [
17,
22,
30,
35,
47,
127,
170,
269,
283,
287,
380,
381,
401,
426,
427,
526,
542,
556,
585,
596,
631,
639,
646,
692,
736,
755,
779,
799,
802,
803
],
"T18": [
19,
43,
45,
48,
49,
159,
339,
340,
365,
578,
579,
580,
581,
610,
611,
612,
613,
614
],
"T19": [
15,
331,
366,
377,
379,
529,
626,
634,
636,
637,
638,
644,
687
],
"T20": [
96,
217,
219,
220,
250,
274,
313,
432,
433,
502,
505,
689
],
"T21": [
232,
528,
582,
668,
675,
676,
684
],
"T22": [
222,
329,
425,
568,
775
],
"T23": [
7,
8,
124,
171,
206,
253,
282,
308,
316,
338,
398,
414,
418,
420,
434,
436,
466,
504,
509,
514,
553,
554,
564,
570,
712,
714,
716,
741
],
"T24": [
142,
446,
565,
584,
588,
686,
697,
704,
732
],
"T25": [
13,
16,
20,
28,
29,
88,
135,
141,
266,
332,
368,
495,
573,
586,
604,
607,
628,
629,
630,
690,
727,
748,
759,
784,
817,
818,
819
],
"T26": [
189,
191,
193,
194,
203,
204,
205,
247,
263,
284,
592,
595,
669,
695,
709,
804
],
"T27": [
79,
98,
99,
109,
164,
249,
270,
374,
539,
541,
562,
583,
664,
671,
719,
740,
743,
773
],
"T28": [
181,
346,
347,
349,
350,
353,
460,
518,
535,
722,
726,
737,
738,
814
],
"T29": [
155,
233,
234,
262,
546,
780,
834
],
"T30": [
11,
243,
315,
407,
511,
512,
515,
589,
699,
700,
720,
723,
747
],
"T31": [
31,
32,
34,
296,
530,
681,
682,
724,
725,
752,
766,
767,
768
],
"T32": [
76,
100,
105,
128,
235,
237,
246,
327,
382,
397,
472,
473,
474,
483,
507,
548,
645,
666,
667,
670,
678,
708,
717,
718,
770,
788,
789,
790,
792,
793,
828
],
"T33": [
69,
70,
80,
101,
129,
226,
396,
408,
470,
471,
476,
497,
498,
499,
510,
527,
563,
597,
599,
701,
769,
771,
813
],
"T34": [
4,
118,
229,
351,
417,
449,
450,
656,
657,
772,
781
],
"T35": [
10,
12,
38,
42,
59,
62,
121,
324,
352,
354,
409,
662,
680,
694,
739,
823
],
"T36": [
123,
209,
254,
521,
791
],
"T37": [
67,
68,
104,
137,
169,
231,
260,
273,
278,
298,
321,
429,
431,
493,
508,
572,
705,
713,
733,
745,
746,
796
],
"T38": [
57,
61,
63,
66,
97,
120,
130,
147,
150,
198,
242,
244,
314,
438,
445,
464,
519,
536,
601,
640,
824,
827,
833
],
"T39": [
40,
41,
72,
131,
163,
187,
268,
293,
301,
302,
304,
305,
391,
411,
475,
567,
590,
663,
756,
762,
763
],
"T40": [
122,
299,
412,
707,
778
],
"T41": [
82,
102,
156,
157,
158,
225,
290,
430,
485,
486,
487,
488,
516,
652,
653,
654,
655,
787,
794
]
}
@@ -0,0 +1,42 @@
"""Carry v1 scene summaries over to a re-split corpus, summarising only scenes whose text is new.
cd v2 && STP_OLLAMA=http://host:11434 python3 ../carry_summaries.py ../summaries.json summaries.json
The v1 split merged three quoted-title stories into their neighbours (SELECTION.md, "Pilot"). Re-splitting
renumbers every later scene but leaves almost every scene's text identical, so a scene whose text matches
a v1 scene keeps its v1 summary verbatim, and only the rest go to the model, with summarise.py's model
and prompt. Output has summarise.py's layout, in the new scene order, with 'carried' marking which is which.
"""
import json, pathlib, re, sys, time
import ollama
src, dst = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
old = json.loads(src.read_text(encoding='utf-8'))
old_chunks = json.loads((src.parent / 'chunks.json').read_text(encoding='utf-8'))
chunks = json.loads(pathlib.Path('chunks.json').read_text(encoding='utf-8'))
# summarise.py runs on import, so its prompt is read from the file; it must be the one v1 was made with.
code = (pathlib.Path(__file__).parent / 'summarise.py').read_text(encoding='utf-8')
SYSTEM = re.search(r'SYSTEM = """(.*?)"""', code, re.S).group(1)
END = re.compile(r'(?<!\bMr)(?<!\bMrs)(?<!\bMs)(?<!\bDr)(?<!\bSt)(?<!\bNo)[.!?](?:\s|$)')
if SYSTEM != old['prompt']:
sys.exit('summarise.py prompt differs from the one v1 summaries were made with')
by_text = {old_chunks[x['chunk']]['text']: x for x in old['summaries']}
out, fresh, t0 = [], 0, time.time()
for i, c in enumerate(chunks):
prev = by_text.get(c['text'])
if prev:
out.append({'chunk': i, 'story': c['story'], 'summary': prev['summary'],
'model_output': prev['model_output'], 'carried': prev['chunk']})
continue
text, _, _ = ollama.chat(old['model'], SYSTEM, c['text'])
t = re.sub(r'\s+', ' ', text).strip().strip('"')
m = END.search(t)
out.append({'chunk': i, 'story': c['story'], 'summary': t[:m.end()].strip() if m else t,
'model_output': text, 'carried': None})
fresh += 1
print(f' new scene {i} ({c["title"]}): {out[-1]["summary"]}', flush=True)
dst.write_text(json.dumps({'model': old['model'], 'prompt': SYSTEM, 'summaries': out}, indent=1), encoding='utf-8')
print(f'{len(out)} summaries: {len(out) - fresh} carried from {src}, {fresh} new, {time.time() - t0:.0f}s')
@@ -0,0 +1,108 @@
# Situation catalogue: classify, don't cluster
Built 2026-09-17 from the three hand references: O. Henry (`../v2/claude/`), Wharton (`../wharton/claude/`)
and Jacobs (`../jacobs/claude/`). This is the step that replaces embed-and-cluster in the proposed pipeline:
a model assigns each scene's predicament one catalogue entry, or `none`.
## Files
| File | What |
| --- | --- |
| `catalogue_src.py` | The 36 entries (definition, includes, excludes, tone, recurs) and `SOURCES`, which maps every hand group in the three references to exactly one entry. Running it checks the mapping and writes the rest. No inference. |
| `catalogue.json` | The entries, with up to three worked examples per corpus, each tagged with its corpus |
| `labels-<corpus>.json` | Every scene's catalogue label from the hand grouping, or `none`. This is the answer key for scoring |
| `recurrence.log` | One recurrence rule applied to all three: at least 5 scenes, at least max(3, 4% of the stories) stories, and no story over 50% |
| `classify.py` | Classifies one scene per request through `../ollama.py`. Leave-one-corpus-out: the examples never come from the corpus being classified. `--k 0` offers the whole catalogue |
| `score.py` | Scores classifier output against `labels-*.json`. `--selftest` checks the scorer on hand-vs-hand and on random labels |
| `emb/` | Cached nomic-embed-text vectors for the catalogue and the O. Henry 14B predicaments |
| `out/` | Classifier output |
## Results so far
**1. One rule for all three corpora** (`recurrence.log`). O. Henry keeps 16 recurring situations and
Wharton keeps 16. Jacobs falls from its hand-counted 19 to **6**, because its groups had used a two-story
minimum. With 12 stories, three stories a group is the least that means "recurs", and most of its
groups do not reach it. Only `rival`, `strings` and `newcomer` are found in all three corpora; ten more
are in two. A 20-30 story corpus is a real floor.
**2. Clustering fails even on the best text.** My 839 hand-written O. Henry predicaments were
embedded exactly as `embed.py` embedded the 14B ones (`../v2/claude/predicament_embeddings-claude.json`),
and the same k = 60 k-means was run on them (`../v2/claude/compare-claude-embeddings.log`). The groups
agree with my own hand grouping at **ARI 0.05** (0.05-0.05 over four seeds), no better than the 14B text's
0.07. The fault is the embed-and-cluster step, not the predicament text. Better rewriting will not
rescue clustering.
**3. Embeddings cannot even shortlist.** With the 14B predicaments against the catalogue definitions, the
hand label is the nearest entry for 13% of situated scenes, is in the top 8 for 50%, and is in the top 20
for 83%. So `classify.py` now offers the whole catalogue (`--k 0`), with the catalogue in the system prompt
so the server can reuse the cached prefix.
**4. Classifier pilot: 40 scenes only, too small to judge** (`pilot40.log`; qwen3:14b, shortlist k = 8,
14B predicaments). The model called 34 of 40 scenes situated where the hand labels have 14. Where both
called a scene situated, its label was right in 3 of 13 (23%). Kappa was 0.09. The prompt has since been
changed: the whole catalogue, a note to expect about half `none`, and a fit rating from 1 to 3 that
`score.py --minfit=` can threshold. The changed prompt has not been measured.
**4b. The changed prompt, on 30 scenes before the host hung** (`out/ohenry-q14-dev1.partial.json`, scored
in `pilot40.log`): it tells situated scenes from `none` better (precision 67%, recall 100%, kappa 0.23), but
where both call a scene situated, its label is right in only 2 of 14. Reading the confusions, some of its
labels are defensible. I labelled a whole story arc with one situation (every scene of *A Lickpenny Lover* is
`station`), while the model judges each scene alone (`pretence`, `unspoken`). And the 14B predicaments
("A man struggles to reconcile his pride with...") are too vague to classify from. So the next development
round should (a) also give the classifier the scene summary, or the predicaments either side, and (b) score
at the level a pack uses: which situations recur, with what members, rather than scene-exact agreement.
## Not done: the inference host's GPU fell off the bus
At **22:19:07 on 2026-09-17**, the inference host's GPU fell off the PCIe bus (kernel: `Xid 79`, then `Xid 154, GPU Reset
Required`). It happened in the middle of a `classify.py` development request whose prompt was about 2,450 tokens (the
whole catalogue). Over the ten minutes before, calls had slowed from about 2 s a scene to about 20 s. This is the
second such fault; the first was on 2026-09-13. **No power or link loggers were running**, because SSH was refused
while the key was out of the agent, so there is no evidence either way on power. After the fault, Ollama carried on
on the CPU. That is why nomic-embed-text still answered, and finding 2 was computed on CPU embeddings, which does
not change the method. All client processes were stopped. The host needs a reboot (reset required). The NVIDIA
driver asks for `nvidia-bug-report.sh` to be run as root before the module is unloaded.
The prompt design matters for this host: offering the whole catalogue makes every call a long prefill. Either keep
the catalogue as a cached prefix (the current `classify.py`, which puts it in the system prompt; untested on the
GPU), or shortlist with something better than embeddings, or run with a lower power limit and the loggers on.
### Third fault, with the loggers running: 2026-09-18 01:00:03
After a reboot, with the power limit set to 200 W (from 280 W) and all three loggers running, a 20-scene pilot
(`classify.py ohenry q14 --limit 20 --tag=-dev1`, catalogue as the cached system prompt) lost the GPU about 15 s in.
The logs are in `gpu-logs/*-2026-09-18-0056.*`. From the link/power CSV:
| Time | Power | Utilisation | Notes |
| --- | --- | --- | --- |
| 00:59:54 | 309.31 W | 49% | enforced limit 200 W; throttle reason 0x4 (SW power cap) |
| 00:59:55 to 00:59:59 | about 178 to 202 W | 100% | sustained prefill |
| 01:00:02 | 174.96 W | | last good sample |
| 01:00:03 | | | "GPU is lost"; dmon shows `-` from here |
The temperature never went above 39 °C, so the cause is not thermal. The link was PCIe gen 3 x4. The card drew
309 W for a sample despite a 200 W cap, and then dropped after about ten seconds of load at the cap. That points to
power delivery to the card or the OCuLink dock, not to the prompt or the software. A lower cap did not prevent it.
**No more classification runs on that host until its hardware has been looked at.** The pilot output is not a result.
`gpu-logs/nvidia-bug-report-2026-09-18.log.gz` is the driver's report, taken after the fault. The kernel log in it
shows no AER or PCIe error messages at any fault. That is not evidence of a clean link: at boot the kernel logs `_OSC: platform does not support [SHPCHotplug AER LTR DPC]`, so the firmware never gives the OS AER control and link errors would not be reported. There
was an `Xid 32` (corrupted push-buffer stream) from Ollama at 00:54:48, five minutes before this drop, and an `Xid 13`
graphics exception on 2026-09-13, before the first drop.
## Next, once the host is healthy
The plan below is fixed before any of the runs, so they cannot be tuned after the fact.
- **Development slice:** O. Henry scenes 0-119. Tune the prompt and the `--minfit` threshold only here.
(`classify.py ohenry q14 --limit 120 --tag=-dev1`)
- **Test:** O. Henry scenes 120-838, run once (`--start 120 --tag=-test`). Score with
`score.py out/ohenry-q14-test.json --range=120:839`. Then run the same prompt on the hand-written
predicaments (`ohenry claude`) to separate rewrite quality from classification quality. Then run
`wharton claude` and `jacobs claude`, which have no local-model predicaments yet, to see how the
catalogue transfers.
- **What counts as success** is precision per situation, not the number of situations that pass:
`--selftest` shows random labels also "pass" all 16. The bar is set before the test run: for the
situations both hand and model call recurring, a median model precision of at least 60%.
- At about 2 s a scene, O. Henry takes about 30 minutes a text set. That is a long run, so start the
loggers first.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,271 @@
"""The situation catalogue: a closed list a model classifies scenes against, instead of clustering them.
Seeded from three hand-made references (O. Henry ../v2/claude, Wharton ../wharton/claude, Jacobs ../jacobs/claude).
Every hand group in those references maps to exactly one catalogue entry (SOURCES). Running this file:
1. checks the mapping is total and every entry has at least one source,
2. writes catalogue.json (entries + worked examples, tagged by corpus so a classifier can leave a corpus out),
3. writes labels-<corpus>.json: every scene's catalogue label from the hand grouping, or "none",
4. applies ONE recurrence rule to all three corpora and writes recurrence.log.
No inference.
"""
import collections, json, math, pathlib, sys
HERE = pathlib.Path(__file__).resolve().parent
PROBE = HERE.parent
E = lambda id, name, definition, includes, excludes, tone='any', recurs=True: dict(
id=id, name=name, definition=definition, includes=includes, excludes=excludes, tone=tone, recurs=recurs)
CATALOGUE = [
E('rival', 'A rival for the same person',
'Someone else wants the person you want, and only one of you can have them.',
'suitors competing; fear of losing a partner to someone else; temptation to strike at the rival',
'competing for money or a prize that is not a person (your_scheme)'),
E('strings', 'Help with strings attached',
'Help, money or a chance is offered to you, but only on terms that will cost you something you value.',
'family money conditional on obeying them; a backer who now wants something; a windfall with a hidden cost',
'the terms require doing something you think wrong (compromise_for_money); a stranger\'s trap (sure_thing)'),
E('compromise_for_money', 'Money for going along',
'You can have money, success or a meal if you go along with something you think wrong or false.',
'selling out your standards for pay; the client wants the flattering or fake version; a compromising offer when you are hungry',
'the cost is only inconvenient, not wrong (strings)'),
E('sure_thing', 'The sure thing',
'A plausible person offers an easy fortune that is probably a trap set for you.',
'confidence tricks aimed at you; too-good-to-be-true bargains; strangers who befriend you for your money',
'you are the one running the scheme (your_scheme)'),
E('your_scheme', 'Your scheme turns on you',
'You are running a scheme or quietly tipping a deal your way, and it may be about to turn on you.',
'cheating a partner; rigging a contest; a trick coming apart in front of the people you meant to fool',
'someone else is scheming against you (sure_thing)'),
E('after_your_money', 'People after your money',
'You have money, or are thought to, and people around you are suddenly courting it.',
'heirs circling; flatterers and hangers-on; pressure to reveal a hidden windfall',
'a single stranger\'s con (sure_thing)'),
E('past_comes_back', 'The past comes back',
'Someone from your past, or your partner\'s, turns up with a claim or a hold that threatens your present life.',
'blackmail over an old secret; a former spouse or intimate who still has a claim; an old associate who could expose you',
'you simply discover something a person close to you hid (what_they_hid)'),
E('hiding_from_close', 'Hiding it from someone close',
'You are hiding something, and someone close to you keeps innocently coming near it.',
'keeping a guilty or embarrassing secret from a spouse or family; near misses',
'the secret belongs to someone else (their_secret)'),
E('their_secret', 'Keeping someone else\'s secret',
'You have been drawn into another person\'s secret and must decide whether to lie, stay silent or give it up.',
'covering for a friend or employer; being questioned by a suspicious superior',
'your own secret (hiding_from_close)'),
E('what_they_hid', 'What they hid',
'You discover that a person close to you has been hiding something, and must decide what it changes.',
'a partner\'s concealed past or disappointment; a friend\'s lie revealed',
'the hidden thing arrives as a claim or threat from outside (past_comes_back)'),
E('pretence', 'Passing for more than you are',
'You are pretending to be more, or other, than you are, to impress someone, and may be found out.',
'dressing and talking above your station; invented titles or histories; a thrilling story nobody can check',
'you are genuinely above or below the other person and must decide what to show (station)'),
E('caught_out', 'Caught out',
'You have been caught, or are about to be seen, doing or being what you meant nobody to see.',
'a private arrangement about to be judged publicly; caught somewhere you should not be; a joke taken in earnest',
'you are blamed for something you did not do (wrongly_judged)'),
E('wrongly_judged', 'What it looked like',
'Someone whose opinion matters has judged you by appearances, for something you did not do, and will not hear you out.',
'an innocent moment misread; suspected of a theft; taken for a fraud because of how you look',
'you really did it (caught_out)'),
E('let_another_take_blame', 'Someone else is taking the blame',
'Suspicion has fallen on someone innocent for what you did, and you could let it stay there.',
'an innocent person punished for your act; a stranger\'s blunder gives you a scapegoat',
'you are the innocent one (wrongly_judged)'),
E('cant_prove_it', 'Sure, but cannot prove it',
'You are sure who wronged you, and you cannot prove it.',
'accusing without evidence; authorities brushing you off; trying to trap the suspect',
'you are the one accused (wrongly_judged)'),
E('station', 'Across the gap in station',
'Someone well above or below you in station wants you, and each of you must decide how much to show of where you really stand.',
'courting or marrying across class; ashamed of your origins before a grander partner; a rich suitor not believed',
'pretending to a station you do not have (pretence)'),
E('home_unseen', 'Unseen at home',
'You and a partner at home have stopped really seeing each other, and one of you wants to be noticed.',
'neglect; taking a spouse for granted; a partner whose blind approval or coarseness makes nothing matter',
'open quarrels about blame (peace_at_home)'),
E('peace_at_home', 'Keeping the peace at home',
'Someone at home blames you or demands their way, and you must decide whether to give way to keep the peace.',
'being held responsible for household trouble; backing down before a spouse or elder; household quarrels',
'loneliness without conflict (home_unseen); an outsider in the house (unwelcome_in_home)'),
E('unwelcome_in_home', 'An unwelcome guest',
'Someone you do not want has settled into your home, and you cannot seem to make them go.',
'a relative or intruder who stays; having to make polite room for someone who holds power over the household',
'a former partner\'s claim (past_comes_back)'),
E('power_over_you', 'Under someone\'s thumb',
'Someone with power over you treats you with contempt, or must be humoured though you resent them, and you want respect or to turn the tables.',
'a superior who humiliates you; having to be civil and helpful to someone you fear or dislike because of your position',
'a well-meaning benefactor (benefactor)'),
E('benefactor', 'Help nobody asked for',
'Someone with money wants to do good to someone below them on their own terms: either you are offered such help, or you are the one offering it.',
'charity with a sermon or a price in pride; reformers who stop you working; a rich person seeking gratitude or restitution',
'a conditional offer between equals (strings)'),
E('patron', 'A handshake from the top',
'Someone powerful could be seen with you, and getting that notice will cost you.',
'currying favour with a great man; spending to be seen with the important; wanting a patron at your wedding',
'being humiliated by them (power_over_you)'),
E('pressed_to_wrong', 'Pressed to do wrong',
'Someone is pressing you to do something you believe is wrong or dangerous, including turning on a friend you owe.',
'pressure to testify against or turn in a friend; a loved one begging you to do something wicked; being invited into a crooked scheme',
'you are merely paid to compromise your standards (compromise_for_money)'),
E('go_between', 'Carrying someone else\'s case',
'You are asked to carry another person\'s claim or warning to a third party, knowing it may cost you with one side or the other.',
'pleading a case you doubt; passing on an unwelcome message; warning an outsider about a relative',
'keeping a secret (their_secret)'),
E('sacrifice', 'Giving it up for them',
'You can help or protect someone you care for only by giving up something of your own, and they may never know.',
'selling a treasure for a gift; secret humble work; giving your savings or name away for another',
'being paid to give something up (strings)'),
E('owning_up', 'Owning up',
'You once wronged someone, and now face them, or a stand-in, and must decide how much to admit or how to make amends.',
'confession; seeking forgiveness; a public show of devotion to win someone back',
'an innocent is blamed right now for what you did (let_another_take_blame)'),
E('own_words_back', 'Held to your own words',
'A principle, promise or rule you once used for your own ends is now turned against you.',
'hoist by your own code; a promise you never expected to be called in', '', recurs=False),
E('newcomer', 'New to the place',
'You are new to a place or household that ignores you, warns you, or leaves you lost, and you must find your footing.',
'arriving in the city; a new post with warnings about the person in charge; waking somewhere strange',
'news from the place you left (homecoming)'),
E('homecoming', 'Word from home',
'The people you left want you back, and word of them has reached you.',
'a messenger from home; a parent waiting; a reunion with an old friend from where you came from',
'a claim or threat from the past (past_comes_back)'),
E('dread', 'Something in the dark',
'Something frightening may be close by, and you must decide whether to face it.',
'fear of what a wish or deed has brought; noises in the dark; a thing you must undo before it arrives',
'ordinary social danger', tone='horror'),
E('waiting', 'Waiting on their decision',
'Your hopes hang on a decision entirely in another\'s hands, and nothing you do seems to hurry it.',
'waiting for a proposal, a verdict or a signal; pretending indifference while it hangs',
'you are the one who cannot speak (unspoken)'),
E('unspoken', 'You cannot say it',
'Someone you want is waiting for you to say it, and you cannot find the words or the moment.',
'shy lovers; a single short chance to speak; feelings hidden behind a business arrangement',
'waiting for the other to decide (waiting)'),
E('overlooked', 'Beside the admired one',
'You are the plain or overlooked one beside someone admired, and want, once, to be noticed.',
'the plain co-worker; going to dances on someone else\'s arm',
'a partner at home who ignores you (home_unseen)'),
E('sign', 'A sign',
'Someone in a bad way wants a sign to tell them what to do, and one seems to have turned up.',
'fortune-tellers, omens, mysterious messages, reading a coincidence as meant for you',
'real supernatural danger (dread)'),
E('despised', 'The one nobody thinks much of',
'Someone everybody looks down on wants a chance, and may be the only one who can help.',
'a tramp, a foreigner, a crook or a failure who turns out to be the helper',
'someone powerful (patron)'),
E('habit', 'The old habit',
'An old habit gives you away at exactly the moment you need to be someone new.',
'a former trade\'s manner resurfacing; an old craving at the worst moment',
'', recurs=False),
]
# every hand group in the three references -> one catalogue id
SOURCES = {
'ohenry': {'pose': 'pretence', 'benefactor': 'benefactor', 'taken_for_granted': 'home_unseen', 'station': 'station',
'newcomer': 'newcomer', 'easy_money': 'sure_thing', 'sacrifice': 'sacrifice', 'rival': 'rival',
'price': 'compromise_for_money', 'despised': 'despised', 'loyalty': 'pressed_to_wrong',
'appearances': 'wrongly_judged', 'sign': 'sign', 'strings': 'strings', 'honest_work': 'compromise_for_money',
'unspoken': 'unspoken', 'overlooked': 'overlooked', 'homecoming': 'homecoming', 'patron': 'patron', 'habit': 'habit'},
'wharton': {'tainted_money': 'compromise_for_money', 'strings_attached': 'strings', 'old_attachment': 'past_comes_back',
'what_they_hid': 'what_they_hid', 'keep_their_secret': 'their_secret', 'exposed': 'caught_out',
'civil_to_them': 'power_over_you', 'plead_their_case': 'go_between', 'own_words_back': 'own_words_back',
'give_it_up': 'sacrifice', 'keep_the_peace': 'peace_at_home', 'unseen_at_home': 'home_unseen',
'new_and_warned': 'newcomer', 'the_rival': 'rival', 'owning_up': 'owning_up', 'waiting_on_them': 'waiting'},
'jacobs': {'G01': 'past_comes_back', 'G02': 'hiding_from_close', 'G03': 'let_another_take_blame', 'G04': 'rival',
'G05': 'your_scheme', 'G06': 'your_scheme', 'G07': 'after_your_money', 'G08': 'strings', 'G09': 'your_scheme',
'G10': 'power_over_you', 'G11': 'unwelcome_in_home', 'G12': 'dread', 'G13': 'pressed_to_wrong',
'G14': 'pretence', 'G15': 'caught_out', 'G16': 'peace_at_home', 'G17': 'cant_prove_it', 'G18': 'newcomer',
'G19': 'owning_up'},
}
def load_corpus(name):
"""-> chunks, {chunk: predicament}, {chunk: hand group id}"""
if name == 'ohenry':
d = PROBE / 'v2'
sys.path.insert(0, str(d / 'claude')); from assign import ASSIGN
pred = {x['chunk']: x['summary'] for x in json.load(open(d / 'claude/predicaments-claude.json'))['summaries']}
return json.load(open(d / 'chunks.json')), pred, dict(ASSIGN)
d = PROBE / name
groups = json.load(open(d / 'claude/groups.json'))['groups']
hand = {}
for g in groups:
for m in g['members']:
c = m if isinstance(m, int) else m['chunk']
assert c not in hand, (name, c); hand[c] = g['id']
if name == 'wharton':
pred = {x['chunk']: x['summary'] for x in json.load(open(d / 'claude/predicaments.json'))['summaries']}
else:
rec = json.load(open(d / 'claude/records.json')); rec = rec if isinstance(rec, list) else rec['records']
pred = {r['chunk']: r['predicament'] for r in rec}
return json.load(open(d / 'chunks.json')), pred, hand
CORPORA = ['ohenry', 'wharton', 'jacobs']
# ---- one recurrence rule for all three --------------------------------------------------------------
def min_stories(n_stories): return max(3, math.ceil(0.04 * n_stories)) # 3 for 10-12 stories, 4 for 97
MIN_SCENES, MAX_DOMINANT = 5, 0.50
def recurs(members, story):
st = collections.Counter(story[c] for c in members)
return len(members) >= MIN_SCENES and len(st) >= min_stories(len(set(story))) and max(st.values()) / len(members) <= MAX_DOMINANT, len(st), (max(st.values()) / len(members) if members else 0)
if __name__ == '__main__':
ids = [e['id'] for e in CATALOGUE]
assert len(ids) == len(set(ids)), 'duplicate id'
problems = []
for corpus, m in SOURCES.items():
for g, c in m.items():
if c not in ids: problems.append(f'{corpus}:{g} -> unknown {c}')
used = {c for m in SOURCES.values() for c in m.values()}
problems += [f'catalogue entry {i} has no source group' for i in ids if i not in used]
for e in CATALOGUE:
for x in e['excludes'].split(';'):
for i in ids:
pass
examples = collections.defaultdict(list)
log = []
for corpus in CORPORA:
chunks, pred, hand = load_corpus(corpus)
unmapped = sorted({g for g in hand.values() if g not in SOURCES[corpus]})
if unmapped: problems.append(f'{corpus}: hand groups not in SOURCES: {unmapped}')
labels = {i: SOURCES[corpus].get(hand[i], 'none') if i in hand else 'none' for i in range(len(chunks))}
(HERE / f'labels-{corpus}.json').write_text(json.dumps(
{'corpus': corpus, 'from': 'hand groups mapped through catalogue_src.SOURCES',
'labels': [{'chunk': i, 'story': chunks[i]['story'], 'label': labels[i], 'predicament': pred[i]} for i in range(len(chunks))]},
indent=0, ensure_ascii=False))
# examples: up to 3 per (entry, corpus), spread across stories, taken from the hand members
per = collections.defaultdict(list)
for i in sorted(labels):
if labels[i] != 'none': per[labels[i]].append(i)
for lab, mem in per.items():
seen = set()
for i in mem:
if chunks[i]['story'] in seen: continue
seen.add(chunks[i]['story']); examples[lab].append({'corpus': corpus, 'chunk': i, 'predicament': pred[i]})
if len(seen) == 3: break
story = [c['story'] for c in chunks]
ns = len(set(story))
log.append(f'\n== {corpus}: {ns} stories, {len(chunks)} scenes; rule: >= {MIN_SCENES} scenes, >= {min_stories(ns)} stories, no story over {MAX_DOMINANT:.0%}')
log.append(f' scenes labelled: {sum(l != "none" for l in labels.values())} ({sum(l != "none" for l in labels.values())/len(chunks):.0%})')
passing = []
for lab in sorted(per, key=lambda k: -len(per[k])):
ok, nst, dom = recurs(per[lab], story)
passing += [lab] if ok else []
log.append(f' {"PASS" if ok else " "} {lab:24s} {len(per[lab]):3d} scenes {nst:3d} stories top story {dom:4.0%}')
log.append(f' -> {len(passing)} recurring situations of {len(per)} present')
cover = collections.defaultdict(set)
for corpus, m in SOURCES.items():
for c in m.values(): cover[c].add(corpus)
log.append('\n== catalogue coverage: entries found in 3 / 2 / 1 corpora')
for k in (3, 2, 1):
log.append(f' {k}: ' + ', '.join(i for i in ids if len(cover[i]) == k))
if problems:
print('\n'.join(problems)); sys.exit(1)
out = {'about': 'Situation catalogue for story-to-pack, seeded by hand from O. Henry, Wharton and Jacobs references (2026-09-17). '
'A classifier assigns each scene predicament one entry or "none". Examples carry their corpus so a test '
'on one corpus can use only the others.',
'entries': [dict(e, corpora=sorted(cover[e['id']]), examples=examples[e['id']]) for e in CATALOGUE]}
(HERE / 'catalogue.json').write_text(json.dumps(out, indent=1, ensure_ascii=False))
(HERE / 'recurrence.log').write_text('\n'.join(log).lstrip() + '\n')
print(f'catalogue.json: {len(CATALOGUE)} entries'); print('\n'.join(log))
@@ -0,0 +1,107 @@
"""Classify scene predicaments against the catalogue, one scene at a time.
STP_OLLAMA=http://<host>:11434 python3 classify.py <corpus> <text-set> [--limit N]
corpus ohenry | wharton | jacobs
text-set claude (the hand-written predicaments) | q14 (qwen3:14b predicaments, O. Henry only)
Leave-one-corpus-out: the worked examples shown never come from the corpus being classified, so this
tests the catalogue on a corpus it has not seen. Embeddings (nomic-embed-text) only narrow the choice:
the scene's K nearest entries, plus "none", go to the model. Checkpoints after every scene; resumes.
Writes out/<corpus>-<text-set>.json.
"""
import argparse, json, math, pathlib, re, sys, time
HERE = pathlib.Path(__file__).resolve().parent
PROBE = HERE.parent
sys.path.insert(0, str(PROBE))
import ollama
ap = argparse.ArgumentParser()
ap.add_argument('corpus'); ap.add_argument('textset')
ap.add_argument('--limit', type=int); ap.add_argument('--k', type=int, default=0, help='0 = offer the whole catalogue')
ap.add_argument('--start', type=int, default=0)
ap.add_argument('--tag', default='')
ap.add_argument('--model', default='qwen3:14b')
args = ap.parse_args()
EMB = 'nomic-embed-text'
cat = json.load(open(HERE / 'catalogue.json'))['entries']
labels = json.load(open(HERE / f'labels-{args.corpus}.json'))['labels']
if args.textset == 'claude':
texts = [x['predicament'] for x in labels]
elif args.textset == 'q14':
assert args.corpus == 'ohenry'
q = {x['chunk']: x['summary'] for x in json.load(open(PROBE / 'v2/predicaments_clean.json'))['summaries']}
texts = [q[x['chunk']] for x in labels]
else:
sys.exit('text-set must be claude or q14')
def norm(v):
m = math.sqrt(sum(x * x for x in v)) or 1.0
return [x / m for x in v]
# embeddings, cached
def cached_embed(name, items):
p = HERE / 'emb' / f'{name}.json'; p.parent.mkdir(exist_ok=True)
if p.exists():
d = json.load(open(p))
if d['texts'] == items: return d['vectors']
vecs = []
for i in range(0, len(items), 16):
vecs += ollama.embed(EMB, ['search_query: ' + t if name != 'catalogue' else 'search_document: ' + t for t in items[i:i + 16]])
p.write_text(json.dumps({'model': EMB, 'texts': items, 'vectors': vecs}))
return vecs
entry_text = [f"{e['name']}. {e['definition']} Includes: {e['includes']}." for e in cat]
E = [norm(v) for v in cached_embed('catalogue', entry_text)]
S = [norm(v) for v in cached_embed(f'{args.corpus}-{args.textset}', texts)]
def examples_for(e):
ex = [x for x in e['examples'] if x['corpus'] != args.corpus][:2]
return ''.join(f'\n e.g. "{x["predicament"]}"' for x in ex)
SYSTEM = """You sort scenes from short stories into recurring SITUATIONS for a life-simulation game.
You get one scene, described as its main person's predicament, and a short list of candidate situations.
Answer with the ONE candidate that the predicament is an instance of, or "none".
Choose "none" when the scene is narration, scene-setting, a description of a place or type of person, or a
one-off turn that is not really any of the candidates. A shared word or setting is not enough: the
predicament itself -- what the person is up against and what they want -- must match the situation.
Most scenes in a story are NOT one of these situations: expect about half to be "none".
Also rate how clearly the predicament is an instance of your choice: 3 = plainly this situation,
2 = arguably, 1 = only loosely.
Reply with JSON only: {"label": "<candidate id or none>", "fit": <1, 2 or 3>}"""
out_path = HERE / 'out' / f'{args.corpus}-{args.textset}{args.tag}.json'; out_path.parent.mkdir(exist_ok=True)
state = json.load(open(out_path)) if out_path.exists() else {'model': args.model, 'k': args.k, 'results': []}
done = {r['chunk'] for r in state['results']}
n = len(texts) if not args.limit else min(args.limit, len(texts))
t0 = time.time(); new = 0
for i in range(args.start, n):
if labels[i]['chunk'] in done: continue
order = sorted(range(len(cat)), key=lambda j: -sum(a * b for a, b in zip(S[i], E[j])))
sims = order[:args.k] if args.k else list(range(len(cat)))
cands = '\n'.join(f"- {cat[j]['id']}: {cat[j]['definition']}{examples_for(cat[j])}" for j in sims)
# candidates go in the system prompt, identical from scene to scene when the whole catalogue is offered,
# so the server can reuse its cached prefix; the predicament comes last
system = SYSTEM + '\n\nCandidates:\n' + cands + '\n- none: not one of these situations'
user = f'Predicament: "{texts[i]}"'
raw, _, _ = ollama.chat(args.model, system, user, num_ctx=4096, num_predict=40, fmt='json',
think=False if args.model.startswith('qwen3') else None)
try:
j = json.loads(raw); lab = str(j.get('label', '')).strip(); fit = j.get('fit')
except Exception:
m = re.search(r'"label"\s*:\s*"([^"]+)"', raw); lab = m.group(1) if m else ''; fit = None
valid = {cat[j]['id'] for j in sims} | {'none'}
state['results'].append({'chunk': labels[i]['chunk'], 'label': lab if lab in valid else 'none',
'fit': fit, 'raw': raw if lab not in valid else None, 'candidates': [cat[j]['id'] for j in sims],
'nearest': cat[sims[0]]['id']})
new += 1
if new % 10 == 0 or i == n - 1:
out_path.write_text(json.dumps(state, indent=0))
el = time.time() - t0
print(f' {len(state["results"])}/{n} {el:.0f}s {el / new:.2f}s/scene', flush=True)
out_path.write_text(json.dumps(state, indent=0))
print('done', out_path.name, len(state['results']))
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,948 @@
{
"corpus": "jacobs",
"from": "hand groups mapped through catalogue_src.SOURCES",
"labels": [
{
"chunk": 0,
"story": 0,
"label": "peace_at_home",
"predicament": "Someone waiting up for a relative who comes in late wants to reproach him, and gets cheek back."
},
{
"chunk": 1,
"story": 0,
"label": "peace_at_home",
"predicament": "A man learns a relative has promised a stranger his hospitality without asking, and fears what his spouse will say."
},
{
"chunk": 2,
"story": 0,
"label": "unwelcome_in_home",
"predicament": "A man who wants to turn an unwelcome guest away cannot find the nerve to say it."
},
{
"chunk": 3,
"story": 0,
"label": "peace_at_home",
"predicament": "A man who has let something happen under his roof fears his spouse will hold him responsible for it."
},
{
"chunk": 4,
"story": 0,
"label": "rival",
"predicament": "A young man who brought the person he is courting along is pushed aside by an older man who takes charge of her."
},
{
"chunk": 5,
"story": 0,
"label": "rival",
"predicament": "A jealous suitor tries to put his rival in his place, and only makes himself look childish."
},
{
"chunk": 6,
"story": 0,
"label": "none",
"predicament": "A spurned man realises he holds something over the rival who humiliated him, and decides to use it."
},
{
"chunk": 7,
"story": 0,
"label": "hiding_from_close",
"predicament": "A man hiding an innocent but compromising secret is met by his spouse, while the one person who could clear him refuses to."
},
{
"chunk": 8,
"story": 0,
"label": "hiding_from_close",
"predicament": "A man tries to hurry his spouse away from a secret while someone signals behind his back that something is up."
},
{
"chunk": 9,
"story": 0,
"label": "caught_out",
"predicament": "A man caught with an innocent secret is disowned by the one person who could explain it."
},
{
"chunk": 10,
"story": 0,
"label": "let_another_take_blame",
"predicament": "A man whose spite has got an innocent person abused must decide whether to own up and defend her."
},
{
"chunk": 11,
"story": 0,
"label": "owning_up",
"predicament": "A man tries to win back someone he wronged with a dangerous public show of devotion."
},
{
"chunk": 12,
"story": 0,
"label": "your_scheme",
"predicament": "A man pretends to be in danger to win sympathy, and the one who saved him sees through it."
},
{
"chunk": 13,
"story": 0,
"label": "owning_up",
"predicament": "Two men who quarrelled over a woman are both left out in the cold by the women they wronged."
},
{
"chunk": 14,
"story": 1,
"label": "caught_out",
"predicament": "A man losing a friendly contest tries to distract from his mistake, and is caught at it by his family."
},
{
"chunk": 15,
"story": 1,
"label": "none",
"predicament": "A guest who wants to leave a dangerous subject alone is coaxed by his hosts' curiosity into bringing it out."
},
{
"chunk": 16,
"story": 1,
"label": "strings",
"predicament": "Someone offered a thing that grants wishes is warned by the man who used it that it brings sorrow."
},
{
"chunk": 17,
"story": 1,
"label": "strings",
"predicament": "A man is offered a dangerous gift by someone who begs him to destroy it, and wants to keep it anyway."
},
{
"chunk": 18,
"story": 1,
"label": "strings",
"predicament": "A man half-believing in a promise of easy money must decide whether to test it and what to ask for."
},
{
"chunk": 19,
"story": 1,
"label": "dread",
"predicament": "A family that tempted fate laughs off their fears in daylight while one of them cannot shake them."
},
{
"chunk": 20,
"story": 1,
"label": "strings",
"predicament": "A family waiting on a hoped-for windfall sees a stranger approach who might bring it, or bad news."
},
{
"chunk": 21,
"story": 1,
"label": "none",
"predicament": "Parents receive news that their only child has been killed, from someone who cannot bring himself to say it."
},
{
"chunk": 22,
"story": 1,
"label": "strings",
"predicament": "A man learns the money he wished for has come as compensation for his child's death."
},
{
"chunk": 23,
"story": 1,
"label": "pressed_to_wrong",
"predicament": "A grieving woman seizes on a desperate chance to undo a death, and her spouse is afraid of what it would bring back."
},
{
"chunk": 24,
"story": 1,
"label": "pressed_to_wrong",
"predicament": "A man pressed by his grieving spouse must decide whether to do something he believes is wicked and dangerous."
},
{
"chunk": 25,
"story": 1,
"label": "dread",
"predicament": "A man who dreads what his wish may bring home must stop his spouse from opening the door to it."
},
{
"chunk": 26,
"story": 1,
"label": "dread",
"predicament": "A man must use his last chance to undo what his spouse most longs for, before she lets it in."
},
{
"chunk": 27,
"story": 2,
"label": "after_your_money",
"predicament": "A man who has hoarded his savings all his life falls ill far from anywhere he could spend them."
},
{
"chunk": 28,
"story": 2,
"label": "after_your_money",
"predicament": "A dying miser, surrounded by people hoping to inherit, is determined that nobody will get his money."
},
{
"chunk": 29,
"story": 2,
"label": "pressed_to_wrong",
"predicament": "Someone who spots a colleague quietly scheming to take unclaimed money must decide whether to expose him or join in."
},
{
"chunk": 30,
"story": 2,
"label": "your_scheme",
"predicament": "Two partners convinced a weaker person is secretly holding a fortune want to find it before anyone else learns of it."
},
{
"chunk": 31,
"story": 2,
"label": "cant_prove_it",
"predicament": "A schemer whose secret searches keep failing decides to confront the person he suspects directly."
},
{
"chunk": 32,
"story": 2,
"label": "after_your_money",
"predicament": "Someone suspected of hiding a windfall is pressed with bribes and threats by people who have no right to it."
},
{
"chunk": 33,
"story": 2,
"label": "your_scheme",
"predicament": "Someone forced to share a hidden windfall admits it is now stuck where neither partner can get at it."
},
{
"chunk": 34,
"story": 2,
"label": "your_scheme",
"predicament": "Distrustful partners stare at a fortune they cannot reach without getting caught, and blame each other for it."
},
{
"chunk": 35,
"story": 2,
"label": "your_scheme",
"predicament": "A man wants something a superior owns without revealing why, and tries flattery to get it."
},
{
"chunk": 36,
"story": 2,
"label": "your_scheme",
"predicament": "A buyer who cannot reveal why he wants something is made to pay more by a seller who senses his eagerness."
},
{
"chunk": 37,
"story": 2,
"label": "your_scheme",
"predicament": "A man who wants to cheat his partner out of a shared prize is watched too closely to slip away."
},
{
"chunk": 38,
"story": 2,
"label": "your_scheme",
"predicament": "A man trying to slip away with a shared prize is held up until the partner he meant to cheat catches him."
},
{
"chunk": 39,
"story": 2,
"label": "your_scheme",
"predicament": "A man who schemed to cheat a partner finds the prize gone and realises he may have been cheated himself."
},
{
"chunk": 40,
"story": 2,
"label": "your_scheme",
"predicament": "A schemer who was outwitted must stand and smile at the people who got the better of him."
},
{
"chunk": 41,
"story": 3,
"label": "none",
"predicament": "A man about to marry is needled by a dependent relative who hints at something to his discredit."
},
{
"chunk": 42,
"story": 3,
"label": "none",
"predicament": "A man in desperate debt makes veiled hints to the relative he hopes will pay it."
},
{
"chunk": 43,
"story": 3,
"label": "past_comes_back",
"predicament": "A man is blackmailed by a relative with old letters that could cost him the person he loves."
},
{
"chunk": 44,
"story": 3,
"label": "let_another_take_blame",
"predicament": "A man who has done something terrible in anger must cover for a relative's sudden disappearance."
},
{
"chunk": 45,
"story": 3,
"label": "hiding_from_close",
"predicament": "A man with a guilty secret must keep calm while someone he loves insists on lingering right at the spot."
},
{
"chunk": 46,
"story": 3,
"label": "hiding_from_close",
"predicament": "A man hiding a crime hears it discussed innocently and betrays his feelings with an outburst."
},
{
"chunk": 47,
"story": 3,
"label": "hiding_from_close",
"predicament": "A guilty man tries to hurry someone away from the scene of his crime, and she insists on staying."
},
{
"chunk": 48,
"story": 3,
"label": "hiding_from_close",
"predicament": "A man hiding something learns it may be discovered by an innocent accident, and must not show his fear."
},
{
"chunk": 49,
"story": 3,
"label": "hiding_from_close",
"predicament": "A man must retrieve something from where his secret lies before anyone else goes looking."
},
{
"chunk": 50,
"story": 3,
"label": "hiding_from_close",
"predicament": "A man secretly searching where his crime lies keeps pulling up reminders of it."
},
{
"chunk": 51,
"story": 3,
"label": "hiding_from_close",
"predicament": "A man who has failed to fix things quietly must now involve others while still keeping his secret."
},
{
"chunk": 52,
"story": 3,
"label": "hiding_from_close",
"predicament": "A man hiding something is offered help he cannot accept without being found out."
},
{
"chunk": 53,
"story": 3,
"label": "none",
"predicament": "A man who must face the thing he did alone gathers his nerve while others watch and wonder."
},
{
"chunk": 54,
"story": 3,
"label": "your_scheme",
"predicament": "A man who hid a crime is dragged down by it at the moment he tries to undo its consequences."
},
{
"chunk": 55,
"story": 4,
"label": "peace_at_home",
"predicament": "A woman who has lost something valuable suspects the people around her, and resents her spouse for not caring as much."
},
{
"chunk": 56,
"story": 4,
"label": "cant_prove_it",
"predicament": "Someone who has lost something accuses the most defenceless person nearby, and punishes her before any proof."
},
{
"chunk": 57,
"story": 4,
"label": "peace_at_home",
"predicament": "A man who thinks a household fuss is foolish keeps defending the accused and is scolded for taking her side."
},
{
"chunk": 58,
"story": 4,
"label": "caught_out",
"predicament": "A man who finds a family inquiry ridiculous cannot keep a straight face in front of its self-important leader."
},
{
"chunk": 59,
"story": 4,
"label": "let_another_take_blame",
"predicament": "A man who sees an innocent neighbour blamed for evidence he himself left must decide whether to own up."
},
{
"chunk": 60,
"story": 4,
"label": "caught_out",
"predicament": "A man who knows more than he says can barely hide his amusement from the self-important person running the inquiry."
},
{
"chunk": 61,
"story": 4,
"label": "cant_prove_it",
"predicament": "A self-important man, teased that a neighbour fancies him, resolves to prove his point by trapping her."
},
{
"chunk": 62,
"story": 4,
"label": "pressed_to_wrong",
"predicament": "A man refuses to let his home be used to entrap a friend, so the schemer plans to go behind his back."
},
{
"chunk": 63,
"story": 4,
"label": "unwelcome_in_home",
"predicament": "A man stuck with an unwelcome relative in his home sees a chance to have him disgraced and driven off."
},
{
"chunk": 64,
"story": 4,
"label": "power_over_you",
"predicament": "A man who has trapped his rival in an embarrassing hiding place decides to make the most of it."
},
{
"chunk": 65,
"story": 4,
"label": "caught_out",
"predicament": "A trapped man must listen in silence while someone tells a neighbour a story about him that he cannot deny."
},
{
"chunk": 66,
"story": 4,
"label": "caught_out",
"predicament": "A man caught somewhere he should not be is handed an embarrassing explanation, and has no better one to offer."
},
{
"chunk": 67,
"story": 5,
"label": "past_comes_back",
"predicament": "A man found out in wrongdoing is ordered by his partner to give up everything and leave by morning."
},
{
"chunk": 68,
"story": 5,
"label": "past_comes_back",
"predicament": "A man offered a harsh but fair way out of his disgrace wants a quieter bargain the other will not give."
},
{
"chunk": 69,
"story": 5,
"label": "pressed_to_wrong",
"predicament": "A man facing ruin threatens the only person who can expose him, and must choose whether to go through with it."
},
{
"chunk": 70,
"story": 5,
"label": "none",
"predicament": "A man who has just done something terrible in rage panics and must steady himself when a figure of the law appears."
},
{
"chunk": 71,
"story": 5,
"label": "none",
"predicament": "A man with a crime behind him and a few hours' start must decide what to take and where to run."
},
{
"chunk": 72,
"story": 5,
"label": "none",
"predicament": "A fugitive planning his escape finds that every choice might be the one that gets him caught."
},
{
"chunk": 73,
"story": 5,
"label": "dread",
"predicament": "A man alone with what he has done hears noises in the dark and fears it has come back for him."
},
{
"chunk": 74,
"story": 5,
"label": "let_another_take_blame",
"predicament": "A guilty man realises a stranger's blunder gives him someone else to blame."
},
{
"chunk": 75,
"story": 5,
"label": "let_another_take_blame",
"predicament": "A man framing an innocent person must act the outraged victim in front of the law."
},
{
"chunk": 76,
"story": 5,
"label": "let_another_take_blame",
"predicament": "A man must play the shocked innocent while the person he framed pleads the truth."
},
{
"chunk": 77,
"story": 5,
"label": "let_another_take_blame",
"predicament": "Someone falsely accused points to the real culprit, who must hold his nerve and keep up his story."
},
{
"chunk": 78,
"story": 5,
"label": "your_scheme",
"predicament": "A man who thought he had got away with it learns the one witness who could hang him is still alive."
},
{
"chunk": 79,
"story": 5,
"label": "your_scheme",
"predicament": "A man whose deceit has collapsed can only watch as the truth comes out and he is taken away."
},
{
"chunk": 80,
"story": 6,
"label": "none",
"predicament": "A penniless newcomer forces his way into a place he does not belong, counting on a secret to back him."
},
{
"chunk": 81,
"story": 6,
"label": "past_comes_back",
"predicament": "A man who has built a respectable new life is found by an old associate who threatens to expose his past."
},
{
"chunk": 82,
"story": 6,
"label": "past_comes_back",
"predicament": "A man pays off a blackmailer and learns the price will be half of everything, for good."
},
{
"chunk": 83,
"story": 6,
"label": "past_comes_back",
"predicament": "A reformed man warns a blackmailer not to push him too far, while the blackmailer sets his sights on the man's family."
},
{
"chunk": 84,
"story": 6,
"label": "none",
"predicament": "A man who has seized power over another knows the danger he is in, and guards himself even as he gloats."
},
{
"chunk": 85,
"story": 6,
"label": "unwelcome_in_home",
"predicament": "A household must make polite space for someone who holds them hostage."
},
{
"chunk": 86,
"story": 6,
"label": "unwelcome_in_home",
"predicament": "Someone watches a person she trusts give way at every turn to an intruder, and demands that he stand up for himself."
},
{
"chunk": 87,
"story": 6,
"label": "past_comes_back",
"predicament": "A man who is being blackmailed hears his tormentor threaten someone he loves, and must choose between swallowing it and hitting back."
},
{
"chunk": 88,
"story": 6,
"label": "your_scheme",
"predicament": "A blackmailer's hold depends on his victim's life, and that victim suddenly collapses."
},
{
"chunk": 89,
"story": 6,
"label": "none",
"predicament": "A blackmailer whose victim is dying must grab what he can before his hold disappears."
},
{
"chunk": 90,
"story": 6,
"label": "past_comes_back",
"predicament": "A dying man pressed by his tormentor bargains for time and for provision for someone he loves."
},
{
"chunk": 91,
"story": 6,
"label": "your_scheme",
"predicament": "A man who thinks his helpless victim is at his mercy leans in close to hear the secret."
},
{
"chunk": 92,
"story": 6,
"label": "power_over_you",
"predicament": "A man who pretended to be helpless strikes at the tormentor who threatened his family."
},
{
"chunk": 93,
"story": 6,
"label": "let_another_take_blame",
"predicament": "A man who has done a desperate thing arranges things so that suspicion falls elsewhere."
},
{
"chunk": 94,
"story": 7,
"label": "none",
"predicament": "A craftsman told his work looks like something else quietly fixes it before anyone else notices."
},
{
"chunk": 95,
"story": 7,
"label": "pretence",
"predicament": "An old man looking for a free drink offers a story in exchange."
},
{
"chunk": 96,
"story": 7,
"label": "dread",
"predicament": "People who laughed at a distant danger panic when it seems to have arrived on their doorstep."
},
{
"chunk": 97,
"story": 7,
"label": "dread",
"predicament": "Someone must choose between keeping himself safe behind a barred door and letting in a stranger in danger."
},
{
"chunk": 98,
"story": 7,
"label": "pretence",
"predicament": "A stranger with a thrilling tale of danger is rewarded by a frightened crowd without anyone checking it."
},
{
"chunk": 99,
"story": 7,
"label": "dread",
"predicament": "A community living in fear of an unseen threat ignores the one doubter, because he has a bad name."
},
{
"chunk": 100,
"story": 7,
"label": "cant_prove_it",
"predicament": "A man who has suffered a loss goes to the authorities for help and is brushed off as a nuisance."
},
{
"chunk": 101,
"story": 7,
"label": "none",
"predicament": "A community suffering repeated losses cannot get help, and cannot agree how to protect itself."
},
{
"chunk": 102,
"story": 7,
"label": "pretence",
"predicament": "A man tells a dramatic story of narrow escape to an audience eager to believe it."
},
{
"chunk": 103,
"story": 7,
"label": "pretence",
"predicament": "A man caught in a boastful lie is 'backed up' by a rival, who now has him trapped by it."
},
{
"chunk": 104,
"story": 7,
"label": "none",
"predicament": "People told their fears are nonsense by outsiders decide to take matters into their own hands."
},
{
"chunk": 105,
"story": 7,
"label": "none",
"predicament": "A band that set out to face a danger returns relieved to have found nothing, and is then called to face it for real."
},
{
"chunk": 106,
"story": 7,
"label": "caught_out",
"predicament": "A man with something to hide has an armed crowd force its way into his home."
},
{
"chunk": 107,
"story": 7,
"label": "caught_out",
"predicament": "A man caught with the evidence of his thefts brazens it out and dares his accusers to prove anything."
},
{
"chunk": 108,
"story": 7,
"label": "caught_out",
"predicament": "A man accused of wrongdoing turns his accusers' own exaggerations into his defence."
},
{
"chunk": 109,
"story": 7,
"label": "cant_prove_it",
"predicament": "People who know they were swindled cannot prove it, and must watch the swindler profit."
},
{
"chunk": 110,
"story": 8,
"label": "none",
"predicament": "A man nervously setting out to do something that matters to him is stopped by a friend with questions."
},
{
"chunk": 111,
"story": 8,
"label": "rival",
"predicament": "A man discovers a friend is after the same person he is, on the same day, and each races to get there first."
},
{
"chunk": 112,
"story": 8,
"label": "rival",
"predicament": "Two rivals agree someone must go first, and neither will be the one to wait."
},
{
"chunk": 113,
"story": 8,
"label": "rival",
"predicament": "Two rivals for the same prize trade claims of priority, and each suspects the other of a trick."
},
{
"chunk": 114,
"story": 8,
"label": "your_scheme",
"predicament": "Two rivals agree to settle who goes first by a game, and one slips in a condition the other does not examine."
},
{
"chunk": 115,
"story": 8,
"label": "your_scheme",
"predicament": "A man who tried to tip a contest in his favour loses it through his own oversight."
},
{
"chunk": 116,
"story": 8,
"label": "rival",
"predicament": "A man who lost the right to try first must wait while his rival takes his chance."
},
{
"chunk": 117,
"story": 8,
"label": "rival",
"predicament": "A man finds the rival he lost to has been refused, but can still use the terms of their bargain to keep him out."
},
{
"chunk": 118,
"story": 8,
"label": "rival",
"predicament": "A man bound by a promise he made watches a rival use it to keep him from what he wants."
},
{
"chunk": 119,
"story": 8,
"label": "caught_out",
"predicament": "A man who has been avoiding someone he cares for must explain why, and the only honest answer is embarrassing."
},
{
"chunk": 120,
"story": 8,
"label": "none",
"predicament": "A man bound by a rash promise finds the person he hoped to win is willing to help him get round it."
},
{
"chunk": 121,
"story": 8,
"label": "none",
"predicament": "Someone arranges to accept the suitor she prefers in a way his rival cannot object to."
},
{
"chunk": 122,
"story": 9,
"label": "newcomer",
"predicament": "A man wakes up somewhere strange with no idea how he got there."
},
{
"chunk": 123,
"story": 9,
"label": "newcomer",
"predicament": "A man in an unknown place looks for his one companion and a first clue to how they came to be there."
},
{
"chunk": 124,
"story": 9,
"label": "power_over_you",
"predicament": "A man who has always been obeyed tries to give orders somewhere nobody knows or believes who he is."
},
{
"chunk": 125,
"story": 9,
"label": "your_scheme",
"predicament": "Two people in trouble together try to put on a united front and end up blaming each other."
},
{
"chunk": 126,
"story": 9,
"label": "power_over_you",
"predicament": "Two people who claim to be respected professionals are treated as impostors and forced to do menial work."
},
{
"chunk": 127,
"story": 9,
"label": "power_over_you",
"predicament": "People brought low insist on their old status, and are mocked for it by those around them."
},
{
"chunk": 128,
"story": 9,
"label": "power_over_you",
"predicament": "A humiliated man pretends to submit while he quietly prepares his revenge."
},
{
"chunk": 129,
"story": 9,
"label": "power_over_you",
"predicament": "A man mocked for claiming skill is suddenly needed for it, and names his price."
},
{
"chunk": 130,
"story": 9,
"label": "power_over_you",
"predicament": "A man who has gained the upper hand over those who humiliated him enjoys every moment of it."
},
{
"chunk": 131,
"story": 9,
"label": "power_over_you",
"predicament": "A man who has secretly caused the trouble only he can cure makes the most of it."
},
{
"chunk": 132,
"story": 9,
"label": "none",
"predicament": "A man who swore he would never pay the person he scorned is forced by suffering to haggle with him."
},
{
"chunk": 133,
"story": 9,
"label": "power_over_you",
"predicament": "People who scorned two outsiders now depend on them completely, and pay for the privilege."
},
{
"chunk": 134,
"story": 9,
"label": "power_over_you",
"predicament": "A man who got even with his tormentors leaves them knowing they were had."
},
{
"chunk": 135,
"story": 10,
"label": "strings",
"predicament": "Someone given news that might change their life cannot take it in, while the people around them itch to know."
},
{
"chunk": 136,
"story": 10,
"label": "strings",
"predicament": "Someone who has never had luck is told of a windfall and is afraid to believe it."
},
{
"chunk": 137,
"story": 10,
"label": "strings",
"predicament": "A man sees a relative come into money and resents that luck never comes his way."
},
{
"chunk": 138,
"story": 10,
"label": "after_your_money",
"predicament": "A man flattered by new friends is told they are really after a relative's money."
},
{
"chunk": 139,
"story": 10,
"label": "after_your_money",
"predicament": "A man trying to expose flattering suitors trips over his own family's sensibilities."
},
{
"chunk": 140,
"story": 10,
"label": "rival",
"predicament": "Two rivals courting the same person keep topping each other's compliments while the family grows fed up."
},
{
"chunk": 141,
"story": 10,
"label": "rival",
"predicament": "A slow-talking man losing a contest to a glib rival looks for a trick instead of better words."
},
{
"chunk": 142,
"story": 10,
"label": "rival",
"predicament": "A man hires someone to spread a false rumour that will drive off his rival, and the hireling drives a hard bargain."
},
{
"chunk": 143,
"story": 10,
"label": "your_scheme",
"predicament": "A man courting for money pretends not to care about it and flatters the one he courts by letting her win."
},
{
"chunk": 144,
"story": 10,
"label": "your_scheme",
"predicament": "A schemer who thinks his deal is done walks away gloating too soon."
},
{
"chunk": 145,
"story": 10,
"label": "your_scheme",
"predicament": "A man makes a public show of selfless devotion, sure that it will cost him nothing."
},
{
"chunk": 146,
"story": 10,
"label": "your_scheme",
"predicament": "A schemer whose plan has worked is thanked for his goodness while the ground starts to shift under him."
},
{
"chunk": 147,
"story": 10,
"label": "your_scheme",
"predicament": "A man who faked a disaster to win a prize learns the disaster is real, and he is bound to his promise."
},
{
"chunk": 148,
"story": 11,
"label": "pretence",
"predicament": "A sceptic among believers is pressed to admit whether he has ever seen anything he could not explain."
},
{
"chunk": 149,
"story": 11,
"label": "newcomer",
"predicament": "A young man who arrived to find no one expecting him has to fill empty days in a strange place."
},
{
"chunk": 150,
"story": 11,
"label": "newcomer",
"predicament": "A traveller lost in the dark and the cold must find shelter before night closes in."
},
{
"chunk": 151,
"story": 11,
"label": "dread",
"predicament": "A worn-out traveller must choose between a long walk on and a nearby shelter he has been warned against."
},
{
"chunk": 152,
"story": 11,
"label": "dread",
"predicament": "A stranger taken in by odd hosts who seem keen to keep him wonders whether he has walked into a trap."
},
{
"chunk": 153,
"story": 11,
"label": "dread",
"predicament": "A guest must sit down in the dark with someone he cannot see, on hosts' terms he does not understand."
},
{
"chunk": 154,
"story": 11,
"label": "dread",
"predicament": "A guest left alone with a stranger he cannot see grows afraid, and must decide whether to trust his hosts."
},
{
"chunk": 155,
"story": 11,
"label": "dread",
"predicament": "A frightened man sees what he feared, and learns it is someone who was hurt and ashamed to be seen."
},
{
"chunk": 156,
"story": 11,
"label": "owning_up",
"predicament": "A man who shrank from someone must make amends, and finds they deserve his respect."
}
]
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,36 @@
Pilot, qwen3:14b, shortlist k=8, 14B predicaments, O. Henry scenes 0-39. The output file was later deleted;
these figures were copied by hand from the session transcript (score.py output):
situated? precision 38% recall 93% (model calls 34, hand 14)
label right where both situated: 23% of 13
kappa over all labels incl. none: 0.09
embedding-only baseline (nearest entry) on hand-situated scenes: 36%
hand label inside the shortlist offered: 71%
ohenry-q14-dev1.partial: 30 scenes
situated? precision 67% recall 100% (model calls 21, hand 14)
label right where both situated: 14% of 14
kappa over all labels incl. none: 0.23
ohenry-q14-dev1.partial fit>=3: 30 scenes
situated? precision 62% recall 57% (model calls 13, hand 14)
label right where both situated: 0% of 8
kappa over all labels incl. none: 0.09
6 hand=none model=unspoken fit=3 | Someone and the narrator sit in silence, caught between the quiet intimacy of the moment and the unspoken weig
9 hand=home_unseen model=home_unseen fit=2 | Someone longs for connection but is trapped in a life of monotony and shallow conversation.
10 hand=none model=caught_out fit=3 | A man tries to pay for a purchase with a promise of future money, but his dishonesty sparks a violent confront
11 hand=none model=caught_out fit=2 | A man flees from police pursuit, seeking refuge in an unknown vehicle that offers him a chance to escape, but
12 hand=none model=their_secret fit=3 | A man finds himself unexpectedly drawn into a personal conflict he did not ask for, yet must navigate with cou
13 hand=none model=owning_up fit=3 | Someone must confront a man who has wronged a woman he now feels compelled to protect, risking his own safety
14 hand=none model=compromise_for_money fit=2 | A man struggles to reconcile his growing affection for a woman with the rigid expectations of his social circl
16 hand=station model=compromise_for_money fit=2 | A man finds himself trapped in an awkward encounter with a shopgirl whose icy beauty challenges his composure,
17 hand=station model=pretence fit=2 | A man struggling with his pride and social conventions finds himself awkwardly drawn to a shopgirl, determined
18 hand=station model=compromise_for_money fit=2 | A young woman balances between curiosity and caution as she agrees to meet a man she barely knows, hoping to e
19 hand=station model=unspoken fit=3 | A man torn between his growing feelings for a woman he barely knows and his fear of losing control of his life
20 hand=station model=pretence fit=3 | A man insists a woman marry him, claiming his wealth and status make them equal, but she sees through his word
21 hand=station model=strings fit=2 | A man tries to convince a hesitant woman to leave her mundane life and embark on an extravagant, uncertain jou
22 hand=station model=pretence fit=3 | A woman hides her growing feelings for a man while pretending to be uninterested, hoping to keep their connect
23 hand=none model=hiding_from_close fit=3 | Big someone must navigate the rigid codes of his underground world while seeking a way to break free and claim
24 hand=home_unseen model=home_unseen fit=2 | A man struggles to balance his cultural identity and personal life while navigating the expectations of his co
25 hand=home_unseen model=habit fit=3 | A man accustomed to routine is confronted with an unexpected request for companionship, forcing him to reconsi
26 hand=home_unseen model=hiding_from_close fit=3 | A man struggling to reconcile his desire to display his wife publicly with his fear of judgment finds himself
27 hand=home_unseen model=what_they_hid fit=3 | A man struggles to reconcile his pride with the growing awareness of his wife's hidden talents and the shame o
28 hand=home_unseen model=hiding_from_close fit=3 | A man struggles to reconcile his quiet life with the sudden, overwhelming attention his wife receives, threate
29 hand=home_unseen model=peace_at_home fit=3 | A man faces a choice between social obligation and personal pride, seeking to assert his dignity while preserv
@@ -0,0 +1,68 @@
== ohenry: 97 stories, 839 scenes; rule: >= 5 scenes, >= 4 stories, no story over 50%
scenes labelled: 475 (57%)
PASS pretence 52 scenes 12 stories top story 15%
PASS benefactor 48 scenes 14 stories top story 17%
PASS home_unseen 47 scenes 11 stories top story 15%
PASS station 47 scenes 10 stories top story 17%
PASS compromise_for_money 38 scenes 13 stories top story 18%
PASS newcomer 32 scenes 8 stories top story 22%
PASS sure_thing 30 scenes 7 stories top story 23%
PASS sacrifice 25 scenes 8 stories top story 28%
PASS rival 24 scenes 6 stories top story 29%
PASS despised 20 scenes 7 stories top story 30%
PASS pressed_to_wrong 19 scenes 5 stories top story 32%
PASS wrongly_judged 19 scenes 7 stories top story 26%
PASS sign 17 scenes 5 stories top story 35%
strings 15 scenes 4 stories top story 53%
PASS unspoken 13 scenes 4 stories top story 38%
overlooked 9 scenes 3 stories top story 67%
PASS homecoming 8 scenes 4 stories top story 50%
PASS patron 8 scenes 4 stories top story 50%
habit 4 scenes 3 stories top story 50%
-> 16 recurring situations of 19 present
== wharton: 10 stories, 262 scenes; rule: >= 5 scenes, >= 3 stories, no story over 50%
scenes labelled: 129 (49%)
PASS caught_out 12 scenes 4 stories top story 50%
PASS compromise_for_money 12 scenes 4 stories top story 50%
PASS past_comes_back 12 scenes 3 stories top story 50%
PASS what_they_hid 10 scenes 6 stories top story 20%
PASS peace_at_home 8 scenes 5 stories top story 38%
PASS newcomer 8 scenes 3 stories top story 50%
PASS rival 8 scenes 4 stories top story 50%
PASS power_over_you 8 scenes 4 stories top story 50%
PASS sacrifice 8 scenes 5 stories top story 38%
PASS owning_up 8 scenes 4 stories top story 38%
PASS home_unseen 7 scenes 4 stories top story 43%
PASS go_between 6 scenes 3 stories top story 50%
PASS waiting 6 scenes 3 stories top story 50%
PASS own_words_back 6 scenes 3 stories top story 50%
PASS strings 5 scenes 4 stories top story 40%
PASS their_secret 5 scenes 3 stories top story 40%
-> 16 recurring situations of 16 present
== jacobs: 12 stories, 157 scenes; rule: >= 5 scenes, >= 3 stories, no story over 50%
scenes labelled: 137 (87%)
PASS your_scheme 23 scenes 8 stories top story 39%
PASS dread 12 scenes 4 stories top story 42%
rival 11 scenes 3 stories top story 55%
power_over_you 11 scenes 3 stories top story 82%
hiding_from_close 10 scenes 2 stories top story 80%
PASS caught_out 10 scenes 5 stories top story 40%
PASS let_another_take_blame 8 scenes 5 stories top story 50%
strings 8 scenes 2 stories top story 62%
past_comes_back 8 scenes 3 stories top story 62%
peace_at_home 5 scenes 2 stories top story 60%
PASS pressed_to_wrong 5 scenes 4 stories top story 40%
after_your_money 5 scenes 2 stories top story 60%
PASS cant_prove_it 5 scenes 3 stories top story 40%
pretence 5 scenes 2 stories top story 80%
unwelcome_in_home 4 scenes 3 stories top story 50%
newcomer 4 scenes 2 stories top story 50%
owning_up 3 scenes 2 stories top story 67%
-> 6 recurring situations of 17 present
== catalogue coverage: entries found in 3 / 2 / 1 corpora
3: rival, strings, newcomer
2: compromise_for_money, past_comes_back, pretence, caught_out, home_unseen, peace_at_home, power_over_you, pressed_to_wrong, sacrifice, owning_up
1: sure_thing, your_scheme, after_your_money, hiding_from_close, their_secret, what_they_hid, wrongly_judged, let_another_take_blame, cant_prove_it, station, unwelcome_in_home, benefactor, patron, go_between, own_words_back, homecoming, dread, waiting, unspoken, overlooked, sign, despised, habit
@@ -0,0 +1,70 @@
"""Score classifier output against the hand labels. No inference.
python3 score.py out/ohenry-q14.json [more files] # or --selftest
Reports: (1) does it find the situated scenes (label vs none); (2) on scenes both call situated, is the
label right; (3) Cohen's kappa over everything; (4) the embedding-only baseline (nearest entry, no
model) on the same scenes; (5) the recurring situations a pack would get under the one shared rule,
from the model's labels against the hand labels. The recall of the K-nearest shortlist is reported
too: a hand label that was never offered cannot be chosen.
"""
import collections, json, pathlib, random, sys
HERE = pathlib.Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
from catalogue_src import recurs, load_corpus
def kappa(a, b):
n = len(a); po = sum(x == y for x, y in zip(a, b)) / n
ca, cb = collections.Counter(a), collections.Counter(b)
pe = sum(ca[k] * cb[k] for k in ca) / n / n
return (po - pe) / (1 - pe) if pe < 1 else 1.0
def score(corpus, hand, pred, nearest=None, cands=None, name=''):
chunks = sorted(pred)
h = [hand[c] for c in chunks]; p = [pred[c] for c in chunks]
tp = sum(x != 'none' and y != 'none' for x, y in zip(h, p))
prec = tp / max(1, sum(y != 'none' for y in p)); rec = tp / max(1, sum(x != 'none' for x in h))
both = [(x, y) for x, y in zip(h, p) if x != 'none' and y != 'none']
acc = sum(x == y for x, y in both) / max(1, len(both))
lines = [f'{name}: {len(chunks)} scenes',
f' situated? precision {prec:.0%} recall {rec:.0%} (model calls {sum(y != "none" for y in p)}, hand {sum(x != "none" for x in h)})',
f' label right where both situated: {acc:.0%} of {len(both)}',
f' kappa over all labels incl. none: {kappa(h, p):.2f}']
if nearest:
sit = [c for c in chunks if hand[c] != 'none']
lines.append(f' embedding-only baseline (nearest entry) on hand-situated scenes: {sum(nearest[c] == hand[c] for c in sit) / max(1, len(sit)):.0%}')
lines.append(f' hand label inside the shortlist offered: {sum(hand[c] in cands[c] for c in sit) / max(1, len(sit)):.0%}')
chunks_all, _, _ = load_corpus(corpus); story = [c['story'] for c in chunks_all]
def passing(lab):
mem = collections.defaultdict(list)
for c, l in lab.items():
if l != 'none': mem[l].append(c)
return {l: m for l, m in mem.items() if recurs(m, story)[0]}
ph = passing({c: hand[c] for c in chunks}); pp = passing({c: pred[c] for c in chunks})
common = set(ph) & set(pp)
lines.append(f' recurring situations: hand {len(ph)}, model {len(pp)}, both {len(common)}')
lines.append(f' model only: {", ".join(sorted(set(pp) - set(ph))) or "-"}')
lines.append(f' hand only: {", ".join(sorted(set(ph) - set(pp))) or "-"}')
for l in sorted(common, key=lambda l: -len(ph[l])):
a, b = set(ph[l]), set(pp[l])
lines.append(f' {l:24s} hand {len(a):3d} model {len(b):3d} overlap {len(a & b):3d} (model precision {len(a & b) / len(b):.0%}, recall {len(a & b) / len(a):.0%})')
return '\n'.join(lines)
if __name__ == '__main__':
out = []
if sys.argv[1:] == ['--selftest']:
lab = json.load(open(HERE / 'labels-ohenry.json'))['labels']; hand = {x['chunk']: x['label'] for x in lab}
out.append(score('ohenry', hand, dict(hand), name='SELFTEST hand vs itself (expect 100% / kappa 1.00)'))
rnd = random.Random(1); ids = sorted(set(hand.values()))
out.append(score('ohenry', hand, {c: rnd.choice(ids) for c in hand}, name='SELFTEST random labels (expect kappa ~0)'))
for f in sys.argv[1:]:
if f.startswith('--'): continue
corpus = pathlib.Path(f).stem.split('-')[0]
lab = json.load(open(HERE / f'labels-{corpus}.json'))['labels']; hand = {x['chunk']: x['label'] for x in lab}
res = json.load(open(f))['results']
minfit = int(next((a.split('=')[1] for a in sys.argv if a.startswith('--minfit=')), 0))
rng = next((a.split('=')[1] for a in sys.argv if a.startswith('--range=')), None)
if rng: lo, hi = map(int, rng.split(':')); res = [r for r in res if lo <= r['chunk'] < hi]
pred = {r['chunk']: (r['label'] if (r.get('fit') or 3) >= minfit else 'none') for r in res}
out.append(score(corpus, hand, pred, {r['chunk']: r['nearest'] for r in res}, {r['chunk']: r['candidates'] for r in res}, name=pathlib.Path(f).stem + (f' fit>={minfit}' if minfit else '') + (f' scenes {rng}' if rng else '')))
print('\n\n'.join(out))
+30
View File
@@ -0,0 +1,30 @@
dev: 40 candidates, 5 labelled 2, rule must accept >= 3 of them
baseline accept all candidates accepted 40 P2 0.12 P1+ 0.62 R2 1.00
baseline B0: tightness filter accepted 17 P2 0.24 P1+ 0.76 R2 0.80
best single-signal rule per signal:
S1 >= 0.6153 accepted 9 P2 0.44 P1+ 0.89 R2 0.80
S2 >= 0.9083 accepted 20 P2 0.15 P1+ 0.60 R2 0.60
S3 >= 0.4580 accepted 31 P2 0.16 P1+ 0.68 R2 1.00
S4 >= 0.0295 accepted 14 P2 0.21 P1+ 0.64 R2 0.60
S5 >= 0.1733 accepted 10 P2 0.30 P1+ 0.60 R2 0.60
S6 <= 0.2137 accepted 33 P2 0.15 P1+ 0.70 R2 1.00
leaderboard (all eligible rules):
S1 >= 0.6119 AND S6 <= 0.1603 accepted 5 P2 0.60 P1+ 1.00 R2 0.60
S1 >= 0.6119 AND S6 <= 0.1742 accepted 5 P2 0.60 P1+ 1.00 R2 0.60
S1 >= 0.6153 AND S6 <= 0.1603 accepted 5 P2 0.60 P1+ 1.00 R2 0.60
S1 >= 0.6153 AND S6 <= 0.1742 accepted 5 P2 0.60 P1+ 1.00 R2 0.60
S1 >= 0.6153 AND S5 >= 0.1733 accepted 5 P2 0.60 P1+ 0.80 R2 0.60
S1 >= 0.6119 AND S6 <= 0.2137 accepted 7 P2 0.57 P1+ 1.00 R2 0.80
S1 >= 0.6119 AND S6 <= 0.3012 accepted 7 P2 0.57 P1+ 1.00 R2 0.80
S1 >= 0.6153 AND S6 <= 0.2137 accepted 7 P2 0.57 P1+ 1.00 R2 0.80
S1 >= 0.6153 AND S6 <= 0.3012 accepted 7 P2 0.57 P1+ 1.00 R2 0.80
S1 >= 0.6153 AND S5 >= 0.1278 accepted 7 P2 0.57 P1+ 0.86 R2 0.80
S1 >= 0.6153 AND S5 >= 0.1318 accepted 7 P2 0.57 P1+ 0.86 R2 0.80
S1 >= 0.6153 AND S5 >= 0.1356 accepted 7 P2 0.57 P1+ 0.86 R2 0.80
CHOSEN: S1 >= 0.6119 AND S6 <= 0.1603 -> rule-frozen.json
accepted on dev: D04(2), D09(1), D13(2), D16(2), D33(1)
exit 0
+26
View File
@@ -0,0 +1,26 @@
"""Cut stories into scene-sized chunks on paragraph boundaries."""
import json, pathlib, re
TARGET = 320 # words; a scene is a situation with people in it
stories = json.loads(pathlib.Path('stories.json').read_text(encoding='utf-8'))
chunks = []
for si, s in enumerate(stories):
paras = [p.strip() for p in re.split(r'\n\s*\n', s['text']) if p.strip()]
buf, n = [], 0
for p in paras:
w = len(p.split())
if n + w > TARGET and buf:
chunks.append({'story': si, 'title': s['title'], 'volume': s['volume'],
'text': ' '.join(buf), 'words': n})
buf, n = [], 0
buf.append(p); n += w
if buf and n >= 80:
chunks.append({'story': si, 'title': s['title'], 'volume': s['volume'],
'text': ' '.join(buf), 'words': n})
pathlib.Path('chunks.json').write_text(json.dumps(chunks), encoding='utf-8')
ws = sorted(c['words'] for c in chunks)
print(f'chunks: {len(chunks)} from {len(stories)} stories')
print(f'words/chunk min {ws[0]} median {ws[len(ws)//2]} max {ws[-1]}')
print(f'total words: {sum(ws):,}')
File diff suppressed because one or more lines are too long
+80
View File
@@ -0,0 +1,80 @@
"""Cluster scene embeddings and report what the clusters actually contain.
The question this answers: does a thematically coherent corpus contain
RECURRING situations, or 800 singletons? A pack needs ~15 repeatable
situations per stage; a cluster that draws from many different stories is a
recurring situation, one that draws from a single story is just a scene.
"""
import json, math, pathlib, random, sys
K = int(sys.argv[1]) if len(sys.argv) > 1 else 40
SEED = 11
chunks = json.loads(pathlib.Path('chunks.json').read_text(encoding='utf-8'))
vecs = json.loads(pathlib.Path('embeddings.json').read_text(encoding='utf-8'))
n = min(len(chunks), len(vecs))
chunks, vecs = chunks[:n], vecs[:n]
print(f'clustering {n} chunks into k={K}\n')
def norm(v):
m = math.sqrt(sum(x * x for x in v)) or 1.0
return [x / m for x in v]
vecs = [norm(v) for v in vecs]
dim = len(vecs[0])
def dot(a, b): return sum(x * y for x, y in zip(a, b))
# k-means++ init on cosine distance (vectors are unit length, so dot == cos)
random.seed(SEED)
cent = [vecs[random.randrange(n)]]
while len(cent) < K:
d2 = [min((1 - dot(v, c)) for c in cent) ** 2 for v in vecs]
tot = sum(d2) or 1.0
r = random.random() * tot
acc = 0.0
for i, x in enumerate(d2):
acc += x
if acc >= r:
cent.append(vecs[i]); break
else:
cent.append(vecs[random.randrange(n)])
assign = [0] * n
for it in range(30):
moved = 0
for i, v in enumerate(vecs):
best, bs = 0, -2.0
for k, c in enumerate(cent):
s = dot(v, c)
if s > bs: bs, best = s, k
if assign[i] != best: assign[i] = best; moved += 1
for k in range(K):
mem = [vecs[i] for i in range(n) if assign[i] == k]
if not mem: continue
cent[k] = norm([sum(m[d] for m in mem) / len(mem) for d in range(dim)])
if moved == 0: break
groups = {}
for i, k in enumerate(assign): groups.setdefault(k, []).append(i)
rows = []
for k, idx in groups.items():
stories = {chunks[i]['story'] for i in idx}
coh = sum(dot(vecs[i], cent[k]) for i in idx) / len(idx)
rows.append({'k': k, 'size': len(idx), 'stories': len(stories), 'coh': coh, 'idx': idx})
rows.sort(key=lambda r: (-r['stories'], -r['size']))
print(f"{'size':>5} {'stories':>8} {'coh':>6} sample titles")
print('-' * 92)
for r in rows:
titles = []
for i in r['idx']:
t = chunks[i]['title']
if t not in titles: titles.append(t)
print(f"{r['size']:5} {r['stories']:8} {r['coh']:6.3f} {', '.join(titles[:4])[:66]}")
multi = [r for r in rows if r['stories'] >= 4]
print(f"\nclusters drawing on >=4 different stories: {len(multi)} of {K}")
print(f"clusters that are essentially one story: {sum(1 for r in rows if r['stories'] <= 2)}")
pathlib.Path('clusters.json').write_text(json.dumps(
[{'k': r['k'], 'size': r['size'], 'stories': r['stories'], 'coh': r['coh'], 'idx': r['idx']} for r in rows]))
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
838 texts, resuming at 0, model nomic-embed-text
16/838 2s (7.22/s)
32/838 3s (10.12/s)
48/838 4s (11.75/s)
64/838 5s (12.87/s)
80/838 6s (13.54/s)
96/838 7s (14.07/s)
112/838 8s (14.48/s)
128/838 9s (14.78/s)
144/838 10s (15.01/s)
160/838 11s (15.15/s)
176/838 12s (15.29/s)
192/838 12s (15.41/s)
208/838 13s (15.52/s)
224/838 14s (15.61/s)
240/838 15s (15.70/s)
256/838 16s (15.77/s)
272/838 17s (15.83/s)
288/838 18s (15.88/s)
304/838 19s (15.91/s)
320/838 20s (15.94/s)
336/838 21s (15.98/s)
352/838 22s (15.99/s)
368/838 23s (16.01/s)
384/838 24s (16.00/s)
400/838 25s (16.00/s)
416/838 26s (16.00/s)
432/838 27s (16.03/s)
448/838 28s (16.04/s)
464/838 29s (16.07/s)
480/838 30s (16.07/s)
496/838 31s (16.07/s)
512/838 32s (16.08/s)
528/838 33s (16.08/s)
544/838 34s (16.08/s)
560/838 35s (16.08/s)
576/838 36s (16.07/s)
592/838 37s (16.06/s)
608/838 38s (16.06/s)
624/838 39s (16.07/s)
640/838 40s (16.05/s)
656/838 41s (16.04/s)
672/838 42s (16.03/s)
688/838 43s (16.02/s)
704/838 44s (16.01/s)
720/838 45s (16.00/s)
736/838 46s (15.98/s)
752/838 47s (15.96/s)
768/838 48s (15.95/s)
784/838 49s (15.94/s)
800/838 50s (15.93/s)
816/838 51s (15.91/s)
832/838 52s (15.90/s)
838/838 53s (15.76/s)
done: 838 vectors, 53s this session
exit 0
+37
View File
@@ -0,0 +1,37 @@
"""Embed texts, checkpointing after every batch so an interrupted run resumes.
python3 embed.py chunks.json embeddings.json # the scenes' prose
python3 embed.py summaries.json summary_embeddings.json # one-line situations
python3 embed.py chunks.json embeddings.json --limit 48 # pilot
Input is a JSON list of objects with a 'text' field (chunks.json), or of
strings/objects with a 'summary' field (summaries.json).
"""
import argparse, json, os, pathlib, time
import ollama
ap = argparse.ArgumentParser()
ap.add_argument('src'); ap.add_argument('dst')
ap.add_argument('--limit', type=int, default=None)
ap.add_argument('--batch', type=int, default=16)
args = ap.parse_args()
MODEL = os.environ.get('STP_EMBED_MODEL', 'nomic-embed-text')
items = json.loads(pathlib.Path(args.src).read_text(encoding='utf-8'))
if isinstance(items, dict): items = items['summaries']
texts = [x if isinstance(x, str) else (x.get('summary') or x['text']) for x in items]
if args.limit: texts = texts[:args.limit]
out = pathlib.Path(args.dst)
state = json.loads(out.read_text()) if out.exists() else {'model': MODEL, 'vectors': []}
if state['model'] != MODEL: raise SystemExit(f'{out} was made with {state["model"]}, not {MODEL}')
done = state['vectors']
print(f'{len(texts)} texts, resuming at {len(done)}, model {MODEL}', flush=True)
t0, start = time.time(), len(done)
for i in range(start, len(texts), args.batch):
done.extend(ollama.embed(MODEL, texts[i:i + args.batch]))
out.write_text(json.dumps(state))
el = time.time() - t0
print(f' {len(done)}/{len(texts)} {el:.0f}s ({(len(done) - start) / max(el, 1):.2f}/s)', flush=True)
print(f'done: {len(done)} vectors, {time.time() - t0:.0f}s this session')
File diff suppressed because one or more lines are too long
@@ -0,0 +1,42 @@
{
"file": "embeddings.json",
"chunks": 838,
"rows": [
{
"k": 25,
"sized": 25,
"median_dominant": 0.25,
"recurring": 21,
"coverage": 0.9284009546539379,
"tight_recurring": 9,
"tight_coverage": 0.2863961813842482
},
{
"k": 40,
"sized": 39,
"median_dominant": 0.35714285714285715,
"recurring": 21,
"coverage": 0.7088305489260143,
"tight_recurring": 5,
"tight_coverage": 0.09069212410501193
},
{
"k": 60,
"sized": 55,
"median_dominant": 0.5714285714285714,
"recurring": 20,
"coverage": 0.5501193317422435,
"tight_recurring": 3,
"tight_coverage": 0.02028639618138425
},
{
"k": 100,
"sized": 78,
"median_dominant": 0.6666666666666666,
"recurring": 16,
"coverage": 0.29594272076372313,
"tight_recurring": 0,
"tight_coverage": 0.0
}
]
}
@@ -0,0 +1,82 @@
qwen3:14b: 250 labelled cluster rows; parse failures 0
theta chosen on dev: 0.6
dev FIT >= 0.3 accepted 29 >=1 19 label2 5 P1+ 0.66 recall 0.76
dev FIT >= 0.4 accepted 28 >=1 18 label2 5 P1+ 0.64 recall 0.72
dev FIT >= 0.5 accepted 14 >=1 10 label2 2 P1+ 0.71 recall 0.40
dev FIT >= 0.6 accepted 8 >=1 7 label2 2 P1+ 0.88 recall 0.28
dev FIT >= 0.7 accepted 4 >=1 4 label2 1 P1+ 1.00 recall 0.16
dev FIT >= 0.8 accepted 2 >=1 2 label2 0 P1+ 1.00 recall 0.08
dev FIT >= 0.9 accepted 0 >=1 0 label2 0 P1+ 0.00 recall 0.00
dev FIT >= 1.0 accepted 0 >=1 0 label2 0 P1+ 0.00 recall 0.00
mean FIT by label (all rows):
label 0: n=121 mean 0.40 median 0.42
label 1: n=106 mean 0.47 median 0.47
label 2: n= 23 mean 0.52 median 0.53
PRIMARY: validate + validate3: 148 clusters, 67 labelled >=1
all accepted 148 >=1 67 label2 10 P1+ 0.45 recall 1.00
G2 accepted 49 >=1 36 label2 5 P1+ 0.73 recall 0.54
G4 FIT >= 0.6 accepted 42 >=1 22 label2 4 P1+ 0.52 recall 0.33
G4 AND G2 accepted 10 >=1 10 label2 1 P1+ 1.00 recall 0.15
G4 size <=9 accepted 17 >=1 5 label2 2 P1+ 0.29 recall 0.56 (of 48)
G4 size 10-25 accepted 14 >=1 7 label2 2 P1+ 0.50 recall 0.23 (of 63)
G4 size >=26 accepted 11 >=1 10 label2 0 P1+ 0.91 recall 0.37 (of 37)
secondary: test + recover: 62 clusters, 37 labelled >=1
all accepted 62 >=1 37 label2 8 P1+ 0.60 recall 1.00
G2 accepted 28 >=1 23 label2 5 P1+ 0.82 recall 0.62
G4 FIT >= 0.6 accepted 15 >=1 9 label2 1 P1+ 0.60 recall 0.24
G4 AND G2 accepted 11 >=1 8 label2 1 P1+ 0.73 recall 0.22
G4 size <=9 accepted 1 >=1 0 label2 0 P1+ 0.00 recall 0.00 (of 8)
G4 size 10-25 accepted 5 >=1 3 label2 1 P1+ 0.60 recall 0.15 (of 30)
G4 size >=26 accepted 9 >=1 6 label2 0 P1+ 0.67 recall 0.35 (of 24)
criterion 1 (P1+ >= 0.80): fail (0.52)
criterion 2 (more >=1 than G2): fail (22 vs 36)
criterion 3 (<=9 members P1+ >= 0.67 if >= 3 accepted): fail (17 accepted, P1+ 0.29)
OVERALL: fail
accepted on the primary set, model situation vs label name:
Y026 size 6 label 0 fit 1.00 model: Someone wants something from someone else, but there is | label:
Y052 size 23 label 2 fit 1.00 model: Planning or attending a dinner with hidden intentions | label: an invitation to dine
X002 size 10 label 1 fit 0.90 model: Someone seeks artistic validation or support from anoth | label: struggling artists and money
Y010 size 18 label 1 fit 0.87 model: Artist seeking financial support or patronage | label: struggling artists and money
Y053 size 45 label 1 fit 0.87 model: Conflict between individuals over personal issues | label: a brawl or showdown
X015 size 38 label 0 fit 0.80 model: Seeking something from someone in a city setting | label:
X030 size 18 label 1 fit 0.80 model: Confrontation between individuals or groups leading to | label: a brawl or showdown
X055 size 21 label 2 fit 0.80 model: Marital conflict and tension | label: a spouse schemes against or confronts a spouse over infidelity
Y008 size 5 label 0 fit 0.80 model: Someone holds power over someone else | label:
Y011 size 15 label 0 fit 0.80 model: Someone confronts someone else over a personal issue | label:
Y034 size 5 label 0 fit 0.80 model: conflict between perception and reality | label:
X038 size 8 label 0 fit 0.75 model: Struggling to find acceptance or connection in a hostil | label:
X048 size 20 label 1 fit 0.73 model: Financial transaction with potential conflict | label: money owed and repayment
X058 size 16 label 0 fit 0.73 model: A host welcomes a visitor with mixed emotions and inten | label:
Y017 size 45 label 1 fit 0.73 model: Invitation to dinner with hidden intentions | label: an invitation to dine
Y073 size 36 label 1 fit 0.73 model: Someone demands information or action from another, who | label: writers and editors
X013 size 7 label 1 fit 0.71 model: Seeking financial advice | label: what to do with money, and whose advice to take
X024 size 7 label 0 fit 0.71 model: Demanding repayment or compensation | label:
Y030 size 7 label 1 fit 0.71 model: Social interaction at a dining establishment | label: who holds sway in a dining room or hotel
X020 size 24 label 0 fit 0.67 model: secretive interactions between individuals | label:
X050 size 45 label 1 fit 0.67 model: Financial transactions involving coercion or manipulati | label: schemes, cons and theft over money
X059 size 19 label 1 fit 0.67 model: Marriage proposals and preparations | label: a proposal or engagement, accepted or refused
X068 size 6 label 2 fit 0.67 model: Marital conflict and confrontation | label: a spouse schemes against or confronts a spouse over infidelity
X072 size 17 label 0 fit 0.67 model: Resentment over unreciprocated affection | label:
Y031 size 6 label 0 fit 0.67 model: Older person mentoring younger person | label:
Y036 size 6 label 0 fit 0.67 model: envy and desire for wealth or power | label:
Y040 size 6 label 1 fit 0.67 model: Competing for social status through dining experiences | label: who holds sway in a dining room or hotel
Y068 size 6 label 0 fit 0.67 model: Conflict between individuals with opposing goals | label:
X010 size 11 label 0 fit 0.64 model: competition for status or position in a city | label:
Y050 size 8 label 0 fit 0.62 model: Someone trying to win someone else's affection | label:
Y063 size 8 label 0 fit 0.62 model: Power dynamics between individuals | label:
X009 size 45 label 1 fit 0.60 model: conflict over marriage proposals and social expectation | label: a proposal or engagement, accepted or refused
X012 size 10 label 0 fit 0.60 model: A person of high status seeking to help someone in need | label:
X014 size 10 label 0 fit 0.60 model: Someone threatens someone else | label:
X037 size 5 label 0 fit 0.60 model: conflict between individuals due to past relationships | label:
X042 size 5 label 2 fit 0.60 model: Dispute over payment for a service | label: a public quarrel with a shopkeeper, driver or official
X043 size 38 label 1 fit 0.60 model: Dinner party with social dynamics | label: who holds sway in a dining room or hotel
X056 size 45 label 1 fit 0.60 model: Attempting to gain favor or influence through social in | label: trying to impress or win someone's favour
X057 size 45 label 1 fit 0.60 model: Someone tries to manipulate someone else for personal g | label: schemes, cons and theft over money
Y021 size 45 label 1 fit 0.60 model: Someone manipulates someone into a transaction for pers | label: schemes, cons and theft over money
Y033 size 5 label 0 fit 0.60 model: Someone is suspected of a crime they did not commit | label:
Y043 size 31 label 1 fit 0.60 model: Confrontation over a dispute or accusation | label: a run-in with the police
@@ -0,0 +1,90 @@
qwen2.5:3b-instruct: 250 labelled cluster rows; parse failures 1
theta chosen on dev: 0.6
dev FIT >= 0.3 accepted 22 >=1 15 label2 4 P1+ 0.68 recall 0.60
dev FIT >= 0.4 accepted 20 >=1 13 label2 3 P1+ 0.65 recall 0.52
dev FIT >= 0.5 accepted 16 >=1 11 label2 2 P1+ 0.69 recall 0.44
dev FIT >= 0.6 accepted 9 >=1 8 label2 2 P1+ 0.89 recall 0.32
dev FIT >= 0.7 accepted 6 >=1 5 label2 2 P1+ 0.83 recall 0.20
dev FIT >= 0.8 accepted 4 >=1 4 label2 2 P1+ 1.00 recall 0.16
dev FIT >= 0.9 accepted 2 >=1 2 label2 1 P1+ 1.00 recall 0.08
dev FIT >= 1.0 accepted 1 >=1 1 label2 0 P1+ 1.00 recall 0.04
mean FIT by label (all rows):
label 0: n=121 mean 0.39 median 0.40
label 1: n=106 mean 0.49 median 0.50
label 2: n= 23 mean 0.42 median 0.33
PRIMARY: validate + validate3: 148 clusters, 67 labelled >=1
all accepted 148 >=1 67 label2 10 P1+ 0.45 recall 1.00
G2 accepted 49 >=1 36 label2 5 P1+ 0.73 recall 0.54
G4 FIT >= 0.6 accepted 50 >=1 24 label2 4 P1+ 0.48 recall 0.36
G4 AND G2 accepted 18 >=1 11 label2 1 P1+ 0.61 recall 0.16
G4 size <=9 accepted 13 >=1 3 label2 1 P1+ 0.23 recall 0.33 (of 48)
G4 size 10-25 accepted 24 >=1 12 label2 3 P1+ 0.50 recall 0.39 (of 63)
G4 size >=26 accepted 13 >=1 9 label2 0 P1+ 0.69 recall 0.33 (of 37)
secondary: test + recover: 62 clusters, 37 labelled >=1
all accepted 62 >=1 37 label2 8 P1+ 0.60 recall 1.00
G2 accepted 28 >=1 23 label2 5 P1+ 0.82 recall 0.62
G4 FIT >= 0.6 accepted 21 >=1 15 label2 0 P1+ 0.71 recall 0.41
G4 AND G2 accepted 14 >=1 12 label2 0 P1+ 0.86 recall 0.32
G4 size <=9 accepted 1 >=1 0 label2 0 P1+ 0.00 recall 0.00 (of 8)
G4 size 10-25 accepted 8 >=1 5 label2 0 P1+ 0.62 recall 0.25 (of 30)
G4 size >=26 accepted 12 >=1 10 label2 0 P1+ 0.83 recall 0.59 (of 24)
criterion 1 (P1+ >= 0.80): fail (0.48)
criterion 2 (more >=1 than G2): fail (24 vs 36)
criterion 3 (<=9 members P1+ >= 0.67 if >= 3 accepted): fail (13 accepted, P1+ 0.23)
OVERALL: fail
accepted on the primary set, model situation vs label name:
X003 size 39 label 0 fit 1.00 model: demand and confrontation | label:
X019 size 45 label 1 fit 1.00 model: tension between unequal parties | label: a public quarrel with a shopkeeper, driver or official
X067 size 23 label 0 fit 1.00 model: power struggle/conflict over resources | label:
Y006 size 45 label 1 fit 1.00 model: manipulation and exploitation | label: schemes, cons and theft over money
Y008 size 5 label 0 fit 1.00 model: power struggle/conflict | label:
Y017 size 45 label 1 fit 1.00 model: coercion and manipulation for personal gain | label: an invitation to dine
Y032 size 13 label 1 fit 1.00 model: confrontation over a personal issue | label: a run-in with the police
Y040 size 6 label 1 fit 1.00 model: people seek social status or attention | label: who holds sway in a dining room or hotel
Y044 size 5 label 0 fit 1.00 model: dinner party | label:
Y047 size 18 label 2 fit 1.00 model: competing for status or position | label: working girls: jobs, wages and standing
Y053 size 45 label 1 fit 1.00 model: confrontation | label: a brawl or showdown
X035 size 45 label 0 fit 0.93 model: Tension between parties | label:
X043 size 38 label 1 fit 0.93 model: dinner party/hosting | label: who holds sway in a dining room or hotel
Y071 size 21 label 1 fit 0.93 model: Negotiation and deal-making | label: schemes, cons and theft over money
Y074 size 11 label 1 fit 0.91 model: courtship and social status | label: working girls: jobs, wages and standing
Y019 size 10 label 0 fit 0.90 model: seeking novelty or adventure | label:
Y063 size 8 label 0 fit 0.88 model: power dynamics in social hierarchies | label:
Y052 size 23 label 2 fit 0.87 model: dinner plans and dynamics | label: an invitation to dine
X028 size 6 label 0 fit 0.83 model: power dynamics in social hierarchies | label:
X012 size 10 label 0 fit 0.80 model: people seeking or offering help or support | label:
Y001 size 23 label 0 fit 0.80 model: Money disputes and transactions | label:
Y066 size 17 label 0 fit 0.80 model: power dynamics in luxury vs. poverty | label:
Y070 size 5 label 0 fit 0.80 model: people pursuing their livelihood or interests | label:
Y002 size 9 label 0 fit 0.78 model: manipulation and hidden agendas | label:
X011 size 43 label 1 fit 0.73 model: NEGOTIATION OVER RESOURCES OR SERVICES | label: drinking in the saloon
X020 size 24 label 0 fit 0.73 model: people hiding or avoiding attention | label:
X033 size 45 label 0 fit 0.73 model: negotiation and payment | label:
X036 size 47 label 1 fit 0.73 model: reveal death to family | label: illness, loss and a missing relative
X048 size 20 label 1 fit 0.73 model: financial transaction or dispute | label: money owed and repayment
X049 size 25 label 0 fit 0.73 model: coercion or manipulation | label:
X055 size 21 label 2 fit 0.73 model: marital conflict | label: a spouse schemes against or confronts a spouse over infidelity
X058 size 16 label 0 fit 0.73 model: welcoming and revealing history | label:
Y011 size 15 label 0 fit 0.73 model: conflict between individuals seeking different outcomes | label:
Y022 size 24 label 1 fit 0.73 model: con or scam | label: schemes, cons and theft over money
Y062 size 45 label 1 fit 0.73 model: Confrontation over a personal relationship | label: a spouse schemes against or confronts a spouse over infidelity
X039 size 11 label 1 fit 0.73 model: Workplace conflict and exploitation | label: working girls: jobs, wages and standing
X024 size 7 label 0 fit 0.71 model: NEGOCIACTION | label:
Y030 size 7 label 1 fit 0.71 model: dining and socializing | label: who holds sway in a dining room or hotel
X053 size 16 label 0 fit 0.67 model: exploit and exploitation | label:
X057 size 45 label 1 fit 0.67 model: NEGOTIATION AND POWER PLAY | label: schemes, cons and theft over money
X059 size 19 label 1 fit 0.67 model: Wedding elopement and deception | label: a proposal or engagement, accepted or refused
X063 size 6 label 0 fit 0.67 model: confrontation | label:
Y048 size 12 label 0 fit 0.67 model: seeking advice or guidance | label:
Y055 size 6 label 0 fit 0.67 model: confrontation | label:
Y020 size 11 label 0 fit 0.64 model: seeking connections or companionship | label:
Y029 size 8 label 0 fit 0.62 model: dinner party with hidden agendas | label:
X041 size 17 label 1 fit 0.60 model: manipulation and control | label: money and marriage: wealthy suitors, inheritances
X042 size 5 label 2 fit 0.60 model: argue over payment | label: a public quarrel with a shopkeeper, driver or official
X045 size 25 label 1 fit 0.60 model: seeking/stuck in job/searching employment | label: struggling artists and money
X073 size 45 label 0 fit 0.60 model: coercion and manipulation | label:
@@ -0,0 +1,42 @@
method acc P1+ P2 #2 distinct dup names cover multi success
B 4 1.00 0.00 0 4 0 13% 0 -
M1 4 1.00 0.00 0 4 0 13% 0 fail
M2 3 1.00 1.00 3 3 0 5% 0 fail
M3 16 0.69 0.00 0 10 1 45% 68 fail
per method, accepted clusters:
B:
R001 size 13 label 1 an inheritance or fortune and what it obliges
R006 size 36 label 1 a spouse schemes against or confronts a spouse over infidelity
R007 size 38 label 1 schemes, cons and theft over money
R012 size 22 label 1 who holds sway in a dining room or hotel
M1:
R001 size 13 label 1 an inheritance or fortune and what it obliges
R006 size 36 label 1 a spouse schemes against or confronts a spouse over infidelity
R007 size 38 label 1 schemes, cons and theft over money
R012 size 22 label 1 who holds sway in a dining room or hotel
M2:
R011 size 14 label 2 money and marriage: wealthy suitors, inheritances
R015 size 15 label 2 a spouse schemes against or confronts a spouse over infidelity
R020 size 17 label 2 someone coerces someone with a threat
M3:
R002 size 30 label 0 -
R003 size 30 label 1 someone coerces someone with a threat
R004 size 28 label 1 a run-in with the police
R005 size 30 label 0 -
R006 size 36 label 1 a spouse schemes against or confronts a spouse over infidelity
R007 size 38 label 1 schemes, cons and theft over money
R008 size 30 label 1 courtship
R009 size 30 label 1 coercion over money: threats, bribes, demands for payment
R010 size 37 label 1 courtship
R013 size 30 label 0 -
R014 size 30 label 1 drinking in the saloon
R016 size 17 label 0 -
R017 size 30 label 1 money owed and repayment
R018 size 39 label 1 trying to impress or win someone's favour
R019 size 30 label 1 who holds sway in a dining room or hotel
R021 size 30 label 0 -
@@ -0,0 +1,90 @@
"""Score G4 (model-judged FIT) against the blind labels (SELECTION.md, "G4").
python3 evaluate_judge.py qwen2.5:3b-instruct
"""
import hashlib, json, pathlib, statistics, sys
import gate2 as g
MODEL = sys.argv[1]
judged = json.loads(pathlib.Path(f"judge-{MODEL.replace(':', '_').replace('/', '_')}.json").read_text())
G2 = json.loads(pathlib.Path('gate2-frozen.json').read_text())
def key(members):
return hashlib.sha1(','.join(map(str, sorted(members))).encode()).hexdigest()[:16]
sources = [('dev', 'candidates-dev.json', 'labels-dev.json', False), ('test', 'candidates-test.json', 'labels-test.json', False),
('recover', 'recover-map.json', 'labels-recover.json', True), ('validate', 'validate-map.json', 'labels-validate.json', True),
('validate3', 'validate3-map.json', 'labels-validate3.json', True)]
rows = []
for name, cfile, lfile, is_map in sources:
cand = json.loads(pathlib.Path(cfile).read_text())
lab = json.loads(pathlib.Path(lfile).read_text())
for cid, v in cand.items():
members = v['members'] if is_map else v
j = judged.get(key(members))
if j is None: continue
sig = g.signals(members)
rows.append({'set': name, 'id': cid, 'label': lab[cid]['label'], 'name': lab[cid]['name'],
'size': len(members), 'fit': j['fit'], 'parsed': j['parsed'], 'situation': j['situation'],
'g2': g.g2_passes(sig, G2)})
def score(sub, accept):
acc = [r for r in sub if accept(r)]
n1 = sum(r['label'] >= 1 for r in acc)
return {'accepted': len(acc), 'label1plus': n1, 'label2': sum(r['label'] == 2 for r in acc),
'P1+': n1 / len(acc) if acc else 0.0,
'recall1+': n1 / max(1, sum(r['label'] >= 1 for r in sub))}
def line(label, s):
return (f" {label:22} accepted {s['accepted']:3} >=1 {s['label1plus']:3} label2 {s['label2']:2} "
f"P1+ {s['P1+']:.2f} recall {s['recall1+']:.2f}")
dev = [r for r in rows if r['set'] == 'dev']
best = None
for theta in (0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0):
s = score(dev, lambda r: r['fit'] >= theta)
if s['accepted'] == 0 or s['P1+'] < 0.85: continue
k = (s['label1plus'], s['label2'], theta)
if best is None or k > best[0]: best = (k, theta)
theta = best[1] if best else None
print(f'{MODEL}: {len(rows)} labelled cluster rows; parse failures {sum(not r["parsed"] for r in rows)}')
print(f'theta chosen on dev: {theta}' + ('' if theta is not None else ' (no theta reaches P1+ >= 0.85 on dev)'))
for t in (0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0):
print(line(f'dev FIT >= {t}', score(dev, lambda r: r['fit'] >= t)))
print('\nmean FIT by label (all rows):')
for lab in (0, 1, 2):
f = [r['fit'] for r in rows if r['label'] == lab]
print(f' label {lab}: n={len(f):3} mean {statistics.mean(f):.2f} median {statistics.median(f):.2f}')
if theta is not None:
g4 = lambda r: r['fit'] >= theta
for title, names in (('PRIMARY: validate + validate3', ('validate', 'validate3')), ('secondary: test + recover', ('test', 'recover'))):
sub = [r for r in rows if r['set'] in names]
print(f'\n{title}: {len(sub)} clusters, {sum(r["label"] >= 1 for r in sub)} labelled >=1')
print(line('all', score(sub, lambda r: True)))
print(line('G2', score(sub, lambda r: r['g2'])))
print(line(f'G4 FIT >= {theta}', score(sub, g4)))
print(line('G4 AND G2', score(sub, lambda r: g4(r) and r['g2'])))
for band, lo, hi in (('<=9', 0, 9), ('10-25', 10, 25), ('>=26', 26, 999)):
b = [r for r in sub if lo <= r['size'] <= hi]
print(line(f' G4 size {band}', score(b, g4)) + f' (of {len(b)})')
prim = [r for r in rows if r['set'] in ('validate', 'validate3')]
s4, s2 = score(prim, g4), score(prim, lambda r: r['g2'])
small = score([r for r in prim if r['size'] <= 9], g4)
c1 = s4['P1+'] >= 0.80
c2 = s4['label1plus'] > s2['label1plus']
c3 = small['accepted'] < 3 or small['P1+'] >= 0.67
print(f'\ncriterion 1 (P1+ >= 0.80): {"PASS" if c1 else "fail"} ({s4["P1+"]:.2f})')
print(f'criterion 2 (more >=1 than G2): {"PASS" if c2 else "fail"} ({s4["label1plus"]} vs {s2["label1plus"]})')
print(f'criterion 3 (<=9 members P1+ >= 0.67 if >= 3 accepted): {"PASS" if c3 else "fail"} '
f'({small["accepted"]} accepted, P1+ {small["P1+"]:.2f})')
print(f'OVERALL: {"PASS" if c1 and c2 and c3 else "fail"}')
print('\naccepted on the primary set, model situation vs label name:')
for r in sorted(prim, key=lambda r: -r['fit']):
if g4(r):
print(f" {r['id']} size {r['size']:2} label {r['label']} fit {r['fit']:.2f} model: {r['situation'][:55]:55} | label: {r['name']}")
pathlib.Path(f"judge-result-{MODEL.replace(':', '_')}.json").write_text(json.dumps(
{'theta': theta, 'primary_G4': s4, 'primary_G2': s2, 'small': small, 'criteria': [c1, c2, c3]}, indent=1))
@@ -0,0 +1,46 @@
"""Score the recall-recovery methods against the blind labels (SELECTION.md, "Recovering recall").
python3 evaluate_recover.py
"""
import collections, json, pathlib
mapping = json.loads(pathlib.Path('recover-map.json').read_text())
labels = json.loads(pathlib.Path('labels-recover.json').read_text())
assert all(labels[r]['label'] in (0, 1, 2) for r in mapping), 'labels-recover.json is incomplete'
N_SCENES = 838
stats = {}
for method in ('B', 'M1', 'M2', 'M3'):
ids = [r for r, e in mapping.items() if method in e['methods']]
lab = [labels[r]['label'] for r in ids]
names = [labels[r]['name'].strip().lower() for r in ids if labels[r]['label'] >= 1]
scenes = collections.Counter(i for r in ids for i in mapping[r]['members'])
stats[method] = {
'accepted': len(ids),
'precision1plus': sum(l >= 1 for l in lab) / len(ids) if ids else 0.0,
'precision2': sum(l == 2 for l in lab) / len(ids) if ids else 0.0,
'label2': sum(l == 2 for l in lab),
'distinct_situations': len(set(names)),
'repeated_names': sum(c - 1 for c in collections.Counter(names).values() if c > 1),
'coverage': len(scenes) / N_SCENES,
'scenes_in_several': sum(v > 1 for v in scenes.values()),
'ids': ids,
}
b = stats['B']
print(f"{'method':6} {'acc':>4} {'P1+':>5} {'P2':>5} {'#2':>3} {'distinct':>8} {'dup names':>9} {'cover':>6} {'multi':>5} success")
for m, s in stats.items():
ok = (m != 'B' and s['accepted'] >= 2 * b['accepted'] and s['precision1plus'] >= 0.80
and s['distinct_situations'] >= 2 * b['distinct_situations'])
print(f"{m:6} {s['accepted']:4} {s['precision1plus']:5.2f} {s['precision2']:5.2f} {s['label2']:3} "
f"{s['distinct_situations']:8} {s['repeated_names']:9} {s['coverage']:6.0%} {s['scenes_in_several']:5} "
f"{'-' if m == 'B' else ('PASS' if ok else 'fail')}")
print('\nper method, accepted clusters:')
for m, s in stats.items():
print(f'\n{m}:')
for r in s['ids']:
e = mapping[r]
print(f" {r} size {len(e['members']):2} label {labels[r]['label']} {labels[r]['name'] or '-'}")
pathlib.Path('recover-result.json').write_text(json.dumps(
{m: {k: v for k, v in s.items() if k != 'ids'} for m, s in stats.items()}, indent=1))
@@ -0,0 +1,6 @@
{
"S1": 0.6036626536817667,
"Z1": 5,
"MMIN": null,
"W": 0.6
}
File diff suppressed because it is too large Load Diff
+182
View File
@@ -0,0 +1,182 @@
"""G2, the size-invariant gate: signals, the old gate for comparison, and k-means. No inference.
Import it from the probe directory. On first import it builds and caches pairwise similarities
(sim.pkl) and the same-size random baseline for Z1 (null.pkl).
S1 mean summary cosine over cross-story member pairs
Z1 S1 as a z-score against random member sets of the same size
MMIN lowest member-level mean cosine to members of other stories
W largest share among words whose sharing is significant (hypergeometric, Bonferroni 0.01)
"""
import collections, json, math, pathlib, pickle, random, re
HERE = pathlib.Path(__file__).resolve().parent
MIN_SIZE, MIN_STORIES, MAX_DOMINANT = 5, 4, 0.40
chunks = json.loads((HERE / 'chunks.json').read_text(encoding='utf-8'))
_raw = json.loads((HERE / 'summary_embeddings.json').read_text())
_vecs = _raw['vectors'] if isinstance(_raw, dict) else _raw
n = min(len(chunks), len(_vecs))
def norm(v):
m = math.sqrt(sum(x * x for x in v)) or 1.0
return [x / m for x in v]
V = [norm(v) for v in _vecs[:n]]
dim = len(V[0])
story = [chunks[i]['story'] for i in range(n)]
text = {x['chunk']: x['summary'] for x in
json.loads((HERE / 'summaries_clean.json').read_text(encoding='utf-8'))['summaries']}
def dot(a, b):
s = 0.0
for x, y in zip(a, b): s += x * y
return s
_sim_path = HERE / 'sim.pkl'
if _sim_path.exists():
SIM = pickle.loads(_sim_path.read_bytes())
else:
SIM = [[0.0] * n for _ in range(n)]
for a in range(n):
va, row = V[a], SIM[a]
for b in range(a, n):
s = dot(va, V[b])
row[b] = s
SIM[b][a] = s
_sim_path.write_bytes(pickle.dumps(SIM))
# Identical to signals.py.
STOP = set("""a an the and or but of to in on at for with by from as into over under about after before
while during than then that this these those it its his her hers him he she they them their theirs we our
you your i me my is are was were be been being has have had do does did not no nor so such who whom whose
which what when where why how all any each both either neither one two some other others another more most
less least very just only also even yet still now up down out off away back again once own same too can
could will would shall should may might must upon despite toward towards between among amid amidst
someone someone's someone’s""".split())
docs = [set(re.findall(r"[a-z][a-z'’]+", text[i].lower())) - STOP for i in range(n)]
df = collections.Counter(w for d in docs for w in d)
ALPHA = 0.01 / len(df)
def describe(m):
counts = collections.Counter(story[i] for i in m)
return len(m), len(counts), max(counts.values()) / len(m)
def eligible(m):
size, nst, dom = describe(m)
return size >= MIN_SIZE and nst >= MIN_STORIES and dom <= MAX_DOMINANT
def cohesion(m):
"""(S1, MMIN)."""
tot, cnt, lowest = 0.0, 0, None
for a in m:
s, c, row = 0.0, 0, SIM[a]
for b in m:
if story[a] != story[b]:
s += row[b]; c += 1
if c:
tot += s; cnt += c
mean = s / c
lowest = mean if lowest is None else min(lowest, mean)
return (tot / cnt if cnt else 0.0), (lowest if lowest is not None else 0.0)
# Same-size random baseline for Z1.
_null_path = HERE / 'null.pkl'
if _null_path.exists():
NULL = pickle.loads(_null_path.read_bytes())
else:
rnd, NULL = random.Random(2026), {}
for m in range(5, 151):
samples, need = [], 300 if m <= 40 else 120
while len(samples) < need:
members = rnd.sample(range(n), m)
if eligible(members):
samples.append(cohesion(members)[0])
mu = sum(samples) / len(samples)
sd = math.sqrt(sum((x - mu) ** 2 for x in samples) / (len(samples) - 1))
NULL[m] = (mu, sd)
_null_path.write_bytes(pickle.dumps(NULL))
def _lchoose(a, b):
return math.lgamma(a + 1) - math.lgamma(b + 1) - math.lgamma(a - b + 1)
def hyper_sf(c, m, k, total):
"""P(X >= c) for X ~ Hypergeometric(total, k successes, m draws)."""
top = min(m, k)
if c > top: return 0.0
logs = [_lchoose(k, x) + _lchoose(total - k, m - x) - _lchoose(total, m)
for x in range(c, top + 1) if m - x <= total - k]
if not logs: return 0.0
mx = max(logs)
return math.exp(mx) * sum(math.exp(l - mx) for l in logs)
def word_dominance(m):
"""(W, word): largest member share among significantly shared words."""
size = len(m)
cw = collections.Counter(w for i in m for w in docs[i])
best = (0.0, '')
for w, c in cw.items():
if c < 2: continue
if hyper_sf(c, size, df[w], n) <= ALPHA and c / size > best[0]:
best = (c / size, w)
return best
def old_s6(m):
"""The first gate's S6, identical to signals.py."""
size = len(m)
cw = collections.Counter(w for i in m for w in docs[i])
best = None
for w, c in cw.items():
if c < 2: continue
score = math.log((c + .5) / (size - c + .5)) - math.log((df[w] + .5) / (n - df[w] + .5))
if best is None or score > best[0]: best = (score, c)
return best[1] / size if best else 0.0
def signals(m):
size, nst, dom = describe(m)
s1, mmin = cohesion(m)
mu, sd = NULL[min(max(size, 5), 150)]
w, word = word_dominance(m)
return {'size': size, 'stories': nst, 'dominant': dom, 'S1': s1, 'Z1': (s1 - mu) / sd,
'MMIN': mmin, 'W': w, 'W_word': word, 'S6_old': old_s6(m)}
OLD_GATE = json.loads((HERE / 'rule-frozen.json').read_text())
_OLD = {s: t for s, _, t in OLD_GATE['terms']}
def old_passes(sig):
return sig['size'] >= MIN_SIZE and sig['stories'] >= MIN_STORIES and sig['dominant'] <= MAX_DOMINANT \
and sig['S1'] >= _OLD['S1'] and sig['S6_old'] <= _OLD['S6']
def g2_passes(sig, gate):
if not (sig['size'] >= MIN_SIZE and sig['stories'] >= MIN_STORIES and sig['dominant'] <= MAX_DOMINANT):
return False
if gate.get('S1') is not None and sig['S1'] < gate['S1']: return False
if gate.get('Z1') is not None and sig['Z1'] < gate['Z1']: return False
if gate.get('MMIN') is not None and sig['MMIN'] < gate['MMIN']: return False
if gate.get('W') is not None and sig['W'] >= gate['W']: return False
return True
def kmeans(k, seed, iters=15):
"""Identical to measure.py's k-means over the summary vectors."""
rnd = random.Random(seed)
cent = [V[i] for i in rnd.sample(range(n), k)]
assign = [-1] * n
for _ in range(iters):
moved = 0
for i, v in enumerate(V):
best, bs = 0, -2.0
for j, c in enumerate(cent):
s = dot(v, c)
if s > bs: bs, best = s, j
if assign[i] != best: assign[i] = best; moved += 1
if moved == 0: break
groups = collections.defaultdict(list)
for i, j in enumerate(assign): groups[j].append(i)
for j, mem in groups.items():
cent[j] = norm([sum(V[i][d] for i in mem) / len(mem) for d in range(dim)])
groups = collections.defaultdict(list)
for i, j in enumerate(assign): groups[j].append(i)
return [sorted(g) for g in groups.values()]
def centroid(m):
return norm([sum(V[i][d] for i in m) / len(m) for d in range(dim)])
@@ -0,0 +1,6 @@
{
"S1": 0.6175176123715982,
"PCT": 0.9,
"MMIN": 0.5295953845017977,
"W": 0.6
}
File diff suppressed because it is too large Load Diff
+40
View File
@@ -0,0 +1,40 @@
"""G3: S1 floor, percentile against the same-size null, mandatory member floor (MMIN), optional W.
Builds on gate2 (similarities, word test, k-means). On first import it samples the same-size null
distribution of S1 and caches the sorted samples in null-samples.pkl.
"""
import bisect, pathlib, pickle, random
import gate2 as g
HERE = pathlib.Path(__file__).resolve().parent
_path = HERE / 'null-samples.pkl'
if _path.exists():
NULLS = pickle.loads(_path.read_bytes())
else:
rnd, NULLS = random.Random(3033), {}
for m in range(5, 151):
need, samples = (1000 if m <= 60 else 200), []
while len(samples) < need:
members = rnd.sample(range(g.n), m)
if g.eligible(members):
samples.append(g.cohesion(members)[0])
NULLS[m] = sorted(samples)
_path.write_bytes(pickle.dumps(NULLS))
def percentile(s1, size):
samples = NULLS[min(max(size, 5), 150)]
return bisect.bisect_left(samples, s1) / len(samples)
def signals(m):
sig = g.signals(m) # size, stories, dominant, S1, Z1, MMIN, W, W_word, S6_old
sig['PCT'] = percentile(sig['S1'], sig['size'])
return sig
def passes(sig, gate):
if not (sig['size'] >= g.MIN_SIZE and sig['stories'] >= g.MIN_STORIES and sig['dominant'] <= g.MAX_DOMINANT):
return False
if sig['S1'] < gate['S1'] or sig['PCT'] < gate['PCT'] or sig['MMIN'] < gate['MMIN']:
return False
if gate.get('W') is not None and sig['W'] >= gate['W']:
return False
return True
+138
View File
@@ -0,0 +1,138 @@
"""A reading page for regrouped predicaments: does each group read as one predicament?
cd v2 && python3 ../groups_page.py predicament_embeddings.json predicaments_clean.json > groups-k60-s24.html
Clusters the embeddings at k = 60, seed 24 (the seed the first review used, kept apart from the
measurement seeds 11-13), keeps the candidate groups (at least 5 scenes, 4 stories, no story over 40%),
and lists them most cohesive first. Each scene shows its predicament sentence, the scene's original
summary underneath, and its story. The reviewer marks each group: one predicament, a few mixed, or not
a predicament, with an optional name and note. Marks are kept in the browser and saved as a JSON file.
Naming a kept group ("A ___ wants ___ from ___") is left for the full review page, once groups read well.
No inference.
"""
import collections, html, json, pathlib, re, sys
from topic_words import TOPIC_WORDS
emb_path, text_path = sys.argv[1], sys.argv[2]
K, SEED = 60, 24
sys.argv = ['measure.py', emb_path]
exec(pathlib.Path(__file__).with_name('measure.py').read_text(encoding='utf-8').split("print(f'{path.name}")[0])
# now defined: chunks, V, n, story, kmeans, describe, MIN_SIZE, MIN_STORIES, MAX_DOMINANT
pred = {x['chunk']: x['summary'] for x in json.loads(pathlib.Path(text_path).read_text(encoding='utf-8'))['summaries']}
orig = {x['chunk']: x['summary'] for x in
json.loads(pathlib.Path('summaries_clean.json').read_text(encoding='utf-8'))['summaries']}
def cross_story_cohesion(group):
tot = cnt = 0
for a in group:
for b in group:
if a < b and story[a] != story[b]:
tot += sum(x * y for x, y in zip(V[a], V[b])); cnt += 1
return tot / cnt if cnt else 0.0
groups = []
for g in kmeans(K, SEED):
size, nst, dom, _ = describe(g)
if size < MIN_SIZE or nst < MIN_STORIES or dom > MAX_DOMINANT:
continue
topics = collections.Counter(w for i in g for w in set(re.findall(r"[a-zé]+", orig.get(i, '').lower())) & TOPIC_WORDS)
word, hits = topics.most_common(1)[0] if topics else ('-', 0)
groups.append({'members': g, 'stories': nst, 'cohesion': cross_story_cohesion(g), 'topic': (word, hits / size)})
groups.sort(key=lambda x: -x['cohesion'])
run = f'{pathlib.Path(emb_path).stem}-k{K}-s{SEED}'
sections = []
for gi, grp in enumerate(groups, 1):
rows = ''.join(
f'<li><span class="p">{html.escape(pred.get(i, ""))}</span>'
f'<span class="o">{html.escape(orig.get(i, ""))}</span>'
f'<span class="t">{html.escape(chunks[i]["title"].title())}</span></li>'
for i in grp['members'])
word, share = grp['topic']
sections.append(f'''<section id="g{gi}" data-g="{gi}">
<h2>Group {gi}</h2>
<p class="meta">{len(grp["members"])} scenes from {grp["stories"]} stories · cohesion {grp["cohesion"]:.3f} ·
most common job, relationship or place word in the original summaries: <b>{html.escape(word)}</b>, {share:.0%} of scenes</p>
<ol>{rows}</ol>
<div class="verdict">
<label><input type="radio" name="v{gi}" value="one"> One predicament</label>
<label><input type="radio" name="v{gi}" value="mixed"> Two or three mixed together</label>
<label><input type="radio" name="v{gi}" value="none"> Not a predicament</label>
<input type="text" class="name" data-g="{gi}" placeholder="Short name, if it is one (optional)" maxlength="80">
<input type="text" class="note" data-g="{gi}" placeholder="Note (optional)" maxlength="300">
</div></section>''')
index = ''.join(f'<a href="#g{i}" data-g="{i}">{i}</a>' for i in range(1, len(groups) + 1))
data = json.dumps({'run': run, 'groups': [{'group': i, 'chunks': grp['members']} for i, grp in enumerate(groups, 1)]})
print(f'''<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Predicament groups</title>
<style>
:root{{--bg:#f7f6f2;--card:#fff;--ink:#222;--dim:#666;--line:#ddd;--one:#dff0e3;--mixed:#fdf1d6;--none:#f6dede;--accent:#2f6f4f}}
@media (prefers-color-scheme:dark){{:root{{--bg:#1b1b1d;--card:#252528;--ink:#e6e6e6;--dim:#9a9a9a;--line:#3a3a3e;--one:#233d2a;--mixed:#443a1c;--none:#472828;--accent:#7cc39c}}}}
body{{background:var(--bg);color:var(--ink);font:15px/1.5 system-ui,sans-serif;margin:0;padding:24px 16px}}
main{{max-width:900px;margin:auto}} h1{{margin:0 0 6px}} .dim,.meta{{color:var(--dim)}} .meta{{font-size:13px;margin:0 0 8px}}
#bar{{position:sticky;top:0;background:var(--bg);border-bottom:1px solid var(--line);padding:8px 0;z-index:1}}
#index{{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}}
#index a{{min-width:26px;text-align:center;font-size:12px;border:1px solid var(--line);border-radius:4px;color:var(--ink);text-decoration:none;padding:1px 3px}}
#index a.one{{background:var(--one)}} #index a.mixed{{background:var(--mixed)}} #index a.none{{background:var(--none)}}
button{{font:inherit;padding:4px 12px;border-radius:6px;border:1px solid var(--accent);background:var(--accent);color:var(--bg);cursor:pointer}}
section{{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:12px 16px;margin:16px 0}}
section.one{{border-left:6px solid #3a9a63}} section.mixed{{border-left:6px solid #c9962b}} section.none{{border-left:6px solid #c45050}}
h2{{margin:0 0 2px;font-size:18px}} ol{{padding-left:22px;margin:6px 0}} li{{margin:0 0 8px}}
.p{{display:block}} .o{{display:block;font-size:13px;color:var(--dim)}} .t{{display:block;font-size:12px;color:var(--dim);font-style:italic}}
.verdict{{display:flex;flex-wrap:wrap;gap:6px 14px;align-items:center;border-top:1px solid var(--line);padding-top:8px}}
.verdict input[type=text]{{flex:1 1 220px;min-width:0;font:inherit;padding:4px 6px;border:1px solid var(--line);border-radius:4px;background:var(--bg);color:var(--ink)}}
</style>
<main>
<h1>Predicament groups</h1>
<p class="dim">Every scene was re-described as its main person's predicament and the scenes were regrouped by
those sentences. Each line below is one scene: its predicament, the scene's original summary under it, and
its story. Read a group and mark whether most of its scenes are <b>one predicament</b> (the same kind of
trouble, and wanting the same kind of way out), <b>two or three mixed together</b>, or <b>not a
predicament</b> (held together by wording, a topic, or nothing). Most cohesive groups first; the first ten
or so are enough to judge. Marks save in this browser as you go; "Save marks" downloads them for the program.</p>
<div id="bar"><span id="tally"></span> <button id="save">Save marks</button><div id="index">{index}</div></div>
{"".join(sections)}
</main>
<script>
const DATA={data.replace("</", "<\\\\/")};
const KEY='stp-groups:'+DATA.run;
let marks={{}}; try{{marks=JSON.parse(localStorage.getItem(KEY)||'{{}}')}}catch(e){{}}
function mark(g){{return marks[g]||(marks[g]={{verdict:'',name:'',note:''}})}}
function paint(){{
const c={{one:0,mixed:0,none:0}};
for(const {{group:g}} of DATA.groups){{
const m=marks[g]||{{}}, v=m.verdict||'';
if(v) c[v]++;
document.getElementById('g'+g).className=v;
document.querySelector(`#index a[data-g="${{g}}"]`).className=v;
}}
const done=c.one+c.mixed+c.none;
document.getElementById('tally').textContent=`Marked ${{done}} of ${{DATA.groups.length}}: one predicament ${{c.one}}, mixed ${{c.mixed}}, not ${{c.none}}`;
}}
function store(){{try{{localStorage.setItem(KEY,JSON.stringify(marks))}}catch(e){{}} paint()}}
for(const [g,m] of Object.entries(marks)){{
const r=document.querySelector(`input[name="v${{g}}"][value="${{m.verdict}}"]`); if(r) r.checked=true;
const n=document.querySelector(`.name[data-g="${{g}}"]`); if(n) n.value=m.name||'';
const t=document.querySelector(`.note[data-g="${{g}}"]`); if(t) t.value=m.note||'';
}}
document.addEventListener('change',e=>{{
if(e.target.type==='radio'){{mark(e.target.name.slice(1)).verdict=e.target.value; store()}}
}});
document.addEventListener('input',e=>{{
const g=e.target.dataset.g; if(!g) return;
mark(g)[e.target.classList.contains('name')?'name':'note']=e.target.value; store();
}});
document.getElementById('save').onclick=()=>{{
const out={{run:DATA.run,saved:new Date().toISOString(),groups:DATA.groups.map(x=>({{...x,...(marks[x.group]||{{verdict:'',name:'',note:''}})}}))}};
const a=document.createElement('a');
a.href=URL.createObjectURL(new Blob([JSON.stringify(out,null,1)],{{type:'application/json'}}));
a.download=`groups-marks-${{DATA.run}}.json`; a.click(); URL.revokeObjectURL(a.href);
}};
paint();
</script>''')
@@ -0,0 +1,43 @@
# Jacobs reference set: *The Lady of the Barge* (1902)
A hand-made comparison baseline for the story-to-pack pipeline, the Jacobs twin of the O. Henry
reference in `../v2/claude/`. Every summary, predicament, group and option here was written by
Claude reading the scenes. **No local inference was used.** Compare the local-model runs against it.
Do not treat it as ground truth: it is one careful reading.
| File | What it is |
| --- | --- |
| `corpus/lady-of-the-barge.html` | Standard Ebooks single-page text (public domain) |
| `split_se.py` | → `stories.json` (12 stories, split on `<h2>`) and `chunks.json` (157 scenes, same 320-word paragraph rule as `../chunk.py`) |
| `dump.py` | `python3 dump.py A B` prints scenes A..B for reading |
| `claude/batches/sNN.json` | per story: `{chunk, story, summary, predicament}`, the same schema as `../v2/claude/batches/` |
| `claude/groups_src.py` | the groups, options and per-story protagonist standing |
| `claude/build.py` | checks everything, then writes `claude/records.json` and `claude/groups.json` |
| `claude/page.py` | → `claude/REFERENCE.html`, the readable page |
Rebuild: `python3 split_se.py && cd claude && python3 build.py && python3 page.py`.
## What is in it
- **157 predicaments**: one sentence each (median 19 words, max 24), written to the `redescribe.py`
rules: no names, jobs, places or story-only objects. The `topic_words` leak check flags 23 of 157.
All 23 are role words (rival, partner, guest, neighbour, suitor, newcomer), kept on purpose because
the design's cast is roles.
- **19 groups**, each a situation that recurs in **two or more stories**. Each scene sits in at most
one group: 137 are assigned and 20 fit nothing that recurs.
- **Options**: 3 to 5 per group, 79 in all. 61 come from what someone in a scene did (`scene:N`),
10 are archetypes and 8 fill gaps. **8 scene options cite a scene outside their own group.** That
is looser than DESIGN.md's "drawn from other scenes in the cluster", and `groups.json` marks each
one `in_group: false`.
- **Moves**: spine paths with a direction only (`+`, `-`, `risk`, `set`). There are no magnitudes,
as the design requires. Skill labels proposed for this corpus: `nerve` and `cunning`.
- **Stage** comes from protagonist standing, judged **per story, not per scene**. That is the
coarsest judgement in the set, recorded in `STANDING` in `groups_src.py`.
## How this corpus differs from O. Henry
It is small, 12 stories against 97. It also mixes registers: comic waterfront and village yarns,
plus four horror or crime stories (*The Monkey's Paw*, *The Well*, *In the Library*,
*Captain Rogers*). So several groups straddle the two registers. G09 "Your scheme turns on you"
holds both a comic fake-drowning and a murderer caught out. A tool that groups by register or
mood, rather than by predicament, should split them.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,86 @@
[
{
"chunk": 0,
"story": 0,
"summary": "A skipper waiting up for his late-returning mate and brother-in-law has just lost a shouted argument with a neighbouring crew; the mate arrives and answers his rebuke with cheek.",
"predicament": "Someone waiting up for a relative who comes in late wants to reproach him, and gets cheek back."
},
{
"chunk": 1,
"story": 0,
"summary": "The mate tells the skipper he has invited his sweetheart on the voyage without asking; the skipper is appalled and asks what his wife will say.",
"predicament": "A man learns a relative has promised a stranger his hospitality without asking, and fears what his spouse will say."
},
{
"chunk": 2,
"story": 0,
"summary": "The skipper tries to refuse the young woman passage but cannot bring himself to say so, and ends up eating supper with the couple.",
"predicament": "A man who wants to turn an unwelcome guest away cannot find the nerve to say it."
},
{
"chunk": 3,
"story": 0,
"summary": "The girl balks at her cramped quarters but stays; the skipper warns that his wife will blame him, and the mate says his sister can mind her own business.",
"predicament": "A man who has let something happen under his roof fears his spouse will hold him responsible for it."
},
{
"chunk": 4,
"story": 0,
"summary": "The skipper insists on chaperoning the girl at meals himself, leaving the mate to steer and eat cold leftovers while she laughs at the skipper's stories.",
"predicament": "A young man who brought the person he is courting along is pushed aside by an older man who takes charge of her."
},
{
"chunk": 5,
"story": 0,
"summary": "Jealous, the mate hints at the skipper's wife, is mocked by the pair, is rude to the girl, and is sent away.",
"predicament": "A jealous suitor tries to put his rival in his place, and only makes himself look childish."
},
{
"chunk": 6,
"story": 0,
"summary": "The girl snubs the mate as 'only the mate'; he turns the tables by threatening to let the skipper explain her presence to his wife alone.",
"predicament": "A spurned man realises he holds something over the rival who humiliated him, and decides to use it."
},
{
"chunk": 7,
"story": 0,
"summary": "The mate refuses to back the skipper's story; the skipper hides the girl below as they arrive to find his wife waiting on the quay.",
"predicament": "A man hiding an innocent but compromising secret is met by his spouse, while the one person who could clear him refuses to."
},
{
"chunk": 8,
"story": 0,
"summary": "The mate pulls meaningful faces behind the skipper's back until the wife grows suspicious; the skipper invents a toothache and tries to steer her away.",
"predicament": "A man tries to hurry his spouse away from a secret while someone signals behind his back that something is up."
},
{
"chunk": 9,
"story": 0,
"summary": "The wife stamps her foot, the hidden girl appears, and the mate lets his brother-in-law take the blame while pretending to shield him.",
"predicament": "A man caught with an innocent secret is disowned by the one person who could explain it."
},
{
"chunk": 10,
"story": 0,
"summary": "The wife abuses the girl, knocks her purse into the water, and orders her off; the mate, now remorseful, takes responsibility and defends her.",
"predicament": "A man whose spite has got an innocent person abused must decide whether to own up and defend her."
},
{
"chunk": 11,
"story": 0,
"summary": "The mate dives repeatedly for the purse to win the girl back; she begs him to come out but refuses to make up.",
"predicament": "A man tries to win back someone he wronged with a dangerous public show of devotion."
},
{
"chunk": 12,
"story": 0,
"summary": "The mate fakes drowning; the skipper rescues him, sees him wink while the girl kisses him, and the wife boxes his ears.",
"predicament": "A man pretends to be in danger to win sympathy, and the one who saved him sees through it."
},
{
"chunk": 13,
"story": 0,
"summary": "The wife takes the girl home to breakfast, leaving both men soaked, while the skipper fends off the mate's apologies with a boathook.",
"predicament": "Two men who quarrelled over a woman are both left out in the cold by the women they wronged."
}
]
@@ -0,0 +1,15 @@
[
{"chunk":14,"story":1,"summary":"An old man losing at chess to his son grumbles at the weather and their remote house while waiting for a guest, who arrives: an old friend back from long service abroad.","predicament":"A man losing a friendly contest tries to distract from his mistake, and is caught at it by his family."},
{"chunk":15,"story":1,"summary":"Warmed by whisky, the guest tells tales of distant lands; pressed by his host, he reluctantly produces a dried monkey's paw he calls magic.","predicament":"A guest who wants to leave a dangerous subject alone is coaxed by his hosts' curiosity into bringing it out."},
{"chunk":16,"story":1,"summary":"The guest explains the paw grants three men three wishes each, that he has had his, and that the last owner wished for death; asked if he would wish again, he wavers.","predicament":"Someone offered a thing that grants wishes is warned by the man who used it that it brings sorrow."},
{"chunk":17,"story":1,"summary":"The guest throws the paw on the fire; the host snatches it back and keeps it despite repeated warnings to wish sensibly or not at all.","predicament":"A man is offered a dangerous gift by someone who begs him to destroy it, and wants to keep it anyway."},
{"chunk":18,"story":1,"summary":"Egged on by his joking son, the old man wishes for two hundred pounds to clear the house; the paw twists in his hand, but no money appears.","predicament":"A man half-believing in a promise of easy money must decide whether to test it and what to ask for."},
{"chunk":19,"story":1,"summary":"An uneasy night passes; in the morning the family laugh off their fears, though the father recalls that the wishes come true in ways that look like coincidence.","predicament":"A family that tempted fate laughs off their fears in daylight while one of them cannot shake them."},
{"chunk":20,"story":1,"summary":"The son leaves for work; later the mother, half-hoping for money, sees a well-dressed stranger hesitate several times at the gate before coming in.","predicament":"A family waiting on a hoped-for windfall sees a stranger approach who might bring it, or bad news."},
{"chunk":21,"story":1,"summary":"The stranger, from the son's employers, tells the parents their only son has been killed in the machinery.","predicament":"Parents receive news that their only child has been killed, from someone who cannot bring himself to say it."},
{"chunk":22,"story":1,"summary":"The firm denies liability but offers compensation of exactly two hundred pounds; the father collapses. They bury their son and sink into silent grief.","predicament":"A man learns the money he wished for has come as compensation for his child's death."},
{"chunk":23,"story":1,"summary":"A week later the grieving mother remembers the paw's other two wishes and demands that her husband wish their son alive; he calls it madness.","predicament":"A grieving woman seizes on a desperate chance to undo a death, and her spouse is afraid of what it would bring back."},
{"chunk":24,"story":1,"summary":"The father tells her their son was mutilated beyond recognition, but she insists; terrified, he fetches the paw and makes the second wish.","predicament":"A man pressed by his grieving spouse must decide whether to do something he believes is wicked and dangerous."},
{"chunk":25,"story":1,"summary":"Nothing seems to happen; then a stealthy knock comes at the door, and the mother cries that it is their son while the father holds her back.","predicament":"A man who dreads what his wish may bring home must stop his spouse from opening the door to it."},
{"chunk":26,"story":1,"summary":"The mother breaks free and struggles with the bolt; the father finds the paw and makes the third wish, and the door opens on an empty road.","predicament":"A man must use his last chance to undo what his spouse most longs for, before she lets it in."}
]
@@ -0,0 +1,16 @@
[
{"chunk":27,"story":2,"summary":"An old watchman recalls a shipmate, an aged miser worth six hundred pounds, who fell ill a month out on the voyage home.","predicament":"A man who has hoarded his savings all his life falls ill far from anywhere he could spend them."},
{"chunk":28,"story":2,"summary":"The dying miser refuses to believe he is dying and leave his money; his shipmates circle hopefully, and one finds a money belt on him and guesses his plan to be buried with it.","predicament":"A dying miser, surrounded by people hoping to inherit, is determined that nobody will get his money."},
{"chunk":29,"story":2,"summary":"The miser dies boasting that he had a boy throw his banknotes overboard; one sailor scares the boy into silence, and the narrator threatens to tell unless let in on the scheme.","predicament":"Someone who spots a colleague quietly scheming to take unclaimed money must decide whether to expose him or join in."},
{"chunk":30,"story":2,"summary":"The schemer reveals he thinks the boy kept the money; the two agree to split it, and search the boy's bunk and pockets at night without success.","predicament":"Two partners convinced a weaker person is secretly holding a fortune want to find it before anyone else learns of it."},
{"chunk":31,"story":2,"summary":"A night search is foiled when a ticklish shipmate wakes screaming; a week of prodding the boy finds nothing, so they corner him and demand the money outright.","predicament":"A schemer whose secret searches keep failing decides to confront the person he suspects directly."},
{"chunk":32,"story":2,"summary":"The boy insists he threw the money overboard; they search him, offer him a third, then threaten to have him searched ashore and jailed.","predicament":"Someone suspected of hiding a windfall is pressed with bribes and threats by people who have no right to it."},
{"chunk":33,"story":2,"summary":"When the sailor heads to tell the captain, the boy gives in: he kept the money but, frightened by the searches, hid it in an officer's mattress where he cannot now reach it.","predicament":"Someone forced to share a hidden windfall admits it is now stuck where neither partner can get at it."},
{"chunk":34,"story":2,"summary":"Neither the man nor the boy can get aft unseen, the officer sleeps on the money every night, and the two partners blame each other.","predicament":"Distrustful partners stare at a fortune they cannot reach without getting caught, and blame each other for it."},
{"chunk":35,"story":2,"summary":"The boy proposes the man fake an accident to get quartered aft; the man refuses and secretly flatters the officer, asking to buy his mattress as a keepsake.","predicament":"A man wants something a superior owns without revealing why, and tries flattery to get it."},
{"chunk":36,"story":2,"summary":"The officer plays up the mattress's value and the sailor, afraid to seem eager, settles at two pounds, to be handed over ashore.","predicament":"A buyer who cannot reveal why he wants something is made to pay more by a seller who senses his eagerness."},
{"chunk":37,"story":2,"summary":"The sailor now plans to cut the boy out and split with the narrator alone, but the suspicious boy shadows him all the way into port.","predicament":"A man who wants to cheat his partner out of a shared prize is watched too closely to slip away."},
{"chunk":38,"story":2,"summary":"The officer delays the hand-over with a long speech until the boy arrives, sees the sack, and refuses to let them leave without his share.","predicament":"A man trying to slip away with a shared prize is held up until the partner he meant to cheat catches him."},
{"chunk":39,"story":2,"summary":"The boy stays on deck while the men cut the mattress open below; it is empty, and when they come up the boy has vanished.","predicament":"A man who schemed to cheat a partner finds the prize gone and realises he may have been cheated himself."},
{"chunk":40,"story":2,"summary":"The boy never returns, not even for his pay, and the officer smilingly asks the sailor how he slept.","predicament":"A schemer who was outwitted must stand and smile at the people who got the better of him."}
]
@@ -0,0 +1,16 @@
[
{"chunk":41,"story":3,"summary":"A rich man soon to marry sits with his idle, indebted cousin, who needles him about his wealth and his fiancée's goodness.","predicament":"A man about to marry is needled by a dependent relative who hints at something to his discredit."},
{"chunk":42,"story":3,"summary":"The cousin hints that the fiancée would drop him if she knew what he really was, then says he needs fifteen hundred pounds within a fortnight or faces prison.","predicament":"A man in desperate debt makes veiled hints to the relative he hopes will pay it."},
{"chunk":43,"story":3,"summary":"Refused, the cousin offers to sell compromising old letters, threatening to sell them to a rival suitor; the rich man seizes him and is stopped only by a servant entering.","predicament":"A man is blackmailed by a relative with old letters that could cost him the person he loves."},
{"chunk":44,"story":3,"summary":"The cousin walks out into the park to let him think it over; the rich man follows, and later tells his mother the cousin has gone for good after a quarrel over money.","predicament":"A man who has done something terrible in anger must cover for a relative's sudden disappearance."},
{"chunk":45,"story":3,"summary":"The man walks with his fiancée to a disused well in the park, her favourite spot; he urges her not to sit on its edge.","predicament":"A man with a guilty secret must keep calm while someone he loves insists on lingering right at the spot."},
{"chunk":46,"story":3,"summary":"She asks about the missing cousin and defends him; the man bursts out that the cousin was a blackmailer, a liar and a cur.","predicament":"A man hiding a crime hears it discussed innocently and betrays his feelings with an outburst."},
{"chunk":47,"story":3,"summary":"She is startled by a whisper she imagines saying 'Jem, help me out'; shaken, he tries to take her indoors, but she insists on staying.","predicament":"A guilty man tries to hurry someone away from the scene of his crime, and she insists on staying."},
{"chunk":48,"story":3,"summary":"She drops her mother's diamond bracelet down the well and wants it drained; he looks at her in horror, then covers it by saying he feared she was falling.","predicament":"A man hiding something learns it may be discovered by an innocent accident, and must not show his fear."},
{"chunk":49,"story":3,"summary":"He promises to recover the bracelet himself by morning and makes her swear to tell no one; his mother suggests filling the well in.","predicament":"A man must retrieve something from where his secret lies before anyone else goes looking."},
{"chunk":50,"story":3,"summary":"At night he secretly fishes the well with a line and hook, and dredges up keys instead of the bracelet.","predicament":"A man secretly searching where his crime lies keeps pulling up reminders of it."},
{"chunk":51,"story":3,"summary":"The hook snags; he gives up, spends a sleepless night, and at dawn meets his grooms, whom he has ordered to lower him into the well.","predicament":"A man who has failed to fix things quietly must now involve others while still keeping his secret."},
{"chunk":52,"story":3,"summary":"His groom offers to go down instead, since the master looks ill; he refuses curtly and starts to test the air with a candle.","predicament":"A man hiding something is offered help he cannot accept without being found out."},
{"chunk":53,"story":3,"summary":"The candle is lost; he stops the groom fetching another, strips, and sits a long time on the well's edge before going down.","predicament":"A man who must face the thing he did alone gathers his nerve while others watch and wonder."},
{"chunk":54,"story":3,"summary":"Below, something drags at the rope; the grooms haul up the dead cousin's face with their master behind it, let go in horror, and both fall back into the water.","predicament":"A man who hid a crime is dragged down by it at the moment he tries to undo its consequences."}
]
@@ -0,0 +1,74 @@
[
{
"chunk": 55,
"story": 4,
"summary": "A farmer's wife, her husband, and her retired-policeman uncle discuss a stolen brooch; the uncle has been hiding under their bed hoping to catch the thief, and the wife casts suspicion on a neighbour while accusing her husband of not caring.",
"predicament": "A woman who has lost something valuable suspects the people around her, and resents her spouse for not caring as much."
},
{
"chunk": 56,
"story": 4,
"summary": "The uncle declares everyone guilty until proven innocent; the wife recounts missing the brooch, accusing and sacking her servant, who cried on her box.",
"predicament": "Someone who has lost something accuses the most defenceless person nearby, and punishes her before any proof."
},
{
"chunk": 57,
"story": 4,
"summary": "The servant's box held nothing; the husband defends her and is snapped at, while the uncle, pencil and notebook out, pursues the investigation with great ceremony.",
"predicament": "A man who thinks a household fuss is foolish keeps defending the accused and is scolded for taking her side."
},
{
"chunk": 58,
"story": 4,
"summary": "The husband eats a lozenge from the evidence box and cannot stop laughing; the uncle establishes that no one could have entered the bedroom unheard.",
"predicament": "A man who finds a family inquiry ridiculous cannot keep a straight face in front of its self-important leader."
},
{
"chunk": 59,
"story": 4,
"summary": "Suspicion turns to a widow who visited that morning; the uncle finds muddy footprints on the stairs, which are in fact the husband's, and he stays guiltily quiet.",
"predicament": "A man who sees an innocent neighbour blamed for evidence he himself left must decide whether to own up."
},
{
"chunk": 60,
"story": 4,
"summary": "The husband slyly deflects the footprint evidence, then exchanges silent signals with his wife until the uncle demands to know what is funny.",
"predicament": "A man who knows more than he says can barely hide his amusement from the self-important person running the inquiry."
},
{
"chunk": 61,
"story": 4,
"summary": "The husband reveals he thinks the widow is after the uncle, not the brooch; the uncle is outraged, then proposes inviting her to tea to trap her into confessing.",
"predicament": "A self-important man, teased that a neighbour fancies him, resolves to prove his point by trapping her."
},
{
"chunk": 62,
"story": 4,
"summary": "The husband refuses to have a guest trapped in his house; the uncle secretly tells his niece he will search the widow's cottage without leave.",
"predicament": "A man refuses to let his home be used to entrap a friend, so the schemer plans to go behind his back."
},
{
"chunk": 63,
"story": 4,
"summary": "The husband, who resents the uncle living with them for his wife's hopes of an inheritance, plans to let the widow catch the uncle searching her house, and shadows him.",
"predicament": "A man stuck with an unwelcome relative in his home sees a chance to have him disgraced and driven off."
},
{
"chunk": 64,
"story": 4,
"summary": "The husband brings the widow home early; the uncle, trapped inside, hides in the larder, and the husband settles in for beer and begins praising him.",
"predicament": "A man who has trapped his rival in an embarrassing hiding place decides to make the most of it."
},
{
"chunk": 65,
"story": 4,
"summary": "Within the uncle's hearing, the husband tells the widow the uncle is shy and devoted to her, and has been asking whether she likes jewellery.",
"predicament": "A trapped man must listen in silence while someone tells a neighbour a story about him that he cannot deny."
},
{
"chunk": 66,
"story": 4,
"summary": "Asked for bread and cheese, the widow opens the larder, the uncle bursts out, and her friend takes his hiding there as a bashful suitor's courtship.",
"predicament": "A man caught somewhere he should not be is handed an embarrassing explanation, and has no better one to offer."
}
]
@@ -0,0 +1,15 @@
[
{"chunk":67,"story":5,"summary":"Late at night one co-owner of a house and a business sits hating his partner, who has discovered his embezzlement and ordered him to leave both by morning.","predicament":"A man found out in wrongdoing is ordered by his partner to give up everything and leave by morning."},
{"chunk":68,"story":5,"summary":"The honest partner refuses to cover the theft quietly, offers the exile two hundred pounds, and mentions the servants are away until morning.","predicament":"A man offered a harsh but fair way out of his disgrace wants a quieter bargain the other will not give."},
{"chunk":69,"story":5,"summary":"The exile demands the matter be hushed up, draws a sword from the wall, and stabs his partner when he refuses.","predicament":"A man facing ruin threatens the only person who can expose him, and must choose whether to go through with it."},
{"chunk":70,"story":5,"summary":"Sickened, the killer turns out the lights, flees to the gate, and sees a constable coming up the road.","predicament":"A man who has just done something terrible in rage panics and must steady himself when a figure of the law appears."},
{"chunk":71,"story":5,"summary":"He bids the constable good night, returns inside, and gathers his cash and valuables, reckoning he has eight hours' start.","predicament":"A man with a crime behind him and a few hours' start must decide what to take and where to run."},
{"chunk":72,"story":5,"summary":"He plans his escape, weighing a crowded district against the seaside, and finds every choice may mean life or death.","predicament":"A fugitive planning his escape finds that every choice might be the one that gets him caught."},
{"chunk":73,"story":5,"summary":"At one o'clock he hears creaks on the stairs, and fears the dead man is moving about the house.","predicament":"A man alone with what he has done hears noises in the dark and fears it has come back for him."},
{"chunk":74,"story":5,"summary":"Finding the library door open, he locks someone inside, then realises it is a burglar he can blame for the killing.","predicament":"A guilty man realises a stranger's blunder gives him someone else to blame."},
{"chunk":75,"story":5,"summary":"He fires through the door, raises the alarm, and the police break in and seize the terrified intruder.","predicament":"A man framing an innocent person must act the outraged victim in front of the law."},
{"chunk":76,"story":5,"summary":"The intruder swears the body was there when he arrived; the sergeant uncovers it, and the killer feigns horror.","predicament":"A man must play the shocked innocent while the person he framed pleads the truth."},
{"chunk":77,"story":5,"summary":"The killer names his friend and accuses the intruder, who accuses him back; the sergeant seems to side with the killer.","predicament":"Someone falsely accused points to the real culprit, who must hold his nerve and keep up his story."},
{"chunk":78,"story":5,"summary":"The sergeant takes the pistol, handcuffs the killer without warning, and sends for a doctor, since the victim is not dead.","predicament":"A man who thought he had got away with it learns the one witness who could hang him is still alive."},
{"chunk":79,"story":5,"summary":"The dying partner opens his eyes and speaks; the sergeant takes notes and leads the killer away.","predicament":"A man whose deceit has collapsed can only watch as the truth comes out and he is taken away."}
]
@@ -0,0 +1,86 @@
[
{
"chunk": 80,
"story": 6,
"summary": "A ragged stranger swaggers into the best inn in a small town, bullies the waiter who tries to turn him out, and demands to see the landlord.",
"predicament": "A penniless newcomer forces his way into a place he does not belong, counting on a secret to back him."
},
{
"chunk": 81,
"story": 6,
"summary": "The stranger greets the landlord by another name, a wanted pirate's, and demands clothes, lodging and money in return for silence.",
"predicament": "A man who has built a respectable new life is found by an old associate who threatens to expose his past."
},
{
"chunk": 82,
"story": 6,
"summary": "The landlord gives him money; the blackmailer draws a pistol, declares he will take half of everything from now on, and settles in.",
"predicament": "A man pays off a blackmailer and learns the price will be half of everything, for good."
},
{
"chunk": 83,
"story": 6,
"summary": "The landlord says he has lived honestly for fifteen years and warns the man not to wake the devil in him; the blackmailer meets the landlord's stepdaughter and says he will stay.",
"predicament": "A reformed man warns a blackmailer not to push him too far, while the blackmailer sets his sights on the man's family."
},
{
"chunk": 84,
"story": 6,
"summary": "The blackmailer orders the best supper and room, and sleeps with his door barricaded and a pistol under his pillow.",
"predicament": "A man who has seized power over another knows the danger he is in, and guards himself even as he gloats."
},
{
"chunk": 85,
"story": 6,
"summary": "He pushes in at the family breakfast table, eyes the stepdaughter, hints at the landlord's past before her, and goes out to buy new clothes.",
"predicament": "A household must make polite space for someone who holds them hostage."
},
{
"chunk": 86,
"story": 6,
"summary": "Within a month the blackmailer runs the inn; the landlord, failing in health, backs him in sacking a loyal old servant, and his daughter demands to know who is master.",
"predicament": "Someone watches a person she trusts give way at every turn to an intruder, and demands that he stand up for himself."
},
{
"chunk": 87,
"story": 6,
"summary": "The landlord sides with the blackmailer, who threatens to ruin the girl and have the landlord hanged; the landlord pleads for peace.",
"predicament": "A man who is being blackmailed hears his tormentor threaten someone he loves, and must choose between swallowing it and hitting back."
},
{
"chunk": 88,
"story": 6,
"summary": "The inn decays under the blackmailer's rule; the landlord collapses with a fit, which upsets the blackmailer's plans.",
"predicament": "A blackmailer's hold depends on his victim's life, and that victim suddenly collapses."
},
{
"chunk": 89,
"story": 6,
"summary": "The landlord lies dying, anxious to please the blackmailer, while his daughter nurses him; the blackmailer resolves to get the hidden money before he dies.",
"predicament": "A blackmailer whose victim is dying must grab what he can before his hold disappears."
},
{
"chunk": 90,
"story": 6,
"summary": "The blackmailer threatens to inform; the dying man begs for time and promises to reveal his money that night if his daughter gets a share.",
"predicament": "A dying man pressed by his tormentor bargains for time and for provision for someone he loves."
},
{
"chunk": 91,
"story": 6,
"summary": "At night the blackmailer drugs the nurse and demands the money; the landlord extracts his promise about the daughter.",
"predicament": "A man who thinks his helpless victim is at his mercy leans in close to hear the secret."
},
{
"chunk": 92,
"story": 6,
"summary": "The 'dying' landlord seizes and strangles him, revealing he has been shamming weakness and planning to blame the killing on the blackmailer's own followers.",
"predicament": "A man who pretended to be helpless strikes at the tormentor who threatened his family."
},
{
"chunk": 93,
"story": 6,
"summary": "He stages the scene as a robbery and returns to bed as the drugged nurse unwittingly provides his alibi.",
"predicament": "A man who has done a desperate thing arranges things so that suspicion falls elsewhere."
}
]
@@ -0,0 +1,18 @@
[
{"chunk":94,"story":7,"summary":"An old villager chats with a travelling painter repainting the inn sign, which had been mistaken for a portrait of a king.","predicament":"A craftsman told his work looks like something else quietly fixes it before anyone else notices."},
{"chunk":95,"story":7,"summary":"The painter shares his beer, and the old man begins the tale of a travelling circus that stopped in the village.","predicament":"An old man looking for a free drink offers a story in exchange."},
{"chunk":96,"story":7,"summary":"News comes that the circus tiger has escaped nearby; a tramp comes running to the inn shouting that the tiger is after him, and the drinkers rush indoors.","predicament":"People who laughed at a distant danger panic when it seems to have arrived on their doorstep."},
{"chunk":97,"story":7,"summary":"The landlord bars the door against the tramp; a man knocks the landlord down to get out to protect the families, and the tramp gets in.","predicament":"Someone must choose between keeping himself safe behind a barred door and letting in a stranger in danger."},
{"chunk":98,"story":7,"summary":"The tramp, revived with free brandy, tells a vivid tale of fleeing the tiger, and the men are afraid to go home.","predicament":"A stranger with a thrilling tale of danger is rewarded by a frightened crowd without anyone checking it."},
{"chunk":99,"story":7,"summary":"The men sleep at the inn; the tramp is gone by morning; the village lives in fear, though the local rogue laughs at the whole thing.","predicament":"A community living in fear of an unseen threat ignores the one doubter, because he has a bad name."},
{"chunk":100,"story":7,"summary":"A villager's hens are taken; he carries the evidence to the constable in the next village, who shoves him away as drunk.","predicament":"A man who has suffered a loss goes to the authorities for help and is brushed off as a nuisance."},
{"chunk":101,"story":7,"summary":"Pigs vanish night after night; the authorities do nothing, and neighbours squabble over whether to take animals indoors.","predicament":"A community suffering repeated losses cannot get help, and cannot agree how to protect itself."},
{"chunk":102,"story":7,"summary":"Children claim sightings, and a villager staggers in describing how the tiger chased him up a tree.","predicament":"A man tells a dramatic story of narrow escape to an audience eager to believe it."},
{"chunk":103,"story":7,"summary":"When one man questions the story, the rogue 'confirms' it, claiming he watched from another tree; the storyteller, caught out, goes home silent.","predicament":"A man caught in a boastful lie is 'backed up' by a rival, who now has him trapped by it."},
{"chunk":104,"story":7,"summary":"The neighbouring town and the clergyman say there was no tiger; after more losses a former militiaman raises a band of seventeen to hunt it.","predicament":"People told their fears are nonsense by outsiders decide to take matters into their own hands."},
{"chunk":105,"story":7,"summary":"The hunt searches the wood timidly and finds nothing; back at the inn, an old man reports the tiger has just gone into the rogue's cottage.","predicament":"A band that set out to face a danger returns relieved to have found nothing, and is then called to face it for real."},
{"chunk":106,"story":7,"summary":"The armed men crowd into the rogue's cottage, pushing him ahead of them, despite his protests.","predicament":"A man with something to hide has an armed crowd force its way into his home."},
{"chunk":107,"story":7,"summary":"His kitchen is full of pork and poultry; accused of theft, he brazenly claims to have gone into the meat trade.","predicament":"A man caught with the evidence of his thefts brazens it out and dares his accusers to prove anything."},
{"chunk":108,"story":7,"summary":"He turns their own tiger sightings against them: those who swore they saw it would have to repeat it on oath.","predicament":"A man accused of wrongdoing turns his accusers' own exaggerations into his defence."},
{"chunk":109,"story":7,"summary":"He claims to have receipts, offers to sell them the meat, and gets away with it; he and the tramp turn out to have known each other.","predicament":"People who know they were swindled cannot prove it, and must watch the swindler profit."}
]
@@ -0,0 +1,14 @@
[
{"chunk":110,"story":8,"summary":"A retired officer, dressed with great care, sets out on a hot afternoon and overtakes a friend who stares uneasily at his loud tie.","predicament":"A man nervously setting out to do something that matters to him is stopped by a friend with questions."},
{"chunk":111,"story":8,"summary":"The officer confides he is going to propose to a widow; his friend, equally dressed up, says he is on the same errand, and the two race along the road.","predicament":"A man discovers a friend is after the same person he is, on the same day, and each races to get there first."},
{"chunk":112,"story":8,"summary":"Followed by jeering boys, they stop and agree they cannot propose together, but neither will yield the day, each pleading an excuse.","predicament":"Two rivals agree someone must go first, and neither will be the one to wait."},
{"chunk":113,"story":8,"summary":"Each claims precedence; the officer vows that if they go together he will match any proposal on the spot, and refuses to be put off.","predicament":"Two rivals for the same prize trade claims of priority, and each suspects the other of a trick."},
{"chunk":114,"story":8,"summary":"The officer proposes they play one game of chess for the right to propose first; the friend agrees, on condition that if the winner is refused, the loser may not propose without his permission.","predicament":"Two rivals agree to settle who goes first by a game, and one slips in a condition the other does not examine."},
{"chunk":115,"story":8,"summary":"The officer offers his rival a large brandy, which the rival sets aside; after a long game the rival checkmates him with a bishop the officer had forgotten.","predicament":"A man who tried to tip a contest in his favour loses it through his own oversight."},
{"chunk":116,"story":8,"summary":"The winner tidies himself in the loser's house and goes off to propose; the loser waits up all night and next morning goes to hear the result.","predicament":"A man who lost the right to try first must wait while his rival takes his chance."},
{"chunk":117,"story":8,"summary":"The rival was refused, but holds the officer to the condition and plans to keep proposing until the widow accepts him.","predicament":"A man finds the rival he lost to has been refused, but can still use the terms of their bargain to keep him out."},
{"chunk":118,"story":8,"summary":"The rival says he will warn the widow off the officer, suggests the officer court her companion instead, and the officer storms out and sulks for weeks.","predicament":"A man bound by a promise he made watches a rival use it to keep him from what he wants."},
{"chunk":119,"story":8,"summary":"Meeting the widow by chance, the officer confesses the chess game to explain his absence; she is indignant, then laughs.","predicament":"A man who has been avoiding someone he cares for must explain why, and the only honest answer is embarrassing."},
{"chunk":120,"story":8,"summary":"She says she is angry with him for losing, and proposes a plan so that he can win at cards what he lost at chess, under the rival's eyes.","predicament":"A man bound by a rash promise finds the person he hoped to win is willing to help him get round it."},
{"chunk":121,"story":8,"summary":"Playing cards, the widow asks the officer if he would like her hand; he takes it and kisses it, she 'meant the cards', then lets him keep it, and the rival is left fuming.","predicament":"Someone arranges to accept the suitor she prefers in a way his rival cannot object to."}
]
@@ -0,0 +1,15 @@
[
{"chunk":122,"story":9,"summary":"A man wakes up thirsty in a cramped, reeking bunk, with a stranger's face swearing at him, and cannot tell where he is.","predicament":"A man wakes up somewhere strange with no idea how he got there."},
{"chunk":123,"story":9,"summary":"He realises he is on a ship, dressed in rags; he finds his friend among the sleepers and learns the ship's name.","predicament":"A man in an unknown place looks for his one companion and a first clue to how they came to be there."},
{"chunk":124,"story":9,"summary":"Seasick and far from land, he goes aft and demands to be taken home; the officer calls him mad and orders him forward.","predicament":"A man who has always been obeyed tries to give orders somewhere nobody knows or believes who he is."},
{"chunk":125,"story":9,"summary":"His friend tries to threaten legal consequences but is too sick to finish; the two bicker over which of them was drunk.","predicament":"Two people in trouble together try to put on a united front and end up blaming each other."},
{"chunk":126,"story":9,"summary":"The captain says they signed on as cook and seaman, calls them drunk impostors, and threatens jail; they are put to work.","predicament":"Two people who claim to be respected professionals are treated as impostors and forced to do menial work."},
{"chunk":127,"story":9,"summary":"The crew mocks their claims of being drugged and of sobriety; their airs make the officers dislike them all the more.","predicament":"People brought low insist on their old status, and are mocked for it by those around them."},
{"chunk":128,"story":9,"summary":"The captain humiliates the cook; the cook obeys, tidies the cabin, and comes back with a secret smile and something in his pocket, telling his friend it won't be for long.","predicament":"A humiliated man pretends to submit while he quietly prepares his revenge."},
{"chunk":129,"story":9,"summary":"The captain is struck with violent pains; the cook refuses to treat him, as he 'only shipped as cook', until he agrees to pay a professional fee.","predicament":"A man mocked for claiming skill is suddenly needed for it, and names his price."},
{"chunk":130,"story":9,"summary":"The cook prescribes, collects his fee, scares the captain about making a will, and has the mate sent out for snuffling.","predicament":"A man who has gained the upper hand over those who humiliated him enjoys every moment of it."},
{"chunk":131,"story":9,"summary":"The cook hints he dosed the food from the medicine chest; the mate falls ill too, and the cook demands to be treated as surgeon.","predicament":"A man who has secretly caused the trouble only he can cure makes the most of it."},
{"chunk":132,"story":9,"summary":"The mate haggles over the fee; the second officer falls ill too; the sick captain scolds the sick mate for being ill at all.","predicament":"A man who swore he would never pay the person he scorned is forced by suffering to haggle with him."},
{"chunk":133,"story":9,"summary":"The cook treats and weakens all three officers; the captain and mate pay the other man to draw up their wills; they recover slowly as they reach port.","predicament":"People who scorned two outsiders now depend on them completely, and pay for the privilege."},
{"chunk":134,"story":9,"summary":"Ashore, the cook tells the captain he ate something that disagreed with him, and leaves with a wave.","predicament":"A man who got even with his tormentors leaves them knowing they were had."}
]
@@ -0,0 +1,15 @@
[
{"chunk":135,"story":10,"summary":"At a crowded family breakfast, a poor widow living with her sister's family gets a letter and reads it so slowly that the others nearly burst with impatience.","predicament":"Someone given news that might change their life cannot take it in, while the people around them itch to know."},
{"chunk":136,"story":10,"summary":"The widow fears the letter is a hoax; her brother-in-law thinks so too, until her sister reads it and rejoices.","predicament":"Someone who has never had luck is told of a windfall and is afraid to believe it."},
{"chunk":137,"story":10,"summary":"The widow has inherited two thousand pounds from a former employer; the brother-in-law grumbles that nobody ever left him anything, and spreads the news through the town.","predicament":"A man sees a relative come into money and resents that luck never comes his way."},
{"chunk":138,"story":10,"summary":"Local men suddenly befriend the brother-in-law; his wife points out that two old dealers are courting the heiress for her money.","predicament":"A man flattered by new friends is told they are really after a relative's money."},
{"chunk":139,"story":10,"summary":"The brother-in-law watches the two suitors flatter the widow and tries to undercut them by announcing her age, nearly giving away his wife's.","predicament":"A man trying to expose flattering suitors trips over his own family's sensibilities."},
{"chunk":140,"story":10,"summary":"The suitors outbid each other with compliments; the wife's temper frays, and the visitors are shown out with the door shut hard.","predicament":"Two rivals courting the same person keep topping each other's compliments while the family grows fed up."},
{"chunk":141,"story":10,"summary":"The family, wanting to keep the money, fail to warn the widow; the slow dealer, losing ground to his glib rival, hatches a plan and enlists a small-time dealer.","predicament":"A slow-talking man losing a contest to a glib rival looks for a trick instead of better words."},
{"chunk":142,"story":10,"summary":"The dealer bribes an accomplice, who holds out for a bigger fee, to tell the rival in confidence that the widow has lost her fortune; the rival swallows it.","predicament":"A man hires someone to spread a false rumour that will drive off his rival, and the hireling drives a hard bargain."},
{"chunk":143,"story":10,"summary":"With the rival gone, the dealer courts the widow alone, preaching the virtues of poverty, and loses seven games of draughts to her on purpose.","predicament":"A man courting for money pretends not to care about it and flatters the one he courts by letting her win."},
{"chunk":144,"story":10,"summary":"He blows her kisses and fusses over a bumped head, invites himself back, and walks home sure the money is his.","predicament":"A schemer who thinks his deal is done walks away gloating too soon."},
{"chunk":145,"story":10,"summary":"He spreads the rumour of her ruin through the town, then proposes publicly 'now that she is poor', to look noble.","predicament":"A man makes a public show of selfless devotion, sure that it will cost him nothing."},
{"chunk":146,"story":10,"summary":"She accepts; the family, touched, apologise for misjudging him, then mention a letter that arrived.","predicament":"A schemer whose plan has worked is thanked for his goodness while the ground starts to shift under him."},
{"chunk":147,"story":10,"summary":"The lawyers' letter says a later will was found and she has nothing; the dealer, now engaged to a poor woman, is left alone with her to court.","predicament":"A man who faked a disaster to win a prize learns the disaster is real, and he is bound to his promise."}
]
@@ -0,0 +1,56 @@
[
{
"chunk": 148,
"story": 11,
"summary": "In an inn, men trade ghost stories; a sceptical old sailor mocks sailors' visions, then admits one uncanny experience on land.",
"predicament": "A sceptic among believers is pressed to admit whether he has ever seen anything he could not explain."
},
{
"chunk": 149,
"story": 11,
"summary": "As a young man back from abroad, the narrator found his relatives away, took lodgings at a dull inn, and set out on a long country walk.",
"predicament": "A young man who arrived to find no one expecting him has to fill empty days in a strange place."
},
{
"chunk": 150,
"story": 11,
"summary": "He loses his way in the marshes as fog and night come down, and at last finds a road and hears footsteps.",
"predicament": "A traveller lost in the dark and the cold must find shelter before night closes in."
},
{
"chunk": 151,
"story": 11,
"summary": "A countryman sets him on the road but warns him away from a lit house nearby, said to hide something monstrous; tired, he goes to it anyway.",
"predicament": "A worn-out traveller must choose between a long walk on and a nearby shelter he has been warned against."
},
{
"chunk": 152,
"story": 11,
"summary": "A grim old woman and a huge old man answer; after a furtive exchange the man insists he stay for dinner, and he feels trapped.",
"predicament": "A stranger taken in by odd hosts who seem keen to keep him wonders whether he has walked into a trap."
},
{
"chunk": 153,
"story": 11,
"summary": "The host says his son will dine with them in the dark because of his eyes; he puts out the candles, and a strange voice speaks.",
"predicament": "A guest must sit down in the dark with someone he cannot see, on hosts' terms he does not understand."
},
{
"chunk": 154,
"story": 11,
"summary": "The host leaves him alone with the unseen son, and the guest thinks he sees wild eyes shining at him across the table.",
"predicament": "A guest left alone with a stranger he cannot see grows afraid, and must decide whether to trust his hosts."
},
{
"chunk": 155,
"story": 11,
"summary": "The screen falls, and firelight shows the son's ruined face; the father lights the candles and explains his son was disfigured in a fire.",
"predicament": "A frightened man sees what he feared, and learns it is someone who was hurt and ashamed to be seen."
},
{
"chunk": 156,
"story": 11,
"summary": "The guest apologises warmly; they celebrate the son's birthday until midnight, and the father toasts the children his son saved.",
"predicament": "A man who shrank from someone must make amends, and finds they deserve his respect."
}
]
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Assemble records.json and groups.json from batches/ and groups_src.py, and check them."""
import collections, glob, json, pathlib
from groups_src import G, STANDING
here = pathlib.Path(__file__).parent
chunks = json.loads((here.parent / 'chunks.json').read_text())
stories = json.loads((here.parent / 'stories.json').read_text())
recs = [r for f in sorted(glob.glob(str(here / 'batches/s*.json'))) for r in json.loads(open(f).read())]
assert [r['chunk'] for r in recs] == list(range(len(chunks))), 'every scene, once, in order'
assert all(chunks[r['chunk']]['story'] == r['story'] for r in recs)
seen = collections.Counter(m for g in G for m in g['members'])
dup = [m for m, n in seen.items() if n > 1]
assert not dup, f'scenes in two groups: {dup}'
by = {r['chunk']: r for r in recs}
out = []
for g in G:
assert 3 <= len(g['options']) <= 5, g['id']
st = sorted({by[m]['story'] for m in g['members']})
stand = collections.Counter(STANDING[by[m]['story']] for m in g['members'])
out.append({
'id': g['id'], 'name': g['name'], 'predicament': g['predicament'],
'sentence': f"A {g['actor']} wants {g['wants']} from {g['counterpart']}.",
'actor': g['actor'], 'wants': g['wants'], 'counterpart': g['counterpart'],
'stories': st, 'story_titles': [stories[s]['title'] for s in st],
'standing': dict(stand), 'stage': stand.most_common(1)[0][0],
'members': [{'chunk': m, 'story': by[m]['story'], 'predicament': by[m]['predicament']} for m in g['members']],
'options': [{'text': t, 'source': s, 'moves': mv,
'in_group': (int(s.split(':')[1]) in g['members']) if s.startswith('scene:') else None}
for t, s, mv in g['options']],
})
unassigned = [r['chunk'] for r in recs if r['chunk'] not in seen]
(here / 'records.json').write_text(json.dumps(recs, indent=1, ensure_ascii=False))
(here / 'groups.json').write_text(json.dumps({'groups': out, 'unassigned': unassigned}, indent=1, ensure_ascii=False))
multi = sum(len(g['stories']) >= 2 for g in out)
print(f'{len(recs)} scenes, {len(out)} groups ({multi} span 2+ stories), '
f'{sum(seen.values())} assigned, {len(unassigned)} unassigned: {unassigned}')
for g in out:
print(f"{g['id']} {len(g['members']):2} scenes {len(g['stories'])} stories stage {g['stage']:6} {g['name']}")
src = collections.Counter(o['source'].split(':')[0] for g in out for o in g['options'])
print('option sources:', dict(src), '| scene options citing a scene outside their group:',
sum(o['in_group'] is False for g in out for o in g['options']))
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,192 @@
"""Hand-built reference grouping of the Jacobs predicaments (Claude-written, no local inference).
Each scene is in at most one group; scenes that fit no recurring situation stay unassigned.
Options follow DESIGN.md: 3-5 per group, each marked by where it came from --
'archetype' a fixed, setting-free option
'scene:N' what someone in scene N actually did (or refused to do)
'filled' written to fill a gap the scenes leave
`moves` names spine paths and a direction only (+ / - / risk). No magnitudes: the model never
chooses a number. Skill labels proposed for this corpus: nerve, cunning.
"""
G = [
dict(id='G01', name='Someone who knows your past names a price',
predicament='Someone who knows what you did wants paying to keep quiet, and you have a life to lose.',
actor='respectable person', wants='silence', counterpart='someone who knows their past',
members=[43, 67, 68, 81, 82, 83, 87, 90],
options=[
('Pay what they ask.', 'scene:82', {'money':'-', 'standing':'+', 'relationships.blackmailer':'+'}),
('Refuse, and dare them to tell.', 'scene:43', {'nerve':'+', 'standing':'risk'}),
('Seem to give in, and wait for your moment.', 'scene:87', {'cunning':'+', 'relationships.family':'-'}),
('Try to take the evidence off them by force.', 'scene:43', {'nerve':'+', 'flags.violent':'set', 'standing':'risk'}),
('Tell the truth yourself before they can.', 'filled', {'standing':'-', 'relationships.blackmailer':'-'})]),
dict(id='G02', name='Hiding something from someone close who keeps getting near it',
predicament='You are hiding something, and someone you care for keeps innocently coming close to it.',
actor='person with a secret', wants='to keep it hidden', counterpart='someone close to them',
members=[7, 8, 45, 46, 47, 48, 49, 50, 51, 52],
options=[
('Change the subject and steer them away.', 'scene:47', {'cunning':'+', 'relationships.close':'-'}),
('Keep your face still and say nothing.', 'archetype', {'nerve':'+'}),
('Promise to deal with it yourself, alone.', 'scene:49', {'nerve':'+', 'flags.secret_kept':'set'}),
('Tell them the truth.', 'filled', {'relationships.close':'risk', 'standing':'-'})]),
dict(id='G03', name='Someone innocent is taking the blame for what you did',
predicament='Suspicion has fallen on someone else for what you did, and you could let it stay there.',
actor='person at fault', wants='to escape blame', counterpart='an innocent suspect',
members=[10, 44, 59, 74, 75, 76, 77, 93],
options=[
('Own up.', 'scene:10', {'standing':'-', 'relationships.suspect':'+'}),
('Say nothing and let it pass.', 'archetype', {'relationships.suspect':'-', 'flags.guilty_secret':'set'}),
('Quietly steer suspicion away from them without confessing.', 'scene:60', {'cunning':'+'}),
('Point at them outright.', 'scene:75', {'standing':'+', 'relationships.suspect':'-', 'flags.guilty_secret':'set'})]),
dict(id='G04', name='A rival is after the same person',
predicament='Someone else wants the person you want, and only one of you can have them.',
actor='suitor', wants='the person they are courting', counterpart='a rival',
members=[4, 5, 111, 112, 113, 116, 117, 118, 140, 141, 142],
options=[
('Race them to it.', 'scene:111', {'nerve':'+', 'relationships.rival':'-'}),
('Settle it between you by a game or a bargain.', 'scene:114', {'relationships.rival':'+', 'standing':'risk'}),
('Put the rival in a bad light.', 'scene:142', {'cunning':'+', 'relationships.rival':'-', 'money':'-'}),
('Outdo their compliments.', 'scene:140', {'relationships.courted':'+'}),
('Step aside.', 'archetype', {'relationships.rival':'+', 'relationships.courted':'-'})]),
dict(id='G05', name='A deal you could tip in your favour',
predicament='You want something from someone and could quietly tip the deal your way.',
actor='bargainer', wants='the better end of a deal', counterpart='the other party',
members=[35, 36, 114, 115, 143],
options=[
('Deal straight.', 'archetype', {'standing':'+'}),
('Hide why you want it and haggle.', 'scene:36', {'cunning':'+', 'money':'-'}),
('Butter them up first: let them win, fill their glass.', 'scene:143', {'cunning':'+', 'relationships.other':'+'}),
('Slip in a condition they will not notice.', 'scene:114', {'cunning':'+', 'relationships.other':'risk'})]),
dict(id='G06', name='Partners who do not trust each other over a shared prize',
predicament='You and a partner are after the same prize, and neither of you trusts the other.',
actor='partner', wants='their share, or all of it', counterpart='their partner',
members=[30, 33, 34, 37, 38, 125],
options=[
('Split it fairly and keep your word.', 'archetype', {'relationships.partner':'+', 'money':'+'}),
('Cut your partner out.', 'scene:37', {'cunning':'+', 'money':'risk', 'relationships.partner':'-'}),
('Keep your partner where you can see them.', 'scene:38', {'nerve':'+'}),
('Threaten to tell unless they cut you in.', 'scene:29', {'money':'+', 'relationships.partner':'-'})]),
dict(id='G07', name='People are after your money',
predicament='You have money, or people think you do, and everyone around you suddenly wants a piece.',
actor='person with money', wants='to keep it', counterpart='people courting it',
members=[27, 28, 32, 138, 139],
options=[
('Enjoy the attention while it lasts.', 'scene:138', {'relationships.flatterer':'+', 'money':'risk'}),
('Tell them plainly what you think they are after.', 'filled', {'nerve':'+', 'relationships.flatterer':'-'}),
('Make sure nobody gets it.', 'scene:28', {'money':'+', 'standing':'-'}),
('Deny you have anything.', 'scene:32', {'cunning':'+'})]),
dict(id='G08', name='A windfall with a catch',
predicament='Something could change your fortunes overnight, and you are not sure what it will cost.',
actor='person of small means', wants='a change of fortune', counterpart='whoever offers it',
members=[16, 17, 18, 20, 22, 135, 136, 137],
options=[
('Try it and see.', 'scene:18', {'money':'+', 'flags.tempted_fate':'set'}),
('Throw it away.', 'scene:17', {'nerve':'+'}),
('Ask someone who knows before you decide.', 'scene:16', {'relationships.adviser':'+'}),
('Keep it quiet until you are sure.', 'filled', {'cunning':'+'})]),
dict(id='G09', name='Your scheme turns on you',
predicament='A plan you were sure of is coming apart, and the people you meant to fool are watching.',
actor='schemer', wants='to come out ahead', counterpart='the people they tried to fool',
members=[12, 39, 40, 54, 78, 79, 88, 91, 144, 145, 146, 147],
options=[
('Keep smiling and take your loss.', 'scene:40', {'standing':'-', 'nerve':'+'}),
('Brazen it out.', 'scene:107', {'nerve':'+', 'standing':'risk'}),
('Double down on the plan.', 'scene:145', {'cunning':'+', 'money':'risk'}),
('Own up and ask for mercy.', 'filled', {'standing':'-', 'relationships.fooled':'+'})]),
dict(id='G10', name='Treated with contempt by those over you',
predicament='People with power over you treat you with contempt, and you want to turn the tables.',
actor='person brought low', wants='respect, or revenge', counterpart='those in charge',
members=[64, 92, 124, 126, 127, 128, 129, 130, 131, 133, 134],
options=[
('Swallow it and do as you are told.', 'scene:128', {'relationships.authority':'+', 'standing':'-'}),
('Insist on who you really are.', 'scene:124', {'standing':'risk', 'relationships.authority':'-'}),
('Wait until they need you, then name your price.', 'scene:129', {'money':'+', 'cunning':'+'}),
('Get even quietly.', 'scene:131', {'cunning':'+', 'relationships.authority':'-', 'flags.revenge':'set'})]),
dict(id='G11', name='Someone unwelcome has settled into your home',
predicament='Someone you do not want has settled into your home, and you cannot seem to make them go.',
actor='householder', wants='them gone', counterpart='an unwelcome guest',
members=[2, 63, 85, 86],
options=[
('Tell them to go.', 'filled', {'nerve':'+', 'relationships.guest':'-'}),
('Put up with it.', 'scene:85', {'relationships.household':'-'}),
('Arrange for them to disgrace themselves.', 'scene:63', {'cunning':'+', 'relationships.guest':'-'}),
('Stand up for the people they are hurting.', 'scene:86', {'relationships.household':'+', 'nerve':'+'})]),
dict(id='G12', name='Something may be waiting in the dark',
predicament='Something frightening may be close by in the dark, and you must decide whether to face it.',
actor='frightened person', wants='to be safe', counterpart='whatever is out there',
members=[19, 25, 26, 73, 96, 97, 99, 151, 152, 153, 154, 155],
options=[
('Open the door and look.', 'scene:155', {'nerve':'+', 'flags.faced_fear':'set'}),
('Bar the door and stay put.', 'scene:97', {'nerve':'-'}),
('Go on through the dark anyway.', 'scene:151', {'nerve':'+', 'standing':'risk'}),
('Laugh it off in front of the others.', 'scene:19', {'standing':'+'})]),
dict(id='G13', name='Pressed to do something you believe is wrong',
predicament='Someone is pressing you to do something you believe is wrong or dangerous.',
actor='reluctant person', wants='to do right without losing them', counterpart='the one pressing',
members=[23, 24, 29, 62, 69],
options=[
('Refuse flatly.', 'scene:62', {'nerve':'+', 'relationships.presser':'-'}),
('Do it, to keep the peace.', 'scene:24', {'relationships.presser':'+', 'flags.did_wrong':'set'}),
('Go along, for a share.', 'scene:29', {'money':'+', 'flags.did_wrong':'set'}),
('Agree, then quietly undo it.', 'scene:26', {'cunning':'+', 'relationships.presser':'risk'})]),
dict(id='G14', name='A story nobody can check',
predicament='You have a story to tell that nobody can check, and people are ready to believe it.',
actor='storyteller', wants='to be believed', counterpart='a listening crowd',
members=[95, 98, 102, 103, 148],
options=[
('Tell it big.', 'scene:102', {'standing':'+', 'flags.tall_tale':'set'}),
('Tell it straight.', 'archetype', {'standing':'+'}),
('Get someone to vouch for you.', 'scene:103', {'relationships.voucher':'risk'}),
('Say you have seen nothing worth telling.', 'scene:148', {'nerve':'+'})]),
dict(id='G15', name='Caught where you should not be',
predicament='You have been caught where you should not be, and need an explanation right now.',
actor='person caught out', wants='to get away with it', counterpart='whoever caught them',
members=[9, 14, 58, 60, 65, 66, 106, 107, 108, 119],
options=[
('Brazen it out.', 'scene:107', {'nerve':'+', 'standing':'risk'}),
('Take the embarrassing explanation you are offered.', 'scene:66', {'standing':'-', 'relationships.catcher':'+'}),
('Tell the plain truth.', 'scene:119', {'standing':'-', 'relationships.catcher':'+'}),
('Say nothing and slip away.', 'archetype', {'relationships.catcher':'-'})]),
dict(id='G16', name='Blamed at home',
predicament='Someone at home holds you responsible for trouble you think is not your fault.',
actor='person being blamed', wants='peace at home', counterpart='the one blaming them',
members=[0, 1, 3, 55, 57],
options=[
('Put it on someone else.', 'scene:9', {'relationships.spouse':'+', 'relationships.other':'-'}),
('Keep your head down until it blows over.', 'archetype', {'relationships.spouse':'-'}),
('Speak up for yourself.', 'scene:57', {'nerve':'+', 'relationships.spouse':'risk'}),
('Smooth it over with a peace offering.', 'filled', {'money':'-', 'relationships.spouse':'+'})]),
dict(id='G17', name='You are sure who did it and cannot prove it',
predicament='You are sure who wronged you, and you cannot prove it.',
actor='wronged person', wants='what was taken, or justice', counterpart='the suspect',
members=[31, 56, 61, 100, 109],
options=[
('Search them.', 'scene:31', {'relationships.suspect':'-', 'standing':'risk'}),
('Accuse them openly.', 'scene:56', {'nerve':'+', 'relationships.suspect':'-'}),
('Set a trap.', 'scene:61', {'cunning':'+'}),
('Take it to the authorities.', 'scene:100', {'relationships.authority':'risk'}),
('Let it drop.', 'archetype', {'money':'-', 'standing':'-'})]),
dict(id='G18', name='Lost, far from anything you know',
predicament='You find yourself lost, far from anyone you know, and must get your bearings.',
actor='stranded person', wants='to get back', counterpart='whoever is nearby',
members=[122, 123, 149, 150],
options=[
('Ask the first person you meet for help.', 'scene:151', {'relationships.stranger':'+'}),
('Find your companion and make a plan together.', 'scene:123', {'relationships.companion':'+'}),
('Push on alone.', 'scene:150', {'nerve':'+', 'standing':'risk'}),
('Demand to be taken back at once.', 'scene:124', {'standing':'risk', 'relationships.authority':'-'})]),
dict(id='G19', name='Making amends',
predicament='You have wronged someone, and want them to forgive you.',
actor='person in the wrong', wants='forgiveness', counterpart='the person they wronged',
members=[11, 13, 156],
options=[
('Apologise plainly.', 'scene:156', {'relationships.wronged':'+'}),
('Make a grand gesture.', 'scene:11', {'nerve':'+', 'relationships.wronged':'risk'}),
('Pay for the damage.', 'filled', {'money':'-', 'relationships.wronged':'+'}),
('Leave it and hope it is forgotten.', 'archetype', {'relationships.wronged':'-'})]),
]
# Protagonist standing by story, for stage assignment (DESIGN.md "Stages"). Per story, not per
# scene: a coarse first cut, and the one place this grouping uses a judgement not in the text.
STANDING = {0:'middle', 1:'middle', 2:'low', 3:'high', 4:'middle', 5:'high', 6:'middle',
7:'low', 8:'high', 9:'low', 10:'low', 11:'middle'}
# Story 9's professionals are treated as crew for the whole voyage, so they rank low.
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Render groups.json + records.json as one self-contained page: REFERENCE.html."""
import html, json, pathlib
here = pathlib.Path(__file__).parent
d = json.loads((here / 'groups.json').read_text())
recs = {r['chunk']: r for r in json.loads((here / 'records.json').read_text())}
stories = json.loads((here.parent / 'stories.json').read_text())
e = html.escape
SRC = {'archetype': 'archetype', 'filled': 'filled gap'}
def src(o):
if o['source'].startswith('scene:'):
n = o['source'].split(':')[1]
return f'from scene {n}' + ('' if o['in_group'] else ' <span class="warn">(outside this group)</span>')
return SRC[o['source']]
def moves(m): return ' '.join(f'<code>{e(k)} {e(v)}</code>' for k, v in m.items())
def scene(c):
r = recs[c]
return (f'<details><summary><b>{c}</b> · <i>{e(stories[r["story"]]["title"])}</i> — {e(r["predicament"])}</summary>'
f'<p class="sum">{e(r["summary"])}</p></details>')
out = []
for g in d['groups']:
out.append(f'''<section><h2>{g["id"]} · {e(g["name"])}</h2>
<p class="pred">{e(g["predicament"])}</p>
<p class="meta">{e(g["sentence"])} · {len(g["members"])} scenes from {len(g["stories"])} stories
({e(", ".join(g["story_titles"]))}) · stage <b>{g["stage"]}</b></p>
<h3>Options</h3><ol>{"".join(f"<li>{e(o['text'])} <span class='src'>{src(o)}</span><br>{moves(o['moves'])}</li>" for o in g["options"])}</ol>
<h3>Scenes</h3>{"".join(scene(m["chunk"]) for m in g["members"])}</section>''')
un = ''.join(scene(c) for c in d['unassigned'])
page = f'''<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"><title>Jacobs Reference Groups</title>
<style>
:root{{--bg:#fbfaf7;--fg:#1d1d1b;--mute:#6b6760;--line:#e2ded6;--acc:#2f5d8a;--warn:#a4481c}}
@media (prefers-color-scheme:dark){{:root{{--bg:#191917;--fg:#e8e6e1;--mute:#a09c94;--line:#34332f;--acc:#8ab4dc;--warn:#e6915f}}}}
body{{background:var(--bg);color:var(--fg);font:16px/1.5 Georgia,serif;max-width:52rem;margin:0 auto;padding:1rem 16px 4rem}}
h1{{font-size:1.5rem}} h2{{font-size:1.15rem;margin:0 0 .3rem}} h3{{font-size:.8rem;text-transform:uppercase;letter-spacing:.06em;color:var(--mute);margin:.9rem 0 .3rem}}
section{{border-top:1px solid var(--line);padding:1.2rem 0}} .pred{{font-size:1.05rem;margin:.2rem 0}}
.meta,.src,.sum{{color:var(--mute);font-size:.88rem}} .warn{{color:var(--warn)}}
code{{font:12px ui-monospace,monospace;border:1px solid var(--line);border-radius:3px;padding:0 .3em;margin-right:.2em}}
li{{margin:.35rem 0}} details{{margin:.2rem 0}} summary{{cursor:pointer}} .sum{{margin:.2rem 0 .5rem 1.2rem}}
</style></head><body>
<h1>The Lady of the Barge: reference groups</h1>
<p class="meta">W. W. Jacobs, 1902 (Standard Ebooks text). 12 stories, 157 scenes cut by the same rule as <code>chunk.py</code>.
Summaries, predicaments, groups and options written by Claude by reading every scene: no local inference.
A comparison baseline, not a pack. Option moves give a direction only, never a number.</p>
{"".join(out)}
<section><h2>Unassigned ({len(d["unassigned"])})</h2><p class="meta">Scenes that fit no situation recurring in two or more stories.</p>{un}</section>
</body></html>'''
(here / 'REFERENCE.html').write_text(page, encoding='utf-8')
print('wrote', here / 'REFERENCE.html', len(page), 'bytes')
@@ -0,0 +1,944 @@
[
{
"chunk": 0,
"story": 0,
"summary": "A skipper waiting up for his late-returning mate and brother-in-law has just lost a shouted argument with a neighbouring crew; the mate arrives and answers his rebuke with cheek.",
"predicament": "Someone waiting up for a relative who comes in late wants to reproach him, and gets cheek back."
},
{
"chunk": 1,
"story": 0,
"summary": "The mate tells the skipper he has invited his sweetheart on the voyage without asking; the skipper is appalled and asks what his wife will say.",
"predicament": "A man learns a relative has promised a stranger his hospitality without asking, and fears what his spouse will say."
},
{
"chunk": 2,
"story": 0,
"summary": "The skipper tries to refuse the young woman passage but cannot bring himself to say so, and ends up eating supper with the couple.",
"predicament": "A man who wants to turn an unwelcome guest away cannot find the nerve to say it."
},
{
"chunk": 3,
"story": 0,
"summary": "The girl balks at her cramped quarters but stays; the skipper warns that his wife will blame him, and the mate says his sister can mind her own business.",
"predicament": "A man who has let something happen under his roof fears his spouse will hold him responsible for it."
},
{
"chunk": 4,
"story": 0,
"summary": "The skipper insists on chaperoning the girl at meals himself, leaving the mate to steer and eat cold leftovers while she laughs at the skipper's stories.",
"predicament": "A young man who brought the person he is courting along is pushed aside by an older man who takes charge of her."
},
{
"chunk": 5,
"story": 0,
"summary": "Jealous, the mate hints at the skipper's wife, is mocked by the pair, is rude to the girl, and is sent away.",
"predicament": "A jealous suitor tries to put his rival in his place, and only makes himself look childish."
},
{
"chunk": 6,
"story": 0,
"summary": "The girl snubs the mate as 'only the mate'; he turns the tables by threatening to let the skipper explain her presence to his wife alone.",
"predicament": "A spurned man realises he holds something over the rival who humiliated him, and decides to use it."
},
{
"chunk": 7,
"story": 0,
"summary": "The mate refuses to back the skipper's story; the skipper hides the girl below as they arrive to find his wife waiting on the quay.",
"predicament": "A man hiding an innocent but compromising secret is met by his spouse, while the one person who could clear him refuses to."
},
{
"chunk": 8,
"story": 0,
"summary": "The mate pulls meaningful faces behind the skipper's back until the wife grows suspicious; the skipper invents a toothache and tries to steer her away.",
"predicament": "A man tries to hurry his spouse away from a secret while someone signals behind his back that something is up."
},
{
"chunk": 9,
"story": 0,
"summary": "The wife stamps her foot, the hidden girl appears, and the mate lets his brother-in-law take the blame while pretending to shield him.",
"predicament": "A man caught with an innocent secret is disowned by the one person who could explain it."
},
{
"chunk": 10,
"story": 0,
"summary": "The wife abuses the girl, knocks her purse into the water, and orders her off; the mate, now remorseful, takes responsibility and defends her.",
"predicament": "A man whose spite has got an innocent person abused must decide whether to own up and defend her."
},
{
"chunk": 11,
"story": 0,
"summary": "The mate dives repeatedly for the purse to win the girl back; she begs him to come out but refuses to make up.",
"predicament": "A man tries to win back someone he wronged with a dangerous public show of devotion."
},
{
"chunk": 12,
"story": 0,
"summary": "The mate fakes drowning; the skipper rescues him, sees him wink while the girl kisses him, and the wife boxes his ears.",
"predicament": "A man pretends to be in danger to win sympathy, and the one who saved him sees through it."
},
{
"chunk": 13,
"story": 0,
"summary": "The wife takes the girl home to breakfast, leaving both men soaked, while the skipper fends off the mate's apologies with a boathook.",
"predicament": "Two men who quarrelled over a woman are both left out in the cold by the women they wronged."
},
{
"chunk": 14,
"story": 1,
"summary": "An old man losing at chess to his son grumbles at the weather and their remote house while waiting for a guest, who arrives: an old friend back from long service abroad.",
"predicament": "A man losing a friendly contest tries to distract from his mistake, and is caught at it by his family."
},
{
"chunk": 15,
"story": 1,
"summary": "Warmed by whisky, the guest tells tales of distant lands; pressed by his host, he reluctantly produces a dried monkey's paw he calls magic.",
"predicament": "A guest who wants to leave a dangerous subject alone is coaxed by his hosts' curiosity into bringing it out."
},
{
"chunk": 16,
"story": 1,
"summary": "The guest explains the paw grants three men three wishes each, that he has had his, and that the last owner wished for death; asked if he would wish again, he wavers.",
"predicament": "Someone offered a thing that grants wishes is warned by the man who used it that it brings sorrow."
},
{
"chunk": 17,
"story": 1,
"summary": "The guest throws the paw on the fire; the host snatches it back and keeps it despite repeated warnings to wish sensibly or not at all.",
"predicament": "A man is offered a dangerous gift by someone who begs him to destroy it, and wants to keep it anyway."
},
{
"chunk": 18,
"story": 1,
"summary": "Egged on by his joking son, the old man wishes for two hundred pounds to clear the house; the paw twists in his hand, but no money appears.",
"predicament": "A man half-believing in a promise of easy money must decide whether to test it and what to ask for."
},
{
"chunk": 19,
"story": 1,
"summary": "An uneasy night passes; in the morning the family laugh off their fears, though the father recalls that the wishes come true in ways that look like coincidence.",
"predicament": "A family that tempted fate laughs off their fears in daylight while one of them cannot shake them."
},
{
"chunk": 20,
"story": 1,
"summary": "The son leaves for work; later the mother, half-hoping for money, sees a well-dressed stranger hesitate several times at the gate before coming in.",
"predicament": "A family waiting on a hoped-for windfall sees a stranger approach who might bring it, or bad news."
},
{
"chunk": 21,
"story": 1,
"summary": "The stranger, from the son's employers, tells the parents their only son has been killed in the machinery.",
"predicament": "Parents receive news that their only child has been killed, from someone who cannot bring himself to say it."
},
{
"chunk": 22,
"story": 1,
"summary": "The firm denies liability but offers compensation of exactly two hundred pounds; the father collapses. They bury their son and sink into silent grief.",
"predicament": "A man learns the money he wished for has come as compensation for his child's death."
},
{
"chunk": 23,
"story": 1,
"summary": "A week later the grieving mother remembers the paw's other two wishes and demands that her husband wish their son alive; he calls it madness.",
"predicament": "A grieving woman seizes on a desperate chance to undo a death, and her spouse is afraid of what it would bring back."
},
{
"chunk": 24,
"story": 1,
"summary": "The father tells her their son was mutilated beyond recognition, but she insists; terrified, he fetches the paw and makes the second wish.",
"predicament": "A man pressed by his grieving spouse must decide whether to do something he believes is wicked and dangerous."
},
{
"chunk": 25,
"story": 1,
"summary": "Nothing seems to happen; then a stealthy knock comes at the door, and the mother cries that it is their son while the father holds her back.",
"predicament": "A man who dreads what his wish may bring home must stop his spouse from opening the door to it."
},
{
"chunk": 26,
"story": 1,
"summary": "The mother breaks free and struggles with the bolt; the father finds the paw and makes the third wish, and the door opens on an empty road.",
"predicament": "A man must use his last chance to undo what his spouse most longs for, before she lets it in."
},
{
"chunk": 27,
"story": 2,
"summary": "An old watchman recalls a shipmate, an aged miser worth six hundred pounds, who fell ill a month out on the voyage home.",
"predicament": "A man who has hoarded his savings all his life falls ill far from anywhere he could spend them."
},
{
"chunk": 28,
"story": 2,
"summary": "The dying miser refuses to believe he is dying and leave his money; his shipmates circle hopefully, and one finds a money belt on him and guesses his plan to be buried with it.",
"predicament": "A dying miser, surrounded by people hoping to inherit, is determined that nobody will get his money."
},
{
"chunk": 29,
"story": 2,
"summary": "The miser dies boasting that he had a boy throw his banknotes overboard; one sailor scares the boy into silence, and the narrator threatens to tell unless let in on the scheme.",
"predicament": "Someone who spots a colleague quietly scheming to take unclaimed money must decide whether to expose him or join in."
},
{
"chunk": 30,
"story": 2,
"summary": "The schemer reveals he thinks the boy kept the money; the two agree to split it, and search the boy's bunk and pockets at night without success.",
"predicament": "Two partners convinced a weaker person is secretly holding a fortune want to find it before anyone else learns of it."
},
{
"chunk": 31,
"story": 2,
"summary": "A night search is foiled when a ticklish shipmate wakes screaming; a week of prodding the boy finds nothing, so they corner him and demand the money outright.",
"predicament": "A schemer whose secret searches keep failing decides to confront the person he suspects directly."
},
{
"chunk": 32,
"story": 2,
"summary": "The boy insists he threw the money overboard; they search him, offer him a third, then threaten to have him searched ashore and jailed.",
"predicament": "Someone suspected of hiding a windfall is pressed with bribes and threats by people who have no right to it."
},
{
"chunk": 33,
"story": 2,
"summary": "When the sailor heads to tell the captain, the boy gives in: he kept the money but, frightened by the searches, hid it in an officer's mattress where he cannot now reach it.",
"predicament": "Someone forced to share a hidden windfall admits it is now stuck where neither partner can get at it."
},
{
"chunk": 34,
"story": 2,
"summary": "Neither the man nor the boy can get aft unseen, the officer sleeps on the money every night, and the two partners blame each other.",
"predicament": "Distrustful partners stare at a fortune they cannot reach without getting caught, and blame each other for it."
},
{
"chunk": 35,
"story": 2,
"summary": "The boy proposes the man fake an accident to get quartered aft; the man refuses and secretly flatters the officer, asking to buy his mattress as a keepsake.",
"predicament": "A man wants something a superior owns without revealing why, and tries flattery to get it."
},
{
"chunk": 36,
"story": 2,
"summary": "The officer plays up the mattress's value and the sailor, afraid to seem eager, settles at two pounds, to be handed over ashore.",
"predicament": "A buyer who cannot reveal why he wants something is made to pay more by a seller who senses his eagerness."
},
{
"chunk": 37,
"story": 2,
"summary": "The sailor now plans to cut the boy out and split with the narrator alone, but the suspicious boy shadows him all the way into port.",
"predicament": "A man who wants to cheat his partner out of a shared prize is watched too closely to slip away."
},
{
"chunk": 38,
"story": 2,
"summary": "The officer delays the hand-over with a long speech until the boy arrives, sees the sack, and refuses to let them leave without his share.",
"predicament": "A man trying to slip away with a shared prize is held up until the partner he meant to cheat catches him."
},
{
"chunk": 39,
"story": 2,
"summary": "The boy stays on deck while the men cut the mattress open below; it is empty, and when they come up the boy has vanished.",
"predicament": "A man who schemed to cheat a partner finds the prize gone and realises he may have been cheated himself."
},
{
"chunk": 40,
"story": 2,
"summary": "The boy never returns, not even for his pay, and the officer smilingly asks the sailor how he slept.",
"predicament": "A schemer who was outwitted must stand and smile at the people who got the better of him."
},
{
"chunk": 41,
"story": 3,
"summary": "A rich man soon to marry sits with his idle, indebted cousin, who needles him about his wealth and his fiancée's goodness.",
"predicament": "A man about to marry is needled by a dependent relative who hints at something to his discredit."
},
{
"chunk": 42,
"story": 3,
"summary": "The cousin hints that the fiancée would drop him if she knew what he really was, then says he needs fifteen hundred pounds within a fortnight or faces prison.",
"predicament": "A man in desperate debt makes veiled hints to the relative he hopes will pay it."
},
{
"chunk": 43,
"story": 3,
"summary": "Refused, the cousin offers to sell compromising old letters, threatening to sell them to a rival suitor; the rich man seizes him and is stopped only by a servant entering.",
"predicament": "A man is blackmailed by a relative with old letters that could cost him the person he loves."
},
{
"chunk": 44,
"story": 3,
"summary": "The cousin walks out into the park to let him think it over; the rich man follows, and later tells his mother the cousin has gone for good after a quarrel over money.",
"predicament": "A man who has done something terrible in anger must cover for a relative's sudden disappearance."
},
{
"chunk": 45,
"story": 3,
"summary": "The man walks with his fiancée to a disused well in the park, her favourite spot; he urges her not to sit on its edge.",
"predicament": "A man with a guilty secret must keep calm while someone he loves insists on lingering right at the spot."
},
{
"chunk": 46,
"story": 3,
"summary": "She asks about the missing cousin and defends him; the man bursts out that the cousin was a blackmailer, a liar and a cur.",
"predicament": "A man hiding a crime hears it discussed innocently and betrays his feelings with an outburst."
},
{
"chunk": 47,
"story": 3,
"summary": "She is startled by a whisper she imagines saying 'Jem, help me out'; shaken, he tries to take her indoors, but she insists on staying.",
"predicament": "A guilty man tries to hurry someone away from the scene of his crime, and she insists on staying."
},
{
"chunk": 48,
"story": 3,
"summary": "She drops her mother's diamond bracelet down the well and wants it drained; he looks at her in horror, then covers it by saying he feared she was falling.",
"predicament": "A man hiding something learns it may be discovered by an innocent accident, and must not show his fear."
},
{
"chunk": 49,
"story": 3,
"summary": "He promises to recover the bracelet himself by morning and makes her swear to tell no one; his mother suggests filling the well in.",
"predicament": "A man must retrieve something from where his secret lies before anyone else goes looking."
},
{
"chunk": 50,
"story": 3,
"summary": "At night he secretly fishes the well with a line and hook, and dredges up keys instead of the bracelet.",
"predicament": "A man secretly searching where his crime lies keeps pulling up reminders of it."
},
{
"chunk": 51,
"story": 3,
"summary": "The hook snags; he gives up, spends a sleepless night, and at dawn meets his grooms, whom he has ordered to lower him into the well.",
"predicament": "A man who has failed to fix things quietly must now involve others while still keeping his secret."
},
{
"chunk": 52,
"story": 3,
"summary": "His groom offers to go down instead, since the master looks ill; he refuses curtly and starts to test the air with a candle.",
"predicament": "A man hiding something is offered help he cannot accept without being found out."
},
{
"chunk": 53,
"story": 3,
"summary": "The candle is lost; he stops the groom fetching another, strips, and sits a long time on the well's edge before going down.",
"predicament": "A man who must face the thing he did alone gathers his nerve while others watch and wonder."
},
{
"chunk": 54,
"story": 3,
"summary": "Below, something drags at the rope; the grooms haul up the dead cousin's face with their master behind it, let go in horror, and both fall back into the water.",
"predicament": "A man who hid a crime is dragged down by it at the moment he tries to undo its consequences."
},
{
"chunk": 55,
"story": 4,
"summary": "A farmer's wife, her husband, and her retired-policeman uncle discuss a stolen brooch; the uncle has been hiding under their bed hoping to catch the thief, and the wife casts suspicion on a neighbour while accusing her husband of not caring.",
"predicament": "A woman who has lost something valuable suspects the people around her, and resents her spouse for not caring as much."
},
{
"chunk": 56,
"story": 4,
"summary": "The uncle declares everyone guilty until proven innocent; the wife recounts missing the brooch, accusing and sacking her servant, who cried on her box.",
"predicament": "Someone who has lost something accuses the most defenceless person nearby, and punishes her before any proof."
},
{
"chunk": 57,
"story": 4,
"summary": "The servant's box held nothing; the husband defends her and is snapped at, while the uncle, pencil and notebook out, pursues the investigation with great ceremony.",
"predicament": "A man who thinks a household fuss is foolish keeps defending the accused and is scolded for taking her side."
},
{
"chunk": 58,
"story": 4,
"summary": "The husband eats a lozenge from the evidence box and cannot stop laughing; the uncle establishes that no one could have entered the bedroom unheard.",
"predicament": "A man who finds a family inquiry ridiculous cannot keep a straight face in front of its self-important leader."
},
{
"chunk": 59,
"story": 4,
"summary": "Suspicion turns to a widow who visited that morning; the uncle finds muddy footprints on the stairs, which are in fact the husband's, and he stays guiltily quiet.",
"predicament": "A man who sees an innocent neighbour blamed for evidence he himself left must decide whether to own up."
},
{
"chunk": 60,
"story": 4,
"summary": "The husband slyly deflects the footprint evidence, then exchanges silent signals with his wife until the uncle demands to know what is funny.",
"predicament": "A man who knows more than he says can barely hide his amusement from the self-important person running the inquiry."
},
{
"chunk": 61,
"story": 4,
"summary": "The husband reveals he thinks the widow is after the uncle, not the brooch; the uncle is outraged, then proposes inviting her to tea to trap her into confessing.",
"predicament": "A self-important man, teased that a neighbour fancies him, resolves to prove his point by trapping her."
},
{
"chunk": 62,
"story": 4,
"summary": "The husband refuses to have a guest trapped in his house; the uncle secretly tells his niece he will search the widow's cottage without leave.",
"predicament": "A man refuses to let his home be used to entrap a friend, so the schemer plans to go behind his back."
},
{
"chunk": 63,
"story": 4,
"summary": "The husband, who resents the uncle living with them for his wife's hopes of an inheritance, plans to let the widow catch the uncle searching her house, and shadows him.",
"predicament": "A man stuck with an unwelcome relative in his home sees a chance to have him disgraced and driven off."
},
{
"chunk": 64,
"story": 4,
"summary": "The husband brings the widow home early; the uncle, trapped inside, hides in the larder, and the husband settles in for beer and begins praising him.",
"predicament": "A man who has trapped his rival in an embarrassing hiding place decides to make the most of it."
},
{
"chunk": 65,
"story": 4,
"summary": "Within the uncle's hearing, the husband tells the widow the uncle is shy and devoted to her, and has been asking whether she likes jewellery.",
"predicament": "A trapped man must listen in silence while someone tells a neighbour a story about him that he cannot deny."
},
{
"chunk": 66,
"story": 4,
"summary": "Asked for bread and cheese, the widow opens the larder, the uncle bursts out, and her friend takes his hiding there as a bashful suitor's courtship.",
"predicament": "A man caught somewhere he should not be is handed an embarrassing explanation, and has no better one to offer."
},
{
"chunk": 67,
"story": 5,
"summary": "Late at night one co-owner of a house and a business sits hating his partner, who has discovered his embezzlement and ordered him to leave both by morning.",
"predicament": "A man found out in wrongdoing is ordered by his partner to give up everything and leave by morning."
},
{
"chunk": 68,
"story": 5,
"summary": "The honest partner refuses to cover the theft quietly, offers the exile two hundred pounds, and mentions the servants are away until morning.",
"predicament": "A man offered a harsh but fair way out of his disgrace wants a quieter bargain the other will not give."
},
{
"chunk": 69,
"story": 5,
"summary": "The exile demands the matter be hushed up, draws a sword from the wall, and stabs his partner when he refuses.",
"predicament": "A man facing ruin threatens the only person who can expose him, and must choose whether to go through with it."
},
{
"chunk": 70,
"story": 5,
"summary": "Sickened, the killer turns out the lights, flees to the gate, and sees a constable coming up the road.",
"predicament": "A man who has just done something terrible in rage panics and must steady himself when a figure of the law appears."
},
{
"chunk": 71,
"story": 5,
"summary": "He bids the constable good night, returns inside, and gathers his cash and valuables, reckoning he has eight hours' start.",
"predicament": "A man with a crime behind him and a few hours' start must decide what to take and where to run."
},
{
"chunk": 72,
"story": 5,
"summary": "He plans his escape, weighing a crowded district against the seaside, and finds every choice may mean life or death.",
"predicament": "A fugitive planning his escape finds that every choice might be the one that gets him caught."
},
{
"chunk": 73,
"story": 5,
"summary": "At one o'clock he hears creaks on the stairs, and fears the dead man is moving about the house.",
"predicament": "A man alone with what he has done hears noises in the dark and fears it has come back for him."
},
{
"chunk": 74,
"story": 5,
"summary": "Finding the library door open, he locks someone inside, then realises it is a burglar he can blame for the killing.",
"predicament": "A guilty man realises a stranger's blunder gives him someone else to blame."
},
{
"chunk": 75,
"story": 5,
"summary": "He fires through the door, raises the alarm, and the police break in and seize the terrified intruder.",
"predicament": "A man framing an innocent person must act the outraged victim in front of the law."
},
{
"chunk": 76,
"story": 5,
"summary": "The intruder swears the body was there when he arrived; the sergeant uncovers it, and the killer feigns horror.",
"predicament": "A man must play the shocked innocent while the person he framed pleads the truth."
},
{
"chunk": 77,
"story": 5,
"summary": "The killer names his friend and accuses the intruder, who accuses him back; the sergeant seems to side with the killer.",
"predicament": "Someone falsely accused points to the real culprit, who must hold his nerve and keep up his story."
},
{
"chunk": 78,
"story": 5,
"summary": "The sergeant takes the pistol, handcuffs the killer without warning, and sends for a doctor, since the victim is not dead.",
"predicament": "A man who thought he had got away with it learns the one witness who could hang him is still alive."
},
{
"chunk": 79,
"story": 5,
"summary": "The dying partner opens his eyes and speaks; the sergeant takes notes and leads the killer away.",
"predicament": "A man whose deceit has collapsed can only watch as the truth comes out and he is taken away."
},
{
"chunk": 80,
"story": 6,
"summary": "A ragged stranger swaggers into the best inn in a small town, bullies the waiter who tries to turn him out, and demands to see the landlord.",
"predicament": "A penniless newcomer forces his way into a place he does not belong, counting on a secret to back him."
},
{
"chunk": 81,
"story": 6,
"summary": "The stranger greets the landlord by another name, a wanted pirate's, and demands clothes, lodging and money in return for silence.",
"predicament": "A man who has built a respectable new life is found by an old associate who threatens to expose his past."
},
{
"chunk": 82,
"story": 6,
"summary": "The landlord gives him money; the blackmailer draws a pistol, declares he will take half of everything from now on, and settles in.",
"predicament": "A man pays off a blackmailer and learns the price will be half of everything, for good."
},
{
"chunk": 83,
"story": 6,
"summary": "The landlord says he has lived honestly for fifteen years and warns the man not to wake the devil in him; the blackmailer meets the landlord's stepdaughter and says he will stay.",
"predicament": "A reformed man warns a blackmailer not to push him too far, while the blackmailer sets his sights on the man's family."
},
{
"chunk": 84,
"story": 6,
"summary": "The blackmailer orders the best supper and room, and sleeps with his door barricaded and a pistol under his pillow.",
"predicament": "A man who has seized power over another knows the danger he is in, and guards himself even as he gloats."
},
{
"chunk": 85,
"story": 6,
"summary": "He pushes in at the family breakfast table, eyes the stepdaughter, hints at the landlord's past before her, and goes out to buy new clothes.",
"predicament": "A household must make polite space for someone who holds them hostage."
},
{
"chunk": 86,
"story": 6,
"summary": "Within a month the blackmailer runs the inn; the landlord, failing in health, backs him in sacking a loyal old servant, and his daughter demands to know who is master.",
"predicament": "Someone watches a person she trusts give way at every turn to an intruder, and demands that he stand up for himself."
},
{
"chunk": 87,
"story": 6,
"summary": "The landlord sides with the blackmailer, who threatens to ruin the girl and have the landlord hanged; the landlord pleads for peace.",
"predicament": "A man who is being blackmailed hears his tormentor threaten someone he loves, and must choose between swallowing it and hitting back."
},
{
"chunk": 88,
"story": 6,
"summary": "The inn decays under the blackmailer's rule; the landlord collapses with a fit, which upsets the blackmailer's plans.",
"predicament": "A blackmailer's hold depends on his victim's life, and that victim suddenly collapses."
},
{
"chunk": 89,
"story": 6,
"summary": "The landlord lies dying, anxious to please the blackmailer, while his daughter nurses him; the blackmailer resolves to get the hidden money before he dies.",
"predicament": "A blackmailer whose victim is dying must grab what he can before his hold disappears."
},
{
"chunk": 90,
"story": 6,
"summary": "The blackmailer threatens to inform; the dying man begs for time and promises to reveal his money that night if his daughter gets a share.",
"predicament": "A dying man pressed by his tormentor bargains for time and for provision for someone he loves."
},
{
"chunk": 91,
"story": 6,
"summary": "At night the blackmailer drugs the nurse and demands the money; the landlord extracts his promise about the daughter.",
"predicament": "A man who thinks his helpless victim is at his mercy leans in close to hear the secret."
},
{
"chunk": 92,
"story": 6,
"summary": "The 'dying' landlord seizes and strangles him, revealing he has been shamming weakness and planning to blame the killing on the blackmailer's own followers.",
"predicament": "A man who pretended to be helpless strikes at the tormentor who threatened his family."
},
{
"chunk": 93,
"story": 6,
"summary": "He stages the scene as a robbery and returns to bed as the drugged nurse unwittingly provides his alibi.",
"predicament": "A man who has done a desperate thing arranges things so that suspicion falls elsewhere."
},
{
"chunk": 94,
"story": 7,
"summary": "An old villager chats with a travelling painter repainting the inn sign, which had been mistaken for a portrait of a king.",
"predicament": "A craftsman told his work looks like something else quietly fixes it before anyone else notices."
},
{
"chunk": 95,
"story": 7,
"summary": "The painter shares his beer, and the old man begins the tale of a travelling circus that stopped in the village.",
"predicament": "An old man looking for a free drink offers a story in exchange."
},
{
"chunk": 96,
"story": 7,
"summary": "News comes that the circus tiger has escaped nearby; a tramp comes running to the inn shouting that the tiger is after him, and the drinkers rush indoors.",
"predicament": "People who laughed at a distant danger panic when it seems to have arrived on their doorstep."
},
{
"chunk": 97,
"story": 7,
"summary": "The landlord bars the door against the tramp; a man knocks the landlord down to get out to protect the families, and the tramp gets in.",
"predicament": "Someone must choose between keeping himself safe behind a barred door and letting in a stranger in danger."
},
{
"chunk": 98,
"story": 7,
"summary": "The tramp, revived with free brandy, tells a vivid tale of fleeing the tiger, and the men are afraid to go home.",
"predicament": "A stranger with a thrilling tale of danger is rewarded by a frightened crowd without anyone checking it."
},
{
"chunk": 99,
"story": 7,
"summary": "The men sleep at the inn; the tramp is gone by morning; the village lives in fear, though the local rogue laughs at the whole thing.",
"predicament": "A community living in fear of an unseen threat ignores the one doubter, because he has a bad name."
},
{
"chunk": 100,
"story": 7,
"summary": "A villager's hens are taken; he carries the evidence to the constable in the next village, who shoves him away as drunk.",
"predicament": "A man who has suffered a loss goes to the authorities for help and is brushed off as a nuisance."
},
{
"chunk": 101,
"story": 7,
"summary": "Pigs vanish night after night; the authorities do nothing, and neighbours squabble over whether to take animals indoors.",
"predicament": "A community suffering repeated losses cannot get help, and cannot agree how to protect itself."
},
{
"chunk": 102,
"story": 7,
"summary": "Children claim sightings, and a villager staggers in describing how the tiger chased him up a tree.",
"predicament": "A man tells a dramatic story of narrow escape to an audience eager to believe it."
},
{
"chunk": 103,
"story": 7,
"summary": "When one man questions the story, the rogue 'confirms' it, claiming he watched from another tree; the storyteller, caught out, goes home silent.",
"predicament": "A man caught in a boastful lie is 'backed up' by a rival, who now has him trapped by it."
},
{
"chunk": 104,
"story": 7,
"summary": "The neighbouring town and the clergyman say there was no tiger; after more losses a former militiaman raises a band of seventeen to hunt it.",
"predicament": "People told their fears are nonsense by outsiders decide to take matters into their own hands."
},
{
"chunk": 105,
"story": 7,
"summary": "The hunt searches the wood timidly and finds nothing; back at the inn, an old man reports the tiger has just gone into the rogue's cottage.",
"predicament": "A band that set out to face a danger returns relieved to have found nothing, and is then called to face it for real."
},
{
"chunk": 106,
"story": 7,
"summary": "The armed men crowd into the rogue's cottage, pushing him ahead of them, despite his protests.",
"predicament": "A man with something to hide has an armed crowd force its way into his home."
},
{
"chunk": 107,
"story": 7,
"summary": "His kitchen is full of pork and poultry; accused of theft, he brazenly claims to have gone into the meat trade.",
"predicament": "A man caught with the evidence of his thefts brazens it out and dares his accusers to prove anything."
},
{
"chunk": 108,
"story": 7,
"summary": "He turns their own tiger sightings against them: those who swore they saw it would have to repeat it on oath.",
"predicament": "A man accused of wrongdoing turns his accusers' own exaggerations into his defence."
},
{
"chunk": 109,
"story": 7,
"summary": "He claims to have receipts, offers to sell them the meat, and gets away with it; he and the tramp turn out to have known each other.",
"predicament": "People who know they were swindled cannot prove it, and must watch the swindler profit."
},
{
"chunk": 110,
"story": 8,
"summary": "A retired officer, dressed with great care, sets out on a hot afternoon and overtakes a friend who stares uneasily at his loud tie.",
"predicament": "A man nervously setting out to do something that matters to him is stopped by a friend with questions."
},
{
"chunk": 111,
"story": 8,
"summary": "The officer confides he is going to propose to a widow; his friend, equally dressed up, says he is on the same errand, and the two race along the road.",
"predicament": "A man discovers a friend is after the same person he is, on the same day, and each races to get there first."
},
{
"chunk": 112,
"story": 8,
"summary": "Followed by jeering boys, they stop and agree they cannot propose together, but neither will yield the day, each pleading an excuse.",
"predicament": "Two rivals agree someone must go first, and neither will be the one to wait."
},
{
"chunk": 113,
"story": 8,
"summary": "Each claims precedence; the officer vows that if they go together he will match any proposal on the spot, and refuses to be put off.",
"predicament": "Two rivals for the same prize trade claims of priority, and each suspects the other of a trick."
},
{
"chunk": 114,
"story": 8,
"summary": "The officer proposes they play one game of chess for the right to propose first; the friend agrees, on condition that if the winner is refused, the loser may not propose without his permission.",
"predicament": "Two rivals agree to settle who goes first by a game, and one slips in a condition the other does not examine."
},
{
"chunk": 115,
"story": 8,
"summary": "The officer offers his rival a large brandy, which the rival sets aside; after a long game the rival checkmates him with a bishop the officer had forgotten.",
"predicament": "A man who tried to tip a contest in his favour loses it through his own oversight."
},
{
"chunk": 116,
"story": 8,
"summary": "The winner tidies himself in the loser's house and goes off to propose; the loser waits up all night and next morning goes to hear the result.",
"predicament": "A man who lost the right to try first must wait while his rival takes his chance."
},
{
"chunk": 117,
"story": 8,
"summary": "The rival was refused, but holds the officer to the condition and plans to keep proposing until the widow accepts him.",
"predicament": "A man finds the rival he lost to has been refused, but can still use the terms of their bargain to keep him out."
},
{
"chunk": 118,
"story": 8,
"summary": "The rival says he will warn the widow off the officer, suggests the officer court her companion instead, and the officer storms out and sulks for weeks.",
"predicament": "A man bound by a promise he made watches a rival use it to keep him from what he wants."
},
{
"chunk": 119,
"story": 8,
"summary": "Meeting the widow by chance, the officer confesses the chess game to explain his absence; she is indignant, then laughs.",
"predicament": "A man who has been avoiding someone he cares for must explain why, and the only honest answer is embarrassing."
},
{
"chunk": 120,
"story": 8,
"summary": "She says she is angry with him for losing, and proposes a plan so that he can win at cards what he lost at chess, under the rival's eyes.",
"predicament": "A man bound by a rash promise finds the person he hoped to win is willing to help him get round it."
},
{
"chunk": 121,
"story": 8,
"summary": "Playing cards, the widow asks the officer if he would like her hand; he takes it and kisses it, she 'meant the cards', then lets him keep it, and the rival is left fuming.",
"predicament": "Someone arranges to accept the suitor she prefers in a way his rival cannot object to."
},
{
"chunk": 122,
"story": 9,
"summary": "A man wakes up thirsty in a cramped, reeking bunk, with a stranger's face swearing at him, and cannot tell where he is.",
"predicament": "A man wakes up somewhere strange with no idea how he got there."
},
{
"chunk": 123,
"story": 9,
"summary": "He realises he is on a ship, dressed in rags; he finds his friend among the sleepers and learns the ship's name.",
"predicament": "A man in an unknown place looks for his one companion and a first clue to how they came to be there."
},
{
"chunk": 124,
"story": 9,
"summary": "Seasick and far from land, he goes aft and demands to be taken home; the officer calls him mad and orders him forward.",
"predicament": "A man who has always been obeyed tries to give orders somewhere nobody knows or believes who he is."
},
{
"chunk": 125,
"story": 9,
"summary": "His friend tries to threaten legal consequences but is too sick to finish; the two bicker over which of them was drunk.",
"predicament": "Two people in trouble together try to put on a united front and end up blaming each other."
},
{
"chunk": 126,
"story": 9,
"summary": "The captain says they signed on as cook and seaman, calls them drunk impostors, and threatens jail; they are put to work.",
"predicament": "Two people who claim to be respected professionals are treated as impostors and forced to do menial work."
},
{
"chunk": 127,
"story": 9,
"summary": "The crew mocks their claims of being drugged and of sobriety; their airs make the officers dislike them all the more.",
"predicament": "People brought low insist on their old status, and are mocked for it by those around them."
},
{
"chunk": 128,
"story": 9,
"summary": "The captain humiliates the cook; the cook obeys, tidies the cabin, and comes back with a secret smile and something in his pocket, telling his friend it won't be for long.",
"predicament": "A humiliated man pretends to submit while he quietly prepares his revenge."
},
{
"chunk": 129,
"story": 9,
"summary": "The captain is struck with violent pains; the cook refuses to treat him, as he 'only shipped as cook', until he agrees to pay a professional fee.",
"predicament": "A man mocked for claiming skill is suddenly needed for it, and names his price."
},
{
"chunk": 130,
"story": 9,
"summary": "The cook prescribes, collects his fee, scares the captain about making a will, and has the mate sent out for snuffling.",
"predicament": "A man who has gained the upper hand over those who humiliated him enjoys every moment of it."
},
{
"chunk": 131,
"story": 9,
"summary": "The cook hints he dosed the food from the medicine chest; the mate falls ill too, and the cook demands to be treated as surgeon.",
"predicament": "A man who has secretly caused the trouble only he can cure makes the most of it."
},
{
"chunk": 132,
"story": 9,
"summary": "The mate haggles over the fee; the second officer falls ill too; the sick captain scolds the sick mate for being ill at all.",
"predicament": "A man who swore he would never pay the person he scorned is forced by suffering to haggle with him."
},
{
"chunk": 133,
"story": 9,
"summary": "The cook treats and weakens all three officers; the captain and mate pay the other man to draw up their wills; they recover slowly as they reach port.",
"predicament": "People who scorned two outsiders now depend on them completely, and pay for the privilege."
},
{
"chunk": 134,
"story": 9,
"summary": "Ashore, the cook tells the captain he ate something that disagreed with him, and leaves with a wave.",
"predicament": "A man who got even with his tormentors leaves them knowing they were had."
},
{
"chunk": 135,
"story": 10,
"summary": "At a crowded family breakfast, a poor widow living with her sister's family gets a letter and reads it so slowly that the others nearly burst with impatience.",
"predicament": "Someone given news that might change their life cannot take it in, while the people around them itch to know."
},
{
"chunk": 136,
"story": 10,
"summary": "The widow fears the letter is a hoax; her brother-in-law thinks so too, until her sister reads it and rejoices.",
"predicament": "Someone who has never had luck is told of a windfall and is afraid to believe it."
},
{
"chunk": 137,
"story": 10,
"summary": "The widow has inherited two thousand pounds from a former employer; the brother-in-law grumbles that nobody ever left him anything, and spreads the news through the town.",
"predicament": "A man sees a relative come into money and resents that luck never comes his way."
},
{
"chunk": 138,
"story": 10,
"summary": "Local men suddenly befriend the brother-in-law; his wife points out that two old dealers are courting the heiress for her money.",
"predicament": "A man flattered by new friends is told they are really after a relative's money."
},
{
"chunk": 139,
"story": 10,
"summary": "The brother-in-law watches the two suitors flatter the widow and tries to undercut them by announcing her age, nearly giving away his wife's.",
"predicament": "A man trying to expose flattering suitors trips over his own family's sensibilities."
},
{
"chunk": 140,
"story": 10,
"summary": "The suitors outbid each other with compliments; the wife's temper frays, and the visitors are shown out with the door shut hard.",
"predicament": "Two rivals courting the same person keep topping each other's compliments while the family grows fed up."
},
{
"chunk": 141,
"story": 10,
"summary": "The family, wanting to keep the money, fail to warn the widow; the slow dealer, losing ground to his glib rival, hatches a plan and enlists a small-time dealer.",
"predicament": "A slow-talking man losing a contest to a glib rival looks for a trick instead of better words."
},
{
"chunk": 142,
"story": 10,
"summary": "The dealer bribes an accomplice, who holds out for a bigger fee, to tell the rival in confidence that the widow has lost her fortune; the rival swallows it.",
"predicament": "A man hires someone to spread a false rumour that will drive off his rival, and the hireling drives a hard bargain."
},
{
"chunk": 143,
"story": 10,
"summary": "With the rival gone, the dealer courts the widow alone, preaching the virtues of poverty, and loses seven games of draughts to her on purpose.",
"predicament": "A man courting for money pretends not to care about it and flatters the one he courts by letting her win."
},
{
"chunk": 144,
"story": 10,
"summary": "He blows her kisses and fusses over a bumped head, invites himself back, and walks home sure the money is his.",
"predicament": "A schemer who thinks his deal is done walks away gloating too soon."
},
{
"chunk": 145,
"story": 10,
"summary": "He spreads the rumour of her ruin through the town, then proposes publicly 'now that she is poor', to look noble.",
"predicament": "A man makes a public show of selfless devotion, sure that it will cost him nothing."
},
{
"chunk": 146,
"story": 10,
"summary": "She accepts; the family, touched, apologise for misjudging him, then mention a letter that arrived.",
"predicament": "A schemer whose plan has worked is thanked for his goodness while the ground starts to shift under him."
},
{
"chunk": 147,
"story": 10,
"summary": "The lawyers' letter says a later will was found and she has nothing; the dealer, now engaged to a poor woman, is left alone with her to court.",
"predicament": "A man who faked a disaster to win a prize learns the disaster is real, and he is bound to his promise."
},
{
"chunk": 148,
"story": 11,
"summary": "In an inn, men trade ghost stories; a sceptical old sailor mocks sailors' visions, then admits one uncanny experience on land.",
"predicament": "A sceptic among believers is pressed to admit whether he has ever seen anything he could not explain."
},
{
"chunk": 149,
"story": 11,
"summary": "As a young man back from abroad, the narrator found his relatives away, took lodgings at a dull inn, and set out on a long country walk.",
"predicament": "A young man who arrived to find no one expecting him has to fill empty days in a strange place."
},
{
"chunk": 150,
"story": 11,
"summary": "He loses his way in the marshes as fog and night come down, and at last finds a road and hears footsteps.",
"predicament": "A traveller lost in the dark and the cold must find shelter before night closes in."
},
{
"chunk": 151,
"story": 11,
"summary": "A countryman sets him on the road but warns him away from a lit house nearby, said to hide something monstrous; tired, he goes to it anyway.",
"predicament": "A worn-out traveller must choose between a long walk on and a nearby shelter he has been warned against."
},
{
"chunk": 152,
"story": 11,
"summary": "A grim old woman and a huge old man answer; after a furtive exchange the man insists he stay for dinner, and he feels trapped.",
"predicament": "A stranger taken in by odd hosts who seem keen to keep him wonders whether he has walked into a trap."
},
{
"chunk": 153,
"story": 11,
"summary": "The host says his son will dine with them in the dark because of his eyes; he puts out the candles, and a strange voice speaks.",
"predicament": "A guest must sit down in the dark with someone he cannot see, on hosts' terms he does not understand."
},
{
"chunk": 154,
"story": 11,
"summary": "The host leaves him alone with the unseen son, and the guest thinks he sees wild eyes shining at him across the table.",
"predicament": "A guest left alone with a stranger he cannot see grows afraid, and must decide whether to trust his hosts."
},
{
"chunk": 155,
"story": 11,
"summary": "The screen falls, and firelight shows the son's ruined face; the father lights the candles and explains his son was disfigured in a fire.",
"predicament": "A frightened man sees what he feared, and learns it is someone who was hurt and ashamed to be seen."
},
{
"chunk": 156,
"story": 11,
"summary": "The guest apologises warmly; they celebrate the son's birthday until midnight, and the father toasts the children his son saved.",
"predicament": "A man who shrank from someone must make amends, and finds they deserve his respect."
}
]
File diff suppressed because one or more lines are too long
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env python3
"""Print a range of scenes for reading. Usage: dump.py <first_chunk> <last_chunk>"""
import json, sys
chunks = json.load(open(__file__.rsplit('/',1)[0] + '/chunks.json'))
a, b = int(sys.argv[1]), int(sys.argv[2])
cur = None
for i in range(a, min(b + 1, len(chunks))):
c = chunks[i]
if c['story'] != cur:
cur = c['story']
print(f"\n########## STORY {cur}: {c['title']} ##########")
print(f"\n----- scene {i} (story {cur}) -----")
print(c['text'].strip())
@@ -0,0 +1,48 @@
"""Split the Standard Ebooks single-page HTML of W. W. Jacobs, *The Lady of the Barge*, into stories,
then cut scenes with the same rule as ../chunk.py (paragraph boundaries, ~320 words, tail >= 80).
Deterministic: each story is a <section> with an <h2 epub:type="title">. Front and back matter
(contents, imprint, colophon, uncopyright) are dropped by name."""
import html, json, pathlib, re
SKIP = {'Table of Contents', 'Imprint', 'Colophon', 'Uncopyright'}
TARGET = 320
VOLUME = 'The Lady of the Barge (W. W. Jacobs, 1902)'
raw = pathlib.Path('corpus/lady-of-the-barge.html').read_text(encoding='utf-8')
body = raw[raw.find('<body'):]
parts = re.split(r'<h2[^>]*epub:type="title"[^>]*>(.*?)</h2>', body, flags=re.S)
def text_of(fragment):
paras = []
for p in re.findall(r'<p[^>]*>(.*?)</p>', fragment, flags=re.S):
t = html.unescape(re.sub(r'<[^>]+>', '', p))
t = re.sub(r'\s+', ' ', t).strip()
if t: paras.append(t)
return paras
stories = []
for i in range(1, len(parts), 2):
title = html.unescape(re.sub(r'<[^>]+>', '', parts[i])).strip()
if title in SKIP: continue
paras = text_of(parts[i + 1])
stories.append({'volume': VOLUME, 'title': title, 'words': sum(len(p.split()) for p in paras),
'text': '\n\n'.join(paras)})
chunks = []
for si, s in enumerate(stories):
buf, n = [], 0
for p in s['text'].split('\n\n'):
w = len(p.split())
if n + w > TARGET and buf:
chunks.append({'story': si, 'title': s['title'], 'volume': s['volume'], 'text': ' '.join(buf), 'words': n})
buf, n = [], 0
buf.append(p); n += w
if buf and n >= 80:
chunks.append({'story': si, 'title': s['title'], 'volume': s['volume'], 'text': ' '.join(buf), 'words': n})
pathlib.Path('stories.json').write_text(json.dumps(stories, indent=1, ensure_ascii=False), encoding='utf-8')
pathlib.Path('chunks.json').write_text(json.dumps(chunks, ensure_ascii=False), encoding='utf-8')
for si, s in enumerate(stories):
print(f"{si:2} {s['title']:24} {s['words']:6} words {sum(c['story']==si for c in chunks):3} scenes")
ws = sorted(c['words'] for c in chunks)
print(f'stories {len(stories)} scenes {len(chunks)} words/scene min {ws[0]} median {ws[len(ws)//2]} max {ws[-1]} total {sum(ws):,}')
File diff suppressed because one or more lines are too long
+27
View File
@@ -0,0 +1,27 @@
qwen3:14b: 250 distinct clusters, 5 already judged
10/250 28s parse failures 0 last: D15 fit 0.47 "eviction threat or action"
20/250 58s parse failures 0 last: D25 fit 0.53 "Wealthy individual seeking social or personal advancement th"
30/250 89s parse failures 0 last: D35 fit 0.47 "Someone is suspicious of another person's intentions"
40/250 119s parse failures 0 last: T05 fit 0.73 "Attempting to obtain money through deception or coercion"
50/250 147s parse failures 0 last: T15 fit 0.47 "Someone pressures someone else into an action against their "
60/250 175s parse failures 0 last: T25 fit 0.33 "Someone trying to impress someone else with their qualities "
70/250 204s parse failures 0 last: T35 fit 0.53 "Threats and demands for money"
80/250 232s parse failures 0 last: R004 fit 0.60 "Confrontation between individuals over a personal or social "
90/250 264s parse failures 0 last: R014 fit 0.73 "Drinking in a bar with social tension"
100/250 296s parse failures 0 last: X003 fit 0.27 "Someone courts someone, while someone interrupts them"
110/250 326s parse failures 0 last: X013 fit 0.71 "Seeking financial advice"
120/250 355s parse failures 0 last: X023 fit 0.33 "Trying to impress someone through a dinner invitation"
130/250 378s parse failures 0 last: X033 fit 0.53 "someone pressures someone else for personal gain"
140/250 405s parse failures 0 last: X043 fit 0.60 "Dinner party with social dynamics"
150/250 436s parse failures 0 last: X053 fit 0.27 "Exploitation of the naive or vulnerable"
160/250 475s parse failures 0 last: X063 fit 0.50 "Confrontation between individuals over a disagreement or con"
170/250 526s parse failures 0 last: X073 fit 0.53 "Manipulation for personal gain"
180/250 586s parse failures 0 last: Y010 fit 0.87 "Artist seeking financial support or patronage"
190/250 643s parse failures 0 last: Y020 fit 0.45 "Seeking relief from hardship in a city"
200/250 690s parse failures 0 last: Y030 fit 0.71 "Social interaction at a dining establishment"
210/250 722s parse failures 0 last: Y040 fit 0.67 "Competing for social status through dining experiences"
220/250 764s parse failures 0 last: Y050 fit 0.62 "Someone trying to win someone else's affection"
230/250 794s parse failures 0 last: Y060 fit 0.00 "NONE"
240/250 818s parse failures 0 last: Y070 fit 0.00 "NONE"
done: 245 judged this session in 832s; parse failures 0
exit 0
+27
View File
@@ -0,0 +1,27 @@
qwen2.5:3b-instruct: 250 distinct clusters, 5 already judged
10/250 11s parse failures 0 last: D15 fit 0.80 "Eviction and housing disputes"
20/250 24s parse failures 1 last: D25 fit 0.33 "seeking advice or assistance"
30/250 37s parse failures 1 last: D35 fit 0.73 "romantic interest or potential romance"
40/250 48s parse failures 1 last: T05 fit 0.87 "con or scam"
50/250 60s parse failures 1 last: T15 fit 0.40 "manipulation and hidden agenda"
60/250 71s parse failures 1 last: T25 fit 0.40 "flattery and attention seeking"
70/250 83s parse failures 1 last: T35 fit 0.20 "Demanding payment/repayment"
80/250 94s parse failures 1 last: R004 fit 0.47 "confrontation over authority or status"
90/250 108s parse failures 1 last: R014 fit 0.87 "drinking and socializing"
100/250 120s parse failures 1 last: X003 fit 1.00 "demand and confrontation"
110/250 131s parse failures 1 last: X013 fit 0.43 "seeking financial advice"
120/250 142s parse failures 1 last: X023 fit 0.47 "social gathering or dinner party"
130/250 153s parse failures 1 last: X033 fit 0.73 "negotiation and payment"
140/250 165s parse failures 1 last: X043 fit 0.93 "dinner party/hosting"
150/250 177s parse failures 1 last: X053 fit 0.67 "exploit and exploitation"
160/250 189s parse failures 1 last: X063 fit 0.67 "confrontation"
170/250 200s parse failures 1 last: X073 fit 0.60 "coercion and manipulation"
180/250 214s parse failures 1 last: Y010 fit 0.00 "NONE"
190/250 226s parse failures 1 last: Y020 fit 0.64 "seeking connections or companionship"
200/250 236s parse failures 1 last: Y030 fit 0.71 "dining and socializing"
210/250 248s parse failures 1 last: Y040 fit 1.00 "people seek social status or attention"
220/250 258s parse failures 1 last: Y050 fit 0.25 "courtship and seduction"
230/250 268s parse failures 1 last: Y060 fit 0.40 "confrontation over professional issue"
240/250 280s parse failures 1 last: Y070 fit 0.80 "people pursuing their livelihood or interests"
done: 245 judged this session in 286s; parse failures 1
exit 0
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,29 @@
{
"theta": 0.6,
"primary_G4": {
"accepted": 50,
"label1plus": 24,
"label2": 4,
"P1+": 0.48,
"recall1+": 0.3582089552238806
},
"primary_G2": {
"accepted": 49,
"label1plus": 36,
"label2": 5,
"P1+": 0.7346938775510204,
"recall1+": 0.5373134328358209
},
"small": {
"accepted": 13,
"label1plus": 3,
"label2": 1,
"P1+": 0.23076923076923078,
"recall1+": 0.3333333333333333
},
"criteria": [
false,
false,
false
]
}
@@ -0,0 +1,29 @@
{
"theta": 0.6,
"primary_G4": {
"accepted": 42,
"label1plus": 22,
"label2": 4,
"P1+": 0.5238095238095238,
"recall1+": 0.3283582089552239
},
"primary_G2": {
"accepted": 49,
"label1plus": 36,
"label2": 5,
"P1+": 0.7346938775510204,
"recall1+": 0.5373134328358209
},
"small": {
"accepted": 17,
"label1plus": 5,
"label2": 2,
"P1+": 0.29411764705882354,
"recall1+": 0.5555555555555556
},
"criteria": [
false,
false,
false
]
}
+94
View File
@@ -0,0 +1,94 @@
"""G4: ask the model whether a cluster's summaries share one social situation (SELECTION.md, "G4").
STP_OLLAMA=http://host:11434 python3 judge.py qwen2.5:3b-instruct --pilot # first 5 dev clusters
STP_OLLAMA=http://host:11434 python3 judge.py qwen2.5:3b-instruct # all labelled clusters
Writes judge-<model>.json, keyed by a hash of the member set, checkpointed after every call.
"""
import hashlib, json, pathlib, random, re, sys, time
import ollama
MODEL = sys.argv[1]
PILOT = '--pilot' in sys.argv
SHOW = 15
SYSTEM = """You read short summaries of scenes from different stories and decide whether they share one kind of social situation.
A social situation is a specific transaction between people: who wants what from whom, and what is at stake. Describe it in your own words.
Rules:
- Find the single situation that the largest number of summaries clearly show.
- It is normal for many summaries not to fit. Include a summary only if it clearly shows that situation.
- A shared place, mood, profession or word is not a situation.
- If no situation is clearly shown by at least three summaries, the situation is NONE.
Reply with JSON only: {"situation": "<at most 10 words, or NONE>", "fits": [<numbers of the summaries that clearly show it>]}"""
text = {x['chunk']: x['summary'] for x in
json.loads(pathlib.Path('summaries_clean.json').read_text(encoding='utf-8'))['summaries']}
def clusters():
"""(set name, cluster id, members) for every labelled cluster, development first."""
out = []
for name in ('dev', 'test'):
for cid, m in json.loads(pathlib.Path(f'candidates-{name}.json').read_text()).items():
out.append((name, cid, m))
for name, mapfile in (('recover', 'recover-map.json'), ('validate', 'validate-map.json'),
('validate3', 'validate3-map.json')):
for cid, e in json.loads(pathlib.Path(mapfile).read_text()).items():
out.append((name, cid, e['members']))
return out
def key(members):
return hashlib.sha1(','.join(map(str, sorted(members))).encode()).hexdigest()[:16]
def parse(raw, shown):
raw = re.sub(r'<think>.*?</think>', '', raw, flags=re.S).strip()
m = re.search(r'\{.*\}', raw, flags=re.S)
if not m: return None
try:
d = json.loads(m.group(0))
except Exception:
return None
situation = str(d.get('situation', '')).strip()
fits = d.get('fits', [])
if not isinstance(fits, list): return None
valid = sorted({int(f) for f in fits if isinstance(f, (int, float, str)) and str(f).strip().isdigit()
and 1 <= int(f) <= shown})
none = not situation or situation.upper().startswith('NONE')
return {'situation': situation, 'fits': valid, 'fit': 0.0 if none else len(valid) / shown}
out_path = pathlib.Path(f"judge-{MODEL.replace(':', '_').replace('/', '_')}.json")
cache = json.loads(out_path.read_text()) if out_path.exists() else {}
todo, seen = [], set()
for name, cid, members in clusters():
k = key(members)
if k in seen: continue
seen.add(k)
todo.append((name, cid, members, k))
if PILOT: todo = todo[:5]
print(f'{MODEL}: {len(todo)} distinct clusters, {sum(k in cache for *_, k in todo)} already judged', flush=True)
t0, done, failures = time.time(), 0, 0
for name, cid, members, k in todo:
if k in cache: continue
sample = sorted(members)
rnd = random.Random(int(k, 16))
if len(sample) > SHOW: sample = rnd.sample(sample, SHOW)
rnd.shuffle(sample)
user = '\n'.join(f'{i + 1}. {text[c]}' for i, c in enumerate(sample))
if MODEL.startswith('qwen3'): user += '\n\n/no_think'
if MODEL.startswith('qwen3'):
raw, gen, pre = ollama.chat(MODEL, SYSTEM, user, num_ctx=2048, num_predict=300, fmt='json', think=False)
else:
raw, gen, pre = ollama.chat(MODEL, SYSTEM, user, num_ctx=2048, num_predict=200, fmt='json')
parsed = parse(raw, len(sample))
failures += parsed is None
cache[k] = {'set': name, 'id': cid, 'size': len(members), 'shown': len(sample), 'raw': raw,
'parsed': parsed is not None, **(parsed or {'situation': '', 'fits': [], 'fit': 0.0})}
out_path.write_text(json.dumps(cache, indent=1))
done += 1
if PILOT or done % 10 == 0:
print(f' {done}/{len(todo)} {time.time() - t0:.0f}s parse failures {failures} '
f'last: {cid} fit {cache[k]["fit"]:.2f} "{cache[k]["situation"][:60]}"', flush=True)
print(f'done: {done} judged this session in {time.time() - t0:.0f}s; parse failures {failures}')
+42
View File
@@ -0,0 +1,42 @@
{
"D01": {"label": 1, "name": "a heated disagreement over whose view is right"},
"D02": {"label": 0, "name": ""},
"D03": {"label": 1, "name": "money and marriage: wealthy suitors, inheritances"},
"D04": {"label": 2, "name": "a spouse schemes against or confronts a spouse over infidelity"},
"D05": {"label": 1, "name": "struggling artists and money"},
"D06": {"label": 1, "name": "writers and editors"},
"D07": {"label": 1, "name": "schemes, cons and theft over money"},
"D08": {"label": 1, "name": "a relative ill, missing or in trouble"},
"D09": {"label": 1, "name": "who holds sway in a dining room or hotel"},
"D10": {"label": 0, "name": ""},
"D11": {"label": 1, "name": "working girls: jobs, wages and standing"},
"D12": {"label": 1, "name": "a woman at the centre of rival admirers"},
"D13": {"label": 2, "name": "a run-in with the police"},
"D14": {"label": 1, "name": "cabs, traffic and accidents in the street"},
"D15": {"label": 2, "name": "landlord or housekeeper against tenant: rooms and eviction"},
"D16": {"label": 2, "name": "someone coerces someone with a threat"},
"D17": {"label": 0, "name": ""},
"D18": {"label": 0, "name": ""},
"D19": {"label": 0, "name": ""},
"D20": {"label": 1, "name": "grief and loneliness at home"},
"D21": {"label": 1, "name": "a reunion or first meeting"},
"D22": {"label": 1, "name": "romantic pursuit and confrontation"},
"D23": {"label": 1, "name": "artist and patron or model"},
"D24": {"label": 0, "name": ""},
"D25": {"label": 1, "name": "what to do with money, and whose advice to take"},
"D26": {"label": 0, "name": ""},
"D27": {"label": 1, "name": "climbing socially by appearances"},
"D28": {"label": 0, "name": ""},
"D29": {"label": 1, "name": "searching for a missing person, demanding information"},
"D30": {"label": 2, "name": "a run-in with the police"},
"D31": {"label": 0, "name": ""},
"D32": {"label": 1, "name": "a meal given as patronage"},
"D33": {"label": 1, "name": "a secret past revealed in a romance"},
"D34": {"label": 0, "name": ""},
"D35": {"label": 0, "name": ""},
"D36": {"label": 0, "name": ""},
"D37": {"label": 1, "name": "a public quarrel with a shopkeeper, driver or official"},
"D38": {"label": 0, "name": ""},
"D39": {"label": 0, "name": ""},
"D40": {"label": 0, "name": ""}
}
@@ -0,0 +1,23 @@
{
"R001": {"label": 1, "name": "an inheritance or fortune and what it obliges"},
"R002": {"label": 0, "name": ""},
"R003": {"label": 1, "name": "someone coerces someone with a threat"},
"R004": {"label": 1, "name": "a run-in with the police"},
"R005": {"label": 0, "name": ""},
"R006": {"label": 1, "name": "a spouse schemes against or confronts a spouse over infidelity"},
"R007": {"label": 1, "name": "schemes, cons and theft over money"},
"R008": {"label": 1, "name": "courtship"},
"R009": {"label": 1, "name": "coercion over money: threats, bribes, demands for payment"},
"R010": {"label": 1, "name": "courtship"},
"R011": {"label": 2, "name": "money and marriage: wealthy suitors, inheritances"},
"R012": {"label": 1, "name": "who holds sway in a dining room or hotel"},
"R013": {"label": 0, "name": ""},
"R014": {"label": 1, "name": "drinking in the saloon"},
"R015": {"label": 2, "name": "a spouse schemes against or confronts a spouse over infidelity"},
"R016": {"label": 0, "name": ""},
"R017": {"label": 1, "name": "money owed and repayment"},
"R018": {"label": 1, "name": "trying to impress or win someone's favour"},
"R019": {"label": 1, "name": "who holds sway in a dining room or hotel"},
"R020": {"label": 2, "name": "someone coerces someone with a threat"},
"R021": {"label": 0, "name": ""}
}
@@ -0,0 +1,43 @@
{
"T01": {"label": 0, "name": ""},
"T02": {"label": 0, "name": ""},
"T03": {"label": 1, "name": "money owed and repayment"},
"T04": {"label": 1, "name": "confrontations in the park and on the street"},
"T05": {"label": 1, "name": "schemes, cons and theft over money"},
"T06": {"label": 1, "name": "actresses and the stage"},
"T07": {"label": 1, "name": "courtship"},
"T08": {"label": 0, "name": ""},
"T09": {"label": 0, "name": ""},
"T10": {"label": 0, "name": ""},
"T11": {"label": 0, "name": ""},
"T12": {"label": 1, "name": "hosting in a restaurant or bar"},
"T13": {"label": 0, "name": ""},
"T14": {"label": 0, "name": ""},
"T15": {"label": 1, "name": "meals, drinks and who pays"},
"T16": {"label": 2, "name": "a newcomer seeks acceptance in a social circle"},
"T17": {"label": 2, "name": "a proposal or engagement, accepted or refused"},
"T18": {"label": 0, "name": ""},
"T19": {"label": 1, "name": "shop and service work and standing"},
"T20": {"label": 1, "name": "sizing up a new town"},
"T21": {"label": 0, "name": ""},
"T22": {"label": 0, "name": ""},
"T23": {"label": 0, "name": ""},
"T24": {"label": 0, "name": ""},
"T25": {"label": 1, "name": "trying to impress or win someone's favour"},
"T26": {"label": 0, "name": ""},
"T27": {"label": 1, "name": "cabs, traffic and accidents in the street"},
"T28": {"label": 0, "name": ""},
"T29": {"label": 0, "name": ""},
"T30": {"label": 2, "name": "a run-in with the police"},
"T31": {"label": 0, "name": ""},
"T32": {"label": 1, "name": "illness, loss and a missing relative"},
"T33": {"label": 1, "name": "a brawl or showdown"},
"T34": {"label": 1, "name": "drinking in the saloon"},
"T35": {"label": 2, "name": "coercion over money: threats, bribes, demands for payment"},
"T36": {"label": 0, "name": ""},
"T37": {"label": 1, "name": "a reunion or first meeting"},
"T38": {"label": 0, "name": ""},
"T39": {"label": 2, "name": "a spouse schemes against or confronts a spouse over infidelity"},
"T40": {"label": 0, "name": ""},
"T41": {"label": 1, "name": "struggling artists and money"}
}
@@ -0,0 +1,75 @@
{
"X001": {"label": 1, "name": "schemes, cons and theft over money"},
"X002": {"label": 1, "name": "struggling artists and money"},
"X003": {"label": 0, "name": ""},
"X004": {"label": 1, "name": "schemes, cons and theft over money"},
"X005": {"label": 1, "name": "who holds sway in a dining room or hotel"},
"X006": {"label": 1, "name": "a meal given as patronage"},
"X007": {"label": 0, "name": ""},
"X008": {"label": 2, "name": "a reunion or first meeting"},
"X009": {"label": 1, "name": "a proposal or engagement, accepted or refused"},
"X010": {"label": 0, "name": ""},
"X011": {"label": 1, "name": "drinking in the saloon"},
"X012": {"label": 0, "name": ""},
"X013": {"label": 1, "name": "what to do with money, and whose advice to take"},
"X014": {"label": 0, "name": ""},
"X015": {"label": 0, "name": ""},
"X016": {"label": 1, "name": "who holds sway in a dining room or hotel"},
"X017": {"label": 0, "name": ""},
"X018": {"label": 1, "name": "shop and service work and standing"},
"X019": {"label": 1, "name": "a public quarrel with a shopkeeper, driver or official"},
"X020": {"label": 0, "name": ""},
"X021": {"label": 1, "name": "courtship"},
"X022": {"label": 1, "name": "working girls: jobs, wages and standing"},
"X023": {"label": 1, "name": "who holds sway in a dining room or hotel"},
"X024": {"label": 0, "name": ""},
"X025": {"label": 0, "name": ""},
"X026": {"label": 0, "name": ""},
"X027": {"label": 0, "name": ""},
"X028": {"label": 0, "name": ""},
"X029": {"label": 0, "name": ""},
"X030": {"label": 1, "name": "a brawl or showdown"},
"X031": {"label": 1, "name": "actresses and the stage"},
"X032": {"label": 0, "name": ""},
"X033": {"label": 0, "name": ""},
"X034": {"label": 1, "name": "struggling artists and money"},
"X035": {"label": 0, "name": ""},
"X036": {"label": 1, "name": "illness, loss and a missing relative"},
"X037": {"label": 0, "name": ""},
"X038": {"label": 0, "name": ""},
"X039": {"label": 1, "name": "working girls: jobs, wages and standing"},
"X040": {"label": 0, "name": ""},
"X041": {"label": 1, "name": "money and marriage: wealthy suitors, inheritances"},
"X042": {"label": 2, "name": "a public quarrel with a shopkeeper, driver or official"},
"X043": {"label": 1, "name": "who holds sway in a dining room or hotel"},
"X044": {"label": 2, "name": "landlord or housekeeper against tenant: rooms and eviction"},
"X045": {"label": 1, "name": "struggling artists and money"},
"X046": {"label": 0, "name": ""},
"X047": {"label": 1, "name": "family pressures over work and marriage"},
"X048": {"label": 1, "name": "money owed and repayment"},
"X049": {"label": 0, "name": ""},
"X050": {"label": 1, "name": "schemes, cons and theft over money"},
"X051": {"label": 2, "name": "a spouse schemes against or confronts a spouse over infidelity"},
"X052": {"label": 1, "name": "schemes, cons and theft over money"},
"X053": {"label": 0, "name": ""},
"X054": {"label": 0, "name": ""},
"X055": {"label": 2, "name": "a spouse schemes against or confronts a spouse over infidelity"},
"X056": {"label": 1, "name": "trying to impress or win someone's favour"},
"X057": {"label": 1, "name": "schemes, cons and theft over money"},
"X058": {"label": 0, "name": ""},
"X059": {"label": 1, "name": "a proposal or engagement, accepted or refused"},
"X060": {"label": 0, "name": ""},
"X061": {"label": 0, "name": ""},
"X062": {"label": 0, "name": ""},
"X063": {"label": 0, "name": ""},
"X064": {"label": 1, "name": "a spouse schemes against or confronts a spouse over infidelity"},
"X065": {"label": 1, "name": "a heated disagreement over whose view is right"},
"X066": {"label": 0, "name": ""},
"X067": {"label": 0, "name": ""},
"X068": {"label": 2, "name": "a spouse schemes against or confronts a spouse over infidelity"},
"X069": {"label": 1, "name": "trying to impress or win someone's favour"},
"X070": {"label": 1, "name": "a spouse schemes against or confronts a spouse over infidelity"},
"X071": {"label": 0, "name": ""},
"X072": {"label": 0, "name": ""},
"X073": {"label": 0, "name": ""}
}
@@ -0,0 +1,77 @@
{
"Y001": {"label": 0, "name": ""},
"Y002": {"label": 0, "name": ""},
"Y003": {"label": 1, "name": "struggling artists and money"},
"Y004": {"label": 1, "name": "landlord or housekeeper against tenant: rooms and eviction"},
"Y005": {"label": 0, "name": ""},
"Y006": {"label": 1, "name": "schemes, cons and theft over money"},
"Y007": {"label": 0, "name": ""},
"Y008": {"label": 0, "name": ""},
"Y009": {"label": 0, "name": ""},
"Y010": {"label": 1, "name": "struggling artists and money"},
"Y011": {"label": 0, "name": ""},
"Y012": {"label": 0, "name": ""},
"Y013": {"label": 1, "name": "struggling artists and money"},
"Y014": {"label": 0, "name": ""},
"Y015": {"label": 0, "name": ""},
"Y016": {"label": 1, "name": "a proposal or engagement, accepted or refused"},
"Y017": {"label": 1, "name": "an invitation to dine"},
"Y018": {"label": 0, "name": ""},
"Y019": {"label": 0, "name": ""},
"Y020": {"label": 0, "name": ""},
"Y021": {"label": 1, "name": "schemes, cons and theft over money"},
"Y022": {"label": 1, "name": "schemes, cons and theft over money"},
"Y023": {"label": 1, "name": "working girls: jobs, wages and standing"},
"Y024": {"label": 2, "name": "someone coerces someone with a threat"},
"Y025": {"label": 0, "name": ""},
"Y026": {"label": 0, "name": ""},
"Y027": {"label": 0, "name": ""},
"Y028": {"label": 1, "name": "actresses and the stage"},
"Y029": {"label": 0, "name": ""},
"Y030": {"label": 1, "name": "who holds sway in a dining room or hotel"},
"Y031": {"label": 0, "name": ""},
"Y032": {"label": 1, "name": "a run-in with the police"},
"Y033": {"label": 0, "name": ""},
"Y034": {"label": 0, "name": ""},
"Y035": {"label": 0, "name": ""},
"Y036": {"label": 0, "name": ""},
"Y037": {"label": 0, "name": ""},
"Y038": {"label": 1, "name": "someone coerces someone with a threat"},
"Y039": {"label": 0, "name": ""},
"Y040": {"label": 1, "name": "who holds sway in a dining room or hotel"},
"Y041": {"label": 0, "name": ""},
"Y042": {"label": 0, "name": ""},
"Y043": {"label": 1, "name": "a run-in with the police"},
"Y044": {"label": 0, "name": ""},
"Y045": {"label": 0, "name": ""},
"Y046": {"label": 0, "name": ""},
"Y047": {"label": 2, "name": "working girls: jobs, wages and standing"},
"Y048": {"label": 0, "name": ""},
"Y049": {"label": 2, "name": "a spouse schemes against or confronts a spouse over infidelity"},
"Y050": {"label": 0, "name": ""},
"Y051": {"label": 0, "name": ""},
"Y052": {"label": 2, "name": "an invitation to dine"},
"Y053": {"label": 1, "name": "a brawl or showdown"},
"Y054": {"label": 0, "name": ""},
"Y055": {"label": 0, "name": ""},
"Y056": {"label": 0, "name": ""},
"Y057": {"label": 0, "name": ""},
"Y058": {"label": 0, "name": ""},
"Y059": {"label": 0, "name": ""},
"Y060": {"label": 0, "name": ""},
"Y061": {"label": 0, "name": ""},
"Y062": {"label": 1, "name": "a spouse schemes against or confronts a spouse over infidelity"},
"Y063": {"label": 0, "name": ""},
"Y064": {"label": 1, "name": "a proposal or engagement, accepted or refused"},
"Y065": {"label": 0, "name": ""},
"Y066": {"label": 0, "name": ""},
"Y067": {"label": 1, "name": "illness, loss and a missing relative"},
"Y068": {"label": 0, "name": ""},
"Y069": {"label": 1, "name": "trying to impress or win someone's favour"},
"Y070": {"label": 0, "name": ""},
"Y071": {"label": 1, "name": "schemes, cons and theft over money"},
"Y072": {"label": 0, "name": ""},
"Y073": {"label": 1, "name": "writers and editors"},
"Y074": {"label": 1, "name": "working girls: jobs, wages and standing"},
"Y075": {"label": 0, "name": ""}
}
+46
View File
@@ -0,0 +1,46 @@
"""Dump candidate clusters to blind label sheets (SELECTION.md, protocol steps 1-3).
python3 make_sheets.py
Writes, for development (seed 11) and test (seed 12):
candidates-<set>.json cluster id -> member chunk indices (what signals are computed on)
sheet-<set>.txt per cluster, member summaries only, shuffled; no titles, no scores
labels-<set>.json an empty template to fill in: {id: {"label": null, "name": ""}}
Cluster ids are assigned in a shuffled order so position carries no information.
"""
import json, pathlib, random, sys
K = 60
SETS = {'dev': 11, 'test': 12}
sys.argv = ['measure.py', 'summary_embeddings.json']
exec(pathlib.Path('measure.py').read_text(encoding='utf-8').split("print(f'{path.name}")[0])
summaries = json.loads(pathlib.Path('summaries_clean.json').read_text(encoding='utf-8'))['summaries']
text = {x['chunk']: x['summary'] for x in summaries}
for name, seed in SETS.items():
cands = []
for c in kmeans(K, seed):
size, nst, dom, _ = describe(c)
if size >= MIN_SIZE and nst >= MIN_STORIES and dom <= MAX_DOMINANT:
cands.append(sorted(c))
rnd = random.Random(seed * 1000 + 7)
rnd.shuffle(cands)
prefix = name[0].upper()
ids = {f'{prefix}{i + 1:02d}': c for i, c in enumerate(cands)}
pathlib.Path(f'candidates-{name}.json').write_text(json.dumps(ids, indent=1))
lines = []
for cid, members in ids.items():
shown = members[:]
rnd.shuffle(shown)
lines.append(f'=== {cid}')
lines.extend(f' - {text[i]}' for i in shown)
lines.append('')
pathlib.Path(f'sheet-{name}.txt').write_text('\n'.join(lines), encoding='utf-8')
labels = pathlib.Path(f'labels-{name}.json')
if not labels.exists():
labels.write_text(json.dumps({cid: {'label': None, 'name': ''} for cid in ids}, indent=1))
print(f'{name}: seed {seed}, {len(ids)} candidate clusters, {sum(map(len, ids.values()))} scenes')
@@ -0,0 +1,11 @@
embeddings.json: 838 chunks from 94 stories, seeds (11, 12, 13)
k sized>=5 median dom recurring coverage tight rec tight cov
----------------------------------------------------------------------
25 25 0.25 21 93% 9 29%
40 39 0.36 21 71% 5 9%
60 55 0.57 20 55% 3 2%
100 78 0.67 16 30% 0 0%
recurring = size>=5, stories>=4, dominant share<=40%; tight = coherence at or above this file's median cluster. Judge at k>=60. Wrote embeddings.measure.json
exit 0
@@ -0,0 +1,11 @@
summary_embeddings.json: 838 chunks from 94 stories, seeds (11, 12, 13)
k sized>=5 median dom recurring coverage tight rec tight cov
----------------------------------------------------------------------
25 25 0.14 23 97% 11 40%
40 36 0.17 32 95% 14 36%
60 51 0.22 41 87% 18 26%
100 70 0.28 50 73% 20 19%
recurring = size>=5, stories>=4, dominant share<=40%; tight = coherence at or above this file's median cluster. Judge at k>=60. Wrote summary_embeddings.measure.json
exit 0
+103
View File
@@ -0,0 +1,103 @@
"""Do the clusters in an embedding file capture recurring SITUATIONS?
python3 measure.py embeddings.json # prose embeddings (the 2026-09-10 baseline)
python3 measure.py summary_embeddings.json # summarise-then-embed (the fix under test)
The 2026-09-10 probe counted distinct stories per cluster and was fooled: a
cluster reporting 5 stories was routinely 60-79% ONE story plus stragglers.
So this reports concentration, not just spread.
dominant share = largest single-story count / cluster size
recurring = size >= MIN_SIZE, >= MIN_STORIES distinct stories,
and dominant share <= MAX_DOMINANT
Pass mark: see DESIGN.md, "Assumptions under test". Judged at k = 60 and
k = 100 only. Results are the median over SEEDS, so one lucky (or unlucky)
initialisation cannot decide the test -- the 2026-09-10 "2% coverage" was
seed 11 alone; seeds 12 and 13 give 8-10%.
Pure Python -- no numpy on this machine. Only chunk indices that have an
embedding are used, so a partial (pilot) file is fine.
"""
import json, math, pathlib, random, statistics, sys
KS = (25, 40, 60, 100)
SEEDS = (11, 12, 13)
MIN_SIZE, MIN_STORIES, MAX_DOMINANT = 5, 4, 0.40
path = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else 'embeddings.json')
chunks = json.loads(pathlib.Path('chunks.json').read_text(encoding='utf-8'))
raw = json.loads(path.read_text(encoding='utf-8'))
vecs = raw['vectors'] if isinstance(raw, dict) else raw
n = min(len(chunks), len(vecs))
def norm(v):
m = math.sqrt(sum(x * x for x in v)) or 1.0
return [x / m for x in v]
V = [norm(v) for v in vecs[:n]]
dim = len(V[0])
story = [chunks[i]['story'] for i in range(n)]
def dot(a, b):
s = 0.0
for x, y in zip(a, b): s += x * y
return s
def kmeans(K, seed, iters=15):
rnd = random.Random(seed)
cent = [V[i] for i in rnd.sample(range(n), K)]
assign = [-1] * n
for _ in range(iters):
moved = 0
for i, v in enumerate(V):
best, bs = 0, -2.0
for k, c in enumerate(cent):
s = dot(v, c)
if s > bs: bs, best = s, k
if assign[i] != best: assign[i] = best; moved += 1
if moved == 0: break
groups = {}
for i, k in enumerate(assign): groups.setdefault(k, []).append(i)
for k, mem in groups.items():
cent[k] = norm([sum(V[i][d] for i in mem) / len(mem) for d in range(dim)])
groups = {}
for i, k in enumerate(assign): groups.setdefault(k, []).append(i)
return list(groups.values())
def describe(cluster):
counts = {}
for i in cluster: counts[story[i]] = counts.get(story[i], 0) + 1
cen = norm([sum(V[i][d] for i in cluster) / len(cluster) for d in range(dim)])
coh = sum(dot(V[i], cen) for i in cluster) / len(cluster)
return len(cluster), len(counts), max(counts.values()) / len(cluster), coh
# "Tight" is relative to the file's own median cluster coherence. An absolute
# cutoff (the 2026-09-10 probe used 0.83) does not transfer between embedding
# files: one-sentence summaries and 300-word scenes sit at different cosines.
# Without it, loose clusters held together by prose register count as recurring.
print(f'{path.name}: {n} chunks from {len(set(story))} stories, seeds {SEEDS}\n')
print(f'{"k":>4} {"sized>=%d" % MIN_SIZE:>9} {"median dom":>11} {"recurring":>10} {"coverage":>9} {"tight rec":>10} {"tight cov":>10}')
print('-' * 70)
report = {'file': path.name, 'chunks': n, 'rows': []}
for K in KS:
if K * 2 > n: continue
per_seed = []
for seed in SEEDS:
rows = [describe(c) for c in kmeans(K, seed)]
sized = [r for r in rows if r[0] >= MIN_SIZE]
rec = [r for r in sized if r[1] >= MIN_STORIES and r[2] <= MAX_DOMINANT]
cut = statistics.median(r[3] for r in sized) if sized else 1.0
tight = [r for r in rec if r[3] >= cut]
per_seed.append((len(sized),
statistics.median(r[2] for r in sized) if sized else 1.0,
len(rec), sum(r[0] for r in rec) / n,
len(tight), sum(r[0] for r in tight) / n))
med = [statistics.median(s[j] for s in per_seed) for j in range(6)]
print(f'{K:>4} {med[0]:>9.0f} {med[1]:>11.2f} {med[2]:>10.0f} {med[3]:>8.0%} {med[4]:>10.0f} {med[5]:>9.0%}')
report['rows'].append({'k': K, 'sized': med[0], 'median_dominant': med[1],
'recurring': med[2], 'coverage': med[3],
'tight_recurring': med[4], 'tight_coverage': med[5]})
out = path.with_name(path.stem + '.measure.json')
out.write_text(json.dumps(report, indent=1))
print(f'\nrecurring = size>={MIN_SIZE}, stories>={MIN_STORIES}, dominant share<={MAX_DOMINANT:.0%}; '
f'tight = coherence at or above this file\'s median cluster. Judge at k>=60. Wrote {out.name}')
+38
View File
@@ -0,0 +1,38 @@
"""Near-duplicate situations among accepted clusters (SELECTION.md protocol step 6).
python3 merge_dupes.py dev # list every accepted pair's centroid cosine, with label names
python3 merge_dupes.py test 0.93 # merge pairs at or above a threshold chosen on dev
Centroids are the mean unit summary vector of each cluster. On dev the threshold is chosen by
reading: the lowest cosine that still separates pairs whose names say "same situation" from pairs
that do not. On test it is applied as given, and each merge is reported with both names so it can
be checked.
"""
import json, pathlib, sys
set_name = sys.argv[1]
threshold = float(sys.argv[2]) if len(sys.argv) > 2 else None
sys.argv = ['measure.py', 'summary_embeddings.json']
exec(pathlib.Path('measure.py').read_text(encoding='utf-8').split("print(f'{path.name}")[0])
sys.argv = ['select_rule.py']
exec(pathlib.Path('select_rule.py').read_text(encoding='utf-8').split("mode = sys.argv")[0])
candidates = json.loads(pathlib.Path(f'candidates-{set_name}.json').read_text())
frozen = json.loads(pathlib.Path('rule-frozen.json').read_text())
terms = [tuple(t) for t in frozen['terms']]
rows = {r['id']: r for r in load(set_name)}
accepted = [cid for cid, r in rows.items() if all(passes(r, t) for t in terms)]
cent = {cid: norm([sum(V[i][d] for i in candidates[cid]) / len(candidates[cid]) for d in range(dim)])
for cid in accepted}
pairs = sorted(((dot(cent[a], cent[b]), a, b) for x, a in enumerate(accepted) for b in accepted[x + 1:]),
reverse=True)
print(f'{set_name}: {len(accepted)} accepted clusters, {len(pairs)} pairs\n')
for cos, a, b in pairs[:25]:
mark = ' MERGE' if threshold is not None and cos >= threshold else ''
print(f"{cos:.4f} {a}({rows[a]['label']}) {rows[a]['name'] or '-'}\n {b}({rows[b]['label']}) {rows[b]['name'] or '-'}{mark}")
if threshold is not None:
merged = [(a, b) for cos, a, b in pairs if cos >= threshold]
print(f'\nthreshold {threshold}: {len(merged)} merges')
+49
View File
@@ -0,0 +1,49 @@
"""The one place that talks to an inference host.
The host is shared with other development work, so this is deliberately
gentle: one request at a time, a fixed pause between requests, and a short
keep_alive so the model is not left occupying the GPU after a run.
Configuration comes from the environment, never from a file in this tree:
STP_OLLAMA base URL of an Ollama server, e.g. http://inference.lan:11434
STP_PAUSE seconds between requests (default 0.5)
"""
import json, os, sys, time, urllib.request
BASE = os.environ.get('STP_OLLAMA', '').rstrip('/')
PAUSE = float(os.environ.get('STP_PAUSE', '0.5'))
KEEP_ALIVE = '2m'
if not BASE:
sys.exit('STP_OLLAMA is not set -- refusing to guess an inference host')
def post(route, body, timeout=300):
req = urllib.request.Request(BASE + route, data=json.dumps(body).encode(),
headers={'Content-Type': 'application/json'})
for attempt in range(4):
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
result = json.load(r)
time.sleep(PAUSE)
return result
except Exception as e:
if attempt == 3: raise
print(f' retry {attempt + 1}: {e}', flush=True)
time.sleep(5 * (attempt + 1))
def embed(model, texts):
return post('/api/embed', {'model': model, 'input': texts, 'keep_alive': KEEP_ALIVE})['embeddings']
def chat(model, system, user, num_ctx=2048, num_predict=80, fmt=None, think=None):
# num_ctx is pinned small on purpose: the 2026-09-10 hang was a 16k-context
# variant prefilling on CPU. A scene is ~450 tokens with the prompt.
body = {'model': model, 'stream': False, 'keep_alive': KEEP_ALIVE,
'messages': [{'role': 'system', 'content': system},
{'role': 'user', 'content': user}],
'options': {'num_ctx': num_ctx, 'num_predict': num_predict, 'temperature': 0}}
if fmt: body['format'] = fmt
# Thinking models (qwen3) otherwise spend the token budget in a separate thinking
# channel and return empty content.
if think is not None: body['think'] = think
d = post('/api/chat', body)
return d['message']['content'].strip(), d.get('eval_duration', 0), d.get('prompt_eval_duration', 0)
@@ -0,0 +1,351 @@
{
"model": "qwen3:14b",
"prompt": "You describe the predicament of the main person in a passage of fiction, in ONE sentence.\n\nA predicament is what that person is up against and what they want out of it: the kind of trouble or opportunity that could happen to anyone, in any line of work, in any town.\n\nRules:\n- Exactly one sentence, at most 20 words.\n- Leave out everything that ties it to this story: no names, no jobs or professions, no places, no businesses, and no objects that only belong to this story.\n- Say what the person is up against, and what they want.\n- Do not describe the writing, the mood, or who is telling the story.\n- Output only the sentence.",
"summaries": [
{
"chunk": 0,
"story": 0,
"summary": "A man struggles to understand the overwhelming complexity of a city's collective voice, seeking clarity in a world filled with individual sounds but no unified message.",
"model_output": "A man struggles to understand the overwhelming complexity of a city's collective voice, seeking clarity in a world filled with individual sounds but no unified message.",
"raw": "A man struggles to understand the overwhelming complexity of a city's collective voice, seeking clarity in a world filled with individual sounds but no unified message.",
"names_removed": []
},
{
"chunk": 1,
"story": 0,
"summary": "The person is trying to understand the city's hidden message, hoping to find a unified voice that reveals its true nature.",
"model_output": "The person is trying to understand the city's hidden message, hoping to find a unified voice that reveals its true nature.",
"raw": "The person is trying to understand the city's hidden message, hoping to find a unified voice that reveals its true nature.",
"names_removed": []
},
{
"chunk": 2,
"story": 0,
"summary": "A man determined to uncover the unique voice of a city faces the challenge of distinguishing its true character from the distractions and distractions of daily life.",
"model_output": "A man determined to uncover the unique voice of a city faces the challenge of distinguishing its true character from the distractions and distractions of daily life.",
"raw": "A man determined to uncover the unique voice of a city faces the challenge of distinguishing its true character from the distractions and distractions of daily life.",
"names_removed": []
},
{
"chunk": 3,
"story": 0,
"summary": "A man tries to uncover the city's hidden voice through a cop, only to find that the cop's personal life and indifference to the city's soul leave him with no real answers.",
"model_output": "A man tries to uncover the city's hidden voice through a cop, only to find that the cop's personal life and indifference to the city's soul leave him with no real answers.",
"raw": "A man tries to uncover the city's hidden voice through a cop, only to find that the cop's personal life and indifference to the city's soul leave him with no real answers.",
"names_removed": []
},
{
"chunk": 4,
"story": 0,
"summary": "A man seeks to capture the essence of a city's soul through a unique blend of its sounds, hoping to create a single, powerful vocalization that represents its spirit.",
"model_output": "A man seeks to capture the essence of a city's soul through a unique blend of its sounds, hoping to create a single, powerful vocalization that represents its spirit.",
"raw": "A man seeks to capture the essence of a city's soul through a unique blend of its sounds, hoping to create a single, powerful vocalization that represents its spirit.",
"names_removed": []
},
{
"chunk": 5,
"story": 0,
"summary": "A man seeks understanding from a city that refuses to speak, desperate to uncover a hidden truth that eludes everyone else.",
"model_output": "A man seeks understanding from a city that refuses to speak, desperate to uncover a hidden truth that eludes everyone else.",
"raw": "A man seeks understanding from a city that refuses to speak, desperate to uncover a hidden truth that eludes everyone else.",
"names_removed": []
},
{
"chunk": 6,
"story": 0,
"summary": "Someone and the narrator sit in silence, caught between the quiet intimacy of the moment and the unspoken weight of the city's presence.",
"model_output": "Aurelia and the narrator sit in silence, caught between the quiet intimacy of the moment and the unspoken weight of the city's presence.",
"raw": "Aurelia and the narrator sit in silence, caught between the quiet intimacy of the moment and the unspoken weight of the city's presence.",
"names_removed": [
"Aurelia"
]
},
{
"chunk": 7,
"story": 1,
"summary": "Someone must navigate the harsh realities of poverty, love, and war all at once in the crowded, unforgiving city.",
"model_output": "John Hopkins must navigate the harsh realities of poverty, love, and war all at once in the crowded, unforgiving city.",
"raw": "John Hopkins must navigate the harsh realities of poverty, love, and war all at once in the crowded, unforgiving city.",
"names_removed": [
"John",
"Hopkins"
]
},
{
"chunk": 8,
"story": 1,
"summary": "Someone must navigate the chaotic unpredictability of city life while clinging to the comfort of her familiar routines.",
"model_output": "Mrs. Hopkins must navigate the chaotic unpredictability of city life while clinging to the comfort of her familiar routines.",
"raw": "Mrs. Hopkins must navigate the chaotic unpredictability of city life while clinging to the comfort of her familiar routines.",
"names_removed": [
"Mrs.",
"Hopkins"
]
},
{
"chunk": 9,
"story": 1,
"summary": "Someone longs for connection but is trapped in a life of monotony and shallow conversation.",
"model_output": "John Hopkins longs for connection but is trapped in a life of monotony and shallow conversation.",
"raw": "John Hopkins longs for connection but is trapped in a life of monotony and shallow conversation.",
"names_removed": [
"John",
"Hopkins"
]
},
{
"chunk": 10,
"story": 1,
"summary": "A man tries to pay for a purchase with a promise of future money, but his dishonesty sparks a violent confrontation with a shopkeeper who sees his actions as proof of humanity's corruption.",
"model_output": "A man tries to pay for a purchase with a promise of future money, but his dishonesty sparks a violent confrontation with a shopkeeper who sees his actions as proof of humanity's corruption.",
"raw": "A man tries to pay for a purchase with a promise of future money, but his dishonesty sparks a violent confrontation with a shopkeeper who sees his actions as proof of humanity's corruption.",
"names_removed": []
},
{
"chunk": 11,
"story": 1,
"summary": "A man flees from police pursuit, seeking refuge in an unknown vehicle that offers him a chance to escape, but must decide whether to trust his mysterious rescuer.",
"model_output": "A man flees from police pursuit, seeking refuge in an unknown vehicle that offers him a chance to escape, but must decide whether to trust his mysterious rescuer.",
"raw": "A man flees from police pursuit, seeking refuge in an unknown vehicle that offers him a chance to escape, but must decide whether to trust his mysterious rescuer.",
"names_removed": []
},
{
"chunk": 12,
"story": 1,
"summary": "A man finds himself unexpectedly drawn into a personal conflict he did not ask for, yet must navigate with courage and discretion.",
"model_output": "A man finds himself unexpectedly drawn into a personal conflict he did not ask for, yet must navigate with courage and discretion.",
"raw": "A man finds himself unexpectedly drawn into a personal conflict he did not ask for, yet must navigate with courage and discretion.",
"names_removed": []
},
{
"chunk": 13,
"story": 1,
"summary": "Someone must confront a man who has wronged a woman he now feels compelled to protect, risking his own safety to do so.",
"model_output": "John Hopkins must confront a man who has wronged a woman he now feels compelled to protect, risking his own safety to do so.",
"raw": "John Hopkins must confront a man who has wronged a woman he now feels compelled to protect, risking his own safety to do so.",
"names_removed": [
"John",
"Hopkins"
]
},
{
"chunk": 14,
"story": 1,
"summary": "A man struggles to reconcile his growing affection for a woman with the rigid expectations of his social circle.",
"model_output": "A man struggles to reconcile his growing affection for a woman with the rigid expectations of his social circle.",
"raw": "A man struggles to reconcile his growing affection for a woman with the rigid expectations of his social circle.",
"names_removed": []
},
{
"chunk": 15,
"story": 2,
"summary": "Someone, trapped in a monotonous job she despises, longs for a life that offers more than just the dull routine of selling gloves.",
"model_output": "Masie, trapped in a monotonous job she despises, longs for a life that offers more than just the dull routine of selling gloves.",
"raw": "Masie, trapped in a monotonous job she despises, longs for a life that offers more than just the dull routine of selling gloves.",
"names_removed": [
"Masie"
]
},
{
"chunk": 16,
"story": 2,
"summary": "A man finds himself trapped in an awkward encounter with a shopgirl whose icy beauty challenges his composure, forcing him to navigate the delicate line between professional decorum and unexpected attraction.",
"model_output": "A man finds himself trapped in an awkward encounter with a shopgirl whose icy beauty challenges his composure, forcing him to navigate the delicate line between professional decorum and unexpected attraction.",
"raw": "A man finds himself trapped in an awkward encounter with a shopgirl whose icy beauty challenges his composure, forcing him to navigate the delicate line between professional decorum and unexpected attraction.",
"names_removed": []
},
{
"chunk": 17,
"story": 2,
"summary": "A man struggling with his pride and social conventions finds himself awkwardly drawn to a shopgirl, determined to win her favor despite his usual confidence.",
"model_output": "A man struggling with his pride and social conventions finds himself awkwardly drawn to a shopgirl, determined to win her favor despite his usual confidence.",
"raw": "A man struggling with his pride and social conventions finds himself awkwardly drawn to a shopgirl, determined to win her favor despite his usual confidence.",
"names_removed": []
},
{
"chunk": 18,
"story": 2,
"summary": "A young woman balances between curiosity and caution as she agrees to meet a man she barely knows, hoping to explore new possibilities without disappointing her family or losing her sense of self.",
"model_output": "A young woman balances between curiosity and caution as she agrees to meet a man she barely knows, hoping to explore new possibilities without disappointing her family or losing her sense of self.",
"raw": "A young woman balances between curiosity and caution as she agrees to meet a man she barely knows, hoping to explore new possibilities without disappointing her family or losing her sense of self.",
"names_removed": []
},
{
"chunk": 19,
"story": 2,
"summary": "A man torn between his growing feelings for a woman he barely knows and his fear of losing control of his life struggles to navigate the uncertain path of love.",
"model_output": "A man torn between his growing feelings for a woman he barely knows and his fear of losing control of his life struggles to navigate the uncertain path of love.",
"raw": "A man torn between his growing feelings for a woman he barely knows and his fear of losing control of his life struggles to navigate the uncertain path of love.",
"names_removed": []
},
{
"chunk": 20,
"story": 2,
"summary": "A man insists a woman marry him, claiming his wealth and status make them equal, but she sees through his words and mocks his desperate, insincere pursuit.",
"model_output": "A man insists a woman marry him, claiming his wealth and status make them equal, but she sees through his words and mocks his desperate, insincere pursuit.",
"raw": "A man insists a woman marry him, claiming his wealth and status make them equal, but she sees through his words and mocks his desperate, insincere pursuit.",
"names_removed": []
},
{
"chunk": 21,
"story": 2,
"summary": "A man tries to convince a hesitant woman to leave her mundane life and embark on an extravagant, uncertain journey with him.",
"model_output": "A man tries to convince a hesitant woman to leave her mundane life and embark on an extravagant, uncertain journey with him.",
"raw": "A man tries to convince a hesitant woman to leave her mundane life and embark on an extravagant, uncertain journey with him.",
"names_removed": []
},
{
"chunk": 22,
"story": 2,
"summary": "A woman hides her growing feelings for a man while pretending to be uninterested, hoping to keep their connection without giving in to the possibility of love.",
"model_output": "A woman hides her growing feelings for a man while pretending to be uninterested, hoping to keep their connection without giving in to the possibility of love.",
"raw": "A woman hides her growing feelings for a man while pretending to be uninterested, hoping to keep their connection without giving in to the possibility of love.",
"names_removed": []
},
{
"chunk": 23,
"story": 3,
"summary": "Big someone must navigate the rigid codes of his underground world while seeking a way to break free and claim a life beyond the shadows.",
"model_output": "Big Jim Dougherty must navigate the rigid codes of his underground world while seeking a way to break free and claim a life beyond the shadows.",
"raw": "Big Jim Dougherty must navigate the rigid codes of his underground world while seeking a way to break free and claim a life beyond the shadows.",
"names_removed": [
"Jim",
"Dougherty"
]
},
{
"chunk": 24,
"story": 3,
"summary": "A man struggles to balance his cultural identity and personal life while navigating the expectations of his community and the quiet routine of his domestic existence.",
"model_output": "A man struggles to balance his cultural identity and personal life while navigating the expectations of his community and the quiet routine of his domestic existence.",
"raw": "A man struggles to balance his cultural identity and personal life while navigating the expectations of his community and the quiet routine of his domestic existence.",
"names_removed": []
},
{
"chunk": 25,
"story": 3,
"summary": "A man accustomed to routine is confronted with an unexpected request for companionship, forcing him to reconsider the balance between habit and connection.",
"model_output": "A man accustomed to routine is confronted with an unexpected request for companionship, forcing him to reconsider the balance between habit and connection.",
"raw": "A man accustomed to routine is confronted with an unexpected request for companionship, forcing him to reconsider the balance between habit and connection.",
"names_removed": []
},
{
"chunk": 26,
"story": 3,
"summary": "A man struggling to reconcile his desire to display his wife publicly with his fear of judgment finds himself torn between pride and secrecy.",
"model_output": "A man struggling to reconcile his desire to display his wife publicly with his fear of judgment finds himself torn between pride and secrecy.",
"raw": "A man struggling to reconcile his desire to display his wife publicly with his fear of judgment finds himself torn between pride and secrecy.",
"names_removed": []
},
{
"chunk": 27,
"story": 3,
"summary": "A man struggles to reconcile his pride with the growing awareness of his wife's hidden talents and the shame of having underestimated her.",
"model_output": "A man struggles to reconcile his pride with the growing awareness of his wife's hidden talents and the shame of having underestimated her.",
"raw": "A man struggles to reconcile his pride with the growing awareness of his wife's hidden talents and the shame of having underestimated her.",
"names_removed": []
},
{
"chunk": 28,
"story": 3,
"summary": "A man struggles to reconcile his quiet life with the sudden, overwhelming attention his wife receives, threatening to expose the hidden depths of their marriage.",
"model_output": "A man struggles to reconcile his quiet life with the sudden, overwhelming attention his wife receives, threatening to expose the hidden depths of their marriage.",
"raw": "A man struggles to reconcile his quiet life with the sudden, overwhelming attention his wife receives, threatening to expose the hidden depths of their marriage.",
"names_removed": []
},
{
"chunk": 29,
"story": 3,
"summary": "A man struggles to maintain his pride and dignity after a public embarrassment, while trying to preserve his relationship and quiet his wife's growing happiness.",
"model_output": "A man struggles to maintain his pride and dignity after a public embarrassment, while trying to preserve his relationship and quiet his wife's growing happiness.",
"raw": "A man struggles to maintain his pride and dignity after a public embarrassment, while trying to preserve his relationship and quiet his wife's growing happiness.",
"names_removed": []
},
{
"chunk": 30,
"story": 3,
"summary": "A woman trapped in a love that bends her will longs for a simple peach, while her powerful but obedient husband struggles to fulfill her wish in a world that resists giving way.",
"model_output": "A woman trapped in a love that bends her will longs for a simple peach, while her powerful but obedient husband struggles to fulfill her wish in a world that resists giving way.",
"raw": "A woman trapped in a love that bends her will longs for a simple peach, while her powerful but obedient husband struggles to fulfill her wish in a world that resists giving way.",
"names_removed": []
},
{
"chunk": 31,
"story": 3,
"summary": "The Kid needs to find a peach for a woman who insists on it, but every vendor says it's too soon and offers only oranges instead.",
"model_output": "The Kid needs to find a peach for a woman who insists on it, but every vendor says it's too soon and offers only oranges instead.",
"raw": "The Kid needs to find a peach for a woman who insists on it, but every vendor says it's too soon and offers only oranges instead.",
"names_removed": []
},
{
"chunk": 32,
"story": 3,
"summary": "The Kid needs to convince the captain to raid a gambling den controlled by an old enemy, but he must do so quickly before the enemy discovers his betrayal.",
"model_output": "The Kid needs to convince the captain to raid a gambling den controlled by an old enemy, but he must do so quickly before the enemy discovers his betrayal.",
"raw": "The Kid needs to convince the captain to raid a gambling den controlled by an old enemy, but he must do so quickly before the enemy discovers his betrayal.",
"names_removed": []
},
{
"chunk": 33,
"story": 3,
"summary": "A man must prove his worth in a dangerous confrontation to claim a prize he's risking everything for.",
"model_output": "A man must prove his worth in a dangerous confrontation to claim a prize he's risking everything for.",
"raw": "A man must prove his worth in a dangerous confrontation to claim a prize he's risking everything for.",
"names_removed": []
},
{
"chunk": 34,
"story": 3,
"summary": "A man desperate to find a peach before it's too late races against time and rivals to secure his prize before it vanishes entirely.",
"model_output": "A man desperate to find a peach before it's too late races against time and rivals to secure his prize before it vanishes entirely.",
"raw": "A man desperate to find a peach before it's too late races against time and rivals to secure his prize before it vanishes entirely.",
"names_removed": []
},
{
"chunk": 35,
"story": 3,
"summary": "He risks danger to fetch a trivial gift for his bride, only to find her preference has changed.",
"model_output": "He risks danger to fetch a trivial gift for his bride, only to find her preference has changed.",
"raw": "He risks danger to fetch a trivial gift for his bride, only to find her preference has changed.",
"names_removed": []
},
{
"chunk": 36,
"story": 4,
"summary": "A man trapped in the monotony of city life longs for the genuine signs of spring, but finds only artificial reports replacing nature's quiet signals.",
"model_output": "A man trapped in the monotony of city life longs for the genuine signs of spring, but finds only artificial reports replacing nature's quiet signals.",
"raw": "A man trapped in the monotony of city life longs for the genuine signs of spring, but finds only artificial reports replacing nature's quiet signals.",
"names_removed": []
},
{
"chunk": 37,
"story": 4,
"summary": "Three men plot to steal a dollar from someone\u2019s wife, who unknowingly possesses it, forcing him to confront his friends' greed and protect her from their violent intentions.",
"model_output": "Three men plot to steal a dollar from Mr. Peters's wife, who unknowingly possesses it, forcing him to confront his friends' greed and protect her from their violent intentions.",
"raw": "Three men plot to steal a dollar from Mr. Peters's wife, who unknowingly possesses it, forcing him to confront his friends' greed and protect her from their violent intentions.",
"names_removed": [
"Mr.",
"Peters's"
]
},
{
"chunk": 38,
"story": 4,
"summary": "Someone must steal a case note from his wife's hidden stash to buy a dollar for beer, but he refuses to harm her despite his desperation.",
"model_output": "Mr. Peters must steal a case note from his wife's hidden stash to buy a dollar for beer, but he refuses to harm her despite his desperation.",
"raw": "Mr. Peters must steal a case note from his wife's hidden stash to buy a dollar for beer, but he refuses to harm her despite his desperation.",
"names_removed": [
"Mr.",
"Peters"
]
},
{
"chunk": 39,
"story": 4,
"summary": "A man trying to manipulate a woman with false promises faces her sharp rejection and must find another way to achieve his goal.",
"model_output": "A man trying to manipulate a woman with false promises faces her sharp rejection and must find another way to achieve his goal.",
"raw": "A man trying to manipulate a woman with false promises faces her sharp rejection and must find another way to achieve his goal.",
"names_removed": []
}
]
}
@@ -0,0 +1,410 @@
{
"model": "qwen2.5:3b-instruct",
"prompt": "You describe the predicament of the main person in a passage of fiction, in ONE sentence.\n\nA predicament is what that person is up against and what they want out of it: the kind of trouble or opportunity that could happen to anyone, in any line of work, in any town.\n\nRules:\n- Exactly one sentence, at most 20 words.\n- Leave out everything that ties it to this story: no names, no jobs or professions, no places, no businesses, and no objects that only belong to this story.\n- Say what the person is up against, and what they want.\n- Do not describe the writing, the mood, or who is telling the story.\n- Output only the sentence.",
"summaries": [
{
"chunk": 0,
"story": 0,
"summary": "Finding the voice of a big city, I sought in vain among school chants.",
"model_output": "Finding the voice of a big city, I sought in vain among school chants.",
"raw": "Finding the voice of a big city, I sought in vain among school chants.",
"names_removed": []
},
{
"chunk": 1,
"story": 0,
"summary": "I want to know the city's voice, but it echoes like someone.",
"model_output": "I want to know the city's voice, but it echoes like Philadelphia.",
"raw": "I want to know the city's voice, but it echoes like Philadelphia.",
"names_removed": [
"Philadelphia"
]
},
{
"chunk": 2,
"story": 0,
"summary": "I must find the unique voice of New someone, proving it doesn't treat me like a cigar.",
"model_output": "I must find the unique voice of New York, proving it doesn't treat me like a cigar.",
"raw": "I must find the unique voice of New York, proving it doesn't treat me like a cigar.",
"names_removed": [
"York"
]
},
{
"chunk": 3,
"story": 0,
"summary": "I want to know the city's cry, but the cop only follows orders.",
"model_output": "I want to know the city's cry, but the cop only follows orders.",
"raw": "I want to know the city's cry, but the cop only follows orders.",
"names_removed": []
},
{
"chunk": 4,
"story": 0,
"summary": "Bill, find the essence of New someone\u2019s chaos and harmony.",
"model_output": "Bill, find the essence of New York's chaos and harmony.",
"raw": "Bill, find the essence of New York's chaos and harmony.",
"names_removed": [
"York's"
]
},
{
"chunk": 5,
"story": 0,
"summary": "The poet seeks a hit with his poem, fearing rejection.",
"model_output": "The poet seeks a hit with his poem, fearing rejection.",
"raw": "The poet seeks a hit with his poem, fearing rejection.",
"names_removed": []
},
{
"chunk": 6,
"story": 0,
"summary": "Someone waits alone, wanting her companion to speak.",
"model_output": "Aurelia waits alone, wanting her companion to speak.",
"raw": "Aurelia waits alone, wanting her companion to speak.",
"names_removed": [
"Aurelia"
]
},
{
"chunk": 7,
"story": 1,
"summary": "Someone finds deepest life joys in a hidden quarter, not wealth.",
"model_output": "John Hopkins finds deepest life joys in a hidden quarter, not wealth.",
"raw": "John Hopkins finds deepest life joys in a hidden quarter, not wealth.",
"names_removed": [
"John",
"Hopkins"
]
},
{
"chunk": 8,
"story": 1,
"summary": "Someone struggles to balance her desires and fears in the fast-paced city.",
"model_output": "Mrs. Hopkins struggles to balance her desires and fears in the fast-paced city.",
"raw": "Mrs. Hopkins struggles to balance her desires and fears in the fast-paced city.",
"names_removed": [
"Mrs.",
"Hopkins"
]
},
{
"chunk": 9,
"story": 1,
"summary": "Someone sought to inject conversation into his tasteless existence by buying a cigar.",
"model_output": "John Hopkins sought to inject conversation into his tasteless existence by buying a cigar.",
"raw": "John Hopkins sought to inject conversation into his tasteless existence by buying a cigar.",
"names_removed": [
"John",
"Hopkins"
]
},
{
"chunk": 10,
"story": 1,
"summary": "Someone buys spinach, finds no cash, challenges tobacconist someone.",
"model_output": "Hopkins buys spinach, finds no cash, challenges tobacconist Freshmayer.",
"raw": "Hopkins buys spinach, finds no cash, challenges tobacconist Freshmayer.",
"names_removed": [
"Hopkins",
"Freshmayer"
]
},
{
"chunk": 11,
"story": 1,
"summary": "Someone sought revenge, but found himself pursued by law enforcers and a mysterious chauffeur in a racing car.",
"model_output": "John Hopkins sought revenge, but found himself pursued by law enforcers and a mysterious chauffeur in a racing car.",
"raw": "John Hopkins sought revenge, but found himself pursued by law enforcers and a mysterious chauffeur in a racing car.",
"names_removed": [
"John",
"Hopkins"
]
},
{
"chunk": 12,
"story": 1,
"summary": "Someone wants Milady's assistance against unseen attackers, while Milady seeks a brave companion.",
"model_output": "Hopkins wants Milady's assistance against unseen attackers, while Milady seeks a brave companion.",
"raw": "Hopkins wants Milady's assistance against unseen attackers, while Milady seeks a brave companion.",
"names_removed": [
"Hopkins"
]
},
{
"chunk": 13,
"story": 1,
"summary": "Someone seeks a romantic encounter, only to be kidnapped by a mysterious lady.",
"model_output": "John Hopkins seeks a romantic encounter, only to be kidnapped by a mysterious lady.",
"raw": "John Hopkins seeks a romantic encounter, only to be kidnapped by a mysterious lady.",
"names_removed": [
"John",
"Hopkins"
]
},
{
"chunk": 14,
"story": 1,
"summary": "Bess wants her St. Bernard, but someone wants her to stop reading dangerous novels.",
"model_output": "Bess wants her St. Bernard, but John wants her to stop reading dangerous novels.",
"raw": "Bess wants her St. Bernard, but John wants her to stop reading dangerous novels.",
"names_removed": [
"John"
]
},
{
"chunk": 15,
"story": 2,
"summary": "Someone, a young saleslady, seeks to navigate the complexities of human nature while hiding her true identity.",
"model_output": "Masie, a young saleslady, seeks to navigate the complexities of human nature while hiding her true identity.",
"raw": "Masie, a young saleslady, seeks to navigate the complexities of human nature while hiding her true identity.",
"names_removed": [
"Masie"
]
},
{
"chunk": 16,
"story": 2,
"summary": "The shopgirl someone confronts the floorwalker someone with her flirtatious gaze, testing his resolve.",
"model_output": "The shopgirl Masie confronts the floorwalker Irving Carter with her flirtatious gaze, testing his resolve.",
"raw": "The shopgirl Masie confronts the floorwalker Irving Carter with her flirtatious gaze, testing his resolve.",
"names_removed": [
"Masie",
"Irving",
"Carter"
]
},
{
"chunk": 17,
"story": 2,
"summary": "Someone desires to charm a shopgirl despite feeling out of place.",
"model_output": "Irving Carter desires to charm a shopgirl despite feeling out of place.",
"raw": "Irving Carter desires to charm a shopgirl despite feeling out of place.",
"names_removed": [
"Irving",
"Carter"
]
},
{
"chunk": 18,
"story": 2,
"summary": "Willie nervously asked for her company, hoping she'd agree to a date.",
"model_output": "Willie nervously asked for her company, hoping she'd agree to a date.",
"raw": "Willie nervously asked for her company, hoping she'd agree to a date.",
"names_removed": []
},
{
"chunk": 19,
"story": 2,
"summary": "Someone seeks love but fears rejection, while someone uses him to prove her worth.",
"model_output": "Carter seeks love but fears rejection, while Masie uses him to prove her worth.",
"raw": "Carter seeks love but fears rejection, while Masie uses him to prove her worth.",
"names_removed": [
"Carter",
"Masie"
]
},
{
"chunk": 20,
"story": 2,
"summary": "Someone faces someone\u2019s marriage proposal, wanting to see through his sincerity.",
"model_output": "Masie faces Carter's marriage proposal, wanting to see through his sincerity.",
"raw": "Masie faces Carter's marriage proposal, wanting to see through his sincerity.",
"names_removed": [
"Masie",
"Carter's"
]
},
{
"chunk": 21,
"story": 2,
"summary": "Someone looked up at someone, her eyes alight with a flicker of possibility.",
"model_output": "Masie looked up at Carter, her eyes alight with a flicker of possibility.",
"raw": "Masie looked up at Carter, her eyes alight with a flicker of possibility.",
"names_removed": [
"Masie",
"Carter"
]
},
{
"chunk": 22,
"story": 2,
"summary": "She wants to end their secret affair, but he feels a hidden joy.",
"model_output": "She wants to end their secret affair, but he feels a hidden joy.",
"raw": "She wants to end their secret affair, but he feels a hidden joy.",
"names_removed": []
},
{
"chunk": 23,
"story": 3,
"summary": "Big someone sought acceptance in someone\u2019s titled nobility of sportdom.",
"model_output": "Big Jim Dougherty sought acceptance in Manhattan's titled nobility of sportdom.",
"raw": "Big Jim Dougherty sought acceptance in Manhattan's titled nobility of sportdom.",
"names_removed": [
"Jim",
"Dougherty",
"Manhattan's"
]
},
{
"chunk": 24,
"story": 3,
"summary": "He seeks a private life away from his public Oriental sport, yet feels trapped in public life.",
"model_output": "He seeks a private life away from his public Oriental sport, yet feels trapped in public life.",
"raw": "He seeks a private life away from his public Oriental sport, yet feels trapped in public life.",
"names_removed": []
},
{
"chunk": 25,
"story": 3,
"summary": "Big someone\" expected her to ask for a dinner date, not insist on one.",
"model_output": "\"Big Jim\" expected her to ask for a dinner date, not insist on one.",
"raw": "Big Jim\" expected her to ask for a dinner date, not insist on one.",
"names_removed": [
"Jim"
]
},
{
"chunk": 26,
"story": 3,
"summary": "Big someone wanted to hide his wife's presence, but a mysterious bird-like being at her side made him change plans.",
"model_output": "Big Jim Dougherty wanted to hide his wife's presence, but a mysterious bird-like being at her side made him change plans.",
"raw": "Big Jim Dougherty wanted to hide his wife's presence, but a mysterious bird-like being at her side made him change plans.",
"names_removed": [
"Jim",
"Dougherty"
]
},
{
"chunk": 27,
"story": 3,
"summary": "Big someone seeks to impress his wife at a fancy restaurant, hoping her beauty and charm will win him praise.",
"model_output": "Big Jim seeks to impress his wife at a fancy restaurant, hoping her beauty and charm will win him praise.",
"raw": "Big Jim seeks to impress his wife at a fancy restaurant, hoping her beauty and charm will win him praise.",
"names_removed": [
"Jim"
]
},
{
"chunk": 28,
"story": 3,
"summary": "The honorable someone seeks to expose a secret and win someone\u2019s heart, while \"Big someone\" struggles to keep up.",
"model_output": "The honorable Patrick Corrigan seeks to expose a secret and win Mrs. Dougherty's heart, while \"Big Jim\" struggles to keep up.",
"raw": "The honorable Patrick Corrigan seeks to expose a secret and win Mrs. Dougherty's heart, while \"Big Jim\" struggles to keep up.",
"names_removed": [
"Patrick",
"Corrigan",
"Mrs.",
"Dougherty's",
"Jim"
]
},
{
"chunk": 29,
"story": 3,
"summary": "Someone seeks to impress someone with a bold gesture, but is thwarted by her quicker action.",
"model_output": "Patrick seeks to impress Mrs. Dougherty with a bold gesture, but is thwarted by her quicker action.",
"raw": "Patrick seeks to impress Mrs. Dougherty with a bold gesture, but is thwarted by her quicker action.",
"names_removed": [
"Patrick",
"Mrs.",
"Dougherty"
]
},
{
"chunk": 30,
"story": 3,
"summary": "The bride desires her husband's affection, yet fears losing him to distant admirers.",
"model_output": "The bride desires her husband's affection, yet fears losing him to distant admirers.",
"raw": "The bride desires her husband's affection, yet fears losing him to distant admirers.",
"names_removed": []
},
{
"chunk": 31,
"story": 3,
"summary": "The Kid seeks a peach but finds oranges instead, rushing to a fruiterer just as the lights go out.",
"model_output": "The Kid seeks a peach but finds oranges instead, rushing to a fruiterer just as the lights go out.",
"raw": "The Kid seeks a peach but finds oranges instead, rushing to a fruiterer just as the lights go out.",
"names_removed": []
},
{
"chunk": 32,
"story": 3,
"summary": "Kid seeks to infiltrate someone\u2019s gambling den but finds it closed, needing help from the captain.",
"model_output": "Kid seeks to infiltrate Denver Dick's gambling den but finds it closed, needing help from the captain.",
"raw": "Kid seeks to infiltrate Denver Dick's gambling den but finds it closed, needing help from the captain.",
"names_removed": [
"Denver",
"Dick's"
]
},
{
"chunk": 33,
"story": 3,
"summary": "The captain seeks to lead a raid, but doubts success as the Kid's plan unfolds.",
"model_output": "The captain seeks to lead a raid, but doubts success as the Kid's plan unfolds.",
"raw": "The captain seeks to lead a raid, but doubts success as the Kid's plan unfolds.",
"names_removed": []
},
{
"chunk": 34,
"story": 3,
"summary": "The Kid seeks a last peach at a lavish dinner, fearing theft, while authorities arrive.",
"model_output": "The Kid seeks a last peach at a lavish dinner, fearing theft, while authorities arrive.",
"raw": "The Kid seeks a last peach at a lavish dinner, fearing theft, while authorities arrive.",
"names_removed": []
},
{
"chunk": 35,
"story": 3,
"summary": "The Kid seeks to please his bride by delivering a requested peach, despite its trivial nature.",
"model_output": "The Kid seeks to please his bride by delivering a requested peach, despite its trivial nature.",
"raw": "The Kid seeks to please his bride by delivering a requested peach, despite its trivial nature.",
"names_removed": []
},
{
"chunk": 36,
"story": 4,
"summary": "Stuck in stone walls, city man craves nature's first signs to confirm spring's arrival.",
"model_output": "Stuck in stone walls, city man craves nature's first signs to confirm spring's arrival.",
"raw": "Stuck in stone walls, city man craves nature's first signs to confirm spring's arrival.",
"names_removed": []
},
{
"chunk": 37,
"story": 4,
"summary": "Someone wants the dollar his wife has, but fears losing face among friends.",
"model_output": "Mr. Peters wants the dollar his wife has, but fears losing face among friends.",
"raw": "Mr. Peters wants the dollar his wife has, but fears losing face among friends.",
"names_removed": [
"Mr.",
"Peters"
]
},
{
"chunk": 38,
"story": 4,
"summary": "Someone wants the dollar, but someone plans to get the case note instead.",
"model_output": "Maguire wants the dollar, but Mr. Peters plans to get the case note instead.",
"raw": "Maguire wants the dollar, but Mr. Peters plans to get the case note instead.",
"names_removed": [
"Maguire",
"Mr.",
"Peters"
]
},
{
"chunk": 39,
"story": 4,
"summary": "Someone schemes to charm someone away from a tea job, while she resists with a tough stance.",
"model_output": "Mr. Peters schemes to charm Mrs. Peters away from a tea job, while she resists with a tough stance.",
"raw": "Mr. Peters schemes to charm Mrs. Peters away from a tea job, while she resists with a tough stance.",
"names_removed": [
"Mr.",
"Peters",
"Mrs.",
"Peters"
]
}
]
}
@@ -0,0 +1,862 @@
<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Predicament pilot</title>
<style>
:root{--bg:#f7f6f2;--card:#fff;--ink:#222;--dim:#666;--line:#ddd;--mark:#ffe08a;--pick:#e3f1e6}
@media (prefers-color-scheme:dark){:root{--bg:#1b1b1d;--card:#252528;--ink:#e6e6e6;--dim:#999;--line:#3a3a3e;--mark:#6b5415;--pick:#23402b}}
body{background:var(--bg);color:var(--ink);font:15px/1.5 system-ui,sans-serif;margin:0;padding:24px 16px}
main{max-width:980px;margin:auto} h1{margin:0 0 4px} .dim{color:var(--dim)}
table{border-collapse:collapse;margin:12px 0;font-size:14px} td,th{border-bottom:1px solid var(--line);padding:4px 10px;text-align:left}
.wrap{overflow-x:auto}
#tally{position:sticky;top:0;background:var(--bg);padding:8px 0;border-bottom:1px solid var(--line);z-index:1}
section{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:12px 14px;margin:14px 0}
header .n{color:var(--dim);margin-right:4px} summary{cursor:pointer;color:var(--dim);margin:6px 0}
.scene{white-space:pre-wrap;font-size:13px;color:var(--dim)}
.pair{display:grid;grid-template-columns:1fr 1fr;gap:10px} @media (max-width:640px){.pair{grid-template-columns:1fr}}
.pred{border:1px solid var(--line);border-radius:6px;padding:8px 10px} .pred p{margin:4px 0 8px}
.pred:has(input:checked){background:var(--pick)} .who{font-size:12px;color:var(--dim)}
mark{background:var(--mark);color:inherit;border-radius:2px} .both{display:block;margin-top:8px;font-size:13px;color:var(--dim)}
</style>
<main>
<h1>Predicament pilot</h1>
<p class="dim">The first 40 scenes, each re-described by two models as the main person's predicament. Names are
already removed as the full run would. <mark>Highlighted</mark> words are jobs, relationships and places, the
words the old groups formed around. Pick the better sentence in each scene, or "neither". The column order
is fixed: qwen2.5:3b-instruct on the left.</p>
<p class="dim">What to look for: does the sentence say what the person is up against and what they want? Could
it happen to anyone, anywhere? Would two scenes with the same predicament end up with similar sentences?</p>
<div class="wrap"><table><tr><th>Model</th><th>Has a highlighted word</th><th>Says "city"</th><th>Words, mean</th><th>Most common openings</th></tr><tr><td>qwen2.5:3b-instruct</td><td>15 of 40</td><td>5</td><td>14</td><td>“The Kid seeks…” ×3; “I want to…” ×2; “Someone wants the…” ×2</td></tr><tr><td>qwen3:14b</td><td>13 of 40</td><td>10</td><td>25</td><td>“A man struggles…” ×6; “A man tries…” ×3; “A man seeks…” ×2</td></tr></table></div>
<div id="tally"></div>
<section data-i="0">
<header><span class="n">1</span> <b>THE VOICE OF THE CITY</b></header>
<details><summary>Scene summary: Someone, a schoolteacher, seeks the wisdom of her former students to understand the complex vocal messages of a bustling city.</summary>
<p class="scene">Twenty-five years ago the school children used to chant their
lessons. The manner of their delivery was a singsong recitative
between the utterance of an Episcopal minister and the drone of a
tired sawmill. I mean no disrespect. We must have lumber and sawdust. I remember one beautiful and instructive little lyric that emanated
from the physiology class. The most striking line of it was this: &quot;The shin-bone is the long-est bone in the hu-man bod-y.&quot; What an inestimable boon it would have been if all the corporeal
and spiritual facts pertaining to man had thus been tunefully and
logically inculcated in our youthful minds! But what we gained in
anatomy, music and philosophy was meagre. The other day I became confused. I needed a ray of light. I turned
back to those school days for aid. But in all the nasal harmonies
we whined forth from those hard benches I could not recall one that
treated of the voice of agglomerated mankind. In other words, of the composite vocal message of massed humanity. In other words, of the Voice of a Big City. Now, the individual voice is not lacking. We can understand the
song of the poet, the ripple of the brook, the meaning of the man
who wants $5 until …</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Finding the voice of a big city, I sought in vain among school chants.</p><label><input type="radio" name="p0" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man struggles to understand the overwhelming complexity of a city&#x27;s collective voice, seeking clarity in a world filled with individual sounds but no unified message.</p><label><input type="radio" name="p0" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p0" value="n"> neither is a predicament</label>
</section><section data-i="1">
<header><span class="n">2</span> <b>THE VOICE OF THE CITY</b></header>
<details><summary>Scene summary: I want to know what the city says, but someone thinks it repeats what someone says.</summary>
<p class="scene">&quot;Tell me,&quot; I said, stammeringly, for I have no voice of my own, &quot;what
does this big--er--enormous--er--whopping city say? It must have a
voice of some kind. Does it ever speak to you? How do you interpret
its meaning? It is a tremendous mass, but it must have a key.&quot; &quot;Like a Saratoga trunk?&quot; asked Aurelia. &quot;No,&quot; said I. &quot;Please do not refer to the lid. I have a fancy that
every city has a voice. Each one has something to say to the one who
can hear it. What does the big one say to you?&quot; &quot;All cities,&quot; said Aurelia, judicially, &quot;say the same thing. When
they get through saying it there is an echo from Philadelphia. So,
they are unanimous.&quot; &quot;Here are 4,000,000 people,&quot; said I, scholastically, &quot;compressed upon
an island, which is mostly lamb surrounded by Wall Street water. The
conjunction of so many units into so small a space must result in an
identity--or, or rather a homogeneity that finds its oral expression
through a common channel. It is, as you might say, a consensus of
translation, concentrating in a crystallized, general idea which
reveals itself in what may be termed the Voice of the City. Can you
tell me what it is?&quot; Aurelia smiled wonderfully. She sat on the high stoop. A …</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>I want to know the city&#x27;s voice, but it echoes like someone.</p><label><input type="radio" name="p1" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>The person is trying to understand the city&#x27;s hidden message, hoping to find a unified voice that reveals its true nature.</p><label><input type="radio" name="p1" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p1" value="n"> neither is a predicament</label>
</section><section data-i="2">
<header><span class="n">3</span> <b>THE VOICE OF THE CITY</b></header>
<details><summary>Scene summary: I must find the Voice of New someone, but first I seek out someone, the best <mark>bartender</mark>, to learn the city&#x27;s unique ways.</summary>
<p class="scene">&quot;I must go and find out,&quot; I said, &quot;what is the Voice of this City.
Other cities have voices. It is an assignment. I must have it. New
York,&quot; I continued, in a rising tone, &quot;had better not hand me a cigar
and say: &#x27;Old man, I can&#x27;t talk for publication.&#x27; No other city acts
in that way. Chicago says, unhesitatingly, &#x27;I will;&#x27; I Philadelphia
says, &#x27;I should;&#x27; New Orleans says, &#x27;I used to;&#x27; Louisville says,
&#x27;Don&#x27;t care if I do;&#x27; St. Louis says, &#x27;Excuse me;&#x27; Pittsburg says,
&#x27;Smoke up.&#x27; Now, New York--&quot; Aurelia smiled. &quot;Very well,&quot; said I, &quot;I must go elsewhere and find out.&quot; I went into a palace, tile-floored, cherub-ceilinged and square with
the cop. I put my foot on the brass rail and said to Billy Magnus,
the best bartender in the diocese: &quot;Billy, you&#x27;ve lived in New York a long time--what kind of a
song-and-dance does this old town give you? What I mean is, doesn&#x27;t
the gab of it seem to kind of bunch up and slide over the bar to you
in a sort of amalgamated tip that hits off the burg in a kind of an
epigram with a dash of bitters and a slice of--&quot; &quot;Excuse me a minute,&quot; said Billy, &quot;somebody&#x27;s punching the button at
the side door.&quot; He went away; came back with an empty tin bucket; aga…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>I must find the unique voice of New someone, proving it doesn&#x27;t treat me like a cigar.</p><label><input type="radio" name="p2" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man determined to uncover the unique voice of a city faces the challenge of distinguishing its true character from the distractions and distractions of daily life.</p><label><input type="radio" name="p2" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p2" value="n"> neither is a predicament</label>
</section><section data-i="3">
<header><span class="n">4</span> <b>THE VOICE OF THE CITY</b></header>
<details><summary>Scene summary: The <mark>cop</mark>, tasked with maintaining order, confronts a young man who questions the <mark>cop</mark>&#x27;s role and civic duty, leading to a conversation about the city&#x27;s unrest and the <mark>cop</mark>&#x27;s mundane nightly duties.</summary>
<p class="scene">I walked up to Broadway. I saw a cop on the corner. The cops take
kids up, women across, and men in. I went up to him. &quot;If I&#x27;m not exceeding the spiel limit,&quot; I said, &quot;let me ask you. You
see New York during its vocative hours. It is the function of you and
your brother cops to preserve the acoustics of the city. There must
be a civic voice that is intelligible to you. At night during your
lonely rounds you must have heard it. What is the epitome of its
turmoil and shouting? What does the city say to you?&quot; &quot;Friend,&quot; said the policeman, spinning his club, &quot;it don&#x27;t say
nothing. I get my orders from the man higher up. Say, I guess you&#x27;re
all right. Stand here for a few minutes and keep an eye open for the
roundsman.&quot; The cop melted into the darkness of the side street. In ten minutes
he had returned. &quot;Married last Tuesday,&quot; he said, half gruffly. &quot;You know how they
are. She comes to that corner at nine every night for a--comes to say
&#x27;hello!&#x27; I generally manage to be there. Say, what was it you asked
me a bit ago--what&#x27;s doing in the city? Oh, there&#x27;s a roof-garden or
two just opened, twelve blocks up.&quot; I crossed a crow&#x27;s-foot of street-car tracks, and skirted the edge
of an umbrageo…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>I want to know the city&#x27;s cry, but the <mark>cop</mark> only follows orders.</p><label><input type="radio" name="p3" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man tries to uncover the city&#x27;s hidden voice through a <mark>cop</mark>, only to find that the <mark>cop</mark>&#x27;s personal life and indifference to the city&#x27;s soul leave him with no real answers.</p><label><input type="radio" name="p3" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p3" value="n"> neither is a predicament</label>
</section><section data-i="4">
<header><span class="n">5</span> <b>THE VOICE OF THE CITY</b></header>
<details><summary>Scene summary: Bill, in the role of someone, seeks a poetic description of New someone’s soul from someone, someone, someone, May, and someone, hinting at a symposium to capture the city&#x27;s multifaceted voice.</summary>
<p class="scene">&quot;Bill,&quot; said I (in the magazine he is Cleon), &quot;give me a lift. I am
on an assignment to find out the Voice of the city. You see, it&#x27;s a
special order. Ordinarily a symposium comprising the views of Henry
Clews, John L. Sullivan, Edwin Markham, May Irwin and Charles Schwab
would be about all. But this is a different matter. We want a broad,
poetic, mystic vocalization of the city&#x27;s soul and meaning. You are
the very chap to give me a hint. Some years ago a man got at the
Niagara Falls and gave us its pitch. The note was about two feet
below the lowest G on the piano. Now, you can&#x27;t put New York into a
note unless it&#x27;s better indorsed than that. But give me an idea of
what it would say if it should speak. It is bound to be a mighty and
far-reaching utterance. To arrive at it we must take the tremendous
crash of the chords of the day&#x27;s traffic, the laughter and music
of the night, the solemn tones of Dr. Parkhurst, the rag-time, the
weeping, the stealthy hum of cab-wheels, the shout of the press
agent, the tinkle of fountains on the roof gardens, the hullabaloo
of the strawberry vender and the covers of _Everybody&#x27;s Magazine_,
the whispers of the lovers in the parks--all these sounds …</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Bill, find the essence of New someone’s chaos and harmony.</p><label><input type="radio" name="p4" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man seeks to capture the essence of a city&#x27;s soul through a unique blend of its sounds, hoping to create a single, powerful vocalization that represents its spirit.</p><label><input type="radio" name="p4" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p4" value="n"> neither is a predicament</label>
</section><section data-i="5">
<header><span class="n">6</span> <b>THE VOICE OF THE CITY</b></header>
<details><summary>Scene summary: The <mark>poet</mark> seeks advice from someone about his failed ties and unfulfilled prophecy, while a newsboy offers practical advice about buying a birthday present for someone.</summary>
<p class="scene">&quot;Do you remember,&quot; asked the poet, with a chuckle, &quot;that California
girl we met at Stiver&#x27;s studio last week? Well, I&#x27;m on my way to see
her. She repeated that poem of mine, &#x27;The Tribute of Spring,&#x27; word
for word. She&#x27;s the smartest proposition in this town just at
present. Say, how does this confounded tie look? I spoiled four
before I got one to set right.&quot; &quot;And the Voice that I asked you about?&quot; I inquired. &quot;Oh, she doesn&#x27;t sing,&quot; said Cleon. &quot;But you ought to hear her recite
my &#x27;Angel of the Inshore Wind.&#x27;&quot; I passed on. I cornered a newsboy and he flashed at me prophetic pink
papers that outstripped the news by two revolutions of the clock&#x27;s
longest hand. &quot;Son,&quot; I said, while I pretended to chase coins in my penny pocket,
&quot;doesn&#x27;t it sometimes seem to you as if the city ought to be able to
talk? All these ups and downs and funny business and queer things
happening every day--what would it say, do you think, if it could
speak?&quot; &quot;Quit yer kiddin&#x27;,&quot; said the boy. &quot;Wot paper yer want? I got no time
to waste. It&#x27;s Mag&#x27;s birthday, and I want thirty cents to git her a
present.&quot; Here was no interpreter of the city&#x27;s mouthpiece. I bought a paper,
and consigned its undeclared treaties, i…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>The <mark>poet</mark> seeks a hit with his poem, fearing rejection.</p><label><input type="radio" name="p5" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man seeks understanding from a city that refuses to speak, desperate to uncover a hidden truth that eludes everyone else.</p><label><input type="radio" name="p5" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p5" value="n"> neither is a predicament</label>
</section><section data-i="6">
<header><span class="n">7</span> <b>THE VOICE OF THE CITY</b></header>
<details><summary>Scene summary: Someone waits on her porch as night deepens, while her companion remains silent, their hands finally meeting in a moment of unspoken understanding.</summary>
<p class="scene">Aurelia was still on the stoop. The moon was higher and the ivy
shadows were deeper. I sat at her side and we watched a little cloud
tilt at the drifting moon and go asunder quite pale and discomfited. And then, wonder of wonders and delight of delights! our hands
somehow touched, and our fingers closed together and did not part. After half an hour Aurelia said, with that smile of hers: &quot;Do you know, you haven&#x27;t spoken a word since you came back!&quot; &quot;That,&quot; said I, nodding wisely, &quot;is the Voice of the City.&quot; II</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone waits alone, wanting her companion to speak.</p><label><input type="radio" name="p6" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>Someone and the narrator sit in silence, caught between the quiet intimacy of the moment and the unspoken weight of the city&#x27;s presence.</p><label><input type="radio" name="p6" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p6" value="n"> neither is a predicament</label>
</section><section data-i="7">
<header><span class="n">8</span> <b>THE COMPLETE LIFE OF JOHN HOPKINS</b></header>
<details><summary>Scene summary: Someone, a <mark>clerk</mark> at Insurance, seeks to understand life through poverty, love, and war, finding deeper meaning in urban experiences.</summary>
<p class="scene">There is a saying that no man has tasted the full flavour of life
until he has known poverty, love and war. The justness of this
reflection commends it to the lover of condensed philosophy. The
three conditions embrace about all there is in life worth knowing. A
surface thinker might deem that wealth should be added to the list.
Not so. When a poor man finds a long-hidden quarter-dollar that has
slipped through a rip into his vest lining, he sounds the pleasure of
life with a deeper plummet than any millionaire can hope to cast. It seems that the wise executive power that rules life has thought
best to drill man in these three conditions; and none may escape all
three. In rural places the terms do not mean so much. Poverty is less
pinching; love is temperate; war shrinks to contests about boundary
lines and the neighbors&#x27; hens. It is in the cities that our epigram
gains in truth and vigor; and it has remained for one John Hopkins to
crowd the experience into a rather small space of time. The Hopkins flat was like a thousand others. There was a rubber plant
in one window; a flea-bitten terrier sat in the other, wondering when
he was to have his day. John Hopkins was like a thousand …</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone finds deepest life joys in a hidden quarter, not wealth.</p><label><input type="radio" name="p7" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>Someone must navigate the harsh realities of poverty, love, and war all at once in the crowded, unforgiving city.</p><label><input type="radio" name="p7" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p7" value="n"> neither is a predicament</label>
</section><section data-i="8">
<header><span class="n">9</span> <b>THE COMPLETE LIFE OF JOHN HOPKINS</b></header>
<details><summary>Scene summary: Someone navigated the mundane routines and sudden upheavals of city life, from thrift <mark>store</mark> sales to <mark>street</mark> encounters with danger.</summary>
<p class="scene">Mrs. Hopkins was like a thousand others. The auriferous tooth, the
sedentary disposition, the Sunday afternoon wanderlust, the draught
upon the delicatessen store for home-made comforts, the furor for
department store marked-down sales, the feeling of superiority to
the lady in the third-floor front who wore genuine ostrich tips and
had two names over her bell, the mucilaginous hours during which
she remained glued to the window sill, the vigilant avoidance of
the instalment man, the tireless patronage of the acoustics of the
dumb-waiter shaft--all the attributes of the Gotham flat-dweller were
hers. One moment yet of sententiousness and the story moves. In the Big City large and sudden things happen. You round a corner
and thrust the rib of your umbrella into the eye of your old friend
from Kootenai Falls. You stroll out to pluck a Sweet William in
the park--and lo! bandits attack you--you are ambulanced to the
hospital--you marry your nurse; are divorced--get squeezed while
short on U. P. S. and D. O. W. N. S.--stand in the bread line--marry
an heiress, take out your laundry and pay your club dues--seemingly
all in the wink of an eye. You travel the streets, and a finger
beckons …</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone struggles to balance her desires and fears in the fast-paced city.</p><label><input type="radio" name="p8" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>Someone must navigate the chaotic unpredictability of city life while clinging to the comfort of her familiar routines.</p><label><input type="radio" name="p8" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p8" value="n"> neither is a predicament</label>
</section><section data-i="9">
<header><span class="n">10</span> <b>THE COMPLETE LIFE OF JOHN HOPKINS</b></header>
<details><summary>Scene summary: Someone sat in his <mark>flat</mark>, seeking conversation to spice up his life, while someone complained about the smells from across the hall.</summary>
<p class="scene">John Hopkins sat, after a compressed dinner, in his glove-fitting
straight-front flat. He sat upon a hornblende couch and gazed, with
satiated eyes, at Art Brought Home to the People in the shape of &quot;The
Storm&quot; tacked against the wall. Mrs. Hopkins discoursed droningly
of the dinner smells from the flat across the hall. The flea-bitten
terrier gave Hopkins a look of disgust, and showed a man-hating
tooth. Here was neither poverty, love, nor war; but upon such barren stems
may be grafted those essentials of a complete life. John Hopkins sought to inject a few raisins of conversation into
the tasteless dough of existence. &quot;Putting a new elevator in at the
office,&quot; he said, discarding the nominative noun, &quot;and the boss has
turned out his whiskers.&quot; &quot;You don&#x27;t mean it!&quot; commented Mrs. Hopkins. &quot;Mr. Whipples,&quot; continued John, &quot;wore his new spring suit down
to-day. I liked it fine It&#x27;s a gray with--&quot; He stopped, suddenly
stricken by a need that made itself known to him. &quot;I believe I&#x27;ll
walk down to the corner and get a five-cent cigar,&quot; he concluded. John Hopkins took his hat and picked his way down the musty halls and
stairs of the flat-house. The evening air was mild, and the streets s…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone sought to inject conversation into his tasteless existence by buying a cigar.</p><label><input type="radio" name="p9" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>Someone longs for connection but is trapped in a life of monotony and shallow conversation.</p><label><input type="radio" name="p9" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p9" value="n"> neither is a predicament</label>
</section><section data-i="10">
<header><span class="n">11</span> <b>THE COMPLETE LIFE OF JOHN HOPKINS</b></header>
<details><summary>Scene summary: Someone, seeking groceries, confronts someone over payment, escalating into a physical altercation in the <mark>store</mark>.</summary>
<p class="scene">Hopkins, unknown in the store, entered and called genially for his
&quot;bunch of spinach, car-fare grade.&quot; This imputation deepened the
pessimism of Freshmayer; but he set out a brand that came perilously
near to filling the order. Hopkins bit off the roots of his purchase,
and lighted up at the swinging gas jet. Feeling in his pockets to
make payment, he found not a penny there. &quot;Say, my friend,&quot; he explained, frankly, &quot;I&#x27;ve come out without any
change. Hand you that nickel first time I pass.&quot; Joy surged in Freshmayer&#x27;s heart. Here was corroboration of his
belief that the world was rotten and man a peripatetic evil. Without
a word he rounded the end of his counter and made earnest onslaught
upon his customer. Hopkins was no man to serve as a punching-bag for
a pessimistic tobacconist. He quickly bestowed upon Freshmayer a
colorado-maduro eye in return for the ardent kick that he received
from that dealer in goods for cash only. The impetus of the enemy&#x27;s attack forced the Hopkins line back to
the sidewalk. There the conflict raged; the pacific wooden Indian,
with his carven smile, was overturned, and those of the street who
delighted in carnage pressed round to view the zealous joust.</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone buys spinach, finds no cash, challenges tobacconist someone.</p><label><input type="radio" name="p10" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man tries to pay for a purchase with a promise of future money, but his dishonesty sparks a violent confrontation with a <mark>shopkeeper</mark> who sees his actions as proof of humanity&#x27;s corruption.</p><label><input type="radio" name="p10" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p10" value="n"> neither is a predicament</label>
</section><section data-i="11">
<header><span class="n">12</span> <b>THE COMPLETE LIFE OF JOHN HOPKINS</b></header>
<details><summary>Scene summary: Someone, a rebus worker, eludes a <mark>policeman</mark> by running and jumping into a speeding car driven by a masked <mark>chauffeur</mark>, unaware of his passenger&#x27;s indifference.</summary>
<p class="scene">But then came the inevitable cop and imminent inconvenience for both
the attacker and attacked. John Hopkins was a peaceful citizen, who
worked at rebuses of nights in a flat, but he was not without the
fundamental spirit of resistance that comes with the battle-rage.
He knocked the policeman into a grocer&#x27;s sidewalk display of goods
and gave Freshmayer a punch that caused him temporarily to regret
that he had not made it a rule to extend a five-cent line of credit
to certain customers. Then Hopkins took spiritedly to his heels down
the sidewalk, closely followed by the cigar-dealer and the policeman,
whose uniform testified to the reason in the grocer&#x27;s sign that read:
&quot;Eggs cheaper than anywhere else in the city.&quot; As Hopkins ran he became aware of a big, low, red, racing automobile
that kept abreast of him in the street. This auto steered in to the
side of the sidewalk, and the man guiding it motioned to Hopkins
to jump into it. He did so without slackening his speed, and fell
into the turkey-red upholstered seat beside the chauffeur. The big
machine, with a diminuendo cough, flew away like an albatross down
the avenue into which the street emptied. The driver of the auto sped hi…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone sought revenge, but found himself pursued by law enforcers and a mysterious <mark>chauffeur</mark> in a racing car.</p><label><input type="radio" name="p11" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man flees from police pursuit, seeking refuge in an unknown vehicle that offers him a chance to escape, but must decide whether to trust his mysterious rescuer.</p><label><input type="radio" name="p11" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p11" value="n"> neither is a predicament</label>
</section><section data-i="12">
<header><span class="n">13</span> <b>THE COMPLETE LIFE OF JOHN HOPKINS</b></header>
<details><summary>Scene summary: Someone, seeking revenge for mistreatment by a man who has insulted the lady, agrees to help despite someone’s initial reluctance.</summary>
<p class="scene">&quot;Come quick. The lady, she will explain. It is the great honor you
will have, monsieur. Ah, that milady could call upon Armand to do
this thing! But, no, I am only one chauffeur.&quot; With vehement gestures the chauffeur conducted Hopkins into the
house. He was ushered into a small but luxurious reception chamber. A
lady, young, and possessing the beauty of visions, rose from a chair.
In her eyes smouldered a becoming anger. Her high-arched, threadlike
brows were ruffled into a delicious frown. &quot;Milady,&quot; said the chauffeur, bowing low, &quot;I have the honor to
relate to you that I went to the house of Monsieur Long and found
him to be not at home. As I came back I see this gentleman in
combat against--how you say--greatest odds. He is fighting with
five--ten--thirty men--gendarmes, _aussi_. Yes, milady, he what you
call &#x27;swat&#x27; one--three--eight policemans. If that Monsieur Long is
out I say to myself this gentleman he will serve milady so well, and
I bring him here.&quot; &quot;Very well, Armand,&quot; said the lady, &quot;you may go.&quot; She turned to
Hopkins. &quot;I sent my chauffeur,&quot; she said, &quot;to bring my cousin, Walter Long.
There is a man in this house who has treated me with insult and
abuse. I have complain…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone wants Milady&#x27;s assistance against unseen attackers, while Milady seeks a brave companion.</p><label><input type="radio" name="p12" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man finds himself unexpectedly drawn into a personal conflict he did not ask for, yet must navigate with courage and discretion.</p><label><input type="radio" name="p12" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p12" value="n"> neither is a predicament</label>
</section><section data-i="13">
<header><span class="n">14</span> <b>THE COMPLETE LIFE OF JOHN HOPKINS</b></header>
<details><summary>Scene summary: Someone, smug after a romantic encounter, confronts a young man who had been flirting with a lady he desires.</summary>
<p class="scene">John Hopkins thrust the remains of his cigar into his coat pocket.
He looked upon this winning creature and felt his first thrill of
romance. It was a knightly love, and contained no disloyalty to the
flat with the flea-bitten terrier and the lady of his choice. He had
married her after a picnic of the Lady Label Stickers&#x27; Union, Lodge
No. 2, on a dare and a bet of new hats and chowder all around with
his friend, Billy McManus. This angel who was begging him to come
to her rescue was something too heavenly for chowder, and as for
hats--golden, jewelled crowns for her! &quot;Say,&quot; said John Hopkins, &quot;just show me the guy that you&#x27;ve got the
grouch at. I&#x27;ve neglected my talents as a scrapper heretofore, but
this is my busy night.&quot; &quot;He is in there,&quot; said the lady, pointing to a closed door. &quot;Come.
Are you sure that you do not falter or fear?&quot; &quot;Me?&quot; said John Hopkins. &quot;Just give me one of those roses in the
bunch you are wearing, will you?&quot; The lady gave him a red, red rose. John Hopkins kissed it, stuffed it
into his vest pocket, opened the door and walked into the room. It
was a handsome library, softly but brightly lighted. A young man was
there, reading. &quot;Books on etiquette is what you …</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone seeks a romantic encounter, only to be kidnapped by a mysterious lady.</p><label><input type="radio" name="p13" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>Someone must confront a man who has wronged a woman he now feels compelled to protect, risking his own safety to do so.</p><label><input type="radio" name="p13" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p13" value="n"> neither is a predicament</label>
</section><section data-i="14">
<header><span class="n">15</span> <b>THE COMPLETE LIFE OF JOHN HOPKINS</b></header>
<details><summary>Scene summary: Someone and someone argue over someone, a St. Bernard dog, while someone watches his philandering <mark>wife</mark> and <mark>daughter</mark>.</summary>
<p class="scene">&quot;Bess,&quot; he said calmly, &quot;I wish you would quit reading historical
novels. How in the world did that fellow get in here?&quot; &quot;Armand brought him,&quot; said the young lady. &quot;I think you are awfully
mean not to let me have that St. Bernard. I sent Armand for Walter. I
was so angry with you.&quot; &quot;Be sensible, Bess,&quot; said the young man, taking her arm. &quot;That dog
isn&#x27;t safe. He has bitten two or three people around the kennels.
Come now, let&#x27;s go tell auntie we are in good humor again.&quot; Arm in arm, they moved away. John Hopkins walked to his flat. The janitor&#x27;s five-year-old daughter
was playing on the steps. Hopkins gave her a nice, red rose and
walked upstairs. Mrs. Hopkins was philandering with curl-papers. &quot;Get your cigar?&quot; she asked, disinterestedly. &quot;Sure,&quot; said Hopkins, &quot;and I knocked around a while outside. It&#x27;s a
nice night.&quot; He sat upon the hornblende sofa, took out the stump of his cigar,
lighted it, and gazed at the graceful figures in &quot;The Storm&quot; on the
opposite wall. &quot;I was telling you,&quot; said he, &quot;about Mr. Whipple&#x27;s suit. It&#x27;s a gray,
with an invisible check, and it looks fine.&quot; III</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Bess wants her St. Bernard, but someone wants her to stop reading dangerous novels.</p><label><input type="radio" name="p14" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man struggles to reconcile his growing affection for a woman with the rigid expectations of his social circle.</p><label><input type="radio" name="p14" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p14" value="n"> neither is a predicament</label>
</section><section data-i="15">
<header><span class="n">16</span> <b>A LICKPENNY LOVER</b></header>
<details><summary>Scene summary: Someone, a 19-year-old <mark>saleslady</mark> in men&#x27;s gloves, learns much about customers and human nature in the Biggest <mark>Store</mark>, while keeping her own secrets.</summary>
<p class="scene">There, were 3,000 girls in the Biggest Store. Masie was one of them.
She was eighteen and a saleslady in the gents&#x27; gloves. Here she
became versed in two varieties of human beings--the kind of gents who
buy their gloves in department stores and the kind of women who buy
gloves for unfortunate gents. Besides this wide knowledge of the
human species, Masie had acquired other information. She had listened
to the promulgated wisdom of the 2,999 other girls and had stored it
in a brain that was as secretive and wary as that of a Maltese cat.
Perhaps nature, foreseeing that she would lack wise counsellors, had
mingled the saving ingredient of shrewdness along with her beauty, as
she has endowed the silver fox of the priceless fur above the other
animals with cunning. For Masie was beautiful. She was a deep-tinted blonde, with the calm
poise of a lady who cooks butter cakes in a window. She stood behind
her counter in the Biggest Store; and as you closed your hand over
the tape-line for your glove measure you thought of Hebe; and as you
looked again you wondered how she had come by Minerva&#x27;s eyes. When the floorwalker was not looking Masie chewed tutti frutti; when
he was looking she gaze…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone, a young <mark>saleslady</mark>, seeks to navigate the complexities of human nature while hiding her true identity.</p><label><input type="radio" name="p15" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>Someone, trapped in a monotonous job she despises, longs for a life that offers more than just the dull routine of selling gloves.</p><label><input type="radio" name="p15" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p15" value="n"> neither is a predicament</label>
</section><section data-i="16">
<header><span class="n">17</span> <b>A LICKPENNY LOVER</b></header>
<details><summary>Scene summary: Someone, a wealthy <mark>visitor</mark>, hesitates as floorwalker someone confronts him with a <mark>shopgirl</mark> smile, unaware of the flirtatious nature of his profession.</summary>
<p class="scene">That is the shopgirl smile, and I enjoin you to shun it unless you
are well fortified with callosity of the heart, caramels and a
congeniality for the capers of Cupid. This smile belonged to Masie&#x27;s
recreation hours and not to the store; but the floorwalker must have
his own. He is the Shylock of the stores. When he comes nosing around
the bridge of his nose is a toll-bridge. It is goo-goo eyes or &quot;git&quot;
when he looks toward a pretty girl. Of course not all floorwalkers
are thus. Only a few days ago the papers printed news of one over
eighty years of age. One day Irving Carter, painter, millionaire, traveller, poet,
automobilist, happened to enter the Biggest Store. It is due to him
to add that his visit was not voluntary. Filial duty took him by the
collar and dragged him inside, while his mother philandered among the
bronze and terra-cotta statuettes. Carter strolled across to the glove counter in order to shoot a
few minutes on the wing. His need for gloves was genuine; he had
forgotten to bring a pair with him. But his action hardly calls for
apology, because he had never heard of glove-counter flirtations. As he neared the vicinity of his fate he hesitated, suddenly
conscious o…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>The <mark>shopgirl</mark> someone confronts the floorwalker someone with her flirtatious gaze, testing his resolve.</p><label><input type="radio" name="p16" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man finds himself trapped in an awkward encounter with a <mark>shopgirl</mark> whose icy beauty challenges his composure, forcing him to navigate the delicate line between professional decorum and unexpected attraction.</p><label><input type="radio" name="p16" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p16" value="n"> neither is a predicament</label>
</section><section data-i="17">
<header><span class="n">18</span> <b>A LICKPENNY LOVER</b></header>
<details><summary>Scene summary: Someone, <mark>millionaire</mark>, felt both flattered and awkward as he encountered someone, a <mark>shopgirl</mark>, in a glove <mark>shop</mark>, realizing he had no social means to propose to her.</summary>
<p class="scene">And then Irving Carter, painter, millionaire, etc., felt a warm flush
rise to his aristocratically pale face. But not from diffidence. The
blush was intellectual in origin. He knew in a moment that he stood
in the ranks of the ready-made youths who wooed the giggling girls at
other counters. Himself leaned against the oaken trysting place of
a cockney Cupid with a desire in his heart for the favor of a glove
salesgirl. He was no more than Bill and Jack and Mickey. And then he
felt a sudden tolerance for them, and an elating, courageous contempt
for the conventions upon which he had fed, and an unhesitating
determination to have this perfect creature for his own. When the gloves were paid for and wrapped Carter lingered for a
moment. The dimples at the corners of Masie&#x27;s damask mouth deepened.
All gentlemen who bought gloves lingered in just that way. She curved
an arm, showing like Psyche&#x27;s through her shirt-waist sleeve, and
rested an elbow upon the show-case edge. Carter had never before encountered a situation of which he had not
been perfect master. But now he stood far more awkward than Bill
or Jack or Mickey. He had no chance of meeting this beautiful girl
socially. His mind …</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone desires to charm a <mark>shopgirl</mark> despite feeling out of place.</p><label><input type="radio" name="p17" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man struggling with his pride and social conventions finds himself awkwardly drawn to a <mark>shopgirl</mark>, determined to win her favor despite his usual confidence.</p><label><input type="radio" name="p17" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p17" value="n"> neither is a predicament</label>
</section><section data-i="18">
<header><span class="n">19</span> <b>A LICKPENNY LOVER</b></header>
<details><summary>Scene summary: Someone, a lady in her late twenties, diplomatically turns down a man&#x27;s romantic overtures while subtly arranging a clandestine meeting with him.</summary>
<p class="scene">&quot;Will you please pardon me,&quot; he said, &quot;if I seem too bold; but I
earnestly hope you will allow me the pleasure of seeing you again.
There is my name; I assure you that it is with the greatest respect
that I ask the favor of becoming one of your fr--acquaintances. May
I not hope for the privilege?&quot; Masie knew men--especially men who buy gloves. Without hesitation she
looked him frankly and smilingly in the eyes, and said: &quot;Sure. I guess you&#x27;re all right. I don&#x27;t usually go out with strange
gentlemen, though. It ain&#x27;t quite ladylike. When should you want to
see me again?&quot; &quot;As soon as I may,&quot; said Carter. &quot;If you would allow me to call at
your home, I--&quot; Masie laughed musically. &quot;Oh, gee, no!&quot; she said, emphatically. &quot;If
you could see our flat once! There&#x27;s five of us in three rooms. I&#x27;d
just like to see ma&#x27;s face if I was to bring a gentleman friend
there!&quot; &quot;Anywhere, then,&quot; said the enamored Carter, &quot;that will be convenient
to you.&quot; &quot;Say,&quot; suggested Masie, with a bright-idea look in her peach-blow
face; &quot;I guess Thursday night will about suit me. Suppose you come to
the corner of Eighth Avenue and Forty-eighth Street at 7:30. I live
right near the corner. But I&#x27;ve got to be back hom…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Willie nervously asked for her company, hoping she&#x27;d agree to a date.</p><label><input type="radio" name="p18" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A young woman balances between curiosity and caution as she agrees to meet a man she barely knows, hoping to explore new possibilities without disappointing her family or losing her sense of self.</p><label><input type="radio" name="p18" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p18" value="n"> neither is a predicament</label>
</section><section data-i="19">
<header><span class="n">20</span> <b>A LICKPENNY LOVER</b></header>
<details><summary>Scene summary: Someone, a young man, eagerly awaits a date with a <mark>street</mark>-corner <mark>shopgirl</mark>, while his <mark>mother</mark> chauffeurs him to a fancy <mark>restaurant</mark>.</summary>
<p class="scene">&quot;Permission to call!&quot; echoed small eyes, with a snigger. &quot;Did he say
anything about dinner in the Waldorf and a spin in his auto
afterward?&quot; &quot;Oh, cheese it!&quot; said Masie, wearily. &quot;You&#x27;ve been used to swell
things, I don&#x27;t think. You&#x27;ve had a swelled head ever since that
hose-cart driver took you out to a chop suey joint. No, he never
mentioned the Waldorf; but there&#x27;s a Fifth Avenue address on his
card, and if he buys the supper you can bet your life there won&#x27;t be
no pigtail on the waiter what takes the order.&quot; As Carter glided away from the Biggest Store with his mother in his
electric runabout, he bit his lip with a dull pain at his heart.
He knew that love had come to him for the first time in all the
twenty-nine years of his life. And that the object of it should make
so readily an appointment with him at a street corner, though it was
a step toward his desires, tortured him with misgivings. Carter did not know the shopgirl. He did not know that her home is
often either a scarcely habitable tiny room or a domicile filled to
overflowing with kith and kin. The street-corner is her parlor, the
park is her drawing-room; the avenue is her garden walk; yet for the
most part she is a…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone seeks love but fears rejection, while someone uses him to prove her worth.</p><label><input type="radio" name="p19" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man torn between his growing feelings for a woman he barely knows and his fear of losing control of his life struggles to navigate the uncertain path of love.</p><label><input type="radio" name="p19" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p19" value="n"> neither is a predicament</label>
</section><section data-i="20">
<header><span class="n">21</span> <b>A LICKPENNY LOVER</b></header>
<details><summary>Scene summary: Someone, a <mark>suitor</mark>, pressures someone into marriage, emphasizing his social superiority and wealth, while someone questions his sincerity and social status.</summary>
<p class="scene">&quot;Masie,&quot; said Carter, earnestly, &quot;you surely know that I love you. I
ask you sincerely to marry me. You know me well enough by this time
to have no doubts of me. I want you, and I must have you. I care
nothing for the difference in our stations.&quot; &quot;What is the difference?&quot; asked Masie, curiously. &quot;Well, there isn&#x27;t any,&quot; said Carter, quickly, &quot;except in the minds
of foolish people. It is in my power to give you a life of luxury. My
social position is beyond dispute, and my means are ample.&quot; &quot;They all say that,&quot; remarked Masie. &quot;It&#x27;s the kid they all give you.
I suppose you really work in a delicatessen or follow the races. I
ain&#x27;t as green as I look.&quot; &quot;I can furnish you all the proofs you want,&quot; said Carter, gently.
&quot;And I want you, Masie. I loved you the first day I saw you.&quot; &quot;They all do,&quot; said Masie, with an amused laugh, &quot;to hear &#x27;em talk.
If I could meet a man that got stuck on me the third time he&#x27;d seen
me I think I&#x27;d get mashed on him.&quot; &quot;Please don&#x27;t say such things,&quot; pleaded Carter. &quot;Listen to me, dear.
Ever since I first looked into your eyes you have been the only woman
in the world for me.&quot; &quot;Oh, ain&#x27;t you the kidder!&quot; smiled Masie. &quot;How many other girls did
you ever tell…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone faces someone’s marriage proposal, wanting to see through his sincerity.</p><label><input type="radio" name="p20" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man insists a woman marry him, claiming his wealth and status make them equal, but she sees through his words and mocks his desperate, insincere pursuit.</p><label><input type="radio" name="p20" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p20" value="n"> neither is a predicament</label>
</section><section data-i="21">
<header><span class="n">22</span> <b>A LICKPENNY LOVER</b></header>
<details><summary>Scene summary: Someone persistently courted someone, a <mark>shopgirl</mark>, offering her a life of leisure in exotic locations to break her from her mundane existence.</summary>
<p class="scene">But Carter persisted. And at length he reached the flimsy, fluttering
little soul of the shopgirl that existed somewhere deep down in her
lovely bosom. His words penetrated the heart whose very lightness was
its safest armor. She looked up at him with eyes that saw. And a warm
glow visited her cool cheeks. Tremblingly, awfully, her moth wings
closed, and she seemed about to settle upon the flower of love. Some
faint glimmer of life and its possibilities on the other side of her
glove counter dawned upon her. Carter felt the change and crowded the
opportunity. &quot;Marry me, Masie,&quot; he whispered softly, &quot;and we will go away from
this ugly city to beautiful ones. We will forget work and business,
and life will be one long holiday. I know where I should take you--I
have been there often. Just think of a shore where summer is eternal,
where the waves are always rippling on the lovely beach and the
people are happy and free as children. We will sail to those shores
and remain there as long as you please. In one of those far-away
cities there are grand and lovely palaces and towers full of
beautiful pictures and statues. The streets of the city are water,
and one travels about in--&quot; &quot;I know,…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone looked up at someone, her eyes alight with a flicker of possibility.</p><label><input type="radio" name="p21" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man tries to convince a hesitant woman to leave her mundane life and embark on an extravagant, uncertain journey with him.</p><label><input type="radio" name="p21" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p21" value="n"> neither is a predicament</label>
</section><section data-i="22">
<header><span class="n">23</span> <b>A LICKPENNY LOVER</b></header>
<details><summary>Scene summary: Someone, cool in his authority, insists they leave while someone’s uncertain nature holds sway, hinting at her potential marriage proposal to someone’s friend.</summary>
<p class="scene">&quot;I think we had better be going home,&quot; she said, coolly. &quot;It&#x27;s
getting late.&quot; Carter humored her. He had come to know her varying, thistle-down
moods, and that it was useless to combat them. But he felt a certain
happy triumph. He had held for a moment, though but by a silken
thread, the soul of his wild Psyche, and hope was stronger within
him. Once she had folded her wings and her cool hand had closed about
his own. At the Biggest Store the next day Masie&#x27;s chum, Lulu, waylaid her in
an angle of the counter. &quot;How are you and your swell friend making it? she asked. &quot;Oh, him?&quot; said Masie, patting her side curls. &quot;He ain&#x27;t in it any
more. Say, Lu, what do you think that fellow wanted me to do?&quot; &quot;Go on the stage?&quot; guessed Lulu, breathlessly. &quot;Nit; he&#x27;s too cheap a guy for that. He wanted me to marry him and go
down to Coney Island for a wedding tour!&quot; IV</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>She wants to end their secret affair, but he feels a hidden joy.</p><label><input type="radio" name="p22" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A woman hides her growing feelings for a man while pretending to be uninterested, hoping to keep their connection without giving in to the possibility of love.</p><label><input type="radio" name="p22" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p22" value="n"> neither is a predicament</label>
</section><section data-i="23">
<header><span class="n">24</span> <b>DOUGHERTY&#x27;S EYE-OPENER</b></header>
<details><summary>Scene summary: Big someone coveted social acceptance from the titled nobility of someone’s sport world.</summary>
<p class="scene">Big Jim Dougherty was a sport. He belonged to that race of men.
In Manhattan it is a distinct race. They are the Caribs of the
North--strong, artful, self-sufficient, clannish, honorable within
the laws of their race, holding in lenient contempt neighboring
tribes who bow to the measure of Society&#x27;s tapeline. I refer, of
course, to the titled nobility of sportdom. There is a class which
bears as a qualifying adjective the substantive belonging to a wind
instrument made of a cheap and base metal. But the tin mines of
Cornwall never produced the material for manufacturing descriptive
nomenclature for &quot;Big Jim&quot; Dougherty. The habitat of the sport is the lobby or the outside corner of
certain hotels and combination restaurants and cafés. They are mostly
men of different sizes, running from small to large; but they are
unanimous in the possession of a recently shaven, blue-black cheek
and chin and dark overcoats (in season) with black velvet collars. Of the domestic life of the sport little is known. It has been said
that Cupid and Hymen sometimes take a hand in the game and copper the
queen of hearts to lose. Daring theorists have averred--not content
with simply saying--that a sport o…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Big someone sought acceptance in someone’s titled nobility of sportdom.</p><label><input type="radio" name="p23" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>Big someone must navigate the rigid codes of his underground world while seeking a way to break free and claim a life beyond the shadows.</p><label><input type="radio" name="p23" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p23" value="n"> neither is a predicament</label>
</section><section data-i="24">
<header><span class="n">25</span> <b>DOUGHERTY&#x27;S EYE-OPENER</b></header>
<details><summary>Scene summary: Big someone&quot; attends social gatherings with his fellow someone, avoiding his <mark>wife</mark> at home.</summary>
<p class="scene">But mostly the sport is Oriental. He believes his women-folk should
not be too patent. Somewhere behind grilles or flower-ornamented fire
escapes they await him. There, no doubt, they tread on rugs from
Teheran and are diverted by the bulbul and play upon the dulcimer and
feed upon sweetmeats. But away from his home the sport is an integer.
He does not, as men of other races in Manhattan do, become the convoy
in his unoccupied hours of fluttering laces and high heels that tick
off delectably the happy seconds of the evening parade. He herds with
his own race at corners, and delivers a commentary in his Carib lingo
upon the passing show. &quot;Big Jim&quot; Dougherty had a wife, but he did not wear a button portrait
of her upon his lapel. He had a home in one of those brown-stone,
iron-railed streets on the west side that look like a recently
excavated bowling alley of Pompeii. To this home of his Mr. Dougherty repaired each night when the hour
was so late as to promise no further diversion in the arch domains
of sport. By that time the occupant of the monogamistic harem would
be in dreamland, the bulbul silenced and the hour propitious for
slumber. &quot;Big Jim&quot; always arose at twelve, meridian,…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>He seeks a private life away from his public Oriental sport, yet feels trapped in public life.</p><label><input type="radio" name="p24" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man struggles to balance his cultural identity and personal life while navigating the expectations of his community and the quiet routine of his domestic existence.</p><label><input type="radio" name="p24" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p24" value="n"> neither is a predicament</label>
</section><section data-i="25">
<header><span class="n">26</span> <b>DOUGHERTY&#x27;S EYE-OPENER</b></header>
<details><summary>Scene summary: Big someone&quot; someone reluctantly agreed to go out for <mark>dinner</mark> after his <mark>wife</mark> insisted, challenging his usual reluctance to leave the <mark>house</mark>.</summary>
<p class="scene">&quot;Big Jim&quot; Dougherty even listened to this conversation of hers
sometimes. He knew that she would have a nice dinner ready for him
every evening at seven when he came for it. She sometimes went to
matinées, and she had a talking machine with six dozen records. Once
when her Uncle Amos blew in on a wind from up-state, she went with
him to the Eden Musée. Surely these things were diversions enough for
any woman. One afternoon Mr. Dougherty finished his breakfast, put on his hat
and got away fairly for the door. When his hand was on the knob be
heard his wife&#x27;s voice. &quot;Jim,&quot; she said, firmly, &quot;I wish you would take me out to dinner this
evening. It has been three years since you have been outside the door
with me.&quot; &quot;Big Jim&quot; was astounded. She had never asked anything like this
before. It had the flavour of a totally new proposition. But he was a
game sport. &quot;All right,&quot; he said. &quot;You be ready when I come at seven. None of
this &#x27;wait two minutes till I primp an hour or two&#x27; kind of business,
now, Dele.&quot; &quot;I&#x27;ll be ready,&quot; said his wife, calmly. At seven she descended the stone steps in the Pompeian bowling alley
at the side of &quot;Big Jim&quot; Dougherty. She wore a dinner gown made of
a stuff t…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Big someone&quot; expected her to ask for a <mark>dinner</mark> date, not insist on one.</p><label><input type="radio" name="p25" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man accustomed to routine is confronted with an unexpected request for companionship, forcing him to reconsider the balance between habit and connection.</p><label><input type="radio" name="p25" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p25" value="n"> neither is a predicament</label>
</section><section data-i="26">
<header><span class="n">27</span> <b>DOUGHERTY&#x27;S EYE-OPENER</b></header>
<details><summary>Scene summary: Big someone, unsure of his new <mark>wife</mark>&#x27;s identity, decided to flaunt her at his usual haunts to impress his social circle.</summary>
<p class="scene">&quot;Big Jim&quot; Dougherty was troubled. There was a being at his side whom
he did not know. He thought of the sober-hued plumage that this bird
of paradise was accustomed to wear in her cage, and this winged
revelation puzzled him. In some way she reminded him of the Delia
Cullen that he had married four years before. Shyly and rather
awkwardly he stalked at her right hand. &quot;After dinner I&#x27;ll take you back home, Dele,&quot; said Mr. Dougherty,
&quot;and then I&#x27;ll drop back up to Seltzer&#x27;s with the boys. You can have
swell chuck to-night if you want it. I made a winning on Anaconda
yesterday; so you can go as far as you like.&quot; Mr. Dougherty had intended to make the outing with his unwonted wife
an inconspicuous one. Uxoriousness was a weakness that the precepts
of the Caribs did not countenance. If any of his friends of the
track, the billiard cloth or the square circle had wives they had
never complained of the fact in public. There were a number of table
d&#x27;hôte places on the cross streets near the broad and shining way;
and to one of these he had purposed to escort her, so that the bushel
might not be removed from the light of his domesticity. But while on the way Mr. Dougherty altered those inte…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Big someone wanted to hide his <mark>wife</mark>&#x27;s presence, but a mysterious bird-like being at her side made him change plans.</p><label><input type="radio" name="p26" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man struggling to reconcile his desire to display his <mark>wife</mark> publicly with his fear of judgment finds himself torn between pride and secrecy.</p><label><input type="radio" name="p26" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p26" value="n"> neither is a predicament</label>
</section><section data-i="27">
<header><span class="n">28</span> <b>DOUGHERTY&#x27;S EYE-OPENER</b></header>
<details><summary>Scene summary: Seltzer&#x27;s tribal gentlemen watch as someone and his <mark>wife</mark> someone enter, impressed but wary, while the guests enjoy a lively meal.</summary>
<p class="scene">The congregation of smooth-faced tribal gentlemen were on watch at
Seltzer&#x27;s. As Mr. Dougherty and his reorganized Delia passed they
stared, momentarily petrified, and then removed their hats--a
performance as unusual to them as was the astonishing innovation
presented to their gaze by &quot;Big Jim&quot;. On the latter gentleman&#x27;s
impassive face there appeared a slight flicker of triumph--a faint
flicker, no more to be observed than the expression called there by
the draft of little casino to a four-card spade flush. Hoogley&#x27;s was animated. Electric lights shone as, indeed, they were
expected to do. And the napery, the glassware and the flowers also
meritoriously performed the spectacular duties required of them. The
guests were numerous, well-dressed and gay. A waiter--not necessarily obsequious--conducted &quot;Big Jim&quot; Dougherty
and his wife to a table. &quot;Play that menu straight across for what you like, Dele,&quot; said &quot;Big
Jim.&quot; &quot;It&#x27;s you for a trough of the gilded oats to-night. It strikes
me that maybe we&#x27;ve been sticking too fast to home fodder.&quot; &quot;Big Jim&#x27;s&quot; wife gave her order. He looked at her with respect. She
had mentioned truffles; and he had not known that she knew what
truffles were. F…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Big someone seeks to impress his <mark>wife</mark> at a fancy <mark>restaurant</mark>, hoping her beauty and charm will win him praise.</p><label><input type="radio" name="p27" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man struggles to reconcile his pride with the growing awareness of his <mark>wife</mark>&#x27;s hidden talents and the shame of having underestimated her.</p><label><input type="radio" name="p27" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p27" value="n"> neither is a predicament</label>
</section><section data-i="28">
<header><span class="n">29</span> <b>DOUGHERTY&#x27;S EYE-OPENER</b></header>
<details><summary>Scene summary: The Honorable someone, seeking to impress, engages the reluctant someone in a lively conversation, transforming her into the center of attention as other politicians and acquaintances arrive.</summary>
<p class="scene">But when the Honorable Patrick Corrigan, leader in Dougherty&#x27;s
district and a friend of his, saw them and came over to the table,
matters got to the three-quarter stretch. The Honorable Patrick was a
gallant man, both in deeds and words. As for the Blarney stone, his
previous actions toward it must have been pronounced. Heavy damages
for breach of promise could surely have been obtained had the Blarney
stone seen fit to sue the Honorable Patrick. &quot;Jimmy, old man!&quot; he called; he clapped Dougherty on the back; he
shone like a midday sun upon Delia. &quot;Honorable Mr. Corrigan--Mrs. Dougherty,&quot; said &quot;Big Jim.&quot; The Honorable Patrick became a fountain of entertainment and
admiration. The waiter had to fetch a third chair for him; he made
another at the table, and the wineglasses were refilled. &quot;You selfish old rascal!&quot; he exclaimed, shaking an arch finger at
&quot;Big Jim,&quot; &quot;to have kept Mrs. Dougherty a secret from us.&quot; And then &quot;Big Jim&quot; Dougherty, who was no talker, sat dumb, and saw
the wife who had dined every evening for three years at home, blossom
like a fairy flower. Quick, witty, charming, full of light and ready
talk, she received the experienced attack of the Honorable Patrick on
the…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>The honorable someone seeks to expose a secret and win someone’s heart, while &quot;Big someone&quot; struggles to keep up.</p><label><input type="radio" name="p28" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man struggles to reconcile his quiet life with the sudden, overwhelming attention his <mark>wife</mark> receives, threatening to expose the hidden depths of their marriage.</p><label><input type="radio" name="p28" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p28" value="n"> neither is a predicament</label>
</section><section data-i="29">
<header><span class="n">30</span> <b>DOUGHERTY&#x27;S EYE-OPENER</b></header>
<details><summary>Scene summary: The Honorable someone, seeking to impress someone, attempts to seduce her, leading to a confrontation and a decision to take her to a pub instead.</summary>
<p class="scene">The dinner came to an end. The Honorable Patrick reached for Mrs.
Dougherty&#x27;s cloak; but that was a matter of action instead of words,
and Dougherty&#x27;s big hand got it first by two seconds. While the farewells were being said at the door the Honorable Patrick
smote Dougherty mightily between the shoulders. &quot;Jimmy, me boy,&quot; he declared, in a giant whisper, &quot;the madam is a
jewel of the first water. Ye&#x27;re a lucky dog.&quot; &quot;Big Jim&quot; walked homeward with his wife. She seemed quite as
pleased with the lights and show windows in the streets as with the
admiration of the men in Hoogley&#x27;s. As they passed Seltzer&#x27;s they
heard the sound of many voices in the café. The boys would be
starting the drinks around now and discussing past performances. At the door of their home Delia paused. The pleasure of the outing
radiated softly from her countenance. She could not hope for Jim of
evenings, but the glory of this one would lighten her lonely hours
for a long time. &quot;Thank you for taking me out, Jim,&quot; she said, gratefully. &quot;You&#x27;ll be
going back up to Seltzer&#x27;s now, of course.&quot; &quot;To ---- with Seltzer&#x27;s,&quot; said &quot;Big Jim,&quot; emphatically. &quot;And d----
Pat Corrigan! Does he think I haven&#x27;t got any eyes?&quot; And the…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone seeks to impress someone with a bold gesture, but is thwarted by her quicker action.</p><label><input type="radio" name="p29" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man struggles to maintain his pride and dignity after a public embarrassment, while trying to preserve his relationship and quiet his <mark>wife</mark>&#x27;s growing happiness.</p><label><input type="radio" name="p29" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p29" value="n"> neither is a predicament</label>
</section><section data-i="30">
<header><span class="n">31</span> <b>DOUGHERTY&#x27;S EYE-OPENER</b></header>
<details><summary>Scene summary: The <mark>bride</mark> desires a peach from her distant <mark>lover</mark>, while he holds power over her marriage and her desires.</summary>
<p class="scene">The bride sat in the rocker with her feet resting upon the world. She
was wrapt in rosy dreams and a kimono of the same hue. She wondered
what the people in Greenland and Tasmania and Beloochistan were
saying one to another about her marriage to Kid McGarry. Not that it
made any difference. There was no welter-weight from London to the
Southern Cross that could stand up four hours--no; four rounds--with
her bridegroom. And he had been hers for three weeks; and the crook
of her little finger could sway him more than the fist of any
142-pounder in the world. Love, when it is ours, is the other name for self-abnegation and
sacrifice. When it belongs to people across the airshaft it means
arrogance and self-conceit. The bride crossed her oxfords and looked thoughtfully at the
distemper Cupids on the ceiling. &quot;Precious,&quot; said she, with the air of Cleopatra asking Antony for
Rome done up in tissue paper and delivered at residence, &quot;I think
I would like a peach.&quot; Kid McGarry arose and put on his coat and hat. He was serious,
shaven, sentimental, and spry. &quot;All right,&quot; said he, as coolly as though he were only agreeing to
sign articles to fight the champion of England. &quot;I&#x27;ll step down and
…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>The <mark>bride</mark> desires her <mark>husband</mark>&#x27;s affection, yet fears losing him to distant admirers.</p><label><input type="radio" name="p30" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A woman trapped in a love that bends her will longs for a simple peach, while her powerful but obedient <mark>husband</mark> struggles to fulfill her wish in a world that resists giving way.</p><label><input type="radio" name="p30" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p30" value="n"> neither is a predicament</label>
</section><section data-i="31">
<header><span class="n">32</span> <b>DOUGHERTY&#x27;S EYE-OPENER</b></header>
<details><summary>Scene summary: The Kid seeks a peach for a <mark>lover</mark>, vying with a fruit vendor and a reluctant someone, who ultimately agrees to find oranges instead.</summary>
<p class="scene">&quot;Gotta da peach?&quot; asked the Kid in the tongue of Dante, the lover of
lovers. &quot;Ah, no,--&quot; sighed the vender. &quot;Not for one mont com-a da peach. Too
soon. Gotta da nice-a orange. Like-a da orange?&quot; Scornful, the Kid pursued his quest. He entered the all-night
chop-house, café, and bowling-alley of his friend and admirer, Justus
O&#x27;Callahan. The O&#x27;Callahan was about in his institution, looking for
leaks. &quot;I want it straight,&quot; said the Kid to him. &quot;The old woman has got a
hunch that she wants a peach. Now, if you&#x27;ve got a peach, Cal, get it
out quick. I want it and others like it if you&#x27;ve got &#x27;em in plural
quantities.&quot; &quot;The house is yours,&quot; said O&#x27;Callahan. &quot;But there&#x27;s no peach in it.
It&#x27;s too soon. I don&#x27;t suppose you could even find &#x27;em at one of the
Broadway joints. That&#x27;s too bad. When a lady fixes her mouth for a
certain kind of fruit nothing else won&#x27;t do. It&#x27;s too late now to
find any of the first-class fruiterers open. But if you think the
missis would like some nice oranges I&#x27;ve just got a box of fine ones
in that she might--&quot; &quot;Much obliged, Cal. It&#x27;s a peach proposition right from the ring of
the gong. I&#x27;ll try further.&quot; The time was nearly midnight as the Kid walked down the…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>The Kid seeks a peach but finds oranges instead, rushing to a fruiterer just as the lights go out.</p><label><input type="radio" name="p31" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>The Kid needs to find a peach for a woman who insists on it, but every vendor says it&#x27;s too soon and offers only oranges instead.</p><label><input type="radio" name="p31" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p31" value="n"> neither is a predicament</label>
</section><section data-i="32">
<header><span class="n">33</span> <b>DOUGHERTY&#x27;S EYE-OPENER</b></header>
<details><summary>Scene summary: The Kid seeks employment, while the <mark>Captain</mark> of Police considers helping him infiltrate gambling den someone’s, despite previous conflicts.</summary>
<p class="scene">&quot;Peaches?&quot; said he, with extreme deliberation. &quot;Well, no, Sir. Not for three or four weeks yet. I haven&#x27;t any idea
where you might find some. There may be a few in town from under
the glass, but they&#x27;d be hard to locate. Maybe at one of the more
expensive hotels--some place where there&#x27;s plenty of money to waste.
I&#x27;ve got some very fine oranges, though--from a shipload that came in
to-day.&quot; The Kid lingered on the corner for a moment, and then set out briskly
toward a pair of green lights that flanked the steps of a building
down a dark side street. &quot;Captain around anywhere?&quot; he asked of the desk sergeant of the
police station. At that moment the captain came briskly forward from the rear. He was
in plain clothes and had a busy air. &quot;Hello, Kid,&quot; he said to the pugilist. &quot;Thought you were
bridal-touring? &quot;Got back yesterday. I&#x27;m a solid citizen now. Think I&#x27;ll take an
interest in municipal doings. How would it suit you to get into
Denver Dick&#x27;s place to-night, Cap? &quot;Past performances,&quot; said the captain, twisting his moustache.
&quot;Denver was closed up two months ago.&quot; &quot;Correct,&quot; said the Kid. &quot;Rafferty chased him out of the Forty-third.
He&#x27;s running in your precinct now, and his game&#x27;…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Kid seeks to infiltrate someone’s gambling den but finds it closed, needing help from the <mark>captain</mark>.</p><label><input type="radio" name="p32" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>The Kid needs to convince the <mark>captain</mark> to raid a gambling den controlled by an old enemy, but he must do so quickly before the enemy discovers his betrayal.</p><label><input type="radio" name="p32" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p32" value="n"> neither is a predicament</label>
</section><section data-i="33">
<header><span class="n">34</span> <b>DOUGHERTY&#x27;S EYE-OPENER</b></header>
<details><summary>Scene summary: The <mark>captain</mark> and his dozen men, led by the Kid, raided a dark building, encountering a heated game of chance and a <mark>rival</mark> gang, before capturing the premises.</summary>
<p class="scene">Before ten minutes had sped the captain with a dozen men stole with
their guide into the hallway of a dark and virtuous-looking building
in which many businesses were conducted by day. &quot;Third floor, rear,&quot; said the Kid, softly. &quot;I&#x27;ll lead the way.&quot; Two axemen faced the door that he pointed out to them. &quot;It seems all quiet,&quot; said the captain, doubtfully. &quot;Are you sure
your tip is straight?&quot; &quot;Cut away!&quot; said the Kid. &quot;It&#x27;s on me if it ain&#x27;t.&quot; The axes crashed through the as yet unprotected door. A blaze of
light from within poured through the smashed panels. The door fell,
and the raiders sprang into the room with their guns handy. The big room was furnished with the gaudy magnificence dear to Denver
Dick&#x27;s western ideas. Various well-patronized games were in progress.
About fifty men who were in the room rushed upon the police in a
grand break for personal liberty. The plain-clothes men had to do a
little club-swinging. More than half the patrons escaped. Denver Dick had graced his game with his own presence that night.
He led the rush that was intended to sweep away the smaller body of
raiders, But when he saw the Kid his manner became personal. Being
in the heavyweight class he ca…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>The <mark>captain</mark> seeks to lead a raid, but doubts success as the Kid&#x27;s plan unfolds.</p><label><input type="radio" name="p33" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man must prove his worth in a dangerous confrontation to claim a prize he&#x27;s risking everything for.</p><label><input type="radio" name="p33" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p33" value="n"> neither is a predicament</label>
</section><section data-i="34">
<header><span class="n">35</span> <b>DOUGHERTY&#x27;S EYE-OPENER</b></header>
<details><summary>Scene summary: The Kid demands control of a lavish <mark>dinner</mark> <mark>party</mark>, suspects theft, and retrieves the last peach, threatening violence if it happens again.</summary>
<p class="scene">Here was a long table set with choicest chinaware and silver, and
lavishly furnished with food of that expensive and spectacular sort
of which the devotees of sport are supposed to be fond. Here again
was to be perceived the liberal and florid taste of the gentleman
with the urban cognomenal prefix. A No. 10 patent leather shoe protruded a few of its inches outside
the tablecloth along the floor. The Kid seized this and plucked forth
a black man in a white tie and the garb of a servitor. &quot;Get up!&quot; commanded the Kid. &quot;Are you in charge of this free lunch?&quot; &quot;Yes, sah, I was. Has they done pinched us ag&#x27;in, boss?&quot; &quot;Looks that way. Listen to me. Are there any peaches in this layout?
If there ain&#x27;t I&#x27;ll have to throw up the sponge.&quot; &quot;There was three dozen, sah, when the game opened this evenin&#x27;; but
I reckon the gentlemen done eat &#x27;em all up. If you&#x27;d like to eat a
fust-rate orange, sah, I kin find you some.&quot; &quot;Get busy,&quot; ordered the Kid, sternly, &quot;and move whatever peach crop
you&#x27;ve got quick or there&#x27;ll be trouble. If anybody oranges me again
to-night, I&#x27;ll knock his face off.&quot; The raid on Denver Dick&#x27;s high-priced and prodigal luncheon revealed
one lone, last peach that had escaped th…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>The Kid seeks a last peach at a lavish <mark>dinner</mark>, fearing theft, while authorities arrive.</p><label><input type="radio" name="p34" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man desperate to find a peach before it&#x27;s too late races against time and rivals to secure his prize before it vanishes entirely.</p><label><input type="radio" name="p34" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p34" value="n"> neither is a predicament</label>
</section><section data-i="35">
<header><span class="n">36</span> <b>DOUGHERTY&#x27;S EYE-OPENER</b></header>
<details><summary>Scene summary: The Knight returns to someone with a stolen peach for his <mark>bride</mark>, who prefers an orange.</summary>
<p class="scene">His heart was light as he went. So rode the knights back to Camelot
after perils and high deeds done for their ladies fair. The Kid&#x27;s
lady had commanded him and he had obeyed. True, it was but a peach
that she had craved; but it had been no small deed to glean a peach
at midnight from that wintry city where yet the February snows lay
like iron. She had asked for a peach; she was his bride; in his
pocket the peach was warming in his hand that held it for fear that
it might fall out and be lost. On the way the Kid turned in at an all-night drug store and said to
the spectacled clerk: &quot;Say, sport, I wish you&#x27;d size up this rib of mine and see if it&#x27;s
broke. I was in a little scrap and bumped down a flight or two of
stairs.&quot; The druggist made an examination. &quot;It isn&#x27;t broken,&quot; was his
diagnosis, &quot;but you have a bruise there that looks like you&#x27;d fallen
off the Flatiron twice.&quot; &quot;That&#x27;s all right,&quot; said the Kid. &quot;Let&#x27;s have your clothesbrush,
please.&quot; The bride waited in the rosy glow of the pink lamp shade. The
miracles were not all passed away. By breathing a desire for some
slight thing--a flower, a pomegranate, a--oh, yes, a peach--she could
send forth her man into the night, into th…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>The Kid seeks to please his <mark>bride</mark> by delivering a requested peach, despite its trivial nature.</p><label><input type="radio" name="p35" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>He risks danger to fetch a trivial gift for his <mark>bride</mark>, only to find her preference has changed.</p><label><input type="radio" name="p35" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p35" value="n"> neither is a predicament</label>
</section><section data-i="36">
<header><span class="n">37</span> <b>THE HARBINGER</b></header>
<details><summary>Scene summary: The city <mark>editor</mark> reads news from across the country while the farmer sees only winter on his fields, unaware that someone’s search for someone signals the arrival of spring.</summary>
<p class="scene">Long before the springtide is felt in the dull bosom of the yokel
does the city man know that the grass-green goddess is upon her
throne. He sits at his breakfast eggs and toast, begirt by stone
walls, opens his morning paper and sees journalism leave vernalism
at the post. For, whereas, spring&#x27;s couriers were once the evidence of our finer
senses, now the Associated Press does the trick. The warble of the first robin in Hackensack, the stirring of the
maple sap in Bennington, the budding of the pussy willows along Main
Street in Syracuse, the first chirp of the bluebird, the swan song of
the Blue Point, the annual tornado in St. Louis, the plaint of the
peach pessimist from Pompton, N. J., the regular visit of the tame
wild goose with a broken leg to the pond near Bilgewater Junction,
the base attempt of the Drug Trust to boost the price of quinine
foiled in the House by Congressman Jinks, the first tall poplar
struck by lightning and the usual stunned picknickers who had taken
refuge, the first crack of the ice jam in the Allegheny River, the
finding of a violet in its mossy bed by the correspondent at Round
Corners--these are the advance signs of the burgeoning season that
are w…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Stuck in stone walls, city man craves nature&#x27;s first signs to confirm spring&#x27;s arrival.</p><label><input type="radio" name="p36" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man trapped in the monotony of city life longs for the genuine signs of spring, but finds only artificial reports replacing nature&#x27;s quiet signals.</p><label><input type="radio" name="p36" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p36" value="n"> neither is a predicament</label>
</section><section data-i="37">
<header><span class="n">38</span> <b>THE HARBINGER</b></header>
<details><summary>Scene summary: Someone, desperate for his <mark>wife</mark>&#x27;s dollar, conspires with someone and someone to steal it from their married friends in Union <mark>Square</mark>.</summary>
<p class="scene">Ere the first violet blew, Mr. Peters, Mr. Ragsdale and Mr. Kidd sat
together on a bench in Union Square and conspired. Mr. Peters was the
D&#x27;Artagnan of the loafers there. He was the dingiest, the laziest,
the sorriest brown blot against the green background of any bench in
the park. But just then he was the most important of the trio. Mr. Peters had a wife. This had not heretofore affected his standing
with Ragsy and Kidd. But to-day it invested him with a peculiar
interest. His friends, having escaped matrimony, had shown a
disposition to deride Mr. Peters for his venture on that troubled
sea. But at last they had been forced to acknowledge that either
he had been gifted with a large foresight or that he was one of
Fortune&#x27;s lucky sons. For, Mrs. Peters had a dollar. A whole dollar bill, good and
receivable by the Government for customs, taxes and all public
dues. How to get possession of that dollar was the question up for
discussion by the three musty musketeers. &quot;How do you know it was a dollar?&quot; asked Ragsy, the immensity of the
sum inclining him to scepticism. &quot;The coalman seen her have it,&quot; said Mr. Peters. &quot;She went out
and done some washing yesterday. And look what she gi…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone wants the dollar his <mark>wife</mark> has, but fears losing face among friends.</p><label><input type="radio" name="p37" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>Three men plot to steal a dollar from someone’s <mark>wife</mark>, who unknowingly possesses it, forcing him to confront his friends&#x27; greed and protect her from their violent intentions.</p><label><input type="radio" name="p37" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p37" value="n"> neither is a predicament</label>
</section><section data-i="38">
<header><span class="n">39</span> <b>THE HARBINGER</b></header>
<details><summary>Scene summary: Someone threatens to leave someone to seek payment from their adulterous <mark>daughter</mark>-in-law someone.</summary>
<p class="scene">&quot;Maguire,&quot; said Ragsy, pointedly, &quot;has got his bock beer sign out. If
we had a dollar we could--&quot; &quot;Hush up!&quot; said Mr. Peters, licking his lips. &quot;We got to get that
case note somehow, boys. Ain&#x27;t what&#x27;s a man&#x27;s wife&#x27;s his? Leave it to
me. I&#x27;ll go over to the house and get it. Wait here for me.&quot; &quot;I&#x27;ve seen &#x27;em give up quick, and tell you where it&#x27;s hid if you kick
&#x27;em in the ribs,&quot; said Kidd. &quot;No man would kick a woman,&quot; said Peters, virtuously. &quot;A little
choking--just a touch on the windpipe--that gets away with &#x27;em--and
no marks left. Wait for me. I&#x27;ll bring back that dollar, boys.&quot; High up in a tenement-house between Second Avenue and the river lived
the Peterses in a back room so gloomy that the landlord blushed to
take the rent for it. Mrs. Peters worked at sundry times, doing odd
jobs of scrubbing and washing. Mr. Peters had a pure, unbroken record
of five years without having earned a penny. And yet they clung
together, sharing each other&#x27;s hatred and misery, being creatures
of habit. Of habit, the power that keeps the earth from flying to
pieces; though there is some silly theory of gravitation. Mrs. Peters reposed her 200 pounds on the safer of the two chairs and
gazed stoli…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone wants the dollar, but someone plans to get the case note instead.</p><label><input type="radio" name="p38" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>Someone must steal a case note from his <mark>wife</mark>&#x27;s hidden stash to buy a dollar for beer, but he refuses to harm her despite his desperation.</p><label><input type="radio" name="p38" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p38" value="n"> neither is a predicament</label>
</section><section data-i="39">
<header><span class="n">40</span> <b>THE HARBINGER</b></header>
<details><summary>Scene summary: Someone calculated someone’s distance, eyeing her as a potential victim for his planned blackmail.</summary>
<p class="scene">Mr. Peters&#x27;s eye calculated the distance between them. By taking her
by surprise it might be possible to spring upon her, overthrow her,
and apply the throttling tactics of which he had boasted to his
waiting comrades. True, it had been only a boast; never yet had he
dared to lay violent hands upon her; but with the thoughts of the
delicious, cool bock or Culmbacher bracing his nerves, he was near
to upsetting his own theories of the treatment due by a gentleman
to a lady. But, with his loafer&#x27;s love for the more artistic and
less strenuous way, he chose diplomacy first, the high card in the
game--the assumed attitude of success already attained. &quot;You have a dollar,&quot; he said, loftily, but significantly in the tone
that goes with the lighting of a cigar--when the properties are at
hand. &quot;I have,&quot; said Mrs. Peters, producing the bill from her bosom and
crackling it, teasingly. &quot;I am offered a position in a--in a tea store,&quot; said Mr. Peters. &quot;I
am to begin work to-morrow. But it will be necessary for me to buy a
pair of--&quot; &quot;You are a liar,&quot; said Mrs. Peters, reinterring the note. &quot;No tea
store, nor no A B C store, nor no junk shop would have you. I rubbed
the skin off both me hands wa…</p></details>
<div class="pair"><div class="pred"><span class="who">qwen2.5:3b-instruct</span><p>Someone schemes to charm someone away from a tea job, while she resists with a tough stance.</p><label><input type="radio" name="p39" value="0"> better</label></div><div class="pred"><span class="who">qwen3:14b</span><p>A man trying to manipulate a woman with false promises faces her sharp rejection and must find another way to achieve his goal.</p><label><input type="radio" name="p39" value="1"> better</label></div></div>
<label class="both"><input type="radio" name="p39" value="n"> neither is a predicament</label>
</section>
</main>
<script>
const KEY='stp-pilot:'+["qwen2.5:3b-instruct", "qwen3:14b"].join('|'), labels=["qwen2.5:3b-instruct", "qwen3:14b"];
let saved={}; try{saved=JSON.parse(localStorage.getItem(KEY)||'{}')}catch(e){}
for(const [k,v] of Object.entries(saved)){const el=document.querySelector(`input[name="${k}"][value="${v}"]`); if(el) el.checked=true}
function tally(){
const c={0:0,1:0,n:0}; let done=0;
document.querySelectorAll('input[type=radio]:checked').forEach(r=>{c[r.value]++;done++});
document.getElementById('tally').textContent=`Picked ${done} of 40: ${labels[0]} ${c[0]}, ${labels[1]} ${c[1]}, neither ${c.n}`;
}
document.addEventListener('change',e=>{if(e.target.type!=='radio')return; saved[e.target.name]=e.target.value;
try{localStorage.setItem(KEY,JSON.stringify(saved))}catch(err){} tally()});
tally();
</script>
@@ -0,0 +1,246 @@
{
"model": "qwen3:14b",
"prompt": "You describe the predicament of the main person in a passage of fiction, in ONE sentence.\n\nA predicament is what that person is up against and what they want out of it: the kind of trouble or opportunity that could happen to anyone, in any line of work, in any town.\n\nRules:\n- Exactly one sentence, at most 20 words.\n- Leave out everything that ties it to this story: no names, no jobs or professions, no places, no businesses, and no objects that only belong to this story.\n- Say what the person is up against, and what they want.\n- Do not describe the writing, the mood, or who is telling the story.\n- Output only the sentence.",
"summaries": [
{
"chunk": 0,
"story": 0,
"summary": "A man struggles to understand the overwhelming complexity of a city's collective voice, seeking clarity in a world filled with individual sounds but no unified message.",
"model_output": "A man struggles to understand the overwhelming complexity of a city's collective voice, seeking clarity in a world filled with individual sounds but no unified message."
},
{
"chunk": 1,
"story": 0,
"summary": "The person is trying to understand the city's hidden message, hoping to find a unified voice that reveals its true nature.",
"model_output": "The person is trying to understand the city's hidden message, hoping to find a unified voice that reveals its true nature."
},
{
"chunk": 2,
"story": 0,
"summary": "A man determined to uncover the unique voice of a city faces the challenge of distinguishing its true character from the distractions and distractions of daily life.",
"model_output": "A man determined to uncover the unique voice of a city faces the challenge of distinguishing its true character from the distractions and distractions of daily life."
},
{
"chunk": 3,
"story": 0,
"summary": "A man tries to uncover the city's hidden voice through a cop, only to find that the cop's personal life and indifference to the city's soul leave him with no real answers.",
"model_output": "A man tries to uncover the city's hidden voice through a cop, only to find that the cop's personal life and indifference to the city's soul leave him with no real answers."
},
{
"chunk": 4,
"story": 0,
"summary": "A man seeks to capture the essence of a city's soul through a unique blend of its sounds, hoping to create a single, powerful vocalization that represents its spirit.",
"model_output": "A man seeks to capture the essence of a city's soul through a unique blend of its sounds, hoping to create a single, powerful vocalization that represents its spirit."
},
{
"chunk": 5,
"story": 0,
"summary": "A man seeks understanding from a city that refuses to speak, desperate to uncover a hidden truth that eludes everyone else.",
"model_output": "A man seeks understanding from a city that refuses to speak, desperate to uncover a hidden truth that eludes everyone else."
},
{
"chunk": 6,
"story": 0,
"summary": "Aurelia and the narrator sit in silence, caught between the quiet intimacy of the moment and the unspoken weight of the city's presence.",
"model_output": "Aurelia and the narrator sit in silence, caught between the quiet intimacy of the moment and the unspoken weight of the city's presence."
},
{
"chunk": 7,
"story": 1,
"summary": "John Hopkins must navigate the harsh realities of poverty, love, and war all at once in the crowded, unforgiving city.",
"model_output": "John Hopkins must navigate the harsh realities of poverty, love, and war all at once in the crowded, unforgiving city."
},
{
"chunk": 8,
"story": 1,
"summary": "Mrs. Hopkins must navigate the chaotic unpredictability of city life while clinging to the comfort of her familiar routines.",
"model_output": "Mrs. Hopkins must navigate the chaotic unpredictability of city life while clinging to the comfort of her familiar routines."
},
{
"chunk": 9,
"story": 1,
"summary": "John Hopkins longs for connection but is trapped in a life of monotony and shallow conversation.",
"model_output": "John Hopkins longs for connection but is trapped in a life of monotony and shallow conversation."
},
{
"chunk": 10,
"story": 1,
"summary": "A man tries to pay for a purchase with a promise of future money, but his dishonesty sparks a violent confrontation with a shopkeeper who sees his actions as proof of humanity's corruption.",
"model_output": "A man tries to pay for a purchase with a promise of future money, but his dishonesty sparks a violent confrontation with a shopkeeper who sees his actions as proof of humanity's corruption."
},
{
"chunk": 11,
"story": 1,
"summary": "A man flees from police pursuit, seeking refuge in an unknown vehicle that offers him a chance to escape, but must decide whether to trust his mysterious rescuer.",
"model_output": "A man flees from police pursuit, seeking refuge in an unknown vehicle that offers him a chance to escape, but must decide whether to trust his mysterious rescuer."
},
{
"chunk": 12,
"story": 1,
"summary": "A man finds himself unexpectedly drawn into a personal conflict he did not ask for, yet must navigate with courage and discretion.",
"model_output": "A man finds himself unexpectedly drawn into a personal conflict he did not ask for, yet must navigate with courage and discretion."
},
{
"chunk": 13,
"story": 1,
"summary": "John Hopkins must confront a man who has wronged a woman he now feels compelled to protect, risking his own safety to do so.",
"model_output": "John Hopkins must confront a man who has wronged a woman he now feels compelled to protect, risking his own safety to do so."
},
{
"chunk": 14,
"story": 1,
"summary": "A man struggles to reconcile his growing affection for a woman with the rigid expectations of his social circle.",
"model_output": "A man struggles to reconcile his growing affection for a woman with the rigid expectations of his social circle."
},
{
"chunk": 15,
"story": 2,
"summary": "Masie, trapped in a monotonous job she despises, longs for a life that offers more than just the dull routine of selling gloves.",
"model_output": "Masie, trapped in a monotonous job she despises, longs for a life that offers more than just the dull routine of selling gloves."
},
{
"chunk": 16,
"story": 2,
"summary": "A man finds himself trapped in an awkward encounter with a shopgirl whose icy beauty challenges his composure, forcing him to navigate the delicate line between professional decorum and unexpected attraction.",
"model_output": "A man finds himself trapped in an awkward encounter with a shopgirl whose icy beauty challenges his composure, forcing him to navigate the delicate line between professional decorum and unexpected attraction."
},
{
"chunk": 17,
"story": 2,
"summary": "A man struggling with his pride and social conventions finds himself awkwardly drawn to a shopgirl, determined to win her favor despite his usual confidence.",
"model_output": "A man struggling with his pride and social conventions finds himself awkwardly drawn to a shopgirl, determined to win her favor despite his usual confidence."
},
{
"chunk": 18,
"story": 2,
"summary": "A young woman balances between curiosity and caution as she agrees to meet a man she barely knows, hoping to explore new possibilities without disappointing her family or losing her sense of self.",
"model_output": "A young woman balances between curiosity and caution as she agrees to meet a man she barely knows, hoping to explore new possibilities without disappointing her family or losing her sense of self."
},
{
"chunk": 19,
"story": 2,
"summary": "A man torn between his growing feelings for a woman he barely knows and his fear of losing control of his life struggles to navigate the uncertain path of love.",
"model_output": "A man torn between his growing feelings for a woman he barely knows and his fear of losing control of his life struggles to navigate the uncertain path of love."
},
{
"chunk": 20,
"story": 2,
"summary": "A man insists a woman marry him, claiming his wealth and status make them equal, but she sees through his words and mocks his desperate, insincere pursuit.",
"model_output": "A man insists a woman marry him, claiming his wealth and status make them equal, but she sees through his words and mocks his desperate, insincere pursuit."
},
{
"chunk": 21,
"story": 2,
"summary": "A man tries to convince a hesitant woman to leave her mundane life and embark on an extravagant, uncertain journey with him.",
"model_output": "A man tries to convince a hesitant woman to leave her mundane life and embark on an extravagant, uncertain journey with him."
},
{
"chunk": 22,
"story": 2,
"summary": "A woman hides her growing feelings for a man while pretending to be uninterested, hoping to keep their connection without giving in to the possibility of love.",
"model_output": "A woman hides her growing feelings for a man while pretending to be uninterested, hoping to keep their connection without giving in to the possibility of love."
},
{
"chunk": 23,
"story": 3,
"summary": "Big Jim Dougherty must navigate the rigid codes of his underground world while seeking a way to break free and claim a life beyond the shadows.",
"model_output": "Big Jim Dougherty must navigate the rigid codes of his underground world while seeking a way to break free and claim a life beyond the shadows."
},
{
"chunk": 24,
"story": 3,
"summary": "A man struggles to balance his cultural identity and personal life while navigating the expectations of his community and the quiet routine of his domestic existence.",
"model_output": "A man struggles to balance his cultural identity and personal life while navigating the expectations of his community and the quiet routine of his domestic existence."
},
{
"chunk": 25,
"story": 3,
"summary": "A man accustomed to routine is confronted with an unexpected request for companionship, forcing him to reconsider the balance between habit and connection.",
"model_output": "A man accustomed to routine is confronted with an unexpected request for companionship, forcing him to reconsider the balance between habit and connection."
},
{
"chunk": 26,
"story": 3,
"summary": "A man struggling to reconcile his desire to display his wife publicly with his fear of judgment finds himself torn between pride and secrecy.",
"model_output": "A man struggling to reconcile his desire to display his wife publicly with his fear of judgment finds himself torn between pride and secrecy."
},
{
"chunk": 27,
"story": 3,
"summary": "A man struggles to reconcile his pride with the growing awareness of his wife's hidden talents and the shame of having underestimated her.",
"model_output": "A man struggles to reconcile his pride with the growing awareness of his wife's hidden talents and the shame of having underestimated her."
},
{
"chunk": 28,
"story": 3,
"summary": "A man struggles to reconcile his quiet life with the sudden, overwhelming attention his wife receives, threatening to expose the hidden depths of their marriage.",
"model_output": "A man struggles to reconcile his quiet life with the sudden, overwhelming attention his wife receives, threatening to expose the hidden depths of their marriage."
},
{
"chunk": 29,
"story": 3,
"summary": "A man struggles to maintain his pride and dignity after a public embarrassment, while trying to preserve his relationship and quiet his wife's growing happiness.",
"model_output": "A man struggles to maintain his pride and dignity after a public embarrassment, while trying to preserve his relationship and quiet his wife's growing happiness."
},
{
"chunk": 30,
"story": 3,
"summary": "A woman trapped in a love that bends her will longs for a simple peach, while her powerful but obedient husband struggles to fulfill her wish in a world that resists giving way.",
"model_output": "A woman trapped in a love that bends her will longs for a simple peach, while her powerful but obedient husband struggles to fulfill her wish in a world that resists giving way."
},
{
"chunk": 31,
"story": 3,
"summary": "The Kid needs to find a peach for a woman who insists on it, but every vendor says it's too soon and offers only oranges instead.",
"model_output": "The Kid needs to find a peach for a woman who insists on it, but every vendor says it's too soon and offers only oranges instead."
},
{
"chunk": 32,
"story": 3,
"summary": "The Kid needs to convince the captain to raid a gambling den controlled by an old enemy, but he must do so quickly before the enemy discovers his betrayal.",
"model_output": "The Kid needs to convince the captain to raid a gambling den controlled by an old enemy, but he must do so quickly before the enemy discovers his betrayal."
},
{
"chunk": 33,
"story": 3,
"summary": "A man must prove his worth in a dangerous confrontation to claim a prize he's risking everything for.",
"model_output": "A man must prove his worth in a dangerous confrontation to claim a prize he's risking everything for."
},
{
"chunk": 34,
"story": 3,
"summary": "A man desperate to find a peach before it's too late races against time and rivals to secure his prize before it vanishes entirely.",
"model_output": "A man desperate to find a peach before it's too late races against time and rivals to secure his prize before it vanishes entirely."
},
{
"chunk": 35,
"story": 3,
"summary": "He risks danger to fetch a trivial gift for his bride, only to find her preference has changed.",
"model_output": "He risks danger to fetch a trivial gift for his bride, only to find her preference has changed."
},
{
"chunk": 36,
"story": 4,
"summary": "A man trapped in the monotony of city life longs for the genuine signs of spring, but finds only artificial reports replacing nature's quiet signals.",
"model_output": "A man trapped in the monotony of city life longs for the genuine signs of spring, but finds only artificial reports replacing nature's quiet signals."
},
{
"chunk": 37,
"story": 4,
"summary": "Three men plot to steal a dollar from Mr. Peters's wife, who unknowingly possesses it, forcing him to confront his friends' greed and protect her from their violent intentions.",
"model_output": "Three men plot to steal a dollar from Mr. Peters's wife, who unknowingly possesses it, forcing him to confront his friends' greed and protect her from their violent intentions."
},
{
"chunk": 38,
"story": 4,
"summary": "Mr. Peters must steal a case note from his wife's hidden stash to buy a dollar for beer, but he refuses to harm her despite his desperation.",
"model_output": "Mr. Peters must steal a case note from his wife's hidden stash to buy a dollar for beer, but he refuses to harm her despite his desperation."
},
{
"chunk": 39,
"story": 4,
"summary": "A man trying to manipulate a woman with false promises faces her sharp rejection and must find another way to achieve his goal.",
"model_output": "A man trying to manipulate a woman with false promises faces her sharp rejection and must find another way to achieve his goal."
}
]
}
@@ -0,0 +1,246 @@
{
"model": "qwen2.5:3b-instruct",
"prompt": "You describe the predicament of the main person in a passage of fiction, in ONE sentence.\n\nA predicament is what that person is up against and what they want out of it: the kind of trouble or opportunity that could happen to anyone, in any line of work, in any town.\n\nRules:\n- Exactly one sentence, at most 20 words.\n- Leave out everything that ties it to this story: no names, no jobs or professions, no places, no businesses, and no objects that only belong to this story.\n- Say what the person is up against, and what they want.\n- Do not describe the writing, the mood, or who is telling the story.\n- Output only the sentence.",
"summaries": [
{
"chunk": 0,
"story": 0,
"summary": "Finding the voice of a big city, I sought in vain among school chants.",
"model_output": "Finding the voice of a big city, I sought in vain among school chants."
},
{
"chunk": 1,
"story": 0,
"summary": "I want to know the city's voice, but it echoes like Philadelphia.",
"model_output": "I want to know the city's voice, but it echoes like Philadelphia."
},
{
"chunk": 2,
"story": 0,
"summary": "I must find the unique voice of New York, proving it doesn't treat me like a cigar.",
"model_output": "I must find the unique voice of New York, proving it doesn't treat me like a cigar."
},
{
"chunk": 3,
"story": 0,
"summary": "I want to know the city's cry, but the cop only follows orders.",
"model_output": "I want to know the city's cry, but the cop only follows orders."
},
{
"chunk": 4,
"story": 0,
"summary": "Bill, find the essence of New York's chaos and harmony.",
"model_output": "Bill, find the essence of New York's chaos and harmony."
},
{
"chunk": 5,
"story": 0,
"summary": "The poet seeks a hit with his poem, fearing rejection.",
"model_output": "The poet seeks a hit with his poem, fearing rejection."
},
{
"chunk": 6,
"story": 0,
"summary": "Aurelia waits alone, wanting her companion to speak.",
"model_output": "Aurelia waits alone, wanting her companion to speak."
},
{
"chunk": 7,
"story": 1,
"summary": "John Hopkins finds deepest life joys in a hidden quarter, not wealth.",
"model_output": "John Hopkins finds deepest life joys in a hidden quarter, not wealth."
},
{
"chunk": 8,
"story": 1,
"summary": "Mrs. Hopkins struggles to balance her desires and fears in the fast-paced city.",
"model_output": "Mrs. Hopkins struggles to balance her desires and fears in the fast-paced city."
},
{
"chunk": 9,
"story": 1,
"summary": "John Hopkins sought to inject conversation into his tasteless existence by buying a cigar.",
"model_output": "John Hopkins sought to inject conversation into his tasteless existence by buying a cigar."
},
{
"chunk": 10,
"story": 1,
"summary": "Hopkins buys spinach, finds no cash, challenges tobacconist Freshmayer.",
"model_output": "Hopkins buys spinach, finds no cash, challenges tobacconist Freshmayer."
},
{
"chunk": 11,
"story": 1,
"summary": "John Hopkins sought revenge, but found himself pursued by law enforcers and a mysterious chauffeur in a racing car.",
"model_output": "John Hopkins sought revenge, but found himself pursued by law enforcers and a mysterious chauffeur in a racing car."
},
{
"chunk": 12,
"story": 1,
"summary": "Hopkins wants Milady's assistance against unseen attackers, while Milady seeks a brave companion.",
"model_output": "Hopkins wants Milady's assistance against unseen attackers, while Milady seeks a brave companion."
},
{
"chunk": 13,
"story": 1,
"summary": "John Hopkins seeks a romantic encounter, only to be kidnapped by a mysterious lady.",
"model_output": "John Hopkins seeks a romantic encounter, only to be kidnapped by a mysterious lady."
},
{
"chunk": 14,
"story": 1,
"summary": "Bess wants her St. Bernard, but John wants her to stop reading dangerous novels.",
"model_output": "Bess wants her St. Bernard, but John wants her to stop reading dangerous novels."
},
{
"chunk": 15,
"story": 2,
"summary": "Masie, a young saleslady, seeks to navigate the complexities of human nature while hiding her true identity.",
"model_output": "Masie, a young saleslady, seeks to navigate the complexities of human nature while hiding her true identity."
},
{
"chunk": 16,
"story": 2,
"summary": "The shopgirl Masie confronts the floorwalker Irving Carter with her flirtatious gaze, testing his resolve.",
"model_output": "The shopgirl Masie confronts the floorwalker Irving Carter with her flirtatious gaze, testing his resolve."
},
{
"chunk": 17,
"story": 2,
"summary": "Irving Carter desires to charm a shopgirl despite feeling out of place.",
"model_output": "Irving Carter desires to charm a shopgirl despite feeling out of place."
},
{
"chunk": 18,
"story": 2,
"summary": "Willie nervously asked for her company, hoping she'd agree to a date.",
"model_output": "Willie nervously asked for her company, hoping she'd agree to a date."
},
{
"chunk": 19,
"story": 2,
"summary": "Carter seeks love but fears rejection, while Masie uses him to prove her worth.",
"model_output": "Carter seeks love but fears rejection, while Masie uses him to prove her worth."
},
{
"chunk": 20,
"story": 2,
"summary": "Masie faces Carter's marriage proposal, wanting to see through his sincerity.",
"model_output": "Masie faces Carter's marriage proposal, wanting to see through his sincerity."
},
{
"chunk": 21,
"story": 2,
"summary": "Masie looked up at Carter, her eyes alight with a flicker of possibility.",
"model_output": "Masie looked up at Carter, her eyes alight with a flicker of possibility."
},
{
"chunk": 22,
"story": 2,
"summary": "She wants to end their secret affair, but he feels a hidden joy.",
"model_output": "She wants to end their secret affair, but he feels a hidden joy."
},
{
"chunk": 23,
"story": 3,
"summary": "Big Jim Dougherty sought acceptance in Manhattan's titled nobility of sportdom.",
"model_output": "Big Jim Dougherty sought acceptance in Manhattan's titled nobility of sportdom."
},
{
"chunk": 24,
"story": 3,
"summary": "He seeks a private life away from his public Oriental sport, yet feels trapped in public life.",
"model_output": "He seeks a private life away from his public Oriental sport, yet feels trapped in public life."
},
{
"chunk": 25,
"story": 3,
"summary": "Big Jim\" expected her to ask for a dinner date, not insist on one.",
"model_output": "\"Big Jim\" expected her to ask for a dinner date, not insist on one."
},
{
"chunk": 26,
"story": 3,
"summary": "Big Jim Dougherty wanted to hide his wife's presence, but a mysterious bird-like being at her side made him change plans.",
"model_output": "Big Jim Dougherty wanted to hide his wife's presence, but a mysterious bird-like being at her side made him change plans."
},
{
"chunk": 27,
"story": 3,
"summary": "Big Jim seeks to impress his wife at a fancy restaurant, hoping her beauty and charm will win him praise.",
"model_output": "Big Jim seeks to impress his wife at a fancy restaurant, hoping her beauty and charm will win him praise."
},
{
"chunk": 28,
"story": 3,
"summary": "The honorable Patrick Corrigan seeks to expose a secret and win Mrs. Dougherty's heart, while \"Big Jim\" struggles to keep up.",
"model_output": "The honorable Patrick Corrigan seeks to expose a secret and win Mrs. Dougherty's heart, while \"Big Jim\" struggles to keep up."
},
{
"chunk": 29,
"story": 3,
"summary": "Patrick seeks to impress Mrs. Dougherty with a bold gesture, but is thwarted by her quicker action.",
"model_output": "Patrick seeks to impress Mrs. Dougherty with a bold gesture, but is thwarted by her quicker action."
},
{
"chunk": 30,
"story": 3,
"summary": "The bride desires her husband's affection, yet fears losing him to distant admirers.",
"model_output": "The bride desires her husband's affection, yet fears losing him to distant admirers."
},
{
"chunk": 31,
"story": 3,
"summary": "The Kid seeks a peach but finds oranges instead, rushing to a fruiterer just as the lights go out.",
"model_output": "The Kid seeks a peach but finds oranges instead, rushing to a fruiterer just as the lights go out."
},
{
"chunk": 32,
"story": 3,
"summary": "Kid seeks to infiltrate Denver Dick's gambling den but finds it closed, needing help from the captain.",
"model_output": "Kid seeks to infiltrate Denver Dick's gambling den but finds it closed, needing help from the captain."
},
{
"chunk": 33,
"story": 3,
"summary": "The captain seeks to lead a raid, but doubts success as the Kid's plan unfolds.",
"model_output": "The captain seeks to lead a raid, but doubts success as the Kid's plan unfolds."
},
{
"chunk": 34,
"story": 3,
"summary": "The Kid seeks a last peach at a lavish dinner, fearing theft, while authorities arrive.",
"model_output": "The Kid seeks a last peach at a lavish dinner, fearing theft, while authorities arrive."
},
{
"chunk": 35,
"story": 3,
"summary": "The Kid seeks to please his bride by delivering a requested peach, despite its trivial nature.",
"model_output": "The Kid seeks to please his bride by delivering a requested peach, despite its trivial nature."
},
{
"chunk": 36,
"story": 4,
"summary": "Stuck in stone walls, city man craves nature's first signs to confirm spring's arrival.",
"model_output": "Stuck in stone walls, city man craves nature's first signs to confirm spring's arrival."
},
{
"chunk": 37,
"story": 4,
"summary": "Mr. Peters wants the dollar his wife has, but fears losing face among friends.",
"model_output": "Mr. Peters wants the dollar his wife has, but fears losing face among friends."
},
{
"chunk": 38,
"story": 4,
"summary": "Maguire wants the dollar, but Mr. Peters plans to get the case note instead.",
"model_output": "Maguire wants the dollar, but Mr. Peters plans to get the case note instead."
},
{
"chunk": 39,
"story": 4,
"summary": "Mr. Peters schemes to charm Mrs. Peters away from a tea job, while she resists with a tough stance.",
"model_output": "Mr. Peters schemes to charm Mrs. Peters away from a tea job, while she resists with a tough stance."
}
]
}
+115
View File
@@ -0,0 +1,115 @@
"""Side-by-side page for reading the predicament pilot: the scene, its summary, and what each model made of it.
python3 strip_names.py pilot-predicaments-3b.json pilot-3b-clean.json
python3 strip_names.py pilot-predicaments-14b.json pilot-14b-clean.json
python3 pilot_page.py pilot-3b-clean.json pilot-14b-clean.json > pilot-compare.html
Takes strip_names.py's output, so the page shows what the full run would actually embed. (strip_names
runs as a script on import, so it is not imported here.) Job, relationship and place words are
highlighted. A pick per scene is kept in the browser only; the tally at the top is what to report
back. No inference.
"""
import collections, html, json, pathlib, re, sys
from topic_words import TOPIC_WORDS
def clean(text):
return text # inputs are already name-stripped
a_path, b_path = sys.argv[1], sys.argv[2]
cols = [json.loads(pathlib.Path(p).read_text(encoding='utf-8')) for p in (a_path, b_path)]
labels = [c['model'] for c in cols]
chunks = json.loads(pathlib.Path('chunks.json').read_text(encoding='utf-8'))
summary = {x['chunk']: x['summary'] for x in
json.loads(pathlib.Path('summaries_clean.json').read_text(encoding='utf-8'))['summaries']}
def marked(text):
out = []
for part in re.split(r"([A-Za-zé]+)", text):
esc = html.escape(part)
out.append(f'<mark>{esc}</mark>' if part.lower() in TOPIC_WORDS else esc)
return ''.join(out)
def leaks(text):
return len(set(re.findall(r"[a-zé]+", text.lower())) & TOPIC_WORDS)
def opening(text):
return ' '.join(text.split()[:3])
rows, stats = [], []
for col in cols:
texts = [clean(x['summary']) for x in col['summaries']]
stats.append({
'leaky': sum(leaks(t) > 0 for t in texts),
'city': sum('city' in t.lower() for t in texts),
'words': sum(len(t.split()) for t in texts) / len(texts),
'openings': collections.Counter(opening(t) for t in texts).most_common(3),
})
n = min(len(c['summaries']) for c in cols)
for i in range(n):
x = cols[0]['summaries'][i]
ch = chunks[x['chunk']]
cells = ''.join(
f'<div class="pred"><span class="who">{html.escape(labels[j])}</span>'
f'<p>{marked(clean(cols[j]["summaries"][i]["summary"]))}</p>'
f'<label><input type="radio" name="p{i}" value="{j}"> better</label></div>'
for j in range(2))
rows.append(f'''<section data-i="{i}">
<header><span class="n">{i + 1}</span> <b>{html.escape(ch["title"])}</b></header>
<details><summary>Scene summary: {marked(summary.get(x["chunk"], ""))}</summary>
<p class="scene">{html.escape(ch["text"][:1200])}{"…" if len(ch["text"]) > 1200 else ""}</p></details>
<div class="pair">{cells}</div>
<label class="both"><input type="radio" name="p{i}" value="n"> neither is a predicament</label>
</section>''')
stat_rows = ''.join(
f'<tr><td>{html.escape(labels[j])}</td><td>{s["leaky"]} of {n}</td><td>{s["city"]}</td>'
f'<td>{s["words"]:.0f}</td><td>{"; ".join(f"“{html.escape(o)}…” ×{c}" for o, c in s["openings"])}</td></tr>'
for j, s in enumerate(stats))
print(f'''<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Predicament pilot</title>
<style>
:root{{--bg:#f7f6f2;--card:#fff;--ink:#222;--dim:#666;--line:#ddd;--mark:#ffe08a;--pick:#e3f1e6}}
@media (prefers-color-scheme:dark){{:root{{--bg:#1b1b1d;--card:#252528;--ink:#e6e6e6;--dim:#999;--line:#3a3a3e;--mark:#6b5415;--pick:#23402b}}}}
body{{background:var(--bg);color:var(--ink);font:15px/1.5 system-ui,sans-serif;margin:0;padding:24px 16px}}
main{{max-width:980px;margin:auto}} h1{{margin:0 0 4px}} .dim{{color:var(--dim)}}
table{{border-collapse:collapse;margin:12px 0;font-size:14px}} td,th{{border-bottom:1px solid var(--line);padding:4px 10px;text-align:left}}
.wrap{{overflow-x:auto}}
#tally{{position:sticky;top:0;background:var(--bg);padding:8px 0;border-bottom:1px solid var(--line);z-index:1}}
section{{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:12px 14px;margin:14px 0}}
header .n{{color:var(--dim);margin-right:4px}} summary{{cursor:pointer;color:var(--dim);margin:6px 0}}
.scene{{white-space:pre-wrap;font-size:13px;color:var(--dim)}}
.pair{{display:grid;grid-template-columns:1fr 1fr;gap:10px}} @media (max-width:640px){{.pair{{grid-template-columns:1fr}}}}
.pred{{border:1px solid var(--line);border-radius:6px;padding:8px 10px}} .pred p{{margin:4px 0 8px}}
.pred:has(input:checked){{background:var(--pick)}} .who{{font-size:12px;color:var(--dim)}}
mark{{background:var(--mark);color:inherit;border-radius:2px}} .both{{display:block;margin-top:8px;font-size:13px;color:var(--dim)}}
</style>
<main>
<h1>Predicament pilot</h1>
<p class="dim">The first {n} scenes, each re-described by two models as the main person's predicament. Names are
already removed as the full run would. <mark>Highlighted</mark> words are jobs, relationships and places, the
words the old groups formed around. Pick the better sentence in each scene, or "neither". The column order
is fixed: {html.escape(labels[0])} on the left.</p>
<p class="dim">What to look for: does the sentence say what the person is up against and what they want? Could
it happen to anyone, anywhere? Would two scenes with the same predicament end up with similar sentences?</p>
<div class="wrap"><table><tr><th>Model</th><th>Has a highlighted word</th><th>Says "city"</th><th>Words, mean</th><th>Most common openings</th></tr>{stat_rows}</table></div>
<div id="tally"></div>
{"".join(rows)}
</main>
<script>
const KEY='stp-pilot:'+{json.dumps(labels)}.join('|'), labels={json.dumps(labels)};
let saved={{}}; try{{saved=JSON.parse(localStorage.getItem(KEY)||'{{}}')}}catch(e){{}}
for(const [k,v] of Object.entries(saved)){{const el=document.querySelector(`input[name="${{k}}"][value="${{v}}"]`); if(el) el.checked=true}}
function tally(){{
const c={{0:0,1:0,n:0}}; let done=0;
document.querySelectorAll('input[type=radio]:checked').forEach(r=>{{c[r.value]++;done++}});
document.getElementById('tally').textContent=`Picked ${{done}} of {n}: ${{labels[0]}} ${{c[0]}}, ${{labels[1]}} ${{c[1]}}, neither ${{c.n}}`;
}}
document.addEventListener('change',e=>{{if(e.target.type!=='radio')return; saved[e.target.name]=e.target.value;
try{{localStorage.setItem(KEY,JSON.stringify(saved))}}catch(err){{}} tally()}});
tally();
</script>''')
+731
View File
@@ -0,0 +1,731 @@
{
"R001": {
"members": [
57,
58,
61,
63,
75,
279,
281,
326,
358,
359,
364,
435,
640
],
"methods": [
"B",
"M1"
]
},
"R002": {
"members": [
5,
78,
107,
128,
130,
131,
153,
178,
247,
277,
334,
335,
342,
344,
345,
420,
443,
445,
446,
447,
449,
515,
519,
555,
561,
613,
775,
818,
824,
832
],
"methods": [
"M3"
]
},
"R003": {
"members": [
22,
41,
76,
80,
98,
128,
130,
131,
161,
163,
200,
261,
340,
357,
363,
397,
407,
448,
478,
498,
519,
599,
662,
663,
693,
701,
708,
709,
769,
788
],
"methods": [
"M3"
]
},
"R004": {
"members": [
3,
101,
130,
131,
243,
272,
407,
408,
409,
411,
444,
476,
510,
513,
515,
597,
599,
600,
662,
693,
694,
695,
696,
698,
701,
762,
771,
830
],
"methods": [
"M3"
]
},
"R005": {
"members": [
52,
126,
144,
160,
166,
169,
208,
257,
258,
303,
356,
431,
480,
517,
573,
588,
606,
608,
648,
651,
658,
691,
706,
730,
750,
751,
805,
807,
809,
816
],
"methods": [
"M3"
]
},
"R006": {
"members": [
38,
41,
66,
69,
121,
127,
131,
149,
163,
187,
198,
227,
283,
293,
322,
391,
475,
483,
488,
526,
536,
550,
590,
614,
721,
756,
757,
758,
759,
760,
761,
762,
763,
764,
786,
804
],
"methods": [
"B",
"M1",
"M3"
]
},
"R007": {
"members": [
39,
62,
93,
125,
148,
172,
175,
178,
199,
201,
215,
222,
224,
251,
254,
264,
265,
267,
306,
323,
328,
352,
361,
367,
479,
490,
500,
511,
521,
533,
534,
543,
589,
662,
680,
799,
803,
823
],
"methods": [
"B",
"M1",
"M3"
]
},
"R008": {
"members": [
17,
19,
20,
29,
45,
46,
49,
60,
108,
159,
170,
178,
185,
221,
278,
333,
342,
343,
369,
372,
522,
538,
579,
586,
587,
611,
635,
639,
692,
799
],
"methods": [
"M3"
]
},
"R009": {
"members": [
42,
57,
58,
59,
61,
63,
75,
76,
175,
265,
267,
275,
279,
281,
326,
352,
358,
359,
364,
435,
479,
500,
519,
540,
543,
640,
662,
680,
739,
799
],
"methods": [
"M3"
]
},
"R010": {
"members": [
18,
25,
26,
55,
77,
107,
126,
167,
186,
285,
289,
392,
403,
419,
421,
424,
427,
482,
492,
493,
506,
532,
569,
596,
604,
605,
606,
608,
627,
651,
690,
691,
704,
705,
755,
801,
802
],
"methods": [
"M3"
]
},
"R011": {
"members": [
20,
65,
125,
135,
198,
221,
381,
459,
477,
488,
639,
642,
692,
799
],
"methods": [
"M2"
]
},
"R012": {
"members": [
27,
47,
144,
168,
216,
308,
311,
355,
416,
422,
559,
619,
648,
649,
660,
665,
675,
774,
805,
808,
809,
836
],
"methods": [
"B",
"M1"
]
},
"R013": {
"members": [
13,
22,
41,
95,
106,
115,
117,
119,
128,
129,
130,
131,
163,
227,
231,
271,
336,
451,
464,
519,
599,
662,
663,
693,
701,
710,
740,
762,
765,
790
],
"methods": [
"M3"
]
},
"R014": {
"members": [
29,
150,
229,
252,
272,
275,
276,
309,
324,
349,
355,
398,
399,
450,
504,
631,
656,
657,
659,
661,
662,
693,
695,
728,
731,
749,
754,
768,
772,
825
],
"methods": [
"M3"
]
},
"R015": {
"members": [
41,
72,
106,
131,
148,
149,
205,
293,
301,
302,
304,
305,
411,
590,
663
],
"methods": [
"M2"
]
},
"R016": {
"members": [
7,
8,
17,
21,
124,
171,
282,
414,
547,
554,
570,
602,
638,
641,
727,
746,
834
],
"methods": [
"M3"
]
},
"R017": {
"members": [
10,
12,
59,
62,
150,
175,
244,
245,
252,
275,
276,
312,
324,
352,
355,
374,
378,
415,
422,
479,
520,
543,
662,
680,
684,
717,
739,
825,
826,
833
],
"methods": [
"M3"
]
},
"R018": {
"members": [
13,
16,
20,
28,
29,
44,
65,
67,
87,
91,
92,
94,
135,
136,
141,
146,
169,
170,
250,
266,
332,
366,
368,
377,
494,
495,
509,
573,
607,
628,
629,
630,
636,
692,
748,
785,
796,
817,
819
],
"methods": [
"M3"
]
},
"R019": {
"members": [
27,
47,
144,
159,
168,
216,
275,
308,
311,
355,
416,
422,
520,
559,
605,
606,
608,
619,
648,
649,
651,
660,
665,
675,
691,
774,
805,
808,
809,
836
],
"methods": [
"M3"
]
},
"R020": {
"members": [
12,
38,
62,
121,
306,
324,
354,
375,
410,
448,
519,
615,
662,
680,
703,
739,
823
],
"methods": [
"M2"
]
},
"R021": {
"members": [
20,
21,
22,
29,
53,
54,
60,
173,
174,
175,
178,
230,
367,
406,
479,
489,
491,
540,
608,
634,
639,
643,
644,
662,
692,
710,
734,
739,
792,
799
],
"methods": [
"M3"
]
}
}
@@ -0,0 +1,42 @@
{
"B": {
"accepted": 4,
"precision1plus": 1.0,
"precision2": 0.0,
"label2": 0,
"distinct_situations": 4,
"repeated_names": 0,
"coverage": 0.13007159904534607,
"scenes_in_several": 0
},
"M1": {
"accepted": 4,
"precision1plus": 1.0,
"precision2": 0.0,
"label2": 0,
"distinct_situations": 4,
"repeated_names": 0,
"coverage": 0.13007159904534607,
"scenes_in_several": 0
},
"M2": {
"accepted": 3,
"precision1plus": 1.0,
"precision2": 1.0,
"label2": 3,
"distinct_situations": 3,
"repeated_names": 0,
"coverage": 0.05489260143198091,
"scenes_in_several": 0
},
"M3": {
"accepted": 16,
"precision1plus": 0.6875,
"precision2": 0.0,
"label2": 0,
"distinct_situations": 10,
"repeated_names": 1,
"coverage": 0.45465393794749404,
"scenes_in_several": 68
}
}
+10
View File
@@ -0,0 +1,10 @@
seed 13: 42 candidates; B accepts 4
M1: pool 729, k=36; accepts 4 (0 new)
M2: accepts 3
M3: accepts 16 (12 recovered by pruning); scenes in >1 cluster: 68
blind sheet: 21 distinct clusters, 576 lines
real 0m54.699s
user 0m39.193s
sys 0m14.866s
exit 0
+170
View File
@@ -0,0 +1,170 @@
"""Recall recovery: baseline gate and three methods on the seed-13 partition (SELECTION.md).
python3 recover.py
Writes:
recover-map.json sheet id -> member chunks and which methods produced it (do not read before labelling)
sheet-recover.txt blind sheet: member summaries only, shuffled
labels-recover.json empty label template (only if absent)
Prints counts per method only.
"""
import collections, json, math, pathlib, random, re, sys
SEED, K = 13, 60
MIN_GATE_SIZE, GROW_CAP = 13, 30
sys.argv = ['measure.py', 'summary_embeddings.json']
exec(pathlib.Path('measure.py').read_text(encoding='utf-8').split("print(f'{path.name}")[0])
# defined: chunks, V, n, dim, story, dot, norm, kmeans (on V), describe, MIN_SIZE, MIN_STORIES, MAX_DOMINANT
frozen = json.loads(pathlib.Path('rule-frozen.json').read_text())
CUT = {s: t for s, _, t in frozen['terms']}
S1_CUT, S6_CUT = CUT['S1'], CUT['S6']
summaries = json.loads(pathlib.Path('summaries_clean.json').read_text(encoding='utf-8'))['summaries']
text = {x['chunk']: x['summary'] for x in summaries}
# Identical to signals.py.
STOP = set("""a an the and or but of to in on at for with by from as into over under about after before
while during than then that this these those it its his her hers him he she they them their theirs we our
you your i me my is are was were be been being has have had do does did not no nor so such who whom whose
which what when where why how all any each both either neither one two some other others another more most
less least very just only also even yet still now up down out off away back again once own same too can
could will would shall should may might must upon despite toward towards between among amid amidst
someone someone's someone’s""".split())
docs = [set(re.findall(r"[a-z][a-z'’]+", text[i].lower())) - STOP for i in range(n)]
df = collections.Counter(w for d in docs for w in d)
def s1(m):
tot, cnt = 0.0, 0
for x in range(len(m)):
for y in range(x + 1, len(m)):
if story[m[x]] != story[m[y]]:
tot += dot(V[m[x]], V[m[y]]); cnt += 1
return tot / cnt if cnt else 0.0
def s6(m):
size = len(m)
cw = collections.Counter(w for i in m for w in docs[i])
best = None
for w, c in cw.items():
if c < 2: continue
score = math.log((c + .5) / (size - c + .5)) - math.log((df[w] + .5) / (n - df[w] + .5))
if best is None or score > best[0]: best = (score, c)
return best[1] / size if best else 0.0
def eligible(m):
size, nst, dom, _ = describe(m)
return size >= MIN_SIZE and nst >= MIN_STORIES and dom <= MAX_DOMINANT
def passes(m):
return eligible(m) and s1(m) >= S1_CUT and s6(m) <= S6_CUT
# Guard against drift from signals.py: recompute S1/S6 for the dev candidates.
dev_sig = json.loads(pathlib.Path('signals-dev.json').read_text())
dev_cand = json.loads(pathlib.Path('candidates-dev.json').read_text())
for cid, m in dev_cand.items():
assert abs(s1(m) - dev_sig[cid]['S1']) < 1e-3 and abs(s6(m) - dev_sig[cid]['S6']) < 1e-3, cid
def kmeans_on(vecs, k, seed, iters=15):
"""Same algorithm as measure.kmeans, over arbitrary unit vectors; returns index groups."""
m, d = len(vecs), len(vecs[0])
rnd = random.Random(seed)
cent = [vecs[i] for i in rnd.sample(range(m), k)]
assign = [-1] * m
for _ in range(iters):
moved = 0
for i, v in enumerate(vecs):
best, bs = 0, -2.0
for j, c in enumerate(cent):
s = sum(a * b for a, b in zip(v, c))
if s > bs: bs, best = s, j
if assign[i] != best: assign[i] = best; moved += 1
if moved == 0: break
groups = collections.defaultdict(list)
for i, j in enumerate(assign): groups[j].append(i)
for j, mem in groups.items():
cent[j] = norm([sum(vecs[i][x] for i in mem) / len(mem) for x in range(d)])
groups = collections.defaultdict(list)
for i, j in enumerate(assign): groups[j].append(i)
return list(groups.values())
def centroid(m):
return norm([sum(V[i][x] for i in m) / len(m) for x in range(dim)])
# Baseline
partition = [sorted(c) for c in kmeans(K, SEED)]
candidates = [c for c in partition if eligible(c)]
B = [c for c in candidates if passes(c)]
print(f'seed {SEED}: {len(candidates)} candidates; B accepts {len(B)}', flush=True)
# M1: re-cluster everything outside B's accepted clusters
in_B = {i for c in B for i in c}
pool = [i for i in range(n) if i not in in_B]
k1 = max(2, round(len(pool) / 20))
M1 = [list(c) for c in B]
for g in kmeans_on([V[i] for i in pool], k1, SEED):
m = sorted(pool[x] for x in g)
if passes(m): M1.append(m)
print(f'M1: pool {len(pool)}, k={k1}; accepts {len(M1)} ({len(M1) - len(B)} new)', flush=True)
# M2: consensus clustering over co-association rows
co = json.loads(pathlib.Path('coassoc.json').read_text())
rows = [norm([float(x) for x in co[i]]) for i in range(n)]
M2 = [sorted(g) for g in kmeans_on(rows, K, SEED) if passes(sorted(g))]
print(f'M2: accepts {len(M2)}', flush=True)
# M3: prune failing candidates to a passing core, then grow every passing cluster
def prune(m):
m = list(m)
while not passes(m):
if len(m) <= MIN_GATE_SIZE: return None
def cross_mean(x):
others = [y for y in m if story[y] != story[x]]
return sum(dot(V[x], V[y]) for y in others) / len(others) if others else -1.0
m.remove(min(m, key=cross_mean))
return m
def grow(m):
m = list(m)
while len(m) < GROW_CAP:
cen = centroid(m)
members = set(m)
j = max((i for i in range(n) if i not in members), key=lambda i: dot(V[i], cen))
if not passes(m + [j]): break
m.append(j)
return sorted(m)
M3, pruned_ok = [], 0
for c in candidates:
core = c if passes(c) else prune(c)
if core is None: continue
if core is not c: pruned_ok += 1
M3.append(grow(core))
cover3 = collections.Counter(i for c in M3 for i in c)
print(f'M3: accepts {len(M3)} ({pruned_ok} recovered by pruning); scenes in >1 cluster: {sum(v > 1 for v in cover3.values())}', flush=True)
# Pool for blind labelling
methods = {'B': B, 'M1': M1, 'M2': M2, 'M3': M3}
union = {}
for name, clusters in methods.items():
for c in clusters:
union.setdefault(tuple(sorted(c)), []).append(name)
keys = list(union)
rnd = random.Random(1307)
rnd.shuffle(keys)
mapping = {f'R{i + 1:03d}': {'members': list(k), 'methods': union[k]} for i, k in enumerate(keys)}
pathlib.Path('recover-map.json').write_text(json.dumps(mapping, indent=1))
lines = []
for rid, entry in mapping.items():
shown = entry['members'][:]
rnd.shuffle(shown)
lines.append(f'=== {rid}')
lines.extend(f' - {text[i]}' for i in shown)
lines.append('')
pathlib.Path('sheet-recover.txt').write_text('\n'.join(lines), encoding='utf-8')
lab = pathlib.Path('labels-recover.json')
if not lab.exists():
lab.write_text(json.dumps({rid: {'label': None, 'name': ''} for rid in mapping}, indent=1))
print(f'blind sheet: {len(mapping)} distinct clusters, {sum(len(e["members"]) for e in mapping.values())} lines')
+70
View File
@@ -0,0 +1,70 @@
"""Re-describe each scene as the predicament its main person is in, with nothing tying it to the story.
The first human review found the summary groups were topics (artists, police, courtship, hotels)
because summaries keep jobs and places, and those dominate the embeddings. The unit a pack needs is
a predicament: what someone is up against and what they want out of it (SELECTION.md, "Predicaments").
This rewrites the scene's own prose into that, leaving out whatever makes a topic.
STP_OLLAMA=http://host:11434 STP_CHAT_MODEL=qwen3:14b python3 redescribe.py --limit 40 --out pilot-predicaments-14b.json
STP_OLLAMA=http://host:11434 python3 redescribe.py # every scene -> predicaments.json
The output has the same layout as summaries.json ({"summaries": [{"chunk", "story", "summary"}]}),
so strip_names.py and embed.py read it unchanged. One request at a time; progress is saved after
every scene, so an interrupted run resumes.
"""
import argparse, json, os, pathlib, re, time
import ollama
from topic_words import TOPIC_WORDS
ap = argparse.ArgumentParser()
ap.add_argument('--limit', type=int, default=None)
ap.add_argument('--out', default='predicaments.json')
args = ap.parse_args()
MODEL = os.environ.get('STP_CHAT_MODEL', 'qwen2.5:3b-instruct')
# Fixed before any call (SELECTION.md). No example of the output vocabulary: the 3B model copied
# the examples it was given when summarising.
SYSTEM = """You describe the predicament of the main person in a passage of fiction, in ONE sentence.
A predicament is what that person is up against and what they want out of it: the kind of trouble or opportunity that could happen to anyone, in any line of work, in any town.
Rules:
- Exactly one sentence, at most 20 words.
- Leave out everything that ties it to this story: no names, no jobs or professions, no places, no businesses, and no objects that only belong to this story.
- Say what the person is up against, and what they want.
- Do not describe the writing, the mood, or who is telling the story.
- Output only the sentence."""
# A full stop after these is not the end of a sentence; nor is one after a single capital (an initial).
END = re.compile(r'(?<!\bMr)(?<!\bMrs)(?<!\bMs)(?<!\bDr)(?<!\bSt)(?<!\bNo)(?<![\s.][A-Z])[.!?](?:\s|$)')
def first_sentence(text):
text = re.sub(r'<think>.*?</think>', '', text, flags=re.S)
text = re.sub(r'\s+', ' ', text).strip().strip('"')
m = END.search(text)
return text[:m.end()].strip() if m else text
chunks = json.loads(pathlib.Path('chunks.json').read_text(encoding='utf-8'))
if args.limit:
chunks = chunks[:args.limit]
out = pathlib.Path(args.out)
state = json.loads(out.read_text()) if out.exists() else {'model': MODEL, 'prompt': SYSTEM, 'summaries': []}
if state['model'] != MODEL or state['prompt'] != SYSTEM:
raise SystemExit(f'{out} was made with a different model or prompt; choose another --out')
done = state['summaries']
print(f'{len(chunks)} scenes, resuming at {len(done)}, model {MODEL}', flush=True)
t0 = time.time()
for i in range(len(done), len(chunks)):
think = False if MODEL.startswith('qwen3') else None
text, _, _ = ollama.chat(MODEL, SYSTEM, chunks[i]['text'], num_ctx=2048, num_predict=80, think=think)
done.append({'chunk': i, 'story': chunks[i]['story'], 'summary': first_sentence(text), 'model_output': text})
out.write_text(json.dumps(state, indent=1, ensure_ascii=False), encoding='utf-8')
if (i + 1) % 10 == 0 or i + 1 == len(chunks):
print(f' {i + 1}/{len(chunks)} {time.time() - t0:.0f}s', flush=True)
leaks = [x for x in done if set(re.findall(r"[a-zé]+", x['summary'].lower())) & TOPIC_WORDS]
print(f'done: {len(done)} predicaments, {time.time() - t0:.0f}s this session; '
f'{len(leaks)} still mention a job, relationship or place word')
+100
View File
@@ -0,0 +1,100 @@
"""Build a review page: the groups gate G2 accepts, plus the nearest misses.
python3 review.py # k = 60, seed 24 -> review/k60-s24/
python3 review.py --k 60 --seed 31 --borderline 8
Writes into the run directory:
REVIEW.html open it in a browser: the whole review happens there
candidates.json what was found, for apply_review.py to check the saved review against
When the review is saved from the page:
python3 apply_review.py review/k60-s24 -> situations.json
No inference: this uses the summaries and embeddings already on disk.
"""
import argparse, datetime, hashlib, json, pathlib
import gate2 as g
import review_format as rf
import review_page
def gate_failures(sig, gate):
out = []
if gate.get('S1') is not None and sig['S1'] < gate['S1']:
out.append(f"cohesion S1 {sig['S1']:.3f}, needs {gate['S1']:.3f}")
if gate.get('Z1') is not None and sig['Z1'] < gate['Z1']:
out.append(f"cohesion against same-size random sets Z1 {sig['Z1']:.1f}, needs {gate['Z1']}")
if gate.get('MMIN') is not None and sig['MMIN'] < gate['MMIN']:
out.append(f"weakest scene MMIN {sig['MMIN']:.3f}, needs {gate['MMIN']:.3f}")
if gate.get('W') is not None and sig['W'] >= gate['W']:
out.append(f"the word \"{sig['W_word']}\" is in {sig['W']:.0%} of summaries, limit {gate['W']:.0%}")
return out
def shortfall(sig, gate):
"""How far a rejected group is from passing; smaller is closer."""
s = 0.0
if gate.get('S1') is not None: s += max(0.0, gate['S1'] - sig['S1']) / 0.01
if gate.get('Z1') is not None: s += max(0.0, gate['Z1'] - sig['Z1'])
if gate.get('W') is not None and sig['W'] >= gate['W']: s += 10 * (sig['W'] - gate['W'] + 0.01)
return s
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--k', type=int, default=60, help='number of k-means clusters (default 60)')
ap.add_argument('--seed', type=int, default=24, help='k-means seed (default 24)')
ap.add_argument('--borderline', type=int, default=8, help='near misses offered as optional extras (default 8)')
ap.add_argument('--out', help='run directory (default review/k<K>-s<SEED>)')
args = ap.parse_args()
run_id = f'k{args.k}-s{args.seed}'
out = pathlib.Path(args.out or f'review/{run_id}')
gate = json.loads(pathlib.Path('gate2-frozen.json').read_text())
found = [c for c in g.kmeans(args.k, args.seed) if g.eligible(c)]
scored = [(c, g.signals(c)) for c in found]
accepted = sorted([cs for cs in scored if g.g2_passes(cs[1], gate)], key=lambda cs: -cs[1]['S1'])
rejected = sorted([cs for cs in scored if not g.g2_passes(cs[1], gate)], key=lambda cs: shortfall(cs[1], gate))
borderline = rejected[:args.borderline]
listed = [(f'C{i + 1:02d}', 'accepted', c, s) for i, (c, s) in enumerate(accepted)]
listed += [(f'B{i + 1:02d}', 'borderline', c, s) for i, (c, s) in enumerate(borderline)]
candidates = {}
for cid, kind, members, sig in listed:
member_rows = [{'member': rf.member_id(cid, n + 1), 'chunk': chunk, 'story': g.chunks[chunk]['story'],
'title': g.chunks[chunk]['title'].title(), 'summary': g.text[chunk]}
for n, chunk in enumerate(sorted(members))]
candidates[cid] = {
'kind': kind,
'stats': {'scenes': sig['size'], 'stories': sig['stories'], 'largest_story_share': round(sig['dominant'], 3),
'S1': round(sig['S1'], 4), 'Z1': round(sig['Z1'], 2), 'W': round(sig['W'], 3), 'W_word': sig['W_word']},
'gate_failures': gate_failures(sig, gate),
'role_hints': review_page.role_hints(member_rows),
'members': member_rows,
}
# Ties a saved review to exactly these groups, so progress for a different set of groups
# that happens to share the run name is never loaded into this page.
fingerprint = hashlib.sha1(json.dumps({cid: [m['chunk'] for m in c['members']] for cid, c in candidates.items()},
sort_keys=True).encode()).hexdigest()[:12]
document = {
'schema_version': rf.SCHEMA_VERSION,
'run': {'id': run_id, 'k': args.k, 'seed': args.seed, 'fingerprint': fingerprint,
'created': datetime.datetime.now().astimezone().isoformat(timespec='seconds'),
'gate': {'name': 'G2', **gate}, 'scenes': g.n, 'stories': len(set(g.story)),
'candidates_found': len(found), 'accepted': len(accepted), 'borderline': len(borderline)},
'candidates': candidates,
}
out.mkdir(parents=True, exist_ok=True)
(out / 'candidates.json').write_text(json.dumps(document, indent=1, ensure_ascii=False), encoding='utf-8')
(out / 'REVIEW.html').write_text(review_page.render_page(document), encoding='utf-8')
print(f'{run_id}: {len(found)} candidate groups, G2 accepts {len(accepted)}, {len(borderline)} optional extras')
print(f'open this in a browser: {(out / "REVIEW.html").resolve()}')
print(f'when the review is saved: python3 apply_review.py {out}')
if __name__ == '__main__':
main()

Some files were not shown because too many files have changed in this diff Show More