Add hosted npm client

#4
README.md CHANGED
@@ -27,6 +27,29 @@ The app includes two modes:
27
 
28
  The model never edits code directly. It only proposes comments, and the app rejects any annotated file whose semantic AST changes.
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  ## Hackathon Fit
31
 
32
  - **Track:** Backyard AI
 
27
 
28
  The model never edits code directly. It only proposes comments, and the app rejects any annotated file whose semantic AST changes.
29
 
30
+ ## CLI Usage
31
+
32
+ Use the hosted Space from Node/npm without installing anything:
33
+
34
+ ```bash
35
+ npx between-the-lines-cli path/to/file.py --model base --summary
36
+ ```
37
+
38
+ By default, this creates an annotated sibling file next to the input:
39
+
40
+ ```text
41
+ path/to/file.annotated.py
42
+ ```
43
+
44
+ You can also choose an output path or replace the input file:
45
+
46
+ ```bash
47
+ npx between-the-lines-cli path/to/file.py --model base --output annotated.py
48
+ npx between-the-lines-cli path/to/file.py --model tuned --in-place
49
+ ```
50
+
51
+ `--model base` uses the richer Mellum2 GGUF path. `--model tuned` uses the LoRA adapter for shorter comments. Both modes run the AST validation before writing output.
52
+
53
  ## Hackathon Fit
54
 
55
  - **Track:** Backyard AI
app.py CHANGED
@@ -1,12 +1,11 @@
1
- import ast
2
  import html
3
- from dataclasses import dataclass
4
  from pathlib import Path
5
  from typing import Any
6
 
7
  import gradio as gr
8
 
9
- from btl.model import ModelUnavailableError, generate_comment, generate_comment_with_llm, load_llm
 
10
 
11
 
12
  CSS = """
@@ -156,130 +155,18 @@ code {
156
  """
157
 
158
 
159
- @dataclass(frozen=True)
160
- class BlockInfo:
161
- kind: str
162
- name: str
163
- lineno: int
164
- source: str
165
-
166
-
167
- def _parse_python(source: str) -> ast.Module:
168
- return ast.parse(source)
169
-
170
-
171
- def _strip_docstrings(node: ast.AST) -> ast.AST:
172
- copied = ast.fix_missing_locations(ast.parse(ast.unparse(node)))
173
- for child in ast.walk(copied):
174
- if isinstance(child, (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
175
- if (
176
- child.body
177
- and isinstance(child.body[0], ast.Expr)
178
- and isinstance(child.body[0].value, ast.Constant)
179
- and isinstance(child.body[0].value.value, str)
180
- ):
181
- child.body = child.body[1:]
182
- return copied
183
-
184
-
185
- def _semantic_ast_dump(source: str) -> str:
186
- tree = _parse_python(source)
187
- stripped = _strip_docstrings(tree)
188
- return ast.dump(stripped, include_attributes=False)
189
-
190
-
191
- def _collect_blocks(tree: ast.Module, source: str) -> list[BlockInfo]:
192
- blocks: list[BlockInfo] = []
193
- for node in ast.walk(tree):
194
- if isinstance(node, ast.ClassDef):
195
- block_source = ast.get_source_segment(source, node) or ast.unparse(node)
196
- blocks.append(BlockInfo("class", node.name, node.lineno, block_source))
197
- elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
198
- kind = "async function" if isinstance(node, ast.AsyncFunctionDef) else "function"
199
- block_source = ast.get_source_segment(source, node) or ast.unparse(node)
200
- blocks.append(BlockInfo(kind, node.name, node.lineno, block_source))
201
- return sorted(blocks, key=lambda item: item.lineno)
202
-
203
-
204
- def _placeholder_summary(blocks: list[BlockInfo]) -> str:
205
- if not blocks:
206
- return "This Python file has no classes or functions to annotate yet."
207
- names = ", ".join(f"{block.kind} `{block.name}`" for block in blocks[:8])
208
- overflow = "" if len(blocks) <= 8 else f", plus {len(blocks) - 8} more"
209
- return f"This file defines {names}{overflow}. Model-generated summaries will replace this deterministic placeholder."
210
-
211
-
212
- def _insert_comments(source: str, comments: dict[int, str]) -> str:
213
- lines = source.splitlines()
214
- inserts: dict[int, list[str]] = {}
215
- for lineno, comment_text in comments.items():
216
- indent = len(lines[lineno - 1]) - len(lines[lineno - 1].lstrip())
217
- inserts.setdefault(lineno - 1, []).append(" " * indent + comment_text)
218
-
219
- annotated: list[str] = []
220
- for index, line in enumerate(lines):
221
- annotated.extend(inserts.get(index, []))
222
- annotated.append(line)
223
- return "\n".join(annotated) + ("\n" if source.endswith("\n") else "")
224
-
225
-
226
  MODEL_LABELS = {
227
  "Base Mellum2 (richer)": "base",
228
  "Fine-tuned LoRA (concise)": "tuned",
229
  }
230
 
231
 
232
- def _generate_block_comments(blocks: list[BlockInfo], model_label: str) -> tuple[dict[int, str], list[str]]:
233
- comments: dict[int, str] = {}
234
- notes: list[str] = []
235
- model_variant = MODEL_LABELS.get(model_label, "base")
236
- llm = load_llm() if model_variant == "base" else None
237
- for block in blocks:
238
- try:
239
- if model_variant == "base":
240
- comments[block.lineno] = generate_comment_with_llm(llm, block.kind, block.name, block.source)
241
- else:
242
- comments[block.lineno] = generate_comment(block.kind, block.name, block.source, variant="tuned")
243
- except Exception as exc:
244
- comments[block.lineno] = f"# TODO(model): Could not explain {block.kind} `{block.name}`."
245
- notes.append(f"{block.name}: model generation failed ({type(exc).__name__}).")
246
- return comments, notes
247
-
248
-
249
  def annotate_code(source: str, model_label: str) -> tuple[str, str, str]:
250
- source = source.strip("\ufeff")
251
- if not source.strip():
252
- return "", "", "Paste a Python file to annotate."
253
-
254
- try:
255
- original_tree = _parse_python(source)
256
- except SyntaxError as exc:
257
- return "", "", f"Syntax error on line {exc.lineno}: {html.escape(exc.msg)}"
258
-
259
- blocks = _collect_blocks(original_tree, source)
260
- if not blocks:
261
- return _placeholder_summary(blocks), source, "Parsed successfully. No classes or functions to annotate."
262
-
263
  try:
264
- comments, generation_notes = _generate_block_comments(blocks, model_label)
265
  except ModelUnavailableError as exc:
266
- return _placeholder_summary(blocks), source, f"Model unavailable: {html.escape(str(exc))}"
267
-
268
- annotated = _insert_comments(source, comments)
269
-
270
- try:
271
- same_ast = _semantic_ast_dump(source) == _semantic_ast_dump(annotated)
272
- except SyntaxError as exc:
273
- return "", annotated, f"Generated annotation failed to parse on line {exc.lineno}: {html.escape(exc.msg)}"
274
-
275
- status = (
276
- f"Validated: parsed {len(blocks)} block(s), inserted {model_label} comments, semantic AST unchanged."
277
- if same_ast
278
- else "Rejected: annotation changed the semantic AST."
279
- )
280
- if generation_notes:
281
- status += "\n" + "\n".join(generation_notes)
282
- return _placeholder_summary(blocks), annotated if same_ast else source, status
283
 
284
 
285
  def load_uploaded_python(file_obj: Any) -> tuple[str, str]:
@@ -298,11 +185,11 @@ def load_uploaded_python(file_obj: Any) -> tuple[str, str]:
298
  return "", f"Could not read `{path.name}`: {exc}"
299
 
300
  try:
301
- tree = _parse_python(source)
302
  except SyntaxError as exc:
303
  return source, f"Loaded `{path.name}`, but parsing failed on line {exc.lineno}: {html.escape(exc.msg)}"
304
 
305
- blocks = _collect_blocks(tree, source)
306
  return source, f"Loaded `{path.name}`. Parsed {len(blocks)} class/function block(s)."
307
 
308
 
@@ -373,6 +260,7 @@ def build_app() -> gr.Blocks:
373
  annotate_code,
374
  inputs=[input_code, model_choice],
375
  outputs=[summary, output_code, status],
 
376
  )
377
  upload_file.upload(
378
  load_uploaded_python,
 
 
1
  import html
 
2
  from pathlib import Path
3
  from typing import Any
4
 
5
  import gradio as gr
6
 
7
+ from btl.annotate import annotate_python, collect_blocks, parse_python
8
+ from btl.model import ModelUnavailableError
9
 
10
 
11
  CSS = """
 
155
  """
156
 
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  MODEL_LABELS = {
159
  "Base Mellum2 (richer)": "base",
160
  "Fine-tuned LoRA (concise)": "tuned",
161
  }
162
 
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  def annotate_code(source: str, model_label: str) -> tuple[str, str, str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  try:
166
+ result = annotate_python(source, MODEL_LABELS.get(model_label, "base"))
167
  except ModelUnavailableError as exc:
168
+ return "", source, f"Model unavailable: {html.escape(str(exc))}"
169
+ return result.summary, result.annotated_source, result.status
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
 
172
  def load_uploaded_python(file_obj: Any) -> tuple[str, str]:
 
185
  return "", f"Could not read `{path.name}`: {exc}"
186
 
187
  try:
188
+ tree = parse_python(source)
189
  except SyntaxError as exc:
190
  return source, f"Loaded `{path.name}`, but parsing failed on line {exc.lineno}: {html.escape(exc.msg)}"
191
 
192
+ blocks = collect_blocks(tree, source)
193
  return source, f"Loaded `{path.name}`. Parsed {len(blocks)} class/function block(s)."
194
 
195
 
 
260
  annotate_code,
261
  inputs=[input_code, model_choice],
262
  outputs=[summary, output_code, status],
263
+ api_name="annotate",
264
  )
265
  upload_file.upload(
266
  load_uploaded_python,
btl/annotate.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ from dataclasses import dataclass
3
+ from typing import Literal
4
+
5
+ from .model import ModelUnavailableError, generate_comment, generate_comment_with_llm, load_llm
6
+
7
+
8
+ ModelChoice = Literal["base", "tuned"]
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class BlockInfo:
13
+ kind: str
14
+ name: str
15
+ lineno: int
16
+ source: str
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class AnnotationResult:
21
+ summary: str
22
+ annotated_source: str
23
+ status: str
24
+ valid: bool
25
+ block_count: int
26
+ notes: tuple[str, ...] = ()
27
+
28
+
29
+ def parse_python(source: str) -> ast.Module:
30
+ return ast.parse(source)
31
+
32
+
33
+ def strip_docstrings(node: ast.AST) -> ast.AST:
34
+ copied = ast.fix_missing_locations(ast.parse(ast.unparse(node)))
35
+ for child in ast.walk(copied):
36
+ if isinstance(child, (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
37
+ if (
38
+ child.body
39
+ and isinstance(child.body[0], ast.Expr)
40
+ and isinstance(child.body[0].value, ast.Constant)
41
+ and isinstance(child.body[0].value.value, str)
42
+ ):
43
+ child.body = child.body[1:]
44
+ return copied
45
+
46
+
47
+ def semantic_ast_dump(source: str) -> str:
48
+ tree = parse_python(source)
49
+ stripped = strip_docstrings(tree)
50
+ return ast.dump(stripped, include_attributes=False)
51
+
52
+
53
+ def collect_blocks(tree: ast.Module, source: str) -> list[BlockInfo]:
54
+ blocks: list[BlockInfo] = []
55
+ for node in ast.walk(tree):
56
+ if isinstance(node, ast.ClassDef):
57
+ block_source = ast.get_source_segment(source, node) or ast.unparse(node)
58
+ blocks.append(BlockInfo("class", node.name, node.lineno, block_source))
59
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
60
+ kind = "async function" if isinstance(node, ast.AsyncFunctionDef) else "function"
61
+ block_source = ast.get_source_segment(source, node) or ast.unparse(node)
62
+ blocks.append(BlockInfo(kind, node.name, node.lineno, block_source))
63
+ return sorted(blocks, key=lambda item: item.lineno)
64
+
65
+
66
+ def summarize_blocks(blocks: list[BlockInfo]) -> str:
67
+ if not blocks:
68
+ return "This Python file has no classes or functions to annotate yet."
69
+ names = ", ".join(f"{block.kind} `{block.name}`" for block in blocks[:8])
70
+ overflow = "" if len(blocks) <= 8 else f", plus {len(blocks) - 8} more"
71
+ return f"This file defines {names}{overflow}."
72
+
73
+
74
+ def insert_comments(source: str, comments: dict[int, str]) -> str:
75
+ lines = source.splitlines()
76
+ inserts: dict[int, list[str]] = {}
77
+ for lineno, comment_text in comments.items():
78
+ indent = len(lines[lineno - 1]) - len(lines[lineno - 1].lstrip())
79
+ inserts.setdefault(lineno - 1, []).append(" " * indent + comment_text)
80
+
81
+ annotated: list[str] = []
82
+ for index, line in enumerate(lines):
83
+ annotated.extend(inserts.get(index, []))
84
+ annotated.append(line)
85
+ return "\n".join(annotated) + ("\n" if source.endswith("\n") else "")
86
+
87
+
88
+ def generate_block_comments(blocks: list[BlockInfo], model_choice: ModelChoice = "base") -> tuple[dict[int, str], list[str]]:
89
+ comments: dict[int, str] = {}
90
+ notes: list[str] = []
91
+ llm = load_llm() if model_choice == "base" else None
92
+
93
+ for block in blocks:
94
+ try:
95
+ if model_choice == "base":
96
+ comments[block.lineno] = generate_comment_with_llm(llm, block.kind, block.name, block.source)
97
+ else:
98
+ comments[block.lineno] = generate_comment(block.kind, block.name, block.source, variant="tuned")
99
+ except ModelUnavailableError:
100
+ raise
101
+ except Exception as exc:
102
+ comments[block.lineno] = f"# TODO(model): Could not explain {block.kind} `{block.name}`."
103
+ notes.append(f"{block.name}: model generation failed ({type(exc).__name__}).")
104
+ return comments, notes
105
+
106
+
107
+ def annotate_python(source: str, model_choice: ModelChoice = "base") -> AnnotationResult:
108
+ source = source.strip("\ufeff")
109
+ if not source.strip():
110
+ return AnnotationResult("", "", "Paste a Python file to annotate.", False, 0)
111
+
112
+ try:
113
+ original_tree = parse_python(source)
114
+ except SyntaxError as exc:
115
+ return AnnotationResult("", "", f"Syntax error on line {exc.lineno}: {exc.msg}", False, 0)
116
+
117
+ blocks = collect_blocks(original_tree, source)
118
+ summary = summarize_blocks(blocks)
119
+ if not blocks:
120
+ return AnnotationResult(summary, source, "Parsed successfully. No classes or functions to annotate.", True, 0)
121
+
122
+ comments, generation_notes = generate_block_comments(blocks, model_choice)
123
+ annotated = insert_comments(source, comments)
124
+
125
+ try:
126
+ same_ast = semantic_ast_dump(source) == semantic_ast_dump(annotated)
127
+ except SyntaxError as exc:
128
+ return AnnotationResult(
129
+ "",
130
+ annotated,
131
+ f"Generated annotation failed to parse on line {exc.lineno}: {exc.msg}",
132
+ False,
133
+ len(blocks),
134
+ tuple(generation_notes),
135
+ )
136
+
137
+ status = (
138
+ f"Validated: parsed {len(blocks)} block(s), inserted {model_choice} model comments, semantic AST unchanged."
139
+ if same_ast
140
+ else "Rejected: annotation changed the semantic AST."
141
+ )
142
+ if generation_notes:
143
+ status += "\n" + "\n".join(generation_notes)
144
+
145
+ return AnnotationResult(
146
+ summary,
147
+ annotated if same_ast else source,
148
+ status,
149
+ same_ast,
150
+ len(blocks),
151
+ tuple(generation_notes),
152
+ )
packages/npm/README.md ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # between-the-lines-cli
2
+
3
+ Hosted CLI client for [between-the-lines](https://huggingface.co/spaces/build-small-hackathon/between-the-lines).
4
+
5
+ Run without installing:
6
+
7
+ ```bash
8
+ npx between-the-lines-cli path/to/file.py --model base --summary
9
+ ```
10
+
11
+ By default, the CLI creates a sibling file next to the input:
12
+
13
+ ```text
14
+ path/to/file.annotated.py
15
+ ```
16
+
17
+ Install globally if you prefer a reusable command:
18
+
19
+ ```bash
20
+ npm install -g between-the-lines-cli
21
+ between-the-lines path/to/file.py --model base
22
+ ```
23
+
24
+ Choose an exact output path or replace the input file:
25
+
26
+ ```bash
27
+ npx between-the-lines-cli path/to/file.py --model base --output annotated.py
28
+ npx between-the-lines-cli path/to/file.py --model tuned --in-place
29
+ ```
30
+
31
+ The CLI sends the file contents to the hosted Hugging Face Space and writes back the AST-validated annotated code.
32
+
33
+ ## Options
34
+
35
+ ```text
36
+ --model base|tuned Comment model to use. Default: base
37
+ --output <file.py> Write annotated code to a specific file
38
+ --in-place Replace the input file after validation
39
+ --summary Print summary and validation status to stderr
40
+ --space <repo-id|url> Override the hosted Gradio Space
41
+ ```
packages/npm/package.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "between-the-lines-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI client for the hosted between-the-lines Python annotator.",
5
+ "type": "module",
6
+ "bin": {
7
+ "between-the-lines": "./src/cli.js",
8
+ "btl": "./src/cli.js"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "dependencies": {
18
+ "@gradio/client": "^1.15.0"
19
+ },
20
+ "keywords": [
21
+ "huggingface",
22
+ "gradio",
23
+ "python",
24
+ "ast",
25
+ "code-comments",
26
+ "cli"
27
+ ],
28
+ "license": "MIT"
29
+ }
packages/npm/src/cli.js ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+
7
+ const DEFAULT_SPACE = "build-small-hackathon/between-the-lines";
8
+ const MODEL_LABELS = {
9
+ base: "Base Mellum2 (richer)",
10
+ tuned: "Fine-tuned LoRA (concise)"
11
+ };
12
+
13
+ function usage() {
14
+ return `between-the-lines <file.py> [options]
15
+
16
+ Options:
17
+ --model base|tuned Comment model to use. Default: base
18
+ --output <file.py> Write annotated code to a specific file
19
+ --in-place Replace the input file after validation
20
+ --summary Print summary and validation status to stderr
21
+ --space <repo-id|url> Hosted Gradio Space. Default: ${DEFAULT_SPACE}
22
+ -h, --help Show this help
23
+ `;
24
+ }
25
+
26
+ function parseArgs(argv) {
27
+ const args = {
28
+ input: "",
29
+ model: "base",
30
+ output: "",
31
+ inPlace: false,
32
+ summary: false,
33
+ space: DEFAULT_SPACE
34
+ };
35
+
36
+ for (let index = 0; index < argv.length; index += 1) {
37
+ const arg = argv[index];
38
+ if (arg === "-h" || arg === "--help") {
39
+ args.help = true;
40
+ } else if (arg === "--model") {
41
+ args.model = argv[++index] ?? "";
42
+ } else if (arg === "--output" || arg === "-o") {
43
+ args.output = argv[++index] ?? "";
44
+ } else if (arg === "--in-place") {
45
+ args.inPlace = true;
46
+ } else if (arg === "--summary") {
47
+ args.summary = true;
48
+ } else if (arg === "--space") {
49
+ args.space = argv[++index] ?? "";
50
+ } else if (!args.input) {
51
+ args.input = arg;
52
+ } else {
53
+ throw new Error(`unexpected argument: ${arg}`);
54
+ }
55
+ }
56
+
57
+ if (args.help) {
58
+ return args;
59
+ }
60
+ if (!args.input) {
61
+ throw new Error("missing input file");
62
+ }
63
+ if (!Object.hasOwn(MODEL_LABELS, args.model)) {
64
+ throw new Error("--model must be 'base' or 'tuned'");
65
+ }
66
+ if (args.output && args.inPlace) {
67
+ throw new Error("use either --output or --in-place, not both");
68
+ }
69
+ return args;
70
+ }
71
+
72
+ function defaultOutputPath(inputPath) {
73
+ const parsed = path.parse(inputPath);
74
+ return path.join(parsed.dir, `${parsed.name}.annotated${parsed.ext || ".py"}`);
75
+ }
76
+
77
+ async function predict(client, source, modelLabel) {
78
+ try {
79
+ return await client.predict("/annotate", [source, modelLabel]);
80
+ } catch (arrayError) {
81
+ try {
82
+ return await client.predict("/annotate", {
83
+ source,
84
+ model_label: modelLabel
85
+ });
86
+ } catch {
87
+ throw arrayError;
88
+ }
89
+ }
90
+ }
91
+
92
+ async function main() {
93
+ let args;
94
+ try {
95
+ args = parseArgs(process.argv.slice(2));
96
+ } catch (error) {
97
+ console.error(`error: ${error.message}\n`);
98
+ console.error(usage());
99
+ return 2;
100
+ }
101
+
102
+ if (args.help) {
103
+ console.log(usage());
104
+ return 0;
105
+ }
106
+
107
+ const source = await readFile(args.input, "utf8");
108
+ const { Client } = await import("@gradio/client");
109
+ const client = await Client.connect(args.space);
110
+ const result = await predict(client, source, MODEL_LABELS[args.model]);
111
+ const [summary, annotated, status] = result.data;
112
+
113
+ if (args.summary) {
114
+ if (summary) {
115
+ console.error(summary);
116
+ }
117
+ console.error(status);
118
+ }
119
+
120
+ if (!String(status).startsWith("Validated:") && !String(status).startsWith("Parsed successfully.")) {
121
+ console.error(status);
122
+ return 1;
123
+ }
124
+
125
+ if (args.inPlace) {
126
+ await writeFile(args.input, annotated, "utf8");
127
+ } else if (args.output) {
128
+ await writeFile(args.output, annotated, "utf8");
129
+ } else {
130
+ const outputPath = defaultOutputPath(args.input);
131
+ await writeFile(outputPath, annotated, "utf8");
132
+ console.error(`Wrote ${outputPath}`);
133
+ }
134
+ return 0;
135
+ }
136
+
137
+ main()
138
+ .then((code) => {
139
+ process.exitCode = code;
140
+ })
141
+ .catch((error) => {
142
+ console.error(`error: ${error.message}`);
143
+ process.exitCode = 1;
144
+ });
pyproject.toml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "between-the-lines"
7
+ version = "0.1.0"
8
+ description = "Annotate Python files with small-model comments and AST validation."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "accelerate",
13
+ "bitsandbytes",
14
+ "gradio==5.50.0",
15
+ "huggingface_hub",
16
+ "llama-cpp-python",
17
+ "peft",
18
+ "safetensors",
19
+ "torch",
20
+ "transformers @ git+https://github.com/huggingface/transformers.git",
21
+ "truststore",
22
+ ]
23
+
24
+ [tool.setuptools.packages.find]
25
+ include = ["btl*"]