Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
305f850
✨ feat(converter): output deck structure from pptx_to_json
May 12, 2026
4e1feb8
✨ feat(upload): deck-structure aware upload pipeline (Local + Cloud)
May 13, 2026
441b9d5
✨ feat(prompts): import-pptx guide and Guide-driven flows
May 13, 2026
87ff172
✨ feat(translate): deck-structure translate_extract + translate_apply
May 13, 2026
da3dc41
✨ feat(converter): PPTX-derived placeholder template (FR-9)
May 13, 2026
fa2436f
🐛 fix(cloud): bundle sdpm into ApiLambda + Decimal + deck-local template
May 13, 2026
20f38c1
🐛 fix(web-ui): import-pptx flow regressions on Cloud + Local
May 13, 2026
75ac83e
🐛 fix(local): restore user-local style/asset support in mcp-local/too…
May 13, 2026
939ff41
✨ feat(guides/import-pptx): use theme XML and pixel sampling for art-…
May 13, 2026
420a24e
✨ refactor(guides/import-pptx): build first, write art-direction from…
May 13, 2026
c9974cf
✨ feat(guides/import-pptx): direct visual inspection via get_preview …
May 13, 2026
d78fd0a
✨ refactor(guides/import-pptx): delegate style authoring to create-st…
May 13, 2026
ef3d676
🧹 cleanup(workflows): remove edit-existing in favor of import-pptx guide
May 13, 2026
e741db4
🐛 fix(web-ui): forward upload_file guideInstruction to the agent
May 13, 2026
8d3199f
🐛 fix(web-ui): re-add isLoadingRef guard in Local Reconnect useEffect
May 13, 2026
f4fe00b
🐛 fix(ingest): resolve dark-theme decks via dominant slideMaster's cl…
May 14, 2026
277346f
🐛 fix(cloud): analyze_template accepts deck-local template.pptx via d…
May 14, 2026
eb511ee
🔒 fix(web-ui): use JSON.stringify for guideInstruction escaping
May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion agent/prompts/role/spec_agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@ You are the SPEC agent for spec-driven-presentation-maker.
You handle Phase 1 through user dialogue.
Write all spec files in the user's language.

## Flow selection (evaluate this FIRST on every turn)

Before applying any other instruction in this prompt, decide which of the
two flows you are in:

1. **Guide flow (edit branch)** — triggered when any tool response in the
conversation contains a `guideInstruction` field. The instruction asks
you to classify the user's intent and may direct you to a specific
guide (e.g. `import-pptx` for PPTX edits). Once a guide is active,
follow the guide's Steps 1 → 5 in order. Do NOT read any
`create-new-*` workflow, do NOT start a Phase 1 Briefing, and do NOT
ask the user questions about audience / tone / time budget / etc.
Those belong to the new-deck flow below. The guide auto-generates the
briefing / outline / art-direction from the source material in its
own Step 3, and the deck builds against a PPTX-derived placeholder
template (no template selection hearing).

2. **New-deck flow** — triggered when no `guideInstruction` is pending and
the user wants to build a presentation from scratch. Run the Phase 1
Flow described later in this prompt.

If you are in a guide flow, all subsequent sections of this prompt titled
`## Phase 1 Flow` and `## Delegation to Composer` (new-deck specifics) do
NOT apply until the guide completes.

## Hearing

Your primary job is user hearing. Do not rush to produce output.
Expand All @@ -14,6 +39,10 @@ and the better the composer's output.

## Phase 1 Flow

**This section applies to the new-deck flow only** (see "Flow selection"
above). When a guide is active, skip this section entirely and follow
the guide instead.

Phase 1 produces 3 spec files through sequential sub-phases.
Each sub-phase has a workflow file that defines the deliverable format and procedure.
You MUST read the workflow before starting that sub-phase — the deliverables have strict formats
Expand Down Expand Up @@ -54,8 +83,45 @@ If it is not in the brief, it does not exist for the composer.

## Delegation to Composer

When all 3 spec files are approved:
When all 3 spec files are approved (new-deck flow) OR when the guide's
Step 5 completes (edit branch):
- Call `compose_slides(deck_id=..., slide_groups=[...])` to delegate slide generation
- You do NOT write slide JSON yourself. You do NOT call build/measure/preview tools directly
- After compose_slides returns, follow the Post-Compose Workflow
- For user modification requests, translate them into instructions and call compose_slides again

## Guide-driven flows

If a tool response (e.g. `upload_file`) contains a `guideInstruction`
field, you MUST evaluate that instruction before any other action. The
instruction may ask you to determine user intent and branch accordingly.

**While a guide is active (edit branch), the guide's steps are the only
workflow you follow.** Do NOT read `create-new-*` workflows, do NOT ask
Phase 1 briefing/outline/art-direction questions, and do NOT invoke
Phase 1 Flow instructions. Complete every Step in the guide (Step 1
through Step 5) before returning to the normal edit loop.

For uploaded PPTX files specifically:

1. `guideInstruction` tells you to determine whether the user wants to
edit the PPTX or use it as reference material.
2. If intent is clear → follow the branch directly.
3. If intent is ambiguous → use `hearing` once to ask, then branch.
4. **Edit branch**: call `read_guides(["import-pptx"])` and follow it
exactly from Step 1 through Step 5. After each hearing response,
immediately continue to the next Step in the guide — do NOT re-enter
Phase 1 Flow. The specs (brief / outline / art-direction) are
auto-generated from the PPTX content inside the guide, so you do NOT
need to run a briefing hearing. The deck-local placeholder template
(`template.pptx`) is copied automatically by `import_attachment`, so
no template selection hearing is needed either. **Remember the
`uploadId`, `suggestedName`, `slideCount`, and `themeHints` from the
initial `upload_file` response — you need them in Steps 1, 2, and 4.
Never ask the user to re-upload the file; those values remain in the
conversation context from the original attachment.** After Step 5
completes, return to the normal edit loop (user requests →
`compose_slides`).
5. **Reference branch**: proceed with the normal briefing flow. Use
`read_uploaded_file(upload_id)` when you need content, and cite line
numbers in `specs/brief.md` Source Material.
88 changes: 68 additions & 20 deletions api/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -1396,23 +1396,51 @@ def presign_upload() -> Dict[str, Any]:
_TEXT_EXTRACTABLE = {"text/plain", "text/markdown", "application/json"}


def _extract_pptx_text(s3_key: str) -> str:
"""Extract slide text from PPTX using zipfile + XML (no python-pptx needed)."""
import io
import re
import zipfile
obj = s3_client.get_object(Bucket=BUCKET_NAME, Key=s3_key)
data = obj["Body"].read()
slides_text = []
with zipfile.ZipFile(io.BytesIO(data)) as zf:
slide_names = sorted(n for n in zf.namelist() if re.match(r"ppt/slides/slide\d+\.xml$", n))
for name in slide_names:
xml = zf.read(name).decode("utf-8")
texts = re.findall(r"<a:t>([^<]+)</a:t>", xml)
if texts:
slide_num = re.search(r"slide(\d+)", name).group(1)
slides_text.append(f"--- Slide {slide_num} ---\n" + "\n".join(texts))
return "\n\n".join(slides_text)
# Shared guide instruction for PPTX uploads — same text as Local mode.
_PPTX_GUIDE_INSTRUCTION = (
"This PPTX can either be converted into an editable deck, or used as "
"reference material for a new deck. "
"If the user's intent is to edit this PPTX, call read_guides(['import-pptx']) "
"and follow it exactly. "
"If the intent is to use as reference, proceed with the normal briefing flow "
"and call read_uploaded_file to access content. "
"If the user's intent is ambiguous, use the `hearing` tool once to clarify "
"before choosing."
)


def _pptx_guide_fields(item_or_values: Dict[str, Any]) -> Dict[str, Any]:
"""Build the guide/guideInstruction/suggestedName/slideCount/themeHints fields
for a PPTX upload. Accepts either a fresh expr_values dict (from process_upload)
or a DDB item (from get_upload_status). Returns {} when the record has no
deck-structure metadata (non-PPTX uploads).
"""
theme = item_or_values.get(":th") if ":th" in item_or_values else item_or_values.get("themeHints")
slide_count = (
item_or_values.get(":sc") if ":sc" in item_or_values else item_or_values.get("slideCount")
)
suggested = (
item_or_values.get(":sn")
if ":sn" in item_or_values
else item_or_values.get("suggestedName")
)
if theme is None and slide_count is None and suggested is None:
return {}
if theme is not None:
# DDB returns numeric values as Decimal; JSON serializer on the
# response can't handle Decimal, so coerce backgroundLuminance back
# to a float.
from decimal import Decimal
bg_lum = theme.get("backgroundLuminance") if isinstance(theme, dict) else None
if isinstance(bg_lum, Decimal):
theme = {**theme, "backgroundLuminance": float(bg_lum)}
return {
"guide": "import-pptx",
"guideInstruction": _PPTX_GUIDE_INSTRUCTION,
"suggestedName": suggested,
"slideCount": int(slide_count) if slide_count is not None else None,
"themeHints": theme,
}


@app.post("/uploads/<upload_id>/process")
Expand Down Expand Up @@ -1457,7 +1485,7 @@ def process_upload(upload_id: str) -> Dict[str, Any]:
expr_values[":st"] = "completed"

# --- Binary files (PDF/DOCX/XLSX/PPTX): download → convert → upload ---
elif ext in (".pdf", ".docx", ".xlsx") and s3_key:
elif ext in (".pdf", ".docx", ".xlsx", ".pptx") and s3_key:
converted_prefix = f"uploads/{user_id}/{upload_id}/converted"
try:
with tempfile.TemporaryDirectory() as tmp:
Expand Down Expand Up @@ -1496,6 +1524,22 @@ def process_upload(upload_id: str) -> Dict[str, Any]:
if result.warnings:
update_expr_parts.append("conversionWarnings = :cw")
expr_values[":cw"] = result.warnings
# PPTX deck-structure metadata — stored for the edit guide
# and returned by process_upload / get_upload_status.
if result.deck_structure:
from decimal import Decimal
bg_lum = result.theme_hints.get("backgroundLuminance")
update_expr_parts.append("themeHints = :th")
expr_values[":th"] = {
# DynamoDB rejects native floats; round-trip via str(Decimal).
"backgroundLuminance": Decimal(str(bg_lum)) if bg_lum is not None else None,
"accentColors": result.theme_hints.get("accentColors", []),
"fonts": result.theme_hints.get("fonts", {}),
}
update_expr_parts.append("slideCount = :sc")
expr_values[":sc"] = result.slide_count
update_expr_parts.append("suggestedName = :sn")
expr_values[":sn"] = result.suggested_name

except Exception as e:
logger.exception("Conversion failed for %s", upload_id)
Expand All @@ -1517,12 +1561,14 @@ def process_upload(upload_id: str) -> Dict[str, Any]:
if file_type.startswith("image/") and s3_key:
image_url = presigned_url(s3_client, BUCKET_NAME, s3_key)

return {
response: Dict[str, Any] = {
"uploadId": upload_id,
"status": expr_values[":st"],
"extractedText": extracted_text,
"imageUrl": image_url,
}
response.update(_pptx_guide_fields(expr_values))
return response


@app.get("/uploads/<upload_id>/status")
Expand All @@ -1538,14 +1584,16 @@ def get_upload_status(upload_id: str) -> Dict[str, Any]:
if item.get("fileType", "").startswith("image/") and item.get("s3KeyRaw"):
image_url = presigned_url(s3_client, BUCKET_NAME, item["s3KeyRaw"])

return {
response: Dict[str, Any] = {
"uploadId": upload_id,
"fileName": item.get("fileName", ""),
"fileType": item.get("fileType", ""),
"status": item.get("status", "unknown"),
"extractedText": item.get("extractedText"),
"imageUrl": image_url,
}
response.update(_pptx_guide_fields(item))
return response


# ---------------------------------------------------------------------------
Expand Down
7 changes: 7 additions & 0 deletions api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,12 @@ pdfplumber>=0.11.0
pypdf>=4.0.0
python-docx>=1.0.0
openpyxl>=3.1.0
# Required for PPTX upload-time conversion (shared/ingest._convert_pptx).
# Mirrors skill/pyproject.toml because we copy skill/sdpm into the Lambda
# bundle without pip-installing the package itself.
python-pptx>=1.0.0
lxml>=5.0.0
Pillow>=10.0.0
qrcode>=7.4.0
pygments>=2.19.2
defusedxml>=0.7.0
11 changes: 4 additions & 7 deletions infra/lib/web-ui-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,7 @@ function handler(event) {
"bash", "-c",
"pip install -r /asset-input/api/requirements.txt -t /asset-output/ && " +
"cp -r /asset-input/api/* /asset-input/shared /asset-output/ && " +
"mkdir -p /asset-output/sdpm && cp -r /asset-input/skill/sdpm/analyzer /asset-output/sdpm/ && " +
"cp /asset-input/skill/sdpm/__init__.py /asset-output/sdpm/ && " +
"mkdir -p /asset-output/sdpm/utils && cp /asset-input/skill/sdpm/utils/__init__.py /asset-input/skill/sdpm/utils/io.py /asset-output/sdpm/utils/",
"cp -r /asset-input/skill/sdpm /asset-output/sdpm",
],
local: {
tryBundle(outputDir: string): boolean {
Expand All @@ -250,10 +248,9 @@ function handler(event) {
}
execSync(`cp -r ${root}/api/* ${outputDir}/`, { stdio: "inherit" });
execSync(`cp -r ${root}/shared ${outputDir}/shared`, { stdio: "inherit" });
execSync(`mkdir -p ${outputDir}/sdpm/utils`, { stdio: "inherit" });
execSync(`cp -r ${root}/skill/sdpm/analyzer ${outputDir}/sdpm/`, { stdio: "inherit" });
execSync(`cp ${root}/skill/sdpm/__init__.py ${outputDir}/sdpm/`, { stdio: "inherit" });
execSync(`cp ${root}/skill/sdpm/utils/__init__.py ${root}/skill/sdpm/utils/io.py ${outputDir}/sdpm/utils/`, { stdio: "inherit" });
// sdpm Engine — required by shared.ingest._convert_pptx for upload-time
// PPTX → deck conversion (deck.json + slides/*.json + template.pptx).
execSync(`cp -r ${root}/skill/sdpm ${outputDir}/sdpm`, { stdio: "inherit" });
return true;
},
},
Expand Down
68 changes: 67 additions & 1 deletion mcp-local/acp-agent-prompts/spec-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@ You handle Phase 1 through user dialogue.
Respond in the same language as the user.
Write all spec files in the user's language.

## Flow selection (evaluate this FIRST on every turn)

Before applying any other instruction in this prompt, decide which of the
two flows you are in:

1. **Guide flow (edit branch)** — triggered when any tool response in the
conversation contains a `guideInstruction` field. The instruction asks
you to classify the user's intent and may direct you to a specific
guide (e.g. `import-pptx` for PPTX edits). Once a guide is active,
follow the guide's Steps 1 → 5 in order. Do NOT read any
`create-new-*` workflow, do NOT start a Phase 1 Briefing, and do NOT
ask the user questions about audience / tone / time budget / etc.
Those belong to the new-deck flow below. The guide auto-generates the
briefing / outline / art-direction from the source material in its
own Step 3, and the deck builds against a PPTX-derived placeholder
template (no template selection hearing).

2. **New-deck flow** — triggered when no `guideInstruction` is pending and
the user wants to build a presentation from scratch. Run the Phase 1
Flow described later in this prompt.

If you are in a guide flow, all subsequent sections of this prompt titled
`## Phase 1 Flow`, `## Workflow: New Presentation`, and `## Slide Group
Assignment` do NOT apply until the guide completes.

## Hearing

Your primary job is user hearing. Do not rush to produce output.
Expand All @@ -20,6 +45,10 @@ Limit to 5 questions per call. If you need more, call again after the user respo

## Phase 1 Flow

**This section applies to the new-deck flow only** (see "Flow selection"
above). When a guide is active, skip this section entirely and follow
the guide instead.

Phase 1 produces 3 spec files through sequential sub-phases.
Each sub-phase has a workflow file that defines the deliverable format and procedure.
You MUST read the workflow before starting that sub-phase — the deliverables have strict formats
Expand Down Expand Up @@ -66,7 +95,8 @@ When user provides a file path or URL:

## Delegation to Composer

When all 3 spec files are approved:
When all 3 spec files are approved (new-deck flow) OR when the guide's
Step 5 completes (edit branch):
- Split slides into groups and invoke sdpm-composer subagents in parallel (max 4)
- Use `use_subagent` with `subagents: [{"query": "deck_id=... slides: slug1, slug2", "agent_name": "sdpm-composer"}, ...]` (max 4 parallel). ASCII-only queries.
- You do NOT write slide JSON yourself. You do NOT call build/measure/preview tools directly
Expand All @@ -76,6 +106,42 @@ When all 3 spec files are approved:

Each subagent query MUST include: deck_id (path), assigned slide slugs, and a pointer to specs/.

## Guide-driven flows

If a tool response (e.g. `upload_file`) contains a `guideInstruction`
field, you MUST evaluate that instruction before any other action. The
instruction may ask you to determine user intent and branch accordingly.

**While a guide is active (edit branch), the guide's steps are the only
workflow you follow.** Do NOT read `create-new-*` workflows, do NOT ask
Phase 1 briefing/outline/art-direction questions, and do NOT invoke
Phase 1 Flow instructions. Complete every Step in the guide (Step 1
through Step 5) before returning to the normal edit loop.

For uploaded PPTX files specifically:

1. `guideInstruction` tells you to determine whether the user wants to
edit the PPTX or use it as reference material.
2. If intent is clear → follow the branch directly.
3. If intent is ambiguous → use `hearing` once to ask, then branch.
4. **Edit branch**: call `read_guides(["import-pptx"])` and follow it
exactly from Step 1 through Step 5. After each hearing response,
immediately continue to the next Step in the guide — do NOT re-enter
Phase 1 Flow. The specs (brief / outline / art-direction) are
auto-generated from the PPTX content inside the guide, so you do NOT
need to run a briefing hearing. The deck-local placeholder template
(`template.pptx`) is copied automatically by `import_attachment`, so
no template selection hearing is needed either. **Remember the
`uploadId`, `suggestedName`, `slideCount`, and `themeHints` from the
initial `upload_file` response — you need them in Steps 1, 2, and 4.
Never ask the user to re-upload the file; those values remain in the
conversation context from the original attachment.** After Step 5
completes, return to the normal edit loop (user requests → invoke
sdpm-composer subagents via `use_subagent`).
5. **Reference branch**: proceed with the normal briefing flow. Use
`read_uploaded_file(upload_id)` when you need content, and cite line
numbers in `specs/brief.md` Source Material.

## Workflow: New Presentation
→ Read `read_workflows(["create-new-1-briefing"])` to start. Follow each file's Next Step from there.

Expand Down
7 changes: 5 additions & 2 deletions mcp-local/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,11 @@

## Workflow B: Edit Existing PPTX

When an existing PPTX is provided.
→ Read `read_workflows(["edit-existing"])` to start.
When an existing PPTX is provided. The Web UI / API path is the
preferred entrypoint: `upload_file` returns
`guideInstruction: "read_guides([\"import-pptx\"])"` automatically,
so just follow that instruction. For CLI-only flows, call
`read_guides(["import-pptx"])` directly.

## Workflow C: Hand-Edit Sync

Expand Down
4 changes: 2 additions & 2 deletions mcp-local/server_acp.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,8 @@ def init_presentation(name: str, template: str = "") -> str:
from datetime import datetime
from sdpm.analyzer import extract_fonts

root = os.environ.get("SDPM_DECK_ROOT", "")
base_dir = Path(root) if root else Path.home() / "Documents" / "SDPM-Presentations"
base_dir_env = os.environ.get("SDPM_DECK_ROOT", "").strip()
base_dir = Path(base_dir_env) if base_dir_env else Path.home() / "Documents" / "SDPM-Presentations"
ts = datetime.now().strftime("%Y%m%d-%H%M")
dir_name = f"{ts}-{name}" if name else ts
out_dir = base_dir / dir_name
Expand Down
Loading
Loading