@kortix/sandbox 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/config/customize.sh +143 -0
  2. package/config/kortix-env-setup.sh +25 -0
  3. package/kortix-master/package.json +22 -0
  4. package/kortix-master/src/config.ts +22 -0
  5. package/kortix-master/src/index.ts +44 -0
  6. package/kortix-master/src/routes/env.ts +65 -0
  7. package/kortix-master/src/routes/proxy.ts +108 -0
  8. package/kortix-master/src/routes/update.ts +185 -0
  9. package/kortix-master/src/services/proxy.ts +43 -0
  10. package/kortix-master/src/services/secret-store.ts +156 -0
  11. package/kortix-master/tsconfig.json +14 -0
  12. package/opencode/agents/kortix-browser.md +142 -0
  13. package/opencode/agents/kortix-build.md +62 -0
  14. package/opencode/agents/kortix-explore.md +66 -0
  15. package/opencode/agents/kortix-image-gen.md +33 -0
  16. package/opencode/agents/kortix-main.md +450 -0
  17. package/opencode/agents/kortix-plan.md +100 -0
  18. package/opencode/agents/kortix-research.md +84 -0
  19. package/opencode/agents/kortix-sheets.md +61 -0
  20. package/opencode/agents/kortix-slides.md +64 -0
  21. package/opencode/agents/kortix-web-dev.md +572 -0
  22. package/opencode/commands/email.md +36 -0
  23. package/opencode/commands/init.md +43 -0
  24. package/opencode/commands/journal.md +44 -0
  25. package/opencode/commands/memory-init.md +81 -0
  26. package/opencode/commands/memory-search.md +50 -0
  27. package/opencode/commands/memory-status.md +56 -0
  28. package/opencode/commands/research.md +36 -0
  29. package/opencode/commands/search.md +38 -0
  30. package/opencode/commands/slides.md +32 -0
  31. package/opencode/commands/spreadsheet.md +30 -0
  32. package/opencode/memory.json +37 -0
  33. package/opencode/ocx.jsonc +10 -0
  34. package/opencode/opencode.jsonc +103 -0
  35. package/opencode/package.json +25 -0
  36. package/opencode/patches/apply.sh +19 -0
  37. package/opencode/patches/opencode-pty-spawn.txt +49 -0
  38. package/opencode/plugin/background-agents.ts.disabled +483 -0
  39. package/opencode/plugin/kdco-primitives/get-project-id.ts +172 -0
  40. package/opencode/plugin/kdco-primitives/index.ts +26 -0
  41. package/opencode/plugin/kdco-primitives/log-warn.ts +51 -0
  42. package/opencode/plugin/kdco-primitives/mutex.ts +122 -0
  43. package/opencode/plugin/kdco-primitives/shell.ts +138 -0
  44. package/opencode/plugin/kdco-primitives/temp.ts +36 -0
  45. package/opencode/plugin/kdco-primitives/terminal-detect.ts +34 -0
  46. package/opencode/plugin/kdco-primitives/types.ts +13 -0
  47. package/opencode/plugin/kdco-primitives/with-timeout.ts +84 -0
  48. package/opencode/plugin/memory.ts +306 -0
  49. package/opencode/plugin/worktree/state.ts +412 -0
  50. package/opencode/plugin/worktree/terminal.ts +1002 -0
  51. package/opencode/plugin/worktree.ts +861 -0
  52. package/opencode/skills/KORTIX-browser/SKILL.md +478 -0
  53. package/opencode/skills/KORTIX-cron-triggers/SKILL.md +173 -0
  54. package/opencode/skills/KORTIX-deep-research/SKILL.md +278 -0
  55. package/opencode/skills/KORTIX-docx/SKILL.md +398 -0
  56. package/opencode/skills/KORTIX-docx/scripts/__init__.py +1 -0
  57. package/opencode/skills/KORTIX-docx/scripts/accept_changes.py +104 -0
  58. package/opencode/skills/KORTIX-docx/scripts/comment.py +244 -0
  59. package/opencode/skills/KORTIX-docx/scripts/office/helpers/__init__.py +0 -0
  60. package/opencode/skills/KORTIX-docx/scripts/office/helpers/merge_runs.py +199 -0
  61. package/opencode/skills/KORTIX-docx/scripts/office/helpers/simplify_redlines.py +197 -0
  62. package/opencode/skills/KORTIX-docx/scripts/office/pack.py +159 -0
  63. package/opencode/skills/KORTIX-docx/scripts/office/soffice.py +183 -0
  64. package/opencode/skills/KORTIX-docx/scripts/office/unpack.py +132 -0
  65. package/opencode/skills/KORTIX-docx/scripts/office/validate.py +111 -0
  66. package/opencode/skills/KORTIX-docx/scripts/office/validators/__init__.py +15 -0
  67. package/opencode/skills/KORTIX-docx/scripts/office/validators/base.py +847 -0
  68. package/opencode/skills/KORTIX-docx/scripts/office/validators/docx.py +446 -0
  69. package/opencode/skills/KORTIX-docx/scripts/office/validators/pptx.py +275 -0
  70. package/opencode/skills/KORTIX-docx/scripts/office/validators/redlining.py +247 -0
  71. package/opencode/skills/KORTIX-docx/scripts/render_docx.py +179 -0
  72. package/opencode/skills/KORTIX-docx/scripts/templates/comments.xml +3 -0
  73. package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtended.xml +3 -0
  74. package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtensible.xml +3 -0
  75. package/opencode/skills/KORTIX-docx/scripts/templates/commentsIds.xml +3 -0
  76. package/opencode/skills/KORTIX-docx/scripts/templates/people.xml +3 -0
  77. package/opencode/skills/KORTIX-domain-research/SKILL.md +96 -0
  78. package/opencode/skills/KORTIX-domain-research/scripts/domain-lookup.py +810 -0
  79. package/opencode/skills/KORTIX-elevenlabs/SKILL.md +230 -0
  80. package/opencode/skills/KORTIX-elevenlabs/scripts/tts.py +389 -0
  81. package/opencode/skills/KORTIX-email/SKILL.md +145 -0
  82. package/opencode/skills/KORTIX-legal-writer/SKILL.md +409 -0
  83. package/opencode/skills/KORTIX-legal-writer/references/bluebook.md +152 -0
  84. package/opencode/skills/KORTIX-legal-writer/references/document-types.md +416 -0
  85. package/opencode/skills/KORTIX-legal-writer/scripts/courtlistener.py +291 -0
  86. package/opencode/skills/KORTIX-legal-writer/scripts/ecfr_lookup.py +299 -0
  87. package/opencode/skills/KORTIX-legal-writer/scripts/verify-legal.py +507 -0
  88. package/opencode/skills/KORTIX-logo-creator/SKILL.md +293 -0
  89. package/opencode/skills/KORTIX-logo-creator/references/prompt-patterns.md +134 -0
  90. package/opencode/skills/KORTIX-logo-creator/scripts/compose_logo.py +406 -0
  91. package/opencode/skills/KORTIX-logo-creator/scripts/create_logo_sheet.py +258 -0
  92. package/opencode/skills/KORTIX-logo-creator/scripts/remove_bg.py +96 -0
  93. package/opencode/skills/KORTIX-memory/SKILL.md +261 -0
  94. package/opencode/skills/KORTIX-memory/scripts/export-sessions.py +409 -0
  95. package/opencode/skills/KORTIX-paper-creator/SKILL.md +549 -0
  96. package/opencode/skills/KORTIX-paper-creator/assets/template.tex +101 -0
  97. package/opencode/skills/KORTIX-paper-creator/scripts/compile.sh +177 -0
  98. package/opencode/skills/KORTIX-paper-creator/scripts/openalex_to_bibtex.py +220 -0
  99. package/opencode/skills/KORTIX-paper-creator/scripts/verify.sh +354 -0
  100. package/opencode/skills/KORTIX-paper-search/SKILL.md +418 -0
  101. package/opencode/skills/KORTIX-pdf/SKILL.md +232 -0
  102. package/opencode/skills/KORTIX-pdf/forms.md +36 -0
  103. package/opencode/skills/KORTIX-pdf/reference.md +105 -0
  104. package/opencode/skills/KORTIX-pdf/scripts/check_bounding_boxes.py +65 -0
  105. package/opencode/skills/KORTIX-pdf/scripts/check_fillable_fields.py +11 -0
  106. package/opencode/skills/KORTIX-pdf/scripts/convert_pdf_to_images.py +33 -0
  107. package/opencode/skills/KORTIX-pdf/scripts/create_validation_image.py +37 -0
  108. package/opencode/skills/KORTIX-pdf/scripts/extract_form_field_info.py +122 -0
  109. package/opencode/skills/KORTIX-pdf/scripts/extract_form_structure.py +115 -0
  110. package/opencode/skills/KORTIX-pdf/scripts/fill_fillable_fields.py +98 -0
  111. package/opencode/skills/KORTIX-pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
  112. package/opencode/skills/KORTIX-plan/SKILL.md +228 -0
  113. package/opencode/skills/KORTIX-presentation-viewer/SKILL.md +87 -0
  114. package/opencode/skills/KORTIX-presentation-viewer/serve.ts +136 -0
  115. package/opencode/skills/KORTIX-presentation-viewer/viewer.html +559 -0
  116. package/opencode/skills/KORTIX-presentations/SKILL.md +344 -0
  117. package/opencode/skills/KORTIX-remotion/SKILL.md +56 -0
  118. package/opencode/skills/KORTIX-remotion/rules/3d.md +86 -0
  119. package/opencode/skills/KORTIX-remotion/rules/animations.md +29 -0
  120. package/opencode/skills/KORTIX-remotion/rules/assets.md +78 -0
  121. package/opencode/skills/KORTIX-remotion/rules/audio-visualization.md +198 -0
  122. package/opencode/skills/KORTIX-remotion/rules/audio.md +169 -0
  123. package/opencode/skills/KORTIX-remotion/rules/calculate-metadata.md +104 -0
  124. package/opencode/skills/KORTIX-remotion/rules/can-decode.md +75 -0
  125. package/opencode/skills/KORTIX-remotion/rules/charts.md +120 -0
  126. package/opencode/skills/KORTIX-remotion/rules/compositions.md +141 -0
  127. package/opencode/skills/KORTIX-remotion/rules/display-captions.md +184 -0
  128. package/opencode/skills/KORTIX-remotion/rules/extract-frames.md +229 -0
  129. package/opencode/skills/KORTIX-remotion/rules/ffmpeg.md +38 -0
  130. package/opencode/skills/KORTIX-remotion/rules/fonts.md +152 -0
  131. package/opencode/skills/KORTIX-remotion/rules/get-audio-duration.md +58 -0
  132. package/opencode/skills/KORTIX-remotion/rules/get-video-dimensions.md +68 -0
  133. package/opencode/skills/KORTIX-remotion/rules/get-video-duration.md +58 -0
  134. package/opencode/skills/KORTIX-remotion/rules/gifs.md +141 -0
  135. package/opencode/skills/KORTIX-remotion/rules/images.md +130 -0
  136. package/opencode/skills/KORTIX-remotion/rules/import-srt-captions.md +69 -0
  137. package/opencode/skills/KORTIX-remotion/rules/light-leaks.md +73 -0
  138. package/opencode/skills/KORTIX-remotion/rules/lottie.md +68 -0
  139. package/opencode/skills/KORTIX-remotion/rules/maps.md +401 -0
  140. package/opencode/skills/KORTIX-remotion/rules/measuring-dom-nodes.md +35 -0
  141. package/opencode/skills/KORTIX-remotion/rules/measuring-text.md +143 -0
  142. package/opencode/skills/KORTIX-remotion/rules/parameters.md +98 -0
  143. package/opencode/skills/KORTIX-remotion/rules/sequencing.md +118 -0
  144. package/opencode/skills/KORTIX-remotion/rules/subtitles.md +36 -0
  145. package/opencode/skills/KORTIX-remotion/rules/tailwind.md +11 -0
  146. package/opencode/skills/KORTIX-remotion/rules/text-animations.md +20 -0
  147. package/opencode/skills/KORTIX-remotion/rules/timing.md +179 -0
  148. package/opencode/skills/KORTIX-remotion/rules/transcribe-captions.md +70 -0
  149. package/opencode/skills/KORTIX-remotion/rules/transitions.md +197 -0
  150. package/opencode/skills/KORTIX-remotion/rules/transparent-videos.md +106 -0
  151. package/opencode/skills/KORTIX-remotion/rules/trimming.md +53 -0
  152. package/opencode/skills/KORTIX-remotion/rules/videos.md +171 -0
  153. package/opencode/skills/KORTIX-secrets/SKILL.md +280 -0
  154. package/opencode/skills/KORTIX-semantic-search/SKILL.md +213 -0
  155. package/opencode/skills/KORTIX-session-search/SKILL.md +807 -0
  156. package/opencode/skills/KORTIX-session-search/Untitled +1 -0
  157. package/opencode/skills/KORTIX-skill-creator/SKILL.md +163 -0
  158. package/opencode/skills/KORTIX-web-research/SKILL.md +69 -0
  159. package/opencode/skills/KORTIX-xlsx/LICENSE.txt +30 -0
  160. package/opencode/skills/KORTIX-xlsx/SKILL.md +549 -0
  161. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/__init__.py +0 -0
  162. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/merge_runs.py +199 -0
  163. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
  164. package/opencode/skills/KORTIX-xlsx/scripts/office/pack.py +159 -0
  165. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  166. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  167. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  168. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  169. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  170. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  171. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  172. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  173. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  174. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  175. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  176. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  177. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  178. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  179. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  180. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  181. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  182. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  183. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  184. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  185. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  186. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  187. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  188. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  189. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  190. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  191. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  192. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  193. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  194. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  195. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  196. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
  197. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  198. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  199. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  200. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  201. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  202. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  203. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  204. package/opencode/skills/KORTIX-xlsx/scripts/office/soffice.py +183 -0
  205. package/opencode/skills/KORTIX-xlsx/scripts/office/unpack.py +132 -0
  206. package/opencode/skills/KORTIX-xlsx/scripts/office/validate.py +111 -0
  207. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/__init__.py +15 -0
  208. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/base.py +847 -0
  209. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/docx.py +446 -0
  210. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/pptx.py +275 -0
  211. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/redlining.py +247 -0
  212. package/opencode/skills/KORTIX-xlsx/scripts/recalc.py +184 -0
  213. package/opencode/tools/image-gen.ts +342 -0
  214. package/opencode/tools/image-search.ts +190 -0
  215. package/opencode/tools/memory-get.ts +168 -0
  216. package/opencode/tools/memory-search.ts +247 -0
  217. package/opencode/tools/presentation-gen.ts +723 -0
  218. package/opencode/tools/scrape-webpage.ts +115 -0
  219. package/opencode/tools/scripts/.python-version +1 -0
  220. package/opencode/tools/scripts/convert_pdf.py +184 -0
  221. package/opencode/tools/scripts/convert_pptx.py +562 -0
  222. package/opencode/tools/scripts/pyproject.toml +11 -0
  223. package/opencode/tools/scripts/uv.lock +287 -0
  224. package/opencode/tools/scripts/validate_slide.py +74 -0
  225. package/opencode/tools/show-user.ts +217 -0
  226. package/opencode/tools/tests/e2e-presentation-fix.ts +277 -0
  227. package/opencode/tools/tests/image-gen.test.ts +215 -0
  228. package/opencode/tools/tests/image-search.test.ts +125 -0
  229. package/opencode/tools/tests/memory-system-benchmark.ts +1076 -0
  230. package/opencode/tools/tests/presentation-gen.test.ts +389 -0
  231. package/opencode/tools/tests/scrape-webpage.test.ts +74 -0
  232. package/opencode/tools/tests/show-user.test.ts +241 -0
  233. package/opencode/tools/tests/video-gen.test.ts +110 -0
  234. package/opencode/tools/tests/web-search.test.ts +106 -0
  235. package/opencode/tools/video-gen.ts +200 -0
  236. package/opencode/tools/web-search.ts +153 -0
  237. package/opencode/tsconfig.json +29 -0
  238. package/package.json +36 -0
  239. package/patch-agent-browser.js +100 -0
  240. package/postinstall.sh +88 -0
  241. package/services/KORTIX-presentation-viewer/run +37 -0
  242. package/services/agent-browser-viewer/run +48 -0
  243. package/services/kortix-master/run +16 -0
  244. package/services/lss-sync/run +22 -0
  245. package/services/opencode-serve/run +25 -0
  246. package/services/opencode-web/run +21 -0
@@ -0,0 +1,562 @@
1
+ """Convert HTML presentation slides to PPTX with editable text.
2
+
3
+ Usage: uv run convert_pptx.py <presentation_dir> <output_path>
4
+
5
+ Three-layer PPTX per slide (matching Suna's approach):
6
+ 1. Clean background screenshot (text + visual elements hidden)
7
+ 2. Individual visual element screenshots (positioned exactly)
8
+ 3. Native editable text boxes (extracted via DOM inspection)
9
+ """
10
+
11
+ import asyncio
12
+ import json
13
+ import re
14
+ import sys
15
+ import tempfile
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+
19
+ from playwright.async_api import async_playwright
20
+ from pptx import Presentation
21
+ from pptx.util import Inches, Pt
22
+ from pptx.enum.text import PP_ALIGN
23
+ from pptx.dml.color import RGBColor
24
+
25
+
26
+ @dataclass
27
+ class TextElement:
28
+ """Extracted text element with position and styling."""
29
+ text: str
30
+ x: float
31
+ y: float
32
+ width: float
33
+ height: float
34
+ font_family: str
35
+ font_size: float
36
+ font_weight: str
37
+ color: str
38
+ text_align: str
39
+ line_height: float
40
+ tag: str
41
+ depth: int = 0
42
+ style: dict = field(default_factory=dict)
43
+
44
+
45
+ def parse_color(color_str: str) -> tuple[int, int, int]:
46
+ """Parse CSS color string to RGB tuple."""
47
+ if not color_str:
48
+ return (0, 0, 0)
49
+ color_str = color_str.strip().lower()
50
+
51
+ if color_str.startswith("#"):
52
+ h = color_str[1:]
53
+ if len(h) == 3:
54
+ h = "".join(c * 2 for c in h)
55
+ try:
56
+ return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
57
+ except ValueError:
58
+ return (0, 0, 0)
59
+
60
+ m = re.match(r"rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)", color_str)
61
+ if m:
62
+ return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
63
+
64
+ named = {
65
+ "black": (0, 0, 0), "white": (255, 255, 255), "red": (255, 0, 0),
66
+ "green": (0, 128, 0), "blue": (0, 0, 255), "yellow": (255, 255, 0),
67
+ "gray": (128, 128, 128), "grey": (128, 128, 128),
68
+ "orange": (255, 165, 0), "purple": (128, 0, 128),
69
+ }
70
+ return named.get(color_str, (0, 0, 0))
71
+
72
+
73
+ def is_bold(weight_str: str) -> bool:
74
+ """Parse CSS font-weight to bold boolean."""
75
+ if not weight_str:
76
+ return False
77
+ w = weight_str.strip().lower()
78
+ return w in ("bold", "bolder", "700", "800", "900") or (
79
+ w.isdigit() and int(w) >= 700
80
+ )
81
+
82
+
83
+ # ── JS snippets injected into Playwright ──
84
+
85
+ JS_EXTRACT_VISUAL_ELEMENTS = r"""
86
+ () => {
87
+ function hasVisual(el, cs) {
88
+ if (['IMG','SVG','CANVAS','VIDEO','IFRAME'].includes(el.tagName)) return true;
89
+ if (cs.backgroundImage && cs.backgroundImage !== 'none') return true;
90
+ const bg = cs.backgroundColor;
91
+ if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') return true;
92
+ if (cs.borderStyle && cs.borderStyle !== 'none' && cs.borderWidth !== '0px') return true;
93
+ if (cs.boxShadow && cs.boxShadow !== 'none') return true;
94
+ return false;
95
+ }
96
+ function skip(el, cs) {
97
+ const textOnly = ['H1','H2','H3','H4','H5','H6','P','A','SPAN','STRONG','EM','U','BUTTON','LABEL','SMALL','CODE'];
98
+ if (textOnly.includes(el.tagName)) return true;
99
+ if (cs.display === 'none' || cs.visibility === 'hidden') return true;
100
+ return false;
101
+ }
102
+ function walk(el, depth) {
103
+ if (!el || el.nodeType !== 1) return [];
104
+ const cs = getComputedStyle(el);
105
+ const r = el.getBoundingClientRect();
106
+ if (r.width === 0 || r.height === 0) return [];
107
+ if (skip(el, cs)) return [];
108
+ const out = [];
109
+ if (hasVisual(el, cs) && r.width <= 1700) {
110
+ const cid = 'vc-' + Date.now() + '-' + Math.random().toString(36).substr(2,6);
111
+ el.setAttribute('data-capture-id', cid);
112
+ out.push({type:'visual', captureId:cid, x:r.left, y:r.top, width:r.width, height:r.height, tag:el.tagName.toLowerCase(), depth});
113
+ }
114
+ for (const ch of el.children) out.push(...walk(ch, depth+1));
115
+ return out;
116
+ }
117
+ // make text transparent first
118
+ function hideText(el) {
119
+ if (el.nodeType !== 1) return;
120
+ if (el.textContent && el.textContent.trim()) {
121
+ el.style.color = 'transparent';
122
+ el.style.textShadow = 'none';
123
+ el.style.webkitTextFillColor = 'transparent';
124
+ }
125
+ for (const ch of el.children) hideText(ch);
126
+ }
127
+ hideText(document.body);
128
+ const elems = walk(document.body, 0);
129
+ elems.sort((a,b) => a.depth !== b.depth ? a.depth - b.depth : a.y - b.y);
130
+ return elems;
131
+ }
132
+ """
133
+
134
+ JS_EXTRACT_TEXT = r"""
135
+ () => {
136
+ function extract(el) {
137
+ if (!el || el.nodeType !== 1) return [];
138
+ const cs = getComputedStyle(el);
139
+ const r = el.getBoundingClientRect();
140
+ if (cs.display === 'none' || cs.visibility === 'hidden') return [];
141
+ if (r.width === 0 || r.height === 0) return [];
142
+ const out = [];
143
+ let dt = '';
144
+ for (const n of el.childNodes) { if (n.nodeType === 3) dt += n.textContent; }
145
+ dt = dt.trim();
146
+ if (dt) {
147
+ const fsm = cs.fontSize.match(/([0-9.]+)px/);
148
+ const fps = fsm ? parseFloat(fsm[1]) : 16;
149
+ out.push({
150
+ text: dt, x: r.left, y: r.top, width: r.width, height: r.height,
151
+ fps, tag: el.tagName.toLowerCase(),
152
+ style: {
153
+ fontFamily: cs.fontFamily, fontWeight: cs.fontWeight, color: cs.color,
154
+ textAlign: cs.textAlign, lineHeight: cs.lineHeight,
155
+ letterSpacing: cs.letterSpacing, textTransform: cs.textTransform,
156
+ webkitBackgroundClip: cs.webkitBackgroundClip,
157
+ backgroundImage: cs.backgroundImage
158
+ }
159
+ });
160
+ }
161
+ for (const ch of el.children) out.push(...extract(ch));
162
+ return out;
163
+ }
164
+ const all = extract(document.body);
165
+ all.sort((a,b) => Math.abs(a.y-b.y) < 5 ? a.x-b.x : a.y-b.y);
166
+ return all;
167
+ }
168
+ """
169
+
170
+
171
+ async def extract_visual_elements(page, html_path: Path, temp_dir: Path) -> list[dict]:
172
+ """Extract visual elements as positioned screenshots."""
173
+ elements = []
174
+ await page.set_viewport_size({"width": 1920, "height": 1080})
175
+ await page.emulate_media(media="screen")
176
+ await page.goto(f"file://{html_path.resolve()}", wait_until="networkidle", timeout=25000)
177
+ await page.wait_for_timeout(1000)
178
+
179
+ visual_data = await page.evaluate(JS_EXTRACT_VISUAL_ELEMENTS)
180
+
181
+ for i, d in enumerate(visual_data or []):
182
+ try:
183
+ x = max(0, min(d["x"], 1920))
184
+ y = max(0, min(d["y"], 1080))
185
+ w = min(d["width"], 1920 - x)
186
+ h = min(d["height"], 1080 - y)
187
+ if w < 5 or h < 5:
188
+ continue
189
+
190
+ clone_js = """
191
+ (data) => {
192
+ const el = document.querySelector(`[data-capture-id="${data.captureId}"]`);
193
+ if (!el) return {success:false};
194
+ const clone = el.cloneNode(true);
195
+ function cpStyles(orig, cl, root, psvg) {
196
+ const cs = getComputedStyle(orig);
197
+ let s = '';
198
+ for (const p of cs) s += p + ':' + cs.getPropertyValue(p) + ';';
199
+ cl.style.cssText = s;
200
+ if (!root && orig.tagName !== 'svg' && orig.tagName !== 'SVG' &&
201
+ orig.namespaceURI !== 'http://www.w3.org/2000/svg' && !psvg)
202
+ cl.style.opacity = '0';
203
+ const isSvg = orig.tagName === 'svg' || orig.tagName === 'SVG';
204
+ for (let j=0; j<orig.children.length && j<cl.children.length; j++)
205
+ cpStyles(orig.children[j], cl.children[j], false, isSvg||psvg);
206
+ }
207
+ cpStyles(el, clone, true, false);
208
+ const ctr = document.createElement('div');
209
+ ctr.id = 'cap-' + Date.now();
210
+ ctr.style.cssText = `position:fixed;top:0;left:0;width:${data.width}px;height:${data.height}px;background:transparent;z-index:999999;padding:0;margin:0;border:none;overflow:hidden;`;
211
+ clone.style.position = 'absolute';
212
+ clone.style.top = '0';
213
+ clone.style.left = '0';
214
+ clone.style.margin = '0';
215
+ clone.style.transform = 'none';
216
+ ctr.appendChild(clone);
217
+ document.body.appendChild(ctr);
218
+ return {success:true, containerId:ctr.id, rect:{x:0,y:0,width:data.width,height:data.height}};
219
+ }
220
+ """
221
+ result = await page.evaluate(clone_js, d)
222
+ if not result.get("success"):
223
+ continue
224
+
225
+ await page.wait_for_timeout(100)
226
+ cid = result["containerId"]
227
+
228
+ await page.evaluate(f"""() => {{
229
+ window._origHtml = document.documentElement.style.background || '';
230
+ window._origBody = document.body.style.background || '';
231
+ document.documentElement.style.background = 'transparent';
232
+ document.body.style.background = 'transparent';
233
+ document.body.style.visibility = 'hidden';
234
+ const c = document.getElementById('{cid}');
235
+ if (c) c.style.visibility = 'visible';
236
+ }}""")
237
+
238
+ img_path = temp_dir / f"ve_{html_path.stem}_{i:03d}.png"
239
+ cr = result["rect"]
240
+ await page.screenshot(
241
+ path=str(img_path), full_page=False, omit_background=True,
242
+ clip={"x": cr["x"], "y": cr["y"], "width": cr["width"], "height": cr["height"]},
243
+ )
244
+
245
+ await page.evaluate(f"""() => {{
246
+ document.documentElement.style.background = window._origHtml || '';
247
+ document.body.style.background = window._origBody || '';
248
+ document.body.style.visibility = 'visible';
249
+ const c = document.getElementById('{cid}');
250
+ if (c) c.remove();
251
+ }}""")
252
+
253
+ elements.append({
254
+ "x": d["x"], "y": d["y"], "width": d["width"], "height": d["height"],
255
+ "tag": d["tag"], "image_path": img_path, "depth": d["depth"],
256
+ })
257
+ except Exception:
258
+ try:
259
+ await page.evaluate("""() => {
260
+ document.documentElement.style.background = window._origHtml || '';
261
+ document.body.style.background = window._origBody || '';
262
+ document.body.style.visibility = 'visible';
263
+ document.querySelectorAll('[id^="cap-"]').forEach(c => c.remove());
264
+ }""")
265
+ except Exception:
266
+ pass
267
+
268
+ try:
269
+ await page.evaluate("""() => {
270
+ document.body.style.visibility = 'visible';
271
+ document.querySelectorAll('[data-capture-id]').forEach(e => e.removeAttribute('data-capture-id'));
272
+ document.querySelectorAll('[id^="cap-"]').forEach(c => c.remove());
273
+ }""")
274
+ except Exception:
275
+ pass
276
+
277
+ return elements
278
+
279
+
280
+ async def capture_background(page, html_path: Path, temp_dir: Path, visual_elements: list) -> Path:
281
+ """Screenshot the background layer (text + visual elements hidden)."""
282
+ try:
283
+ await page.set_viewport_size({"width": 1920, "height": 1080})
284
+ await page.emulate_media(media="screen")
285
+ await page.evaluate("() => { Object.defineProperty(window, 'devicePixelRatio', { get: () => 1 }); }")
286
+ await page.goto(f"file://{html_path.resolve()}", wait_until="networkidle", timeout=25000)
287
+ await page.wait_for_timeout(2000)
288
+
289
+ await page.evaluate("""(ves) => {
290
+ function hideText(el) {
291
+ if (el.nodeType !== 1) return;
292
+ if (el.textContent && el.textContent.trim()) {
293
+ el.style.color = 'transparent';
294
+ el.style.textShadow = 'none';
295
+ el.style.webkitTextFillColor = 'transparent';
296
+ }
297
+ for (const ch of el.children) hideText(ch);
298
+ }
299
+ hideText(document.body);
300
+ for (const v of ves) {
301
+ if (v.width > 1700) continue;
302
+ for (const el of document.querySelectorAll('*')) {
303
+ const r = el.getBoundingClientRect();
304
+ if (Math.abs(r.left-v.x)<5 && Math.abs(r.top-v.y)<5 &&
305
+ Math.abs(r.width-v.width)<5 && Math.abs(r.height-v.height)<5) {
306
+ el.style.visibility = 'hidden'; break;
307
+ }
308
+ }
309
+ }
310
+ }""", visual_elements or [])
311
+
312
+ await page.wait_for_timeout(500)
313
+ bg_path = temp_dir / f"bg_{html_path.stem}.png"
314
+ await page.screenshot(
315
+ path=str(bg_path), full_page=False,
316
+ clip={"x": 0, "y": 0, "width": 1920, "height": 1080},
317
+ )
318
+ return bg_path
319
+
320
+ except Exception:
321
+ from PIL import Image
322
+ bg_path = temp_dir / f"bg_{html_path.stem}.png"
323
+ Image.new("RGB", (1920, 1080), "white").save(bg_path)
324
+ return bg_path
325
+
326
+
327
+ async def extract_text(page, html_path: Path) -> list[TextElement]:
328
+ """Extract text elements with precise positions."""
329
+ elements = []
330
+ try:
331
+ await page.set_viewport_size({"width": 1920, "height": 1080})
332
+ await page.emulate_media(media="screen")
333
+ await page.evaluate("() => { Object.defineProperty(window, 'devicePixelRatio', { get: () => 1 }); }")
334
+ await page.goto(f"file://{html_path.resolve()}", wait_until="networkidle", timeout=25000)
335
+ await page.wait_for_timeout(2000)
336
+
337
+ data = await page.evaluate(JS_EXTRACT_TEXT)
338
+
339
+ for d in data or []:
340
+ if not d or not d.get("text"):
341
+ continue
342
+ style = d.get("style", {})
343
+ ff = (style.get("fontFamily") or "Arial").split(",")[0].strip().strip("\"'")
344
+ ff_map = {
345
+ "roboto": "Roboto", "arial": "Arial", "helvetica": "Helvetica",
346
+ "sans-serif": "Arial", "inter": "Inter",
347
+ }
348
+ ff = ff_map.get(ff.lower(), ff)
349
+
350
+ lh = 1.2
351
+ lhs = style.get("lineHeight", "normal")
352
+ if lhs and lhs != "normal":
353
+ if lhs.endswith("px"):
354
+ lh = float(lhs[:-2]) / d["fps"]
355
+ else:
356
+ try:
357
+ lh = float(lhs)
358
+ except ValueError:
359
+ lh = 1.2
360
+
361
+ color = style.get("color", "#000000")
362
+ if style.get("webkitBackgroundClip") == "text" and style.get("backgroundImage"):
363
+ m = re.search(r"#[0-9a-fA-F]{6}", style["backgroundImage"])
364
+ color = m.group(0) if m else "#3B82F6"
365
+
366
+ elements.append(TextElement(
367
+ text=d["text"], x=d["x"], y=d["y"], width=d["width"], height=d["height"],
368
+ font_family=ff, font_size=d["fps"] * 0.75, font_weight=style.get("fontWeight", "normal"),
369
+ color=color, text_align=style.get("textAlign", "left"), line_height=lh,
370
+ tag=d["tag"], style=style,
371
+ ))
372
+ except Exception:
373
+ pass
374
+ return elements
375
+
376
+
377
+ def add_text_box(slide, te: TextElement) -> None:
378
+ """Create a native editable PPTX text box."""
379
+ left = Inches(te.x / 96.0)
380
+ top = Inches(te.y / 96.0)
381
+ sf = max(1.0, te.font_size / 20.0)
382
+ tw = te.width
383
+ if tw < 100:
384
+ tw = max(tw, len(te.text) * (8 / sf))
385
+ pad = max(20, te.font_size * 0.5)
386
+ width = Inches((tw + pad * 2) / 96.0)
387
+ height = Inches(max(te.height, 10) / 96.0)
388
+
389
+ tb = slide.shapes.add_textbox(left, top, width, height)
390
+ tf = tb.text_frame
391
+ tf.clear()
392
+ tf.margin_left = tf.margin_right = tf.margin_top = tf.margin_bottom = Pt(0)
393
+ tf.word_wrap = True
394
+ tf.auto_size = None
395
+
396
+ p = tf.paragraphs[0]
397
+ text = te.text
398
+ if te.tag == "li" and not text.startswith("•"):
399
+ text = "• " + text
400
+ tt = te.style.get("textTransform", "none") if te.style else "none"
401
+ if tt == "uppercase":
402
+ text = text.upper()
403
+ elif tt == "lowercase":
404
+ text = text.lower()
405
+ elif tt == "capitalize":
406
+ text = text.title()
407
+ p.text = text
408
+
409
+ align_map = {
410
+ "left": PP_ALIGN.LEFT, "center": PP_ALIGN.CENTER, "right": PP_ALIGN.RIGHT,
411
+ "justify": PP_ALIGN.JUSTIFY, "start": PP_ALIGN.LEFT, "end": PP_ALIGN.RIGHT,
412
+ }
413
+ p.alignment = align_map.get(te.text_align.lower(), PP_ALIGN.LEFT)
414
+ p.space_before = p.space_after = Pt(0)
415
+
416
+ font = p.font
417
+ font.name = te.font_family
418
+ font.size = Pt(max(te.font_size, 8))
419
+ font.bold = is_bold(te.font_weight)
420
+
421
+ try:
422
+ r, g, b = parse_color(te.color)
423
+ font.color.rgb = RGBColor(r, g, b)
424
+ except Exception:
425
+ font.color.rgb = RGBColor(0, 0, 0)
426
+
427
+ tb.fill.background()
428
+ tb.line.fill.background()
429
+
430
+
431
+ async def process_slide(context, slide_info: dict, temp_dir: Path) -> dict:
432
+ """Process one slide: extract visuals, background, text."""
433
+ page = await context.new_page()
434
+ await page.set_viewport_size({"width": 1920, "height": 1080})
435
+ await page.emulate_media(media="screen")
436
+ await page.evaluate("() => { Object.defineProperty(window, 'devicePixelRatio', { get: () => 1 }); }")
437
+
438
+ try:
439
+ vis = await extract_visual_elements(page, slide_info["path"], temp_dir)
440
+ bg = await capture_background(page, slide_info["path"], temp_dir, vis)
441
+ txt = await extract_text(page, slide_info["path"])
442
+ return {"slide_info": slide_info, "visual_elements": vis, "background_path": bg, "text_elements": txt}
443
+ except Exception as e:
444
+ return {"slide_info": slide_info, "visual_elements": [], "background_path": None, "text_elements": [], "error": str(e)}
445
+ finally:
446
+ await page.close()
447
+
448
+
449
+ def build_slide(prs: Presentation, analysis: dict) -> None:
450
+ """Build one PPTX slide from analysis data."""
451
+ layout = prs.slide_layouts[6]
452
+ slide = prs.slides.add_slide(layout)
453
+
454
+ bg_path = analysis.get("background_path")
455
+ if bg_path and Path(bg_path).exists():
456
+ slide.shapes.add_picture(str(bg_path), Inches(0), Inches(0), Inches(20), Inches(11.25))
457
+
458
+ for ve in analysis.get("visual_elements", []):
459
+ ip = ve.get("image_path")
460
+ if ip and Path(ip).exists():
461
+ slide.shapes.add_picture(
462
+ str(ip),
463
+ Inches(ve["x"] / 96.0), Inches(ve["y"] / 96.0),
464
+ Inches(ve["width"] / 96.0), Inches(ve["height"] / 96.0),
465
+ )
466
+
467
+ for te in analysis.get("text_elements", []):
468
+ try:
469
+ add_text_box(slide, te)
470
+ except Exception:
471
+ pass
472
+
473
+
474
+ async def convert(presentation_dir: str, output_path: str) -> dict:
475
+ """Main conversion entrypoint."""
476
+ pres_dir = Path(presentation_dir).resolve()
477
+ meta_path = pres_dir / "metadata.json"
478
+
479
+ if not pres_dir.exists():
480
+ return {"success": False, "error": f"Directory not found: {pres_dir}"}
481
+ if not meta_path.exists():
482
+ return {"success": False, "error": f"metadata.json not found in {pres_dir}"}
483
+
484
+ with open(meta_path) as f:
485
+ metadata = json.load(f)
486
+
487
+ slides_info = []
488
+ for num_str, data in metadata.get("slides", {}).items():
489
+ fp = data.get("file_path", "")
490
+ html_path = (pres_dir.parent.parent / fp) if fp else None
491
+ if html_path and html_path.exists():
492
+ slides_info.append({
493
+ "number": int(num_str),
494
+ "title": data.get("title", f"Slide {num_str}"),
495
+ "path": html_path,
496
+ })
497
+
498
+ if not slides_info:
499
+ return {"success": False, "error": "No valid slides found"}
500
+ slides_info.sort(key=lambda s: s["number"])
501
+
502
+ with tempfile.TemporaryDirectory() as tmp:
503
+ tmp_path = Path(tmp)
504
+
505
+ async with async_playwright() as p:
506
+ browser = await p.chromium.launch(
507
+ executable_path="/Users/markokraemer/Library/Caches/ms-playwright/chromium-1208/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
508
+ headless=True,
509
+ args=[
510
+ "--no-sandbox", "--disable-setuid-sandbox",
511
+ "--disable-dev-shm-usage", "--disable-gpu",
512
+ "--force-device-scale-factor=1",
513
+ ],
514
+ )
515
+ try:
516
+ ctx = await browser.new_context(viewport={"width": 1920, "height": 1080})
517
+ sem = asyncio.Semaphore(5)
518
+
519
+ async def limited(si):
520
+ async with sem:
521
+ return await process_slide(ctx, si, tmp_path)
522
+
523
+ analyses = await asyncio.gather(*[limited(si) for si in slides_info], return_exceptions=True)
524
+ finally:
525
+ await browser.close()
526
+
527
+ prs = Presentation()
528
+ prs.slide_width = Inches(20)
529
+ prs.slide_height = Inches(11.25)
530
+
531
+ for i, a in enumerate(analyses):
532
+ if isinstance(a, Exception):
533
+ a = {"slide_info": slides_info[i], "visual_elements": [], "background_path": None, "text_elements": [], "error": str(a)}
534
+ if a.get("error"):
535
+ layout = prs.slide_layouts[6]
536
+ slide = prs.slides.add_slide(layout)
537
+ tb = slide.shapes.add_textbox(Inches(1), Inches(4), Inches(18), Inches(2))
538
+ tb.text_frame.paragraphs[0].text = f"Error: {a['error']}"
539
+ tb.text_frame.paragraphs[0].font.size = Pt(18)
540
+ tb.text_frame.paragraphs[0].font.color.rgb = RGBColor(255, 0, 0)
541
+ else:
542
+ build_slide(prs, a)
543
+
544
+ out = Path(output_path)
545
+ out.parent.mkdir(parents=True, exist_ok=True)
546
+ prs.save(str(out))
547
+
548
+ return {"success": True, "output_path": str(out), "total_slides": len(slides_info)}
549
+
550
+
551
+ def main():
552
+ if len(sys.argv) != 3:
553
+ print(json.dumps({"success": False, "error": "Usage: convert_pptx.py <presentation_dir> <output_path>"}))
554
+ sys.exit(1)
555
+
556
+ result = asyncio.run(convert(sys.argv[1], sys.argv[2]))
557
+ print(json.dumps(result))
558
+ sys.exit(0 if result["success"] else 1)
559
+
560
+
561
+ if __name__ == "__main__":
562
+ main()
@@ -0,0 +1,11 @@
1
+ [project]
2
+ name = "scripts"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ requires-python = ">=3.12"
6
+ dependencies = [
7
+ "pillow>=12.1.0",
8
+ "playwright>=1.58.0",
9
+ "pypdf2>=3.0.1",
10
+ "python-pptx>=1.0.2",
11
+ ]