cogevol-4b-learning-gen / slide_render.py
multimodalart's picture
multimodalart HF Staff
CogEvol-4B learning environment generation demo
71b36ea verified
Raw
History Blame Contribute Delete
24.7 kB
"""Render a CogEvol / OpenMAIC slide scene-graph JSON into a self-contained HTML page.
The contract implemented here follows the canonical OpenMAIC slide object model
(`packages/@openmaic/dsl/src/slides.ts`, MIT, THU-MAIC): a 1000x562 canvas with
absolutely-positioned `text | shape | line | image | table | chart | latex | video | code`
elements plus a `background` object.
"""
from __future__ import annotations
import html
import json
from typing import Any
CANVAS_W = 1000
CANVAS_H = 562
DEFAULT_THEME_COLORS = [
"#5b9bd5",
"#ed7d31",
"#a5a5a5",
"#ffc000",
"#4472c4",
"#70ad47",
"#264478",
"#9e480e",
]
FONT_STACK = (
"'Microsoft YaHei','PingFang SC','Hiragino Sans GB','Noto Sans CJK SC',"
"'Source Han Sans SC','WenQuanYi Micro Hei',Inter,Arial,sans-serif"
)
def _num(value: Any, default: float = 0.0) -> float:
try:
if isinstance(value, bool):
return default
return float(value)
except (TypeError, ValueError):
return default
def _esc(value: Any) -> str:
return html.escape("" if value is None else str(value), quote=True)
def _css_outline(outline: dict | None, fallback: str = "") -> str:
if not isinstance(outline, dict):
return fallback
width = _num(outline.get("width"), 1)
style = outline.get("style") or "solid"
color = outline.get("color") or "#000000"
if width <= 0:
return fallback
return f"border:{width}px {_esc(style)} {_esc(color)};"
def _shadow(shadow: dict | None) -> str:
if not isinstance(shadow, dict):
return ""
return (
f"filter:drop-shadow({_num(shadow.get('h'))}px {_num(shadow.get('v'))}px "
f"{_num(shadow.get('blur'))}px {_esc(shadow.get('color') or 'rgba(0,0,0,.3)')});"
)
def _base_box(el: dict, extra: str = "") -> str:
left, top = _num(el.get("left")), _num(el.get("top"))
width, height = _num(el.get("width"), 100), _num(el.get("height"), 40)
rotate = _num(el.get("rotate"))
style = (
f"position:absolute;left:{left}px;top:{top}px;"
f"width:{width}px;height:{height}px;"
)
if rotate:
style += f"transform:rotate({rotate}deg);"
opacity = el.get("opacity")
if isinstance(opacity, (int, float)) and opacity != 1:
style += f"opacity:{opacity};"
return style + extra
def _gradient_css(gradient: dict) -> str:
colors = gradient.get("colors") or []
stops = ", ".join(
f"{_esc(c.get('color') or '#ffffff')} {_num(c.get('pos'))}%"
for c in colors
if isinstance(c, dict)
)
if not stops:
return ""
if (gradient.get("type") or "linear") == "radial":
return f"radial-gradient(circle, {stops})"
return f"linear-gradient({_num(gradient.get('rotate'))}deg, {stops})"
# --------------------------------------------------------------------------- text
def _render_text(el: dict) -> str:
style = _base_box(el)
style += f"color:{_esc(el.get('defaultColor') or '#333333')};"
font = el.get("defaultFontName")
if font:
style += f"font-family:{_esc(font)},{FONT_STACK};"
if el.get("fill"):
style += f"background:{_esc(el['fill'])};"
style += _css_outline(el.get("outline"))
style += f"line-height:{_num(el.get('lineHeight'), 1.5)};"
if el.get("wordSpace"):
style += f"letter-spacing:{_num(el.get('wordSpace'))}px;"
style += _shadow(el.get("shadow"))
if el.get("vertical"):
style += "writing-mode:vertical-rl;"
valign = el.get("vAlign") or "top"
flex = {"top": "flex-start", "middle": "center", "bottom": "flex-end"}.get(valign, "flex-start")
style += f"display:flex;flex-direction:column;justify-content:{flex};overflow:visible;"
content = el.get("content") or ""
return f'<div class="el text" style="{style}"><div class="rich">{content}</div></div>'
# -------------------------------------------------------------------------- shape
def _render_shape(el: dict, idx: int) -> str:
view_box = el.get("viewBox") or [200, 200]
if isinstance(view_box, (int, float)):
view_box = [view_box, view_box]
vb_w = _num(view_box[0] if len(view_box) > 0 else 200, 200)
vb_h = _num(view_box[1] if len(view_box) > 1 else vb_w, vb_w)
path = el.get("path") or "M 0 0 L 200 0 L 200 200 L 0 200 Z"
fill = el.get("fill") or "#5b9bd5"
defs = ""
fill_attr = _esc(fill)
gradient = el.get("gradient")
if isinstance(gradient, dict) and gradient.get("colors"):
gid = f"grad{idx}"
stops = "".join(
f'<stop offset="{_num(c.get("pos"))}%" stop-color="{_esc(c.get("color") or "#ffffff")}"/>'
for c in gradient["colors"]
if isinstance(c, dict)
)
if (gradient.get("type") or "linear") == "radial":
defs = f'<defs><radialGradient id="{gid}">{stops}</radialGradient></defs>'
else:
rot = _num(gradient.get("rotate"))
defs = (
f'<defs><linearGradient id="{gid}" gradientTransform="rotate({rot})">'
f"{stops}</linearGradient></defs>"
)
fill_attr = f"url(#{gid})"
outline = el.get("outline") or {}
stroke = ""
if isinstance(outline, dict) and outline.get("color"):
ow = _num(outline.get("width"), 1)
dash = {"dashed": "10 6", "dotted": "2 4"}.get(outline.get("style") or "solid", "")
stroke = f' stroke="{_esc(outline["color"])}" stroke-width="{ow}"'
if dash:
stroke += f' stroke-dasharray="{dash}"'
flip = ""
if el.get("flipH") or el.get("flipV"):
sx, sy = (-1 if el.get("flipH") else 1), (-1 if el.get("flipV") else 1)
flip = f"transform:scale({sx},{sy});"
svg = (
f'<svg class="shape-svg" style="{flip}" width="100%" height="100%" '
f'viewBox="0 0 {vb_w} {vb_h}" preserveAspectRatio="none" '
f'xmlns="http://www.w3.org/2000/svg">{defs}'
f'<path d="{_esc(path)}" fill="{fill_attr}"{stroke}/></svg>'
)
inner = ""
text = el.get("text")
if isinstance(text, dict) and text.get("content"):
align = {"top": "flex-start", "middle": "center", "bottom": "flex-end"}.get(
text.get("align") or "middle", "center"
)
t_style = (
"position:absolute;inset:0;display:flex;flex-direction:column;"
f"justify-content:{align};padding:6px;box-sizing:border-box;"
f"color:{_esc(text.get('defaultColor') or '#333333')};"
f"line-height:{_num(text.get('lineHeight'), 1.5)};"
)
if text.get("defaultFontName"):
t_style += f"font-family:{_esc(text['defaultFontName'])},{FONT_STACK};"
inner = f'<div class="shape-text rich" style="{t_style}">{text["content"]}</div>'
style = _base_box(el, _shadow(el.get("shadow")))
return f'<div class="el shape" style="{style}">{svg}{inner}</div>'
# --------------------------------------------------------------------------- line
def _render_line(el: dict, idx: int) -> str:
start = el.get("start") or [0, 0]
end = el.get("end") or [100, 0]
sx, sy = _num(start[0] if len(start) > 0 else 0), _num(start[1] if len(start) > 1 else 0)
ex, ey = _num(end[0] if len(end) > 0 else 0), _num(end[1] if len(end) > 1 else 0)
ctrl_points = []
for key in ("broken", "broken2", "curve"):
pt = el.get(key)
if isinstance(pt, (list, tuple)) and len(pt) >= 2:
ctrl_points.append((_num(pt[0]), _num(pt[1])))
cubic = el.get("cubic")
if isinstance(cubic, (list, tuple)) and len(cubic) == 2:
for pt in cubic:
if isinstance(pt, (list, tuple)) and len(pt) >= 2:
ctrl_points.append((_num(pt[0]), _num(pt[1])))
stroke_w = max(_num(el.get("width"), 2), 1)
xs = [sx, ex] + [p[0] for p in ctrl_points]
ys = [sy, ey] + [p[1] for p in ctrl_points]
pad = stroke_w * 3 + 6
svg_w = max(xs) - min(min(xs), 0) + pad
svg_h = max(ys) - min(min(ys), 0) + pad
if cubic and len(ctrl_points) >= 2:
c1, c2 = ctrl_points[-2], ctrl_points[-1]
d = f"M {sx} {sy} C {c1[0]} {c1[1]} {c2[0]} {c2[1]} {ex} {ey}"
elif el.get("curve") and ctrl_points:
c = ctrl_points[0]
d = f"M {sx} {sy} Q {c[0]} {c[1]} {ex} {ey}"
elif el.get("broken") and ctrl_points:
pts = " ".join(f"L {p[0]} {p[1]}" for p in ctrl_points)
d = f"M {sx} {sy} {pts} L {ex} {ey}"
else:
d = f"M {sx} {sy} L {ex} {ey}"
color = _esc(el.get("color") or "#333333")
dash = {"dashed": stroke_w * 5, "dotted": stroke_w * 2}.get(el.get("style") or "solid")
dash_attr = f' stroke-dasharray="{dash} {dash}"' if dash else ""
points = el.get("points") or ["", ""]
p_start = points[0] if len(points) > 0 else ""
p_end = points[1] if len(points) > 1 else ""
defs, marker_attrs = [], ""
for name, kind, is_start in (("s", p_start, True), ("e", p_end, False)):
if kind not in ("arrow", "dot"):
continue
mid = f"m{idx}{name}"
if kind == "arrow":
body = f'<path d="M0,0 L0,6 L6,3 z" fill="{color}"/>'
if is_start:
body = f'<path d="M6,0 L6,6 L0,3 z" fill="{color}"/>'
defs.append(
f'<marker id="{mid}" markerWidth="6" markerHeight="6" refX="3" refY="3" '
f'orient="auto" markerUnits="strokeWidth">{body}</marker>'
)
else:
defs.append(
f'<marker id="{mid}" markerWidth="6" markerHeight="6" refX="3" refY="3" '
f'markerUnits="strokeWidth"><circle cx="3" cy="3" r="2.6" fill="{color}"/></marker>'
)
marker_attrs += f' marker-{"start" if is_start else "end"}="url(#{mid})"'
defs_str = f"<defs>{''.join(defs)}</defs>" if defs else ""
left, top = _num(el.get("left")), _num(el.get("top"))
style = f"position:absolute;left:{left}px;top:{top}px;width:{svg_w}px;height:{svg_h}px;overflow:visible;"
return (
f'<div class="el line" style="{style}">'
f'<svg width="{svg_w}" height="{svg_h}" overflow="visible" xmlns="http://www.w3.org/2000/svg">'
f'{defs_str}<path d="{d}" fill="none" stroke="{color}" stroke-width="{stroke_w}"'
f'{dash_attr}{marker_attrs} stroke-linecap="round"/></svg></div>'
)
# -------------------------------------------------------------------------- table
def _render_table(el: dict) -> str:
data = el.get("data") or []
col_widths = el.get("colWidths") or []
n_cols = max((len(r) for r in data if isinstance(r, list)), default=1)
if len(col_widths) != n_cols or not col_widths:
col_widths = [1.0 / max(n_cols, 1)] * n_cols
total = sum(_num(w, 0) for w in col_widths) or 1.0
cols = "".join(f'<col style="width:{_num(w) / total * 100:.4f}%">' for w in col_widths)
outline = el.get("outline") or {}
border = "1px solid #cbd5e1"
if isinstance(outline, dict):
border = (
f"{_num(outline.get('width'), 1)}px {_esc(outline.get('style') or 'solid')} "
f"{_esc(outline.get('color') or '#cbd5e1')}"
)
min_h = _num(el.get("cellMinHeight"), 36)
row_heights = el.get("rowHeights") or []
rows_html = []
for r_i, row in enumerate(data):
if not isinstance(row, list):
continue
h = _num(row_heights[r_i], min_h) if r_i < len(row_heights) else min_h
cells = []
for cell in row:
if not isinstance(cell, dict):
cell = {"text": str(cell)}
st = cell.get("style") or {}
css = f"border:{border};padding:{_esc(cell.get('padding') or '4px 8px')};"
css += f"vertical-align:{_esc(cell.get('vAlign') or 'middle')};"
if st.get("bold"):
css += "font-weight:700;"
if st.get("em"):
css += "font-style:italic;"
deco = []
if st.get("underline"):
deco.append("underline")
if st.get("strikethrough"):
deco.append("line-through")
if deco:
css += f"text-decoration:{' '.join(deco)};"
if st.get("color"):
css += f"color:{_esc(st['color'])};"
if st.get("backcolor"):
css += f"background:{_esc(st['backcolor'])};"
if st.get("fontsize"):
fs = str(st["fontsize"])
css += f"font-size:{_esc(fs if not fs.isdigit() else fs + 'px')};"
if st.get("fontname"):
css += f"font-family:{_esc(st['fontname'])},{FONT_STACK};"
if st.get("align"):
css += f"text-align:{_esc(st['align'])};"
span = ""
colspan = int(_num(cell.get("colspan"), 1)) or 1
rowspan = int(_num(cell.get("rowspan"), 1)) or 1
if colspan > 1:
span += f' colspan="{colspan}"'
if rowspan > 1:
span += f' rowspan="{rowspan}"'
text = str(cell.get("text", ""))
cells.append(f'<td style="{css}"{span}>{text}</td>')
rows_html.append(f'<tr style="height:{h}px">{"".join(cells)}</tr>')
style = _base_box(el, "overflow:visible;")
return (
f'<div class="el table" style="{style}">'
f'<table style="width:100%;table-layout:fixed;border-collapse:collapse;">'
f"<colgroup>{cols}</colgroup><tbody>{''.join(rows_html)}</tbody></table></div>"
)
# -------------------------------------------------------------------------- other
def _render_image(el: dict) -> str:
src = el.get("src") or ""
style = _base_box(el, _css_outline(el.get("outline")) + _shadow(el.get("shadow")))
if el.get("radius"):
style += f"border-radius:{_num(el.get('radius'))}px;overflow:hidden;"
fit = "contain" if el.get("fixedRatio", True) else "fill"
if isinstance(src, str) and (src.startswith("http") or src.startswith("data:")):
return (
f'<div class="el image" style="{style}">'
f'<img src="{_esc(src)}" style="width:100%;height:100%;object-fit:{fit}"></div>'
)
label = _esc(src) if src else "image"
return (
f'<div class="el image placeholder" style="{style}">'
f'<span>🖼 {label}</span></div>'
)
def _render_video(el: dict) -> str:
src = el.get("src") or el.get("mediaRef") or ""
style = _base_box(el)
if isinstance(src, str) and src.startswith("http"):
return (
f'<div class="el video" style="{style}">'
f'<video src="{_esc(src)}" controls style="width:100%;height:100%"></video></div>'
)
return f'<div class="el image placeholder" style="{style}"><span>▶ {_esc(src) or "video"}</span></div>'
def _render_latex(el: dict, idx: int) -> str:
style = _base_box(el, "display:flex;align-items:center;overflow:visible;")
align = el.get("align") or "center"
justify = {"left": "flex-start", "center": "center", "right": "flex-end"}.get(align, "center")
style += f"justify-content:{justify};"
if el.get("color"):
style += f"color:{_esc(el['color'])};"
if el.get("html"):
return f'<div class="el latex" style="{style}">{el["html"]}</div>'
return (
f'<div class="el latex" style="{style}">'
f'<span class="katex-target" id="ktx{idx}" data-latex="{_esc(el.get("latex") or "")}"></span></div>'
)
def _render_code(el: dict) -> str:
lines = el.get("lines") or []
text = "\n".join(
str(line.get("content", "")) if isinstance(line, dict) else str(line) for line in lines
)
style = _base_box(el, "overflow:auto;background:#1e293b;border-radius:6px;")
fs = _num(el.get("fontSize"), 14)
return (
f'<div class="el code" style="{style}">'
f'<pre style="margin:0;padding:10px;color:#e2e8f0;font-size:{fs}px;'
f'font-family:ui-monospace,SFMono-Regular,Menlo,monospace">{_esc(text)}</pre></div>'
)
def _render_chart(el: dict, idx: int) -> tuple[str, dict | None]:
style = _base_box(el)
if el.get("fill"):
style += f"background:{_esc(el['fill'])};"
style += _css_outline(el.get("outline"))
div = f'<div class="el chart" id="chart{idx}" style="{style}"></div>'
data = el.get("data") or {}
labels = [str(x) for x in (data.get("labels") or [])]
legends = [str(x) for x in (data.get("legends") or [])]
series = data.get("series") or []
if not isinstance(series, list) or not labels:
return div, None
series = [s if isinstance(s, list) else [s] for s in series]
colors = el.get("themeColors") or DEFAULT_THEME_COLORS
text_color = el.get("textColor") or "#333333"
line_color = el.get("lineColor") or "#d9d9d9"
chart_type = el.get("chartType") or "bar"
smooth = bool((el.get("options") or {}).get("lineSmooth"))
stack = "total" if (el.get("options") or {}).get("stack") else None
spec = {
"chartType": chart_type,
"labels": labels,
"legends": legends,
"series": series,
"colors": colors,
"textColor": text_color,
"lineColor": line_color,
"smooth": smooth,
"stack": stack,
"id": f"chart{idx}",
}
return div, spec
# ---------------------------------------------------------------------- assemble
_PAGE = """<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
<style>
html,body{margin:0;padding:0;background:#eef1f5;font-family:__FONT__;}
#wrap{width:100%;overflow:hidden;}
#stage{width:1000px;height:562px;position:relative;transform-origin:top left;
box-shadow:0 2px 18px rgba(0,0,0,.18);overflow:hidden;}
.el{box-sizing:border-box;}
.el.text .rich{width:100%;}
.rich p{margin:0 0 4px 0;}
.rich ul,.rich ol{margin:0 0 4px 0;padding-left:1.3em;}
.placeholder{display:flex;align-items:center;justify-content:center;border:1px dashed #94a3b8;
color:#64748b;background:#f8fafc;font-size:13px;text-align:center;}
table{font-size:15px;}
</style></head>
<body><div id="wrap"><div id="stage" style="__BG__">__ELEMENTS__</div></div>
<script>
const CHARTS = __CHARTS__;
function fit(){
const s = document.getElementById('wrap').clientWidth / 1000;
const st = document.getElementById('stage');
st.style.transform = 'scale(' + s + ')';
document.getElementById('wrap').style.height = (562 * s) + 'px';
}
window.addEventListener('resize', fit); fit();
window.addEventListener('load', function(){
document.querySelectorAll('.katex-target').forEach(function(node){
try { katex.render(node.dataset.latex || '', node, {throwOnError:false, displayMode:true}); }
catch(e){ node.textContent = node.dataset.latex || ''; }
});
if (typeof echarts === 'undefined') return;
CHARTS.forEach(function(c){
const dom = document.getElementById(c.id); if(!dom) return;
const axisLabel = {color: c.textColor};
const splitLine = {lineStyle:{color: c.lineColor}};
let option = {color: c.colors, animation:false,
textStyle:{fontFamily:'inherit', color:c.textColor},
legend: c.legends.length > 1 ? {data:c.legends, textStyle:{color:c.textColor}} : undefined,
grid:{left:'8%', right:'6%', top: c.legends.length>1 ? '18%' : '10%', bottom:'12%', containLabel:true}};
const mk = function(type, extra){
return c.series.map(function(s, i){
return Object.assign({name: c.legends[i] || ('Series ' + (i+1)), type: type,
data: s, stack: c.stack || undefined}, extra || {}); });
};
if (c.chartType === 'pie' || c.chartType === 'ring') {
const vals = (c.series[0] || []).map(function(v, i){ return {value:v, name:c.labels[i]}; });
option.grid = undefined;
option.series = [{type:'pie', data:vals, radius: c.chartType === 'ring' ? ['40%','68%'] : '68%',
label:{color:c.textColor}}];
} else if (c.chartType === 'radar') {
const flat = c.series.flat();
const max = Math.max.apply(null, flat.length ? flat : [1]);
option.grid = undefined;
option.radar = {indicator: c.labels.map(function(l){ return {name:l, max:max}; }),
axisName:{color:c.textColor}};
option.series = [{type:'radar', data: c.series.map(function(s,i){
return {value:s, name: c.legends[i] || ('Series ' + (i+1))}; })}];
} else if (c.chartType === 'scatter') {
option.xAxis = {type:'value', axisLabel:axisLabel, splitLine:splitLine};
option.yAxis = {type:'value', axisLabel:axisLabel, splitLine:splitLine};
option.series = c.series.map(function(s,i){
return {type:'scatter', name:c.legends[i] || ('Series ' + (i+1)),
data: s.map(function(v,j){ return [Number(c.labels[j]) || j, v]; })}; });
} else if (c.chartType === 'bar') {
option.xAxis = {type:'value', axisLabel:axisLabel, splitLine:splitLine};
option.yAxis = {type:'category', data:c.labels, axisLabel:axisLabel};
option.series = mk('bar');
} else if (c.chartType === 'line' || c.chartType === 'area') {
option.xAxis = {type:'category', data:c.labels, axisLabel:axisLabel};
option.yAxis = {type:'value', axisLabel:axisLabel, splitLine:splitLine};
option.series = mk('line', c.chartType === 'area'
? {smooth:c.smooth, areaStyle:{}} : {smooth:c.smooth});
} else {
option.xAxis = {type:'category', data:c.labels, axisLabel:axisLabel};
option.yAxis = {type:'value', axisLabel:axisLabel, splitLine:splitLine};
option.series = mk('bar');
}
try { echarts.init(dom).setOption(option); } catch(e) {}
});
});
</script></body></html>
"""
def _background_css(background: Any) -> str:
if not isinstance(background, dict):
return "background:#ffffff;"
b_type = background.get("type") or "solid"
if b_type == "gradient" and isinstance(background.get("gradient"), dict):
css = _gradient_css(background["gradient"])
if css:
return f"background:{css};"
if b_type == "image":
image = background.get("image")
src = image.get("src") if isinstance(image, dict) else background.get("imageSrc")
if isinstance(src, str) and (src.startswith("http") or src.startswith("data:")):
return f"background:#fff url('{_esc(src)}') center/cover no-repeat;"
return f"background:{_esc(background.get('color') or '#ffffff')};"
def render_slide(slide: dict) -> str:
"""Render a slide scene-graph dict into a standalone HTML document string."""
elements = slide.get("elements") or []
parts: list[str] = []
charts: list[dict] = []
for idx, el in enumerate(elements):
if not isinstance(el, dict):
continue
el_type = el.get("type")
try:
if el_type == "text":
parts.append(_render_text(el))
elif el_type == "shape":
parts.append(_render_shape(el, idx))
elif el_type == "line":
parts.append(_render_line(el, idx))
elif el_type == "table":
parts.append(_render_table(el))
elif el_type == "image":
parts.append(_render_image(el))
elif el_type == "video":
parts.append(_render_video(el))
elif el_type == "latex":
parts.append(_render_latex(el, idx))
elif el_type == "code":
parts.append(_render_code(el))
elif el_type == "chart":
div, spec = _render_chart(el, idx)
parts.append(div)
if spec:
charts.append(spec)
else:
parts.append(
f'<div class="el placeholder" style="{_base_box(el)}">'
f"<span>unsupported element: {_esc(el_type)}</span></div>"
)
except Exception as exc: # a malformed element must not kill the page
parts.append(
f'<div class="el placeholder" style="{_base_box(el)}">'
f"<span>{_esc(el_type)} render error: {_esc(exc)}</span></div>"
)
return (
_PAGE.replace("__FONT__", FONT_STACK)
.replace("__BG__", _background_css(slide.get("background")))
.replace("__ELEMENTS__", "".join(parts))
.replace("__CHARTS__", json.dumps(charts))
)