venice-video-harness 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +598 -0
  4. package/dist/assembly/manifest-builder.d.ts +8 -0
  5. package/dist/assembly/manifest-builder.d.ts.map +1 -0
  6. package/dist/assembly/manifest-builder.js +196 -0
  7. package/dist/assembly/manifest-builder.js.map +1 -0
  8. package/dist/assembly/remotion-scaffold.d.ts +14 -0
  9. package/dist/assembly/remotion-scaffold.d.ts.map +1 -0
  10. package/dist/assembly/remotion-scaffold.js +292 -0
  11. package/dist/assembly/remotion-scaffold.js.map +1 -0
  12. package/dist/assembly/types.d.ts +67 -0
  13. package/dist/assembly/types.d.ts.map +1 -0
  14. package/dist/assembly/types.js +5 -0
  15. package/dist/assembly/types.js.map +1 -0
  16. package/dist/characters/describer.d.ts +32 -0
  17. package/dist/characters/describer.d.ts.map +1 -0
  18. package/dist/characters/describer.js +309 -0
  19. package/dist/characters/describer.js.map +1 -0
  20. package/dist/characters/extractor.d.ts +27 -0
  21. package/dist/characters/extractor.d.ts.map +1 -0
  22. package/dist/characters/extractor.js +274 -0
  23. package/dist/characters/extractor.js.map +1 -0
  24. package/dist/characters/reference-manager.d.ts +95 -0
  25. package/dist/characters/reference-manager.d.ts.map +1 -0
  26. package/dist/characters/reference-manager.js +346 -0
  27. package/dist/characters/reference-manager.js.map +1 -0
  28. package/dist/cli.d.ts +3 -0
  29. package/dist/cli.d.ts.map +1 -0
  30. package/dist/cli.js +310 -0
  31. package/dist/cli.js.map +1 -0
  32. package/dist/config.d.ts +22 -0
  33. package/dist/config.d.ts.map +1 -0
  34. package/dist/config.js +66 -0
  35. package/dist/config.js.map +1 -0
  36. package/dist/editing/aligner.d.ts +44 -0
  37. package/dist/editing/aligner.d.ts.map +1 -0
  38. package/dist/editing/aligner.js +201 -0
  39. package/dist/editing/aligner.js.map +1 -0
  40. package/dist/editing/edl.d.ts +45 -0
  41. package/dist/editing/edl.d.ts.map +1 -0
  42. package/dist/editing/edl.js +133 -0
  43. package/dist/editing/edl.js.map +1 -0
  44. package/dist/editing/overlays.d.ts +97 -0
  45. package/dist/editing/overlays.d.ts.map +1 -0
  46. package/dist/editing/overlays.js +41 -0
  47. package/dist/editing/overlays.js.map +1 -0
  48. package/dist/editing/packer.d.ts +52 -0
  49. package/dist/editing/packer.d.ts.map +1 -0
  50. package/dist/editing/packer.js +170 -0
  51. package/dist/editing/packer.js.map +1 -0
  52. package/dist/editing/providers/whisper-cpp.d.ts +32 -0
  53. package/dist/editing/providers/whisper-cpp.d.ts.map +1 -0
  54. package/dist/editing/providers/whisper-cpp.js +239 -0
  55. package/dist/editing/providers/whisper-cpp.js.map +1 -0
  56. package/dist/editing/render.d.ts +36 -0
  57. package/dist/editing/render.d.ts.map +1 -0
  58. package/dist/editing/render.js +227 -0
  59. package/dist/editing/render.js.map +1 -0
  60. package/dist/editing/self-eval.d.ts +48 -0
  61. package/dist/editing/self-eval.d.ts.map +1 -0
  62. package/dist/editing/self-eval.js +293 -0
  63. package/dist/editing/self-eval.js.map +1 -0
  64. package/dist/editing/silence.d.ts +76 -0
  65. package/dist/editing/silence.d.ts.map +1 -0
  66. package/dist/editing/silence.js +189 -0
  67. package/dist/editing/silence.js.map +1 -0
  68. package/dist/editing/types.d.ts +178 -0
  69. package/dist/editing/types.d.ts.map +1 -0
  70. package/dist/editing/types.js +13 -0
  71. package/dist/editing/types.js.map +1 -0
  72. package/dist/index.d.ts +6 -0
  73. package/dist/index.d.ts.map +1 -0
  74. package/dist/index.js +5 -0
  75. package/dist/index.js.map +1 -0
  76. package/dist/interactive.d.ts +12 -0
  77. package/dist/interactive.d.ts.map +1 -0
  78. package/dist/interactive.js +78 -0
  79. package/dist/interactive.js.map +1 -0
  80. package/dist/mini-drama/assembler.d.ts +61 -0
  81. package/dist/mini-drama/assembler.d.ts.map +1 -0
  82. package/dist/mini-drama/assembler.js +466 -0
  83. package/dist/mini-drama/assembler.js.map +1 -0
  84. package/dist/mini-drama/audio-mix.d.ts +65 -0
  85. package/dist/mini-drama/audio-mix.d.ts.map +1 -0
  86. package/dist/mini-drama/audio-mix.js +118 -0
  87. package/dist/mini-drama/audio-mix.js.map +1 -0
  88. package/dist/mini-drama/cli.d.ts +3 -0
  89. package/dist/mini-drama/cli.d.ts.map +1 -0
  90. package/dist/mini-drama/cli.js +2869 -0
  91. package/dist/mini-drama/cli.js.map +1 -0
  92. package/dist/mini-drama/fcpxml-export.d.ts +2 -0
  93. package/dist/mini-drama/fcpxml-export.d.ts.map +1 -0
  94. package/dist/mini-drama/fcpxml-export.js +5 -0
  95. package/dist/mini-drama/fcpxml-export.js.map +1 -0
  96. package/dist/mini-drama/generation-planner.d.ts +24 -0
  97. package/dist/mini-drama/generation-planner.d.ts.map +1 -0
  98. package/dist/mini-drama/generation-planner.js +304 -0
  99. package/dist/mini-drama/generation-planner.js.map +1 -0
  100. package/dist/mini-drama/location-generator.d.ts +23 -0
  101. package/dist/mini-drama/location-generator.d.ts.map +1 -0
  102. package/dist/mini-drama/location-generator.js +144 -0
  103. package/dist/mini-drama/location-generator.js.map +1 -0
  104. package/dist/mini-drama/music-cues.d.ts +96 -0
  105. package/dist/mini-drama/music-cues.d.ts.map +1 -0
  106. package/dist/mini-drama/music-cues.js +332 -0
  107. package/dist/mini-drama/music-cues.js.map +1 -0
  108. package/dist/mini-drama/panel-fixer.d.ts +12 -0
  109. package/dist/mini-drama/panel-fixer.d.ts.map +1 -0
  110. package/dist/mini-drama/panel-fixer.js +319 -0
  111. package/dist/mini-drama/panel-fixer.js.map +1 -0
  112. package/dist/mini-drama/prompt-builder.d.ts +149 -0
  113. package/dist/mini-drama/prompt-builder.d.ts.map +1 -0
  114. package/dist/mini-drama/prompt-builder.js +722 -0
  115. package/dist/mini-drama/prompt-builder.js.map +1 -0
  116. package/dist/mini-drama/reference-slots.d.ts +31 -0
  117. package/dist/mini-drama/reference-slots.d.ts.map +1 -0
  118. package/dist/mini-drama/reference-slots.js +161 -0
  119. package/dist/mini-drama/reference-slots.js.map +1 -0
  120. package/dist/mini-drama/shot-paths.d.ts +77 -0
  121. package/dist/mini-drama/shot-paths.d.ts.map +1 -0
  122. package/dist/mini-drama/shot-paths.js +101 -0
  123. package/dist/mini-drama/shot-paths.js.map +1 -0
  124. package/dist/mini-drama/storyboard-reference-generator.d.ts +47 -0
  125. package/dist/mini-drama/storyboard-reference-generator.d.ts.map +1 -0
  126. package/dist/mini-drama/storyboard-reference-generator.js +293 -0
  127. package/dist/mini-drama/storyboard-reference-generator.js.map +1 -0
  128. package/dist/mini-drama/subtitle-generator.d.ts +12 -0
  129. package/dist/mini-drama/subtitle-generator.d.ts.map +1 -0
  130. package/dist/mini-drama/subtitle-generator.js +139 -0
  131. package/dist/mini-drama/subtitle-generator.js.map +1 -0
  132. package/dist/mini-drama/timeline-export/davinci-fcpxml.d.ts +3 -0
  133. package/dist/mini-drama/timeline-export/davinci-fcpxml.d.ts.map +1 -0
  134. package/dist/mini-drama/timeline-export/davinci-fcpxml.js +150 -0
  135. package/dist/mini-drama/timeline-export/davinci-fcpxml.js.map +1 -0
  136. package/dist/mini-drama/timeline-export/fcpxml.d.ts +11 -0
  137. package/dist/mini-drama/timeline-export/fcpxml.d.ts.map +1 -0
  138. package/dist/mini-drama/timeline-export/fcpxml.js +140 -0
  139. package/dist/mini-drama/timeline-export/fcpxml.js.map +1 -0
  140. package/dist/mini-drama/timeline-export/index.d.ts +13 -0
  141. package/dist/mini-drama/timeline-export/index.d.ts.map +1 -0
  142. package/dist/mini-drama/timeline-export/index.js +26 -0
  143. package/dist/mini-drama/timeline-export/index.js.map +1 -0
  144. package/dist/mini-drama/timeline-export/premiere-xmeml.d.ts +3 -0
  145. package/dist/mini-drama/timeline-export/premiere-xmeml.d.ts.map +1 -0
  146. package/dist/mini-drama/timeline-export/premiere-xmeml.js +211 -0
  147. package/dist/mini-drama/timeline-export/premiere-xmeml.js.map +1 -0
  148. package/dist/mini-drama/timeline-export/probe.d.ts +53 -0
  149. package/dist/mini-drama/timeline-export/probe.d.ts.map +1 -0
  150. package/dist/mini-drama/timeline-export/probe.js +89 -0
  151. package/dist/mini-drama/timeline-export/probe.js.map +1 -0
  152. package/dist/mini-drama/timeline-export/types.d.ts +57 -0
  153. package/dist/mini-drama/timeline-export/types.d.ts.map +1 -0
  154. package/dist/mini-drama/timeline-export/types.js +14 -0
  155. package/dist/mini-drama/timeline-export/types.js.map +1 -0
  156. package/dist/mini-drama/video-generator.d.ts +22 -0
  157. package/dist/mini-drama/video-generator.d.ts.map +1 -0
  158. package/dist/mini-drama/video-generator.js +1305 -0
  159. package/dist/mini-drama/video-generator.js.map +1 -0
  160. package/dist/mini-drama/voice-reference.d.ts +42 -0
  161. package/dist/mini-drama/voice-reference.d.ts.map +1 -0
  162. package/dist/mini-drama/voice-reference.js +170 -0
  163. package/dist/mini-drama/voice-reference.js.map +1 -0
  164. package/dist/output/html-renderer.d.ts +2 -0
  165. package/dist/output/html-renderer.d.ts.map +1 -0
  166. package/dist/output/html-renderer.js +37 -0
  167. package/dist/output/html-renderer.js.map +1 -0
  168. package/dist/parsers/fountain-parser.d.ts +34 -0
  169. package/dist/parsers/fountain-parser.d.ts.map +1 -0
  170. package/dist/parsers/fountain-parser.js +71 -0
  171. package/dist/parsers/fountain-parser.js.map +1 -0
  172. package/dist/parsers/pdf-parser.d.ts +24 -0
  173. package/dist/parsers/pdf-parser.d.ts.map +1 -0
  174. package/dist/parsers/pdf-parser.js +237 -0
  175. package/dist/parsers/pdf-parser.js.map +1 -0
  176. package/dist/parsers/scene-extractor.d.ts +41 -0
  177. package/dist/parsers/scene-extractor.d.ts.map +1 -0
  178. package/dist/parsers/scene-extractor.js +400 -0
  179. package/dist/parsers/scene-extractor.js.map +1 -0
  180. package/dist/series/manager.d.ts +64 -0
  181. package/dist/series/manager.d.ts.map +1 -0
  182. package/dist/series/manager.js +282 -0
  183. package/dist/series/manager.js.map +1 -0
  184. package/dist/series/types.d.ts +641 -0
  185. package/dist/series/types.d.ts.map +1 -0
  186. package/dist/series/types.js +304 -0
  187. package/dist/series/types.js.map +1 -0
  188. package/dist/storyboard/assembler.d.ts +134 -0
  189. package/dist/storyboard/assembler.d.ts.map +1 -0
  190. package/dist/storyboard/assembler.js +319 -0
  191. package/dist/storyboard/assembler.js.map +1 -0
  192. package/dist/storyboard/prompt-builder.d.ts +113 -0
  193. package/dist/storyboard/prompt-builder.d.ts.map +1 -0
  194. package/dist/storyboard/prompt-builder.js +578 -0
  195. package/dist/storyboard/prompt-builder.js.map +1 -0
  196. package/dist/storyboard/shot-planner.d.ts +34 -0
  197. package/dist/storyboard/shot-planner.d.ts.map +1 -0
  198. package/dist/storyboard/shot-planner.js +446 -0
  199. package/dist/storyboard/shot-planner.js.map +1 -0
  200. package/dist/user-config.d.ts +15 -0
  201. package/dist/user-config.d.ts.map +1 -0
  202. package/dist/user-config.js +103 -0
  203. package/dist/user-config.js.map +1 -0
  204. package/dist/venice/audio-preflight.d.ts +80 -0
  205. package/dist/venice/audio-preflight.d.ts.map +1 -0
  206. package/dist/venice/audio-preflight.js +142 -0
  207. package/dist/venice/audio-preflight.js.map +1 -0
  208. package/dist/venice/audio.d.ts +88 -0
  209. package/dist/venice/audio.d.ts.map +1 -0
  210. package/dist/venice/audio.js +234 -0
  211. package/dist/venice/audio.js.map +1 -0
  212. package/dist/venice/client.d.ts +86 -0
  213. package/dist/venice/client.d.ts.map +1 -0
  214. package/dist/venice/client.js +375 -0
  215. package/dist/venice/client.js.map +1 -0
  216. package/dist/venice/edit.d.ts +57 -0
  217. package/dist/venice/edit.d.ts.map +1 -0
  218. package/dist/venice/edit.js +100 -0
  219. package/dist/venice/edit.js.map +1 -0
  220. package/dist/venice/generate.d.ts +47 -0
  221. package/dist/venice/generate.d.ts.map +1 -0
  222. package/dist/venice/generate.js +150 -0
  223. package/dist/venice/generate.js.map +1 -0
  224. package/dist/venice/image-bytes.d.ts +26 -0
  225. package/dist/venice/image-bytes.d.ts.map +1 -0
  226. package/dist/venice/image-bytes.js +101 -0
  227. package/dist/venice/image-bytes.js.map +1 -0
  228. package/dist/venice/models.d.ts +143 -0
  229. package/dist/venice/models.d.ts.map +1 -0
  230. package/dist/venice/models.js +984 -0
  231. package/dist/venice/models.js.map +1 -0
  232. package/dist/venice/multi-edit.d.ts +40 -0
  233. package/dist/venice/multi-edit.d.ts.map +1 -0
  234. package/dist/venice/multi-edit.js +97 -0
  235. package/dist/venice/multi-edit.js.map +1 -0
  236. package/dist/venice/provenance.d.ts +66 -0
  237. package/dist/venice/provenance.d.ts.map +1 -0
  238. package/dist/venice/provenance.js +151 -0
  239. package/dist/venice/provenance.js.map +1 -0
  240. package/dist/venice/recipe.d.ts +69 -0
  241. package/dist/venice/recipe.d.ts.map +1 -0
  242. package/dist/venice/recipe.js +103 -0
  243. package/dist/venice/recipe.js.map +1 -0
  244. package/dist/venice/rejection.d.ts +63 -0
  245. package/dist/venice/rejection.d.ts.map +1 -0
  246. package/dist/venice/rejection.js +111 -0
  247. package/dist/venice/rejection.js.map +1 -0
  248. package/dist/venice/seedance-preflight.d.ts +38 -0
  249. package/dist/venice/seedance-preflight.d.ts.map +1 -0
  250. package/dist/venice/seedance-preflight.js +29 -0
  251. package/dist/venice/seedance-preflight.js.map +1 -0
  252. package/dist/venice/types.d.ts +212 -0
  253. package/dist/venice/types.d.ts.map +1 -0
  254. package/dist/venice/types.js +9 -0
  255. package/dist/venice/types.js.map +1 -0
  256. package/dist/venice/video.d.ts +77 -0
  257. package/dist/venice/video.d.ts.map +1 -0
  258. package/dist/venice/video.js +163 -0
  259. package/dist/venice/video.js.map +1 -0
  260. package/dist/venice/voices.d.ts +40 -0
  261. package/dist/venice/voices.d.ts.map +1 -0
  262. package/dist/venice/voices.js +194 -0
  263. package/dist/venice/voices.js.map +1 -0
  264. package/package.json +79 -0
@@ -0,0 +1,237 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import pdfParse from "pdf-parse";
3
+ // ---------------------------------------------------------------------------
4
+ // Constants
5
+ // ---------------------------------------------------------------------------
6
+ /** Scene heading prefixes (case-insensitive, after trimming). */
7
+ const SCENE_HEADING_RE = /^\s*(INT\.|EXT\.|INT\.\/EXT\.|I\/E\.)\s*/i;
8
+ /**
9
+ * Common transition patterns.
10
+ * Matches lines ending with "TO:" and well-known named transitions.
11
+ */
12
+ const TRANSITION_ENDINGS_RE = /\bTO:$/;
13
+ const NAMED_TRANSITIONS = new Set([
14
+ "CUT TO:",
15
+ "SMASH CUT TO:",
16
+ "MATCH CUT TO:",
17
+ "JUMP CUT TO:",
18
+ "DISSOLVE TO:",
19
+ "FADE IN:",
20
+ "FADE OUT.",
21
+ "FADE OUT:",
22
+ "FADE TO BLACK.",
23
+ "FADE TO BLACK:",
24
+ "FADE TO WHITE.",
25
+ "FADE TO WHITE:",
26
+ "WIPE TO:",
27
+ "IRIS IN:",
28
+ "IRIS OUT.",
29
+ "IRIS OUT:",
30
+ "TIME CUT:",
31
+ ]);
32
+ /**
33
+ * Heuristic: a line is a character cue if it is ALL CAPS (ignoring
34
+ * parenthetical extensions like "(V.O.)" or "(O.S.)"), at least 2
35
+ * characters long, and not a scene heading or transition.
36
+ */
37
+ const CHARACTER_CUE_RE = /^[A-Z][A-Z0-9 .'\-/]+(\s*\(.*\))?$/;
38
+ /**
39
+ * Lines that look like page numbers commonly emitted by PDF renderers.
40
+ */
41
+ const PAGE_NUMBER_RE = /^\s*\d{1,4}\s*\.?\s*$/;
42
+ // ---------------------------------------------------------------------------
43
+ // Raw text extraction
44
+ // ---------------------------------------------------------------------------
45
+ /**
46
+ * Extract the full text content from a PDF screenplay file.
47
+ *
48
+ * @param filePath - Absolute or relative path to a `.pdf` file.
49
+ * @returns The concatenated plain-text content of every page.
50
+ * @throws If the file cannot be read or is not a valid PDF.
51
+ */
52
+ export async function parsePdf(filePath) {
53
+ const buffer = await readFile(filePath);
54
+ const result = await pdfParse(buffer);
55
+ return result.text;
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // Heuristic tokenizer
59
+ // ---------------------------------------------------------------------------
60
+ /**
61
+ * Determine whether a trimmed, non-empty line is a transition.
62
+ */
63
+ function isTransition(line) {
64
+ const upper = line.toUpperCase();
65
+ if (TRANSITION_ENDINGS_RE.test(upper))
66
+ return true;
67
+ if (NAMED_TRANSITIONS.has(upper))
68
+ return true;
69
+ return false;
70
+ }
71
+ /**
72
+ * Determine whether a trimmed, non-empty line looks like a scene heading.
73
+ */
74
+ function isSceneHeading(line) {
75
+ return SCENE_HEADING_RE.test(line);
76
+ }
77
+ /**
78
+ * Determine whether a trimmed, non-empty line looks like a character cue.
79
+ * Must NOT also be a scene heading or transition.
80
+ */
81
+ function isCharacterCue(line) {
82
+ if (isSceneHeading(line))
83
+ return false;
84
+ if (isTransition(line))
85
+ return false;
86
+ if (PAGE_NUMBER_RE.test(line))
87
+ return false;
88
+ // Character names are typically short-ish; reject very long lines.
89
+ if (line.length > 60)
90
+ return false;
91
+ return CHARACTER_CUE_RE.test(line.trim());
92
+ }
93
+ /**
94
+ * Detect whether a line is a parenthetical (e.g. "(softly)").
95
+ */
96
+ function isParenthetical(line) {
97
+ const trimmed = line.trim();
98
+ return trimmed.startsWith("(") && trimmed.endsWith(")");
99
+ }
100
+ /**
101
+ * Extract screenplay text from a PDF and apply format-agnostic heuristics
102
+ * to produce the same `FountainResult` shape that `parseFountain` returns.
103
+ *
104
+ * Because PDF extraction loses explicit formatting metadata, the heuristics
105
+ * are necessarily best-effort. They work well on industry-standard
106
+ * screenplay PDFs (Final Draft, WriterSolo, Highland, etc.) but may
107
+ * misclassify some elements in heavily stylised documents.
108
+ *
109
+ * @param filePath - Path to a `.pdf` screenplay file.
110
+ * @returns A `FountainResult` with heuristically classified tokens.
111
+ */
112
+ export async function parsePdfToTokens(filePath) {
113
+ const rawText = await parsePdf(filePath);
114
+ const lines = rawText.split(/\r?\n/);
115
+ const tokens = [];
116
+ // Title-page heuristic: the very first non-blank lines before the first
117
+ // scene heading are treated as the title block. We try to extract a
118
+ // title, credit, and author from them.
119
+ let title = "";
120
+ let credit = "";
121
+ let author = "";
122
+ let titleBlockDone = false;
123
+ const titleBlockLines = [];
124
+ // State machine for dialogue grouping.
125
+ let expectDialogue = false;
126
+ for (let i = 0; i < lines.length; i++) {
127
+ const raw = lines[i];
128
+ const trimmed = raw.trim();
129
+ // Skip empty lines -- they reset the dialogue expectation.
130
+ if (trimmed.length === 0) {
131
+ expectDialogue = false;
132
+ continue;
133
+ }
134
+ // Skip page numbers.
135
+ if (PAGE_NUMBER_RE.test(trimmed)) {
136
+ continue;
137
+ }
138
+ // --- Title block collection (before first scene heading) ---
139
+ if (!titleBlockDone) {
140
+ if (isSceneHeading(trimmed)) {
141
+ titleBlockDone = true;
142
+ // Parse collected title block.
143
+ parseTitleBlock(titleBlockLines, (t, c, a) => {
144
+ title = t;
145
+ credit = c;
146
+ author = a;
147
+ });
148
+ // Fall through so this line gets processed as a scene heading.
149
+ }
150
+ else {
151
+ titleBlockLines.push(trimmed);
152
+ continue;
153
+ }
154
+ }
155
+ // --- Scene heading ---
156
+ if (isSceneHeading(trimmed)) {
157
+ expectDialogue = false;
158
+ tokens.push({ type: "scene_heading", text: trimmed });
159
+ continue;
160
+ }
161
+ // --- Transition ---
162
+ if (isTransition(trimmed)) {
163
+ expectDialogue = false;
164
+ tokens.push({ type: "transition", text: trimmed });
165
+ continue;
166
+ }
167
+ // --- Parenthetical (only valid within dialogue context) ---
168
+ if (expectDialogue && isParenthetical(trimmed)) {
169
+ tokens.push({ type: "parenthetical", text: trimmed });
170
+ // Stay in dialogue mode.
171
+ continue;
172
+ }
173
+ // --- Dialogue (indented text after a character cue) ---
174
+ if (expectDialogue) {
175
+ tokens.push({ type: "dialogue", text: trimmed });
176
+ // Dialogue can span multiple contiguous non-blank lines.
177
+ continue;
178
+ }
179
+ // --- Character cue ---
180
+ if (isCharacterCue(trimmed)) {
181
+ tokens.push({ type: "character", text: trimmed });
182
+ expectDialogue = true;
183
+ continue;
184
+ }
185
+ // --- Everything else is action ---
186
+ tokens.push({ type: "action", text: trimmed });
187
+ }
188
+ // If there was never a scene heading, the title block was never
189
+ // committed. Handle that edge case.
190
+ if (!titleBlockDone && titleBlockLines.length > 0) {
191
+ parseTitleBlock(titleBlockLines, (t, c, a) => {
192
+ title = t;
193
+ credit = c;
194
+ author = a;
195
+ });
196
+ }
197
+ return { title, credit, author, tokens };
198
+ }
199
+ // ---------------------------------------------------------------------------
200
+ // Title block helpers
201
+ // ---------------------------------------------------------------------------
202
+ /**
203
+ * Very rough heuristic to pull title / credit / author from the lines
204
+ * that precede the first scene heading in a PDF screenplay.
205
+ *
206
+ * Common patterns:
207
+ * Line 1: TITLE
208
+ * Line 2: "Written by" | "Screenplay by" | "by" (credit)
209
+ * Line 3: Author Name
210
+ */
211
+ function parseTitleBlock(lines, cb) {
212
+ if (lines.length === 0) {
213
+ cb("", "", "");
214
+ return;
215
+ }
216
+ let title = lines[0] ?? "";
217
+ let credit = "";
218
+ let author = "";
219
+ for (let i = 1; i < lines.length; i++) {
220
+ const lower = lines[i].toLowerCase();
221
+ if (lower.startsWith("written by") ||
222
+ lower.startsWith("screenplay by") ||
223
+ lower === "by") {
224
+ credit = lines[i];
225
+ // The next non-empty line is likely the author.
226
+ for (let j = i + 1; j < lines.length; j++) {
227
+ if (lines[j].trim().length > 0) {
228
+ author = lines[j].trim();
229
+ break;
230
+ }
231
+ }
232
+ break;
233
+ }
234
+ }
235
+ cb(title, credit, author);
236
+ }
237
+ //# sourceMappingURL=pdf-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf-parser.js","sourceRoot":"","sources":["../../src/parsers/pdf-parser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,QAAQ,MAAM,WAAW,CAAC;AAMjC,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E,iEAAiE;AACjE,MAAM,gBAAgB,GAAG,2CAA2C,CAAC;AAErE;;;GAGG;AACH,MAAM,qBAAqB,GAAG,QAAQ,CAAC;AACvC,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC;IAChC,SAAS;IACT,eAAe;IACf,eAAe;IACf,cAAc;IACd,cAAc;IACd,UAAU;IACV,WAAW;IACX,WAAW;IACX,gBAAgB;IAChB,gBAAgB;IAChB,gBAAgB;IAChB,gBAAgB;IAChB,UAAU;IACV,UAAU;IACV,WAAW;IACX,WAAW;IACX,WAAW;CACZ,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,gBAAgB,GAAG,oCAAoC,CAAC;AAE9D;;GAEG;AACH,MAAM,cAAc,GAAG,uBAAuB,CAAC;AAE/C,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,QAAgB;IAC7C,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;IACtC,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC;AAED,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E;;GAEG;AACH,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,IAAI,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnD,IAAI,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9C,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,SAAS,cAAc,CAAC,IAAY;IAClC,OAAO,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACrC,CAAC;AAED;;;GAGG;AACH,SAAS,cAAc,CAAC,IAAY;IAClC,IAAI,cAAc,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACvC,IAAI,YAAY,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACrC,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5C,mEAAmE;IACnE,IAAI,IAAI,CAAC,MAAM,GAAG,EAAE;QAAE,OAAO,KAAK,CAAC;IACnC,OAAO,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED;;GAEG;AACH,SAAS,eAAe,CAAC,IAAY;IACnC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,OAAO,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,QAAgB;IAEhB,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAErC,MAAM,MAAM,GAAoB,EAAE,CAAC;IAEnC,wEAAwE;IACxE,qEAAqE;IACrE,uCAAuC;IACvC,IAAI,KAAK,GAAG,EAAE,CAAC;IACf,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,cAAc,GAAG,KAAK,CAAC;IAC3B,MAAM,eAAe,GAAa,EAAE,CAAC;IAErC,uCAAuC;IACvC,IAAI,cAAc,GAAG,KAAK,CAAC;IAE3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACrB,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QAE3B,2DAA2D;QAC3D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,cAAc,GAAG,KAAK,CAAC;YACvB,SAAS;QACX,CAAC;QAED,qBAAqB;QACrB,IAAI,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACjC,SAAS;QACX,CAAC;QAED,8DAA8D;QAC9D,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,IAAI,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC5B,cAAc,GAAG,IAAI,CAAC;gBACtB,+BAA+B;gBAC/B,eAAe,CAAC,eAAe,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE;oBAC3C,KAAK,GAAG,CAAC,CAAC;oBACV,MAAM,GAAG,CAAC,CAAC;oBACX,MAAM,GAAG,CAAC,CAAC;gBACb,CAAC,CAAC,CAAC;gBACH,+DAA+D;YACjE,CAAC;iBAAM,CAAC;gBACN,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAC9B,SAAS;YACX,CAAC;QACH,CAAC;QAED,wBAAwB;QACxB,IAAI,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5B,cAAc,GAAG,KAAK,CAAC;YACvB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;YACtD,SAAS;QACX,CAAC;QAED,qBAAqB;QACrB,IAAI,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1B,cAAc,GAAG,KAAK,CAAC;YACvB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;YACnD,SAAS;QACX,CAAC;QAED,6DAA6D;QAC7D,IAAI,cAAc,IAAI,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC;YAC/C,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;YACtD,yBAAyB;YACzB,SAAS;QACX,CAAC;QAED,yDAAyD;QACzD,IAAI,cAAc,EAAE,CAAC;YACnB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;YACjD,yDAAyD;YACzD,SAAS;QACX,CAAC;QAED,wBAAwB;QACxB,IAAI,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5B,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;YAClD,cAAc,GAAG,IAAI,CAAC;YACtB,SAAS;QACX,CAAC;QAED,oCAAoC;QACpC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IACjD,CAAC;IAED,gEAAgE;IAChE,qCAAqC;IACrC,IAAI,CAAC,cAAc,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClD,eAAe,CAAC,eAAe,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE;YAC3C,KAAK,GAAG,CAAC,CAAC;YACV,MAAM,GAAG,CAAC,CAAC;YACX,MAAM,GAAG,CAAC,CAAC;QACb,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC3C,CAAC;AAED,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E;;;;;;;;GAQG;AACH,SAAS,eAAe,CACtB,KAAe,EACf,EAA2D;IAE3D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QACf,OAAO;IACT,CAAC;IAED,IAAI,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,MAAM,GAAG,EAAE,CAAC;IAEhB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QACrC,IACE,KAAK,CAAC,UAAU,CAAC,YAAY,CAAC;YAC9B,KAAK,CAAC,UAAU,CAAC,eAAe,CAAC;YACjC,KAAK,KAAK,IAAI,EACd,CAAC;YACD,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YAClB,gDAAgD;YAChD,KAAK,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC1C,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC/B,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;oBACzB,MAAM;gBACR,CAAC;YACH,CAAC;YACD,MAAM;QACR,CAAC;IACH,CAAC;IAED,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,41 @@
1
+ import type { FountainResult, FountainToken } from "./fountain-parser.js";
2
+ export type { FountainResult, FountainToken };
3
+ export interface DialogueLine {
4
+ character: string;
5
+ parenthetical?: string;
6
+ text: string;
7
+ }
8
+ export interface Scene {
9
+ number: number;
10
+ heading: string;
11
+ location: string;
12
+ timeOfDay: string;
13
+ characters: string[];
14
+ action: string[];
15
+ dialogue: DialogueLine[];
16
+ transitions: string[];
17
+ mood: string;
18
+ }
19
+ /**
20
+ * Walk through a flat token stream produced by either `parseFountain` or
21
+ * `parsePdfToTokens` and group them into structured `Scene` objects.
22
+ *
23
+ * Tokens appearing before the first `scene_heading` token are discarded
24
+ * (they typically represent title-page metadata that is already captured
25
+ * in `FountainResult.title` etc.).
26
+ *
27
+ * @param parsed - The `FountainResult` from any parser.
28
+ * @returns An ordered array of `Scene` objects, numbered from 1.
29
+ */
30
+ export declare function extractScenes(parsed: FountainResult): Scene[];
31
+ /**
32
+ * Collect every unique character name that appears across all scenes.
33
+ *
34
+ * Names are normalised (parenthetical extensions removed) and de-duplicated.
35
+ * The returned array preserves first-appearance order.
36
+ *
37
+ * @param scenes - Array of `Scene` objects produced by `extractScenes`.
38
+ * @returns Ordered, de-duplicated array of character names.
39
+ */
40
+ export declare function extractAllCharacters(scenes: Scene[]): string[];
41
+ //# sourceMappingURL=scene-extractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene-extractor.d.ts","sourceRoot":"","sources":["../../src/parsers/scene-extractor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAG1E,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,CAAC;AAM9C,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd;AA2PD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,KAAK,EAAE,CAiI7D;AAMD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,EAAE,CAc9D"}
@@ -0,0 +1,400 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Heading parsing helpers
3
+ // ---------------------------------------------------------------------------
4
+ /**
5
+ * Extract the location portion from a scene heading.
6
+ *
7
+ * Examples:
8
+ * "INT. JAZZ CLUB - NIGHT" -> "JAZZ CLUB"
9
+ * "EXT. ROOFTOP - DOWNTOWN - DUSK" -> "ROOFTOP - DOWNTOWN"
10
+ * "INT./EXT. CAR" -> "CAR"
11
+ * "I/E. APARTMENT" -> "APARTMENT"
12
+ */
13
+ function extractLocation(heading) {
14
+ // Remove the INT./EXT./INT./EXT./I/E. prefix.
15
+ const withoutPrefix = heading
16
+ .replace(/^\s*(INT\.\s*\/\s*EXT\.|I\s*\/\s*E\.|INT\.|EXT\.)\s*/i, "")
17
+ .trim();
18
+ // Everything before the *last* " - " delimiter is location.
19
+ // If there is no delimiter the entire remainder is the location.
20
+ const lastDash = withoutPrefix.lastIndexOf(" - ");
21
+ if (lastDash === -1)
22
+ return withoutPrefix;
23
+ return withoutPrefix.substring(0, lastDash).trim();
24
+ }
25
+ /**
26
+ * Extract the time-of-day portion from a scene heading.
27
+ *
28
+ * Looks for the text after the last " - " separator. If the result
29
+ * matches a known time-of-day keyword it is returned; otherwise the
30
+ * raw text after the last dash is returned (to preserve non-standard
31
+ * time indicators like "LATER" or "MOMENTS LATER").
32
+ */
33
+ const TIME_OF_DAY_KEYWORDS = new Set([
34
+ "DAY",
35
+ "NIGHT",
36
+ "DAWN",
37
+ "DUSK",
38
+ "MORNING",
39
+ "AFTERNOON",
40
+ "EVENING",
41
+ "SUNRISE",
42
+ "SUNSET",
43
+ "LATER",
44
+ "CONTINUOUS",
45
+ "MOMENTS LATER",
46
+ "SAME",
47
+ "SAME TIME",
48
+ ]);
49
+ function extractTimeOfDay(heading) {
50
+ const lastDash = heading.lastIndexOf(" - ");
51
+ if (lastDash === -1)
52
+ return "";
53
+ const candidate = heading.substring(lastDash + 3).trim().toUpperCase();
54
+ if (TIME_OF_DAY_KEYWORDS.has(candidate))
55
+ return candidate;
56
+ // Partial match: heading may say "NIGHT (FLASHBACK)" etc.
57
+ for (const kw of TIME_OF_DAY_KEYWORDS) {
58
+ if (candidate.startsWith(kw))
59
+ return kw;
60
+ }
61
+ return candidate;
62
+ }
63
+ // ---------------------------------------------------------------------------
64
+ // Mood inference
65
+ // ---------------------------------------------------------------------------
66
+ const MOOD_KEYWORDS = {
67
+ dark: [
68
+ "dark",
69
+ "shadow",
70
+ "dim",
71
+ "gloomy",
72
+ "ominous",
73
+ "sinister",
74
+ "bleak",
75
+ "eerie",
76
+ "foreboding",
77
+ "dread",
78
+ ],
79
+ tense: [
80
+ "tense",
81
+ "nervous",
82
+ "anxious",
83
+ "sweat",
84
+ "grip",
85
+ "clench",
86
+ "trembl",
87
+ "panic",
88
+ "frantic",
89
+ "urgent",
90
+ "desperate",
91
+ "suspense",
92
+ ],
93
+ violent: [
94
+ "punch",
95
+ "kick",
96
+ "gun",
97
+ "shot",
98
+ "blood",
99
+ "fight",
100
+ "stab",
101
+ "explo",
102
+ "crash",
103
+ "smash",
104
+ "attack",
105
+ "scream",
106
+ "wound",
107
+ "kill",
108
+ "dead",
109
+ ],
110
+ romantic: [
111
+ "kiss",
112
+ "embrace",
113
+ "love",
114
+ "tender",
115
+ "gentle",
116
+ "caress",
117
+ "intimate",
118
+ "passion",
119
+ "heart",
120
+ "gaze",
121
+ ],
122
+ warm: [
123
+ "warm",
124
+ "cozy",
125
+ "comfort",
126
+ "smile",
127
+ "laugh",
128
+ "joy",
129
+ "happy",
130
+ "bright",
131
+ "sun",
132
+ "glow",
133
+ "golden",
134
+ "cheerful",
135
+ ],
136
+ bright: [
137
+ "vivid",
138
+ "colorful",
139
+ "radiant",
140
+ "sparkl",
141
+ "gleam",
142
+ "shine",
143
+ "brilliant",
144
+ "luminous",
145
+ ],
146
+ quiet: [
147
+ "quiet",
148
+ "silent",
149
+ "still",
150
+ "calm",
151
+ "peace",
152
+ "serene",
153
+ "hushed",
154
+ "soft",
155
+ "whisper",
156
+ "murmur",
157
+ ],
158
+ chaotic: [
159
+ "chaos",
160
+ "crowd",
161
+ "rush",
162
+ "noise",
163
+ "loud",
164
+ "frenzy",
165
+ "mayhem",
166
+ "hectic",
167
+ "commotion",
168
+ "scramble",
169
+ "disorder",
170
+ ],
171
+ melancholy: [
172
+ "sad",
173
+ "tear",
174
+ "cry",
175
+ "weep",
176
+ "sorrow",
177
+ "grief",
178
+ "mourn",
179
+ "lonely",
180
+ "desolat",
181
+ "wistful",
182
+ "melan",
183
+ "somber",
184
+ ],
185
+ mysterious: [
186
+ "mystery",
187
+ "strange",
188
+ "odd",
189
+ "peculiar",
190
+ "enigma",
191
+ "unknown",
192
+ "secret",
193
+ "hidden",
194
+ "fog",
195
+ "mist",
196
+ "vanish",
197
+ ],
198
+ };
199
+ /**
200
+ * Score the combined action text of a scene against mood keyword buckets
201
+ * and return the mood with the highest hit count. Falls back to "neutral"
202
+ * when no keywords are matched.
203
+ */
204
+ function inferMood(actionLines) {
205
+ const blob = actionLines.join(" ").toLowerCase();
206
+ if (blob.length === 0)
207
+ return "neutral";
208
+ let bestMood = "neutral";
209
+ let bestScore = 0;
210
+ for (const [mood, keywords] of Object.entries(MOOD_KEYWORDS)) {
211
+ let score = 0;
212
+ for (const kw of keywords) {
213
+ // Use indexOf for substring matching so partial stems work
214
+ // (e.g. "trembl" matches "trembling").
215
+ let idx = 0;
216
+ while ((idx = blob.indexOf(kw, idx)) !== -1) {
217
+ score++;
218
+ idx += kw.length;
219
+ }
220
+ }
221
+ if (score > bestScore) {
222
+ bestScore = score;
223
+ bestMood = mood;
224
+ }
225
+ }
226
+ return bestMood;
227
+ }
228
+ // ---------------------------------------------------------------------------
229
+ // Character name normalisation
230
+ // ---------------------------------------------------------------------------
231
+ /**
232
+ * Normalise a character name by stripping parenthetical extensions
233
+ * like "(V.O.)", "(O.S.)", "(CONT'D)" and trimming whitespace.
234
+ */
235
+ function normaliseCharacterName(raw) {
236
+ return raw.replace(/\s*\(.*?\)\s*/g, "").trim();
237
+ }
238
+ // ---------------------------------------------------------------------------
239
+ // Scene extraction
240
+ // ---------------------------------------------------------------------------
241
+ /**
242
+ * Walk through a flat token stream produced by either `parseFountain` or
243
+ * `parsePdfToTokens` and group them into structured `Scene` objects.
244
+ *
245
+ * Tokens appearing before the first `scene_heading` token are discarded
246
+ * (they typically represent title-page metadata that is already captured
247
+ * in `FountainResult.title` etc.).
248
+ *
249
+ * @param parsed - The `FountainResult` from any parser.
250
+ * @returns An ordered array of `Scene` objects, numbered from 1.
251
+ */
252
+ export function extractScenes(parsed) {
253
+ const scenes = [];
254
+ let current = null;
255
+ let sceneNumber = 0;
256
+ // Dialogue accumulation state.
257
+ let currentCharacter = null;
258
+ let currentParenthetical = undefined;
259
+ /** Flush a completed dialogue line into the current scene. */
260
+ function flushDialogue(text) {
261
+ if (current && currentCharacter) {
262
+ const line = {
263
+ character: currentCharacter,
264
+ text,
265
+ };
266
+ if (currentParenthetical) {
267
+ line.parenthetical = currentParenthetical;
268
+ }
269
+ current.dialogue.push(line);
270
+ // Add character to the scene's unique set.
271
+ const normalised = normaliseCharacterName(currentCharacter);
272
+ if (normalised && !current.characters.includes(normalised)) {
273
+ current.characters.push(normalised);
274
+ }
275
+ }
276
+ currentParenthetical = undefined;
277
+ }
278
+ for (const token of parsed.tokens) {
279
+ switch (token.type) {
280
+ case "scene_heading": {
281
+ // Finalise previous scene mood before starting a new one.
282
+ if (current) {
283
+ current.mood = inferMood(current.action);
284
+ }
285
+ sceneNumber++;
286
+ const heading = token.text ?? "";
287
+ current = {
288
+ number: sceneNumber,
289
+ heading,
290
+ location: extractLocation(heading),
291
+ timeOfDay: extractTimeOfDay(heading),
292
+ characters: [],
293
+ action: [],
294
+ dialogue: [],
295
+ transitions: [],
296
+ mood: "neutral",
297
+ };
298
+ scenes.push(current);
299
+ currentCharacter = null;
300
+ currentParenthetical = undefined;
301
+ break;
302
+ }
303
+ case "character": {
304
+ if (!current)
305
+ break;
306
+ currentCharacter = token.text ?? "";
307
+ currentParenthetical = undefined;
308
+ // Track the character even if they have no dialogue lines.
309
+ const normalised = normaliseCharacterName(currentCharacter);
310
+ if (normalised && !current.characters.includes(normalised)) {
311
+ current.characters.push(normalised);
312
+ }
313
+ break;
314
+ }
315
+ case "parenthetical": {
316
+ if (!current)
317
+ break;
318
+ currentParenthetical = token.text ?? "";
319
+ break;
320
+ }
321
+ case "dialogue": {
322
+ if (!current)
323
+ break;
324
+ flushDialogue(token.text ?? "");
325
+ break;
326
+ }
327
+ case "action": {
328
+ if (!current)
329
+ break;
330
+ currentCharacter = null;
331
+ currentParenthetical = undefined;
332
+ const text = token.text ?? "";
333
+ if (text.length > 0) {
334
+ current.action.push(text);
335
+ }
336
+ break;
337
+ }
338
+ case "transition": {
339
+ if (!current)
340
+ break;
341
+ currentCharacter = null;
342
+ currentParenthetical = undefined;
343
+ const text = token.text ?? "";
344
+ if (text.length > 0) {
345
+ current.transitions.push(text);
346
+ }
347
+ break;
348
+ }
349
+ // Sections, synopses, notes, centered text, lyrics -- carry useful
350
+ // context but are treated as action for mood/content purposes.
351
+ case "section":
352
+ case "synopsis":
353
+ case "note":
354
+ case "centered":
355
+ case "lyrics": {
356
+ if (!current)
357
+ break;
358
+ const text = token.text ?? "";
359
+ if (text.length > 0) {
360
+ current.action.push(text);
361
+ }
362
+ break;
363
+ }
364
+ default:
365
+ // Unknown or structural token types are silently ignored.
366
+ break;
367
+ }
368
+ }
369
+ // Finalise mood for the last scene.
370
+ if (current) {
371
+ current.mood = inferMood(current.action);
372
+ }
373
+ return scenes;
374
+ }
375
+ // ---------------------------------------------------------------------------
376
+ // Character extraction
377
+ // ---------------------------------------------------------------------------
378
+ /**
379
+ * Collect every unique character name that appears across all scenes.
380
+ *
381
+ * Names are normalised (parenthetical extensions removed) and de-duplicated.
382
+ * The returned array preserves first-appearance order.
383
+ *
384
+ * @param scenes - Array of `Scene` objects produced by `extractScenes`.
385
+ * @returns Ordered, de-duplicated array of character names.
386
+ */
387
+ export function extractAllCharacters(scenes) {
388
+ const seen = new Set();
389
+ const result = [];
390
+ for (const scene of scenes) {
391
+ for (const name of scene.characters) {
392
+ if (!seen.has(name)) {
393
+ seen.add(name);
394
+ result.push(name);
395
+ }
396
+ }
397
+ }
398
+ return result;
399
+ }
400
+ //# sourceMappingURL=scene-extractor.js.map