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
This commit is contained in:
co-authored by
Claude Opus 5
parent
d84ae495f4
commit
fa3769d0fe
@@ -0,0 +1,67 @@
|
||||
"""Reduce each scene to one generic sentence about the SITUATION, not the story.
|
||||
|
||||
The 2026-09-10 probe found that prose embeddings cluster by story identity --
|
||||
names, setting, narrator voice -- rather than by what kind of situation a scene
|
||||
is. This strips exactly those signals before embedding.
|
||||
|
||||
python3 summarise.py --limit 40 # pilot: time it and read the output
|
||||
python3 summarise.py # everything, resuming from summaries.json
|
||||
|
||||
Model from STP_CHAT_MODEL (default qwen2.5:3b-instruct): the requirement is a
|
||||
small local model, so the prompt is written for one.
|
||||
"""
|
||||
import argparse, json, os, pathlib, re, time
|
||||
import ollama
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--limit', type=int, default=None)
|
||||
args = ap.parse_args()
|
||||
MODEL = os.environ.get('STP_CHAT_MODEL', 'qwen2.5:3b-instruct')
|
||||
|
||||
# v1 gave examples of roles ("a clerk", "a landlady", "a rich man", "a city
|
||||
# park"). A 3B model copied them into scenes that contain none of those people,
|
||||
# which would make unrelated scenes cluster together and fake the very
|
||||
# recurrence this probe measures. v2 names no example role at all.
|
||||
SYSTEM = """Describe the social situation in a passage of fiction in ONE sentence.
|
||||
|
||||
Rules:
|
||||
- Exactly one sentence, at most 25 words.
|
||||
- Never use anyone's name. Refer to each person only by the role the passage gives them: their job, their family tie, or their position.
|
||||
- Never name a place, business, street or city.
|
||||
- Say what is happening between the people: who wants what from whom, who holds power, what is at stake.
|
||||
- Do not describe the writing style, the narrator, or the mood.
|
||||
- Output only the sentence."""
|
||||
|
||||
# The v2 pilot also tried one retry with a reminder whenever a name slipped
|
||||
# through. It fired 35 times in 40, nearly doubled the run, and 27 summaries
|
||||
# still carried names -- so there is no retry. Names are removed afterwards by
|
||||
# strip_names.py, deterministically.
|
||||
|
||||
# A full stop after these is not the end of a sentence ("Mr. Peters", "St. Bernard").
|
||||
END = re.compile(r'(?<!\bMr)(?<!\bMrs)(?<!\bMs)(?<!\bDr)(?<!\bSt)(?<!\bNo)[.!?](?:\s|$)')
|
||||
|
||||
def first_sentence(text):
|
||||
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('summaries.json')
|
||||
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('summaries.json was made with a different model or prompt; move it aside first')
|
||||
done = state['summaries']
|
||||
print(f'{len(chunks)} scenes, resuming at {len(done)}, model {MODEL}', flush=True)
|
||||
|
||||
t0, gen_ns, pre_ns = time.time(), 0, 0
|
||||
for i in range(len(done), len(chunks)):
|
||||
text, g, p = ollama.chat(MODEL, SYSTEM, chunks[i]['text'])
|
||||
gen_ns += g; pre_ns += p
|
||||
done.append({'chunk': i, 'story': chunks[i]['story'],
|
||||
'summary': first_sentence(text), 'model_output': text})
|
||||
out.write_text(json.dumps(state, indent=1))
|
||||
if (i + 1) % 10 == 0 or i + 1 == len(chunks):
|
||||
el = time.time() - t0
|
||||
print(f' {i + 1}/{len(chunks)} {el:.0f}s prefill {pre_ns / 1e9:.0f}s gen {gen_ns / 1e9:.0f}s', flush=True)
|
||||
print(f'done: {len(done)} summaries, {time.time() - t0:.0f}s this session')
|
||||
Reference in New Issue
Block a user