# Story Intelligence Illustration Selection Technical Explanation

This document explains how Story Intelligence currently chooses illustrations, tracing the execution path from scene analysis through Story Memory, illustration matching, demand generation, snapshot construction, and browser rendering.

No code changes are described here as recommendations only; this is documentation of the current implementation.

## Complete Execution Path

```mermaid
flowchart TD
    A[PersistedStoryIntelligenceRunner] -->|Chapter prompt AI call| B[Chapter Structure JSON]
    B -->|Scene boundaries| C[Scene Intelligence prompt AI calls]
    C -->|ParsedJson saved| D[StoryIntelligenceSceneResults]
    D --> E[StoryIntelligenceVisualisationSnapshotService]
    E -->|Build scene states and Story Memory| F[Prototype ViewModel]
    F --> G[IllustrationLibraryService.ApplyApprovedIllustrationsAsync]
    G -->|Exact stable-code lookup| H[Starter/generated image if code matches]
    H --> I[StoryIntelligenceIllustrationMatchingService]
    I -->|Characters| J[Assignment revalidation and scoring]
    I -->|Locations/assets| K[Demand lookup/creation]
    J --> L[StoryIntelligenceCharacterIllustrationAssignments]
    K --> M[StoryIntelligenceIllustrationDemands and queued IllustrationLibraryItems]
    L --> N[Visualisation JSON]
    M --> N
    N --> O[Browser visualisation]
```

```mermaid
sequenceDiagram
    participant Scene as Scene Analysis
    participant Memory as Story Memory
    participant Match as Illustration Matcher
    participant Demand as Demand Generator
    participant Snap as Snapshot Builder
    participant UI as Visualisation

    Scene->>Scene: OpenAI chapter + scene prompts
    Scene->>Snap: Persisted scene result JSON
    Snap->>Memory: Replay analysed scenes in order
    Memory-->>Snap: Carried characters, relationships, POV
    Snap->>Match: Character/location/asset view model
    Match->>Match: Read library + previous assignments
    Match->>Demand: No compatible illustration
    Demand->>Demand: Upsert demand + queue planned image
    Match-->>Snap: Assigned/fallback image resolution
    Snap-->>UI: Snapshot JSON
    UI->>UI: Render imagePath/fallback
```

## Main Services and Responsibilities

### PersistedStoryIntelligenceRunner

Responsible for running Story Intelligence analysis and persisting results.

Input:

- Queued `StoryIntelligenceQueuedRun`
- Chapter source text
- Chapter Structure prompt
- Scene Intelligence prompt
- OpenAI model settings

Output:

- Persisted chapter structure result
- Persisted scene intelligence result
- Run progress/status

Database reads:

- Queued Story Intelligence run via `StoryIntelligenceRun_ClaimNextPending`
- Current run state for cancellation checks

Database writes:

- `StoryIntelligenceRuns`
- `StoryIntelligenceChapterResults`
- `StoryIntelligenceSceneResults`

AI calls:

- Chapter Structure prompt
- Scene Intelligence prompt per scene
- JSON repair retry prompt if parsing fails

Illustration role:

- None directly. It only creates the structured scene data later consumed by the visualisation and matcher.

### StoryIntelligenceVisualisationSnapshotService

Responsible for reconstructing visualisation state from persisted analysis.

Input:

- Import session ID or run ID
- Persisted run and scene-result records
- Parsed scene JSON

Output:

- `StoryIntelligenceExperiencePrototypeViewModel`
- Scene states with characters, location, assets, relationships, knowledge, activity, discoveries
- In-memory Story Memory

Database reads:

- `StoryIntelligenceBookPipelines`
- `StoryIntelligenceRuns`
- `StoryIntelligenceSceneResults`

Database writes:

- None directly.

Cached/derived values:

- `ChangeToken`
- Session change token
- Replay cursor
- In-memory `StoryMemory`
- Stable visual IDs
- Stable fallback library codes

AI calls:

- None.

### IllustrationLibraryService.ApplyApprovedIllustrationsAsync

Responsible for the first image pass, using exact stable-code lookup.

Input:

- Prototype view model with `LibraryCode` values on characters, locations and assets

Output:

- Updated image paths and `ImageResolution` objects where a matching library item exists

Database reads:

- `IllustrationLibraryItems`

Database writes:

- None.

Matching behaviour:

- Exact `StableCode` match only.
- Uses `Approved` before `Generated`.
- Then newest item first.

### StoryIntelligenceIllustrationMatchingService

Responsible for final character assignment, revalidation, scoring, and demand creation.

Input:

- Project ID
- Fully built visualisation prototype view model
- Existing illustration library
- Existing active character assignments

Output:

- Character image assignments
- Character assignment diagnostics
- Demand records when no suitable illustration exists
- Queued planned illustration items when demand threshold is reached

Database reads:

- `IllustrationLibraryItems`
- `StoryIntelligenceCharacterIllustrationAssignments`

Database writes:

- `StoryIntelligenceCharacterIllustrationAssignments`
- `StoryIntelligenceIllustrationDemands`
- `IllustrationLibraryItems` through planned/queued demand records

AI calls:

- None directly.
- It queues illustration generation. A background image-generation worker later calls OpenAI Images.

### IllustrationGenerationRunner and OpenAIIllustrationImageProvider

Responsible for generating queued illustration-library images.

Input:

- Queued `IllustrationLibraryItems`
- Prompt/specification JSON

Output:

- Stored generated image files
- Updated illustration-library item status

Database reads:

- `IllustrationLibraryItems`

Database writes:

- `IllustrationLibraryItems`

AI calls:

- `POST https://api.openai.com/v1/images/generations`

## Character Illustration Selection

Character matching is a two-layer process.

First, `StoryIntelligenceVisualisationSnapshotService` gives each character a likely starter `LibraryCode`. Then `IllustrationLibraryService.ApplyApprovedIllustrationsAsync` resolves exact library-code matches. After that, `StoryIntelligenceIllustrationMatchingService.ApplyCharacterIllustrationsAsync` performs the newer assignment and revalidation pass.

### Where Age Is Determined

Age is determined in two places.

Initial stable-code age:

- Class: `StoryIntelligenceVisualisationSnapshotService.CharacterProfile`
- Method: `DetectAgeBand`
- Used only to pick a starter code.
- Defaults to `YoungAdult`.

Final matcher age:

- Class: `StoryIntelligenceIllustrationCompatibility`
- Method: `InferCharacterEvidence`
- Produces a `CharacterEvidenceProfile`.
- Used by `StoryIntelligenceIllustrationMatchingService.Score`.

### Where Inferred Age Is Determined

Inferred age comes from:

- Explicit age terms: `13`, `14`, `15`, `16`, `17`, `18`, `19`
- Words: `toddler`, `baby`, `infant`, `child`, `schoolboy`, `schoolgirl`, `teenager`, `young adult`, `middle-aged`, `elderly`
- Role/title terms: `Mr`, `Mrs`, `Miss`, `Ms`, `Dr`, `Professor`, `teacher`, `detective`, `police officer`, `nurse`, `receptionist`, `solicitor`, `landlord`, `manager`
- Relationship terms: `mother`, `father`, `parent`, `aunt`, `uncle`, `grandmother`, `grandfather`

### Where Gender/Presentation Is Determined

Presentation is determined by `StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence`.

Sources, in priority order:

1. Title presentation
2. Entity relationship/presentation words in scoped text
3. Relationship evidence
4. Pronouns
5. Hard-coded name list
6. Unknown

### Whether Names Influence Gender

Yes. Names are hard-coded.

Examples:

- Feminine: Beth, Maggie, Rosie, Grace, Zoe, Rebecca, Annie, Mary, Sharon, Catherine, Susan, Helen, Elen
- Masculine: Adam, Colin, David, Kevin, Simon, Graham, Gareth, John, Michael, Peter, Richard, Rick, George

There is also a presentation guard that can override leaked relationship/pronoun evidence using name/title evidence unless explicit contradictory evidence appears.

### Whether Titles Influence Gender

Yes.

- `Mrs`, `Miss`, `Ms`, `Lady` infer feminine.
- `Mr`, `Sir` infer masculine.
- `Dr` and `Professor` infer adult age but not masculine/feminine presentation by themselves.

### Whether Occupations Influence Age

Yes. Occupations such as teacher, driving examiner, detective, police officer, nurse, receptionist, solicitor, landlord, manager, professor and doctor infer adult age.

### Hair Colour, Eye Colour and Ethnicity

Hair colour is extracted in `StoryIntelligenceIllustrationCompatibility.HairColour`.

Recognised values include:

- Bald
- Black
- Brown/brunette/dark hair
- Blonde/fair hair
- Red/auburn/ginger
- Grey/gray
- White

Hair length is also extracted.

Eye colour is not currently extracted for matching.

Ethnicity is not currently extracted for matching. The matcher uses skin tone, not ethnicity.

### Previously Learned Evidence

Previously learned Story Memory evidence can affect the character states that enter the matcher, but there is no persistent Story Memory evidence table read by the matcher.

Previous assignment evidence is stored as JSON in `StoryIntelligenceCharacterIllustrationAssignments`, but `ListAssignmentsAsync` currently returns `EvidenceJson` and `CandidateDiagnosticsJson` as null. That means previous evidence is not rehydrated and used as a true evidence source during matching.

### Conflict Resolution

Conflict handling is mostly rule-based:

- `FirstKnown` picks the first non-unknown signal from prioritised evidence.
- The presentation guard prevents obvious gender leakage from overriding strong name/title evidence.
- Hard rejections eliminate impossible illustration candidates.
- Existing assignments are revalidated against current evidence.
- Replacement requires hard failure, material wrongness, or a better score by margin.

### Hard Rules for Characters

A candidate is rejected if:

- Named character uses Unknown Figure artwork.
- Candidate is marked unsuitable for named-character matching.
- Age is incompatible.
- Strong presentation evidence contradicts candidate presentation.
- Known hair colour contradicts candidate hair colour.
- Candidate is already assigned to another significant named character.

Additional rule:

- If character age evidence is unknown, child/teen candidate artwork is rejected until explicit child/teen evidence appears.

### Character Weighted Scoring

Final matcher score in `StoryIntelligenceIllustrationMatchingService.Score`:

- Age exact match: `+25`
- Age evidence unknown: `+12`
- Candidate age unknown: `+8` plus rejection note
- Presentation exact match with strong evidence: `+42`
- Presentation exact match without strong evidence: `+25`
- Presentation evidence unknown: half weight
- Candidate presentation unknown: one-third weight plus rejection note
- Hair colour exact match: `+20`
- Hair colour evidence unknown: `+10`
- Candidate hair colour unknown: `+6` plus rejection note
- Skin tone exact match: `+20`
- Skin tone evidence unknown: `+10`
- Candidate skin tone unknown: `+6` plus rejection note
- Hair length exact match: `+8`
- Glasses exact match: `+7`
- Facial hair exact match: `+7`
- Approved library item: `+3`

Score is clamped between `0` and `100`.

Minimum suitable score:

- `48`

Reassignment margin:

- `24`

Older stable-code scoring before the final matcher:

- Age exact: `+80`
- Compatible age: `+30`
- Age mismatch: `-45`
- Presentation exact: `+24`
- Hair colour exact: `+18`
- Hair length exact: `+10`
- Signal keyword match: `+14` each

### Why Existing Illustrations Are Retained

An existing illustration is retained when:

- Existing assignment still resolves to a public image URL.
- Existing candidate has no hard rejections.
- Existing candidate is not materially wrong for known age, presentation, hair colour or skin tone.
- No better candidate exists, or the best candidate does not beat existing score by the reassignment margin.
- Existing score is at least `48`.

### Why Existing Illustrations Are Replaced

An existing illustration is replaced when:

- It violates hard constraints.
- It is materially wrong for age, presentation, hair colour or skin tone.
- A better compatible candidate beats the existing score by at least `24`.
- The existing assignment was fallback/null and a generated valid item is now available.

### When Demand Is Created

A character demand record is created when no hard-compatible unused illustration scores at least `48`.

Demand key:

```text
character|age|presentation|hairColour|skinTone
```

Writes:

- `StoryIntelligenceIllustrationDemands`

If request count reaches threshold, currently `1`, it creates or queues:

```text
char-demand-{hash}
```

### When Generated Illustrations Become Eligible

A generated illustration becomes eligible when:

- It exists in `IllustrationLibraryItems`
- Status is `Generated` or `Approved`
- It has a valid thumbnail or file path
- A later snapshot/matcher pass reads it from the library

### Why Duplicate Portraits Sometimes Occur

Duplicate portraits can occur because:

1. Exact stable-code resolution can apply the same starter code before final assignment.
2. Duplicate assignment cleanup only handles assignment records, not every transient fallback/stable-code image.
3. Low-significance or unresolved identities can share generic fallbacks.
4. Character identity canonicalisation can split one person into multiple keys.
5. Existing duplicate assignment handling keeps the first retained item and clears later duplicates only during the matcher pass.

### Why Unknown Figure Can Still Appear

Unknown Figure can still appear when:

- The initial stable-code phase chooses `char-unknown-figure`.
- The character is unresolved/generic, so named-character hard rejection does not apply.
- The matcher records demand and falls back to an existing SVG/fallback.
- The library contains generated/approved unknown figure art used by exact code before the final assignment overrides it.

## Location Illustration Selection

### Classification

Locations are built by `StoryIntelligenceVisualisationSnapshotService.BuildLocation`.

It chooses:

1. Present-in-scene locations before mentioned-only locations.
2. More specific names.
3. Higher confidence.
4. Fallback to scene setting.

### Semantic Typing

Semantic type is determined by `StoryIntelligenceIllustrationCompatibility.LocationType`.

Examples:

- Bathroom
- Kitchen
- Bedroom
- LivingRoom
- Hallway
- Stairwell
- Hospital
- Pub
- Beach
- RailwayStation
- Canal
- Garage
- HouseExterior
- Office
- School
- WaitingRoom
- Road
- CarPark
- Shop
- StreetFurniture
- FlatInterior
- UnknownInterior

### Matching

Location matching has two phases:

1. Initial stable-code mapping in `LocationLibraryCode`.
2. Demand-based exact generated lookup in `ApplyLocationDemandAsync`.

Important current behaviour:

- Bathroom, WaitingRoom and Road intentionally return no starter code.
- This prevents borrowing wrong generic interiors.
- Generated demand selection uses exact code `loc-demand-{hash}`.

There is a `LocationCompatible` helper, but generated supporting image selection currently uses exact demand stable-code lookup rather than weighted compatibility scoring.

### Demand Creation

Location demand is created if:

- Location type is not `UnknownInterior`.
- Current image is still fallback.
- No generated demand image exists for the exact demand code.

Demand key:

```text
location|type
```

Writes:

- `StoryIntelligenceIllustrationDemands`
- `IllustrationLibraryItems` planned/queued demand item

### Reuse Rules

Locations are reusable by type/demand key. They are not unique per manuscript location.

## Asset Illustration Selection

### Classification

Assets are built by `StoryIntelligenceVisualisationSnapshotService.BuildAssets`.

It:

- Excludes mentioned-only assets.
- Excludes low-value/background objects.
- Groups by cleaned asset name.
- Picks highest-confidence entry per name.
- Takes top five by asset weight.

### Semantic Typing

Asset type is determined by `StoryIntelligenceIllustrationCompatibility.AssetType`.

Examples:

- Ambulance
- Stretcher
- BodyBag
- Bottle
- Money
- Clothing
- Document
- Jewellery
- Weapon
- Hatchback
- FamilyCar
- SportsCar
- Van
- Car
- CarPart
- Tin
- Parcel
- Rucksack
- Suitcase
- Note
- Letter
- Notebook
- Book
- Telephone
- Keys
- Photograph
- UnknownObject

### Colour Handling

Colour is determined by `AssetColour`.

Recognised values:

- Blue
- Red
- Green
- Yellow
- Black
- White
- Grey
- Brown
- Unknown

### Matching

Asset matching has two phases:

1. Initial stable-code mapping in `AssetLibraryCode`.
2. Demand-based exact generated lookup in `ApplyAssetDemandAsync`.

Starter mappings include:

- Letter/Note/Document -> `asset-folded-letter`
- Notebook/Book -> `asset-worn-notebook`
- Car -> `asset-red-classic-car`
- Keys -> `asset-old-keys`
- Telephone -> `asset-phone-recorder`
- Photograph -> `asset-photo-print`
- Rucksack -> `asset-rucksack`

Generated demand selection uses exact stable code:

```text
asset-demand-{hash}
```

There is an `AssetCompatible` helper, but generated supporting-image selection currently uses exact demand code rather than weighted compatibility scoring.

### Demand Creation

Asset demand is created if:

- Asset type is not `UnknownObject`.
- Current image is fallback.
- No generated image exists for exact demand code.

Demand key:

```text
asset|type|colour
```

Writes:

- `StoryIntelligenceIllustrationDemands`
- `IllustrationLibraryItems` planned/queued demand item

## Database Table Summary

Reads:

- `StoryIntelligenceRuns`
- `StoryIntelligenceChapterResults`
- `StoryIntelligenceSceneResults`
- `StoryIntelligenceBookPipelines`
- `IllustrationLibraryItems`
- `StoryIntelligenceCharacterIllustrationAssignments`

Writes:

- `StoryIntelligenceRuns`
- `StoryIntelligenceChapterResults`
- `StoryIntelligenceSceneResults`
- `StoryIntelligenceCharacterIllustrationAssignments`
- `StoryIntelligenceIllustrationDemands`
- `IllustrationLibraryItems`

No dedicated persistent Story Memory table is currently used in the illustration matcher.

## Cached Values Used

The system uses these cached or derived values:

- Stored `ParsedJson` in scene results
- Stored illustration library metadata/specification JSON
- Stored generated file/thumbnail paths
- Stored active character assignments
- Stored demand keys/request counts
- Snapshot `ChangeToken`
- Replay cursor index
- In-memory Story Memory during snapshot construction
- Stable hashes for visual entity IDs and demand codes

## Previous Assignments Considered

The matcher considers active previous assignments from `StoryIntelligenceCharacterIllustrationAssignments`.

Used fields include:

- `CharacterKey`
- `IllustrationLibraryItemID`
- `StableCode`
- `MatchConfidence`
- `MatchingScore`
- `ReasonChosen`
- `ReasonReassigned`

Current limitation:

- Previous `EvidenceJson` and `CandidateDiagnosticsJson` are not returned by `ListAssignmentsAsync`; they are selected as null.

## Story Memory Evidence Considered

Story Memory can influence:

- Added remembered characters
- POV identity
- Character role/relevance
- Carried relationships
- Inferred kinship relationships
- Rough presentation and age band

Story Memory does not directly persist or supply a structured evidence object to the final matcher. It affects the visual character state, and the matcher then infers evidence from that state.

## Character Evidence Ignored

Currently ignored or mostly lost for matching:

- Eye colour
- Ethnicity
- Clothing details
- Body type
- Exact age beyond broad age band
- Many action-level descriptors
- Raw manuscript text
- Prior assignment evidence JSON
- Scene observations unless transformed into role/relevance/relationship text

## Five Biggest Weaknesses

1. Two image systems compete: stable-code starter matching and final assignment matching.
2. Story Memory is rebuilt in memory and is not a durable structured evidence store.
3. Character evidence is compressed before matching.
4. Locations and assets do not have a real weighted matcher.
5. Previous evidence is stored but not reloaded as evidence for conflict resolution.

## Five Places Evidence Can Be Lost

1. AI scene JSON may omit physical details.
2. Snapshot character state drops raw action and descriptor detail.
3. Story Memory stores only simplified role/relevance/age/presentation.
4. Matcher ignores eye colour, ethnicity, clothing, build and exact age.
5. Assignment repository does not return previous evidence JSON to the matcher.

## Five Places Incorrect Assignments Can Occur

1. Initial stable-code fallback picks a plausible but wrong starter image.
2. Hard-coded name gender lists misclassify unusual or ambiguous names.
3. Relationship/family terms leak onto the wrong character.
4. Asset/location keyword order maps vague text too broadly.
5. Canonical identity splitting creates multiple assignment keys for one person.

## Five Quickest Improvements

1. Store structured evidence per canonical identity and reload it during matching.
2. Remove or demote pre-matcher stable-code character assignment for live data.
3. Add weighted matchers for locations and assets.
4. Add eye colour, ethnicity/skin-tone source, clothing and exact-age evidence fields.
5. Expand diagnostics to show winning candidate, rejected candidates and exact evidence source per entity.

