Spaces:
Running on Zero
Running on Zero
Download app.py from legitcoconut/droid-code-checker: direct link, hf CLI and curl.
- Browser
- Download file 7.99 kB
-
https://huggingface.co/spaces/legitcoconut/droid-code-checker/resolve/main/app.py
- Command line
-
hf download hf://spaces/legitcoconut/droid-code-checker/app.py
-
curl -L -o app.py https://huggingface.co/spaces/legitcoconut/droid-code-checker/resolve/main/app.py
7.99 kB
| """Code Authorship Checker — DroidDetect-Large (ModernBERT) + static forensics. | |
| Token-protected Gradio app exposing one API endpoint (`api_name="detect"`). | |
| Drop-in replacement for the CodeT5+ Space: same inputs `[code, token]`, and the | |
| response still carries `verdict` / `ai_probability` / `token_count` / | |
| `truncated`, so Code Arena keeps working with no changes at all. Everything | |
| else in the payload is new evidence layered on top. | |
| The model's own prediction is read at three levels of granularity: | |
| binary HUMAN | MACHINE — the old yes/no | |
| ternary HUMAN | MACHINE | REFINED — who actually wrote it | |
| detailed the raw 4 classes — separates ADVERSARIAL | |
| Report the ternary label to moderators. It is the level the model is actually | |
| reliable at (~88.8% weighted F1 on Large vs ~97% binary), and it answers the | |
| question that matters in a contest: did the participant write this, did a model | |
| write it, or did they start it and let a model finish. | |
| """ | |
| import hmac | |
| import os | |
| import gradio as gr | |
| from detector import ( | |
| BINARY_LABELS, | |
| BINARY_OF, | |
| LABELS, | |
| MAX_LENGTH, | |
| POOLING, | |
| REPO, | |
| TERNARY_LABELS, | |
| TERNARY_MEANING, | |
| TERNARY_OF, | |
| classify, | |
| collapse, | |
| ) | |
| from forensics import forensic_signals, static_features | |
| # --- Configuration --------------------------------------------------------- | |
| # Clients must send this exact string as the second argument. Hardcoded on | |
| # purpose. Anyone who can read this repo can call the endpoint, so keep the | |
| # Space private — and rotate this value if the repo ever goes public. | |
| API_TOKEN = "5b0be8195774f35a3388a27524b72a95a673ff3fc70ecafb5cc44aca0390367b" | |
| # Decides the legacy human/AI verdict line. Raise it to make the detector more | |
| # willing to call something AI, lower it to be stricter about accusing. This is | |
| # the knob to tune once you have labelled event data. | |
| HUMAN_THRESHOLD = float(os.environ.get("HUMAN_THRESHOLD", "0.5")) | |
| # Fusion weight between the model score and the static forensic score. | |
| MODEL_WEIGHT = float(os.environ.get("MODEL_WEIGHT", "0.7")) | |
| # A submission is a few KB. Anything past this is a paste bomb, not an answer. | |
| MAX_CHARS = int(os.environ.get("MAX_CHARS", "200000")) | |
| # Below this top-class probability the model isn't actually distinguishing | |
| # anything, and the result should read as inconclusive rather than as a | |
| # verdict. Four classes ⇒ chance is 0.25. | |
| INCONCLUSIVE_BELOW = float(os.environ.get("INCONCLUSIVE_BELOW", "0.45")) | |
| # --- Inference ------------------------------------------------------------- | |
| def detect_code(code_text): | |
| """Run the full evidence pipeline on one submission.""" | |
| probs, embedding, token_count, truncated = classify(code_text) | |
| p_human = probs["HUMAN_GENERATED"] | |
| label = max(probs, key=probs.get) | |
| ternary_probs, ternary_label, ternary_conf = collapse( | |
| probs, TERNARY_OF, TERNARY_LABELS | |
| ) | |
| binary_probs, binary_label, binary_conf = collapse( | |
| probs, BINARY_OF, BINARY_LABELS | |
| ) | |
| # Three of the four classes are machine, so this is a real AI probability — | |
| # not the distance-from-threshold hack the CodeT5+ checkpoint needed. | |
| model_ai_score = round(100 * binary_probs["MACHINE"]) | |
| features = static_features(code_text) | |
| forensic_score, signals = forensic_signals(features) | |
| return { | |
| # --- legacy fields: keep Code Arena working untouched. | |
| # NOTE ai_probability is a HUMAN score, as it always was on the | |
| # CodeT5+ Space. `human_probability` below is the honest name. --- | |
| "verdict": "Human-Written" if p_human >= HUMAN_THRESHOLD else "AI-Generated", | |
| "ai_probability": p_human, | |
| "token_count": token_count, | |
| "truncated": truncated, | |
| # --- the three-tier read of the same prediction --- | |
| "binary": { | |
| "label": binary_label, | |
| "confidence": binary_conf, | |
| "probs": binary_probs, | |
| }, | |
| "ternary": { | |
| "label": ternary_label, | |
| "confidence": ternary_conf, | |
| "probs": ternary_probs, | |
| "meaning": TERNARY_MEANING[ternary_label], | |
| }, | |
| "detailed": { | |
| "label": label, | |
| "confidence": probs[label], | |
| "probs": probs, | |
| }, | |
| # A flat distribution means the model has nothing to say. Surface that | |
| # explicitly so a moderator never reads noise as an accusation. | |
| "inconclusive": probs[label] < INCONCLUSIVE_BELOW, | |
| # --- flat aliases, so callers don't have to reach into the nested | |
| # blocks for the most-used numbers --- | |
| "label": label, | |
| "confidence": probs[label], | |
| "probs": probs, | |
| "human_probability": p_human, | |
| "model_ai_score": model_ai_score, | |
| # 256-d (Large) or 128-d (Base) style vector. Cosine-compare one | |
| # participant's submissions against each other: the odd one out is the | |
| # one that was not written the way they write. | |
| "embedding": embedding, | |
| # --- static forensics: physical traces of pasted text --- | |
| "features": features, | |
| "signals": signals, | |
| "forensic_score": forensic_score, | |
| # --- fused 0-100 for ranking. Peer similarity and submission timing | |
| # live on the platform side and are deliberately NOT in here. --- | |
| "score": round( | |
| MODEL_WEIGHT * model_ai_score + (1 - MODEL_WEIGHT) * forensic_score | |
| ), | |
| "model": REPO, | |
| "pooling": POOLING, | |
| "max_length": MAX_LENGTH, | |
| } | |
| def detect(code, token): | |
| """UI handler + API endpoint. Validates the token before running anything.""" | |
| # isinstance guard first: the API accepts arbitrary JSON, so `token` can | |
| # arrive as null/number/list. compare_digest is constant-time and rejects | |
| # non-str outright, which also keeps a mistyped token from being a timing | |
| # oracle for the real one. | |
| if not isinstance(token, str) or not hmac.compare_digest(token, API_TOKEN): | |
| return {"error": "Invalid or missing token"} | |
| if not code or not code.strip(): | |
| return {"error": "No code provided"} | |
| if len(code) > MAX_CHARS: | |
| return {"error": f"Code too large ({len(code)} chars, limit {MAX_CHARS})"} | |
| return detect_code(code) | |
| # --- UI -------------------------------------------------------------------- | |
| with gr.Blocks(title="Code Authorship Checker") as demo: | |
| gr.Markdown( | |
| "# Code Authorship Checker\n" | |
| f"`{REPO}` + static forensics, running on CPU. Paste code, enter the " | |
| "API token, and click **Analyze Code**. The same function is exposed " | |
| 'as an API (`api_name="detect"`).\n\n' | |
| "**Read the `ternary` block first** — HUMAN / MACHINE / REFINED is the " | |
| "level this model is genuinely reliable at. The 4-way `detailed` block " | |
| "additionally separates *adversarial* (model output deliberately " | |
| "styled to look hand-typed) from ordinary model output; that split is " | |
| "the hardest one and the least certain." | |
| ) | |
| code_input = gr.Code(label="Code", language=None, lines=20) | |
| token_input = gr.Textbox( | |
| label="API token", | |
| type="password", | |
| placeholder="Enter your API token", | |
| ) | |
| analyze_btn = gr.Button("Analyze Code", variant="primary") | |
| output = gr.JSON(label="Result") | |
| analyze_btn.click( | |
| detect, | |
| inputs=[code_input, token_input], | |
| outputs=output, | |
| api_name="detect", | |
| ) | |
| gr.Markdown( | |
| f"4-way classes: `{'` · `'.join(LABELS)}`\n\n" | |
| f"Max {MAX_LENGTH} tokens per request · pooling `{POOLING}` · " | |
| "every request must carry the API token." | |
| ) | |
| if __name__ == "__main__": | |
| from detector import load_model | |
| # Warm-load so the first real request isn't the slow one. On the Space this | |
| # runs at startup, so the app only goes green once the weights are ready. | |
| load_model() | |
| demo.queue(max_size=32).launch() | |