CoolFace
Apppublic

build-small-hackathon/oneiros

sourceHugging Faceupdated 4mo agoView on Hugging Face
1likes
generator.py247 linesDownload Raw Back to map
1"""JSON entities → SVG dream map (radial layout, 5 mood palettes)."""2from __future__ import annotations3 4import html5import math6from typing import Any7 8from map.themes import Palette, get_palette, terangkan9 10_SVG_W = 60011_SVG_H = 42012_CX = 30013_CY = 218   # slightly below midpoint → title strip clearance at top14_R1 = 115   # ring 1: secondary characters + places (safe: max node bottom = 375)15_R2 = 155   # ring 2: symbols (safe: max label bottom = 413)16_NODE_R = 28   # character circle radius17_RECT_W, _RECT_H = 62, 36   # place rect18_DIAMOND_R = 26   # symbol half-size19_LABEL_MAXLEN = 1420 21 22def _trunc(text: str, n: int = _LABEL_MAXLEN) -> str:23    s = str(text).strip()24    return s if len(s) <= n else s[:n - 1] + "…"25 26 27def _esc(text: str) -> str:28    return html.escape(str(text), quote=True)29 30 31def _radial_positions(count: int, radius: float, offset_angle: float = 0.0) -> list[tuple[float, float]]:32    if count == 0:33        return []34    step = 2 * math.pi / count35    return [36        (37            _CX + radius * math.cos(offset_angle + i * step - math.pi / 2),38            _CY + radius * math.sin(offset_angle + i * step - math.pi / 2),39        )40        for i in range(count)41    ]42 43 44def _draw_character(x: float, y: float, label: str, fill: str, text_color: str, is_self: bool) -> str:45    r = _NODE_R + (6 if is_self else 0)46    stroke = terangkan(fill, 0.25)47    sw = 2.5 if is_self else 1.548    txt_y = y + r + 1449    lines = [50        f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{r}" fill="{fill}" '51        f'stroke="{stroke}" stroke-width="{sw}" opacity="0.92"/>',52        f'<text x="{x:.1f}" y="{txt_y:.1f}" text-anchor="middle" '53        f'font-size="11" font-family="DM Sans,system-ui,sans-serif" '54        f'fill="{text_color}" opacity="0.9">{_esc(_trunc(label))}</text>',55    ]56    if is_self:57        lines.insert(0,58            f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{r + 7}" '59            f'fill="none" stroke="{stroke}" stroke-width="1" opacity="0.3"/>')60    return "\n".join(lines)61 62 63def _draw_place(x: float, y: float, label: str, fill: str, text_color: str) -> str:64    rx, ry = _RECT_W / 2, _RECT_H / 265    stroke = terangkan(fill, 0.2)66    txt_y = y + ry + 1467    return (68        f'<rect x="{x - rx:.1f}" y="{y - ry:.1f}" width="{_RECT_W}" height="{_RECT_H}" '69        f'rx="8" fill="{fill}" stroke="{stroke}" stroke-width="1.5" opacity="0.88"/>\n'70        f'<text x="{x:.1f}" y="{txt_y:.1f}" text-anchor="middle" '71        f'font-size="11" font-family="DM Sans,system-ui,sans-serif" '72        f'fill="{text_color}" opacity="0.9">{_esc(_trunc(label))}</text>'73    )74 75 76def _draw_symbol(x: float, y: float, label: str, fill: str, text_color: str) -> str:77    r = _DIAMOND_R78    pts = f"{x:.1f},{y-r:.1f} {x+r:.1f},{y:.1f} {x:.1f},{y+r:.1f} {x-r:.1f},{y:.1f}"79    stroke = terangkan(fill, 0.2)80    txt_y = y + r + 1481    return (82        f'<polygon points="{pts}" fill="{fill}" '83        f'stroke="{stroke}" stroke-width="1.5" opacity="0.85"/>\n'84        f'<text x="{x:.1f}" y="{txt_y:.1f}" text-anchor="middle" '85        f'font-size="10" font-family="DM Sans,system-ui,sans-serif" '86        f'fill="{text_color}" opacity="0.85">{_esc(_trunc(label))}</text>'87    )88 89 90def _draw_edge(91    x1: float, y1: float, x2: float, y2: float,92    conn_type: str, palette: Palette,93) -> str:94    dashed = conn_type in ("tension", "pursuit")95    color = palette["edge_dash"] if dashed else palette["edge_solid"]96    dash_attr = ' stroke-dasharray="6,4"' if dashed else ""97    return (98        f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" '99        f'stroke="{color}" stroke-width="1.5"{dash_attr} opacity="0.7"/>'100    )101 102 103def _empty_map(palette: Palette, title: str = "") -> str:104    msg = _esc(title) if title else "Write a dream and tap the button…"105    return (106        f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {_SVG_W} {_SVG_H}" '107        f'width="100%" style="max-height:420px;display:block">'108        f'<rect width="{_SVG_W}" height="{_SVG_H}" fill="{palette["bg"]}"/>'109        f'<text x="{_CX}" y="{_CY}" text-anchor="middle" dominant-baseline="middle" '110        f'font-size="13" font-family="DM Sans,system-ui,sans-serif" '111        f'fill="{palette["text_dim"]}" opacity="0.6">{msg}</text>'112        f'</svg>'113    )114 115 116def generate_dream_map(entities: dict[str, Any]) -> str:117    """Return a complete SVG string from extracted entities."""118    mood = str(entities.get("mood") or "mysterious")119    palette = get_palette(mood)120    title = str(entities.get("title") or "")121 122    characters: list[dict] = [c for c in (entities.get("characters") or []) if isinstance(c, dict)]123    places: list[dict] = [p for p in (entities.get("places") or []) if isinstance(p, dict)]124    symbols: list[dict] = [s for s in (entities.get("symbols") or []) if isinstance(s, dict)]125    connections: list[dict] = [e for e in (entities.get("connections") or []) if isinstance(e, dict)]126 127    if not characters and not places and not symbols:128        return _empty_map(palette, title)129 130    # --- node registry: name → (x, y, type) ---131    node_pos: dict[str, tuple[float, float]] = {}132 133    # Center: first character (or first place/symbol if no characters)134    center_node = characters[0] if characters else (places[0] if places else symbols[0])135    center_name = str(center_node.get("name", "?"))136    node_pos[center_name] = (_CX, _CY)137 138    # Ring 1: remaining characters + places (exclude center if it came from places)139    if characters:140        ring1_nodes = characters[1:] + places141    elif places:142        ring1_nodes = places[1:]143    else:144        ring1_nodes = []145 146    r1_name_to_idx = {str(n.get("name", "?")): i for i, n in enumerate(ring1_nodes)}147    r1_pos = _radial_positions(len(ring1_nodes), _R1)148    for name, idx in r1_name_to_idx.items():149        if name != center_name:   # jangan overwrite center jika nama sama150            node_pos[name] = r1_pos[idx]151 152    # Ring 2: symbols (exclude center if it came from symbols)153    symbols_r2 = symbols if (characters or places) else symbols[1:]154    r2_offset = math.pi / len(symbols_r2) if symbols_r2 else 0155    r2_pos = _radial_positions(len(symbols_r2), _R2, offset_angle=r2_offset)156    for i, sym in enumerate(symbols_r2):157        node_pos[str(sym.get("name", "?"))] = r2_pos[i]158 159    # --- Build SVG ---160    parts: list[str] = []161 162    # Background gradient163    grad_id = f"grad_{mood}"164    parts.append(165        f'<defs>'166        f'<radialGradient id="{grad_id}" cx="50%" cy="50%" r="70%">'167        f'<stop offset="0%" stop-color="{palette["bg2"]}"/>'168        f'<stop offset="100%" stop-color="{palette["bg"]}"/>'169        f'</radialGradient>'170        f'</defs>'171        f'<rect width="{_SVG_W}" height="{_SVG_H}" fill="url(#{grad_id})"/>'172    )173 174    # Subtle orbit rings — hanya tampil jika ada node di ring tersebut175    if ring1_nodes:176        parts.append(177            f'<circle cx="{_CX}" cy="{_CY}" r="{_R1}" fill="none" '178            f'stroke="{palette["orbit"]}" stroke-width="1" stroke-dasharray="3,6"/>'179        )180    if symbols_r2:181        parts.append(182            f'<circle cx="{_CX}" cy="{_CY}" r="{_R2}" fill="none" '183            f'stroke="{palette["orbit"]}" stroke-width="1" stroke-dasharray="2,8"/>'184        )185 186    # Edges (drawn first, below nodes)187    for conn in connections:188        fr = str(conn.get("from", ""))189        to = str(conn.get("to", ""))190        ctype = str(conn.get("type", "harmony"))191        if fr in node_pos and to in node_pos:192            x1, y1 = node_pos[fr]193            x2, y2 = node_pos[to]194            parts.append(_draw_edge(x1, y1, x2, y2, ctype, palette))195 196    # Nodes197    for char in characters:198        name = str(char.get("name", "?"))199        if name not in node_pos:200            continue201        x, y = node_pos[name]202        is_self = str(char.get("role", "")) == "self" or name == center_name203        fill = terangkan(palette["node_char"], 0.05) if is_self else palette["node_char"]204        parts.append(_draw_character(x, y, name, fill, palette["text"], is_self))205 206    for place in places:207        name = str(place.get("name", "?"))208        if name not in node_pos:209            continue210        x, y = node_pos[name]211        parts.append(_draw_place(x, y, name, palette["node_place"], palette["text"]))212 213    for sym in symbols:214        name = str(sym.get("name", "?"))215        if name not in node_pos:216            continue217        x, y = node_pos[name]218        parts.append(_draw_symbol(x, y, name, palette["node_symbol"], palette["text"]))219 220    # Title + mood badge (top strip)221    if title:222        parts.append(223            f'<text x="16" y="22" font-size="12" font-weight="600" '224            f'font-family="DM Sans,system-ui,sans-serif" fill="{palette["title"]}" opacity="0.85">'225            f'{_esc(_trunc(title, 32))}</text>'226        )227    mood_label = mood.capitalize()228    badge_w = len(mood_label) * 7 + 16229    badge_rx = _SVG_W - 8 - badge_w230    badge_cx = badge_rx + badge_w / 2231    parts.append(232        f'<rect x="{badge_rx:.0f}" y="8" width="{badge_w}" height="20" rx="10" '233        f'fill="{palette["badge_bg"]}"/>'234        f'<text x="{badge_cx:.1f}" y="21" text-anchor="middle" '235        f'font-size="10" font-weight="600" letter-spacing="0.08em" '236        f'font-family="DM Sans,system-ui,sans-serif" fill="{palette["title"]}" '237        f'text-transform="uppercase" opacity="0.9">{_esc(mood_label)}</text>'238    )239 240    inner = "\n".join(parts)241    return (242        f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {_SVG_W} {_SVG_H}" '243        f'width="100%" style="max-height:420px;display:block;border-radius:10px">'244        f'{inner}'245        f'</svg>'246    )247