| import os
|
| import sys
|
| import json
|
| import tempfile
|
| from typing import Union, List, Dict, Any
|
|
|
| import gradio as gr
|
| import torch
|
|
|
|
|
| from ScanDL2 import ScanDL2
|
|
|
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
| _MODELS: Dict[str, ScanDL2] = {}
|
|
|
|
|
| def get_model(text_type: str) -> ScanDL2:
|
| """Load and cache a ScanDL2 model for the given text type."""
|
| if text_type not in _MODELS:
|
|
|
| _MODELS[text_type] = ScanDL2(text_type=text_type, save=None)
|
| return _MODELS[text_type]
|
|
|
|
|
| def predict(
|
| text: str,
|
| text_type: str,
|
| progress=gr.Progress(track_tqdm=True),
|
| ) -> Dict[str, Any]:
|
| """
|
| Run ScanDL2 on the input text and return a structured result.
|
| """
|
| if text is None or text.strip() == "":
|
| raise gr.Error("Please provide some input text.")
|
|
|
| lines = [ln.strip() for ln in text.strip().split("\n") if ln.strip()]
|
| if len(lines) == 0:
|
| raise gr.Error("Input text is empty after cleaning.")
|
|
|
| progress(0.05, desc="Loading ScanDL2 model...")
|
| model = get_model(text_type)
|
|
|
| progress(0.15, desc="Running ScanDL + FixDur modules...")
|
| with torch.no_grad():
|
| output = model(texts=lines)
|
|
|
| return output
|
|
|
|
|
| def format_output(output: Dict[str, Any]) -> str:
|
| """Pretty-print the ScanDL2 output for display in the UI."""
|
| if output is None:
|
| return ""
|
|
|
| n = len(output.get("original_sn", []))
|
| lines: List[str] = []
|
|
|
| for i in range(n):
|
| original_sn = output["original_sn"][i]
|
| sp_words = output["predicted_sp_words"][i]
|
| sp_ids = output["predicted_sp_ids"][i]
|
| fix_durs = output["predicted_fix_durs"][i]
|
|
|
| lines.append(f"### Example {i + 1}")
|
| lines.append("")
|
| lines.append(f"**Original text:** {' '.join(original_sn)}")
|
| lines.append("")
|
|
|
|
|
| lines.append("| # | Word | Word index | Fixation duration (ms) |")
|
| lines.append("|---|------|-----------|------------------------|")
|
| for step, (w, wid, dur) in enumerate(zip(sp_words, sp_ids, fix_durs), start=1):
|
| lines.append(f"| {step} | {w} | {wid} | {dur} |")
|
| lines.append("")
|
|
|
| return "\n".join(lines)
|
|
|
|
|
| def format_json(output: Dict[str, Any]) -> str:
|
| """Return the raw JSON string of the output."""
|
| if output is None:
|
| return "{}"
|
| return json.dumps(output, indent=2, ensure_ascii=False)
|
|
|
|
|
| DESCRIPTION = """
|
| # ScanDL 2.0
|
|
|
| **ScanDL 2.0** predicts human-like **eye-movement scanpaths** (which words are fixated, in what order)
|
| and their **fixation durations** (in milliseconds) directly from text.
|
|
|
| This Space wraps two jointly-trained modules:
|
|
|
| 1. **ScanDL module** — a discrete diffusion model that generates fixation *locations* (a scanpath) over the input text.
|
| 2. **FixDur module** — a sequence-to-sequence model that predicts the *duration* of each fixation.
|
|
|
| ### How to use
|
| 1. Paste your text in the box below.
|
| - In **sentence** mode, each line is treated as a separate sentence.
|
| - In **paragraph** mode, each line is treated as a separate paragraph.
|
| 2. Choose the text type (must match the model checkpoint you want to use).
|
| 3. Click **Run**.
|
|
|
| ### Output
|
| - A human-readable scanpath table with predicted fixation durations per word.
|
| - The raw JSON output (fixated words, word indices, and durations).
|
| """
|
|
|
| EXAMPLES = [
|
| [
|
| "The quick brown fox jumps over the lazy dog.",
|
| "sentence",
|
| ],
|
| [
|
| "Researchers have long been interested in how humans process written language.\n"
|
| "Eye-tracking studies reveal where and for how long readers fixate on words.",
|
| "paragraph",
|
| ],
|
| ]
|
|
|
|
|
| def build_demo() -> gr.Blocks:
|
| with gr.Blocks(
|
| title="ScanDL 2.0 — Eye-Movement Scanpath Prediction",
|
| theme=gr.themes.Soft(),
|
| ) as demo:
|
| gr.Markdown(DESCRIPTION)
|
|
|
| with gr.Row():
|
| with gr.Column(scale=3):
|
| text_in = gr.Textbox(
|
| label="Input text",
|
| placeholder="Paste a sentence or paragraph here...",
|
| lines=8,
|
| )
|
| text_type_in = gr.Radio(
|
| choices=["sentence", "paragraph"],
|
| value="sentence",
|
| label="Text type",
|
| info="Must match an available ScanDL2 checkpoint.",
|
| )
|
| with gr.Row():
|
| run_btn = gr.Button("Run", variant="primary")
|
| clear_btn = gr.Button("Clear")
|
|
|
| with gr.Column(scale=4):
|
| table_out = gr.Markdown(
|
| label="Predicted scanpath",
|
| value="_Results will appear here._",
|
| )
|
| json_out = gr.Code(
|
| label="Raw JSON output",
|
| language="json",
|
| value="{}",
|
| )
|
|
|
| gr.Examples(examples=EXAMPLES, inputs=[text_in, text_type_in])
|
|
|
| def _run(text, text_type):
|
| output = predict(text, text_type)
|
| return format_output(output), format_json(output)
|
|
|
| run_btn.click(
|
| fn=_run,
|
| inputs=[text_in, text_type_in],
|
| outputs=[table_out, json_out],
|
| )
|
| clear_btn.click(
|
| fn=lambda: ("", "sentence", "_Results will appear here._", "{}"),
|
| inputs=None,
|
| outputs=[text_in, text_type_in, table_out, json_out],
|
| )
|
|
|
| return demo
|
|
|
|
|
| if __name__ == "__main__":
|
| demo = build_demo()
|
| demo.queue(max_size=16).launch(
|
| server_name="0.0.0.0",
|
| server_port=int(os.environ.get("PORT", 7860)),
|
| show_error=True,
|
| )
|
|
|