Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
23a2127
✨ feat(svg-composition-animation): SVG compose pipeline + animated pr…
ShotaroKataoka Apr 13, 2026
90f7a31
🐛 fix(svg-composition-animation): DOMPurify strip + preview filter + …
ShotaroKataoka Apr 13, 2026
02d5669
🐛 fix(svg-composition-animation): bg color, epoch keys, initial load,…
ShotaroKataoka Apr 13, 2026
b155843
✨ feat(svg-composition-animation): auto-scroll to updated slide, comp…
ShotaroKataoka Apr 13, 2026
e7e63e3
🐛 fix(svg-composition-animation): subprocess import, _export_svg sepa…
ShotaroKataoka Apr 13, 2026
f98d79b
✨ feat(svg-composition-animation): initial compose animate, scroll po…
ShotaroKataoka Apr 13, 2026
714eac1
🐛 fix(svg-composition-animation): scroll to actually changed slide vi…
ShotaroKataoka Apr 13, 2026
230d1cf
🎯 perf(svg-composition-animation): slower stagger/wireframe, keep typ…
ShotaroKataoka Apr 13, 2026
2a802c8
🐛 fix(svg-composition-animation): arm scroll only after compose URL c…
ShotaroKataoka Apr 13, 2026
8ef250d
🐛 fix(svg-composition-animation): diff by text+class fingerprint, ski…
ShotaroKataoka Apr 13, 2026
c5fac7b
🐛 fix(svg-composition-animation): compose key prefix collision (slide…
ShotaroKataoka Apr 13, 2026
c791e2a
🐛 fix(svg-composition-animation): cancel stale polling on deck switch
ShotaroKataoka Apr 13, 2026
27b45b3
✨ feat(svg-composition-animation): backend diff with sourceHash + com…
ShotaroKataoka Apr 13, 2026
3728ba6
🐛 fix(svg-composition-animation): slot fallback diff + defer during a…
ShotaroKataoka Apr 13, 2026
06932b6
🐛 fix(svg-composition-animation): skipAnimation prop + new slide dete…
ShotaroKataoka Apr 13, 2026
5d04d89
🔒 fix(svg-composition-animation): md5 usedforsecurity=False for bandi…
ShotaroKataoka Apr 13, 2026
8268342
📚 docs: add ASH security scanning to tech.md, opt-in to git
ShotaroKataoka Apr 13, 2026
f99f380
🧹 cleanup: remove tech.md from git (contains internal info)
ShotaroKataoka Apr 13, 2026
b6d4190
📚 docs: add tech-public.md with ASH scanning info
ShotaroKataoka Apr 13, 2026
d8609c0
📚 docs: add pre-PR local check to principles.md
ShotaroKataoka Apr 13, 2026
b30c0a5
🐛 fix(svg-composition-animation): new deck animation + template fallback
ShotaroKataoka Apr 13, 2026
630eb23
🐛 fix(svg-composition-animation): hadSlidesOnMount for skip/animate d…
ShotaroKataoka Apr 13, 2026
4e3beff
🧹 cleanup(svg-composition-animation): fix indent + hoist import re
ShotaroKataoka Apr 13, 2026
146ca06
🔧 config(svg-composition-animation): merge main into feature branch
ShotaroKataoka Apr 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ web-ui/public/aws-exports.json
!.kiro/steering/
.kiro/steering/*
!.kiro/steering/principles.md
!.kiro/steering/tech-public.md
!.kiro/steering/versioning.md
!.kiro/steering/steering-policy.md

Expand Down
8 changes: 8 additions & 0 deletions .kiro/steering/principles.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,11 @@ Do not share:
1. Does the logic exist in the Engine? → Use it
2. Is it infrastructure-dependent? → S3/DynamoDB dependencies allow MCP Remote independent implementation
3. Is the difference only in presentation/output? → Engine API returns data, each layer controls output

## PR前ローカルチェック

PR作成前に以下をローカルで実行し、CI待ちを減らす:

```bash
ash scan --mode local --fail-on-findings
```
14 changes: 14 additions & 0 deletions .kiro/steering/tech-public.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!-- PUBLIC: This file is git-tracked and visible in the public repository. -->

# Tech (Public)

## Deployment
- WebUI: `AWS_DEFAULT_REGION=<region> bash scripts/deploy_webui.sh`
- CDK stacks: SdpmWebUi, SdpmAgent, SdpmRuntime, SdpmPngWorker, SdpmData, SdpmAuth

## Security Scanning
- ASH (Automated Security Helper) v3
- Local: `ash scan --mode local --fail-on-findings`
- Install: `alias ash="uvx git+https://github.com/awslabs/automated-security-helper.git@v3"`
- CI: GitHub Actions `.github/workflows/` で `--fail-on-findings` 付きで実行
- md5等の非セキュリティ用途ハッシュには `usedforsecurity=False` を付与(bandit B303)
31 changes: 30 additions & 1 deletion api/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,15 +424,43 @@ def get_deck(deck_id: str) -> Dict[str, Any]:
resp = s3_client.get_object(Bucket=BUCKET_NAME, Key=pres_key)
presentation = json.loads(resp["Body"].read())
preview_keys = _list_preview_keys(deck_id)
# Check compose data existence (defs + per-slide, epoch-keyed like previews)
compose_keys = set()
try:
for obj in s3_client.list_objects_v2(Bucket=BUCKET_NAME, Prefix=f"decks/{deck_id}/compose/").get("Contents", []):
compose_keys.add(obj["Key"])
except Exception:
pass

import re as _re
def _latest_compose_key(prefix: str, keys: set) -> Optional[str]:
"""Pick the key with the highest epoch from epoch-keyed compose files."""
best_epoch, best_key = -1, None
for k in keys:
if not k.startswith(prefix):
continue
m = _re.search(r"_(\d+)\.json$", k)
epoch = int(m.group(1)) if m else 0
if epoch > best_epoch:
best_epoch, best_key = epoch, k
return best_key

defs_key = _latest_compose_key(f"decks/{deck_id}/compose/defs_", compose_keys)
has_defs = defs_key is not None

for i, s in enumerate(presentation.get("slides", [])):
sid = f"slide_{i + 1:02d}"
slide_preview = _resolve_preview_url(deck_id, sid, preview_keys)
slide_entry: Dict[str, Any] = {"slideId": sid, "previewUrl": slide_preview}
# Compose URL for animation (epoch-keyed)
compose_key = _latest_compose_key(f"decks/{deck_id}/compose/slide_{i + 1}_", compose_keys)
if compose_key:
slide_entry["composeUrl"] = preview_url(compose_key)
if include_json:
slide_entry["slideJson"] = json.dumps(s)
slides.append(slide_entry)
except Exception:
pass
has_defs = False

# Read spec files from S3 (brief.md, outline.md, art-direction.html/.md)
specs: Dict[str, Any] = {}
Expand Down Expand Up @@ -467,6 +495,7 @@ def get_deck(deck_id: str) -> Dict[str, Any]:
"slideCount": len(slides),
"slides": slides,
"specs": specs,
"defsUrl": preview_url(defs_key) if has_defs else None,
"pptxUrl": (_cf_signed_url(pptx_key) or presigned_url(s3_client, BUCKET_NAME, pptx_key)) if pptx_key else None,
"updatedAt": deck.get("updatedAt", ""),
"chatSessionId": deck.get("chatSessionId"),
Expand Down
1 change: 1 addition & 0 deletions mcp-server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Platform: ARM64 (required by AgentCore Runtime)
# Listens on 0.0.0.0:8000/mcp (stateless streamable-http)
# Includes LibreOffice + poppler for synchronous PNG/SVG generation
# cache-bust: 20260413-1457

FROM --platform=linux/arm64 public.ecr.aws/docker/library/fedora:41

Expand Down
171 changes: 137 additions & 34 deletions mcp-server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,19 +461,23 @@ def _build_pptx(tmpdir: Path, slides: list[dict], build_kwargs: dict) -> Path:
return pptx_path


def _run_measure(tmpdir: Path, pptx_path: Path, slide_numbers: list[int]) -> str:
"""PPTX → SVG → bbox measurement → report string."""
def _export_svg(tmpdir: Path, pptx_path: Path) -> Path:
"""PPTX → SVG via LibreOffice. Returns svg_path."""
import subprocess

from sdpm.preview.measure import measure_from_svg, format_measure_report

env = os.environ.copy()
env["HOME"] = str(tmpdir)
subprocess.run( # nosec B603
subprocess.run(
["soffice", "--headless", "--convert-to", "svg", "--outdir", str(tmpdir), str(pptx_path)],
env=env, capture_output=True, text=True, timeout=120, check=True,
)
svg_path = tmpdir / "measure.svg"
return tmpdir / "measure.svg"


def _run_measure(tmpdir: Path, pptx_path: Path, slide_numbers: list[int]) -> str:
"""PPTX → SVG → bbox measurement → report string."""
from sdpm.preview.measure import measure_from_svg, format_measure_report

svg_path = _export_svg(tmpdir, pptx_path)
if not svg_path.exists():
return json.dumps({"error": "LibreOffice SVG export failed"})

Expand Down Expand Up @@ -744,8 +748,7 @@ def run_python(code: str, deck_id: str | None = None, save: bool = False,
result: dict = {"output": output}

# Post-processing: measure_slides triggers PPTX build → measure/lint/bias
# WebP preview generation runs when measure_slides is present and save=True
if deck_id and measure_slides:
if deck_id and (measure_slides or save):
import shutil
import traceback

Expand All @@ -756,35 +759,135 @@ def run_python(code: str, deck_id: str | None = None, save: bool = False,
tmpdir, slides, build_kwargs = _prepare_workspace(deck_id, user_id, _storage)
pptx_path = _build_pptx(tmpdir, slides, build_kwargs)

# Measure
try:
result["measure"] = _run_measure(tmpdir, pptx_path, measure_slides)
except Exception as e:
result["measure"] = json.dumps({"error": str(e)})
# Measure (runs SVG export internally)
if measure_slides:
try:
result["measure"] = _run_measure(tmpdir, pptx_path, measure_slides)
except Exception as e:
result["measure"] = json.dumps({"error": str(e)})

# Lint (filter to measured slides; lint uses 0-based index)
try:
from sdpm.schema.lint import lint as lint_slides
presentation = json.loads((tmpdir / "presentation.json").read_text(encoding="utf-8"))
slide_set = set(measure_slides)
lint_diag = [d for d in lint_slides(presentation) if d.get("slide") + 1 in slide_set]
if lint_diag:
result["errors"] = {"lintDiagnostics": lint_diag}
except Exception as e:
logger.warning("Lint failed: %s", e)

# Layout bias (filter to measured slides; bias uses 1-based)
try:
from sdpm.preview import check_layout_imbalance_data
layout_bias = [b for b in check_layout_imbalance_data(pptx_path, slide_defs=slides) if b.get("slide") in slide_set]
if layout_bias:
result["warnings"] = {"layoutBias": layout_bias}
except Exception as e:
logger.warning("Layout bias check failed: %s", e)
if measure_slides:
try:
from sdpm.schema.lint import lint as lint_slides
presentation = json.loads((tmpdir / "presentation.json").read_text(encoding="utf-8"))
slide_set = set(measure_slides)
lint_diag = [d for d in lint_slides(presentation) if d.get("slide") + 1 in slide_set]
if lint_diag:
result["errors"] = {"lintDiagnostics": lint_diag}
except Exception as e:
logger.warning("Lint failed: %s", e)

# Layout bias (filter to measured slides; bias uses 1-based)
try:
from sdpm.preview import check_layout_imbalance_data
layout_bias = [b for b in check_layout_imbalance_data(pptx_path, slide_defs=slides) if b.get("slide") in slide_set]
if layout_bias:
result["warnings"] = {"layoutBias": layout_bias}
except Exception as e:
logger.warning("Layout bias check failed: %s", e)

if save:
from server_utils import schedule_webp_background
schedule_webp_background(deck_id, pptx_path, tmpdir, _storage)
# Compose: SVG → optimized JSON for WebUI animation
# Runs synchronously — measured at ~300ms for 8 slides (≪3s threshold)
try:
from tools.compose import extract_optimized_defs, split_slide_components, count_slides
import hashlib as _hashlib
svg_path = tmpdir / "measure.svg"
if not svg_path.exists():
_export_svg(tmpdir, pptx_path)
if svg_path.exists():
import json as _json
import time as _time
_epoch = int(_time.time())
compose_prefix = f"decks/{deck_id}/compose/"

# 1. List old compose keys (for cleanup + prev data)
old_keys = _storage.list_files(prefix=compose_prefix, bucket=_storage.pptx_bucket)

# 2. Load previous slide compose data keyed by sourceHash + slot number
prev_by_hash: dict[str, list[dict]] = {}
prev_by_slot: list[list[dict]] = [] # ordered by slide number
prev_slot_map: dict[int, list[dict]] = {}
import re as _re
for k in old_keys:
if "/slide_" not in k:
continue
try:
raw = _storage.download_file_from_pptx_bucket(k)
prev_data = _json.loads(raw)
comps = prev_data.get("components", [])
h = prev_data.get("sourceHash")
if h:
prev_by_hash[h] = comps
# Extract slot number from key: slide_{N}_{epoch}.json
m = _re.search(r"/slide_(\d+)_", k)
if m:
prev_slot_map[int(m.group(1))] = comps
except Exception:
pass
# Build ordered list
if prev_slot_map:
max_slot = max(prev_slot_map.keys())
prev_by_slot = [prev_slot_map.get(i, []) for i in range(1, max_slot + 1)]

# 3. Upload defs
defs_data = extract_optimized_defs(svg_path)
_storage.upload_file(
key=f"{compose_prefix}defs_{_epoch}.json",
data=_json.dumps(defs_data, ensure_ascii=False).encode(),
content_type="application/json",
)

# 4. Generate compose for all slides with changed flags
_total = count_slides(svg_path)
for sn in range(1, _total + 1):
try:
comp_data = split_slide_components(svg_path, sn)
# sourceHash from slide JSON
src_hash = ""
if sn <= len(slides):
src_hash = _hashlib.md5(_json.dumps(slides[sn - 1], sort_keys=True, ensure_ascii=False).encode(), usedforsecurity=False).hexdigest()
comp_data["sourceHash"] = src_hash

# Diff: find prev slide by sourceHash, fallback to same slot number
prev_comps = prev_by_hash.get(src_hash)
if prev_comps is None and sn <= len(prev_by_slot):
prev_comps = prev_by_slot[sn - 1]
if prev_comps is not None:
# Component-level diff
def _mk(c: dict) -> str:
b = c.get("bbox")
return f"{c['class']}|{b['x']},{b['y']},{b['w']},{b['h']}" if b else f"{c['class']}|none"
def _fp(c: dict) -> str:
return f"{c['class']}|{c.get('text', '')}"
prev_map = {_mk(c): _fp(c) for c in prev_comps}
for c in comp_data["components"]:
k = _mk(c)
c["changed"] = k not in prev_map or prev_map[k] != _fp(c)
else:
for c in comp_data["components"]:
c["changed"] = True

_storage.upload_file(
key=f"{compose_prefix}slide_{sn}_{_epoch}.json",
data=_json.dumps(comp_data, ensure_ascii=False).encode(),
content_type="application/json",
)
except Exception:
logger.error("compose failed for slide %d", sn, exc_info=True)

# 5. Cleanup old compose files
for k in old_keys:
try:
_storage._s3.delete_object(Bucket=_storage.pptx_bucket, Key=k)
except Exception:
pass
except Exception:
logger.error("compose failed", exc_info=True)

# tmpdir cleanup (WebP generation only in generate_pptx)
shutil.rmtree(tmpdir, ignore_errors=True)
else:
shutil.rmtree(tmpdir, ignore_errors=True)
except Exception as e:
Expand Down
Loading
Loading