File size: 2,945 Bytes
e1679fa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
"""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)