Download code/coach/infer.py from nonsodev/essay-coach: direct link, hf CLI and curl.
- Browser
- Download file 5.08 kB
-
https://huggingface.co/nonsodev/essay-coach/resolve/main/code/coach/infer.py
- Command line
-
hf download hf://nonsodev/essay-coach/code/coach/infer.py
-
curl -L -o infer.py https://huggingface.co/nonsodev/essay-coach/resolve/main/code/coach/infer.py
5.08 kB
| """One essay in, the coach's reading out: six scores, argument parts with effectiveness, and plain-words notes. | |
| coach = Coach("/data/runs/scores-v1", "/data/runs/parts-v1") | |
| coach.read(text) # dict | |
| """ | |
| import json | |
| import os | |
| import numpy as np | |
| import torch | |
| from safetensors.torch import load_file | |
| from transformers import AutoTokenizer | |
| from coach.data import TRAITS, TYPES, with_breaks | |
| from coach.decode import decode | |
| from coach.model import PartsModel, ScoreModel | |
| TRAIT_WORDS = { | |
| "cohesion": "how the ideas link up and flow from one to the next", | |
| "syntax": "sentence structure and variety", | |
| "vocabulary": "the range of words used", | |
| "phraseology": "natural phrasing and word combinations", | |
| "grammar": "grammatical accuracy", | |
| "conventions": "spelling, punctuation and capitalisation", | |
| } | |
| class Coach: | |
| def __init__(self, scores_dir, parts_dir, device=None): | |
| self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") | |
| sc = json.load(open(os.path.join(scores_dir, "config.json"))) | |
| self.score_tok = AutoTokenizer.from_pretrained(os.path.join(scores_dir, "tokenizer")) | |
| self.score_net = ScoreModel(sc["backbone"], n_tokens=len(self.score_tok)).to(self.device).eval() | |
| self.score_net.load_state_dict(load_file(os.path.join(scores_dir, "model.safetensors"))) | |
| self.score_len = sc["max_len"] | |
| pc = json.load(open(os.path.join(parts_dir, "config.json"))) | |
| self.parts_tok = AutoTokenizer.from_pretrained(os.path.join(parts_dir, "tokenizer")) | |
| self.parts_net = PartsModel(pc["backbone"], n_tokens=len(self.parts_tok)).to(self.device).eval() | |
| self.parts_net.load_state_dict(load_file(os.path.join(parts_dir, "model.safetensors"))) | |
| self.parts_len = pc["max_len"] | |
| def scores(self, text): | |
| enc = self.score_tok(with_breaks(text), truncation=True, max_length=self.score_len, return_tensors="pt").to(self.device) | |
| out = self.score_net(enc["input_ids"], enc["attention_mask"])[0].float().cpu().numpy() | |
| return {t: float(np.clip(round(v * 2) / 2, 1.0, 5.0)) for t, v in zip(TRAITS, out)}, {t: float(v) for t, v in zip(TRAITS, out)} | |
| def parts(self, text): | |
| enc = self.parts_tok(text, truncation=True, max_length=self.parts_len, return_offsets_mapping=True, return_tensors="pt") | |
| offs = [None if (s == e == 0) else (int(s), int(e)) for s, e in enc.pop("offset_mapping")[0].tolist()] | |
| offs[0] = None | |
| enc = enc.to(self.device) | |
| lb, le = self.parts_net(enc["input_ids"], enc["attention_mask"]) | |
| return decode(lb[0].float().cpu().numpy(), le[0].float().cpu().numpy(), offs, text) | |
| def read(self, text): | |
| text = text.replace("\r\n", "\n").strip() | |
| snapped, raw = self.scores(text) | |
| parts = self.parts(text) | |
| return {"scores": snapped, "scores_raw": raw, "parts": parts, "notes": notes(snapped, parts, text)} | |
| def notes(scores, parts, text): | |
| """Plain-words feedback from the numbers: the two weakest skills, and what is missing or weak in the argument.""" | |
| out = [] | |
| weakest = sorted(scores, key=scores.get)[:2] | |
| for t in weakest: | |
| out.append({"kind": "skill", "trait": t, "score": scores[t], "text": f"Weakest area: {TRAIT_WORDS[t]} ({scores[t]:.1f} of 5)."}) | |
| have = {p["type"] for p in parts} | |
| if "Position" not in have: | |
| out.append({"kind": "structure", "text": "No clear position: say early on which side you take, in one sentence."}) | |
| if "Lead" not in have: | |
| out.append({"kind": "structure", "text": "No lead: the first lines should pull the reader in and point at the question."}) | |
| if "Counterclaim" not in have: | |
| out.append({"kind": "structure", "text": "No counterclaim: name the strongest view against yours, then answer it. Examiners look for this."}) | |
| elif "Rebuttal" not in have: | |
| out.append({"kind": "structure", "text": "You raise the other side but never answer it: add a rebuttal after the counterclaim."}) | |
| if "Concluding Statement" not in have: | |
| out.append({"kind": "structure", "text": "No conclusion: finish by restating your position and the main reason for it."}) | |
| claims = [p for p in parts if p["type"] == "Claim"] | |
| for c in claims: | |
| nxt = [p for p in parts if p["start"] > c["start"]] | |
| if not nxt or nxt[0]["type"] != "Evidence": | |
| out.append({"kind": "part", "start": c["start"], "end": c["end"], | |
| "text": "This claim has no evidence right after it: add an example, a fact or a reason."}) | |
| for p in parts: | |
| if p["effectiveness"] == "Ineffective": | |
| out.append({"kind": "part", "start": p["start"], "end": p["end"], | |
| "text": f"This {p['type'].lower()} reads as ineffective: make it specific and tie it to your position."}) | |
| words = len(text.split()) | |
| if words < 250: | |
| out.append({"kind": "length", "text": f"{words} words. IELTS Task 2 needs at least 250; short essays lose marks before they are read."}) | |
| return out | |