Skip to content

Add LaTeX OCR: multimodal env on the new Task API + dataset streaming#1003

Open
adithya-s-k wants to merge 17 commits into
huggingface:mainfrom
adithya-s-k:add-latex-ocr-multimodal-streaming-env
Open

Add LaTeX OCR: multimodal env on the new Task API + dataset streaming#1003
adithya-s-k wants to merge 17 commits into
huggingface:mainfrom
adithya-s-k:add-latex-ocr-multimodal-streaming-env

Conversation

@adithya-s-k

@adithya-s-k adithya-s-k commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Closes #1002

What

Adds envs/latex_ocr_env/ — a multimodal (vision + text) single-step
(bandit) RL environment for image → LaTeX transcription, built on the
newly introduced Task API (#726) and adding dataset streaming for
datasets too large to materialize. Backed by a Hugging Face dataset (default
unsloth/LaTeX_OCR).

Why it matters

  • First env built on the new Task API — a concrete, dataset-backed consumer
    of list_splits / num_tasks / get_task / get_task_range.
  • Dataset streaming — streams the dataset instead of downloading it, so
    TB-scale datasets (millions of rows) can be served from a small Space.
  • Multimodal — image input + LaTeX output, gradeable with any vision-LLM.

Highlights

  • Task API over the datasetlist_splits, num_tasks, get_task,
    get_task_range.
  • Two access modes (LATEX_OCR_MODE):
    • materialize (default) — split loaded + indexed; reset(split, index)
      random access. Best when the dataset fits on disk.
    • stream — sequential cursor over a streamed split (no full download);
      observations carry index / total / remaining / pct_done; num_tasks from
      dataset metadata only; no-repeat within a pass. For TB-scale datasets.
  • Reward (LatexOCRRubric) — 0.8·(1−CER) + 0.2·exact_match against the
    hidden ground truth, whitespace-insensitive; pure-Python edit distance.
  • Custom Gradio "Try it" tab — get a task image, submit LaTeX, see the score
    (via the gradio_builder hook).
  • Typed client with reset/step + Task API helpers; validate.py
    end-to-end driver (optionally drives a vision-LLM policy via the HF Router).

Files

envs/latex_ocr_env/
  __init__.py  client.py  models.py  openenv.yaml  pyproject.toml  uv.lock  README.md
  validate.py
  server/{app.py, latex_ocr_environment.py, rubric.py, gradio_ui.py, Dockerfile, __init__.py}
tests/envs/test_latex_ocr_rubric.py

Testing

  • PYTHONPATH=src:envs uv run pytest tests/envs/test_latex_ocr_rubric.py — 8 passing.
  • ruff format / ruff check / usort — clean.
  • Verified end-to-end on a deployed HF Space: Task API, streaming cursor + live
    progress, no-repeat, and a real vision-LLM policy (mean reward ~0.77, incl.
    exact matches).

Notes

  • Requires core with the Task API — depends on openenv>=0.4.1.
  • No changes to core; uses the existing Environment + new Task API.
  • Any (image, latex) dataset works via LATEX_OCR_DATASET + column env vars.

Note

Low Risk
Additive environment and docs only; no changes to OpenEnv core. Main operational risk is HF dataset download/streaming and memory in materialize mode.

Overview
Adds latex_ocr_env, a new OpenEnv package for single-step image → LaTeX RL on Hugging Face datasets (default unsloth/LaTeX_OCR), wired to the Task API (list_splits, num_tasks, get_task, get_task_range) plus a typed client with matching HTTP helpers.

Episodes expose base64 images on reset and hide ground truth until step; LatexOCRRubric scores server-side with CER + exact-match weighting and a length guard against whitespace-padding hacks. LATEX_OCR_MODE chooses materialize (indexed random access) vs stream (sequential cursor, metadata-only counts, capped task-range stubs).

Also ships FastAPI/WS server, Docker, Gradio Try it tab, validate.py, rubric unit tests, docs/toc entry, and a GRPO + TRL tutorial notebook that uses the env as reward source.

Reviewed by Cursor Bugbot for commit 3154ef9. Bugbot is set up for automated code reviews on this repo. Configure here.

A multimodal (vision + text) single-step (bandit) RL environment for
image -> LaTeX transcription, backed by a Hugging Face dataset (default
unsloth/LaTeX_OCR) and served through OpenEnv.

Built on the newly introduced Task API (huggingface#726) and adds dataset streaming
for datasets too large to materialize:

- Task API over the dataset: list_splits / num_tasks / get_task / get_task_range.
- Two access modes (LATEX_OCR_MODE):
  - materialize: split loaded + indexed; reset(split, index) random access.
  - stream: sequential cursor, NO full download; observations carry progress
    (index / total / remaining / pct_done); num_tasks from dataset metadata
    only; no-repeat within a pass. For TB-scale datasets.
- Multimodal: image input + LaTeX output, gradeable with any vision-LLM policy.
- Reward (LatexOCRRubric): 0.8*(1-CER) + 0.2*exact_match vs the hidden ground
  truth, whitespace-insensitive; pure-Python edit distance (no deps).
- Custom Gradio "Try it" tab; typed client + Task API helpers; validate.py driver.
- Unit tests for the rubric (tests/envs/test_latex_ocr_rubric.py).
Copilot AI review requested due to automatic review settings July 22, 2026 17:58
@bot-ci-comment

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

Comment thread envs/latex_ocr_env/server/latex_ocr_environment.py Outdated
Comment thread envs/latex_ocr_env/server/gradio_ui.py
Comment thread envs/latex_ocr_env/server/latex_ocr_environment.py
Comment thread envs/latex_ocr_env/client.py Outdated
Comment thread envs/latex_ocr_env/server/latex_ocr_environment.py
Comment thread envs/latex_ocr_env/server/latex_ocr_environment.py
Comment thread envs/latex_ocr_env/server/latex_ocr_environment.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new latex_ocr_env OpenEnv environment: a dataset-backed, single-step (bandit) image → LaTeX transcription task that exposes the Task API and supports dataset streaming for large datasets, along with a small rubric-focused test suite and an end-to-end validation script.

Changes:

  • Introduces envs/latex_ocr_env/ (server, client, models, Dockerfile, OpenEnv manifest, docs, validate driver).
  • Implements a pure-Python LaTeX OCR reward rubric (CER + exact-match bonus) and adds unit tests for the rubric.
  • Adds streaming mode support (sequential cursor + progress fields) in the environment implementation.

Alignment Review Report

Automated Checks

  • Lint: NOT RUN — unable to execute repo hook scripts in this review environment.
  • Debug code: NOT RUN (hook) — manual review did not find breakpoints/pdb; prints are present in validate.py (expected for a driver script) and UI messaging.

Open RFCs Context

  • RFC 002 (In Review): Task provider methods are intended for metadata/discovery and should be side-effect-free; Task API method semantics (e.g., list_tasks returns all tasks).
  • RFC 004 (Implemented in code; design doc present): Rubric system exists in core; environments can expose env.rubric for introspection.

Tier 1: Fixes Required

  • envs/latex_ocr_env/server/latex_ocr_environment.py — streaming exhaustion can allow stale-target scoring (needs _target=None / _done=True).
  • envs/latex_ocr_env/server/latex_ocr_environment.py — stream cursor/index bookkeeping is off-by-one and inconsistent across reset/step/task IDs.
  • envs/latex_ocr_env/server/latex_ocr_environment.py — stream mode reset(index=...) is silently ignored (should raise).
  • envs/latex_ocr_env/server/latex_ocr_environment.py — stream mode list_tasks() returns a truncated preview, which conflicts with Task API expectations.
  • tests/envs/test_latex_ocr_rubric.py — add a defensive assertion around dynamic import spec/loader to improve failure clarity.

Tier 2: Alignment Discussion

ALIGNMENT FLAG: list_tasks() semantics in stream mode (preview vs “all tasks”)

  • Principle/RFC at stake: RFC 002 (TaskProvider semantics: discovery APIs; list_tasks = all specs)
  • The concern: Returning a truncated list can mislead downstream tooling into thinking the split only has 100 tasks; raising NotImplementedError (or otherwise making truncation explicit) is safer.
  • Suggested reviewer: @darktex

ALIGNMENT FLAG: Custom rubric class vs core Rubric system introspection

  • Principle/RFC at stake: RFC 004 (rubrics live inside environments; introspection via env.rubric)
  • The concern: LatexOCRRubric is implemented as a standalone helper rather than a core Rubric (openenv.core.rubrics.base.Rubric), which may reduce standard reward introspection/observability.
  • Suggested reviewer: @darktex

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/envs/test_latex_ocr_rubric.py Adds unit tests for the OCR reward rubric (pure-Python import path).
envs/latex_ocr_env/init.py Exposes the typed client and models for the new env package.
envs/latex_ocr_env/client.py Adds a typed EnvClient wrapper plus Task API HTTP helpers.
envs/latex_ocr_env/models.py Defines Action/Observation wire models (including streaming progress fields).
envs/latex_ocr_env/openenv.yaml Declares Space/runtime metadata and config defaults for deployment.
envs/latex_ocr_env/pyproject.toml Adds per-env packaging and dependency declarations (incl. optional VLM driver deps).
envs/latex_ocr_env/README.md Documents usage, Task API endpoints, and configuration env vars.
envs/latex_ocr_env/validate.py Adds an end-to-end driver that can optionally call a VLM via the HF router.
envs/latex_ocr_env/server/init.py Declares the server subpackage.
envs/latex_ocr_env/server/app.py Creates the FastAPI app via core create_app, including a custom Gradio tab.
envs/latex_ocr_env/server/latex_ocr_environment.py Implements the environment logic, Task API methods, and stream/materialize modes.
envs/latex_ocr_env/server/rubric.py Implements the LaTeX OCR reward computation (CER + exact-match bonus).
envs/latex_ocr_env/server/gradio_ui.py Adds a custom “Try it” Gradio playground that uses the live environment instance.
envs/latex_ocr_env/server/Dockerfile Adds a container build for the environment server runtime.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +147 to +153
if self.mode == "stream":
# Positional stubs only; do not enumerate huge splits.
preview = min(n if n > 0 else 0, 100)
return [
{"id": f"{split}-{i}", "index": i, "sequential": True}
for i in range(preview)
]
Comment on lines +190 to +194
self._done = False
self._state = State(episode_id=episode_id or str(uuid4()), step_count=0)
if self.mode == "stream":
return self._reset_stream(split)
return self._reset_materialize(split, index, seed)
Comment on lines +242 to +245
except StopIteration:
self._exhausted = True
return LatexOCRObservation(
done=True,
Comment on lines +254 to +257
self._cursor += 1
self._target = str(row[TEXT_COLUMN])
self._current_split, self._current_index = split, self._cursor
remaining = (total - self._cursor) if total > 0 else -1
Comment on lines +263 to +266
split=split,
index=self._cursor,
task_id=f"{split}-stream-{self._cursor}",
total=total,
Comment on lines +28 to +32
_spec = importlib.util.spec_from_file_location("latex_ocr_rubric", _RUBRIC_PATH)
rubric = importlib.util.module_from_spec(_spec)
# Register before exec so dataclasses in the module can resolve their module.
sys.modules[_spec.name] = rubric
_spec.loader.exec_module(rubric)
Comment on lines +237 to +240
def _reset_stream(self, split: str) -> LatexOCRObservation:
self._ensure_stream(split)
total = _split_total(self.dataset_name, split)
try:
… 16)

Pass max_concurrent_envs to create_app so multiple rollouts / UI users can
run concurrent sessions; overridable via the LATEX_OCR_MAX_SESSIONS env var.
Copilot AI review requested due to automatic review settings July 22, 2026 18:24
Comment thread envs/latex_ocr_env/server/latex_ocr_environment.py Outdated
Comment thread envs/latex_ocr_env/server/latex_ocr_environment.py Outdated
Comment thread envs/latex_ocr_env/validate.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (3)

envs/latex_ocr_env/server/latex_ocr_environment.py:245

  • In stream mode, when the dataset iterator is exhausted, reset() returns a done observation but does not mark the episode as terminated or clear the previous target. A subsequent step() call can incorrectly grade against the previous task instead of erroring out.
        except StopIteration:
            self._exhausted = True
            return LatexOCRObservation(
                done=True,

envs/latex_ocr_env/server/latex_ocr_environment.py:257

  • Streaming cursor bookkeeping is off-by-one: _cursor is incremented before setting observation.index/_current_index, so the first streamed sample reports index=1. This also makes task_id and progress inconsistent with materialize mode (0-based row indices).
        self._cursor += 1
        self._target = str(row[TEXT_COLUMN])
        self._current_split, self._current_index = split, self._cursor
        remaining = (total - self._cursor) if total > 0 else -1

envs/latex_ocr_env/server/latex_ocr_environment.py:292

  • In stream mode, reset() sets task_id to "{split}-stream-{index}" but step() currently reports "{split}-{index}", which can collide with materialize-mode IDs and makes it hard to correlate reset/step logs for the same task.
            task_id=f"{self._current_split}-{self._current_index}",

Comment on lines +169 to +171
n = self.num_tasks(split)
start = 0 if start is None else start
stop = n if stop is None else (min(stop, n) if n > 0 else stop)
Comment thread envs/latex_ocr_env/README.md Outdated
[`unsloth/LaTeX_OCR`](https://huggingface.co/datasets/unsloth/LaTeX_OCR):
`image` + `text` columns, `train`/`test` splits) via the OpenEnv **Task API**.
- **Reward**: `(1 - exact_weight) * (1 - CER) + exact_weight * exact_match`,
where `CER` is the normalized character edit distance over whitespace-collapsed
…ponents

Refactor LatexOCRRubric into a weighted sum of named components (each in [0,1],
weights renormalized -> reward in [0,1], tunable via env vars):

- edit_similarity     (1 - CER)                          LATEX_OCR_W_EDIT   (0.6)
- exact_match                                            LATEX_OCR_W_EXACT  (0.2)
- structural_validity (balanced delimiters + parseable   LATEX_OCR_W_STRUCT (0.1)
                       LaTeX via optional pylatexenc)
- length_format       (length agreement + no code fences) LATEX_OCR_W_LENFMT (0.1)

Per-component scores are surfaced in observation.reward_components and metadata
for training introspection. Empty prediction vs non-empty target scores 0 (no
floor credit). pylatexenc is optional (structural check degrades to a balance
check without it). Tests + docs updated.
Copilot AI review requested due to automatic review settings July 22, 2026 19:58
return [
{"id": f"{split}-{i}", "index": i, "split": split}
for i in range(start, stop)
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task range ignores slice semantics

Medium Severity

get_task_range builds range(start, stop) after only null-coalescing bounds, but the Task API contract requires Python slice semantics. Negative start/stop therefore emit invalid negative indexes (or an empty list) instead of counting from the end of the split, unlike the reference Task API implementations.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a9fa176. Configure here.

Comment thread envs/latex_ocr_env/server/latex_ocr_environment.py Outdated
Comment thread envs/latex_ocr_env/models.py Outdated
Comment thread envs/latex_ocr_env/server/rubric.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (3)

envs/latex_ocr_env/server/latex_ocr_environment.py:270

  • In stream mode, _reset_stream() increments _cursor before assigning index/task_id/current_index, so the first emitted task is numbered 1 (while Task API indices are 0-based). This also makes remaining/pct_done bookkeeping harder to reason about. Suggest keeping observation.index and _current_index 0-based and deriving pct_done from “consumed” = index+1.
        self._cursor += 1
        self._target = str(row[TEXT_COLUMN])
        self._current_split, self._current_index = split, self._cursor
        remaining = (total - self._cursor) if total > 0 else -1
        return LatexOCRObservation(
            done=False,
            image_base64=_encode_image(row[IMAGE_COLUMN]),
            image_format="png",
            prompt=DEFAULT_PROMPT,
            split=split,
            index=self._cursor,
            task_id=f"{split}-stream-{self._cursor}",
            total=total,
            remaining=remaining,
            pct_done=round(self._cursor / total, 6) if total > 0 else 0.0,
            exhausted=False,
        )

envs/latex_ocr_env/server/latex_ocr_environment.py:293

  • In stream mode, reset() returns task_id like "{split}-stream-{i}", but step() returns task_id "{split}-{i}". That makes task_id unstable across the episode and can confuse clients/logging. Suggest preserving the stream prefix in step() when mode=="stream".
            reward=result.reward,
            split=self._current_split,
            index=self._current_index,
            task_id=f"{self._current_split}-{self._current_index}",
            predicted_latex=prediction,

tests/envs/test_latex_ocr_rubric.py:32

  • The dynamic import in this test assumes spec_from_file_location() always returns a spec with a loader. If it fails (e.g., path issues), the error will be a less-informative AttributeError. Adding an explicit check will fail fast with a clearer message.
_spec = importlib.util.spec_from_file_location("latex_ocr_rubric", _RUBRIC_PATH)
rubric = importlib.util.module_from_spec(_spec)
# Register before exec so dataclasses in the module can resolve their module.
sys.modules[_spec.name] = rubric
_spec.loader.exec_module(rubric)

Comment on lines +92 to +96
builder = load_dataset_builder(
dataset_name, name=CONFIG, token=os.environ.get("HF_TOKEN")
)
info = builder.info.splits.get(split)
return int(info.num_examples) if info and info.num_examples else -1
Comment on lines +72 to +75
gr.Markdown(
"Transcribe the image into **LaTeX**, then score it. Reward = "
"`0.8·(1−CER) + 0.2·exact_match` against the hidden ground truth."
)
``reset()`` pulls the *next* sample; every observation carries progress
(``index``, ``total``, ``remaining``, ``pct_done``). No-repeat within a pass;
``reset(index=i)`` random access is unsupported by design. Best for very large
/ TB-scale datasets. See STREAMING.md.
Copilot AI review requested due to automatic review settings July 22, 2026 20:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (3)

envs/latex_ocr_env/server/latex_ocr_environment.py:194

  • In stream mode, reset() currently ignores the provided index argument. That makes reset(split, index=...) appear supported but behave differently than requested (and contradicts the module docstring that says random access is unsupported). Consider rejecting index explicitly in stream mode so callers don’t get silent surprises.
        if split not in self.list_splits():
            split = self.list_splits()[0]
        self._done = False
        self._state = State(episode_id=episode_id or str(uuid4()), step_count=0)
        if self.mode == "stream":
            return self._reset_stream(split)
        return self._reset_materialize(split, index, seed)

envs/latex_ocr_env/server/latex_ocr_environment.py:258

  • Stream mode uses a 1-based index/cursor and emits task IDs that don’t match the Task API’s get_task() IDs (and changes the task_id format between reset and step). This makes tasks hard to correlate across Task API vs episode results and introduces an off-by-one in progress fields.
        self._cursor += 1
        self._target = str(row[TEXT_COLUMN])
        self._current_split, self._current_index = split, self._cursor
        remaining = (total - self._cursor) if total > 0 else -1
        return LatexOCRObservation(

envs/latex_ocr_env/server/gradio_ui.py:75

  • The UI text hard-codes a reward formula (0.8·(1−CER) + 0.2·exact_match) that doesn’t match the rubric’s actual default weights (0.6/0.2/0.1/0.1). This can confuse users when they see rewards/components that don’t align with the stated formula.
        gr.Markdown(
            "Transcribe the image into **LaTeX**, then score it. Reward = "
            "`0.8·(1−CER) + 0.2·exact_match` against the hidden ground truth."
        )

Comment thread envs/latex_ocr_env/validate.py Outdated
Comment on lines +92 to +96
rewards = []
seen_targets = []
for i in range(min(args.num, n)):
# In stream mode `index` is ignored — reset() pulls the next sample.
result = env.reset(split=args.split, index=i)
The composable structural-validity + length/format components introduced
discontinuous (partly binary) terms that made the reward spiky and training
unstable. Roll the rubric back to the smooth dense signal:

    reward = 0.8 * (1 - CER) + 0.2 * exact_match

Removes the extra components, the reward_components field, weight env vars, and
the pylatexenc dependency. Keeps the Gradio UI fix that reads reward from the
top-level step result (observation.reward is None after server serialization).
Copilot AI review requested due to automatic review settings July 22, 2026 20:30
Comment thread envs/latex_ocr_env/server/latex_ocr_environment.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

envs/latex_ocr_env/server/latex_ocr_environment.py:246

  • In streaming mode, when the iterator is exhausted, the environment returns a terminal observation but leaves _target/_done unchanged. If a caller mistakenly calls step() after an exhausted reset(), it may score against a stale target from a previous task. Clear _target and mark the episode done in the StopIteration path (and optionally update current split/index) to prevent reuse of stale state.
        except StopIteration:
            self._exhausted = True
            return LatexOCRObservation(
                done=True,
                split=split,

envs/latex_ocr_env/server/latex_ocr_environment.py:293

  • reset() (stream mode) returns task IDs like "{split}-stream-{n}", but step() currently reports task_id as "{split}-{n}". This makes task identifiers inconsistent within a single episode and can collide with materialize-mode IDs. Use the stream-prefixed task_id format when mode == "stream".
            split=self._current_split,
            index=self._current_index,
            task_id=f"{self._current_split}-{self._current_index}",
            predicted_latex=prediction,

tests/envs/test_latex_ocr_rubric.py:32

  • This test dynamically loads rubric.py via spec_from_file_location, but doesn't guard against _spec (or _spec.loader) being None. If spec creation fails, the test will error with an AttributeError rather than a clear assertion failure. Add an explicit assertion so failures are easier to diagnose.
_spec = importlib.util.spec_from_file_location("latex_ocr_rubric", _RUBRIC_PATH)
rubric = importlib.util.module_from_spec(_spec)
# Register before exec so dataclasses in the module can resolve their module.
sys.modules[_spec.name] = rubric
_spec.loader.exec_module(rubric)

Copilot AI review requested due to automatic review settings July 23, 2026 08:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

envs/latex_ocr_env/server/latex_ocr_environment.py:258

  • In stream mode, _cursor is incremented before being assigned to index/task_id/_current_index, making indices 1-based and causing task_id on reset() (e.g. split-stream-1) to disagree with the Task API’s get_task(split, 0) (split-0) and with step()’s returned task_id (split-<index>). This breaks stable task identification and makes progress reporting off-by-one.
        self._cursor += 1
        self._target = str(row[TEXT_COLUMN])
        self._current_split, self._current_index = split, self._cursor
        remaining = (total - self._cursor) if total > 0 else -1
        return LatexOCRObservation(

envs/latex_ocr_env/server/latex_ocr_environment.py:172

  • get_task_range() behaves incorrectly when num_tasks() returns an unknown size (e.g. stream mode returns -1): if stop is omitted, stop becomes -1, so range(start, stop) is empty and callers can’t page tasks at all. This should either require an explicit stop when n < 0, or choose a sensible default page size.
        n = self.num_tasks(split)
        start = 0 if start is None else start
        stop = n if stop is None else (min(stop, n) if n > 0 else stop)
        return [

tests/envs/test_latex_ocr_rubric.py:32

  • The test dynamically loads rubric.py via spec_from_file_location(), but doesn’t guard against _spec or _spec.loader being None (which can happen if the path is wrong or import machinery can’t create a loader). This would raise an AttributeError later and obscure the real failure.
_spec = importlib.util.spec_from_file_location("latex_ocr_rubric", _RUBRIC_PATH)
rubric = importlib.util.module_from_spec(_spec)
# Register before exec so dataclasses in the module can resolve their module.
sys.modules[_spec.name] = rubric
_spec.loader.exec_module(rubric)

…lit, stream restart, consistent task_id, validate fallback)

- _split_total: report 0 for known-empty splits, -1 only when unknown
- reset(): raise on unknown split instead of silently remapping to splits[0]
- _ensure_stream: rebuild the iterator after a pass is exhausted (restartable)
- step()/reset(): emit a consistent task_id
- validate.py: handle unknown count and fall back to sequential when stream rejects index
- models.py: drop unused RewardValue alias
- docs: remove dangling STREAMING.md reference

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (4)

envs/latex_ocr_env/server/latex_ocr_environment.py:303

  • In stream mode, the observation index/task_id and _current_index become 1-based and don’t match the Task API’s 0-based indices (get_task/list_tasks use 0..n-1). This also makes reset()’s task_id differ from the subsequent step() task_id.
    def _reset_stream(self, split: str) -> LatexOCRObservation:
        self._ensure_stream(split)
        total = self.num_tasks(split)  # honors LATEX_OCR_MAX_ROWS
        cap = _max_rows()
        if cap is not None and self._cursor >= cap:

examples/grpo_latex_ocr/README.md:3

  • The Colab badge URL is hard-coded to a personal fork/branch. After merge, it should point at the canonical huggingface/OpenEnv repo (or a relative path won’t work for Colab).
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/adithya-s-k/OpenEnv/blob/add-latex-ocr-multimodal-streaming-env/examples/grpo_latex_ocr/grpo_latex_ocr_tutorial.ipynb)

examples/grpo_latex_ocr/README.md:39

  • The README claims the reward includes “structural validity” and “length/format”, but the environment’s rubric is currently just edit similarity + exact match. This is misleading for users trying to reproduce results.
See [`envs/latex_ocr_env`](../../envs/latex_ocr_env) for the full environment: a dataset-backed,
single-step (bandit) RL task with a weighted, server-side reward (edit similarity, exact match,
structural validity, length/format). It ships `train` and `test` splits.

examples/grpo_latex_ocr/grpo_latex_ocr_tutorial.ipynb:41

  • The notebook installs the env package from a personal fork/feature branch. For a repo example, this should reference the canonical huggingface/OpenEnv repo so it stays valid after merge.
    "    \"openenv-latex_ocr_env @ git+https://github.com/adithya-s-k/OpenEnv.git@add-latex-ocr-multimodal-streaming-env#subdirectory=envs/latex_ocr_env\" \\\n",

Comment on lines +79 to +83
# Newer core's StepResult carries `info`; older core does not.
try:
return StepResult(**base, info=data.get("info", {}))
except TypeError:
return StepResult(**base)
Comment on lines +327 to +331
total = (
_split_total(self.dataset_name, self._current_split)
if self.mode == "stream"
else -1
)
@adithya-s-k

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review — went through all of it. Here's what changed and where.

Fixed (0bf2592)

  • Stream exhaustion now sets done and clears the target, so a follow-up step() fails fast instead of grading against a stale task.
  • reset(index=…) raises in stream mode instead of silently ignoring the index.
  • LATEX_OCR_MAX_ROWS is honored in stream mode (num_tasks + cursor), and get_task_range caps stub generation so an unbounded range can't OOM.
  • Empty split raises a clear IndexError instead of randrange blowing up.
  • Images are normalized to real PNG, so image_format="png" always holds (even for datasets that store raw JPEG bytes).
  • The client mirrors reward/done onto the observation, matching StepResult.

Fixed (ed46f0d)

  • _split_total reports 0 for a known-empty split and -1 only when the count is genuinely unknown.
  • reset() raises on an unknown split instead of silently remapping to splits[0].
  • The stream iterator rebuilds after a pass is exhausted (restartable).
  • step()/reset() emit a consistent task_id.
  • validate.py handles an unknown count and falls back to sequential pulls when the server rejects a random index.
  • Dropped the unused RewardValue alias and the dangling STREAMING.md reference.

Already addressed / no longer applicable

  • Gradio "score always 0" was fixed earlier (4b57f66) — on_score reads the top-level reward.
  • The structural-validity / arrow-balance / multi-component-reward comments predate the revert to the simple dense reward (0.8·(1−CER) + 0.2·exact_match); the README and the Gradio copy already match that formula.
  • The Try-it Gradio tab is wired through create_app(gradio_builder=…) and provided by the openenv-base image — it's live on the deployed Space.

By design

  • Whitespace is fully stripped before scoring because LaTeX spacing is cosmetic (documented in the README).
  • Stream list_tasks returns a bounded positional preview (sequential: true) — enumerating a TB-scale split isn't feasible; num_tasks gives the honest count and get_task_range serves bounded windows.
  • Stream index is a 1-based progress counter, paired with total / remaining / pct_done.

Pre-merge

  • The Colab badge and the notebook's pip install … @ <branch> point at this fork branch because that's where the code lives until this PR merges; I'll flip both to huggingface/OpenEnv@main once it's in.

CI: added the missing docs/source/environments/latex_ocr.md stub and ran ruff/usort — lint, format, and env-docs checks are green now.

Copilot AI review requested due to automatic review settings July 23, 2026 09:40

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 4 total unresolved issues (including 3 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a81bd87. Configure here.

if self.mode == "stream":
total = _split_total(self.dataset_name, split)
cap = _max_rows()
return min(total, cap) if (cap is not None and total > 0) else total

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cap skipped for unknown totals

Medium Severity

In stream mode, num_tasks only applies LATEX_OCR_MAX_ROWS when metadata total > 0. If _split_total returns -1 (missing num_examples), the cap is ignored and callers still see -1, even though the cursor stops after the capped number of rows.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a81bd87. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (5)

envs/latex_ocr_env/server/latex_ocr_environment.py:342

  • In stream mode, reset() reports total via self.num_tasks() (which honors LATEX_OCR_MAX_ROWS), but step() recomputes total via _split_total() (metadata only). This can make total/remaining/pct_done inconsistent between reset and terminal step when a row cap is configured.
        total = (
            _split_total(self.dataset_name, self._current_split)
            if self.mode == "stream"
            else -1
        )

tests/envs/test_latex_ocr_rubric.py:32

  • The test module loader assumes spec_from_file_location() always returns a spec with a non-null loader. If it returns None (or the loader is missing), this will raise an AttributeError at import time, which is harder to diagnose than a clear failure.
_spec = importlib.util.spec_from_file_location("latex_ocr_rubric", _RUBRIC_PATH)
rubric = importlib.util.module_from_spec(_spec)
# Register before exec so dataclasses in the module can resolve their module.
sys.modules[_spec.name] = rubric
_spec.loader.exec_module(rubric)

examples/grpo_latex_ocr/README.md:39

  • This README claims the environment reward includes “structural validity, length/format”, but the current LaTeX OCR rubric implementation is only edit similarity + exact match. This is misleading for readers trying to understand what is being optimized.
See [`envs/latex_ocr_env`](../../envs/latex_ocr_env) for the full environment: a dataset-backed,
single-step (bandit) RL task with a weighted, server-side reward (edit similarity, exact match,
structural validity, length/format). It ships `train` and `test` splits.

examples/grpo_latex_ocr/README.md:3

  • The Colab badge link points at a personal fork/feature branch (adithya-s-k/OpenEnv@add-latex-ocr-multimodal-streaming-env). After merging, this link will be stale; it should point at the canonical huggingface/OpenEnv repo (typically main).
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/adithya-s-k/OpenEnv/blob/add-latex-ocr-multimodal-streaming-env/examples/grpo_latex_ocr/grpo_latex_ocr_tutorial.ipynb)

examples/grpo_latex_ocr/grpo_latex_ocr_tutorial.ipynb:41

  • This pip install line pins to a personal fork/feature branch. For an in-repo tutorial, point at huggingface/OpenEnv (e.g. main) so the notebook keeps working after merge.
    "    \"openenv-latex_ocr_env @ git+https://github.com/adithya-s-k/OpenEnv.git@add-latex-ocr-multimodal-streaming-env#subdirectory=envs/latex_ocr_env\" \\\n",

Copilot AI review requested due to automatic review settings July 23, 2026 09:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (6)

envs/latex_ocr_env/server/latex_ocr_environment.py:192

  • In stream mode, if the dataset metadata lacks a split size (num_tasks returns -1) and stop is omitted, stop becomes -1 and range(start, stop) returns an empty list. Also, stream-mode task stubs returned here omit the sequential flag that list_tasks()/get_task() include.
        n = self.num_tasks(split)
        start = 0 if start is None else start
        stop = n if stop is None else (min(stop, n) if n > 0 else stop)
        # In stream mode `n` comes from metadata and can be enormous; cap the number
        # of generated stubs so an unbounded range can't OOM the process.

envs/latex_ocr_env/server/latex_ocr_environment.py:313

  • Stream-mode indexing is off-by-one: _cursor is incremented before populating index/task_id, so the first streamed task reports index=1 and uses IDs that don't align with the Task API stubs (which start at 0). This also makes remaining/pct_done semantics harder to reason about.
        self._cursor += 1
        self._target = str(row[TEXT_COLUMN])
        self._current_split, self._current_index = split, self._cursor
        self._current_task_id = f"{split}-stream-{self._cursor}"
        remaining = (total - self._cursor) if total > 0 else -1

envs/latex_ocr_env/server/latex_ocr_environment.py:342

  • In stream mode, reset() reports total via num_tasks() (which honors LATEX_OCR_MAX_ROWS), but step() reports total via _split_total() (which ignores the cap). This makes total/remaining/pct_done inconsistent between reset and step when LATEX_OCR_MAX_ROWS is set.
        total = (
            _split_total(self.dataset_name, self._current_split)
            if self.mode == "stream"
            else -1
        )

examples/grpo_latex_ocr/README.md:3

  • The Colab badge link points to a personal fork/feature branch, which will become stale once this PR merges. It should reference the canonical huggingface/OpenEnv repo (and typically the main branch).
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/adithya-s-k/OpenEnv/blob/add-latex-ocr-multimodal-streaming-env/examples/grpo_latex_ocr/grpo_latex_ocr_tutorial.ipynb)

examples/grpo_latex_ocr/README.md:39

  • This environment description lists reward components ("structural validity, length/format") that aren't implemented by LatexOCRRubric (currently CER + exact match only). This can mislead readers about what the environment actually scores.
See [`envs/latex_ocr_env`](../../envs/latex_ocr_env) for the full environment: a dataset-backed,
single-step (bandit) RL task with a weighted, server-side reward (edit similarity, exact match,
structural validity, length/format). It ships `train` and `test` splits.

examples/grpo_latex_ocr/grpo_latex_ocr_tutorial.ipynb:41

  • The tutorial installs the environment package from a personal fork/feature branch, which will become stale and isn't the canonical source for this repository. It should point to huggingface/OpenEnv (ideally a tag/commit) for long-term reproducibility.
    "    \"openenv-latex_ocr_env @ git+https://github.com/adithya-s-k/OpenEnv.git@add-latex-ocr-multimodal-streaming-env#subdirectory=envs/latex_ocr_env\" \\\n",

Comment on lines +61 to +68
total = o.get("total", -1)
if total and total > 0:
return (
f"**Task `{o.get('task_id', '')}`** · position "
f"**{o.get('index')}/{total}** "
f"({o.get('pct_done', 0):.4%}) · remaining **{o.get('remaining')}**"
)
return f"**Task `{o.get('task_id', '')}`** (index {o.get('index')})"
Comment on lines +108 to +113
prog = ""
if obs.total and obs.total > 0:
prog = (
f" | progress: {obs.index}/{obs.total} "
f"({obs.pct_done:.4%}), remaining={obs.remaining}"
)
Copilot AI review requested due to automatic review settings July 23, 2026 10:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (5)

envs/latex_ocr_env/server/latex_ocr_environment.py:313

  • In stream mode, _cursor is incremented before being used for index/task_id, which makes the first streamed task report index=1 (and task_id suffix 1) even though Task API indices are 0-based. This creates an off-by-one mismatch between get_task(split, 0) and the first reset() result in stream mode.
        self._cursor += 1
        self._target = str(row[TEXT_COLUMN])
        self._current_split, self._current_index = split, self._cursor
        self._current_task_id = f"{split}-stream-{self._cursor}"
        remaining = (total - self._cursor) if total > 0 else -1

envs/latex_ocr_env/server/latex_ocr_environment.py:342

  • In stream mode, step() uses _split_total(...) (raw metadata) for total, but reset() uses self.num_tasks() which applies LATEX_OCR_MAX_ROWS. If LATEX_OCR_MAX_ROWS is set, total/remaining/pct_done will disagree between reset and step responses.
        total = (
            _split_total(self.dataset_name, self._current_split)
            if self.mode == "stream"
            else -1
        )

examples/grpo_latex_ocr/README.md:3

  • The Colab badge URL points at a personal fork/branch (adithya-s-k/OpenEnv@add-latex-ocr-multimodal-streaming-env). After merge, that link will likely 404. It should point at the canonical huggingface/OpenEnv location for this notebook.
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/adithya-s-k/OpenEnv/blob/add-latex-ocr-multimodal-streaming-env/examples/grpo_latex_ocr/grpo_latex_ocr_tutorial.ipynb)

examples/grpo_latex_ocr/README.md:39

  • This README claims the environment reward includes "structural validity, length/format", but the environment's rubric implementation is only edit similarity (CER) + exact match. This description should match the actual reward components to avoid misleading users.
See [`envs/latex_ocr_env`](../../envs/latex_ocr_env) for the full environment: a dataset-backed,
single-step (bandit) RL task with a weighted, server-side reward (edit similarity, exact match,
structural validity, length/format). It ships `train` and `test` splits.

examples/grpo_latex_ocr/grpo_latex_ocr_tutorial.ipynb:41

  • The notebook installs openenv-latex_ocr_env from a personal fork/branch (adithya-s-k/OpenEnv@add-latex-ocr-multimodal-streaming-env). After merge, this will likely break. Install from the canonical huggingface/OpenEnv repo (or a release tag) instead.
    "    \"openenv-latex_ocr_env @ git+https://github.com/adithya-s-k/OpenEnv.git@add-latex-ocr-multimodal-streaming-env#subdirectory=envs/latex_ocr_env\" \\\n",

…ar tunable

Default exact_weight 0.2 -> 0.4 so partial answers cap at 0.6 and exact matches
are rewarded more strongly. Tunable at runtime via LATEX_OCR_EXACT_WEIGHT.
Copilot AI review requested due to automatic review settings July 23, 2026 10:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (6)

envs/latex_ocr_env/server/latex_ocr_environment.py:197

  • In stream mode, get_task_range() can return an empty list when num_tasks() is unknown (n <= 0). Specifically, when stop is omitted (None) and n is -1, stop becomes -1 and range(start, stop) yields no stubs, which breaks default Task API enumeration.
    def get_task_range(
        self, split: str, start: int | None = None, stop: int | None = None
    ) -> list[dict[str, Any]]:
        n = self.num_tasks(split)
        start = 0 if start is None else start
        stop = n if stop is None else (min(stop, n) if n > 0 else stop)
        # In stream mode `n` comes from metadata and can be enormous; cap the number
        # of generated stubs so an unbounded range can't OOM the process.
        if self.mode == "stream" and stop - start > _STREAM_RANGE_CAP:
            logger.warning(

envs/latex_ocr_env/server/latex_ocr_environment.py:345

  • step() computes total via _split_total() in stream mode, which ignores LATEX_OCR_MAX_ROWS. This makes total/remaining/pct_done inconsistent between reset() (which uses num_tasks() and honors the cap) and step().
        total = (
            _split_total(self.dataset_name, self._current_split)
            if self.mode == "stream"
            else -1
        )

envs/latex_ocr_env/server/gradio_ui.py:75

  • The UI hard-codes a 0.8/0.2 reward split, but the environment default exact_weight is 0.4 (and is configurable). This text should match the actual rubric or describe the parameterized formula.
        gr.Markdown(
            "Transcribe the image into **LaTeX**, then score it. Reward = "
            "`0.8·(1−CER) + 0.2·exact_match` against the hidden ground truth."
        )

examples/grpo_latex_ocr/README.md:3

  • This Colab badge links to a personal fork/feature branch. Other examples in this repo link to github/huggingface/OpenEnv/blob/main/...; this one should too so it stays valid after merge.
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/adithya-s-k/OpenEnv/blob/add-latex-ocr-multimodal-streaming-env/examples/grpo_latex_ocr/grpo_latex_ocr_tutorial.ipynb)

examples/grpo_latex_ocr/README.md:39

  • The reward description mentions "structural validity" and "length/format", but the LaTeX OCR environment rubric shown in this PR is edit-distance similarity + exact match only. This should be corrected to avoid misleading users.
See [`envs/latex_ocr_env`](../../envs/latex_ocr_env) for the full environment: a dataset-backed,
single-step (bandit) RL task with a weighted, server-side reward (edit similarity, exact match,
structural validity, length/format). It ships `train` and `test` splits.

examples/grpo_latex_ocr/grpo_latex_ocr_tutorial.ipynb:41

  • The notebook installs openenv-latex_ocr_env from a personal fork/branch. After merge, this should point at huggingface/OpenEnv (like other notebooks) so the example remains reproducible (e.g., git+https://github.com/huggingface/OpenEnv.git#subdirectory=envs/latex_ocr_env).
    "    \"openenv-latex_ocr_env @ git+https://github.com/adithya-s-k/OpenEnv.git@add-latex-ocr-multimodal-streaming-env#subdirectory=envs/latex_ocr_env\" \\\n",

Comment thread docs/source/environments/latex_ocr.md Outdated
Comment on lines +12 to +16
- **Reward**: `(1 - exact_weight) * (1 - CER) + exact_weight * exact_match`,
where `CER` is the normalized character edit distance over whitespace-stripped
LaTeX. Partial answers score in `[0, 0.8]`; only an exact match reaches `1.0`.
Computed **server-side** against the hidden ground truth — the agent never
sees the target on `reset`. Dense and smooth, for stable RL training.

@sergiopaniego sergiopaniego left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great one!
we need to solve some comments from automated PR reviews, many are legit.

some ideas:

  • the CI is failing: python scripts/sync_env_docs.py --fix should fix it.
  • env hosting: the env could be moved the OpenEnv's org and in the notebook we could link it directly
  • docs: we could add the env to docs/source/environments.md

The whitespace-insensitive reward let a policy score 1.0 by emitting the correct
answer followed by unlimited whitespace (the padding is stripped before scoring).
In RL this shows up as completions drifting to the generation-length cap.

Add a length_factor to the rubric that decays the reward once the RAW prediction
exceeds overlong_ratio x the target length (default 4.0, floor 80 chars; tunable
via LATEX_OCR_OVERLONG_RATIO / LATEX_OCR_OVERLONG_FLOOR, 0 disables). The rubric
now also cleans raw completions (code fences, $-delimiters) itself and measures
their true length, so it can score model output directly. Legit answers and
normal LaTeX spacing are unaffected; padded completions go from 1.0 to ~0.
Copilot AI review requested due to automatic review settings July 23, 2026 14:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (4)

envs/latex_ocr_env/server/latex_ocr_environment.py:323

  • In stream mode, the cursor is incremented before being recorded in index/task_id, making the first task report index=1 (and the last task index==total). This is inconsistent with the Task API stubs (get_task/list_tasks are 0-based) and makes progress/accounting off by one.
        self._cursor += 1
        self._target = str(row[TEXT_COLUMN])
        self._current_split, self._current_index = split, self._cursor
        self._current_task_id = f"{split}-stream-{self._cursor}"
        remaining = (total - self._cursor) if total > 0 else -1

envs/latex_ocr_env/server/latex_ocr_environment.py:352

  • In stream mode, step() uses _split_total() (metadata) for total, but reset() uses num_tasks() (which also honors LATEX_OCR_MAX_ROWS). When LATEX_OCR_MAX_ROWS is set, this makes total/remaining/pct_done inconsistent between reset and step.
        total = (
            _split_total(self.dataset_name, self._current_split)
            if self.mode == "stream"
            else -1
        )

envs/latex_ocr_env/server/gradio_ui.py:75

  • The Gradio UI text hardcodes reward weights (0.8/0.2), but the environment defaults to exact_weight=0.4 (edit=0.6, exact=0.4) and is env-var configurable. This is user-facing documentation drift inside the UI.
        gr.Markdown(
            "Transcribe the image into **LaTeX**, then score it. Reward = "
            "`0.8·(1−CER) + 0.2·exact_match` against the hidden ground truth."
        )

examples/grpo_latex_ocr/README.md:39

  • This README claims the reward includes “structural validity”, but the current rubric implementation is edit similarity + exact match (plus the length guard). This mismatch will confuse readers and makes the example documentation inaccurate.
See [`envs/latex_ocr_env`](../../envs/latex_ocr_env) for the full environment: a dataset-backed,
single-step (bandit) RL task with a weighted, server-side reward (edit similarity, exact match,
structural validity, length/format). It ships `train` and `test` splits.

Comment on lines +211 to +214
return [
{"id": f"{split}-{i}", "index": i, "split": split}
for i in range(start, stop)
]
Comment on lines +133 to +164
def grade(self, prediction: str, target: str) -> GradeResult:
raw = prediction or ""
# Clean the raw completion (fences, $-delimiters) before scoring; LaTeX spacing is
# cosmetic, so score on the whitespace-stripped form: an exact match => CER 0.
cleaned = clean_latex(raw)
pred_canon = _strip_all_whitespace(normalize_latex(cleaned))
target_norm = normalize_latex(target)
target_canon = _strip_all_whitespace(target_norm)

exact = pred_canon == target_canon
if not target_canon:
cer = 0.0 if not pred_canon else 1.0
else:
distance = levenshtein(pred_canon, target_canon)
cer = min(1.0, distance / max(len(pred_canon), len(target_canon)))

similarity = 1.0 - cer
reward = (1.0 - self.exact_weight) * similarity + self.exact_weight * float(
exact
)

# Length guard: penalize predictions whose RAW length (measured before whitespace is
# stripped, so padding counts) far exceeds the target's. Reward decays linearly with
# the excess and reaches 0 at twice the allowance.
length_factor = 1.0
if self.overlong_ratio > 0:
allowed = max(self.overlong_floor, self.overlong_ratio * len(target_norm))
if len(raw) > allowed:
over = (len(raw) - allowed) / allowed
length_factor = max(0.0, 1.0 - over)
reward *= length_factor

Add held-out eval results + train/eval curves for four VLMs trained against the
latex_ocr env (Qwen3-VL-2B, Qwen3.5-2B, GLM-OCR, Gemma-4-E2B), showing stable,
monotonic improvement (deltas +3% to +48%).
Copilot AI review requested due to automatic review settings July 24, 2026 06:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 20 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (6)

envs/latex_ocr_env/server/latex_ocr_environment.py:352

  • In stream mode, step() reports total/remaining/pct_done using _split_total() (metadata) and ignores LATEX_OCR_MAX_ROWS, but reset() uses num_tasks() which does honor the cap. This can make progress jump or never reach 100% when a cap is set.
        total = (
            _split_total(self.dataset_name, self._current_split)
            if self.mode == "stream"
            else -1
        )

envs/latex_ocr_env/server/latex_ocr_environment.py:214

  • get_task_range() omits the sequential flag in stream mode, but list_tasks() / get_task() include it. Returning consistent task metadata helps clients understand random access is unsupported in stream mode.
        return [
            {"id": f"{split}-{i}", "index": i, "split": split}
            for i in range(start, stop)
        ]

envs/latex_ocr_env/server/gradio_ui.py:75

  • The UI text hard-codes a 0.8/0.2 reward split, but the environment defaults to exact_weight=0.4 (and it’s env-var tunable). This is user-facing documentation and will mislead users about reward semantics.
        gr.Markdown(
            "Transcribe the image into **LaTeX**, then score it. Reward = "
            "`0.8·(1−CER) + 0.2·exact_match` against the hidden ground truth."
        )

examples/grpo_latex_ocr/README.md:3

  • The Colab badge links to a personal fork/branch (adithya-s-k/OpenEnv + add-latex-ocr-...). Once merged, this URL is likely to 404 and the example becomes unusable from main.
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/adithya-s-k/OpenEnv/blob/add-latex-ocr-multimodal-streaming-env/examples/grpo_latex_ocr/grpo_latex_ocr_tutorial.ipynb)

examples/grpo_latex_ocr/README.md:39

  • This README claims the environment reward includes “structural validity”, but the rubric implemented in this PR is CER + exact-match with a length guard (no structural-validity term). Keeping this accurate matters for users interpreting the results table.
See [`envs/latex_ocr_env`](../../envs/latex_ocr_env) for the full environment: a dataset-backed,
single-step (bandit) RL task with a weighted, server-side reward (edit similarity, exact match,
structural validity, length/format). It ships `train` and `test` splits.

examples/grpo_latex_ocr/grpo_latex_ocr_tutorial.ipynb:41

  • The notebook installs the environment from a personal fork/branch. After merge, this should reference the canonical repo (or PyPI) so the tutorial works for readers.
    "    \"openenv-latex_ocr_env @ git+https://github.com/adithya-s-k/OpenEnv.git@add-latex-ocr-multimodal-streaming-env#subdirectory=envs/latex_ocr_env\" \\\n",

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Env Request] LaTeX OCR — multimodal environment on the new Task API + dataset streaming

3 participants