"""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):,}')