geny-svgforge 0.1.0__tar.gz
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.
- geny_svgforge-0.1.0/.github/workflows/workflow.yml +49 -0
- geny_svgforge-0.1.0/.gitignore +22 -0
- geny_svgforge-0.1.0/LICENSE +21 -0
- geny_svgforge-0.1.0/PKG-INFO +178 -0
- geny_svgforge-0.1.0/README.md +145 -0
- geny_svgforge-0.1.0/examples/render_demo.py +28 -0
- geny_svgforge-0.1.0/examples/token_sequence_positions.json +38 -0
- geny_svgforge-0.1.0/pyproject.toml +46 -0
- geny_svgforge-0.1.0/src/geny_svgforge/__init__.py +25 -0
- geny_svgforge-0.1.0/src/geny_svgforge/api.py +103 -0
- geny_svgforge-0.1.0/src/geny_svgforge/cli.py +67 -0
- geny_svgforge-0.1.0/src/geny_svgforge/fonts.py +157 -0
- geny_svgforge-0.1.0/src/geny_svgforge/geometry.py +110 -0
- geny_svgforge-0.1.0/src/geny_svgforge/layout.py +218 -0
- geny_svgforge-0.1.0/src/geny_svgforge/lint.py +39 -0
- geny_svgforge-0.1.0/src/geny_svgforge/mcp_server.py +47 -0
- geny_svgforge-0.1.0/src/geny_svgforge/render.py +42 -0
- geny_svgforge-0.1.0/src/geny_svgforge/spec.py +76 -0
- geny_svgforge-0.1.0/src/geny_svgforge/themes.py +49 -0
- geny_svgforge-0.1.0/tests/test_core.py +63 -0
- geny_svgforge-0.1.0/uv.lock +1249 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# PyPI Trusted Publishing (OIDC). Configured publisher:
|
|
4
|
+
# project: geny-svgforge · owner: CocoRoF · repo: geny-svgforge
|
|
5
|
+
# workflow: workflow.yml · environment: pypi
|
|
6
|
+
# A published GitHub Release (or manual dispatch) builds and uploads to PyPI.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
release:
|
|
10
|
+
types: [published]
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
build:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.12"
|
|
24
|
+
- name: Build sdist and wheel
|
|
25
|
+
run: |
|
|
26
|
+
python -m pip install --upgrade build
|
|
27
|
+
python -m build
|
|
28
|
+
- name: Smoke-check metadata
|
|
29
|
+
run: |
|
|
30
|
+
python -m pip install --upgrade twine
|
|
31
|
+
python -m twine check dist/*
|
|
32
|
+
- uses: actions/upload-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
|
|
37
|
+
publish:
|
|
38
|
+
needs: build
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
environment: pypi
|
|
41
|
+
permissions:
|
|
42
|
+
id-token: write # OIDC token for trusted publishing
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/download-artifact@v4
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/
|
|
48
|
+
- name: Publish to PyPI
|
|
49
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.venv/
|
|
9
|
+
.uv/
|
|
10
|
+
|
|
11
|
+
# caches
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.mypy_cache/
|
|
15
|
+
|
|
16
|
+
# generated demo output
|
|
17
|
+
examples/out.svg
|
|
18
|
+
examples/out.png
|
|
19
|
+
examples/probe.png
|
|
20
|
+
|
|
21
|
+
# OS
|
|
22
|
+
.DS_Store
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jang HaRyeom (CocoRoF)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: geny-svgforge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI가 좌표 대신 의미(spec)만 내면 결정론적 레이아웃으로 깨끗한 다이어그램 SVG를 만드는 라이브러리
|
|
5
|
+
Project-URL: Homepage, https://github.com/CocoRoF/geny-svgforge
|
|
6
|
+
Project-URL: Repository, https://github.com/CocoRoF/geny-svgforge
|
|
7
|
+
Project-URL: Issues, https://github.com/CocoRoF/geny-svgforge/issues
|
|
8
|
+
Author: Jang HaRyeom (CocoRoF)
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,diagram,layout,llm,mcp,svg
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
21
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: fonttools>=4.40
|
|
24
|
+
Requires-Dist: pydantic>=2.5
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: cairosvg>=2.7; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
28
|
+
Provides-Extra: mcp
|
|
29
|
+
Requires-Dist: mcp>=1.2; extra == 'mcp'
|
|
30
|
+
Provides-Extra: png
|
|
31
|
+
Requires-Dist: cairosvg>=2.7; extra == 'png'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# geny-svgforge
|
|
35
|
+
|
|
36
|
+
**Give an AI a semantic spec instead of raw coordinates, and a deterministic layout engine produces a clean diagram SVG with zero overlaps or clipping.**
|
|
37
|
+
|
|
38
|
+
When an LLM writes `<svg>` by hand, it can't reliably reason about text widths, box bounds, curve paths, or the viewBox — so elements overlap and captions get clipped. geny-svgforge inserts a layout layer between the AI and the SVG. The AI emits only a JSON **spec** (*"two rows of labeled tokens, posN labels, connect A's token 2 to B's token 3 with a curve, a side note, a caption"*) — no coordinates. The library measures text with real font metrics, sizes every box, routes connectors around obstacles, and fits the viewBox to the content. **Overlap and overflow become structurally impossible.**
|
|
39
|
+
|
|
40
|
+
> Same lineage as edit2ppt (AI emits PPT structure, engine renders) and Contextifier (structure-preserving document parsing).
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install geny-svgforge # core (SVG)
|
|
48
|
+
pip install 'geny-svgforge[png]' # + PNG export (cairosvg)
|
|
49
|
+
pip install 'geny-svgforge[mcp]' # + MCP server
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quickstart (Python)
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from geny_svgforge import render
|
|
56
|
+
|
|
57
|
+
spec = {
|
|
58
|
+
"type": "token-sequence",
|
|
59
|
+
"title": "Absolute position changes, relative pattern remains",
|
|
60
|
+
"rows": [
|
|
61
|
+
{"label": "sentence A", "tokens": [
|
|
62
|
+
{"text": "나는", "pos": "pos 0"},
|
|
63
|
+
{"text": "오늘", "pos": "pos 1"},
|
|
64
|
+
{"text": "밥을", "pos": "pos 2", "id": "a2"},
|
|
65
|
+
{"text": "먹었다", "pos": "pos 3", "variant": "highlight"},
|
|
66
|
+
]},
|
|
67
|
+
{"label": "sentence B", "tokens": [
|
|
68
|
+
{"text": "나는", "pos": "pos 0"},
|
|
69
|
+
{"text": "정말", "pos": "pos 1", "variant": "accent"},
|
|
70
|
+
{"text": "오늘", "pos": "pos 2"},
|
|
71
|
+
{"text": "밥을", "pos": "pos 3", "id": "b3"},
|
|
72
|
+
{"text": "먹었다", "pos": "pos 4", "variant": "highlight"},
|
|
73
|
+
]},
|
|
74
|
+
],
|
|
75
|
+
"connectors": [{"from": "a2", "to": "b3", "color": "accent"}],
|
|
76
|
+
"note": {"title": "What the model must learn",
|
|
77
|
+
"lines": ["Memorizing absolute positions is brittle to length changes.",
|
|
78
|
+
"Relative distance and surrounding patterns are handled in attention."]},
|
|
79
|
+
"caption": "Same relative token relationship survives an absolute shift",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
result = render(spec) # portable SVG with the used glyphs embedded
|
|
83
|
+
print(result.warnings) # [] ← no overlap / no clipping (lint passed)
|
|
84
|
+
open("out.svg", "w").write(result.svg)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## CLI
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
geny-svgforge render spec.json -o out.svg
|
|
91
|
+
geny-svgforge render spec.json -o out.png # PNG (requires [png])
|
|
92
|
+
geny-svgforge validate spec.json # validate before rendering
|
|
93
|
+
geny-svgforge schema -o schema.json # dump the JSON Schema
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## MCP server
|
|
97
|
+
|
|
98
|
+
geny-svgforge ships an [MCP](https://modelcontextprotocol.io) server over **stdio** so any MCP-compatible agent can request diagrams. After `pip install 'geny-svgforge[mcp]'` the server is launched with:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
geny-svgforge-mcp # console script
|
|
102
|
+
# or
|
|
103
|
+
python -m geny_svgforge.mcp_server
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Tools exposed
|
|
107
|
+
|
|
108
|
+
| Tool | Input | Returns |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `get_diagram_schema` | – | JSON Schema describing the spec (the agent learns the format from this) |
|
|
111
|
+
| `validate_diagram_spec` | `spec` | `{ ok, errors[], warnings[] }` — check before rendering |
|
|
112
|
+
| `render_diagram` | `spec` | `{ svg, width, height, warnings[] }` — if `warnings` is non-empty, fix the spec and call again |
|
|
113
|
+
|
|
114
|
+
### Client configuration
|
|
115
|
+
|
|
116
|
+
Add the server to your MCP client config. The standard shape is an `mcpServers` map keyed by a server name.
|
|
117
|
+
|
|
118
|
+
**Claude Desktop** (`claude_desktop_config.json`), **Cursor** (`~/.cursor/mcp.json`), or **Claude Code** (`.mcp.json`):
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"mcpServers": {
|
|
123
|
+
"geny-svgforge": {
|
|
124
|
+
"command": "geny-svgforge-mcp"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Zero-install with [uv](https://docs.astral.sh/uv/) (no prior `pip install` needed):
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"mcpServers": {
|
|
135
|
+
"geny-svgforge": {
|
|
136
|
+
"command": "uvx",
|
|
137
|
+
"args": ["--from", "geny-svgforge[mcp]", "geny-svgforge-mcp"]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Claude Code can also add it from the CLI:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
claude mcp add geny-svgforge -- uvx --from 'geny-svgforge[mcp]' geny-svgforge-mcp
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
A typical agent flow: call `get_diagram_schema` once to learn the format → emit a `spec` → call `render_diagram` → if `warnings` is non-empty, repair the spec and retry.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## How it works
|
|
154
|
+
|
|
155
|
+
Three layers: **Spec (JSON Schema) → Layout Engine → Renderer**.
|
|
156
|
+
|
|
157
|
+
- **Real font metrics** — text width is computed in pixels by summing glyph advances via `fontTools` (no browser, no headless engine). The same font used for measurement is embedded into the SVG, so **measured layout == rendered output**.
|
|
158
|
+
- **Deterministic layout** — boxes are sized to their text, connectors are routed through the inter-row band away from boxes, and the viewBox/padding is derived from the bounding box of every element — so nothing can clip.
|
|
159
|
+
- **Font embedding** — only the glyphs actually used are subset and inlined as a base64 `@font-face`, so the SVG renders identically everywhere (browsers, resvg). `to_png()` renders via the installed font (`raster_safe`) because cairosvg ignores embedded `@font-face`.
|
|
160
|
+
- **Lint** — a post-layout pass flags box overlaps and canvas overflow. It should always be empty; if not, the warnings are returned to the agent so it can fix the spec.
|
|
161
|
+
|
|
162
|
+
## Diagram types
|
|
163
|
+
|
|
164
|
+
| Type | Description |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `token-sequence` | Rows of position-labeled token boxes, with inter-row connectors, a side note, and a caption |
|
|
167
|
+
|
|
168
|
+
(`flow`, `grid`, `stack`, `callout`, … are planned — the spec is extensible via the `type` field.)
|
|
169
|
+
|
|
170
|
+
## Roadmap
|
|
171
|
+
|
|
172
|
+
- Automatic collision resolution (constraint / force based)
|
|
173
|
+
- Visual self-repair loop: render → rasterize → multimodal critique → fix spec → re-render
|
|
174
|
+
- More diagram types, themes, templates, accessibility (`<title>`/`<desc>`/aria)
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT. Text is measured against — and a subset is embedded from — a system font (e.g. SIL OFL Noto Sans CJK). When embedding, the embedded font's own license applies.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# geny-svgforge
|
|
2
|
+
|
|
3
|
+
**Give an AI a semantic spec instead of raw coordinates, and a deterministic layout engine produces a clean diagram SVG with zero overlaps or clipping.**
|
|
4
|
+
|
|
5
|
+
When an LLM writes `<svg>` by hand, it can't reliably reason about text widths, box bounds, curve paths, or the viewBox — so elements overlap and captions get clipped. geny-svgforge inserts a layout layer between the AI and the SVG. The AI emits only a JSON **spec** (*"two rows of labeled tokens, posN labels, connect A's token 2 to B's token 3 with a curve, a side note, a caption"*) — no coordinates. The library measures text with real font metrics, sizes every box, routes connectors around obstacles, and fits the viewBox to the content. **Overlap and overflow become structurally impossible.**
|
|
6
|
+
|
|
7
|
+
> Same lineage as edit2ppt (AI emits PPT structure, engine renders) and Contextifier (structure-preserving document parsing).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install geny-svgforge # core (SVG)
|
|
15
|
+
pip install 'geny-svgforge[png]' # + PNG export (cairosvg)
|
|
16
|
+
pip install 'geny-svgforge[mcp]' # + MCP server
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quickstart (Python)
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from geny_svgforge import render
|
|
23
|
+
|
|
24
|
+
spec = {
|
|
25
|
+
"type": "token-sequence",
|
|
26
|
+
"title": "Absolute position changes, relative pattern remains",
|
|
27
|
+
"rows": [
|
|
28
|
+
{"label": "sentence A", "tokens": [
|
|
29
|
+
{"text": "나는", "pos": "pos 0"},
|
|
30
|
+
{"text": "오늘", "pos": "pos 1"},
|
|
31
|
+
{"text": "밥을", "pos": "pos 2", "id": "a2"},
|
|
32
|
+
{"text": "먹었다", "pos": "pos 3", "variant": "highlight"},
|
|
33
|
+
]},
|
|
34
|
+
{"label": "sentence B", "tokens": [
|
|
35
|
+
{"text": "나는", "pos": "pos 0"},
|
|
36
|
+
{"text": "정말", "pos": "pos 1", "variant": "accent"},
|
|
37
|
+
{"text": "오늘", "pos": "pos 2"},
|
|
38
|
+
{"text": "밥을", "pos": "pos 3", "id": "b3"},
|
|
39
|
+
{"text": "먹었다", "pos": "pos 4", "variant": "highlight"},
|
|
40
|
+
]},
|
|
41
|
+
],
|
|
42
|
+
"connectors": [{"from": "a2", "to": "b3", "color": "accent"}],
|
|
43
|
+
"note": {"title": "What the model must learn",
|
|
44
|
+
"lines": ["Memorizing absolute positions is brittle to length changes.",
|
|
45
|
+
"Relative distance and surrounding patterns are handled in attention."]},
|
|
46
|
+
"caption": "Same relative token relationship survives an absolute shift",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
result = render(spec) # portable SVG with the used glyphs embedded
|
|
50
|
+
print(result.warnings) # [] ← no overlap / no clipping (lint passed)
|
|
51
|
+
open("out.svg", "w").write(result.svg)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## CLI
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
geny-svgforge render spec.json -o out.svg
|
|
58
|
+
geny-svgforge render spec.json -o out.png # PNG (requires [png])
|
|
59
|
+
geny-svgforge validate spec.json # validate before rendering
|
|
60
|
+
geny-svgforge schema -o schema.json # dump the JSON Schema
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## MCP server
|
|
64
|
+
|
|
65
|
+
geny-svgforge ships an [MCP](https://modelcontextprotocol.io) server over **stdio** so any MCP-compatible agent can request diagrams. After `pip install 'geny-svgforge[mcp]'` the server is launched with:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
geny-svgforge-mcp # console script
|
|
69
|
+
# or
|
|
70
|
+
python -m geny_svgforge.mcp_server
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Tools exposed
|
|
74
|
+
|
|
75
|
+
| Tool | Input | Returns |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `get_diagram_schema` | – | JSON Schema describing the spec (the agent learns the format from this) |
|
|
78
|
+
| `validate_diagram_spec` | `spec` | `{ ok, errors[], warnings[] }` — check before rendering |
|
|
79
|
+
| `render_diagram` | `spec` | `{ svg, width, height, warnings[] }` — if `warnings` is non-empty, fix the spec and call again |
|
|
80
|
+
|
|
81
|
+
### Client configuration
|
|
82
|
+
|
|
83
|
+
Add the server to your MCP client config. The standard shape is an `mcpServers` map keyed by a server name.
|
|
84
|
+
|
|
85
|
+
**Claude Desktop** (`claude_desktop_config.json`), **Cursor** (`~/.cursor/mcp.json`), or **Claude Code** (`.mcp.json`):
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"geny-svgforge": {
|
|
91
|
+
"command": "geny-svgforge-mcp"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Zero-install with [uv](https://docs.astral.sh/uv/) (no prior `pip install` needed):
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"mcpServers": {
|
|
102
|
+
"geny-svgforge": {
|
|
103
|
+
"command": "uvx",
|
|
104
|
+
"args": ["--from", "geny-svgforge[mcp]", "geny-svgforge-mcp"]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Claude Code can also add it from the CLI:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
claude mcp add geny-svgforge -- uvx --from 'geny-svgforge[mcp]' geny-svgforge-mcp
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
A typical agent flow: call `get_diagram_schema` once to learn the format → emit a `spec` → call `render_diagram` → if `warnings` is non-empty, repair the spec and retry.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## How it works
|
|
121
|
+
|
|
122
|
+
Three layers: **Spec (JSON Schema) → Layout Engine → Renderer**.
|
|
123
|
+
|
|
124
|
+
- **Real font metrics** — text width is computed in pixels by summing glyph advances via `fontTools` (no browser, no headless engine). The same font used for measurement is embedded into the SVG, so **measured layout == rendered output**.
|
|
125
|
+
- **Deterministic layout** — boxes are sized to their text, connectors are routed through the inter-row band away from boxes, and the viewBox/padding is derived from the bounding box of every element — so nothing can clip.
|
|
126
|
+
- **Font embedding** — only the glyphs actually used are subset and inlined as a base64 `@font-face`, so the SVG renders identically everywhere (browsers, resvg). `to_png()` renders via the installed font (`raster_safe`) because cairosvg ignores embedded `@font-face`.
|
|
127
|
+
- **Lint** — a post-layout pass flags box overlaps and canvas overflow. It should always be empty; if not, the warnings are returned to the agent so it can fix the spec.
|
|
128
|
+
|
|
129
|
+
## Diagram types
|
|
130
|
+
|
|
131
|
+
| Type | Description |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `token-sequence` | Rows of position-labeled token boxes, with inter-row connectors, a side note, and a caption |
|
|
134
|
+
|
|
135
|
+
(`flow`, `grid`, `stack`, `callout`, … are planned — the spec is extensible via the `type` field.)
|
|
136
|
+
|
|
137
|
+
## Roadmap
|
|
138
|
+
|
|
139
|
+
- Automatic collision resolution (constraint / force based)
|
|
140
|
+
- Visual self-repair loop: render → rasterize → multimodal critique → fix spec → re-render
|
|
141
|
+
- More diagram types, themes, templates, accessibility (`<title>`/`<desc>`/aria)
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT. Text is measured against — and a subset is embedded from — a system font (e.g. SIL OFL Noto Sans CJK). When embedding, the embedded font's own license applies.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""예시 spec 을 깨끗한 SVG 로 렌더하고(PNG 도) 린트 결과를 출력."""
|
|
2
|
+
import json
|
|
3
|
+
import pathlib
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from geny_svgforge import render
|
|
7
|
+
from geny_svgforge.api import to_png
|
|
8
|
+
|
|
9
|
+
HERE = pathlib.Path(__file__).parent
|
|
10
|
+
spec_path = HERE / (sys.argv[1] if len(sys.argv) > 1 else "token_sequence_positions.json")
|
|
11
|
+
spec = json.loads(spec_path.read_text(encoding="utf-8"))
|
|
12
|
+
|
|
13
|
+
# 프로덕션 SVG (폰트 임베드 — 브라우저/resvg 에서 완전 이식)
|
|
14
|
+
result = render(spec)
|
|
15
|
+
out_svg = HERE / "out.svg"
|
|
16
|
+
out_svg.write_text(result.svg, encoding="utf-8")
|
|
17
|
+
print(f"canvas: {result.width:.0f} x {result.height:.0f}")
|
|
18
|
+
print(f"lint warnings ({len(result.warnings)}):")
|
|
19
|
+
for w in result.warnings:
|
|
20
|
+
print(" -", w)
|
|
21
|
+
print(f"wrote {out_svg} ({len(result.svg)} bytes)")
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
out_png = HERE / "out.png"
|
|
25
|
+
out_png.write_bytes(to_png(spec, scale=2.0))
|
|
26
|
+
print(f"wrote {out_png}")
|
|
27
|
+
except Exception as e: # noqa: BLE001
|
|
28
|
+
print(f"(png skip: {e})")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "token-sequence",
|
|
3
|
+
"title": "Absolute position changes, relative pattern remains",
|
|
4
|
+
"subtitle": "중간에 토큰이 추가되면 뒤쪽 토큰의 절대 위치는 밀린다. 모델은 이런 이동에도 유지되는 관계를 배워야 한다.",
|
|
5
|
+
"theme": "light",
|
|
6
|
+
"rows": [
|
|
7
|
+
{
|
|
8
|
+
"label": "sentence A",
|
|
9
|
+
"tokens": [
|
|
10
|
+
{ "text": "나는", "pos": "pos 0" },
|
|
11
|
+
{ "text": "오늘", "pos": "pos 1" },
|
|
12
|
+
{ "text": "밥을", "pos": "pos 2", "id": "a2" },
|
|
13
|
+
{ "text": "먹었다", "pos": "pos 3", "id": "a3", "variant": "highlight" }
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"label": "sentence B",
|
|
18
|
+
"tokens": [
|
|
19
|
+
{ "text": "나는", "pos": "pos 0" },
|
|
20
|
+
{ "text": "정말", "pos": "pos 1", "variant": "accent" },
|
|
21
|
+
{ "text": "오늘", "pos": "pos 2" },
|
|
22
|
+
{ "text": "밥을", "pos": "pos 3", "id": "b3" },
|
|
23
|
+
{ "text": "먹었다", "pos": "pos 4", "id": "b4", "variant": "highlight" }
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"connectors": [
|
|
28
|
+
{ "from": "a2", "to": "b3", "style": "arc", "color": "accent" }
|
|
29
|
+
],
|
|
30
|
+
"note": {
|
|
31
|
+
"title": "모델이 배워야 하는 것",
|
|
32
|
+
"lines": [
|
|
33
|
+
"절대 position만 외우면 문장 길이 변화에 약하다.",
|
|
34
|
+
"상대 거리와 주변 패턴을 attention에서 같이 다룬다."
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"caption": "절대 위치가 바뀌어도 상대 토큰 관계가 유지되는 문장 예시"
|
|
38
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "geny-svgforge"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "AI가 좌표 대신 의미(spec)만 내면 결정론적 레이아웃으로 깨끗한 다이어그램 SVG를 만드는 라이브러리"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Jang HaRyeom (CocoRoF)" }]
|
|
13
|
+
keywords = ["svg", "diagram", "ai", "mcp", "layout", "llm"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Multimedia :: Graphics",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
24
|
+
"Topic :: Text Processing :: Markup",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"pydantic>=2.5",
|
|
28
|
+
"fonttools>=4.40",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/CocoRoF/geny-svgforge"
|
|
33
|
+
Repository = "https://github.com/CocoRoF/geny-svgforge"
|
|
34
|
+
Issues = "https://github.com/CocoRoF/geny-svgforge/issues"
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
png = ["cairosvg>=2.7"]
|
|
38
|
+
mcp = ["mcp>=1.2"]
|
|
39
|
+
dev = ["pytest>=8", "cairosvg>=2.7"]
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
geny-svgforge = "geny_svgforge.cli:main"
|
|
43
|
+
geny-svgforge-mcp = "geny_svgforge.mcp_server:main"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/geny_svgforge"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""geny-svgforge — AI가 의미(spec)만 내면 결정론적 레이아웃으로 깨끗한 다이어그램 SVG를 만든다."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .api import RenderResult, render, validate_spec
|
|
5
|
+
from .spec import (
|
|
6
|
+
Connector,
|
|
7
|
+
Note,
|
|
8
|
+
Row,
|
|
9
|
+
Token,
|
|
10
|
+
TokenSequenceSpec,
|
|
11
|
+
json_schema,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
__all__ = [
|
|
16
|
+
"render",
|
|
17
|
+
"validate_spec",
|
|
18
|
+
"RenderResult",
|
|
19
|
+
"json_schema",
|
|
20
|
+
"TokenSequenceSpec",
|
|
21
|
+
"Row",
|
|
22
|
+
"Token",
|
|
23
|
+
"Connector",
|
|
24
|
+
"Note",
|
|
25
|
+
]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""공개 API. AI 에이전트/서버/CLI 가 쓰는 진입점."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any, Union
|
|
6
|
+
|
|
7
|
+
from .fonts import EMBED_FAMILY, FONT_FAMILY, default_fonts, subset_b64
|
|
8
|
+
from .geometry import TextEl
|
|
9
|
+
from .layout import build_scene
|
|
10
|
+
from .lint import lint
|
|
11
|
+
from .render import render_svg
|
|
12
|
+
from .spec import TokenSequenceSpec
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class RenderResult:
|
|
17
|
+
svg: str
|
|
18
|
+
width: float
|
|
19
|
+
height: float
|
|
20
|
+
warnings: list[str] = field(default_factory=list)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _coerce(spec: Union[dict, TokenSequenceSpec]) -> TokenSequenceSpec:
|
|
24
|
+
if isinstance(spec, TokenSequenceSpec):
|
|
25
|
+
return spec
|
|
26
|
+
return TokenSequenceSpec.model_validate(spec)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _embed_css(scene) -> tuple[str, str]:
|
|
30
|
+
"""사용된 글자만 subset 해 base64 @font-face 로 임베드.
|
|
31
|
+
|
|
32
|
+
측정에 쓴 폰트를 그대로 임베드하므로 어떤 렌더러에서도 측정==렌더가 성립한다(완전 이식성).
|
|
33
|
+
반환: (font_faces_css, font_family).
|
|
34
|
+
"""
|
|
35
|
+
reg, bold = default_fonts()
|
|
36
|
+
reg_chars: set[str] = set()
|
|
37
|
+
bold_chars: set[str] = set()
|
|
38
|
+
for el in scene.elements:
|
|
39
|
+
if isinstance(el, TextEl):
|
|
40
|
+
(bold_chars if el.weight == "bold" else reg_chars).update(el.text)
|
|
41
|
+
css = ""
|
|
42
|
+
if reg_chars:
|
|
43
|
+
b = subset_b64(reg.path, reg.ttc_index, reg_chars)
|
|
44
|
+
if b:
|
|
45
|
+
css += (f"@font-face{{font-family:'{EMBED_FAMILY}';font-weight:400;"
|
|
46
|
+
f"src:url(data:font/ttf;base64,{b}) format('truetype');}}")
|
|
47
|
+
if bold_chars:
|
|
48
|
+
b = subset_b64(bold.path, bold.ttc_index, bold_chars)
|
|
49
|
+
if b:
|
|
50
|
+
css += (f"@font-face{{font-family:'{EMBED_FAMILY}';font-weight:700;"
|
|
51
|
+
f"src:url(data:font/ttf;base64,{b}) format('truetype');}}")
|
|
52
|
+
real = default_fonts()[0].family_name
|
|
53
|
+
family = f"'{EMBED_FAMILY}', '{real}', {FONT_FAMILY}" if css else FONT_FAMILY
|
|
54
|
+
return css, family
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def render(
|
|
58
|
+
spec: Union[dict, TokenSequenceSpec],
|
|
59
|
+
embed_font: bool = True,
|
|
60
|
+
raster_safe: bool = False,
|
|
61
|
+
) -> RenderResult:
|
|
62
|
+
"""spec(dict or model) -> 깨끗한 SVG + 린트 경고.
|
|
63
|
+
|
|
64
|
+
잘못된 spec 은 pydantic ValidationError 를 던진다(렌더 전 차단).
|
|
65
|
+
- embed_font=True: 사용된 글자를 subset 해 @font-face 로 임베드(브라우저·resvg 에서 완전 이식).
|
|
66
|
+
- raster_safe=True: 임베드 대신 실제 설치 폰트 family 를 우선 지정(cairosvg 등 @font-face 미지원
|
|
67
|
+
래스터라이저에서 CJK 가 깨지지 않게). PNG 내보내기에 사용.
|
|
68
|
+
"""
|
|
69
|
+
model = _coerce(spec)
|
|
70
|
+
scene = build_scene(model)
|
|
71
|
+
warnings = lint(scene)
|
|
72
|
+
if raster_safe:
|
|
73
|
+
real = default_fonts()[0].family_name
|
|
74
|
+
svg = render_svg(scene, font_family=f"'{real}', {FONT_FAMILY}")
|
|
75
|
+
elif embed_font:
|
|
76
|
+
css, family = _embed_css(scene)
|
|
77
|
+
svg = render_svg(scene, font_family=family, font_faces_css=css)
|
|
78
|
+
else:
|
|
79
|
+
svg = render_svg(scene)
|
|
80
|
+
return RenderResult(svg=svg, width=scene.width, height=scene.height, warnings=warnings)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def to_png(spec: Union[dict, TokenSequenceSpec], scale: float = 2.0) -> bytes:
|
|
84
|
+
"""PNG 바이트. cairosvg 필요(optional dep 'png'). @font-face 미지원 래스터라이저를 위해
|
|
85
|
+
실제 설치 폰트로 렌더한다(설치 폰트가 없으면 resvg + 임베드 SVG 사용 권장)."""
|
|
86
|
+
import cairosvg
|
|
87
|
+
|
|
88
|
+
res = render(spec, raster_safe=True)
|
|
89
|
+
return cairosvg.svg2png(
|
|
90
|
+
bytestring=res.svg.encode("utf-8"), output_width=int(res.width * scale)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def validate_spec(spec: dict) -> dict[str, Any]:
|
|
95
|
+
"""렌더 없이 spec 만 검증. {ok, errors[], warnings[]}."""
|
|
96
|
+
from pydantic import ValidationError
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
model = _coerce(spec)
|
|
100
|
+
except ValidationError as e:
|
|
101
|
+
return {"ok": False, "errors": e.errors(include_url=False), "warnings": []}
|
|
102
|
+
scene = build_scene(model)
|
|
103
|
+
return {"ok": True, "errors": [], "warnings": lint(scene)}
|