legitcoconut
DroidDetect-Large code authorship checker
e84edab
Raw History Blame Contribute Delete
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()