"""Push finished runs from the Modal volume to Hugging Face (public, CC BY-NC-SA 4.0 like the data). MODAL_PROFILE=labs3 modal run scripts/upload.py # scores-v1 parts-v1 band-v1 MODAL_PROFILE=labs3 modal run scripts/upload.py --runs scores-v2 """ import modal app = modal.App("essay-coach-upload") vol = modal.Volume.from_name("essay-data") image = modal.Image.debian_slim(python_version="3.11").pip_install("huggingface_hub") HF_REPO = "nonsodev/essay-coach" README = """--- license: cc-by-nc-sa-4.0 language: en tags: [essay-scoring, argument-mining, deberta-v3, ielts] --- # Essay coach Three small models behind the Labs essay coach page: six analytic writing scores (ELLIPSE), argument parts with effectiveness (PERSUADE 2.0), and a ridge band estimate fitted on human-banded IELTS essays. - `runs/scores-v1`: deberta-v3-base regression head, MCRMSE 0.446 on the ELLIPSE test split (2,571 essays). - `runs/parts-v1`: deberta-v3-base token model, BIO over 7 part types plus effectiveness; span F1 0.656 and effectiveness log loss 0.478 on 2,000 held-out PERSUADE essays. - `runs/band-v1`: `band.json`, ridge on the six scores + part counts + length; 5-fold CV RMSE 0.79 bands, within half a band 67% of the time on 1,435 essays. An estimate, not a grade. - `code/`: the training, decoding and serving code as of upload. Trained on ELLIPSE and PERSUADE 2.0 (both CC BY-NC-SA 4.0), so these weights carry the same licence: share alike, non-commercial. Backbone: microsoft/deberta-v3-base. """ @app.function(image=image, volumes={"/data": vol}, secrets=[modal.Secret.from_name("huggingface")], timeout=3600) def upload(runs: list[str], code: dict[str, str]): import os import tempfile from huggingface_hub import HfApi api = HfApi() api.create_repo(HF_REPO, exist_ok=True) for tag in runs: run = f"/data/runs/{tag}" print(tag, sorted(os.listdir(run)), flush=True) api.upload_folder(folder_path=run, repo_id=HF_REPO, path_in_repo=f"runs/{tag}", commit_message=f"upload {tag}") with tempfile.TemporaryDirectory() as d: for rel, text in code.items(): p = os.path.join(d, rel) os.makedirs(os.path.dirname(p), exist_ok=True) open(p, "w").write(text) open(os.path.join(d, "README.md"), "w").write(README) api.upload_folder(folder_path=d, repo_id=HF_REPO, path_in_repo="", commit_message="code snapshot + README") print(f"done: https://huggingface.co/{HF_REPO}") @app.local_entrypoint() def main(runs: str = "scores-v1,parts-v1,band-v1"): import glob import os root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) code = {} for pat in ("coach/*.py", "scripts/*.py", "web/*.html", "PLAN.md"): for p in glob.glob(os.path.join(root, pat)): code["code/" + os.path.relpath(p, root)] = open(p).read() upload.remote(runs.split(","), code)