narRaters 0.1.0__py3-none-any.whl
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.
- narraters/__init__.py +33 -0
- narraters/cli.py +400 -0
- narraters/helpers/__init__.py +1 -0
- narraters/helpers/analysis_recall_metrics.py +288 -0
- narraters/helpers/anthropic_ids.py +36 -0
- narraters/helpers/capture_tutorial_screenshots.py +132 -0
- narraters/helpers/disk_space.py +136 -0
- narraters/helpers/feedback_links.py +7 -0
- narraters/helpers/gemma_environment.py +195 -0
- narraters/helpers/ollama_gemma_e4b.py +169 -0
- narraters/helpers/plot_bar_metrics_comparison.py +174 -0
- narraters/helpers/plot_matrix_comparison.py +280 -0
- narraters/helpers/resource_preflight.py +306 -0
- narraters/helpers/software_paths.py +26 -0
- narraters/helpers/step_types.py +127 -0
- narraters/helpers/test_bar_metrics_all_rated.py +144 -0
- narraters/helpers/test_bar_metrics_temperature.py +137 -0
- narraters/helpers/test_matrix_comparison_multi_story.py +134 -0
- narraters/helpers/test_matrix_comparison_temperature.py +129 -0
- narraters/helpers/test_recall_rater_all_stories.py +384 -0
- narraters/helpers/test_recall_rater_prompt_versions.py +312 -0
- narraters/helpers/test_recall_rater_single_subject.py +21 -0
- narraters/helpers/test_recall_rater_temperature.py +231 -0
- narraters/helpers/test_recall_rater_unrated.py +367 -0
- narraters/helpers/test_story_event_segment.py +82 -0
- narraters/helpers/util_software_env.py +23 -0
- narraters/helpers/utils_recall_data.py +120 -0
- narraters/paths.py +89 -0
- narraters/runtime_install.py +319 -0
- narraters/scripts/1_audio-transcribe.py +282 -0
- narraters/scripts/2_story-event-segment.py +1483 -0
- narraters/scripts/3_spell-grammar-correct.py +1138 -0
- narraters/scripts/4_parse-texts.py +771 -0
- narraters/scripts/5_recall-rater.py +1455 -0
- narraters/scripts/6_causal-rater.py +1016 -0
- narraters/scripts/finish_windows_setup.bat +47 -0
- narraters/scripts/project_python.sh +26 -0
- narraters/scripts/prompt/README.md +32 -0
- narraters/scripts/prompt/causal_rating.txt +15 -0
- narraters/scripts/prompt/event_segment.txt +1 -0
- narraters/scripts/prompt/recall_parse_clause.txt +34 -0
- narraters/scripts/prompt/recall_rating.txt +16 -0
- narraters/scripts/prompt/spell_gram.txt +1 -0
- narraters/scripts/run_event_segment.sh +36 -0
- narraters/scripts/run_recall_rater.sh +22 -0
- narraters/scripts/setup_api_key.sh +53 -0
- narraters/scripts/setup_project_venv.sh +37 -0
- narraters/server/START_HERE.command +116 -0
- narraters/server/test-server.sh +118 -0
- narraters/server/web-interface.py +6934 -0
- narraters/static/app-icon.png +0 -0
- narraters/static/theme-manager.js +152 -0
- narraters/templates/index.html +5055 -0
- narraters/templates/pipeline-config.html +2594 -0
- narraters/templates/subject.html +6503 -0
- narraters-0.1.0.dist-info/METADATA +657 -0
- narraters-0.1.0.dist-info/RECORD +60 -0
- narraters-0.1.0.dist-info/WHEEL +4 -0
- narraters-0.1.0.dist-info/entry_points.txt +2 -0
- narraters-0.1.0.dist-info/licenses/LICENSE +104 -0
narraters/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""narRaters — AI-assisted narrative processing with human-screening.
|
|
2
|
+
|
|
3
|
+
A 6-step pipeline (transcription → segmentation → spell/grammar correction →
|
|
4
|
+
parsing → event matching → causal rating) with a Flask web UI for interactive
|
|
5
|
+
review and editing at each step.
|
|
6
|
+
|
|
7
|
+
Quick start
|
|
8
|
+
-----------
|
|
9
|
+
# Install editable from a clone of the repo:
|
|
10
|
+
pip install -e .
|
|
11
|
+
|
|
12
|
+
# Launch the web interface:
|
|
13
|
+
narraters serve
|
|
14
|
+
|
|
15
|
+
# Run individual pipeline steps:
|
|
16
|
+
narraters segment --method fine --input data/2_story_transcript/foo.txt
|
|
17
|
+
narraters match --method api --model <anthropic-model-id>
|
|
18
|
+
|
|
19
|
+
Library use
|
|
20
|
+
-----------
|
|
21
|
+
from narraters import run_serve
|
|
22
|
+
run_serve(port=5000)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
|
|
27
|
+
from narraters.paths import project_root, repo_root # noqa: F401
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"__version__",
|
|
31
|
+
"project_root",
|
|
32
|
+
"repo_root",
|
|
33
|
+
]
|
narraters/cli.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""Command-line interface for narRaters.
|
|
2
|
+
|
|
3
|
+
Exposes a single `narraters` command with subcommands for each pipeline step
|
|
4
|
+
plus `serve` for the web UI:
|
|
5
|
+
|
|
6
|
+
narraters transcribe ...
|
|
7
|
+
narraters segment ...
|
|
8
|
+
narraters correct ...
|
|
9
|
+
narraters parse ...
|
|
10
|
+
narraters match ...
|
|
11
|
+
narraters rate ...
|
|
12
|
+
narraters serve
|
|
13
|
+
|
|
14
|
+
Phase 1 implementation: each step subcommand delegates to the legacy
|
|
15
|
+
scripts/N_*.py via subprocess, translating CLI flags and env vars as needed.
|
|
16
|
+
`serve` imports and runs the legacy Flask app in-process. Phases 2 and 3 will
|
|
17
|
+
migrate the actual logic into proper Python modules.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import os
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from narraters import __version__
|
|
29
|
+
from narraters.paths import ensure_repo_on_path, repo_root, scripts_dir
|
|
30
|
+
from narraters.runtime_install import (
|
|
31
|
+
prepare_cli_correct,
|
|
32
|
+
prepare_cli_match,
|
|
33
|
+
prepare_cli_parse,
|
|
34
|
+
prepare_cli_rate,
|
|
35
|
+
prepare_cli_segment,
|
|
36
|
+
prepare_cli_transcribe,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Map subcommand name → legacy script filename
|
|
40
|
+
_LEGACY_SCRIPTS = {
|
|
41
|
+
"transcribe": "1_audio-transcribe.py",
|
|
42
|
+
"segment": "2_story-event-segment.py",
|
|
43
|
+
"correct": "3_spell-grammar-correct.py",
|
|
44
|
+
"parse": "4_parse-texts.py",
|
|
45
|
+
"match": "5_recall-rater.py",
|
|
46
|
+
"rate": "6_causal-rater.py",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _run_legacy_script(subcommand: str, forwarded_args: list[str], env_overrides: dict[str, str] | None = None) -> int:
|
|
51
|
+
"""Invoke a legacy scripts/N_*.py via subprocess and return its exit code."""
|
|
52
|
+
script_name = _LEGACY_SCRIPTS[subcommand]
|
|
53
|
+
script_path = scripts_dir() / script_name
|
|
54
|
+
if not script_path.exists():
|
|
55
|
+
print(f"narraters: legacy script not found: {script_path}", file=sys.stderr)
|
|
56
|
+
return 2
|
|
57
|
+
|
|
58
|
+
env = os.environ.copy()
|
|
59
|
+
if env_overrides:
|
|
60
|
+
env.update(env_overrides)
|
|
61
|
+
|
|
62
|
+
cmd = [sys.executable, str(script_path), *forwarded_args]
|
|
63
|
+
return subprocess.call(cmd, cwd=str(repo_root()), env=env)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _add_common_io_args(p: argparse.ArgumentParser, *, include_method: bool = True, include_model: bool = True) -> None:
|
|
67
|
+
"""Shared --method / --model / --input / --output options."""
|
|
68
|
+
if include_method:
|
|
69
|
+
p.add_argument("--method", help="Processing method / backend (see step-specific choices).")
|
|
70
|
+
if include_model:
|
|
71
|
+
p.add_argument("--model", help="LLM model identifier for API-based methods.")
|
|
72
|
+
p.add_argument("-i", "--input", help="Input file or directory path.")
|
|
73
|
+
p.add_argument("-o", "--output", help="Output file or directory path.")
|
|
74
|
+
p.add_argument("--prompt-version", help="Prompt template version (for LLM-based steps).")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Subcommand handlers
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def cmd_transcribe(args: argparse.Namespace, extra: list[str]) -> int:
|
|
82
|
+
prepare_cli_transcribe(args, extra)
|
|
83
|
+
forwarded: list[str] = []
|
|
84
|
+
env: dict[str, str] = {}
|
|
85
|
+
if args.model:
|
|
86
|
+
forwarded += ["--model", args.model]
|
|
87
|
+
if args.timestamps:
|
|
88
|
+
forwarded += ["--timestamps"]
|
|
89
|
+
# --kind sets the conventional default directories; explicit -i/-o override.
|
|
90
|
+
if args.kind == "story":
|
|
91
|
+
env["BATCH_INPUT_DIR"] = "data/1_story_audio"
|
|
92
|
+
env["BATCH_OUTPUT_DIR"] = "output/story_audio-transcribed"
|
|
93
|
+
elif args.kind == "recall":
|
|
94
|
+
env["BATCH_INPUT_DIR"] = "data/4_recall_audio"
|
|
95
|
+
env["BATCH_OUTPUT_DIR"] = "output/recall_audio-transcribed"
|
|
96
|
+
if args.input:
|
|
97
|
+
env["BATCH_INPUT_DIR"] = args.input
|
|
98
|
+
if args.output:
|
|
99
|
+
env["BATCH_OUTPUT_DIR"] = args.output
|
|
100
|
+
if args.filter:
|
|
101
|
+
env["BATCH_ITEM_ID"] = args.filter
|
|
102
|
+
forwarded += extra
|
|
103
|
+
return _run_legacy_script("transcribe", forwarded, env_overrides=env or None)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def cmd_segment(args: argparse.Namespace, extra: list[str]) -> int:
|
|
107
|
+
prepare_cli_segment(args, extra)
|
|
108
|
+
forwarded: list[str] = []
|
|
109
|
+
env: dict[str, str] = {}
|
|
110
|
+
if args.method:
|
|
111
|
+
forwarded += ["--method", args.method]
|
|
112
|
+
if args.model:
|
|
113
|
+
forwarded += ["--model", args.model]
|
|
114
|
+
if args.prompt_version:
|
|
115
|
+
forwarded += ["--prompt-version", args.prompt_version]
|
|
116
|
+
if args.input:
|
|
117
|
+
# Script 2 --input expects a single file. For directories, use the
|
|
118
|
+
# batch-mode env var instead so users can point at a folder of stories.
|
|
119
|
+
if Path(args.input).is_dir():
|
|
120
|
+
env["BATCH_INPUT_DIR"] = args.input
|
|
121
|
+
else:
|
|
122
|
+
forwarded += ["--input", args.input]
|
|
123
|
+
if args.output:
|
|
124
|
+
forwarded += ["--output", args.output]
|
|
125
|
+
if args.list_prompts:
|
|
126
|
+
forwarded += ["--list-prompts"]
|
|
127
|
+
if args.list_models:
|
|
128
|
+
forwarded += ["--list-models"]
|
|
129
|
+
forwarded += extra
|
|
130
|
+
return _run_legacy_script("segment", forwarded, env_overrides=env or None)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def cmd_correct(args: argparse.Namespace, extra: list[str]) -> int:
|
|
134
|
+
prepare_cli_correct(args, extra)
|
|
135
|
+
forwarded: list[str] = []
|
|
136
|
+
if args.method:
|
|
137
|
+
forwarded += ["--method", args.method]
|
|
138
|
+
if args.input:
|
|
139
|
+
forwarded += ["--input-file", args.input]
|
|
140
|
+
if args.output:
|
|
141
|
+
forwarded += ["--output-dir", args.output]
|
|
142
|
+
if args.ollama_model:
|
|
143
|
+
forwarded += ["--ollama-model", args.ollama_model]
|
|
144
|
+
if args.prompt_file:
|
|
145
|
+
forwarded += ["--prompt-file", args.prompt_file]
|
|
146
|
+
forwarded += extra
|
|
147
|
+
return _run_legacy_script("correct", forwarded)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def cmd_parse(args: argparse.Namespace, extra: list[str]) -> int:
|
|
151
|
+
prepare_cli_parse(args, extra)
|
|
152
|
+
# Script 4 reads config primarily from env vars; translate accordingly.
|
|
153
|
+
env: dict[str, str] = {}
|
|
154
|
+
if args.method:
|
|
155
|
+
env["RECALL_PARSE_METHOD"] = args.method # "rules" | "ollama"
|
|
156
|
+
if args.model:
|
|
157
|
+
env["RECALL_PARSE_OLLAMA_MODEL"] = args.model
|
|
158
|
+
if args.prompt_version:
|
|
159
|
+
env["RECALL_PARSE_PROMPT"] = args.prompt_version
|
|
160
|
+
if args.input:
|
|
161
|
+
env["BATCH_INPUT_DIR"] = args.input
|
|
162
|
+
if args.output:
|
|
163
|
+
env["BATCH_OUTPUT_DIR"] = args.output
|
|
164
|
+
|
|
165
|
+
forwarded = list(extra)
|
|
166
|
+
if args.filter_pattern:
|
|
167
|
+
forwarded.append(args.filter_pattern)
|
|
168
|
+
return _run_legacy_script("parse", forwarded, env_overrides=env)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def cmd_match(args: argparse.Namespace, extra: list[str]) -> int:
|
|
172
|
+
prepare_cli_match(args, extra)
|
|
173
|
+
# Script 5 is env-driven.
|
|
174
|
+
env: dict[str, str] = {}
|
|
175
|
+
test_mode = bool(args.test_mode)
|
|
176
|
+
if args.method:
|
|
177
|
+
m = args.method.lower()
|
|
178
|
+
if m == "test":
|
|
179
|
+
test_mode = True
|
|
180
|
+
elif m in ("ollama", "gemma-ollama", "gemma"):
|
|
181
|
+
env["RECALL_RATING_BACKEND"] = "ollama"
|
|
182
|
+
elif m == "rmatch":
|
|
183
|
+
env["RECALL_RATING_BACKEND"] = "rmatch"
|
|
184
|
+
elif m in ("api", "anthropic", "openai"):
|
|
185
|
+
env["RECALL_RATING_BACKEND"] = "api"
|
|
186
|
+
if args.input:
|
|
187
|
+
env["BATCH_INPUT_DIR"] = args.input
|
|
188
|
+
if args.output:
|
|
189
|
+
env["BATCH_OUTPUT_DIR"] = args.output
|
|
190
|
+
if args.story_events:
|
|
191
|
+
env["BATCH_STORY_EVENTS_DIR"] = args.story_events
|
|
192
|
+
if test_mode:
|
|
193
|
+
env["TEST_MODE"] = "1"
|
|
194
|
+
return _run_legacy_script("match", list(extra), env_overrides=env)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def cmd_rate(args: argparse.Namespace, extra: list[str]) -> int:
|
|
198
|
+
prepare_cli_rate(args, extra)
|
|
199
|
+
forwarded: list[str] = []
|
|
200
|
+
env: dict[str, str] = {}
|
|
201
|
+
if args.method:
|
|
202
|
+
forwarded += ["--method", args.method]
|
|
203
|
+
if args.model:
|
|
204
|
+
forwarded += ["--model", args.model]
|
|
205
|
+
if args.prompt_version:
|
|
206
|
+
forwarded += ["--prompt-version", args.prompt_version]
|
|
207
|
+
if args.input:
|
|
208
|
+
# Script 6 --input expects a single events file. Use BATCH_INPUT_DIR
|
|
209
|
+
# for directory mode so a folder of stories can be processed.
|
|
210
|
+
if Path(args.input).is_dir():
|
|
211
|
+
env["BATCH_INPUT_DIR"] = args.input
|
|
212
|
+
else:
|
|
213
|
+
forwarded += ["--input", args.input]
|
|
214
|
+
if args.output:
|
|
215
|
+
forwarded += ["--output", args.output]
|
|
216
|
+
forwarded += extra
|
|
217
|
+
return _run_legacy_script("rate", forwarded, env_overrides=env or None)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def cmd_serve(args: argparse.Namespace, extra: list[str]) -> int:
|
|
221
|
+
"""Launch the Flask web UI in-process."""
|
|
222
|
+
import importlib.util
|
|
223
|
+
|
|
224
|
+
ensure_repo_on_path()
|
|
225
|
+
server_script = repo_root() / "server" / "web-interface.py"
|
|
226
|
+
if not server_script.exists():
|
|
227
|
+
print(f"narraters: server script not found: {server_script}", file=sys.stderr)
|
|
228
|
+
return 2
|
|
229
|
+
|
|
230
|
+
# Load server/web-interface.py as a module (its filename has a hyphen so
|
|
231
|
+
# we can't use a regular import statement).
|
|
232
|
+
spec = importlib.util.spec_from_file_location("narraters._legacy_server", server_script)
|
|
233
|
+
if spec is None or spec.loader is None:
|
|
234
|
+
print("narraters: failed to load server module", file=sys.stderr)
|
|
235
|
+
return 2
|
|
236
|
+
module = importlib.util.module_from_spec(spec)
|
|
237
|
+
spec.loader.exec_module(module)
|
|
238
|
+
|
|
239
|
+
if not hasattr(module, "app"):
|
|
240
|
+
print("narraters: server module did not expose a Flask `app`", file=sys.stderr)
|
|
241
|
+
return 2
|
|
242
|
+
|
|
243
|
+
# Default to loopback. The UI runs subprocesses on the user's behalf and
|
|
244
|
+
# has open file-write endpoints — binding to 0.0.0.0 by default would
|
|
245
|
+
# expose those to anyone on the local network. Users who genuinely need
|
|
246
|
+
# LAN access can pass --host 0.0.0.0 (warning is printed below).
|
|
247
|
+
host = args.host or "127.0.0.1"
|
|
248
|
+
port = args.port or 5000
|
|
249
|
+
debug = args.debug or (os.environ.get("FLASK_DEBUG", "0") == "1")
|
|
250
|
+
|
|
251
|
+
if host not in ("127.0.0.1", "localhost"):
|
|
252
|
+
print(
|
|
253
|
+
f"⚠️ Binding to {host} — the web UI will be reachable from other "
|
|
254
|
+
"machines on this network. Only do this on a trusted network.",
|
|
255
|
+
file=sys.stderr,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if not args.no_browser:
|
|
259
|
+
_open_browser_when_ready(host, port)
|
|
260
|
+
|
|
261
|
+
print(f"narRaters web UI starting on http://localhost:{port}")
|
|
262
|
+
module.app.run(host=host, port=port, debug=debug, use_reloader=False)
|
|
263
|
+
return 0
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _open_browser_when_ready(host: str, port: int) -> None:
|
|
267
|
+
"""Open the default browser to the local server, in a background thread."""
|
|
268
|
+
import threading
|
|
269
|
+
import time
|
|
270
|
+
import webbrowser
|
|
271
|
+
|
|
272
|
+
def opener() -> None:
|
|
273
|
+
time.sleep(1.0)
|
|
274
|
+
url = f"http://localhost:{port}"
|
|
275
|
+
try:
|
|
276
|
+
webbrowser.open(url)
|
|
277
|
+
except Exception:
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
threading.Thread(target=opener, daemon=True).start()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
# Parser construction
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
288
|
+
ensure_repo_on_path()
|
|
289
|
+
try:
|
|
290
|
+
from helpers.feedback_links import FEEDBACK_ISSUE_URL
|
|
291
|
+
except ImportError:
|
|
292
|
+
FEEDBACK_ISSUE_URL = "https://github.com/xianNeuro/narRaters/issues/new?template=feedback"
|
|
293
|
+
|
|
294
|
+
parser = argparse.ArgumentParser(
|
|
295
|
+
prog="narraters",
|
|
296
|
+
description=(
|
|
297
|
+
"narRaters — AI-assisted narrative processing with human-screening. "
|
|
298
|
+
"Run `narraters serve` for the web UI, or use the "
|
|
299
|
+
"per-step subcommands for scripted pipelines.\n\n"
|
|
300
|
+
f"Feedback and suggestions: {FEEDBACK_ISSUE_URL}"
|
|
301
|
+
),
|
|
302
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
303
|
+
)
|
|
304
|
+
parser.add_argument("-V", "--version", action="version", version=f"narRaters {__version__}")
|
|
305
|
+
sub = parser.add_subparsers(dest="command", metavar="<command>", required=True)
|
|
306
|
+
|
|
307
|
+
# transcribe
|
|
308
|
+
p_t = sub.add_parser("transcribe", help="Step 1: Transcribe story or recall audio to text (WhisperX/Whisper).")
|
|
309
|
+
p_t.add_argument("--model", help="Whisper model name (e.g. tiny, base, small, medium, large-v2, large-v3).")
|
|
310
|
+
p_t.add_argument("--timestamps", action="store_true", help="Also write Excel files with word-level timestamps.")
|
|
311
|
+
p_t.add_argument(
|
|
312
|
+
"--kind",
|
|
313
|
+
choices=["recall", "story"],
|
|
314
|
+
default="recall",
|
|
315
|
+
help="Which audio set to transcribe. recall=data/4_recall_audio (default), "
|
|
316
|
+
"story=data/1_story_audio. Sets the conventional input/output directories.",
|
|
317
|
+
)
|
|
318
|
+
p_t.add_argument("-i", "--input", help="Input audio directory (overrides the --kind default).")
|
|
319
|
+
p_t.add_argument("-o", "--output", help="Output directory (overrides the --kind default).")
|
|
320
|
+
p_t.add_argument("--filter", help="Only transcribe files whose name matches this item id.")
|
|
321
|
+
p_t.set_defaults(func=cmd_transcribe)
|
|
322
|
+
|
|
323
|
+
# segment — matches scripts/2_story-event-segment.py VALID_METHODS
|
|
324
|
+
p_s = sub.add_parser("segment", help="Step 2: Segment story transcripts into events.")
|
|
325
|
+
p_s.add_argument(
|
|
326
|
+
"--method",
|
|
327
|
+
choices=["clause", "fine", "coarse", "api"],
|
|
328
|
+
help="Segmentation method (clause=heuristic, fine/coarse=spaCy, api=LLM).",
|
|
329
|
+
)
|
|
330
|
+
_add_common_io_args(p_s, include_method=False)
|
|
331
|
+
p_s.add_argument("--list-prompts", action="store_true", help="List available prompt versions and exit.")
|
|
332
|
+
p_s.add_argument("--list-models", action="store_true", help="List supported models and exit.")
|
|
333
|
+
p_s.set_defaults(func=cmd_segment)
|
|
334
|
+
|
|
335
|
+
# correct — matches scripts/3_spell-grammar-correct.py choices
|
|
336
|
+
p_c = sub.add_parser("correct", help="Step 3: Spell/grammar correction of raw recall text.")
|
|
337
|
+
p_c.add_argument("--method", choices=["rules", "gemma-ollama"], help="Correction method.")
|
|
338
|
+
p_c.add_argument("--ollama-model", help="Ollama model tag for --method gemma-ollama (default: gemma4:e4b).")
|
|
339
|
+
p_c.add_argument("--prompt-file", help="Instructions file for --method gemma-ollama.")
|
|
340
|
+
p_c.add_argument("-i", "--input", help="Single recall .txt file to process.")
|
|
341
|
+
p_c.add_argument("-o", "--output", help="Output directory.")
|
|
342
|
+
p_c.set_defaults(func=cmd_correct)
|
|
343
|
+
|
|
344
|
+
# parse — script 4 reads RECALL_PARSE_METHOD env var; valid values: rules | ollama
|
|
345
|
+
p_p = sub.add_parser("parse", help="Step 4: Parse corrected recall text into segments.")
|
|
346
|
+
p_p.add_argument(
|
|
347
|
+
"--method",
|
|
348
|
+
choices=["rules", "ollama"],
|
|
349
|
+
help="Parsing method (rules=regex heuristics, ollama=local Gemma via Ollama).",
|
|
350
|
+
)
|
|
351
|
+
_add_common_io_args(p_p, include_method=False)
|
|
352
|
+
p_p.add_argument("--filter-pattern", nargs="?", help="Optional substring to filter recall files by.")
|
|
353
|
+
p_p.set_defaults(func=cmd_parse)
|
|
354
|
+
|
|
355
|
+
# match — script 5 reads RECALL_RATING_BACKEND env var; valid backends: api | gemma-ollama | rmatch
|
|
356
|
+
p_m = sub.add_parser("match", help="Step 5: Match recall segments to story events.")
|
|
357
|
+
p_m.add_argument(
|
|
358
|
+
"--method",
|
|
359
|
+
choices=["api", "gemma-ollama", "rmatch", "test"],
|
|
360
|
+
help="Matching backend (api=Anthropic Messages API, gemma-ollama=local Gemma, rmatch=embedding match, test=simulated/no API).",
|
|
361
|
+
)
|
|
362
|
+
_add_common_io_args(p_m, include_method=False, include_model=False)
|
|
363
|
+
p_m.add_argument("--story-events", help="Directory containing story event Excel files (default: data/3_story_events).")
|
|
364
|
+
p_m.add_argument("--test-mode", action="store_true", help="Run in simulated/test mode (no API calls). Equivalent to --method test.")
|
|
365
|
+
p_m.set_defaults(func=cmd_match)
|
|
366
|
+
|
|
367
|
+
# rate — matches scripts/6_causal-rater.py VALID_METHODS
|
|
368
|
+
p_r = sub.add_parser("rate", help="Step 6: Rate causal relationships between event pairs.")
|
|
369
|
+
p_r.add_argument(
|
|
370
|
+
"--method",
|
|
371
|
+
choices=["linguistic", "api", "manual"],
|
|
372
|
+
help="Rating method (linguistic=regex/spaCy heuristics, api=LLM, manual=write empty scaffold for hand rating).",
|
|
373
|
+
)
|
|
374
|
+
_add_common_io_args(p_r, include_method=False)
|
|
375
|
+
p_r.set_defaults(func=cmd_rate)
|
|
376
|
+
|
|
377
|
+
# serve
|
|
378
|
+
p_serve = sub.add_parser("serve", help="Launch the web UI (Flask) for interactive review and editing.")
|
|
379
|
+
p_serve.add_argument("--host", default=None, help="Bind address (default: 127.0.0.1, loopback only). Pass 0.0.0.0 to allow LAN access — only on a trusted network.")
|
|
380
|
+
p_serve.add_argument("--port", type=int, default=None, help="Bind port (default: 5000).")
|
|
381
|
+
p_serve.add_argument("--debug", action="store_true", help="Enable Flask debug mode.")
|
|
382
|
+
p_serve.add_argument("--no-browser", action="store_true", help="Do not auto-open the browser on startup.")
|
|
383
|
+
p_serve.set_defaults(func=cmd_serve)
|
|
384
|
+
|
|
385
|
+
return parser
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def main(argv: list[str] | None = None) -> int:
|
|
389
|
+
ensure_repo_on_path()
|
|
390
|
+
parser = build_parser()
|
|
391
|
+
args, extra = parser.parse_known_args(argv)
|
|
392
|
+
try:
|
|
393
|
+
return args.func(args, extra)
|
|
394
|
+
except (subprocess.CalledProcessError, RuntimeError) as e:
|
|
395
|
+
print(f"narraters: dependency setup failed: {e}", file=sys.stderr)
|
|
396
|
+
return 1
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
if __name__ == "__main__":
|
|
400
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Helper modules for narrative-processor tooling and checks.
|