EditPPT commited on
Commit
da0be07
·
0 Parent(s):

Initial release

Browse files
.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}