mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(skills): preserve transparency in composed PowerPoint slides (#7312)
* fix(skills): preserve transparency in composed PowerPoint slides * ci(skills): run public skill regressions * ci: remove public skill test workflow * docs: move skill test commands out of inherited guidance --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
5dca9ae240
commit
9b89c7569d
@ -338,6 +338,10 @@ before review.
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Public skill scripts (from the repository root)
|
||||
python -m pip install -r tests/skills/requirements.txt
|
||||
python -m pytest tests/skills -q
|
||||
|
||||
# Default backend tests (four parallel shards; excludes live and blocking-I/O tests)
|
||||
cd backend
|
||||
make test
|
||||
|
||||
@ -1551,6 +1551,8 @@ view. Built-in and user-created/imported skills are listed separately. The
|
||||
Community tab supports importing `.skill` archives into My skills.
|
||||
General preferences remain in Settings.
|
||||
|
||||
The **ppt-generation** skill preserves transparent slide images when composing a presentation, including grayscale and palette PNG transparency.
|
||||
|
||||
Administrators can see **Could not load** warnings in Capability Center > Skills
|
||||
when their own custom skill's `SKILL.md` contains invalid YAML. Warnings show the
|
||||
package-relative file and, when available, its line and column, with a quoting
|
||||
|
||||
@ -146,6 +146,8 @@ python /mnt/skills/public/image-generation/scripts/generate.py \
|
||||
|
||||
After all slide images are generated, call the composition script:
|
||||
|
||||
Transparent slide images retain their alpha channel in the presentation. Opaque images are embedded as JPEG as before.
|
||||
|
||||
```bash
|
||||
python /mnt/skills/public/ppt-generation/scripts/generate.py \
|
||||
--plan-file /mnt/user-data/workspace/presentation-plan.json \
|
||||
|
||||
@ -60,8 +60,11 @@ def generate_ppt(
|
||||
|
||||
# Load and process image
|
||||
with Image.open(image_path) as img:
|
||||
# Convert to RGB if necessary (for PNG with transparency)
|
||||
if img.mode in ("RGBA", "P"):
|
||||
image_format = "JPEG"
|
||||
if "A" in img.getbands() or "transparency" in img.info:
|
||||
img = img.convert("RGBA")
|
||||
image_format = "PNG"
|
||||
elif img.mode == "P":
|
||||
img = img.convert("RGB")
|
||||
|
||||
# Calculate dimensions to fill slide while maintaining aspect ratio
|
||||
@ -88,12 +91,16 @@ def generate_ppt(
|
||||
|
||||
# Save processed image to bytes
|
||||
img_bytes = BytesIO()
|
||||
img.save(img_bytes, format="JPEG", quality=95)
|
||||
img.save(img_bytes, format=image_format, quality=95)
|
||||
img_bytes.seek(0)
|
||||
|
||||
# Add image to slide
|
||||
slide.shapes.add_picture(
|
||||
img_bytes, left, top, Inches(new_width_emu / 914400), Inches(new_height_emu / 914400)
|
||||
img_bytes,
|
||||
left,
|
||||
top,
|
||||
Inches(new_width_emu / 914400),
|
||||
Inches(new_height_emu / 914400),
|
||||
)
|
||||
|
||||
# Add speaker notes if available in plan
|
||||
|
||||
7
tests/skills/requirements.txt
Normal file
7
tests/skills/requirements.txt
Normal file
@ -0,0 +1,7 @@
|
||||
# Standalone public skill tests; no backend or model credentials required.
|
||||
duckdb==2.0.0
|
||||
openpyxl==3.1.5
|
||||
Pillow==12.3.0
|
||||
pytest==9.1.1
|
||||
python-pptx==1.0.2
|
||||
requests==2.34.2
|
||||
72
tests/skills/test_ppt_transparency.py
Normal file
72
tests/skills/test_ppt_transparency.py
Normal file
@ -0,0 +1,72 @@
|
||||
"""Slide composition preserves transparency in the embedded image."""
|
||||
|
||||
import json
|
||||
import runpy
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
from pptx import Presentation
|
||||
|
||||
SCRIPT = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "skills/public/ppt-generation/scripts/generate.py"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["RGBA", "LA", "P", "RGB", "L"])
|
||||
def test_ppt_preserves_png_transparency(tmp_path, mode):
|
||||
source = tmp_path / "slide.png"
|
||||
if mode == "P":
|
||||
image = Image.new("P", (64, 64), 0)
|
||||
image.putpalette([0, 0, 0, 240, 80, 40] + [0] * 762)
|
||||
image.putpixel((48, 32), 1)
|
||||
image.save(source, transparency=0)
|
||||
elif mode in {"RGB", "L"}:
|
||||
image = Image.new(mode, (64, 64), 0)
|
||||
image.putpixel((48, 32), (240, 80, 40) if mode == "RGB" else 160)
|
||||
image.save(source, transparency=(0, 0, 0) if mode == "RGB" else 0)
|
||||
else:
|
||||
image = Image.new(mode, (64, 64), (0, 0, 0, 0) if mode == "RGBA" else (0, 0))
|
||||
image.putpixel((48, 32), (240, 80, 40, 128) if mode == "RGBA" else (160, 128))
|
||||
image.save(source)
|
||||
image.close()
|
||||
with Image.open(source) as original:
|
||||
expected = original.convert("RGBA")
|
||||
|
||||
plan = tmp_path / "plan.json"
|
||||
plan.write_text(
|
||||
json.dumps({"slides": [{"title": "Transparent slide"}]}), encoding="utf-8"
|
||||
)
|
||||
output = tmp_path / "slides.pptx"
|
||||
generate_ppt = runpy.run_path(str(SCRIPT))["generate_ppt"]
|
||||
|
||||
generate_ppt(str(plan), [str(source)], str(output))
|
||||
|
||||
presentation = Presentation(output)
|
||||
picture = presentation.slides[0].shapes[0]
|
||||
with Image.open(BytesIO(picture.image.blob)) as embedded:
|
||||
assert embedded.format == "PNG"
|
||||
assert embedded.convert("RGBA").tobytes() == expected.tobytes()
|
||||
assert (
|
||||
"Title: Transparent slide"
|
||||
in presentation.slides[0].notes_slide.notes_text_frame.text
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["RGB", "L", "P"])
|
||||
def test_ppt_keeps_opaque_images_as_jpeg(tmp_path, mode):
|
||||
source = tmp_path / "slide.png"
|
||||
with Image.new(mode, (64, 64), 100) as image:
|
||||
image.save(source)
|
||||
plan = tmp_path / "plan.json"
|
||||
plan.write_text("{}", encoding="utf-8")
|
||||
output = tmp_path / "slides.pptx"
|
||||
|
||||
runpy.run_path(str(SCRIPT))["generate_ppt"](str(plan), [str(source)], str(output))
|
||||
|
||||
picture = Presentation(output).slides[0].shapes[0]
|
||||
with Image.open(BytesIO(picture.image.blob)) as embedded:
|
||||
assert embedded.format == "JPEG"
|
||||
assert embedded.size == (64, 64)
|
||||
Loading…
x
Reference in New Issue
Block a user