
Skywork Ppt
- 611 installs
- 196 repo stars
- Updated April 2, 2026
- skyworkai/skywork-skills
skywork-ppt is a Skywork agent skill that generates, restyles, and edits PowerPoint .pptx decks from natural language for developers who need stakeholder slides for pitches, launches, or executive updates.
About
skywork-ppt is a Skywork skill for PowerPoint work triggered across English, Chinese, Japanese, and Korean phrases—generate a PPT from a topic, imitate an existing .pptx template or style, or edit slides by number with commands like modify slide N, change background, or add a slide. Developers reach for skywork-ppt when a launch deck, investor pitch, or internal stakeholder update must ship quickly without rebuilding masters in Keynote or Google Slides. The skill handles creation, template cloning, visual polish requests, and targeted slide edits through conversational instructions rather than manual ribbon clicking.
- Four capabilities: generate from topic, imitate .pptx template/style, natural-language slide edits, local .pptx file ops
- Multilingual triggers for generate, template, edit, merge, reorder, and slide-count queries
- Local operations: delete/reorder slides, merge pptx, extract slides without backend when applicable
- Requires python3 and SKYWORK_API_KEY for cloud generation and templating flows
- Edit intents: modify slide N, backgrounds, add slides, beautify existing decks
Skywork Ppt by the numbers
- 611 all-time installs (skills.sh)
- +9 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #142 of 688 Office & Documents skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/skyworkai/skywork-skills --skill skywork-pptAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 611 |
|---|---|
| repo stars | ★ 196 |
| Security audit | 1 / 3 scanners passed |
| Last updated | April 2, 2026 |
| Repository | skyworkai/skywork-skills ↗ |
How do you generate a PowerPoint from a topic?
Generate, restyle, or edit PowerPoint decks from natural language using Skywork when you need slides for pitches, launches, or stakeholder updates.
Who is it for?
Developers and PMs who must ship pitch or launch PowerPoint decks quickly using natural-language edits instead of manual slide authoring.
Skip if: Developers who need browser-based HTML/CSS slide decks with cinematic animations rather than native .pptx files.
When should I use this skill?
The user asks to generate a PPT, imitate a template, modify slide N, change backgrounds, or beautify an existing PowerPoint.
What you get
Finished or updated .pptx presentation files with new slides, restyled layouts, or template-matched decks.
- .pptx presentation file
- restyled slide deck
By the numbers
- Supports creation triggers in 4 languages: English, Chinese, Japanese, and Korean
Files
PPT Write Skill
Four capabilities: generate, template imitation, edit existing PPT, and local file operations.
---
Prerequisites
API Key Configuration (Required First)
This skill requires a SKYWORK_API_KEY to be configured in OpenClaw.
If you don't have an API key yet, please visit: https://skywork.ai
For detailed setup instructions, see: references/apikey-fetch.md
---
Privacy & Remote Calls (Read Before Use)
- Remote upload & processing: Layers 1/2/4 upload local files and send the full, verbatim user query to the Skywork service. Avoid sensitive or confidential content unless you trust the remote service and its data handling policies.
- Local-only operations: Layer 3 (local ops) runs entirely on-device and does not call the remote gateway. Use Layer 3 if you need strict local processing.
- Polling behavior: The generation/edit workflows include periodic status polling (about every 5 seconds) while waiting for backend jobs. This is expected.
---
Routing — Identify the user's intent first
| User intent | Which path |
|---|---|
| Generate a new PPT from a topic, set of requirements or reference files | Layer 1 — Generate |
| Use an existing .pptx as a layout/style template to create a new presentation | Layer 2 — Imitate |
| Edit an existing PPT: modify slides, add slides, change style, split/merge | Layer 4 — Edit |
| Delete / reorder / extract / merge slides in a local file (no backend) | Layer 3 — Local ops |
---
Environment check (always run this first)
This skill requires Python 3 (>=3.8). Run the following before any script to locate a valid Python binary and install dependencies.
PYTHON_CMD=""
for cmd in python3 python python3.13 python3.12 python3.11 python3.10 python3.9 python3.8; do
if command -v "$cmd" &>/dev/null && "$cmd" -c "import sys; exit(0 if sys.version_info >= (3,8) else 1)" 2>/dev/null; then
PYTHON_CMD="$cmd"
break
fi
done
if [ -z "$PYTHON_CMD" ]; then
echo "ERROR: Python 3.8+ not found."
echo "Install on macOS: brew install python3 or visit https://www.python.org/downloads/"
exit 1
fi
echo "Found Python: $PYTHON_CMD ($($PYTHON_CMD --version))"
$PYTHON_CMD -m pip install -q --break-system-packages python-pptx
echo "Dependencies ready."After this check, replacepythonwith the discovered$PYTHON_CMD(e.g.python3) in all subsequent commands.
---
Layer 1 — Generate PPT
Steps
0. REQUIRED FIRST STEP — Read workflow_generate.md NOW, before taking any other action. After reading, output exactly: ✅ workflow_generate.md loaded. — then proceed. 1. Environment check — run the check above to get $PYTHON_CMD. 2. Upload reference files (if the user provides local files as content source) — parse the file using tool in script/parse_file.py and pass the result to --files. See the --files note below. 3. Web search (required if no relevant content is already in the conversation) — call web_search tool in script to search the topic and distill results into a reference-file file of ≤ 2000 words. 4. Run the script:
Important: set exec toolyieldMsto600000(10 minutes).
5. Deliver — provide the absolute .pptx path and the download URL.
---
Layer 2 — Imitate PPT (template-based generation)
Steps
0. REQUIRED FIRST STEP - Read workflow_imitate.md immidiately before any action you do!!! 1. Environment check — run the check above to get $PYTHON_CMD. 2. Locate the template — extract the absolute path of the local .pptx from the user's message; ask the user if it's unclear. 3. Upload the template — upload it and extract TEMPLATE_URL from the output. 4. Upload reference files (if the user provides additional local files as content source) — parse the file using tool in script/parse_file.py and pass the result to --files. See the --files 5. Web search (required if no relevant content is already in the conversation) — call web_search tool in script to search the new topic and distill results into a reference-file file of ≤ 2000 words. 6. Run the script:
Important: set exec toolyieldMsto600000(10 minutes).
7. Deliver — provide the absolute .pptx path, the download URL, and the template filename used.
---
Layer 4 — Edit PPT (AI-powered modification)
Use this layer when the user wants to modify an existing PPT using natural language. Requires an OSS/CDN URL of the PPTX (from a previous generation or upload).
Steps
0. Detailed workflow - Read workflow_edit.md immediately before any action you do!!! 1. Environment check — run the check above to get $PYTHON_CMD. 2. Get PPTX URL — from the user's message or upload a local file first. 3. Run the script with --pptx-url:
$PYTHON_CMD scripts/run_ppt_write.py "edit instruction" \
--language Chinese \
--pptx-url "https://cdn.example.com/file.pptx" \
-o /absolute/path/output.pptxImportant: set exec toolyieldMsto600000(10 minutes).
4. Deliver — provide download link, local path, and summary of changes.
---
Layer 3 — Local file operations
# Inspect slide count and titles
$PYTHON_CMD scripts/local_pptx_ops.py info --file my.pptx
# Delete slides (1-based index; supports ranges like 3,5,7-9; omit -o to overwrite in place)
$PYTHON_CMD scripts/local_pptx_ops.py delete --file my.pptx --slides 3,5,7-9 -o trimmed.pptx
# Reorder slides (must list every slide, no omissions)
$PYTHON_CMD scripts/local_pptx_ops.py reorder --file my.pptx --order 2,1,4,3,5
# Extract a subset of slides into a new file
$PYTHON_CMD scripts/local_pptx_ops.py extract --file my.pptx --slides 1-3 -o subset.pptx
# Merge multiple files
$PYTHON_CMD scripts/local_pptx_ops.py merge --files a.pptx b.pptx -o merged.pptxRead workflow_local.md immidiately before any action you do!!!
---
Error Handling
- Insufficient benefit: When calling scripts (generate, imitate, or edit), the script or log may show a message like
Insufficient benefit. Please upgrade your account at {url}, meaning the user's benefit level does not meet the requirement for this skill.
How to reply when benefit is insufficient
When you detect the above, reply in the user's current language — do not echo the English message. Use this pattern:
- Convey: "Sorry, PPT generation failed. This skill requires upgrading your Skywork membership to use." then a single call-to-action link.
- Format: One short sentence in the user's language + a link like
[Upgrade now →](url)or the equivalent in their language . - URL: Extract the upgrade URL from the log/script output (e.g. the
at https://...part).
Note: Only suggest upgrading when the error is Insufficient benefit. For auth errors likeNO_TOKEN/INVALID_TOKEN/401/ “invalid API key”, keep the error code / raw message and guide users to updateSKYWORK_API_KEY. Do not suggest upgrading membership.
---
Dependencies
- Python 3.8+ (required) —
python3/pythonmust be on PATH - Layer 3 local ops:
pip install python-pptx --break-system-packages
(The environment check step installs all required dependencies automatically.)
---
Which layer to trigger?
| Scenario | Use |
|---|---|
| Generate a PPT from a topic or existing reference files | Layer 1 |
| Imitate the layout/style of an existing .pptx | Layer 2 |
| Edit/modify an existing PPT via natural language | Layer 4 |
| Delete / reorder / extract / merge local .pptx files (no backend) | Layer 3 |
Skywork API Key Setup Guide (OpenClaw)
SKYWORK_API_KEY Not Configured
When the SKYWORK_API_KEY environment variable is not set, follow these steps:
1. Get API Key
Visit the Skywork website and sign in to your account:
https://skywork.ai
- Log in with your Skywork account
- Open account / Settings / API Key (https://skywork.ai/?openApiKeySetting=1)
- Create or copy your API key
If your organization uses a separate console or test environment, use the URL and credentials your team provides.
2. Configure OpenClaw
Edit the OpenClaw configuration file: ~/.openclaw/openclaw.json
In current OpenClaw, Skywork skills store the key under skills.entries.<Skill Name>.apiKey (not under env). OpenClaw will inject this value into the skill's SKYWORK_API_KEY environment when primaryEnv matches. Add or merge the following structure (adjust the skill name to match the installed skill):
{
"skills": {
"entries": {
"Skywork-ppt": {
"enabled": true,
"apiKey": "your_actual_skywork_api_key_here"
}
}
}
}Replace "your_actual_skywork_api_key_here" with your real key.
For multiple Skywork skills, repeat the same apiKey field on each skill entry.
3. Verify Configuration
# Check JSON format
cat ~/.openclaw/openclaw.json | python3 -m json.tool4. Restart OpenClaw
openclaw gateway restartTroubleshooting
- Ensure
~/.openclaw/openclaw.jsonexists and is valid JSON - Confirm the API key is active and not expired
- Check Skywork account status, membership, or quota if requests fail with auth or benefit errors
- Restart OpenClaw after configuration changes
Recommended: Use the OpenClaw configuration file for centralized environment management.
SKYWORK_GATEWAY_URL = "https://api-tools.skywork.ai/theme-gateway"
POD_TYPE = ""
#!/usr/bin/env python3
"""
Local PPTX operation tool (Layer 3, no backend required).
Dependency: pip install python-pptx
Subcommands:
info View basic file info (slide count, slide titles)
delete Delete specified slides (1-based, supports multiple and ranges, e.g. 3,5,7-9)
reorder Reorder slide sequence (e.g. 2,1,4,3,5)
extract Extract selected slides into a new file
merge Merge multiple pptx files
Usage examples:
python scripts/local_pptx_ops.py info --file my.pptx
python scripts/local_pptx_ops.py delete --file my.pptx --slides 3,5,7-9
python scripts/local_pptx_ops.py delete --file my.pptx --slides 3,5,7-9 -o trimmed.pptx
python scripts/local_pptx_ops.py reorder --file my.pptx --order 2,1,4,3,5
python scripts/local_pptx_ops.py extract --file my.pptx --slides 1-3 -o subset.pptx
python scripts/local_pptx_ops.py merge --files a.pptx b.pptx -o merged.pptx
"""
import argparse
import os
import sys
import shutil
import copy
import warnings
try:
from pptx import Presentation
from pptx.util import Inches, Pt
from lxml import etree
except ImportError:
print("Missing dependency, please run: pip install python-pptx", file=sys.stderr)
sys.exit(1)
# ---------------------------------------------------------------------------
# Parse slide number string (1-based), return 0-based index list
# ---------------------------------------------------------------------------
def parse_slide_spec(spec: str, total: int) -> list[int]:
"""Parse a slide number string like '1,3,5-8,10', return a deduplicated sorted list of 0-based indices."""
indices = set()
for part in spec.split(","):
part = part.strip()
if not part:
continue
if "-" in part:
lo, hi = part.split("-", 1)
lo_i, hi_i = int(lo.strip()), int(hi.strip())
if lo_i < 1 or hi_i > total:
raise ValueError(f"Slide range {lo_i}-{hi_i} is out of bounds for a file with {total} slides")
for i in range(lo_i - 1, hi_i):
indices.add(i)
else:
n = int(part)
if n < 1 or n > total:
raise ValueError(f"Slide number {n} is out of bounds for a file with {total} slides")
indices.add(n - 1)
return sorted(indices)
def parse_order_spec(spec: str, total: int) -> list[int]:
"""Parse an order string for reorder, preserving user-specified order, return 0-based index list."""
result = []
seen = set()
for part in spec.split(","):
part = part.strip()
if not part:
continue
n = int(part)
if n < 1 or n > total:
raise ValueError(f"Slide number {n} is out of bounds for a file with {total} slides")
idx = n - 1
if idx in seen:
raise ValueError(f"Slide number {n} appears more than once")
seen.add(idx)
result.append(idx)
return result
# ---------------------------------------------------------------------------
# Core operation: remove a single slide (XML manipulation, not natively supported by python-pptx)
# ---------------------------------------------------------------------------
def _remove_slide(prs: "Presentation", index: int) -> None:
"""Remove the slide at the given index (0-based) from the Presentation."""
xml_slides = prs.slides._sldIdLst
slide_elem = xml_slides[index]
rId = slide_elem.get("{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id")
xml_slides.remove(slide_elem)
# Remove the corresponding relationship from rels to avoid "Duplicate name" warnings on save
prs.part.rels.pop(rId)
# ---------------------------------------------------------------------------
# Subcommand: info
# ---------------------------------------------------------------------------
def cmd_info(args):
prs = Presentation(args.file)
total = len(prs.slides)
print(f"File: {os.path.abspath(args.file)}")
print(f"Total slides: {total}")
print(f"Slide dimensions: {prs.slide_width.inches:.1f}\" x {prs.slide_height.inches:.1f}\"")
print()
for i, slide in enumerate(prs.slides):
title_text = ""
for shape in slide.shapes:
if shape.has_text_frame and shape.shape_type == 13:
continue # Skip images
try:
if hasattr(shape, "placeholder_format") and shape.placeholder_format is not None:
ph_idx = shape.placeholder_format.idx
if ph_idx == 0: # Title placeholder
title_text = shape.text_frame.text.strip()
break
except Exception:
pass
# Fallback: use the first shape that has text
if not title_text:
for shape in slide.shapes:
if shape.has_text_frame:
t = shape.text_frame.text.strip()
if t:
title_text = t[:60]
break
print(f" Slide {i+1:2d}: {title_text or '(no title)'}")
# ---------------------------------------------------------------------------
# Subcommand: delete
# ---------------------------------------------------------------------------
def cmd_delete(args):
prs = Presentation(args.file)
total = len(prs.slides)
indices = parse_slide_spec(args.slides, total)
if not indices:
print("No valid slide numbers specified, no action taken.")
return
# Delete from back to front to avoid index shifting
for idx in reversed(indices):
_remove_slide(prs, idx)
pages_str = ", ".join(str(i + 1) for i in indices)
remaining = total - len(indices)
out = _resolve_output(args, args.file)
prs.save(out)
print(f"Deleted slide(s) {pages_str} ({len(indices)} deleted, {remaining} remaining)")
print(f"Saved to: {os.path.abspath(out)}")
print()
print(f"RESULT: success")
print(f"OUTPUT_FILE: {os.path.abspath(out)}")
# ---------------------------------------------------------------------------
# Subcommand: reorder
# ---------------------------------------------------------------------------
def cmd_reorder(args):
prs = Presentation(args.file)
total = len(prs.slides)
order = parse_order_spec(args.order, total)
if len(order) != total:
raise ValueError(
f"--order specified {len(order)} slide numbers, but the file has {total} slides; all slides must be included without duplicates."
)
if sorted(order) != list(range(total)):
raise ValueError("--order must include each slide exactly once (no duplicates, no omissions).")
xml_slides = prs.slides._sldIdLst
# Retrieve all sldId elements
sld_id_elems = list(xml_slides)
# Rearrange in the new order
for elem in sld_id_elems:
xml_slides.remove(elem)
for new_pos in order:
xml_slides.append(sld_id_elems[new_pos])
out = _resolve_output(args, args.file)
prs.save(out)
order_str = ",".join(str(i + 1) for i in order)
print(f"Reordered to [{order_str}], total {total} slides")
print(f"Saved to: {os.path.abspath(out)}")
print()
print(f"RESULT: success")
print(f"OUTPUT_FILE: {os.path.abspath(out)}")
# ---------------------------------------------------------------------------
# Subcommand: extract
# ---------------------------------------------------------------------------
def cmd_extract(args):
prs = Presentation(args.file)
total = len(prs.slides)
keep = parse_slide_spec(args.slides, total)
if not keep:
print("No valid slide numbers specified, no action taken.")
return
# Delete slides not in the keep list
delete_indices = [i for i in range(total) if i not in set(keep)]
for idx in reversed(delete_indices):
_remove_slide(prs, idx)
out = args.output or "extracted.pptx"
prs.save(out)
pages_str = ", ".join(str(i + 1) for i in keep)
print(f"Extracted slide(s) {pages_str} ({len(keep)} slides)")
print(f"Saved to: {os.path.abspath(out)}")
print()
print(f"RESULT: success")
print(f"OUTPUT_FILE: {os.path.abspath(out)}")
# ---------------------------------------------------------------------------
# Subcommand: merge
# ---------------------------------------------------------------------------
def cmd_merge(args):
if not args.files or len(args.files) < 2:
raise ValueError("At least two --files must be provided.")
# Use the first file as the base
base_prs = Presentation(args.files[0])
slide_width = base_prs.slide_width
slide_height = base_prs.slide_height
for src_path in args.files[1:]:
src_prs = Presentation(src_path)
for slide in src_prs.slides:
# Add a new slide with a blank layout
slide_layout = base_prs.slide_layouts[-1] # Use the last layout (usually blank)
new_slide = base_prs.slides.add_slide(slide_layout)
# Copy the spTree (shape tree) contents from the source slide
new_sp_tree = new_slide.shapes._spTree
old_sp_tree = slide.shapes._spTree
# Remove default placeholders from the new slide
for child in list(new_sp_tree):
new_sp_tree.remove(child)
# Copy all child elements
for child in old_sp_tree:
new_sp_tree.append(copy.deepcopy(child))
out = args.output or "merged.pptx"
base_prs.save(out)
total = len(base_prs.slides)
files_str = ", ".join(args.files)
print(f"Merged: {files_str}")
print(f"Total {total} slides, saved to: {os.path.abspath(out)}")
print()
print(f"RESULT: success")
print(f"OUTPUT_FILE: {os.path.abspath(out)}")
# ---------------------------------------------------------------------------
# Utility functions
# ---------------------------------------------------------------------------
def _resolve_output(args, original_file: str) -> str:
"""Use -o output path if specified by the user, otherwise overwrite the original file."""
if hasattr(args, "output") and args.output:
return args.output
return original_file
# ---------------------------------------------------------------------------
# CLI entry point
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Local PPTX operation tool (delete/reorder/extract/merge slides)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python scripts/local_pptx_ops.py info --file my.pptx
python scripts/local_pptx_ops.py delete --file my.pptx --slides 3,5,7-9
python scripts/local_pptx_ops.py delete --file my.pptx --slides 3,5,7-9 -o trimmed.pptx
python scripts/local_pptx_ops.py reorder --file my.pptx --order 2,1,4,3,5
python scripts/local_pptx_ops.py extract --file my.pptx --slides 1-3 -o subset.pptx
python scripts/local_pptx_ops.py merge --files a.pptx b.pptx -o merged.pptx
""",
)
sub = parser.add_subparsers(dest="cmd", required=True)
# info
p_info = sub.add_parser("info", help="View file info (slide count, titles)")
p_info.add_argument("--file", required=True, help="Input .pptx file path")
# delete
p_del = sub.add_parser("delete", help="Delete specified slides (1-based, supports '3,5,7-9')")
p_del.add_argument("--file", required=True, help="Input .pptx file path")
p_del.add_argument("--slides", required=True, help="Slide numbers to delete, e.g. '3,5,7-9'")
p_del.add_argument("-o", "--output", default="", help="Output path (defaults to overwriting the original file)")
# reorder
p_reorder = sub.add_parser("reorder", help="Reorder slide sequence")
p_reorder.add_argument("--file", required=True, help="Input .pptx file path")
p_reorder.add_argument("--order", required=True, help="New order, e.g. '2,1,4,3,5' (must include all slides)")
p_reorder.add_argument("-o", "--output", default="", help="Output path (defaults to overwriting the original file)")
# extract
p_extract = sub.add_parser("extract", help="Extract selected slides into a new file")
p_extract.add_argument("--file", required=True, help="Input .pptx file path")
p_extract.add_argument("--slides", required=True, help="Slide numbers to keep, e.g. '1-3,5'")
p_extract.add_argument("-o", "--output", default="", help="Output path (defaults to extracted.pptx)")
# merge
p_merge = sub.add_parser("merge", help="Merge multiple pptx files")
p_merge.add_argument("--files", nargs="+", required=True, help="Input file list (merged in order)")
p_merge.add_argument("-o", "--output", default="", help="Output path (defaults to merged.pptx)")
args = parser.parse_args()
try:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message="Duplicate name", category=UserWarning)
if args.cmd == "info":
cmd_info(args)
elif args.cmd == "delete":
cmd_delete(args)
elif args.cmd == "reorder":
cmd_reorder(args)
elif args.cmd == "extract":
cmd_extract(args)
elif args.cmd == "merge":
cmd_merge(args)
except (ValueError, FileNotFoundError) as e:
print(f"Error: {e}", file=sys.stderr)
print()
print(f"RESULT: error")
print(f"ERROR: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Parse a reference file via the Skywork Office file/parse SSE endpoint.
Uploads the file, triggers server-side analysis (OCR, text extraction),
polls until complete, and returns the parsed content.
Usage:
python parse_file.py /path/to/document.pdf [--output parsed_content.txt]
Environment variables:
SKYWORK_API_KEY - Auth api key
"""
import argparse
import io
import json
import mimetypes
import os
import sys
import uuid
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
from constant import SKYWORK_GATEWAY_URL
from skywork_auth import get_skywork_api_key
def build_multipart_body(file_path: str):
"""Build a multipart/form-data request body."""
boundary = f"----FormBoundary{uuid.uuid4().hex[:16]}"
body = io.BytesIO()
file_name = os.path.basename(file_path)
mime_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
body.write(f"--{boundary}\r\n".encode())
body.write(f'Content-Disposition: form-data; name="file"; filename="{file_name}"\r\n'.encode())
body.write(f"Content-Type: {mime_type}\r\n\r\n".encode())
with open(file_path, "rb") as f:
body.write(f.read())
body.write(b"\r\n")
body.write(f"--{boundary}--\r\n".encode())
return body.getvalue(), f"multipart/form-data; boundary={boundary}"
def parse_sse_events(response):
"""Parse SSE events from an HTTP response stream."""
cur_event = None
cur_data = None
for line_bytes in response:
line = line_bytes.decode("utf-8", errors="replace").rstrip("\r\n")
if line == "":
if cur_event is not None and cur_data is not None:
try:
data = json.loads(cur_data)
yield cur_event, data
except json.JSONDecodeError:
pass
cur_event = None
cur_data = None
elif line.startswith("event: "):
cur_event = line[7:].strip()
elif line.startswith("data: "):
cur_data = line[6:]
if cur_event is not None and cur_data is not None:
try:
data = json.loads(cur_data)
yield cur_event, data
except json.JSONDecodeError:
pass
def parse_file(base_url: str, api_key: str, file_path: str) -> dict:
"""Upload and parse a file, streaming progress. Returns parsed metadata."""
url = f"{base_url}/api/sse/file/parse"
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
print(f"[parse] File: {file_name} ({file_size:,} bytes)")
print(f"[parse] Server: {base_url}")
print()
body, content_type = build_multipart_body(file_path)
req = Request(url, data=body, method="POST")
req.add_header("Content-Type", content_type)
if api_key:
req.add_header("Authorization", f"Bearer {api_key}")
try:
resp = urlopen(req, timeout=300)
except HTTPError as e:
body_text = e.read().decode('utf-8', errors='replace')
print(f"[error] HTTP {e.code}: {body_text}", file=sys.stderr)
sys.exit(1)
except URLError as e:
print(f"[error] Cannot reach server: {e.reason}", file=sys.stderr)
sys.exit(1)
result = None
event_count = 0
for event_type, data in parse_sse_events(resp):
event_count += 1
print(f"[debug] Event #{event_count}: {event_type}\n", flush=True)
if event_type == "progress":
stage = data.get("stage", "")
pct = data.get("percentage", 0)
msg = data.get("message", "")
bar = "=" * int(pct / 5) + ">" + " " * (20 - int(pct / 5))
print(f"\r[{bar}] {pct:5.1f}% {msg}", end="", flush=True)
elif event_type == "success":
print() # newline after progress bar
result = data
metadata = data.get("metadata", {})
exec_time = data.get("execution_time", 0)
print()
print(f"[success] File parsed!")
print(f" File ID: {data.get('file_id', '?')}")
print(f" File NAME: {metadata.get('file_name', '')}")
print(f" File URL: {metadata.get('file_url', '')}")
print(f" Title: {metadata.get('parsed_title', '(none)')}")
print(f" Summary: {metadata.get('summary', '(none)')}")
elif event_type == "error":
print()
code = data.get("code", "UNKNOWN")
msg = data.get("message", "Unknown error")
print(f"\n[error] {code}: {msg}", file=sys.stderr)
sys.exit(1)
if result is None:
print("[debug] No success/error event received — stream may have been empty or malformed", file=sys.stderr)
return result
def main():
parser = argparse.ArgumentParser(description="Parse a reference file via Skywork Office API")
parser.add_argument("file", help="Path to the file to parse")
parser.add_argument("--output", "-o", default=None, help="Save parsed content to this file")
parser.add_argument("--json", action="store_true", help="Output full result as JSON")
args = parser.parse_args()
if not os.path.isfile(args.file):
print(f"[error] File not found: {args.file}", file=sys.stderr)
sys.exit(1)
base_url = SKYWORK_GATEWAY_URL
api_key = get_skywork_api_key()
if not api_key:
print("[error] SKYWORK_API_KEY is required", file=sys.stderr)
sys.exit(1)
result = parse_file(base_url, api_key, args.file)
if result:
metadata = result.get("metadata", {})
parsed_content = metadata.get("parsed_content", "")
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
f.write(parsed_content)
print(f"\n[saved] Parsed content written to: {args.output}")
elif args.json:
print()
print(json.dumps(result, indent=2, ensure_ascii=False))
else:
# Print a preview of the parsed content
if parsed_content:
preview = parsed_content[:1000]
if len(parsed_content) > 1000:
preview += f"\n... ({len(parsed_content) - 1000:,} more chars)"
# Machine-readable summary for downstream parsing
file_name = metadata.get("file_name") or os.path.basename(args.file)
file_url = metadata.get("file_url", "")
parsed_info = {
"file_id": result.get("file_id", ""),
"filename": file_name,
"file_type": metadata.get("file_type", ""),
"url": file_url,
}
print(f"PARSED_FILE: {json.dumps(parsed_info, ensure_ascii=False)}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Call the ppt_write streaming API, output progress to stdout (for skill to relay in conversation), and save to disk via download_url upon completion.
Supports multiple environments: specify Base URL via environment variables or --env.
General usage (independent of skill installation path):
- Recommended: have the platform execute run_ppt_write.py from the skill root, e.g.: python <skill_root>/run_ppt_write.py "title" -o my.pptx
When the user types /ppt-write title, the platform resolves the skill root and runs the above command; no need for the user to specify the path.
- Or call this script directly: python <skill_root>/scripts/call_stream.py "title" -o my.pptx
"""
import argparse
import datetime
import json
import os
import sys
import urllib.request
import urllib.error
import uuid
from constant import SKYWORK_GATEWAY_URL, POD_TYPE
from skywork_auth import get_skywork_api_key
def get_base_url() -> str:
return SKYWORK_GATEWAY_URL
def write_log(log_file: str, line: str):
"""Append a plain-text line to the log file."""
try:
time_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_str = f"{time_str} {line.rstrip()}\n"
print(log_str, flush=True)
with open(log_file, "a", encoding="utf-8") as f:
f.write(log_str)
f.flush()
except Exception:
pass
def phase_to_message(phase: str, data: dict) -> str:
"""Convert phase to a message suitable for conversation output (consistent with SKILL progress output)"""
if phase == "outline":
return "Generating outline..."
if phase == "outline_page":
# Outline page-by-page streaming output: show page number and content (newlines collapsed to spaces for single-line console display)
page_num = data.get("page_num") or 0
content = (data.get("content") or "").strip()
if isinstance(content, str):
content = " ".join(content.split())
else:
content = str(content) if content else ""
if content:
line = f" Page {page_num}: {content[:300]}{'...' if len(content) > 300 else ''}"
return line
return f" Page {page_num}: (no body text)"
if phase == "outline_done":
return "Outline generated successfully!"
if phase == "slides":
return "Generating slides..."
if phase == "slides_page":
page_num = data.get("page_num") or 0
return f"Finish generating Page {page_num} "
if phase == "slides_page_start":
page_num = data.get("page_num") or 0
content = " ".join((data.get("content") or "").split())
snippet = content[:200] + ("..." if len(content) > 200 else "")
return f"Start generating page {page_num},content: \n{snippet}" if snippet else f"Start generating page {page_num}"
if phase == "slides_done":
return "Slides generated, exporting PPTX..."
if phase == "export":
status = data.get("status", "")
if status == "done":
return "Export complete."
return "Exporting PPTX... this step takes 2-5 minutes. KEEP reading this log every 5 seconds — do NOT stop polling until you see [DONE] or [ERROR]."
if phase == "done":
return "Generation complete, saving file..."
if phase == "ping":
progress = data.get("progress", "")
stage = data.get("stage", "")
return f"Now {progress}% was done, and is working on {stage}" if stage else f"{progress}%"
# --- Update/Edit PPT specific phases ---
if phase == "parse_pptx":
status = data.get("status", "")
if status == "start":
return "Parsing existing PPTX..."
if status == "done":
count = data.get("original_pptx_slide_count", "")
return f"PPTX parsed, {count} slides total."
return ""
if phase == "gen_update_plan":
status = data.get("status", "")
if status == "start":
return "Generating update plan..."
if status == "done":
update_count = data.get("update_pptx_slide_count", "")
new_count = data.get("new_slide_count", 0)
return f"Update plan ready: {update_count} slides to update, {new_count} new slides to generate."
return ""
if phase == "gen_new_slides":
status = data.get("status", "")
if status == "start":
return "Generating updated slides..."
if status == "done":
count = data.get("generated_new_slide_count", "")
return f"Updated slides generated ({count} slides)."
page_num = data.get("page_num")
if page_num is not None:
return f"Slide {page_num} updated."
return ""
if phase == "export_update_pptx":
status = data.get("status", "")
if status == "start":
return "Exporting updated PPTX..."
if status == "done":
count = data.get("generated_update_pptx_slide_count", "")
return f"Export complete, {count} slides."
return ""
# --- Template-based generation specific phases ---
if phase == "parse_template":
status = data.get("status", "")
if status == "start":
return "Parsing template..."
if status == "done":
slide_count = data.get("slide_count", "")
return f"Template parsing complete, {slide_count} pages in total."
return ""
if phase == "gen_outline":
status = data.get("status", "")
if status == "start":
return "Generating outline..."
if status == "done":
page_count = data.get("page_count", "")
return f"Outline generated, {page_count} pages in total."
return ""
if phase == "assign_templates":
status = data.get("status", "")
page_count = data.get("page_count", "")
if status == "start":
return "Assigning templates to each page..."
if status == "done":
return f"Template assignment complete, {page_count} pages in total."
return ""
if phase == "fill_content":
status = data.get("status", "")
page_count = data.get("page_count", "")
if status == "start":
return "Filling in content..."
if status == "done":
return f"Content filled, {page_count} pages in total."
return ""
if phase == "gen_images":
status = data.get("status", "")
if status == "start":
return "Generating images..."
if status == "done":
return "Image generation complete."
return ""
if phase == "slide_image":
page_num = data.get("page_num", "")
return f"Page {page_num} image generated."
if phase == "convert_html":
status = data.get("status", "")
if status == "start":
return "Converting HTML..."
if status == "done":
page_count = data.get("page_count", "")
return f"HTML conversion complete, {page_count} pages in total."
return ""
if phase:
return f"[{phase}]"
return "..."
def parse_sse_stream(resp):
"""Parse SSE stream, yield (event_type, data_dict).
The backend SendEventData wraps data as {"code":0,"message":"success","data": payload}; the data field needs to be extracted.
"""
cur_event = None
cur_data = None
for line in resp:
line = line.decode("utf-8", errors="replace").rstrip("\r\n")
if line == "":
if cur_event is not None and cur_data is not None:
try:
raw = json.loads(cur_data) if cur_data else {}
except json.JSONDecodeError:
raw = {}
# Compatible with backend wrapper format: {"code":0,"message":"success","data": {...}}; data may also be a JSON string
data = raw.get("data", raw) if isinstance(raw, dict) else raw
if isinstance(data, str):
try:
data = json.loads(data)
except json.JSONDecodeError:
data = {}
if not isinstance(data, dict):
data = {}
yield cur_event, data
cur_event = None
cur_data = None
continue
if line.startswith("event:"):
cur_event = line[6:].strip()
elif line.startswith("data:"):
cur_data = line[5:].strip()
if cur_event is not None and cur_data is not None:
try:
raw = json.loads(cur_data) if cur_data else {}
except json.JSONDecodeError:
raw = {}
data = raw.get("data", raw) if isinstance(raw, dict) else raw
if isinstance(data, str):
try:
data = json.loads(data)
except json.JSONDecodeError:
data = {}
if not isinstance(data, dict):
data = {}
yield cur_event, data
def main():
parser = argparse.ArgumentParser(description="Call ppt_write stream API and save .pptx")
parser.add_argument("query", nargs="?", default="", help="User query / PPT topic")
parser.add_argument("--language", default="en", help="Language used to generate slides. e.g. English, Chinese, etc.")
parser.add_argument("--reference", default="", help="Reference content for enriching slide content, e.g. web search results summary.")
parser.add_argument("--reference-file", default="", dest="reference_file", help="Path to a local file whose content will be used as reference (preferred over --reference to avoid shell encoding issues).")
parser.add_argument("-o", "--output", default="output.pptx", help="Output .pptx path")
parser.add_argument("--session_id", default=str(uuid.uuid4()), help="session_id used for distinct ppt generate query")
parser.add_argument("--template_urls", default="", help="Comma-separated list of template PPTX OSS URLs for style imitation")
parser.add_argument("--files", default="", help='Reference files JSON from upload_files.py UPLOADED_FILES output, format: [{"filename":"a.pdf","url":"https://..."}]')
parser.add_argument("--pptx-url", default="", dest="pptx_url", help="OSS URL of an existing PPTX to edit (triggers update/edit mode instead of generation).")
parser.add_argument("--log_path", default="", dest="log_path", help="A file path to save the progress log.")
args = parser.parse_args()
query = args.query.strip()
if not query:
parser.error("query is required")
return
base_url = get_base_url()
payload = {"query": query, "language": args.language}
session_id = args.session_id.replace('-', '_')
log_file = f"/tmp/ppt_run_{session_id}.log" if args.log_path == "" else args.log_path
open(log_file, "w").close() # clear previous run
print(f"[LOG-File]: {log_file}", flush=True)
# Determine endpoint and mode
if args.pptx_url:
url = f"{base_url}/update_pptx_process"
payload["pptx_url"] = args.pptx_url
mode = "editing"
elif args.template_urls:
url = f"{base_url}/chat_pptx_process"
payload["template_urls"] = [u.strip() for u in args.template_urls.split(",") if u.strip()]
mode = "imitation"
else:
url = f"{base_url}/ppt_write_stream"
mode = "generating"
print(f"[PID] {os.getpid()}", flush=True)
print(f"[START] mode={mode} session={session_id} \nIS about to take 5-10minutes, please wait and check the process log every 5 seconds!", flush=True)
write_log(log_file, f"[PID] {os.getpid()}")
write_log(log_file, f"[START] mode={mode} session={session_id} \nIS about to take 5-10minutes, please wait and check the process log every 5 seconds!")
reference = args.reference
if args.reference_file:
try:
with open(args.reference_file, "r", encoding="utf-8") as f:
reference = f.read()
except Exception as e:
print(f"[error] Failed to read reference file: {e}", file=sys.stderr)
sys.exit(1)
if reference:
payload["reference"] = reference
if args.files:
try:
payload["files"] = json.loads(args.files)
except json.JSONDecodeError as e:
print(f"--files JSON parse error: {e}", file=sys.stderr)
sys.exit(1)
payload["source_platform"] = "skyclaw" if POD_TYPE == "skyclaw" else ""
body = json.dumps(payload).encode("utf-8")
headers = {
"Content-Type": "application/json",
"Accept": "text/event-stream",
"Session-Id": session_id,
"Language": args.language,
}
api_key = get_skywork_api_key()
if not api_key:
print("[error] SKYWORK_API_KEY is required", file=sys.stderr)
sys.exit(1)
headers["Authorization"] = f"Bearer {api_key}"
req = urllib.request.Request(
url,
data=body,
method="POST",
headers=headers,
)
out_abs = os.path.abspath(args.output)
try:
with urllib.request.urlopen(req, timeout=600) as resp:
for event_type, data in parse_sse_stream(resp):
if not isinstance(data, dict):
data = {}
if event_type == "phase":
phase = data.get("phase", "")
msg = phase_to_message(phase, data)
if msg:
tag = "[PROGRESS]" if phase == "ping" else "[PHASE]"
write_log(log_file, f"{tag} {msg}")
if phase == "outline_done":
outline = data.get("outline", "")
if outline:
write_log(log_file, f"[OUTLINE]\n{outline}")
elif event_type == "completionEvent":
phase = data.get("phase", "")
if phase == "done":
download_url = data.get("download_url")
write_log(log_file, "[PHASE] Generation complete, saving file to local disk. This may take 1 or 2 minutes, we are already success!")
if not download_url:
write_log(log_file, "[ERROR] No download_url in completionEvent")
sys.exit(1)
try:
req2 = urllib.request.Request(download_url, method="GET")
with urllib.request.urlopen(req2, timeout=120) as r:
with open(out_abs, "wb") as f:
f.write(r.read())
write_log(log_file, f"[DONE] saved={out_abs} download_url={download_url}")
except Exception as e:
write_log(log_file, f"[ERROR] Download failed: {e}")
sys.exit(1)
elif event_type == "error":
err_msg = data.get("message", str(data))
write_log(log_file, f"[ERROR] {err_msg}")
sys.exit(1)
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
write_log(log_file, f"[ERROR] HTTP {e.code}: {body}")
sys.exit(1)
except Exception as e:
write_log(log_file, f"[ERROR] {e}")
sys.exit(1)
if __name__ == "__main__":
main()
import os
from typing import Optional
def get_skywork_api_key() -> Optional[str]:
"""
Returns skywork api key.
"""
api_key = os.environ.get("SKYWORK_API_KEY", "")
if not api_key:
print("SKYWORK_API_KEY is not set.")
return None
return api_key#!/usr/bin/env python3
"""
Upload local files to OSS by calling the /upload_oss endpoint.
Usage:
python upload_files.py file1.png file2.jpg
python upload_files.py file1.png --path skills/images/
"""
import argparse
import io
import json
import mimetypes
import os
import sys
import urllib.request
import urllib.error
import uuid
from pptx import api
from constant import SKYWORK_GATEWAY_URL
from skywork_auth import get_skywork_api_key
def build_multipart(fields: dict, files: dict) -> tuple[bytes, str]:
"""Build a multipart/form-data request body, returns (body_bytes, content_type).
fields: {"field_name": "value", ...}
files: {"field_name": (filename, file_bytes, mime_type), ...}
"""
boundary = uuid.uuid4().hex
parts = []
for name, value in fields.items():
parts.append(
f'--{boundary}\r\n'
f'Content-Disposition: form-data; name="{name}"\r\n\r\n'
f'{value}\r\n'
)
for name, (filename, data, mime) in files.items():
parts.append(
f'--{boundary}\r\n'
f'Content-Disposition: form-data; name="{name}"; filename="{filename}"\r\n'
f'Content-Type: {mime}\r\n\r\n'
)
if isinstance(data, str):
data = data.encode()
parts_bytes = b''.join(
p.encode() if isinstance(p, str) else p for p in parts
) + data + b'\r\n'
parts = [parts_bytes]
body = b''.join(p.encode() if isinstance(p, str) else p for p in parts)
body += f'--{boundary}--\r\n'.encode()
content_type = f'multipart/form-data; boundary={boundary}'
return body, content_type
def upload_file(local_path: str, oss_prefix: str | None, base_url: str, api_key: str = None) -> str:
"""Upload a single local file and return the OSS URL. Raises an exception on failure."""
if not os.path.isfile(local_path):
raise FileNotFoundError(f"File not found: {local_path}")
filename = os.path.basename(local_path)
mime = mimetypes.guess_type(filename)[0] or "application/octet-stream"
with open(local_path, "rb") as f:
file_bytes = f.read()
fields = {}
if oss_prefix:
fields["path"] = oss_prefix
body, content_type = build_multipart(
fields=fields,
files={"file": (filename, file_bytes, mime)},
)
url = base_url.rstrip("/") + "/upload_oss"
headers = {
"Content-Type": content_type,
}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
req = urllib.request.Request(
url,
data=body,
method="POST",
headers=headers,
)
with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read().decode("utf-8", errors="replace")
result = json.loads(raw)
code = result.get("code")
if code != 200:
raise RuntimeError(f"Upload failed code={code} msg={result.get('msg')}")
return result["url"]
def upload_files(local_paths: list[str], oss_prefix: str | None = None, base_url: str = SKYWORK_GATEWAY_URL, api_key: str = None) -> list[dict]:
"""Upload a batch of local files and return a list of results for each file.
Return format:
[{"path": "/local/file.png", "url": "https://cdn.../xxx.png", "ok": True}, ...]
On failure, url is empty, ok is False, and an error field is included.
"""
results = []
for path in local_paths:
try:
url = upload_file(path, oss_prefix, base_url, api_key)
filename = os.path.basename(path)
print(f"[OK] {path} -> {url}", flush=True)
results.append({"path": path, "filename": filename, "url": url, "ok": True})
except Exception as e:
print(f"[FAIL] {path}: {e}", file=sys.stderr, flush=True)
results.append({"path": path, "filename": os.path.basename(path), "url": "", "ok": False, "error": str(e)})
# Print machine-readable summary for downstream parsing
uploaded = [{"filename": r["filename"], "url": r["url"]} for r in results if r["ok"]]
if uploaded:
print(f"UPLOADED_FILES: {json.dumps(uploaded, ensure_ascii=False)}", flush=True)
return results
def main():
parser = argparse.ArgumentParser(description="Batch upload local files to OSS")
parser.add_argument("files", nargs="+", help="List of local file paths")
args = parser.parse_args()
api_key = get_skywork_api_key()
if not api_key:
print("[error] SKYWORK_API_KEY is required", file=sys.stderr)
sys.exit(1)
results = upload_files(args.files, oss_prefix='', api_key=api_key)
failed = [r for r in results if not r["ok"]]
if failed:
print(f"\n{len(failed)}/{len(results)} file(s) failed to upload", file=sys.stderr)
sys.exit(1)
else:
print(f"\nAll {len(results)} file(s) uploaded successfully")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
web_search.py - Call local web search API and print results.
Usage:
python web_search.py "query1" ["query2" ...]
"""
import argparse
import json
import os
import sys
import tempfile
import urllib.request
from constant import SKYWORK_GATEWAY_URL, POD_TYPE
from skywork_auth import get_skywork_api_key
def search(query: str, api_key: str) -> str:
"""Call web_search API and return formatted text of results."""
url = f"{SKYWORK_GATEWAY_URL}/web_search"
payload = {"query": query, "source_platform": "skyclaw" if POD_TYPE == "skyclaw" else ""}
body = json.dumps(payload).encode("utf-8")
headers = {"Content-Type": "application/json"}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
req = urllib.request.Request(
url,
data=body,
method="POST",
headers=headers,
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
raw = resp.read().decode("utf-8")
except urllib.error.URLError as e:
print(f"[ERROR] Error call web_search, try only 1 time more: {e}", file=sys.stderr)
sys.exit(1)
try:
data = json.loads(raw)
except json.JSONDecodeError:
return raw # fallback: return raw if not JSON
results = data.get("search_res", [])
if not results:
return "(no results)"
lines = []
for i, item in enumerate(results, 1):
content = (item.get("content") or "").strip()
source_url = item.get("url", "")
lines.append(f"[result-{i}] {source_url}\n{content}")
return "\n\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Call local web_search API")
parser.add_argument("queries", nargs="+", help="One or more search queries (max 3)")
args = parser.parse_args()
queries = args.queries[:3]
out_dir = tempfile.mkdtemp(prefix="web_search_")
api_key = get_skywork_api_key()
if not api_key:
print("[error] SKYWORK_API_KEY is required", file=sys.stderr)
sys.exit(1)
for i, q in enumerate(queries, 1):
print(f"[query] {q} ...", file=sys.stderr, flush=True)
raw = search(q, api_key)
out_path = os.path.join(out_dir, f"{q}_result.txt")
with open(out_path, "w", encoding="utf-8") as f:
f.write(f"query: {q}\n\n{raw}")
print(f'Already saved search result for query[{q}], \nout_path: {out_path}', flush=True)
if __name__ == "__main__":
main()
PPT Edit Workflow
Edit an existing PPT via natural language: modify individual slides, add new slides, change the overall style, split or merge slides.
Follow these steps in order: get pptx_url → confirm intent → run script (background) → read log every 5s and report progress (required) → deliver. Never go silent — always report status as you poll the log.
---
1. Get the PPTX URL
The user must supply a publicly accessible OSS/CDN URL of the existing PPTX to edit.
- If the user provides a local file path instead of a URL, upload it first:
$PYTHON_CMD scripts/upload_files.py "/absolute/path/to/file.pptx"Extract the URL from the [OK] ... -> https://... output line.
- If the URL is already provided (e.g. from a previous generation's
Download URL:output), use it directly.
---
2. Confirm edit intent
Understand what the user wants to change. Supported operations include:
| Operation | Example user intent |
|---|---|
| Modify a slide | "change page 4 background to black, text to white" |
| Add a new slide | "insert a new slide after page 3 about X" |
| Change overall style | "make all slides use a dark theme" |
| Split a slide | "split page 5 into two slides" |
| Merge slides | "merge pages 2 and 3 into one slide" |
Pass the user's original instruction verbatim as query. Do not paraphrase — the backend interprets it directly.
---
3. Run the script
Run in the background, then read the progress log file every 5 seconds until done.
3a. Choose a log path and start in background
PPT_LOG=/tmp/ppt_$(date +%s).log
$PYTHON_CMD scripts/run_ppt_write.py "user's edit instruction" \
--language Chinese \
--pptx-url "https://cdn.example.com/path/to/file.pptx" \
--log_path "$PPT_LOG" \
-o /absolute/path/to/output.pptx \
> /dev/null 2>&1 &
echo "Log: $PPT_LOG"- `--log_path` (required): Pass the pre-chosen path. The script writes all progress here.
- `--pptx-url` (required): OSS URL of the PPTX to edit. This triggers edit mode.
- `--language` (required): Detect from user input —
ChineseorEnglish. - `-o` (required): Absolute output path for the edited PPTX.
3b. Monitor progress (REQUIRED)
STRICT RULES — no exceptions:
1. Read the log exactly every 5 seconds. Do NOT extend the interval, do NOT skip reads.
2. Before every read, check if the process is still alive. If alive → only read log, NEVER restart.
Every 5 seconds, run this exact sequence:
# Step 1: extract PID from log
PPT_PID=$(grep '^\[PID\]' "$PPT_LOG" | tail -1 | awk '{print $2}')
# Step 2: check if process is alive
kill -0 "$PPT_PID" 2>/dev/null && PPT_ALIVE=true || PPT_ALIVE=false
# Step 3: read the log regardless
tail -20 "$PPT_LOG"- If process is RUNNING → report status to user, wait 5s, repeat. Do NOT touch the script.
- If process is not running AND log ends with
[DONE]or[ERROR]→ stop polling, proceed to deliver/handle error. - If process is not running AND no
[DONE]/[ERROR]in log → the script crashed; report error to user, ask whether to retry. Both conditions must hold: PID gone AND no terminal log line. NEVER retry without explicit user confirmation.
Each line is plain text:
[PID] <pid>— process ID written at startup[START]— job started[PHASE] <message>— in progress[DONE] saved=<path> download_url=<url>— finished[ERROR] <message>— failed
After each read, report status to the user:
[Main stage] | [current action]
Example: Generating slides | Working on page 3 Progress phases:
| Message contains | Main stage |
|---|---|
| "Parsing existing PPTX" | Parsing original PPTX |
| "update plan" | Planning updates |
| "updated slides" / "Slide N updated" | Generating updated slides |
| "Exporting" / "Export complete" | Exporting PPTX |
Stop polling as soon as you see [DONE] or [ERROR].
---
4. Deliver
Provide all of the following: 1. Download link (download_url from the completion event) 2. Local .pptx absolute path 3. Brief summary of what was changed (which pages, what modifications)
Failure
Briefly explain the error.
PPT Generate Workflow
Follow these steps in order: confirm inputs → run script (streaming) → output progress at every phase (required) → save file → deliver. Never go silent for an extended period — always stream progress as it arrives.
---
1. Parse reference files (optional — only if the user provides local files)
If the user provides local files (PDF, DOCX, PPTX, images, etc.) as content source for the PPT, parse each file with the document parse service. This uploads the file, extracts its content, and returns metadata (file_id, file_url).
Run parse_file.py once per filet, his may cost a few minutes to finish completely::
$PYTHON_CMD scripts/parse_file.py /path/to/file1.pdf
$PYTHON_CMD scripts/parse_file.py /path/to/file2.docx- exec tool `yieldMs`: Must be set to
600000(10 minutes) when invoking the exec tool.
Each call prints a PARSED_FILE: line on success:
PARSED_FILE: {"filename": "file1.pdf", "url": "https://...", "file_id": "123456789"}Collect every PARSED_FILE: result and assemble them into a JSON array as DOC_FILES_JSON:
[
{"filename":"file1.pdf","url":"https://...","file_id":"123456789","file_type":"pdf"},
{"filename":"file2.docx","url":"https://...","file_id":"987654321","file_type":"docx"}
]Pass this array to --files in Step 3. If no local files are provided, skip this step entirely.
---
2. Web search (required if no relevant content is already in the conversation)
Skip this step if the user has already provided sufficient reference material or reference files. Otherwise, run up to 3 targeted searches on the topic, make sure your query contains the date if you need to search the latest information:
Run searches
$PYTHON_CMD scripts/web_search.py "query1" "query2" "query3"- Break the topic into 1–3 focused queries (e.g. for "Beijing travel guide": "Beijing top attractions", "Beijing food recommendations", "Beijing transportation tips").
- If the topic involves current events, trends, statistics, or anything time-sensitive, append the current year (e.g.
2026) or a date range to each query to ensure results are up to date. Example:"AI agent market trends 2026"instead of"AI agent market trends". - Deduplicate and distill all results into a
referencetext to enrich the PPT content.
Synthesize search results
If reference files were parsed in Step 1, use their parsed_content as the primary source and supplement with search results. The parsed content contains the full document text — extract and preserve all detail, do not compress it into a short summary.
Produce a detailed reference report. Target 800–2000 words — do not stop early. Write as much specific detail as the sources provide:
## Key Findings
3–5 sentences covering the main conclusions, background, and why it matters.
## Key Data & Facts
- List every specific number, percentage, date, and metric found. Do not skip data.
- Format: "Exact figure — what it measures (source)"
- Aim for at least 5 data points if the source contains them.
## Topic Breakdown
Organize by sub-topic. For each sub-topic write 3–5 specific bullet points.
Do not use vague phrases like "it is important" — state the actual fact.
Preserve names of people, companies, products, places, and dates exactly as found.
## Cases & Examples (if available)
Concrete examples, case studies, or use cases with specific details and outcomes.
## Summary
3–5 bullet points capturing the core message the PPT must convey.Hard constraints:
- Only include information that explicitly appears in the sources. Do not invent or infer.
- Numbers, percentages, and proper nouns must be quoted exactly — do not paraphrase or round.
- Never produce fewer than 500 words unless the source material is genuinely sparse.
- If a section has no relevant information, omit that section entirely.
- Note the publication date of each source. Always include the current date context in the summary where relevant.
After writing the reference report, save it to a local temp file to avoid shell encoding issues:
cat > /tmp/ppt_reference.md << 'REFERENCE_EOF'
<paste the full reference report here>
REFERENCE_EOFUse this file path as --reference-file in Step 3.
---
2. Confirm inputs
- query (required): Faithfully reproduce the user's full request — include every instruction the user mentioned. Cover the following if the user specified them; do not add anything they didn't mention:
- Slide count (if the user set a limit, it must be included — never drop it)
- Topic / content direction
- Style preference (e.g. business, academic, playful, minimalist)
- Audience / purpose (e.g. "exec briefing for the CXO", "science explainer for kids")
- Structure preference (e.g. "data-heavy", "one case study per slide")
- Other constraints (tone, things to avoid, required talking points, etc.)
Rule: Do not fabricate requirements the user never mentioned, and do not omit anything they explicitly said. If the user gives only a title, the query is exactly that title — don't add qualifiers.
Examples:
| User input | Correct query |
|---|---|
| Make me a PPT about Beijing travel | Beijing Travel Guide PPT |
| 8-page English slides on AI Agent, clean business style, targeting investors | 8-page English slides on AI Agent, clean business style, targeting investors |
| PPT for our Q1 sales summary, 10 slides, with data charts, formal style | Q1 Sales Summary PPT, 10 slides, data charts, formal business style |
| A fun solar system PPT for elementary school kids, 6 pages | Solar System explainer PPT for elementary school kids, fun and engaging style, 6 slides |
- language (required): Detect from the user's input language — if the user writes in Chinese pass
Chinese; English →English; etc. Never default to a fixed value. - --reference-file (optional): Pass the path to the temp file written at the end of Step 2 (e.g.
/tmp/ppt_reference.md). Omit if no search was performed. - --files (optional): Pass the JSON array assembled from the
PARSED_FILE:lines in Step 1. Omit if no files were provided. Format:[{"file_name":"...","url":"...","file_id":"...", "file_type": "pdf"},...]
---
3. Run the script
PPT generation takes ~10 minutes. Choose a log path first, pass it to the script, then read it every 5 seconds.
3a. Choose a log path and start in background
PPT_LOG=/tmp/ppt_$(date +%s).log
$PYTHON_CMD scripts/run_ppt_write.py "user query" \
--language English \
--reference-file /tmp/ppt_reference.md \
--files '[{"file_name":"report.pdf","url":"https://...", "file_id":"1212121", "file_type":"pdf"}]' \
--log_path "$PPT_LOG" \
-o /absolute/path/to/output.pptx \
> /dev/null 2>&1 &
echo "Log: $PPT_LOG"- `--log_path` (required): Pass the pre-chosen path. The script writes all progress here.
- `-o` (required): Use an absolute path.
- `--files`: JSON array from
DOC_FILES_JSON:lines verbatim. Omit if no reference files.
3b. Monitor progress (REQUIRED)
STRICT RULES — no exceptions:
1. Read the log exactly every 5 seconds. Do NOT extend the interval, do NOT skip reads.
2. Before every read, check if the process is still alive. If alive → only read log, NEVER restart.
Every 5 seconds, run this exact sequence:
# Step 1: extract PID from log
PPT_PID=$(grep '^\[PID\]' "$PPT_LOG" | tail -1 | awk '{print $2}')
# Step 2: check if process is alive
kill -0 "$PPT_PID" 2>/dev/null && PPT_ALIVE=true || PPT_ALIVE=false
# Step 3: read the log regardless
tail -20 "$PPT_LOG"- If process is RUNNING → report status to user, wait 5s, repeat. Do NOT touch the script.
- If process is not running AND log ends with
[DONE]or[ERROR]→ stop polling, proceed to deliver/handle error. - If process is not running AND no
[DONE]/[ERROR]in log → the script crashed; report error to user, ask whether to retry. Both conditions must hold: PID gone AND no terminal log line. NEVER retry without explicit user confirmation.
Each line is plain text:
[PID] <pid>— process ID written at startup[START]— job started[PHASE] <message>— stage transition (outline, slides, export, etc.)[PING] <progress>% | <stage>— heartbeat every few seconds with current progress and stage label[OUTLINE]— outline content (appears once after outline_done)[DONE] saved=<path> download_url=<url>— finished[ERROR] <message>— failed
After each read, report status to the user:
[Main stage] | [current action]
Example: Generating slides | Working on page 3 Map phase messages to main stages:
| Message contains | Main stage |
|---|---|
| "outline" | Generating outline |
| "slide" / "page" / "content" | Generating slides |
| "image" / "HTML" | Rendering images |
| "Export" / "export" | Exporting PPTX |
| "Parsing existing PPTX" | Parsing original PPTX |
| "update plan" | Planning updates |
| "template" | Processing template |
Stop polling as soon as you see [DONE] or [ERROR].
---
4. Deliver
Success
Provide all of the following: 1. Download link 2. Local .pptx absolute path 3. Presentation title 4. Brief description of each slide's content
Failure
Briefly explain the error.
Technical Notes
- Timeout: PPT generation takes ~10 minutes, set sufficient timeout
PPT Imitation Workflow
The user provides an existing PPTX file as a layout/style reference. Generate a new presentation on a different topic that follows the same visual structure.
Follow these steps in order: identify intent → locate template → web search → upload template → run script (streaming) → output progress at every phase (required) → save file → deliver. Never go silent for an extended period — always stream progress as it arrives.
---
0. Confirm imitation intent
Use this workflow (instead of the standard generate flow) when both of the following are true:
- The user mentions a template, style reference, or an existing file — e.g. "use this template", "match the style of this PPT", "imitate this layout" — or provides a
.pptxfile path or filename. - The user has a new generation request (topic, content, slide count, etc.).
If the user provides a template but no new topic, ask what they want to create before proceeding.
---
1. Locate the local template file
Extract the local PPTX path from the user's message. It may appear as:
- An absolute path:
/Users/xxx/templates/style.pptx - A relative path or filename:
my_template.pptx(infer the full path from context) - An attachment uploaded in the conversation (the platform will provide a local temp path)
If the path is ambiguous, ask the user for the full absolute path.
---
2. Parse reference files (optional — only if the user provides local files as content source)
If the user provides additional local files (PDF, DOCX, PPTX, images, etc.) to use as content source — separate from the template — parse each file with the document parse service.
Run parse_file.py once per file, this may cost a few minutes to finish completely:
$PYTHON_CMD scripts/parse_file.py /path/to/file1.pdf
$PYTHON_CMD scripts/parse_file.py /path/to/file2.docx- exec tool `yieldMs`: Must be set to
600000(10 minutes) when invoking the exec tool.
Each call prints a PARSED_FILE: line on success:
PARSED_FILE: {"filename": "file1.pdf", "url": "https://...", "file_id": "123456789"}Collect every PARSED_FILE: result and assemble them into a JSON array as DOC_FILES_JSON:
[
{"filename":"file1.pdf","url":"https://...","file_id":"123456789","file_type":"pdf"},
{"filename":"file2.docx","url":"https://...","file_id":"987654321","file_type":"docx"}
]Pass this array to --files in Step 6. If no additional content files are provided, skip this step entirely.
---
3. Web search (required if no relevant content is already in the conversation)
Skip if the user has already provided sufficient reference material or reference files. Otherwise, run up to 3 targeted searches on the new PPT topic:
$PYTHON_CMD scripts/web_search.py "query1" "query2" "query3"Produce a detailed reference report following the same format and constraints as the generate workflow (target 800–2000 words, preserve all specific details, never fewer than 500 words).
After writing the reference report, save it to a local temp file:
cat > /tmp/ppt_reference.md << 'REFERENCE_EOF'
<paste the full reference report here>
REFERENCE_EOFUse this file path as --reference-file in Step 6.
---
4. Upload the template to OSS
$PYTHON_CMD scripts/upload_files.py "/absolute/path/to/template.pptx"- Script output format:
[OK] /path/to/file.pptx -> https://cdn.xxx/skills/upload/yyyy-mm-dd/uuid_filename.pptx - Extract the OSS URL from the output and call it
TEMPLATE_URL. - If the upload fails, inform the user and stop.
---
5. Confirm generation parameters
- query (required): Faithfully reproduce the user's full request — include every instruction (slide count, style, audience, etc.). Do not fabricate or omit anything.
- language (required): Language for the slides, e.g.
Chinese,English. - language (required): Detect from the user's input language — if the user writes in Chinese pass
Chinese; English →English; etc. Never default to a fixed value. - template_urls (required): The OSS URL(s) from Step 4. Multiple URLs are supported (comma-separated) — the backend selects the best-matching layout from each. One URL is sufficient in most cases; pass multiple only if the user provided several template files.
- --reference-file (optional): Path to the temp file written at the end of Step 3 (e.g.
/tmp/ppt_reference.md). Omit if no search was performed. - --files (optional): Pass the JSON array assembled from the
PARSED_FILE:lines in Step 2. Omit if no content files were provided. Format:[{"filename":"...","url":"...","file_id":"...", "file_type": "pdf"},...]
---
6. Run the script
PPT generation takes ~10 minutes. Run it in the background, then read the progress log file every 5 seconds until done.
6a. Choose a log path and start in background
PPT_LOG=/tmp/ppt_$(date +%s).log
$PYTHON_CMD scripts/run_ppt_write.py "user query" \
--language Chinese \
--template_urls "TEMPLATE_URL1,TEMPLATE_URL2" \
--reference-file /tmp/ppt_reference.md \
--files '[{"file_name":"report.pdf","url":"https://...", "file_id":"1212121", "file_type":"pdf"}]' \
--log_path "$PPT_LOG" \
-o /absolute/path/to/output.pptx \
> /dev/null 2>&1 &
echo "Log: $PPT_LOG"- `--log_path` (required): Pass the pre-chosen path. The script writes all progress here.
- `-o` (required): Use an absolute path for reliable delivery.
- `--template_urls`: Comma-separated OSS URLs of template files from Step 4.
- `--files`: Pass the JSON array assembled from
PARSED_FILE:lines verbatim. Omit if no reference files.
6b. Monitor progress (REQUIRED)
STRICT RULES — no exceptions:
1. Read the log exactly every 5 seconds. Do NOT extend the interval, do NOT skip reads.
2. Before every read, check if the process is still alive. If alive → only read log, NEVER restart.
Every 5 seconds, run this exact sequence:
# Step 1: extract PID from log
PPT_PID=$(grep '^\[PID\]' "$PPT_LOG" | tail -1 | awk '{print $2}')
# Step 2: check if process is alive
kill -0 "$PPT_PID" 2>/dev/null && PPT_ALIVE=true || PPT_ALIVE=false
# Step 3: read the log regardless
tail -20 "$PPT_LOG"- If process is RUNNING → report status to user, wait 5s, repeat. Do NOT touch the script.
- If process is not running AND log ends with
[DONE]or[ERROR]→ stop polling, proceed to deliver/handle error. - If process is not running AND no
[DONE]/[ERROR]in log → the script crashed; report error to user, ask whether to retry. Both conditions must hold: PID gone AND no terminal log line. NEVER retry without explicit user confirmation.
Each line is plain text:
[PID] <pid>— process ID written at startup[START]— job started[PHASE] <message>— in progress[DONE] saved=<path> download_url=<url>— finished[ERROR] <message>— failed
After each read, report status to the user:
[Main stage] | [current action]
Example: Generating slides | Working on page 3 Stop polling as soon as you see [DONE] or [ERROR].
7. Deliver
Success
Provide all of the following: 1. Download link 2. Local .pptx absolute path 3. Presentation title 4. Brief description of each slide's content 5. Template filename used (so the user can verify)
Failure
Briefly explain the error. ---
Appendix — Intent classification examples
| User input | Use this workflow? |
|---|---|
| Make me a PPT about Beijing travel | ❌ Standard generate |
Use this template to make a PPT on AI trends — template at /tmp/style.pptx | ✅ Imitate |
| Imitate the uploaded pptx style, 10-slide quarterly review | ✅ Imitate |
| What style is this template? | ❌ Not a generation request — just answer the question |
Local PPTX Operations Workflow (Layer 3)
No backend or token needed — uses python-pptx to operate on local .pptx files directly.
---
Install Dependencies
pip install python-pptx---
Trigger Keywords
When the user expresses the following intents, use this workflow (instead of calling the backend API):
| User Intent | Example Keywords |
|---|---|
| View file info | View PPT page count, how many pages, pptx info |
| Delete specific slides | Delete slide N, remove slide 3, delete slide 3, remove page 5 |
| Reorder slides | Reorder PPT, rearrange slide order, reorder slides |
| Extract slides | Extract slides 1-3, export certain slides, extract slides |
| Merge files | Merge two PPTs, merge pptx, combine a and b |
---
Unified Entry Point
All operations are done via scripts/local_pptx_ops.py:
$PYTHON_CMD scripts/local_pptx_ops.py <subcommand> [arguments]---
Subcommand Details
info — View file info
$PYTHON_CMD scripts/local_pptx_ops.py info --file my.pptxOutput: file path, total slides, dimensions, title of each slide.
---
delete — Delete slides
# Delete slide 3
$PYTHON_CMD scripts/local_pptx_ops.py delete --file my.pptx --slides 3
# Delete slides 3, 5, 7-9, overwrite the original file
$PYTHON_CMD scripts/local_pptx_ops.py delete --file my.pptx --slides 3,5,7-9
# Save to a new file after deletion (don't overwrite the original)
$PYTHON_CMD scripts/local_pptx_ops.py delete --file my.pptx --slides 3,5,7-9 -o trimmed.pptxNote:
- Page numbers are 1-based (slide 1 = 1)
- Supports comma-separated and
-ranges, e.g.1,3,5-8,10 - If
-ois not specified, the original file is overwritten
---
reorder — Reorder slide order
# Move original slide 2 to position 1, slide 1 to position 2 (rest unchanged)
$PYTHON_CMD scripts/local_pptx_ops.py reorder --file my.pptx --order 2,1,3,4,5
# Save to a new file
$PYTHON_CMD scripts/local_pptx_ops.py reorder --file my.pptx --order 2,1,3,4,5 -o reordered.pptxNote: --order must include all page numbers — no omissions or duplicates.
---
extract — Extract specific slides
# Extract slides 1 through 3
$PYTHON_CMD scripts/local_pptx_ops.py extract --file my.pptx --slides 1-3 -o subset.pptx
# Extract slides 1, 3, 5
$PYTHON_CMD scripts/local_pptx_ops.py extract --file my.pptx --slides 1,3,5 -o subset.pptxExtracted result is saved as a new file (original file is not modified).
---
merge — Merge multiple files
# Merge two files
$PYTHON_CMD scripts/local_pptx_ops.py merge --files a.pptx b.pptx -o merged.pptx
# Merge three or more
$PYTHON_CMD scripts/local_pptx_ops.py merge --files a.pptx b.pptx c.pptx -o merged.pptxNote: The slide dimensions of the first file are used as the standard; shapes are preserved as much as possible during merging, but complex animations/video effects may be lost.
---
Agent Decision Logic
User intent → Route selection:
User says "Generate a PPT about X"
→ Layer 1: scripts/run_ppt_write.py (call backend to generate)
User says "Delete slide 3 from this PPT" / "Merge these two pptx" / "Extract the first 5 slides"
→ Layer 3: scripts/local_pptx_ops.py (local operation, no token needed)---
FAQ
Q: Do page numbers change after deletion? A: Yes. After deletion, page numbers are renumbered starting from 1. If you need to delete multiple slides, it's recommended to pass them all at once (--slides 3,5,7) rather than running multiple times.
Q: What if styles are wrong after merging? A: Merging uses deep-copy of the shape tree; themes/masters come from the first file. If there are significant style differences, manual adjustments in PowerPoint are recommended.
Q: Does python-pptx support deleting slides? A: Not natively. This tool operates on the underlying XML (_sldIdLst), consistent with common python-pptx community approaches.
Related skills
How it compares
Pick skywork-ppt for native .pptx output; use frontend-slides when you need HTML/CSS web presentations with custom motion design.
FAQ
What can skywork-ppt do with existing files?
skywork-ppt imitates .pptx templates and styles, then edits them—modify slide N, change backgrounds, add slides, or beautify layouts. Users reference the file and describe changes in natural language.
Which languages trigger skywork-ppt?
skywork-ppt listens for English, Chinese, Japanese, and Korean prompts such as generate a PPT, 帮我做个PPT, PPTを作って, and 슬라이드 만들어줘. The same creation and edit flows work across those locales.
Is Skywork Ppt safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.