Files
TheLadder/tools/story-to-pack/probe/maupassant/split_gutenberg.py
T
JesseMarkowitzandClaude Opus 5.5 3a4758172d story-to-pack: add the corpora and the catalogue v2/v3
Fourteen corpora split and classified: aesop, bierce, chekhov, holmes,
keefe, lawson, lorimer, maupassant, nobody, plaintales, poe, torchy,
wallingford and winesburg, each with its splitter and the hand-written
groups and pages; catalogue v2 and v3; and the shared splitters
gutenberg_chunks.py, se_split.py and se_build.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rwKTmug58sEsJ72AuWsEi
2026-10-07 06:05:51 -04:00

61 lines
3.2 KiB
Python

"""Split the Project Gutenberg plain text of Guy de Maupassant, *Original Short Stories, Volume IV*
(eBook #3080), into stories, then cut scenes with the same rule as ../chunk.py and
../wharton/split_gutenberg.py (paragraph boundaries, ~320 words, tail >= 80).
Deterministic: titles are the twelve contents entries, matched as whole upper-case lines after the
indented contents block. Two body headers differ from their contents entry ("THE TRIP OF LE HORLA",
"FAREWELL!"), so HEADS maps those. "PART I".."PART V" headings are dropped, as is the one bracketed
editorial note ("[This story appeared in ...]"); the Gutenberg licence after END is dropped."""
import json, pathlib, re
TITLES = ['The Moribund', 'The Gamekeeper', 'The Story of a Farm Girl', 'The Wreck',
"Theodule Sabot's Confession", 'The Wrong House', 'The Diamond Necklace',
'The Marquis de Fumerol', 'The Trip of the Horla', 'Farewell', 'The Wolf', 'The Inn']
HEADS = {'The Trip of the Horla': 'THE TRIP OF LE HORLA', 'Farewell': 'FAREWELL!'}
TARGET = 320
VOLUME = 'Original Short Stories, Volume IV (Guy de Maupassant, tr. 1903)'
here = pathlib.Path(__file__).parent
raw = (here / 'corpus/original-short-stories-vol4.txt').read_text(encoding='utf-8').replace('\r', '')
raw = raw[:raw.index('*** END OF THE PROJECT GUTENBERG')]
lines = raw.split('\n')
start = next(i for i, l in enumerate(lines) if l.strip() == 'VOLUME IV.')
heads = []
for t in TITLES:
h = HEADS.get(t, t.upper())
# Body headers sit flush left; the contents block is indented, so match the unstripped line.
hits = [i for i, l in enumerate(lines) if i > start and l.rstrip() == h]
assert len(hits) == 1, (t, hits)
heads.append(hits[0])
assert heads == sorted(heads)
stories = []
for n, (t, a) in enumerate(zip(TITLES, heads)):
b = heads[n + 1] if n + 1 < len(heads) else len(lines)
body = '\n'.join(lines[a + 1:b])
paras = []
for p in re.split(r'\n\s*\n', body):
p = re.sub(r'\s+', ' ', p).strip()
if not p or re.fullmatch(r'PART [IVX]+', p) or re.fullmatch(r'\[.*\]', p): continue
paras.append(p)
stories.append({'volume': VOLUME, 'title': t, '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})
(here / 'stories.json').write_text(json.dumps(stories, indent=1, ensure_ascii=False), encoding='utf-8')
(here / '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']:34} {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):,}')