Instructions to use EditPPT/editppt-vision-validator-lora with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use EditPPT/editppt-vision-validator-lora with PEFT:
from peft import PeftModel from transformers import AutoModelForCausalLM base_model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3.5-9B") model = PeftModel.from_pretrained(base_model, "EditPPT/editppt-vision-validator-lora") - Notebooks
- Google Colab
- Kaggle
Commit ·
da0be07
0
Parent(s):
Initial release
Browse files- .gitattributes +35 -0
- README.md +62 -0
- adapter_config.json +43 -0
- adapter_model.safetensors +3 -0
- inference_example.py +34 -0
- prompt_template.txt +68 -0
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
base_model: Qwen/Qwen3.5-9B
|
| 3 |
+
library_name: peft
|
| 4 |
+
license: apache-2.0
|
| 5 |
+
pipeline_tag: image-text-to-text
|
| 6 |
+
tags:
|
| 7 |
+
- lora
|
| 8 |
+
- peft
|
| 9 |
+
- presentation
|
| 10 |
+
- slide-editing
|
| 11 |
+
- visual-qa
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# EditPPT Vision Validator (LoRA on Qwen3.5-9B)
|
| 15 |
+
|
| 16 |
+
A LoRA adapter that checks whether an edit to a PowerPoint slide **newly introduced a severe visual defect**,
|
| 17 |
+
and if so, returns a repair as tool calls — in one generation. It serves as the vision validator of EditPPT.
|
| 18 |
+
|
| 19 |
+
Given the slide rendered **before** and **after** an edit, plus the edit instruction, the tool calls that made the
|
| 20 |
+
edit and the slide's shape JSON, the model returns:
|
| 21 |
+
|
| 22 |
+
```json
|
| 23 |
+
{"HasCriticalIssues": "Yes",
|
| 24 |
+
"Issues": [{"IssueType": "ELEMENT_OVERLAP", "AffectedShapeIDs": [2],
|
| 25 |
+
"TechnicalDiagnosis": "textbox 2 overlaps the caption below it by about 20 pt"}],
|
| 26 |
+
"ToolCalls": [{"name": "adjust_layout", "arguments": {"slide_number": 3, "shape_id": 2, "top": 470}}]}
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
* **Defect types:** `CONTENT_OFF_SLIDE`, `ELEMENT_OVERLAP`, `LOW_CONTRAST`.
|
| 30 |
+
* **Repair tools:** `set_shape_font_size`, `adjust_layout`, `set_text_style`. Values are absolute targets in slide points.
|
| 31 |
+
* Defects already present in the BEFORE image are treated as the deck's own design and are not reported.
|
| 32 |
+
|
| 33 |
+
## Usage
|
| 34 |
+
|
| 35 |
+
See `inference_example.py` and `prompt_template.txt`.
|
| 36 |
+
|
| 37 |
+
* Message order: `[image BEFORE, image AFTER, prompt text]`.
|
| 38 |
+
* Images: 960x540 (`min_pixels = max_pixels = 960*540`). One slide point = one pixel for a 16:9 deck.
|
| 39 |
+
* Greedy decoding, default chat template.
|
| 40 |
+
|
| 41 |
+
## Training
|
| 42 |
+
|
| 43 |
+
| | |
|
| 44 |
+
|---|---|
|
| 45 |
+
| Base | Qwen3.5-9B, bf16 |
|
| 46 |
+
| Adapter | LoRA, r 16, alpha 32, on the language model and the vision encoder |
|
| 47 |
+
| Data | 7,260 training samples (plus 374 for validation) with human-confirmed labels, from publicly licensed decks (CC0 / CC-BY) |
|
| 48 |
+
|
| 49 |
+
## Evaluation
|
| 50 |
+
|
| 51 |
+
Held-out set of 750 samples from decks not seen in training:
|
| 52 |
+
|
| 53 |
+
| Metric | Value |
|
| 54 |
+
|---|---|
|
| 55 |
+
| Verdict accuracy, edits without a repair (n=497) | 0.932 (base model: 0.60) |
|
| 56 |
+
| Verdict accuracy, all 750 samples | 0.955 |
|
| 57 |
+
| Defect precision / recall / F1, all 750 samples | 0.963 / 0.954 / 0.959 |
|
| 58 |
+
| Repair targets the right shapes with a permitted tool, all 750 samples | 0.948 |
|
| 59 |
+
|
| 60 |
+
## License
|
| 61 |
+
|
| 62 |
+
Apache-2.0, following the base model.
|
adapter_config.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"alora_invocation_tokens": null,
|
| 3 |
+
"alpha_pattern": {},
|
| 4 |
+
"arrow_config": null,
|
| 5 |
+
"auto_mapping": null,
|
| 6 |
+
"base_model_name_or_path": "Qwen/Qwen3.5-9B",
|
| 7 |
+
"bias": "none",
|
| 8 |
+
"corda_config": null,
|
| 9 |
+
"ensure_weight_tying": false,
|
| 10 |
+
"eva_config": null,
|
| 11 |
+
"exclude_modules": null,
|
| 12 |
+
"fan_in_fan_out": false,
|
| 13 |
+
"inference_mode": true,
|
| 14 |
+
"init_lora_weights": true,
|
| 15 |
+
"kasa_config": null,
|
| 16 |
+
"layer_replication": null,
|
| 17 |
+
"layers_pattern": null,
|
| 18 |
+
"layers_to_transform": null,
|
| 19 |
+
"loftq_config": {},
|
| 20 |
+
"lora_alpha": 32,
|
| 21 |
+
"lora_bias": false,
|
| 22 |
+
"lora_dropout": 0.05,
|
| 23 |
+
"lora_ga_config": null,
|
| 24 |
+
"megatron_config": null,
|
| 25 |
+
"megatron_core": "megatron.core",
|
| 26 |
+
"modules_to_save": null,
|
| 27 |
+
"monteclora_config": null,
|
| 28 |
+
"peft_type": "LORA",
|
| 29 |
+
"peft_version": "0.21.2",
|
| 30 |
+
"qalora_group_size": 16,
|
| 31 |
+
"r": 16,
|
| 32 |
+
"rank_pattern": {},
|
| 33 |
+
"revision": null,
|
| 34 |
+
"target_modules": "(?:.*language_model.*\\.(q_proj|k_proj|v_proj|o_proj|gate_proj|up_proj|down_proj))|(?:.*language_model.*\\.linear_attn\\.(in_proj_qkv|in_proj_z|out_proj))|(?:.*visual\\.(blocks\\.\\d+\\.(attn\\.qkv|attn\\.proj|mlp\\.linear_fc1|mlp\\.linear_fc2)|merger\\.(linear_fc1|linear_fc2)))",
|
| 35 |
+
"target_parameters": null,
|
| 36 |
+
"task_type": "CAUSAL_LM",
|
| 37 |
+
"trainable_token_indices": null,
|
| 38 |
+
"use_bdlora": null,
|
| 39 |
+
"use_dora": false,
|
| 40 |
+
"use_qalora": false,
|
| 41 |
+
"use_rslora": false,
|
| 42 |
+
"velora_config": null
|
| 43 |
+
}
|
adapter_model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2146dc09d1a49801a4bcb6058250b4fee4b9586a13e7b9d834d27ec8f9c72c0c
|
| 3 |
+
size 192469744
|
inference_example.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Minimal inference with the EditPPT vision-validator LoRA on Qwen3.5-9B.
|
| 2 |
+
|
| 3 |
+
Input order matters: [image BEFORE the edit, image AFTER the edit, prompt]. Render slides at 960x540
|
| 4 |
+
(1 slide pt = 1 px for a standard 16:9 deck). Fill prompt_template.txt with str.replace, not str.format
|
| 5 |
+
(the template contains literal JSON braces).
|
| 6 |
+
"""
|
| 7 |
+
import json
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from peft import PeftModel
|
| 11 |
+
from PIL import Image
|
| 12 |
+
from transformers import AutoModelForImageTextToText, AutoProcessor
|
| 13 |
+
|
| 14 |
+
BASE, ADAPTER = "Qwen/Qwen3.5-9B", "EditPPT/editppt-vision-validator-lora"
|
| 15 |
+
PIXELS = 960 * 540
|
| 16 |
+
|
| 17 |
+
processor = AutoProcessor.from_pretrained(BASE, min_pixels=PIXELS, max_pixels=PIXELS)
|
| 18 |
+
model = AutoModelForImageTextToText.from_pretrained(BASE, dtype=torch.bfloat16, device_map="cuda:0")
|
| 19 |
+
model = PeftModel.from_pretrained(model, ADAPTER).eval()
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def validate(before_png, after_png, instruction, tools_used, slide_json):
|
| 23 |
+
prompt = (open("prompt_template.txt").read()
|
| 24 |
+
.replace("{instruction}", instruction)
|
| 25 |
+
.replace("{tools_used}", str(tools_used)) # python-repr list of {'name', 'arguments'}
|
| 26 |
+
.replace("{slide_json}", str(slide_json))) # python-repr dict: slide_w, slide_h, current_slide, objects
|
| 27 |
+
msgs = [{"role": "user", "content": [{"type": "image"}, {"type": "image"}, {"type": "text", "text": prompt}]}]
|
| 28 |
+
text = processor.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False)
|
| 29 |
+
imgs = [Image.open(p).convert("RGB") for p in (before_png, after_png)]
|
| 30 |
+
enc = processor(text=[text], images=imgs, return_tensors="pt").to(model.device)
|
| 31 |
+
with torch.no_grad():
|
| 32 |
+
out = model.generate(**enc, max_new_tokens=1024, do_sample=False)
|
| 33 |
+
raw = processor.tokenizer.decode(out[0, enc["input_ids"].shape[1]:], skip_special_tokens=True)
|
| 34 |
+
return json.loads(raw[raw.index("{"): raw.rindex("}") + 1])
|
prompt_template.txt
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are a PPT QA specialist. Evaluate whether THIS modification newly introduced a SEVERE visual defect.
|
| 2 |
+
|
| 3 |
+
## Images
|
| 4 |
+
Two renders of the same slide are attached: the FIRST is the slide BEFORE this modification, the
|
| 5 |
+
SECOND is AFTER it. Judge only what changed between them — anything already visible in the first
|
| 6 |
+
image is the deck's own design and is NOT a defect of this modification.
|
| 7 |
+
|
| 8 |
+
## What was modified
|
| 9 |
+
{instruction}
|
| 10 |
+
|
| 11 |
+
## Tools used
|
| 12 |
+
{tools_used}
|
| 13 |
+
|
| 14 |
+
## Defect categories
|
| 15 |
+
CONTENT_OFF_SLIDE content crosses the slide edge and is cut off
|
| 16 |
+
ELEMENT_OVERLAP content collides with other content, is painted over by it, or spills across a visible boundary (another shape, or its own filled box), so it cannot be read
|
| 17 |
+
LOW_CONTRAST text cannot be read against the background behind it (e.g. dark text on a dark band)
|
| 18 |
+
|
| 19 |
+
## Reporting rule
|
| 20 |
+
Report a defect ONLY if it (a) was newly caused by this modification on the modified shapes or their spatial neighbors, (b) is immediately obvious at normal presentation scale, and (c) clearly degrades readability/credibility. If uncertain, do NOT report.
|
| 21 |
+
|
| 22 |
+
## Repair tools
|
| 23 |
+
A repair is a tool call on the AFTER slide. Only these three tools exist. `slide_number` is the JSON
|
| 24 |
+
`current_slide`, `shape_id` is the JSON `id`, and every value is an absolute target in the slide JSON
|
| 25 |
+
units (the canvas is `slide_w` × `slide_h` from the JSON, 960x540 for a standard 16:9 deck; no unit
|
| 26 |
+
conversion) — read the current value from the slide JSON below (each
|
| 27 |
+
object has `id`, `left`, `top`, `width`, `height`, `max_font_size`, and, when text exists, `text`).
|
| 28 |
+
- set_shape_font_size(slide_number, shape_id, font_size): set the font size (pt) of every run in the
|
| 29 |
+
shape; the current size is `max_font_size`.
|
| 30 |
+
- adjust_layout(slide_number, shape_id, left?, top?, width?, height?): set the box's position and/or
|
| 31 |
+
size; pass only the fields that change, as target values, never as deltas.
|
| 32 |
+
- set_text_style(slide_number, shape_id, scope="all", color_hex): recolour all text in the shape,
|
| 33 |
+
e.g. "#FFFFFF" when the text sits on a dark background (LOW_CONTRAST).
|
| 34 |
+
|
| 35 |
+
## Repair rule
|
| 36 |
+
Fix the shape THIS modification changed, and leave the rest of the slide as the author designed it.
|
| 37 |
+
The defect came from that edit, so the repair belongs there: move it, resize its box, or reduce its
|
| 38 |
+
font size.
|
| 39 |
+
|
| 40 |
+
Choose by the space around the shape, not by habit:
|
| 41 |
+
- Free space beside, above or below the shape -> MOVE it into that space, along one axis where that
|
| 42 |
+
is enough, so the slide's existing alignment survives.
|
| 43 |
+
- No free space -> make the shape fit instead: reduce its font size, or change its box.
|
| 44 |
+
- A box's width is not a fixed property of the shape. Widen it when the text is cramped and there is
|
| 45 |
+
room next to it; narrow it when the shape runs past the slide edge and the text can wrap instead.
|
| 46 |
+
|
| 47 |
+
Combine actions when one alone leaves the defect — moving a box up AND reducing its font size is one
|
| 48 |
+
repair, not two. Prefer the smallest change that removes the defect: one action when one is enough,
|
| 49 |
+
two when one is not.
|
| 50 |
+
|
| 51 |
+
Name a neighbouring shape only when nothing the edited shape may legally change can resolve the
|
| 52 |
+
defect (e.g. its font size was what the user asked for and is therefore fixed) — moving a shape the
|
| 53 |
+
user never mentioned is itself a change to their slide, so it is a last resort, and then move it by
|
| 54 |
+
the smallest amount that clears the overlap.
|
| 55 |
+
|
| 56 |
+
## Output format
|
| 57 |
+
Return one JSON object and nothing else:
|
| 58 |
+
{"HasCriticalIssues": "Yes" | "No",
|
| 59 |
+
"Issues": [{"IssueType": "<category>", "AffectedShapeIDs": [<id>, ...],
|
| 60 |
+
"TechnicalDiagnosis": "<what is wrong, on which shape, by how much>"}],
|
| 61 |
+
"ToolCalls": [{"name": "<tool>", "arguments": {...}}],
|
| 62 |
+
"Unfixable": "<why no permitted change removes the defect>"}
|
| 63 |
+
"No" means empty `Issues` and empty `ToolCalls`. Every "Yes" carries at least one tool call, except
|
| 64 |
+
when the request itself fixed the only property that could change — then `ToolCalls` is empty and
|
| 65 |
+
`Unfixable` says so. Omit `Unfixable` otherwise.
|
| 66 |
+
|
| 67 |
+
## Slide JSON
|
| 68 |
+
{slide_json}
|