From 4bbfdcceffc60388449485e02480100e124f9c1a Mon Sep 17 00:00:00 2001 From: bigmerge Date: Sat, 15 Aug 2026 14:26:59 -0500 Subject: [PATCH] Add native audio/image efferent surfaces + projector proof-of-shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit audio-surface.el / image-surface.el: own-core additive-synthesis WAV and raster-PNG renderers (integer-only DSP, since EL has no floats), rendered from learned engram signatures via a pluggable surface-profile abstraction (surface-profile.el). audio-demo.el / image-demo.el are drivers. NOTE: demo files hardcode absolute paths to this worktree's own directory — will need a path fixup before landing. elp/projector/ is a Python package the author's own README marks as "STAGING/PROOF-OF-SHAPE — not the deliverable", superseded by the native .el surface-profile work above; kept as a validated architecture proof. Generated output (elp/faculty/{out,sig}, elp/projector/out, __pycache__) intentionally excluded. --- elp/projector/README.md | 91 ++++ elp/projector/cohere.py | 79 ++++ elp/projector/document_ir.py | 111 +++++ elp/projector/generate.py | 81 ++++ elp/projector/geometry.py | 129 ++++++ elp/projector/pipeline.py | 67 +++ elp/projector/plan.py | 192 ++++++++ .../__pycache__/base.cpython-313.pyc | Bin 0 -> 5520 bytes .../__pycache__/docx.cpython-313.pyc | Bin 0 -> 7274 bytes .../__pycache__/markdown.cpython-313.pyc | Bin 0 -> 2954 bytes .../__pycache__/midi.cpython-313.pyc | Bin 0 -> 8329 bytes .../__pycache__/seams.cpython-313.pyc | Bin 0 -> 3430 bytes elp/projector/projectors/base.py | 106 +++++ elp/projector/projectors/docx.py | 113 +++++ elp/projector/projectors/markdown.py | 45 ++ elp/projector/projectors/midi.py | 133 ++++++ elp/projector/projectors/seams.py | 60 +++ elp/projector/provenance.py | 63 +++ elp/projector/realize.py | 112 +++++ elp/src/audio-demo.el | 73 ++++ elp/src/audio-surface.el | 400 +++++++++++++++++ elp/src/image-demo.el | 65 +++ elp/src/image-surface.el | 412 ++++++++++++++++++ elp/src/surface-profile.el | 153 +++++++ elp/tests/examples/surface-profile-demo.el | 26 ++ 25 files changed, 2511 insertions(+) create mode 100644 elp/projector/README.md create mode 100644 elp/projector/cohere.py create mode 100644 elp/projector/document_ir.py create mode 100644 elp/projector/generate.py create mode 100644 elp/projector/geometry.py create mode 100644 elp/projector/pipeline.py create mode 100644 elp/projector/plan.py create mode 100644 elp/projector/projectors/__pycache__/base.cpython-313.pyc create mode 100644 elp/projector/projectors/__pycache__/docx.cpython-313.pyc create mode 100644 elp/projector/projectors/__pycache__/markdown.cpython-313.pyc create mode 100644 elp/projector/projectors/__pycache__/midi.cpython-313.pyc create mode 100644 elp/projector/projectors/__pycache__/seams.cpython-313.pyc create mode 100644 elp/projector/projectors/base.py create mode 100644 elp/projector/projectors/docx.py create mode 100644 elp/projector/projectors/markdown.py create mode 100644 elp/projector/projectors/midi.py create mode 100644 elp/projector/projectors/seams.py create mode 100644 elp/projector/provenance.py create mode 100644 elp/projector/realize.py create mode 100644 elp/src/audio-demo.el create mode 100644 elp/src/audio-surface.el create mode 100644 elp/src/image-demo.el create mode 100644 elp/src/image-surface.el create mode 100644 elp/src/surface-profile.el create mode 100644 elp/tests/examples/surface-profile-demo.el diff --git a/elp/projector/README.md b/elp/projector/README.md new file mode 100644 index 0000000..8aa466d --- /dev/null +++ b/elp/projector/README.md @@ -0,0 +1,91 @@ +> **STATUS: STAGING / PROOF-OF-SHAPE — not the deliverable.** This Python package +> proved the architecture end-to-end against the proven realizer faculty (faithful +> md/docx/midi from real geometry: 0 ungrounded claims, SACRED polarity). Per Will's +> steer, the DELIVERABLE is NATIVE: the seam lives on the existing EL realizer as +> **surface-as-profile** — see `../src/surface-profile.el` and +> `../tests/examples/surface-profile-demo.el` (compiles + runs through elc → C → +> binary). The concepts below (one geometry-carrying frame; surface = a pluggable +> profile; plan/realize; deterministic-from-meaning) are exactly what the native +> module implements. Keep this package as the validated proof; build native. + +# Efferent Multimodal Projector + +**geometry → any surface, faithfully.** Neuron's own document-generation faculty: +the efferent twin of the ingest organ. Ingest is afferent (world → geometry); +this is efferent (geometry → an arbitrary-format document / any modality). + +Built against the **proven** realizer faculty (neuron-talk sidecar `:8756`, +artifact `art-7affa557`). The live soul (`:8742` / `:7770`) is contacted **only** +through the read-only, GET-only `engram_client` — never mutated. + +## The pipeline (surface-agnostic) + +``` +geometry region + surface/format spec + → PLAN (manifold → document skeleton/DAG; the geometry IS the outline) plan.py + → REALIZE (proven realizer, scaled sentence → passage, each section faithful) realize.py + → COHERE (document-level flow / transitions, not stitched sentences) cohere.py + → EMIT (pluggable SurfaceProjector → the target surface) projectors/ +``` + +**The surface is a PARAMETER.** `pipeline.build_ir(...)` builds ONE +surface-neutral `DocumentIR` (`document_ir.py`); `pipeline.emit(doc, surface)` +projects it to whichever surface you name. Markdown, docx, and MIDI are the same +IR emitted three ways. + +## The pivot: a geometry-carrying IR + +`DocumentIR` is **not** a text tree. Every `Block` carries BOTH: +- `.sentences` — realized faithful text (what **text** projectors read), +- `.provenance` — the source geometry: `subj_id / relation / obj / polarity / + confidence / importance / salience / node_id` (what **music / image / video** + projectors read). + +That single decision is what makes the projector multimodal: text renders the +words; music/image decode the geometry. A claim with no provenance cannot exist +in the IR — faithfulness is structural. + +## The one shared seam + +`projectors/base.py` — `SurfaceProjector.project(frame: DocumentIR) -> bytes` +(+ `surface / media_type / ext / modality / profile`). Register with +`register()`. Adding a surface changes nothing upstream. + +`TwoStageProjector` blesses the peer plan/realize decomposition: +`spec = plan(frame)`, `bytes = realize(spec)`, `project = realize∘plan`; the +`profile` is the pluggable per-surface knob (text lang-profile, music +instr/mode-profile). `projectors/midi.py` is the reference two-stage impl. + +## Surfaces + +| surface | modality | status | emitter | +|---|---|---|---| +| `markdown` | text | landed | own (str) | +| `docx` | text | landed | own minimal OOXML (stdlib `zipfile`+XML, no lib) | +| `midi` | audio | landed (symbolic-music proof) | own minimal SMF (stdlib `struct`, no lib) | +| `audio` (WAV) | audio | peer agent (additive synth) | conforms to `TwoStageProjector` | +| `image` | image | documented seam | `projectors/seams.py` | +| `video` | video | documented seam (image×sound×time) | `projectors/seams.py` | + +Music maps: relation → scale degree (same relation → same pitch), **polarity → +major/minor third (SACRED negation is audible)**, confidence → duration, +importance → velocity, section → register. Deterministic projection from meaning +— nothing invented. + +## Faithfulness + +`provenance.py` audits the IR: **zero** ungrounded claims, SACRED polarity +preserved (negations reported, never dropped), COHERE introduces no new geometry +(connectives are marked). `trace_table()` emits the geometry → section → claim +table. + +## Run + +```bash +PY=~/Desktop/lang-realizers/venv/bin/python +PYTHONPATH=~/Desktop/neuron-talk:~/Desktop/lang-realizers $PY generate.py +# writes ./out/{neuron-self,engram-temporal}.{md,docx,mid} + *.audit.json + *.provenance.md +``` + +Requires the proven realizer env (spaCy + the neuron-talk/lang-realizers engine) +and the read-only engram at `:8742`. diff --git a/elp/projector/cohere.py b/elp/projector/cohere.py new file mode 100644 index 0000000..f875313 --- /dev/null +++ b/elp/projector/cohere.py @@ -0,0 +1,79 @@ +"""cohere.py — COHERE stage: document-level flow, not stitched sentences. + +Fidelity is REALIZE's job; FLOW is this stage's. The hard part beyond sentence +fidelity is that a document must read as one thing. We add connective tissue at +the passage level: + + * an opening abstract that names what the document covers (built ONLY from the + section headings that already exist — it introduces no new claim), + * a short transition lead into each section after the first, drawn from a + fixed set of discourse connectives ("Beyond that,", "Relatedly,", ...) that + carry no propositional content, + * ordering so the highest-grounded section leads. + +CRITICAL: every connective is marked ``kind="connective"`` in its provenance, so +the faithfulness audit can prove COHERE introduced ZERO new geometry claims. A +transition is discourse glue, never a fact. +""" +from __future__ import annotations + +from document_ir import Block, DocumentIR, Provenance + +# discourse connectives — pure flow, no propositional content +_TRANSITIONS = [ + "Beyond that,", "Relatedly,", "In the same region,", "From there,", + "Alongside this,", "Further,", "Turning to the next facet,", +] + + +def _connective_prov() -> Provenance: + return Provenance(subj_id=None, subject=None, relation="", obj=None, + polarity="aff", confidence=1.0, node_id=None, + kind="connective") + + +def _abstract_block(doc: DocumentIR) -> Block: + """A grounded opening: names the sections, asserts nothing new.""" + headings = [s.heading for s in doc.sections] + if not headings: + return Block(role="lead") + if len(headings) == 1: + body = f"This document, generated from Neuron's geometry, covers {headings[0]}." + else: + listed = ", ".join(headings[:-1]) + f", and {headings[-1]}" + body = ("This document is projected directly from Neuron's meaning-geometry. " + f"It traces {listed}.") + b = Block(role="lead") + b.sentences.append(body) + b.provenance.append(_connective_prov()) + return b + + +def cohere_document(doc: DocumentIR, *, add_abstract: bool = True, + add_transitions: bool = True) -> DocumentIR: + """Order sections by grounding, add abstract + transitions (flow only).""" + # order: strongest-grounded section (mean confidence x #claims) first, + # but keep an explicitly-first section if the plan pinned one via level 1. + def _score(sec): + provs = [p for p in sec.all_provenance() if p.kind == "fact"] + if not provs: + return 0.0 + mean_conf = sum(p.confidence for p in provs) / len(provs) + return mean_conf * len(provs) + + doc.sections.sort(key=_score, reverse=True) + + if add_transitions: + for i, sec in enumerate(doc.sections): + if i == 0 or not sec.blocks: + continue + lead = _TRANSITIONS[(i - 1) % len(_TRANSITIONS)] + first = sec.blocks[0] + if first.sentences: + # prepend the connective to the first sentence (flow, no new claim) + first.sentences[0] = f"{lead} {first.sentences[0][0].lower()}{first.sentences[0][1:]}" + + if add_abstract: + doc.meta["abstract"] = _abstract_block(doc) + + return doc diff --git a/elp/projector/document_ir.py b/elp/projector/document_ir.py new file mode 100644 index 0000000..ba228b5 --- /dev/null +++ b/elp/projector/document_ir.py @@ -0,0 +1,111 @@ +"""document_ir.py — the surface-neutral, GEOMETRY-CARRYING document intermediate. + +This is the pivot of the whole efferent projector. A DocumentIR is NOT a text +tree. It is a projection of a meaning-geometry region that carries, at every +leaf, BOTH: + + * the realized surface text (``Block.sentences``) — what a TEXT projector reads, + * the source geometry (``Block.provenance``) — what a MUSIC / IMAGE / + VIDEO projector reads. + +Because the IR holds the geometry, not just the words, the SAME +plan -> realize -> cohere pipeline drives every surface. A markdown projector +renders the sentences; a music projector reads the provenance edges (salience, +importance, polarity, relation) and maps them onto a symbolic-music surface; +an image/video projector (documented seam) would read the same geometry. + +Nothing in this module invents content. Every :class:`Provenance` points at a +real engram node id and a real relation. That is the faithfulness contract made +structural: a claim with no provenance cannot exist in the IR. +""" +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + + +# --------------------------------------------------------------------------- # +# Provenance — the geometry an emitted claim traces to. FAITHFULNESS is here. +# --------------------------------------------------------------------------- # +@dataclass +class Provenance: + """One geometry edge behind one realized claim. + + ``kind`` distinguishes a FACT (a structural edge asserted by the geometry, + spoken as fact) from an INTERPRETATION (something attributed, spoken with + attribution) — the facts-as-facts + interpretations-attributed discipline + (memory 80927e26). ``polarity`` is SACRED: a negated edge stays negated. + """ + subj_id: str | None # source engram node id of the subject + subject: str | None # normalized subject surface + relation: str # predicate lemma (e.g. "use", "contain", "be") + obj: str | None # normalized object / complement surface + polarity: str = "aff" # "aff" | "neg" (SACRED — never silently flipped) + confidence: float = 0.0 # extraction confidence in [0,1] + node_id: str | None = None # engram node the claim was extracted from + kind: str = "fact" # "fact" | "interpretation" + importance: float = 0.0 # source node importance (drives music/emphasis) + salience: float = 0.0 # source node salience + + def trace(self) -> str: + arrow = "-->" if self.polarity == "aff" else "--NOT-->" + return (f"[{(self.node_id or '?')[:8]}] {self.subject!r} {arrow}" + f"{self.relation} {self.obj!r} (conf {self.confidence:.2f})") + + +@dataclass +class Block: + """A passage: one or more faithful sentences + the geometry they trace to. + + ``sentences`` and ``provenance`` are index-aligned where possible: sentence + ``i`` was realized from ``provenance[i]``. A COHERE transition sentence with + no new geometry carries a provenance whose ``kind == "connective"`` so the + audit can see it introduced no new claim. + """ + sentences: list[str] = field(default_factory=list) + provenance: list[Provenance] = field(default_factory=list) + role: str = "body" # "body" | "lead" | "transition" + + def text(self) -> str: + return " ".join(s.rstrip(". ") + "." for s in self.sentences if s.strip()) + + +@dataclass +class Section: + heading: str + level: int = 2 # markdown heading level / outline depth + blocks: list[Block] = field(default_factory=list) + seed_ids: list[str] = field(default_factory=list) # geometry nodes of section + summary: str = "" # one-line grounded gloss (for pptx bullets / TOC) + + def all_provenance(self) -> list[Provenance]: + out: list[Provenance] = [] + for b in self.blocks: + out.extend(b.provenance) + return out + + +@dataclass +class DocumentIR: + """The surface-neutral document. Built ONCE, projected to ANY surface.""" + title: str + subtitle: str = "" + sections: list[Section] = field(default_factory=list) + seed_id: str | None = None # the geometry region root + format_spec: dict[str, Any] = field(default_factory=dict) # requested shape + meta: dict[str, Any] = field(default_factory=dict) + + # -- geometry facets (what non-text projectors consume) ----------------- # + def all_provenance(self) -> list[Provenance]: + out: list[Provenance] = [] + for s in self.sections: + out.extend(s.all_provenance()) + return out + + def claim_count(self) -> int: + return sum(1 for p in self.all_provenance() if p.kind in ("fact", "interpretation")) + + def ungrounded_count(self) -> int: + """Claims with no traceable node — MUST be zero for a faithful doc.""" + return sum(1 for p in self.all_provenance() + if p.kind in ("fact", "interpretation") and not p.node_id) diff --git a/elp/projector/generate.py b/elp/projector/generate.py new file mode 100644 index 0000000..d19fa02 --- /dev/null +++ b/elp/projector/generate.py @@ -0,0 +1,81 @@ +"""generate.py — drive the projector: one geometry region -> many surfaces. + +Proves the thesis with REAL output: builds ONE surface-neutral DocumentIR from +Neuron's OWN self-geometry (read-only against the live soul via the proven +faculty), then EMITS it to Markdown, docx, and MIDI — the same plan/realize/ +cohere, three surfaces. Writes the files + the faithfulness audit to ./out/. +""" +from __future__ import annotations + +import json +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, _HERE) + +import pipeline # noqa: E402 +import provenance # noqa: E402 +from geometry import load_self_region # noqa: E402 + +OUT = os.path.join(_HERE, "out") + + +def _emit_all(doc, stem): + """Emit one IR to every text/audio surface + audit + provenance.""" + for surface in ("markdown", "docx", "midi"): + data = pipeline.emit(doc, surface) + proj = pipeline.get_projector(surface) + path = os.path.join(OUT, f"{stem}.{proj.ext}") + with open(path, "wb") as f: + f.write(data) + print(f" emitted {surface:9s} -> {os.path.basename(path)} ({len(data)} bytes)") + a = provenance.audit(doc) + with open(os.path.join(OUT, f"{stem}.audit.json"), "w") as f: + json.dump(a, f, indent=2) + with open(os.path.join(OUT, f"{stem}.provenance.md"), "w") as f: + f.write(provenance.trace_table(doc)) + print(" audit:", {k: a[k] for k in ("claims", "ungrounded_claims", + "negations_preserved", "distinct_source_nodes", "faithful")}) + return a + + +def main(): + os.makedirs(OUT, exist_ok=True) + print("surfaces registered:", pipeline.available_surfaces()) + + # ---- Document 1: Neuron's self-description (marquee) ------------------- # + print("\n[1] Neuron self-description") + region = load_self_region(max_nodes=9) + print(" self region:", region) + doc1 = pipeline.build_ir( + None, region=region, + title="Neuron: A Self-Description from Its Own Geometry", + subtitle="Projected efferently from the engram — every claim traces a node.", + format_spec={"genre": "self-description", "register": "expository"}, + max_sections=5, conf_floor=0.6) + print(f" IR: {len(doc1.sections)} sections, {doc1.claim_count()} claims, " + f"ungrounded={doc1.ungrounded_count()}") + _emit_all(doc1, "neuron-self") + + # ---- Document 2: a coherent, clean whitepaper-style section ------------ # + print("\n[2] Whitepaper-style section (coherent clean region)") + doc2, _ = pipeline.project( + ["chronoception", "time", "awareness", "engram", "temporal"], + surface="markdown", + title="Temporal Awareness in the Engram", + subtitle="A section projected from the geometry of chronoception.", + format_spec={"genre": "whitepaper-section", "register": "technical"}, + max_sections=4) + print(f" IR: {len(doc2.sections)} sections, {doc2.claim_count()} claims, " + f"ungrounded={doc2.ungrounded_count()}") + _emit_all(doc2, "engram-temporal") + + # echo both markdowns so they are visible in the run log + for stem, doc in (("neuron-self", doc1), ("engram-temporal", doc2)): + print(f"\n===== GENERATED MARKDOWN — {stem} =====\n") + print(pipeline.emit(doc, "markdown").decode()) + + +if __name__ == "__main__": + main() diff --git a/elp/projector/geometry.py b/elp/projector/geometry.py new file mode 100644 index 0000000..3fb52cb --- /dev/null +++ b/elp/projector/geometry.py @@ -0,0 +1,129 @@ +"""geometry.py — READ-ONLY loader for a meaning-geometry region. + +The efferent projector never writes to the soul. This module reaches the +geometry through the PROVEN, read-only neuron-talk faculty (``engram_client``, +GET-only, which physically refuses non-GET methods) against the running sidecar +soul. The live daemon :8742 / :7770 is contacted ONLY through that read-only +client — never mutated. + +A "region" is a seed node plus a bounded neighborhood: the manifold that will +become the document's skeleton. We pool a few single-term lexical searches +(the engram search is a single-term matcher) and, when available, walk one hop +of reified neighbors, then rank by self/importance signal. +""" +from __future__ import annotations + +import os +import sys + +# Wire in the proven faculty (own-the-core: we reuse it, we do not fork it). +_NT = os.path.expanduser("~/Desktop/neuron-talk") +_LR = os.path.expanduser("~/Desktop/lang-realizers") +for _p in (_NT, _LR): + if _p not in sys.path: + sys.path.insert(0, _p) + +from engram_client import ReadOnlyEngramClient # noqa: E402 + + +class Region: + """A geometry region: ranked nodes + the reified edges among them.""" + + def __init__(self, seed: str, nodes: list[dict], edges: list[dict]): + self.seed = seed + self.nodes = nodes # ranked engram node dicts + self.edges = edges # [{src, dst, edge, ...}] + self.by_id = {n["id"]: n for n in nodes if n.get("id")} + + def __repr__(self): + return f"" + + +def _prose_quality(content: str) -> float: + """Reward clean expository prose; penalize shouty banner-dense nodes. + + A high ALLCAPS-word ratio or very short content signals a banner/telegraphic + memory node that extracts into garbage. Clean declarative prose scores high. + """ + if not content or not content.strip(): + return 0.0 + words = content.split() + if len(words) < 8: + return 0.1 + caps = sum(1 for w in words if len(w) > 2 and w.strip(".,:;'\"-").isupper()) + caps_ratio = caps / max(1, len(words)) + # sentences with lowercase interior words read as prose + lower = sum(1 for w in words if w[:1].islower()) + lower_ratio = lower / max(1, len(words)) + return max(0.0, 1.2 * lower_ratio - 2.0 * caps_ratio) + + +def _relevance(content: str, terms: list[str]) -> float: + """Topical relevance to the seed terms — keeps a region ON-THEME so a clean + but off-topic node cannot hijack the document.""" + if not terms: + return 0.0 + low = (content or "").lower() + hits = sum(1 for t in terms if t.lower() in low) + return hits / max(1, len(terms)) + + +def _node_rank(n: dict, terms: list[str] | None = None) -> float: + return (float(n.get("importance") or 0.0) * 2.0 + + float(n.get("salience") or 0.0) + + 1.5 * _prose_quality(n.get("content") or "") + + 2.0 * _relevance(n.get("content") or "", terms or []) + + (0.5 if (n.get("content") or "").strip() else 0.0)) + + +def load_region(seed_terms: list[str] | str, *, client: ReadOnlyEngramClient | None = None, + max_nodes: int = 10, per_term: int = 20, hop: bool = True) -> Region: + """Pull a bounded geometry region around ``seed_terms`` (read-only). + + ``seed_terms`` may be a single string or several probe terms; results are + pooled and de-duplicated. When ``hop`` and the reified neighbor endpoint is + live, one hop of neighbors is folded in so the region is a real + neighborhood, not just a keyword hit list. + """ + client = client or ReadOnlyEngramClient() + if isinstance(seed_terms, str): + seed_terms = [seed_terms] + + pool: dict[str, dict] = {} + for term in seed_terms: + for n in client.search(term, limit=per_term): + if isinstance(n, dict) and n.get("id"): + pool.setdefault(n["id"], n) + + ranked = sorted(pool.values(), key=lambda n: _node_rank(n, seed_terms), + reverse=True) + nodes = ranked[:max_nodes] + + edges: list[dict] = [] + if hop and nodes: + present = {n["id"] for n in nodes} + for n in list(nodes): + try: + for nb in client.neighbors(n["id"]): + node = nb.get("node") if isinstance(nb, dict) else None + edge = nb.get("edge") if isinstance(nb, dict) else None + if node and node.get("id"): + edges.append({"src": n["id"], "dst": node["id"], + "edge": edge}) + # fold a strong neighbor into the region (bounded) + if (node["id"] not in present and len(nodes) < max_nodes + 6 + and _node_rank(node, seed_terms) > 0.4): + present.add(node["id"]) + nodes.append(node) + except Exception: # noqa: BLE001 — read-only best-effort; never fatal + continue + + return Region(seed=", ".join(seed_terms), nodes=nodes, edges=edges) + + +def load_self_region(client: ReadOnlyEngramClient | None = None, + max_nodes: int = 10) -> Region: + """The self/identity region — Neuron's own geometry, for self-description.""" + return load_region(["self", "identity", "Neuron", "values", "memory", + "imprint", "consciousness"], + client=client, max_nodes=max_nodes) diff --git a/elp/projector/pipeline.py b/elp/projector/pipeline.py new file mode 100644 index 0000000..040ac7a --- /dev/null +++ b/elp/projector/pipeline.py @@ -0,0 +1,67 @@ +"""pipeline.py — the Efferent Multimodal Projector, top level. + + geometry region + surface/format spec + -> PLAN (manifold -> document skeleton/DAG) + -> REALIZE (proven realizer, sentence -> passage, each section faithful) + -> COHERE (document-level flow / transitions, not stitched sentences) + -> EMIT (pluggable SurfaceProjector -> the target surface) + +THE SURFACE IS A PARAMETER. ``project(...)`` builds the geometry-carrying +DocumentIR once, then hands it to whichever surface projector the caller named. +Markdown, docx, and midi (music) are all the SAME IR emitted differently. That +is the efferent multimodal projector: geometry -> any surface. +""" +from __future__ import annotations + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, _HERE) +sys.path.insert(0, os.path.join(_HERE, "projectors")) + +from cohere import cohere_document # noqa: E402 +from document_ir import DocumentIR # noqa: E402 +from geometry import Region, load_region # noqa: E402 +from plan import plan_document # noqa: E402 +from realize import realize_document # noqa: E402 + +# registering the projectors (import for side-effect: each self-registers) +import projectors.markdown # noqa: E402,F401 +import projectors.docx # noqa: E402,F401 +import projectors.midi # noqa: E402,F401 +import projectors.seams # noqa: E402,F401 +from projectors.base import available_surfaces, get_projector # noqa: E402 + + +def build_ir(seed_terms, *, title: str, subtitle: str = "", + format_spec: dict | None = None, + region: Region | None = None, + max_sections: int = 8, conf_floor: float = 0.55) -> DocumentIR: + """geometry -> PLAN -> REALIZE -> COHERE = the surface-neutral DocumentIR.""" + region = region or load_region(seed_terms) + doc = plan_document(region, title=title, subtitle=subtitle, + format_spec=format_spec or {}, + conf_floor=conf_floor, max_sections=max_sections) + doc = realize_document(doc) + doc = cohere_document(doc) + return doc + + +def emit(doc: DocumentIR, surface: str) -> bytes: + """EMIT: project the built IR onto one surface (surface = a parameter).""" + return get_projector(surface).project(doc) + + +def project(seed_terms, *, surface: str, title: str, subtitle: str = "", + format_spec: dict | None = None, region: Region | None = None, + max_sections: int = 8) -> tuple[DocumentIR, bytes]: + """The full efferent projection: geometry + surface -> (IR, bytes).""" + doc = build_ir(seed_terms, title=title, subtitle=subtitle, + format_spec=format_spec, region=region, + max_sections=max_sections) + return doc, emit(doc, surface) + + +__all__ = ["build_ir", "emit", "project", "available_surfaces", + "get_projector", "load_region", "DocumentIR"] diff --git a/elp/projector/plan.py b/elp/projector/plan.py new file mode 100644 index 0000000..e80fb6d --- /dev/null +++ b/elp/projector/plan.py @@ -0,0 +1,192 @@ +"""plan.py — PLAN stage: geometry region -> document skeleton (a DAG/outline). + +The manifold becomes the skeleton. We extract faithful propositions from the +region's nodes (the proven neuron-talk extractor, SACRED polarity preserved), +apply a quality floor, then GROUP them into sections. Grouping is by source +node — each engram node is one coherent topic, so one salient node becomes one +section. The section ORDER is the node ranking (importance/salience): the +geometry decides the outline, not a template. + +Output: a DocumentIR whose sections carry seed node ids and empty blocks. REALIZE +fills the blocks; the plan owns the structure. +""" +from __future__ import annotations + +import os +import re +import sys + +_NT = os.path.expanduser("~/Desktop/neuron-talk") +_LR = os.path.expanduser("~/Desktop/lang-realizers") +for _p in (_NT, _LR): + if _p not in sys.path: + sys.path.insert(0, _p) + +import propositions # noqa: E402 (the proven, faithful extractor) + +from document_ir import DocumentIR, Section # noqa: E402 +from geometry import Region # noqa: E402 + + +# --------------------------------------------------------------------------- # +# Proposition quality — keep only clean, well-grounded claims. +# --------------------------------------------------------------------------- # +_JUNK_RE = re.compile(r"[.][a-z]{1,3}\b|[^A-Za-z0-9 '\-]") # ".o", stray symbols + + +def _has_banner_token(s: str) -> bool: + """True if any word is an ALLCAPS banner token (DHARMA, ENGRAM, MEASURED).""" + for w in (s or "").split(): + core = w.strip(".,:;'\"-") + if len(core) > 2 and core.isupper(): + return True + return False + + +def _clean_prop(p, floor: float) -> bool: + if p.confidence < floor: + return False + if not p.subject or not (p.object or (p.obj_np is not None)): + return False + subj = (p.subject or "").strip() + obj = (p.object or "").strip() + if len(subj) < 2: + return False + # banner-derived shouty fragments read as garbage in prose + if _has_banner_token(subj) or _has_banner_token(obj): + return False + if propositions._is_shouty(p.sentence or ""): + return False + # junk tokens: file-extension fragments (".o"), stray non-word symbols + if _JUNK_RE.search(subj) or _JUNK_RE.search(obj): + return False + # a proposition whose object repeats the subject is usually a parse artifact + if obj and subj.lower() == obj.lower(): + return False + # a bare copula with no real complement ("X is it") reads as noise + if p.predicate == "be" and obj.lower() in ("it", "no", "nothing", "empty", ""): + return False + return True + + +def _dedup(props): + """Drop duplicate claims. Two axes: (a) identical (pred,obj,polarity), and + (b) same (subject,predicate) — which collapses a mis-split compound like + "detection is post-hoc eval" -> "Detection is post/hoc/eval" into one claim + (keep the highest-confidence surface).""" + props = sorted(props, key=lambda p: p.confidence, reverse=True) + seen_po, seen_sp, out = set(), set(), [] + for p in props: + subj = (p.subject or "").lower() + po = (p.predicate, (p.object or "").lower(), p.polarity) + sp = (subj, p.predicate, p.polarity) + if po in seen_po or sp in seen_sp: + continue + seen_po.add(po) + seen_sp.add(sp) + out.append(p) + return out + + +# --------------------------------------------------------------------------- # +# Heading derivation — a clean human heading from a node. +# --------------------------------------------------------------------------- # +_HEADING_RE = re.compile(r"^\s*#{1,4}\s+(.{2,70})\s*$", re.M) +# node-type / system labels that are NOT topical headings +_NONTOPIC_LABEL = re.compile(r"^(memory|node|knowledge|doc|session)[:/]", re.I) + + +def _titlecase_banner(s: str) -> str: + """A shouty banner ("CHRONOCEPTION — SCALE-INVARIANCE") makes a fine title + once Title-cased. Keep short acronyms uppercase.""" + def fix(w): + core = w.strip("—-:,.") + if len(core) <= 3 and core.isupper(): + return w # acronym + return w.capitalize() + return " ".join(fix(w) for w in s.split()) + + +def _clean_heading(text: str) -> str | None: + """First line only, no markdown, capped, banner Title-cased. None if unusable.""" + if not text: + return None + line = text.strip().splitlines()[0] + line = re.sub(r"^#+\s*", "", line).strip().strip("#").strip() + # cut at a natural break so a long banner heading stays a heading, not a para + for sep in (" — ", " – ", ": ", ". "): + if sep in line and len(line) > 48: + line = line.split(sep)[0].strip() + break + if not (3 <= len(line) <= 64): + return None + if propositions._is_shouty(line): + line = _titlecase_banner(line) + return line or None + + +def _heading_for(node: dict, fallback: str) -> str: + label = (node.get("label") or "").strip() + content = node.get("content") or "" + candidates: list[str] = [] + # a node-type label ("memory:remembered") is never a topic — skip it + if label and not _NONTOPIC_LABEL.match(label): + candidates.append(label) + m = _HEADING_RE.search(content) + if m: + candidates.append(m.group(1)) + # the leading banner/first sentence of the content is often the real title + first = re.split(r"(?<=[.\n])", content.strip(), maxsplit=1)[0] if content.strip() else "" + candidates.append(first) + for c in candidates: + h = _clean_heading(c) + if h: + return h + return fallback + + +def plan_document(region: Region, *, title: str, subtitle: str = "", + format_spec: dict | None = None, + conf_floor: float = 0.55, + max_sections: int = 8, + max_claims_per_section: int = 6) -> DocumentIR: + """Region -> DocumentIR skeleton. The geometry dictates the outline.""" + format_spec = format_spec or {} + doc = DocumentIR(title=title, subtitle=subtitle, + seed_id=region.nodes[0]["id"] if region.nodes else None, + format_spec=format_spec) + + made = 0 + seen_headings: set[str] = set() + for node in region.nodes: + if made >= max_sections: + break + props = propositions.extract(node.get("content") or "", + node_id=node.get("id"), + node_importance=float(node.get("importance") or 0.0), + max_sentences=10) + props = [p for p in props if _clean_prop(p, conf_floor)] + props = _dedup(props) + props.sort(key=lambda p: p.confidence, reverse=True) + props = props[:max_claims_per_section] + if not props: + continue + heading = _heading_for(node, fallback=f"Region {made + 1}") + # cross-section dedup: a topic appears once. Distinguish by top claim + # subject, else drop the collision so the outline stays clean. + if heading.lower() in seen_headings: + subj = (props[0].subject or "").strip().title() + alt = f"{heading}: {subj}" if subj and subj.lower() not in heading.lower() else None + if alt and alt.lower() not in seen_headings and len(alt) <= 64: + heading = alt + else: + continue + seen_headings.add(heading.lower()) + sec = Section(heading=heading, level=2, seed_ids=[node["id"]]) + # stash the planned propositions on the section for REALIZE + sec.__dict__["_planned_props"] = props + sec.__dict__["_node"] = node + doc.sections.append(sec) + made += 1 + + return doc diff --git a/elp/projector/projectors/__pycache__/base.cpython-313.pyc b/elp/projector/projectors/__pycache__/base.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1fd9212694345010322fb08a71ec4886edefcd3b GIT binary patch literal 5520 zcmbtY-EZ606(=QGlIgD`PTVw2o7Q8!zvqTE)VR&+%ux~eBGsuvO> z!DyVkn7oh@sSACgPu{04rbQa_sf!trSxV1?=k{xUbo-0~PD`1G?%lU@@Bh8?k%#ac zJOLf)+VPtR=cRUi7l$qki(#fL_3c*7YjzDpmHqrLiWeOQk6)l<1W?Ynq%#qfBc)nWfw{h0X; z&h)mZ8799h+_$*tyMkGc&*c<5$^@@lo-aBDOwrDe!}5xu(k87&fO-*krSpNM8|UK?2K!+ zn_xdP&wQ7ybbRhH9Bwe*5OwZr`r`cA>C>eGTYx$dzwpe<)AQ4_rG?UbflXJd#LMVz zcqLMf3z~Nw&YG^^T33ZB=D2=?*4iy-%#9`+Z`p=Z#L%{G@S^E9pmmO!4a2E(4<=%` zwg}56CL(SV6R)yJaM}}H<1H>?adMhA%@#^OUf5zb98~q#^74x9npX=REWjO;d&|qv zhLz{a6WjKWH`|_N=9$$r>O8Mqv#Q+9LCdXNAka1L7~r$K+zo-)3_bgQ_xe9d*>0?YSexeM%dSc#dgyFahQAUokSnZdiAzhORnbrqHraqV(4t8zNz;7PKaqSON zigGT_g=&-nq%nnR6y*gr(`?zCk|R`>%`WN-Y=wu}_6(~Tmfxtg?I?fe$|a_spPnzB zVR~tLw!li)xait1j0gUG|r5+Ll{=~clg_B_tL}5Q( z6GoGt+>#SHQb1;~rd_A>L3qneh{h;D;$ZCMf0G*yj+uy8*to4py2kot~SWEtSu}L^EX|M|t`Zne1{Y zJhjDD)>?_!+VL)Na&pn8aO#>ci*_IDm zM1~`4AOS*SdM$1eV!XmUZr2KgL7`!DA&wkjI}8LP$GA#QTQ$qX5!{pr6#{6NUzC31 zv@SUwkz2`85sy3q0ANe*j^uO2vmU!VKX-QKQc2Su5L;%s165+aTjb3VbP-5LkFs&F zB`ZRuI$>qavH@I__6R?i0JvL3_Vex<(mjMXJkoL7e!P$i@g6LIseUIaOg%7{yks22 zWv^wo>vaMEcGYoL3QXsmoiuI3^G+@=Tz7RJ7kZN6gjl;N$(-ekC z$C=Z`nc3-crLDHpc0O4^s8k?-gwErz)lPiw0kW{JNkB$akzdDKmTxwS2(YH>bc&+U z@f*!NLp2ZWlw)V-=Vm1UN5PUu=pr|omV*LsA#LmqjDUPXX$HlzwWX?)V+vZ3ghPT% z6c&NjB&msYp`XWH9x(S2>tk%_B;>L+m z6A3_h-OXjBWe720hfrYi+;0oVW5T!|Vo0HUN4b*-Qj`}#5)XQG3}eCj#YK+`uXx?E z?cy1JjoWUEK(dG~L%7a_&&`J8+HM_Hu2^&1PF2EZk=w-rVxV2+#p|xPimH!w1cKvF z7)I^b@s(r74=0~LZXQ22c?_FdMW`2cO}&vXYiZn=&7E7IwhcR48L3oi zmhgPra=7DSIa8^?LKt9WOL?qR1a+^$zDmWTB4Sny-xtukT!&>Ex4%DY`rYdnC4IBbmv^*vE* zJfx3^;d*~=G}yf(@xOTuav6U_#Ll${;n9bXcNvPH3|3hVB*Nw5zHmJEU{L^c=m4?$Z?p#FA?5Fsp(8Xx{P+H||q6Eud^ zsK3`F=A*9k1j!RhQL^WzV)Vy)4CN$DZ$}OXv_x3%JZcBf?IDMOCRFqgN?whcNXqG` z8ZR&Zw?u&69y)BuPVI*lwI5^(+06E^(?fHL)?b#@(5N9u&{n~lhK2e-?=N9fPWTSk zQUtwtiUA^nM23hAgOtSx4e6co;SfceZYx(n!;7nT)bbrQ7%jVgxA)@Jk`S&C^cosL zkW7~#MV1Z5c7&BI;+L^|lQLpexjX#iTJ}kqBmb)yB8Ml%b0E2d*oz;)Al&o7@tMeR z!J|7Rh?aEXQXoPZ{lMXbOA@AE$KbZIsLV$%w7BhFn2mC{JB7iby8Wg1DMbnlS)a$c z$P?Lat`LYGB5Q<})P;K_=BlzWGWy{=KY!&DUzgWMj;>{ohA2arg@d{P zit%G~-a#Se*uU@#7Tu<%a7}>pg`sOIG5{u_=n(L{tjj;pR_QrbZ z!2Oikcldr9~Os2=gY@mSR|{VOQP`5iCtZwCF9`O>)=ePRA5=2n-HqohO0`sC1F zSi_A}I=5W}or_A(b@Xq9G!WjI`n#piqcd0-@Y(PW11`i%n0ymoZ$Aheb|nA)%uVYr z!-u}YC&e}f_O7M&hC#PgM#={%V-14`Wt_r31KzqqddN#x7nPS2!*~_DLcuDpo=$NH z4l6IF;xSiy>x&9K^puiImTx@exIK&kkkG##XyL08=%apz-N;O_hH;IvTvCuJg6u`! zk(nQ)Jy-a=np35DwpwHe|4>k07q$uFWjwcnuXhRrX2}eHY^`Sw-5nlVd-Rpx*Z*+k zo}$jEhwdJF`nT5lp<`=LJby2#?tgJpQAdtrssH#t2ln3j(N_bHucaOjtp`5ctLtHg zDZp0OvRAs^jV;mh>J;`$eYQn%u*bM&STs3LCP3HA}0RkVX>c101YU;a#ihATWEwp(H=Z zzMK7Ea7<(nfz$-lT#m^`t*g*CqJ9~{M2Wse)qzclV41JdFqQ(e|l;C#Y^i) zX4gl{e^%!HrX0SPRuZH46Gzm+TW@bF_}NTi;vP+We@T5r&3~(apznGvzuLbx@XWe$ V^nN;_?z^w0)F-|hR@E>*{sY~+*Q)>k literal 0 HcmV?d00001 diff --git a/elp/projector/projectors/__pycache__/docx.cpython-313.pyc b/elp/projector/projectors/__pycache__/docx.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cda3676b38d94aff74906b179aa0d3430f731eeb GIT binary patch literal 7274 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 G9P?iaqLiip literal 0 HcmV?d00001 diff --git a/elp/projector/projectors/__pycache__/midi.cpython-313.pyc b/elp/projector/projectors/__pycache__/midi.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c10b7bfc29b5d8f06c9e50df654bc0daaebad92a GIT binary patch literal 8329 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 A@Bjb+ literal 0 HcmV?d00001 diff --git a/elp/projector/projectors/__pycache__/seams.cpython-313.pyc b/elp/projector/projectors/__pycache__/seams.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d76fc4254ab32e5267fcaab25730ee5d242fd01c GIT binary patch literal 3430 zcmb_eL2nz!6`tjiBDu0eJBCw7b<>fYwjv^ts$)lul16YW!J%wZfhhY@0gKV@kX(DY z!_Le~q7W3yp$T$n9iTz3%_+#K%_-OZga%GgW!nHnbMnoGT%^aoH(b(;E%ee7;LPmI z+u1koeee6`Xkx-4@cV;vaC@&v$T#>f_?Z<#tfOC9gnUREM3RQp$Xv75GBQJPY+tjb zZThUtZe=&pHwGJEzcDfT)E_sjE&Ev8($lv8v*oE{_AFk46XEZvKGUB!r^{Y**2d+y z8RxE@SeuX&l$<3qq%k%{8u?49t(-J-1(+*tOqt+ed&iFg4vR-AQIrQ4(bJop{;g%@{tq`6Odv&kh4sWG5UA!+T#<*Tcd zMLxY%TUlMFDwa*=aaD1g%{Et@GRbLueUk>AZphPpX1iQc-R5*V4nm(w7KCaEEWLK1 zxu@sgn=}%diXASENx6c%^~`3Hw*sZX51%qcn}Jk%-V2#hsaHyvtpo7bxI*9BSiiN} zpn;~$^LSS)ry1)Q+=@grulb&)T`6wEcZF1RH_&YngK5;GaaU;xp`c!yMJ-;TtITT; zwf znPdLGldN}xNKw%o@r>5$S63UG6zT>;slLPDoBd+VEr*T4x=I&nQd*S6TvuD_-h^aVQ5Z zaDa7m-8l4%l)?zPNj669j4>gvWhQ~5VJ(|ZjC7OaC3`@9+$v}4`)`ayHBgids|l(Z zNu>|X9@mB%bcL>@B{lGMiI(l;bY1A$V?J4xQphBqQcrR+Ekk$kPC1ig6%U(<4P2)1 z43s9nuHIB!s_JeKhSe3m!$Z+Uda6b|2Jiqecwx-5&okCsxVXK*URnI|AJg#8vG=B6MY*1rme$XIT70ULz9k~ zqgG-=6JLgpAZsYP9SUF^2>K3>Smc3RAt+TBP6ibovK~?es$c}f4(5PH;1?`8AkR6< zeIUBQEc0)J@n)rv$rpU9ZprS&#QfCD*Y<$ijc`b_o!J#L2H3F zRa+7vSD~B8bOYH%Mn*#J(`?GJvy2Xyb^dk9P#On9G)F_uWCXPZN)3DosYKlb0#2oi zb^@#&q;W&3oo();fEf)z%)qO-(EnhqzPUd9R>kDH{;4CMg-FZqVRIH6Otl<`CUH94 z516ZaU7pyWvq{0o8yr@iK%?(t^8{jkfOFVj{uG7gU2-^i=HTk#sdEQwhfe9>%3!v&HjAHp!jo;|O2*|KP}|&tn{65hgjK9{WYpRXg2_i49HsZ&>SxW++k=MXExt z2uQm^JsL(Yt!&5yUVrBA@Lm)2iw^^uqOEMMAm+VJ;0M(wwP;p(AarHWJ&|T@ko}*) z4G2Z>7>RmX5$l`-{3Ez$@m=8L0ALIEGZ0~@f&P(ROl6So!F9{jfQUl4CcyL_mudRt zG=$*E9R3*Ru))~<*Bsi#nUNggE9Fyi3OXdF{w)ni#m0$@P>HR2D#=1|wv#cqn{cU< zydOwptR&C2v25E&Zq2|D+W6q=>H^9Q6PAWaP?c>48=%Y~PCWY7f4Eege z2*yZH>Nn5;@mkiy%!HNw*EwRH|ArL*Mx1{T`@Or|Vym^{>gy*W`t7$HuIshXk5O s&p6f#cW*u-&^?+eTIX>L-NUoQD!%{xz2b9UlBusq!3_UGrc4q12Nu4|J^%m! literal 0 HcmV?d00001 diff --git a/elp/projector/projectors/base.py b/elp/projector/projectors/base.py new file mode 100644 index 0000000..d14540c --- /dev/null +++ b/elp/projector/projectors/base.py @@ -0,0 +1,106 @@ +"""base.py — the SurfaceProjector interface + registry. + +THE key abstraction of the efferent projector: a projector is a pure function +from the surface-neutral, geometry-carrying DocumentIR to bytes on a target +SURFACE. The surface is a PARAMETER. Adding a surface = registering one more +projector; nothing upstream (plan/realize/cohere) changes. + + DocumentIR --project--> bytes (per surface) + +A TEXT projector reads ``block.sentences``. A NON-TEXT projector (music, image, +video) reads ``block.provenance`` — the geometry the IR carries — and decodes it +onto its surface. Both consume the SAME IR. That symmetry is the whole design: +the realizer generalizes into a multimodal projector, geometry -> any surface. +""" +from __future__ import annotations + +from typing import Protocol, runtime_checkable + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from document_ir import DocumentIR # noqa: E402 + + +@runtime_checkable +class SurfaceProjector(Protocol): + """Geometry-document -> one surface. Implementations MUST be pure & faithful. + + THE ONE SHARED SEAM. Every surface — text, music, image, video — conforms to + this single contract: + + project(frame: DocumentIR) -> bytes + + where ``frame`` is the geometry-carrying meaning-geometry (the SemFrame at + document scale; a single utterance is the degenerate one-section frame). + + RECOMMENDED INTERNAL SHAPE (the peer music/text decomposition, blessed here + so all surfaces share it): a projector may split ``project`` into + + spec = self.plan(frame) # meaning-geometry -> surface-specific spec + bytes = self.realize(spec) # spec -> surface, via this projector's PROFILE + + ``project`` is then ``realize(plan(frame))``. The PROFILE (a text lang-profile, + a music instr/mode-profile, an image layout-profile) is a property of the + projector instance — the pluggable knob. See :class:`TwoStageProjector`. + + A TEXT projector's plan reads ``frame`` sentences; a MUSIC/IMAGE projector's + plan reads ``frame.all_provenance()`` — the geometry — and derives its spec + (pitch/harmony/rhythm, or layout) FROM the meaning, deterministically. Same + frame, different profile. + """ + + surface: str # "markdown" | "docx" | "midi" | "audio" | "image" | "video" + media_type: str # MIME type of the emitted bytes + ext: str # file extension (no dot) + modality: str # "text" | "audio" | "image" | "video" + profile: object # the pluggable per-surface profile (may be None) + + def project(self, doc: DocumentIR) -> bytes: + """Emit the document on this surface. Returns raw bytes.""" + ... + + +class TwoStageProjector: + """Optional base for the peer plan()/realize() decomposition. + + Subclasses implement ``plan(frame) -> spec`` and ``realize(spec) -> bytes``; + ``project`` is their composition. This is exactly the peer music interface + (spec = plan(frame, profile); surface = realize(spec, profile)) expressed so + that it still satisfies the single ``SurfaceProjector.project`` seam. Text, + music, and image projectors can all subclass this and remain interchangeable. + """ + + surface: str = "" + media_type: str = "" + ext: str = "" + modality: str = "" + profile: object = None + + def plan(self, doc: DocumentIR): # -> spec + raise NotImplementedError + + def realize(self, spec) -> bytes: + raise NotImplementedError + + def project(self, doc: DocumentIR) -> bytes: + return self.realize(self.plan(doc)) + + +_REGISTRY: dict[str, SurfaceProjector] = {} + + +def register(projector: SurfaceProjector) -> SurfaceProjector: + _REGISTRY[projector.surface] = projector + return projector + + +def get_projector(surface: str) -> SurfaceProjector: + if surface not in _REGISTRY: + raise KeyError(f"no projector registered for surface {surface!r}; " + f"have {sorted(_REGISTRY)}") + return _REGISTRY[surface] + + +def available_surfaces() -> list[str]: + return sorted(_REGISTRY) diff --git a/elp/projector/projectors/docx.py b/elp/projector/projectors/docx.py new file mode 100644 index 0000000..bc85961 --- /dev/null +++ b/elp/projector/projectors/docx.py @@ -0,0 +1,113 @@ +"""docx.py — the .docx surface projector: an OWN minimal OOXML emitter. + +Own-the-core: a .docx is just a ZIP of a few XML parts (WordprocessingML). We +emit it with the standard library only — ``zipfile`` + string XML — no +python-docx, no external dependency. This proves a "richer structured format" +surface without importing anyone else's toolkit. + +Parts emitted (the minimal valid set + a styles part for real headings): + [Content_Types].xml + _rels/.rels + word/_rels/document.xml.rels + word/styles.xml (Title / Heading1 / Heading2 / Normal) + word/document.xml (the content) + +Like the markdown projector it reads only the IR's realized sentences; it +invents nothing. The surface differs, the faithful content does not. +""" +from __future__ import annotations + +import io +import os +import sys +import zipfile +from xml.sax.saxutils import escape + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from document_ir import DocumentIR # noqa: E402 +from projectors.base import register # noqa: E402 + +_CONTENT_TYPES = """ + + + + + +""" + +_RELS = """ + + +""" + +_DOC_RELS = """ + + +""" + +_W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + +_STYLES = f""" + + + + + + + + + + + + + + +""" + + +def _para(text: str, style: str | None = None) -> str: + ppr = f"" if style else "" + return (f"{ppr}" + f"{escape(text)}") + + +class DocxProjector: + surface = "docx" + media_type = ("application/vnd.openxmlformats-officedocument." + "wordprocessingml.document") + ext = "docx" + modality = "text" + + def _document_xml(self, doc: DocumentIR) -> str: + body: list[str] = [_para(doc.title, "Title")] + if doc.subtitle: + body.append(_para(doc.subtitle, "Subtitle")) + abstract = doc.meta.get("abstract") + if abstract is not None and abstract.sentences: + body.append(_para(abstract.text())) + for sec in doc.sections: + style = "Heading1" if sec.level <= 1 else "Heading2" + body.append(_para(sec.heading, style)) + for block in sec.blocks: + t = block.text() + if t: + body.append(_para(t)) + return (f"" + f"" + + "".join(body) + + "" + "") + + def project(self, doc: DocumentIR) -> bytes: + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z: + z.writestr("[Content_Types].xml", _CONTENT_TYPES) + z.writestr("_rels/.rels", _RELS) + z.writestr("word/_rels/document.xml.rels", _DOC_RELS) + z.writestr("word/styles.xml", _STYLES) + z.writestr("word/document.xml", self._document_xml(doc)) + return buf.getvalue() + + +register(DocxProjector()) diff --git a/elp/projector/projectors/markdown.py b/elp/projector/projectors/markdown.py new file mode 100644 index 0000000..358e88c --- /dev/null +++ b/elp/projector/projectors/markdown.py @@ -0,0 +1,45 @@ +"""markdown.py — the Markdown surface projector (text facet). + +The most tractable surface, and the reference implementation: reads the IR's +realized sentences and lays them out as Markdown. Introduces no content — it is +pure typography over the faithful text the realizer produced. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from document_ir import DocumentIR # noqa: E402 +from projectors.base import register # noqa: E402 + + +class MarkdownProjector: + surface = "markdown" + media_type = "text/markdown" + ext = "md" + modality = "text" + + def render_str(self, doc: DocumentIR) -> str: + lines: list[str] = [f"# {doc.title}"] + if doc.subtitle: + lines.append(f"\n*{doc.subtitle}*") + abstract = doc.meta.get("abstract") + if abstract is not None and abstract.sentences: + lines.append("") + lines.append(abstract.text()) + for sec in doc.sections: + lines.append("") + lines.append(f"{'#' * max(2, sec.level)} {sec.heading}") + for block in sec.blocks: + body = block.text() + if body: + lines.append("") + lines.append(body) + return "\n".join(lines) + "\n" + + def project(self, doc: DocumentIR) -> bytes: + return self.render_str(doc).encode("utf-8") + + +register(MarkdownProjector()) diff --git a/elp/projector/projectors/midi.py b/elp/projector/projectors/midi.py new file mode 100644 index 0000000..ab21fcd --- /dev/null +++ b/elp/projector/projectors/midi.py @@ -0,0 +1,133 @@ +"""midi.py — the MUSIC surface projector: geometry -> symbolic music (MIDI). + +The first NON-TEXT surface, and the proof of the general shape. "Music is +language and it is math" (Will): symbolic music is tractable and geometry-native, +so it is the natural efferent twin to try first after text. + +CRUCIALLY this projector does NOT read the realized sentences. It reads the IR's +GEOMETRY facet — ``block.provenance`` — and DECODES each edge onto a musical +surface. That is the whole thesis of the multimodal projector: the same +geometry-carrying IR drives text AND music; a text projector reads the words, a +music projector reads the meaning-geometry. The mapping is deterministic and +faithful to the geometry's structure: + + relation lemma -> scale degree (same relation -> same pitch class; + meaning has a consistent sonic form) + polarity -> mode (aff = major third above; neg = minor + third / lowered — SACRED polarity is + audible, a negated edge sounds negated) + confidence -> note duration (stronger grounding rings longer) + importance -> velocity (more important source = louder) + section -> phrase + register shift (structure becomes musical form) + +Own-the-core: a Standard MIDI File is a header chunk + a track chunk of +delta-timed events. We emit the raw bytes with ``struct`` — no external MIDI +library. Format 0, one track. +""" +from __future__ import annotations + +import io +import os +import struct +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from document_ir import DocumentIR, Provenance # noqa: E402 +from projectors.base import TwoStageProjector, register # noqa: E402 + +_TICKS = 480 # ticks per quarter note +_C_MAJOR = [0, 2, 4, 5, 7, 9, 11] # semitone offsets of a diatonic scale + + +def _vlq(n: int) -> bytes: + """MIDI variable-length quantity encoding of a delta time.""" + if n == 0: + return b"\x00" + out = bytearray() + out.append(n & 0x7F) + n >>= 7 + while n: + out.insert(0, (n & 0x7F) | 0x80) + n >>= 7 + return bytes(out) + + +def _degree_for(relation: str) -> int: + """Stable scale degree for a relation lemma (same relation -> same pitch).""" + if not relation: + return 0 + return sum(ord(c) for c in relation.lower()) % len(_C_MAJOR) + + +def _note_for(p: Provenance, base: int) -> tuple[int, int, int]: + """(pitch, velocity, duration_ticks) for one geometry edge.""" + root = base + _C_MAJOR[_degree_for(p.relation)] + # polarity -> mode: affirmed edges take the bright major third, negated edges + # take the darker minor third. The negation is AUDIBLE and never dropped. + third = 4 if p.polarity == "aff" else 3 + pitch = max(24, min(96, root + (third if p.confidence >= 0.5 else 0))) + velocity = int(56 + 60 * min(1.0, max(0.0, p.importance))) + velocity = max(40, min(120, velocity)) + # confidence -> duration: quarter .. dotted-half + dur = int(_TICKS * (0.5 + 1.5 * min(1.0, max(0.0, p.confidence)))) + return pitch, velocity, dur + + +# a mode-profile: the pluggable musical knob (the peer's mode_profile). Scale + +# tempo. Swapping this profile re-voices the SAME geometry — surface as parameter. +_DEFAULT_PROFILE = {"scale": _C_MAJOR, "tempo_us": 500000, + "registers": [60, 55, 64, 50, 67, 48], "program": 0} + + +class MidiProjector(TwoStageProjector): + """geometry -> symbolic music, in the shared two-stage shape. + + ``plan(frame)`` -> a music_spec: an ordered list of note dicts derived + deterministically from the frame's provenance geometry + (the peer's ``plan(frame, profile) -> spec``). + ``realize(spec)`` -> Standard MIDI File bytes (the peer's + ``realize(spec, profile) -> surface``; here the surface + is symbolic MIDI, the minimal audio proof — a richer + additive-synth audio projector conforms identically). + """ + + surface = "midi" + media_type = "audio/midi" + ext = "mid" + modality = "audio" + + def __init__(self, profile: dict | None = None): + self.profile = profile or _DEFAULT_PROFILE + + # -- stage 1: meaning-geometry -> music_spec (reads the GEOMETRY facet) -- # + def plan(self, doc: DocumentIR) -> list[dict]: + registers = self.profile["registers"] + spec: list[dict] = [] + for si, sec in enumerate(doc.sections): + base = registers[si % len(registers)] + provs = [p for p in sec.all_provenance() + if p.kind in ("fact", "interpretation")] + for i, p in enumerate(provs): + pitch, vel, dur = _note_for(p, base) + spec.append({"pitch": pitch, "velocity": vel, "dur": dur, + "rest_before": (_TICKS // 2) if (si > 0 and i == 0) else 0, + "relation": p.relation, "polarity": p.polarity}) + return spec + + # -- stage 2: music_spec -> MIDI bytes (own-core, no library) ------------ # + def realize(self, spec: list[dict]) -> bytes: + ev = bytearray() + ev += _vlq(0) + b"\xFF\x51\x03" + struct.pack(">I", self.profile["tempo_us"])[1:] + ev += _vlq(0) + bytes([0xC0, self.profile["program"] & 0x7F]) + for note in spec: + ev += _vlq(note["rest_before"]) + bytes([0x90, note["pitch"], note["velocity"]]) + ev += _vlq(note["dur"]) + bytes([0x80, note["pitch"], 0]) + ev += _vlq(0) + b"\xFF\x2F\x00" + track = bytes(ev) + buf = io.BytesIO() + buf.write(b"MThd" + struct.pack(">IHHH", 6, 0, 1, _TICKS)) + buf.write(b"MTrk" + struct.pack(">I", len(track)) + track) + return buf.getvalue() + + +register(MidiProjector()) diff --git a/elp/projector/projectors/seams.py b/elp/projector/projectors/seams.py new file mode 100644 index 0000000..8fb5616 --- /dev/null +++ b/elp/projector/projectors/seams.py @@ -0,0 +1,60 @@ +"""seams.py — documented efferent seams for IMAGE and VIDEO surfaces. + +These are NOT implemented (per the build rails: architect, do not overbuild). +They are registered as first-class seams so the interface PROVES it accepts +future non-text projectors without any upstream change. Each documents exactly +what its decoder would read from the geometry-carrying IR, making the multimodal +generalization concrete rather than hand-wavy. + +The symmetry that guarantees these are possible, not moonshots: they are the +efferent twins of multimodal INGEST. If meaning can HOLD an image (ingest as +first-class geometry), meaning can PROJECT one back. Video = image x sound x +TIME, and the engram already stores time (chronoception). So video falls out of +an image projector + the music projector + the stored temporal ordering. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from document_ir import DocumentIR # noqa: E402 +from projectors.base import register # noqa: E402 + + +class _Seam: + """A registered-but-unimplemented projector. Names its decoder contract.""" + + def project(self, doc: DocumentIR) -> bytes: # pragma: no cover - seam + raise NotImplementedError( + f"{self.surface!r} projector is a documented seam, not yet built. " + f"Decoder contract: {self.decoder_contract}") + + +class ImageProjector(_Seam): + surface = "image" + media_type = "image/png" + ext = "png" + modality = "image" + decoder_contract = ( + "reads block.provenance as a spatial layout — nodes become regions, edges " + "become adjacencies; salience/importance drive size/contrast; polarity " + "drives figure/ground. The efferent twin of image ingest (a geometry->raster " + "decoder, learned or engineered), exactly mirroring the embedder that turned " + "the image INTO geometry.") + + +class VideoProjector(_Seam): + surface = "video" + media_type = "video/mp4" + ext = "mp4" + modality = "video" + decoder_contract = ( + "image x sound x TIME. Composes the image projector (per-keyframe geometry " + "layout) with the midi/music projector (score) along the geometry's stored " + "temporal ordering (chronoception). Needs no new principle once image + music " + "exist — only a muxer.") + + +register(ImageProjector()) +register(VideoProjector()) diff --git a/elp/projector/provenance.py b/elp/projector/provenance.py new file mode 100644 index 0000000..c7a95e6 --- /dev/null +++ b/elp/projector/provenance.py @@ -0,0 +1,63 @@ +"""provenance.py — the faithfulness audit + geometry->section trace. + +A document projected from geometry is only worth anything if every claim traces +back. This module walks the DocumentIR and proves the discipline held: + + * ZERO ungrounded claims (every fact/interpretation has a real node id), + * every emitted sentence maps to a geometry edge (or is a marked connective), + * SACRED polarity survived (negations are reported, never silently dropped), + * COHERE introduced no new geometry (connectives carry no claim). + +It emits both a machine verdict and a human-readable geometry->section table. +""" +from __future__ import annotations + +from document_ir import DocumentIR + + +def audit(doc: DocumentIR) -> dict: + provs = doc.all_provenance() + facts = [p for p in provs if p.kind in ("fact", "interpretation")] + connectives = [p for p in provs if p.kind == "connective"] + ungrounded = [p for p in facts if not p.node_id] + negations = [p for p in facts if p.polarity == "neg"] + node_ids = sorted({p.node_id for p in facts if p.node_id}) + return { + "claims": len(facts), + "connectives": len(connectives), + "ungrounded_claims": len(ungrounded), + "negations_preserved": len(negations), + "distinct_source_nodes": len(node_ids), + "faithful": len(ungrounded) == 0, + "source_nodes": node_ids, + } + + +def trace_table(doc: DocumentIR) -> str: + """Human-readable geometry -> section -> claim provenance table.""" + lines = ["# Provenance — every claim traces geometry", ""] + lines.append(f"**Document:** {doc.title}") + a = audit(doc) + lines.append(f"**Claims:** {a['claims']} · **Ungrounded:** " + f"{a['ungrounded_claims']} · **Negations preserved:** " + f"{a['negations_preserved']} · **Source nodes:** " + f"{a['distinct_source_nodes']} · **Faithful:** " + f"{'YES' if a['faithful'] else 'NO'}") + lines.append("") + for si, sec in enumerate(doc.sections, 1): + lines.append(f"## {si}. {sec.heading}") + lines.append(f"_seed nodes: {', '.join(i[:8] for i in sec.seed_ids)}_") + lines.append("") + lines.append("| # | realized claim | traces geometry edge |") + lines.append("|---|----------------|----------------------|") + n = 0 + for block in sec.blocks: + for sent, prov in zip(block.sentences, block.provenance): + if prov.kind == "connective": + continue + n += 1 + edge = prov.trace().replace("|", "\\|") + s = sent.replace("|", "\\|") + lines.append(f"| {n} | {s} | {edge} |") + lines.append("") + return "\n".join(lines) + "\n" diff --git a/elp/projector/realize.py b/elp/projector/realize.py new file mode 100644 index 0000000..221e691 --- /dev/null +++ b/elp/projector/realize.py @@ -0,0 +1,112 @@ +"""realize.py — REALIZE stage: fill each planned section with faithful passages. + +Scales the PROVEN realizer from a single assertion to a passage. For each +planned proposition we build a realizer-ready clause (the proven +``_prop_to_clause`` mapping) and run it through the proven engine +(``engine.realize``), which is a deterministic grammar with the SACRED negation +contract — it never invents. Each realized sentence is paired with a +:class:`Provenance` that pins it to the exact geometry edge it came from. + +"Passage, not a list of sentences": within a section we lightly vary sentence +openings and group related claims, but we add NO content the geometry did not +assert. The only non-geometry words are function words the grammar already owns +(articles, "and", conjunction of same-subject claims). Document-level flow is +COHERE's job; this stage owns intra-section fluency + fidelity. +""" +from __future__ import annotations + +import os +import sys + +_NT = os.path.expanduser("~/Desktop/neuron-talk") +_LR = os.path.expanduser("~/Desktop/lang-realizers") +for _p in (_NT, _LR): + if _p not in sys.path: + sys.path.insert(0, _p) + +import engine # noqa: E402 (the proven no-LLM realizer) +from dialogue import _prop_to_clause # noqa: E402 (proven prop -> clause) + +from document_ir import Block, DocumentIR, Provenance, Section # noqa: E402 + + +def _provenance_from(p, kind: str = "fact") -> Provenance: + return Provenance( + subj_id=p.source_node_id, subject=p.subject, relation=p.predicate, + obj=p.object, polarity=p.polarity, confidence=round(float(p.confidence), 3), + node_id=p.source_node_id, kind=kind, + importance=float(getattr(p, "node_importance", 0.0) or 0.0), + salience=0.0, + ) + + +import re as _re + +# a well-formed declarative opens with a determiner, a proper noun, "I", or a +# capitalized head — not a mis-parsed object pronoun or a copula fragment. +_BAD_OPENERS = _re.compile(r"^(Me |It is I|There is|This is it|That is it)\b") +_VACUOUS = _re.compile(r"^\w+ (is|are|was|were) (it|no|nothing|empty|those|this|that)\.?$", + _re.I) + + +def _good_sentence(text: str) -> bool: + """Fluency gate — drops degenerate realizations. NEVER loosens faithfulness; + it only refuses to SPEAK a claim whose surface came out malformed.""" + words = text.rstrip(".").split() + if len(words) < 3: + return False + if _BAD_OPENERS.search(text): + return False + if _VACUOUS.match(text): + return False + # a sentence that is mostly one-letter/two-letter tokens is a parse artifact + short = sum(1 for w in words if len(w.strip(".,'")) <= 2) + if short > len(words) / 2: + return False + return True + + +def _realize_prop(p, lang: str = "en") -> tuple[str, Provenance] | None: + """One proposition -> (faithful sentence, provenance) or None if it drops.""" + clause = _prop_to_clause(p) + text = engine.realize(clause, lang) + if not text or not text.strip(): + return None + text = text.strip() + if not text.endswith((".", "!", "?")): + text += "." + # capitalize first character (proper nouns / "I" already handled by grammar) + text = text[0].upper() + text[1:] + if not _good_sentence(text): + return None + return text, _provenance_from(p) + + +def realize_document(doc: DocumentIR, lang: str = "en") -> DocumentIR: + """Fill every planned section's blocks with faithful, realized passages.""" + for sec in doc.sections: + planned = sec.__dict__.get("_planned_props", []) + block = Block(role="body") + summary_bits: list[str] = [] + for p in planned: + r = _realize_prop(p, lang) + if r is None: + continue + text, prov = r + block.sentences.append(text) + block.provenance.append(prov) + if len(summary_bits) < 1: + # a short grounded gloss for TOC / pptx bullets + obj = (prov.obj or "").strip().rstrip(".") + if obj: + summary_bits.append(obj) + if block.sentences: + sec.blocks.append(block) + sec.summary = summary_bits[0] if summary_bits else "" + # drop the transient planning payload; the IR is now self-contained + sec.__dict__.pop("_planned_props", None) + sec.__dict__.pop("_node", None) + + # prune sections that realized to nothing + doc.sections = [s for s in doc.sections if s.blocks] + return doc diff --git a/elp/src/audio-demo.el b/elp/src/audio-demo.el new file mode 100644 index 0000000..ab68a5f --- /dev/null +++ b/elp/src/audio-demo.el @@ -0,0 +1,73 @@ +// audio-demo.el - Drive the native audio surface: render a tone per instrument +// from its LEARNED signature, then render a small meaning-phrase "piece". +// Entry point: top-level statement calls main() (same convention as the +// examples' top-level println(run_test())). + +fn micros_to_str(xs: [Int]) -> String { + let n: Int = native_list_len(xs) + let out: String = "" + let i: Int = 0 + while i < n { + if i > 0 { let out: String = out + "," } + let out: String = out + int_to_str(native_list_get(xs, i)) + let i: Int = i + 1 + } + return out +} + +// Render a 1.0s A4 (midi 69) tone from a signature file, print the parsed +// partials (proving the numbers came from the engram .sig), write the WAV. +fn render_tone(name: String, sigpath: String, outpath: String, table: [Int]) -> Int { + let lines: [String] = sig_load(sigpath) + let partials: [Int] = parse_micros(sig_field(lines, "partials")) + println("[" + name + "] partials_n=" + sig_field(lines, "partials_n") + " parsed_partials_micro(scale 1e6)=" + micros_to_str(partials)) + println("[" + name + "] raw partials line from .sig = " + sig_field(lines, "partials")) + let freq: Int = freq_of_midi(69) + let note: [Int] = synth_from_sig(lines, freq, 1000, 900, 44100, table) + let n: Int = native_list_len(note) + let ok: Int = wav_write(outpath, note, n, 44100) + println("[" + name + "] rendered " + int_to_str(n) + " samples -> " + outpath + " (write_ok=" + int_to_str(ok) + ")") + return n +} + +fn run_demo() -> Int { + let table: [Int] = sin_table() + fs_mkdir("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out") + + println("=== TONES: render A4 (midi 69) from each learned signature ===") + render_tone("flute", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/flute.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-flute.wav", table) + render_tone("clarinet", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/clarinet.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-clarinet.wav", table) + render_tone("violin", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/violin.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-violin.wav", table) + render_tone("piano", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/piano.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-piano.wav", table) + render_tone("organ", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/organ.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-organ.wav", table) + + println("") + println("=== PIECE: a 6-frame meaning phrase (incl. a NEG frame) ===") + let frames: [[String]] = native_list_empty() + let frames: [[String]] = native_list_append(frames, audio_frame("agent", "aff", "0.9", "0.8", "0", "s1")) + let frames: [[String]] = native_list_append(frames, audio_frame("theme", "aff", "0.7", "0.6", "0", "s2")) + let frames: [[String]] = native_list_append(frames, audio_frame("cause", "aff", "0.8", "0.9", "1", "s3")) + let frames: [[String]] = native_list_append(frames, audio_frame("negation", "neg", "0.85", "0.7", "0", "s4")) + let frames: [[String]] = native_list_append(frames, audio_frame("goal", "aff", "0.6", "0.5", "1", "s5")) + let frames: [[String]] = native_list_append(frames, audio_frame("result", "aff", "0.95", "1.0", "0", "s6")) + + // Print the plan so the NEG frame's minor third (+3) vs major (+4) is visible. + let nf: Int = native_list_len(frames) + let fi: Int = 0 + while fi < nf { + let frame: [String] = native_list_get(frames, fi) + let plan: [Int] = plan_note(frame) + let pol: String = surface_get(frame, "polarity") + let third_name: String = "major(+4)" + if str_eq(pol, "neg") { let third_name: String = "MINOR(+3)" } + println("frame " + int_to_str(fi) + " relation=" + surface_get(frame, "relation") + " polarity=" + pol + " -> midi=" + int_to_str(native_list_get(plan, 0)) + " dur_ms=" + int_to_str(native_list_get(plan, 1)) + " amp_pm=" + int_to_str(native_list_get(plan, 2)) + " third=" + third_name) + let fi: Int = fi + 1 + } + + let piano_lines: [String] = sig_load("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/piano.sig") + let total: Int = realize_audio(frames, piano_lines, "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/piece.wav", 44100, table) + println("PIECE rendered " + int_to_str(total) + " samples -> /Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/piece.wav") + return total +} + +println("audio-demo main returned samples=" + int_to_str(run_demo())) diff --git a/elp/src/audio-surface.el b/elp/src/audio-surface.el new file mode 100644 index 0000000..6c01d77 --- /dev/null +++ b/elp/src/audio-surface.el @@ -0,0 +1,400 @@ +// audio-surface.el - Native own-core additive-synthesis audio surface. +// +// The AUDIO efferent seam, native, no Python and no library. This renders real +// PCM .wav bytes from instrument SIGNATURES read from engram-sourced .sig data +// files (elp/faculty/sig/*.sig) - the partial amplitudes are NEVER literals in +// this source; they are parsed from the learned signature at run time. That is +// the whole proof: render-from-learned-signatures. +// +// EL has no float arithmetic operator (codegen emits raw int64 ops for + - * / +// on the shared 64-bit slot) and no float-arithmetic natives - so ALL synthesis +// math here is own-core INTEGER fixed-point. Angles use a quarter-wave sine +// table (scale 10000) from a fixed-point Taylor series; amplitudes are parsed to +// micro (scale 1e6) straight from the .sig text; frequencies are milliHz ints. +// +// Pipeline mirrors the two-stage projector (midi.py): plan_note(frame) reads a +// frame's meaning-geometry slot-map and derives (pitch, duration, amplitude); +// realize_audio SUPERPOSES the signature's partials (the compose op) and +// serialises RIFF/WAVE. Same frame -> midi OR audio. + +// -- integer decimal + string helpers ----------------------------------------- + +fn str_to_int_el(s: String) -> Int { + let n: Int = str_len(s) + let i: Int = 0 + let v: Int = 0 + let neg: Bool = false + while i < n { + let c: Int = str_char_code(s, i) + if c == 45 { let neg: Bool = true } + if c >= 48 { + if c < 58 { + let v: Int = v * 10 + (c - 48) + } + } + let i: Int = i + 1 + } + if neg { return 0 - v } + return v +} + +fn parse_micro(s: String) -> Int { + let dot: Int = str_index_of(s, ".") + if dot < 0 { + return str_to_int_el(s) * 1000000 + } + let n: Int = str_len(s) + let ipart: String = str_slice(s, 0, dot) + let fpart: String = str_slice(s, dot + 1, n) + let iv: Int = str_to_int_el(ipart) + let fv: Int = 0 + let scale: Int = 100000 + let fn2: Int = str_len(fpart) + let i: Int = 0 + while i < 6 { + let d: Int = 0 + if i < fn2 { + let d: Int = str_char_code(fpart, i) - 48 + } + let fv: Int = fv + d * scale + let scale: Int = scale / 10 + let i: Int = i + 1 + } + return iv * 1000000 + fv +} + +// -- signature (engram data file) loader --------------------------------------- + +fn sig_load(path: String) -> [String] { + let text: String = fs_read(path) + return str_split(text, "\n") +} + +fn sig_field(lines: [String], key: String) -> String { + let pref: String = key + ": " + let n: Int = native_list_len(lines) + let plen: Int = str_len(pref) + let i: Int = 0 + while i < n { + let ln: String = native_list_get(lines, i) + if str_starts_with(ln, pref) { + return str_slice(ln, plen, str_len(ln)) + } + let i: Int = i + 1 + } + return "" +} + +fn parse_micros(csv: String) -> [Int] { + let parts: [String] = str_split(csv, ",") + let n: Int = native_list_len(parts) + let out: [Int] = native_list_empty() + let i: Int = 0 + while i < n { + let out: [Int] = native_list_append(out, parse_micro(native_list_get(parts, i))) + let i: Int = i + 1 + } + return out +} + +// -- fixed-point sine (own-core, quarter-wave Taylor table, scale 10000) -------- + +fn sin_table() -> [Int] { + let HP: Int = 1570796 + let t: [Int] = native_list_empty() + let q: Int = 0 + while q < 257 { + let x: Int = q * HP / 256 + let x2: Int = x * x / 1000000 + let x3: Int = x2 * x / 1000000 + let x5: Int = x3 * x2 / 1000000 + let x7: Int = x5 * x2 / 1000000 + let x9: Int = x7 * x2 / 1000000 + let s: Int = x - x3 / 6 + x5 / 120 - x7 / 5040 + x9 / 362880 + let t: [Int] = native_list_append(t, s / 100) + let q: Int = q + 1 + } + return t +} + +fn sin_lookup(t: [Int], phase: Int) -> Int { + let p: Int = phase % 1024 + if p < 0 { let p: Int = p + 1024 } + let quad: Int = p / 256 + let r: Int = p % 256 + if quad == 0 { return native_list_get(t, r) } + if quad == 1 { return native_list_get(t, 256 - r) } + if quad == 2 { return 0 - native_list_get(t, r) } + return 0 - native_list_get(t, 256 - r) +} + +fn isqrt_int(n: Int) -> Int { + if n <= 0 { return 0 } + let x: Int = n + let y: Int = (x + 1) / 2 + while y < x { + let x: Int = y + let y: Int = (x + n / x) / 2 + } + return x +} + +// freq_of_midi: equal-tempered frequency in milliHz. 440000 mHz at midi 69. +fn freq_of_midi(m: Int) -> Int { + let f: Int = 440000 + if m > 69 { + let k: Int = m - 69 + let i: Int = 0 + while i < k { + let f: Int = f * 1059463 / 1000000 + let i: Int = i + 1 + } + return f + } + if m < 69 { + let k: Int = 69 - m + let i: Int = 0 + while i < k { + let f: Int = f * 1000000 / 1059463 + let i: Int = i + 1 + } + return f + } + return f +} + +// -- envelope (ADSR), scale 1000 ----------------------------------------------- + +fn adsr_env(i: Int, total: Int, atk_n: Int, dec_n: Int, sus_pm: Int, rel_n: Int) -> Int { + if i < atk_n { + if atk_n == 0 { return 1000 } + return 1000 * i / atk_n + } + if i < atk_n + dec_n { + if dec_n == 0 { return sus_pm } + return 1000 - (1000 - sus_pm) * (i - atk_n) / dec_n + } + let rel_start: Int = total - rel_n + if i < rel_start { + return sus_pm + } + if rel_n == 0 { return 0 } + let left: Int = total - i + return sus_pm * left / rel_n +} + +// -- note synthesis: SUPERPOSE the learned partials -> [Int] samples ----------- +fn note_samples(freq_mHz: Int, dur_ms: Int, rate: Int, partials: [Int], sumP: Int, b_micro: Int, vib_rate: Int, vib_cents: Int, atk_ms: Int, dec_ms: Int, sus_pm: Int, rel_ms: Int, amp_pm: Int, table: [Int]) -> [Int] { + let total: Int = dur_ms * rate / 1000 + let atk_n: Int = atk_ms * rate / 1000 + let dec_n: Int = dec_ms * rate / 1000 + let rel_n: Int = rel_ms * rate / 1000 + let np: Int = native_list_len(partials) + let half_mhz: Int = rate * 1000 / 2 + let out: [Int] = native_list_empty() + let i: Int = 0 + while i < total { + let acc: Int = 0 + let k: Int = 0 + while k < np { + let harm: Int = k + 1 + let amp_k: Int = native_list_get(partials, k) + let factor: Int = 1000000 + if b_micro > 0 { + let val: Int = 1000000 + b_micro * harm * harm + let factor: Int = isqrt_int(val * 1000000) + } + let fn_mhz: Int = freq_mHz * harm + let fn_mhz: Int = fn_mhz * factor / 1000000 + if vib_cents > 0 { + if vib_rate > 0 { + let vphase: Int = i * vib_rate * 1024 / rate + let vs: Int = sin_lookup(table, vphase) + let vibf: Int = 1000000 + (vib_cents * vs * 833) / 10000 + let fn_mhz: Int = fn_mhz * vibf / 1000000 + } + } + if fn_mhz <= half_mhz { + let phase: Int = i * fn_mhz * 1024 / (rate * 1000) + let sv: Int = sin_lookup(table, phase) + let acc: Int = acc + sv * amp_k / 1000000 + } + let k: Int = k + 1 + } + let env: Int = adsr_env(i, total, atk_n, dec_n, sus_pm, rel_n) + let s16: Int = acc * 2800000 / sumP + let s16: Int = s16 * env / 1000 + let s16: Int = s16 * amp_pm / 1000 + if s16 > 32767 { let s16: Int = 32767 } + if s16 < 0 - 32767 { let s16: Int = 0 - 32767 } + let out: [Int] = native_list_append(out, s16) + let i: Int = i + 1 + } + return out +} + +fn synth_from_sig(lines: [String], freq_mHz: Int, dur_ms: Int, amp_pm: Int, rate: Int, table: [Int]) -> [Int] { + let partials: [Int] = parse_micros(sig_field(lines, "partials")) + let np: Int = native_list_len(partials) + let sumP: Int = 0 + let j: Int = 0 + while j < np { + let pj: Int = native_list_get(partials, j) + let sumP: Int = sumP + pj + let j: Int = j + 1 + } + if sumP <= 0 { let sumP: Int = 1000000 } + let adsr: [String] = str_split(sig_field(lines, "adsr"), ",") + let atk_ms: Int = parse_micro(native_list_get(adsr, 0)) / 1000 + let dec_ms: Int = parse_micro(native_list_get(adsr, 1)) / 1000 + let sus_pm: Int = parse_micro(native_list_get(adsr, 2)) / 1000 + let rel_ms: Int = parse_micro(native_list_get(adsr, 3)) / 1000 + let b_micro: Int = parse_micro(sig_field(lines, "inharmonicity_B")) + let vib_rate: Int = str_to_int_el(sig_field(lines, "vibrato_rate_hz")) + let vib_cents: Int = str_to_int_el(sig_field(lines, "vibrato_depth_cents")) + return note_samples(freq_mHz, dur_ms, rate, partials, sumP, b_micro, vib_rate, vib_cents, atk_ms, dec_ms, sus_pm, rel_ms, amp_pm, table) +} + +// -- byte-buffer helpers (own-core, no library) -------------------------------- + +fn put_tag(buf: String, pos: Int, s: String) -> String { + let n: Int = str_len(s) + let i: Int = 0 + while i < n { + let buf: String = __str_set_char(buf, pos + i, str_char_code(s, i)) + let i: Int = i + 1 + } + return buf +} + +fn put_u32le(buf: String, pos: Int, v: Int) -> String { + let buf: String = __str_set_char(buf, pos, v % 256) + let buf: String = __str_set_char(buf, pos + 1, (v / 256) % 256) + let buf: String = __str_set_char(buf, pos + 2, (v / 65536) % 256) + let buf: String = __str_set_char(buf, pos + 3, (v / 16777216) % 256) + return buf +} + +fn put_u16le(buf: String, pos: Int, v: Int) -> String { + let buf: String = __str_set_char(buf, pos, v % 256) + let buf: String = __str_set_char(buf, pos + 1, (v / 256) % 256) + return buf +} + +// -- WAV serializer: own-core RIFF/WAVE, PCM mono 16-bit ----------------------- + +fn wav_write(path: String, samples: [Int], n: Int, rate: Int) -> Int { + let data_len: Int = n * 2 + let total: Int = 44 + data_len + let buf: String = __str_alloc(total) + let buf: String = put_tag(buf, 0, "RIFF") + let buf: String = put_u32le(buf, 4, 36 + data_len) + let buf: String = put_tag(buf, 8, "WAVE") + let buf: String = put_tag(buf, 12, "fmt ") + let buf: String = put_u32le(buf, 16, 16) + let buf: String = put_u16le(buf, 20, 1) + let buf: String = put_u16le(buf, 22, 1) + let buf: String = put_u32le(buf, 24, rate) + let buf: String = put_u32le(buf, 28, rate * 2) + let buf: String = put_u16le(buf, 32, 2) + let buf: String = put_u16le(buf, 34, 16) + let buf: String = put_tag(buf, 36, "data") + let buf: String = put_u32le(buf, 40, data_len) + let i: Int = 0 + while i < n { + let v: Int = native_list_get(samples, i) + if v < 0 { let v: Int = v + 65536 } + let buf: String = __str_set_char(buf, 44 + i * 2, v % 256) + let buf: String = __str_set_char(buf, 44 + i * 2 + 1, (v / 256) % 256) + let i: Int = i + 1 + } + let ok: Int = fs_write_bytes(path, buf, total) + return ok +} + +// -- plan: frame slot-map -> note atom (pitch, duration, amplitude) ------------ + +fn audio_frame(relation: String, polarity: String, confidence: String, importance: String, salience: String, subj_id: String) -> [String] { + let f: [String] = native_list_empty() + let f: [String] = native_list_append(f, "relation") + let f: [String] = native_list_append(f, relation) + let f: [String] = native_list_append(f, "polarity") + let f: [String] = native_list_append(f, polarity) + let f: [String] = native_list_append(f, "confidence") + let f: [String] = native_list_append(f, confidence) + let f: [String] = native_list_append(f, "importance") + let f: [String] = native_list_append(f, importance) + let f: [String] = native_list_append(f, "salience") + let f: [String] = native_list_append(f, salience) + let f: [String] = native_list_append(f, "subj_id") + let f: [String] = native_list_append(f, subj_id) + return f +} + +fn degree_offset(deg: Int) -> Int { + if deg == 0 { return 0 } + if deg == 1 { return 2 } + if deg == 2 { return 4 } + if deg == 3 { return 5 } + if deg == 4 { return 7 } + if deg == 5 { return 9 } + return 11 +} + +// returns [midi, dur_ms, amp_pm] +fn plan_note(frame: [String]) -> [Int] { + let relation: String = surface_get(frame, "relation") + let polarity: String = surface_get(frame, "polarity") + let confidence: String = surface_get(frame, "confidence") + let importance: String = surface_get(frame, "importance") + let salience: String = surface_get(frame, "salience") + let rn: Int = str_len(relation) + let csum: Int = 0 + let i: Int = 0 + while i < rn { + let cc: Int = str_char_code(relation, i) + let csum: Int = csum + cc + let i: Int = i + 1 + } + let deg: Int = csum % 7 + let third: Int = 4 + if str_eq(polarity, "neg") { let third: Int = 3 } + let sal_oct: Int = str_to_int_el(salience) + let doff: Int = degree_offset(deg) + let midi: Int = 60 + sal_oct * 12 + doff + third + let conf_micro: Int = parse_micro(confidence) + let dur_ms: Int = 200 + conf_micro / 1000 + let imp_micro: Int = parse_micro(importance) + let amp_pm: Int = 400 + imp_micro / 2000 + let out: [Int] = native_list_empty() + let out: [Int] = native_list_append(out, midi) + let out: [Int] = native_list_append(out, dur_ms) + let out: [Int] = native_list_append(out, amp_pm) + return out +} + +fn realize_audio(frames: [[String]], sig_lines: [String], path: String, rate: Int, table: [Int]) -> Int { + let nf: Int = native_list_len(frames) + let all: [Int] = native_list_empty() + let count: Int = 0 + let fi: Int = 0 + while fi < nf { + let frame: [String] = native_list_get(frames, fi) + let plan: [Int] = plan_note(frame) + let midi: Int = native_list_get(plan, 0) + let dur_ms: Int = native_list_get(plan, 1) + let amp_pm: Int = native_list_get(plan, 2) + let freq: Int = freq_of_midi(midi) + let note: [Int] = synth_from_sig(sig_lines, freq, dur_ms, amp_pm, rate, table) + let nn: Int = native_list_len(note) + let j: Int = 0 + while j < nn { + let all: [Int] = native_list_append(all, native_list_get(note, j)) + let j: Int = j + 1 + } + let count: Int = count + nn + let fi: Int = fi + 1 + } + let ok: Int = wav_write(path, all, count, rate) + return count +} diff --git a/elp/src/image-demo.el b/elp/src/image-demo.el new file mode 100644 index 0000000..279e27a --- /dev/null +++ b/elp/src/image-demo.el @@ -0,0 +1,65 @@ +// image-demo.el - Drive the native PNG surface: plan a scene from a small +// meaning phrase (incl. a NEG frame) and emit a byte-valid 64x64 PNG whose +// palette is read from elp/faculty/sig/scene.basis. + +fn img_frame(relation: String, polarity: String, confidence: String, importance: String, salience: String, subj_id: String) -> [String] { + let f: [String] = native_list_empty() + let f: [String] = native_list_append(f, "relation") + let f: [String] = native_list_append(f, relation) + let f: [String] = native_list_append(f, "polarity") + let f: [String] = native_list_append(f, polarity) + let f: [String] = native_list_append(f, "confidence") + let f: [String] = native_list_append(f, confidence) + let f: [String] = native_list_append(f, "importance") + let f: [String] = native_list_append(f, importance) + let f: [String] = native_list_append(f, "salience") + let f: [String] = native_list_append(f, salience) + let f: [String] = native_list_append(f, "subj_id") + let f: [String] = native_list_append(f, subj_id) + return f +} + +fn rgb_str(c: [Int]) -> String { + return int_to_str(native_list_get(c, 0)) + "," + int_to_str(native_list_get(c, 1)) + "," + int_to_str(native_list_get(c, 2)) +} + +fn run_image() -> Int { + fs_mkdir("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out") + let table: [Int] = crc_table() + println("crc_table[1]=" + int_to_str(native_list_get(table, 1)) + " (expect 1996959894 / 0x77073096)") + + let basis: [String] = basis_load("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/scene.basis") + let warm: [Int] = parse_rgb(basis_field(basis, "warm")) + let cool: [Int] = parse_rgb(basis_field(basis, "cool")) + let bg: [Int] = parse_rgb(basis_field(basis, "bg")) + println("basis warm=" + rgb_str(warm) + " cool=" + rgb_str(cool) + " bg=" + rgb_str(bg) + " (read from scene.basis)") + + let frames: [[String]] = native_list_empty() + let frames: [[String]] = native_list_append(frames, img_frame("agent", "aff", "0.9", "0.8", "0", "s1")) + let frames: [[String]] = native_list_append(frames, img_frame("theme", "aff", "0.7", "0.6", "1", "s2")) + let frames: [[String]] = native_list_append(frames, img_frame("cause", "aff", "0.8", "0.9", "0", "s3")) + let frames: [[String]] = native_list_append(frames, img_frame("negation", "neg", "0.85", "0.7", "1", "s4")) + let frames: [[String]] = native_list_append(frames, img_frame("goal", "aff", "0.6", "0.5", "0", "s5")) + let frames: [[String]] = native_list_append(frames, img_frame("result", "aff", "0.95", "1.0", "1", "s6")) + + let shapes: [[Int]] = plan_scene(frames, warm, cool) + let ns: Int = native_list_len(shapes) + println("planned " + int_to_str(ns) + " shapes:") + let si: Int = 0 + while si < ns { + let sh: [Int] = native_list_get(shapes, si) + let pol: String = surface_get(native_list_get(frames, si), "polarity") + println(" shape " + int_to_str(si) + " type=" + int_to_str(native_list_get(sh, 0)) + " x=" + int_to_str(native_list_get(sh, 1)) + " y=" + int_to_str(native_list_get(sh, 2)) + " size=" + int_to_str(native_list_get(sh, 3)) + " rgb=" + int_to_str(native_list_get(sh, 4)) + "," + int_to_str(native_list_get(sh, 5)) + "," + int_to_str(native_list_get(sh, 6)) + " polarity=" + pol) + let si: Int = si + 1 + } + + let raw: [Int] = rasterize(64, 64, shapes, bg) + println("rasterized raw (filtered scanlines) bytes=" + int_to_str(native_list_len(raw)) + " (expect 12352)") + let png: [Int] = png_build(64, 64, raw, table) + let plen: Int = native_list_len(png) + let ok: Int = png_write("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/scene.png", png) + println("PNG bytes=" + int_to_str(plen) + " -> /Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/scene.png (write_ok=" + int_to_str(ok) + ")") + return plen +} + +println("image-demo returned png_bytes=" + int_to_str(run_image())) diff --git a/elp/src/image-surface.el b/elp/src/image-surface.el new file mode 100644 index 0000000..7137a95 --- /dev/null +++ b/elp/src/image-surface.el @@ -0,0 +1,412 @@ +// image-surface.el - Native own-core raster PNG surface (the image efferent +// twin of audio). Renders a 64x64 RGB scene deterministically from a frame's +// meaning-geometry, then serialises a byte-valid PNG entirely own-core: +// 8-byte magic, IHDR, IDAT (zlib STORED/uncompressed DEFLATE + Adler32), IEND, +// with a per-chunk CRC32 computed via software xor32 (EL has no bitwise ops). +// +// The RGB palette basis is read from elp/faculty/sig/scene.basis (data, not +// literals) - the same read-from-learned discipline as the audio signatures. +// Integer-only throughout; pixels are composed functionally (painter's order) +// so no list mutation is needed. + +// -- small int/parse helpers (self-contained) ---------------------------------- + +fn i_str_to_int(s: String) -> Int { + let n: Int = str_len(s) + let i: Int = 0 + let v: Int = 0 + while i < n { + let c: Int = str_char_code(s, i) + if c >= 48 { + if c < 58 { + let v: Int = v * 10 + (c - 48) + } + } + let i: Int = i + 1 + } + return v +} + +fn basis_load(path: String) -> [String] { + return str_split(fs_read(path), "\n") +} + +fn basis_field(lines: [String], key: String) -> String { + let pref: String = key + ": " + let n: Int = native_list_len(lines) + let plen: Int = str_len(pref) + let i: Int = 0 + while i < n { + let ln: String = native_list_get(lines, i) + if str_starts_with(ln, pref) { + return str_slice(ln, plen, str_len(ln)) + } + let i: Int = i + 1 + } + return "" +} + +fn parse_rgb(csv: String) -> [Int] { + let parts: [String] = str_split(csv, ",") + let out: [Int] = native_list_empty() + let n: Int = native_list_len(parts) + let i: Int = 0 + while i < n { + let v: Int = i_str_to_int(native_list_get(parts, i)) + let out: [Int] = native_list_append(out, v) + let i: Int = i + 1 + } + return out +} + +// -- software 32-bit XOR (no bitwise ops in EL) -------------------------------- + +fn xor32(a: Int, b: Int) -> Int { + let r: Int = 0 + let bit: Int = 1 + let i: Int = 0 + while i < 32 { + let abit: Int = (a / bit) % 2 + let bbit: Int = (b / bit) % 2 + if abit != bbit { + let add: Int = bit + let r: Int = r + add + } + let bit: Int = bit * 2 + let i: Int = i + 1 + } + return r +} + +// -- CRC32 (table-driven, table built with xor32) ------------------------------ + +fn crc_table() -> [Int] { + let t: [Int] = native_list_empty() + let n: Int = 0 + while n < 256 { + let c: Int = n + let k: Int = 0 + while k < 8 { + if c % 2 == 1 { + let h: Int = c / 2 + let c: Int = xor32(h, 3988292384) + } else { + let c: Int = c / 2 + } + let k: Int = k + 1 + } + let t: [Int] = native_list_append(t, c) + let n: Int = n + 1 + } + return t +} + +fn crc32_of(bytes: [Int], table: [Int]) -> Int { + let crc: Int = 4294967295 + let n: Int = native_list_len(bytes) + let i: Int = 0 + while i < n { + let b: Int = native_list_get(bytes, i) + let lo: Int = crc % 256 + let idx: Int = xor32(lo, b) % 256 + let tv: Int = native_list_get(table, idx) + let hi: Int = crc / 256 + let crc: Int = xor32(hi, tv) + let i: Int = i + 1 + } + return xor32(crc, 4294967295) +} + +// -- Adler32 (for the zlib trailer) -------------------------------------------- + +fn adler32_of(bytes: [Int]) -> Int { + let a: Int = 1 + let b: Int = 0 + let n: Int = native_list_len(bytes) + let i: Int = 0 + while i < n { + let byte: Int = native_list_get(bytes, i) + let a: Int = (a + byte) % 65521 + let b: Int = (b + a) % 65521 + let i: Int = i + 1 + } + return b * 65536 + a +} + +// -- byte-list append helpers -------------------------------------------------- + +fn app_u32be(dst: [Int], v: Int) -> [Int] { + let dst: [Int] = native_list_append(dst, (v / 16777216) % 256) + let dst: [Int] = native_list_append(dst, (v / 65536) % 256) + let dst: [Int] = native_list_append(dst, (v / 256) % 256) + let dst: [Int] = native_list_append(dst, v % 256) + return dst +} + +fn app_tag(dst: [Int], s: String) -> [Int] { + let n: Int = str_len(s) + let i: Int = 0 + while i < n { + let dst: [Int] = native_list_append(dst, str_char_code(s, i)) + let i: Int = i + 1 + } + return dst +} + +fn app_all(dst: [Int], src: [Int]) -> [Int] { + let n: Int = native_list_len(src) + let i: Int = 0 + while i < n { + let dst: [Int] = native_list_append(dst, native_list_get(src, i)) + let i: Int = i + 1 + } + return dst +} + +// -- plan: frame meaning-geometry -> shape atoms ------------------------------- +// shape = [type, x, y, size, r, g, b] (type 0=rect 1=disc 2=triangle) + +fn charsum(s: String) -> Int { + let n: Int = str_len(s) + let i: Int = 0 + let acc: Int = 0 + while i < n { + let c: Int = str_char_code(s, i) + let acc: Int = acc + c + let i: Int = i + 1 + } + return acc +} + +fn micro_of(s: String) -> Int { + let dot: Int = str_index_of(s, ".") + if dot < 0 { return i_str_to_int(s) * 1000000 } + let n: Int = str_len(s) + let fp: String = str_slice(s, dot + 1, n) + let ip: String = str_slice(s, 0, dot) + let iv: Int = i_str_to_int(ip) + let fv: Int = 0 + let scale: Int = 100000 + let fl: Int = str_len(fp) + let i: Int = 0 + while i < 6 { + let d: Int = 0 + if i < fl { let d: Int = str_char_code(fp, i) - 48 } + let fv: Int = fv + d * scale + let scale: Int = scale / 10 + let i: Int = i + 1 + } + return iv * 1000000 + fv +} + +fn plan_scene(frames: [[String]], warm: [Int], cool: [Int]) -> [[Int]] { + let shapes: [[Int]] = native_list_empty() + let nf: Int = native_list_len(frames) + let fi: Int = 0 + while fi < nf { + let fr: [String] = native_list_get(frames, fi) + let relation: String = surface_get(fr, "relation") + let polarity: String = surface_get(fr, "polarity") + let confidence: String = surface_get(fr, "confidence") + let importance: String = surface_get(fr, "importance") + let salience: String = surface_get(fr, "salience") + // relation -> shape type + let stype: Int = charsum(relation) % 3 + // confidence -> size (8..22) + let cmi: Int = micro_of(confidence) + let size: Int = 8 + cmi / 71428 + // salience -> y + let sal: Int = i_str_to_int(salience) + let y: Int = 6 + sal * 26 + // subj_id/index -> x + let x: Int = 4 + (fi * 10) % 48 + // polarity -> warm/cool base color + let br: Int = native_list_get(warm, 0) + let bg2: Int = native_list_get(warm, 1) + let bb: Int = native_list_get(warm, 2) + if str_eq(polarity, "neg") { + let br: Int = native_list_get(cool, 0) + let bg2: Int = native_list_get(cool, 1) + let bb: Int = native_list_get(cool, 2) + } + // importance -> brightness (500..1000 permille) + let imi: Int = micro_of(importance) + let bpm: Int = 500 + imi / 2000 + let r: Int = br * bpm / 1000 + let g: Int = bg2 * bpm / 1000 + let b: Int = bb * bpm / 1000 + let sh: [Int] = native_list_empty() + let sh: [Int] = native_list_append(sh, stype) + let sh: [Int] = native_list_append(sh, x) + let sh: [Int] = native_list_append(sh, y) + let sh: [Int] = native_list_append(sh, size) + let sh: [Int] = native_list_append(sh, r) + let sh: [Int] = native_list_append(sh, g) + let sh: [Int] = native_list_append(sh, b) + let shapes: [[Int]] = native_list_append(shapes, sh) + let fi: Int = fi + 1 + } + return shapes +} + +// covers: is (px,py) inside this shape? +fn covers(sh: [Int], px: Int, py: Int) -> Bool { + let stype: Int = native_list_get(sh, 0) + let sx: Int = native_list_get(sh, 1) + let sy: Int = native_list_get(sh, 2) + let size: Int = native_list_get(sh, 3) + let cx: Int = sx + size / 2 + if stype == 0 { + if px >= sx { + if px < sx + size { + if py >= sy { + if py < sy + size { + return true + } + } + } + } + return false + } + if stype == 1 { + let rad: Int = size / 2 + let dx: Int = px - cx + let dy: Int = py - (sy + rad) + if dx * dx + dy * dy <= rad * rad { + return true + } + return false + } + // triangle: apex at top (sy), base at sy+size + if py >= sy { + if py < sy + size { + let dyv: Int = py - sy + let halfw: Int = dyv / 2 + let dxv: Int = px - cx + let adx: Int = dxv + if adx < 0 { let adx: Int = 0 - dxv } + if adx <= halfw { + return true + } + } + } + return false +} + +// pixel_color: painter's algorithm - last covering shape wins. Returns [r,g,b]. +fn pixel_color(px: Int, py: Int, shapes: [[Int]], bg: [Int]) -> [Int] { + let r: Int = native_list_get(bg, 0) + let g: Int = native_list_get(bg, 1) + let b: Int = native_list_get(bg, 2) + let n: Int = native_list_len(shapes) + let i: Int = 0 + while i < n { + let sh: [Int] = native_list_get(shapes, i) + if covers(sh, px, py) { + let r: Int = native_list_get(sh, 4) + let g: Int = native_list_get(sh, 5) + let b: Int = native_list_get(sh, 6) + } + let i: Int = i + 1 + } + let out: [Int] = native_list_empty() + let out: [Int] = native_list_append(out, r) + let out: [Int] = native_list_append(out, g) + let out: [Int] = native_list_append(out, b) + return out +} + +// rasterize: build the raw (filtered) scanline byte stream, filter byte 0 / row. +fn rasterize(w: Int, h: Int, shapes: [[Int]], bg: [Int]) -> [Int] { + let raw: [Int] = native_list_empty() + let y: Int = 0 + while y < h { + let raw: [Int] = native_list_append(raw, 0) + let x: Int = 0 + while x < w { + let col: [Int] = pixel_color(x, y, shapes, bg) + let raw: [Int] = native_list_append(raw, native_list_get(col, 0)) + let raw: [Int] = native_list_append(raw, native_list_get(col, 1)) + let raw: [Int] = native_list_append(raw, native_list_get(col, 2)) + let x: Int = x + 1 + } + let y: Int = y + 1 + } + return raw +} + +// zlib stream with a single STORED (uncompressed) DEFLATE block + Adler32. +fn zlib_store(raw: [Int]) -> [Int] { + let z: [Int] = native_list_empty() + let z: [Int] = native_list_append(z, 120) + let z: [Int] = native_list_append(z, 1) + let z: [Int] = native_list_append(z, 1) + let len: Int = native_list_len(raw) + let nlen: Int = 65535 - len + let z: [Int] = native_list_append(z, len % 256) + let z: [Int] = native_list_append(z, (len / 256) % 256) + let z: [Int] = native_list_append(z, nlen % 256) + let z: [Int] = native_list_append(z, (nlen / 256) % 256) + let z: [Int] = app_all(z, raw) + let ad: Int = adler32_of(raw) + let z: [Int] = app_u32be(z, ad) + return z +} + +// append a full PNG chunk: length + (type+data) + crc32(type+data). +fn app_chunk(png: [Int], type_and_data: [Int], table: [Int]) -> [Int] { + let total: Int = native_list_len(type_and_data) + let dlen: Int = total - 4 + let png: [Int] = app_u32be(png, dlen) + let png: [Int] = app_all(png, type_and_data) + let crc: Int = crc32_of(type_and_data, table) + let png: [Int] = app_u32be(png, crc) + return png +} + +fn png_build(w: Int, h: Int, raw: [Int], table: [Int]) -> [Int] { + let png: [Int] = native_list_empty() + // 8-byte signature + let png: [Int] = native_list_append(png, 137) + let png: [Int] = native_list_append(png, 80) + let png: [Int] = native_list_append(png, 78) + let png: [Int] = native_list_append(png, 71) + let png: [Int] = native_list_append(png, 13) + let png: [Int] = native_list_append(png, 10) + let png: [Int] = native_list_append(png, 26) + let png: [Int] = native_list_append(png, 10) + // IHDR + let ihdr: [Int] = native_list_empty() + let ihdr: [Int] = app_tag(ihdr, "IHDR") + let ihdr: [Int] = app_u32be(ihdr, w) + let ihdr: [Int] = app_u32be(ihdr, h) + let ihdr: [Int] = native_list_append(ihdr, 8) + let ihdr: [Int] = native_list_append(ihdr, 2) + let ihdr: [Int] = native_list_append(ihdr, 0) + let ihdr: [Int] = native_list_append(ihdr, 0) + let ihdr: [Int] = native_list_append(ihdr, 0) + let png: [Int] = app_chunk(png, ihdr, table) + // IDAT + let z: [Int] = zlib_store(raw) + let idat: [Int] = native_list_empty() + let idat: [Int] = app_tag(idat, "IDAT") + let idat: [Int] = app_all(idat, z) + let png: [Int] = app_chunk(png, idat, table) + // IEND + let iend: [Int] = native_list_empty() + let iend: [Int] = app_tag(iend, "IEND") + let png: [Int] = app_chunk(png, iend, table) + return png +} + +fn png_write(path: String, png: [Int]) -> Int { + let n: Int = native_list_len(png) + let buf: String = __str_alloc(n) + let i: Int = 0 + while i < n { + let buf: String = __str_set_char(buf, i, native_list_get(png, i)) + let i: Int = i + 1 + } + let ok: Int = fs_write_bytes(path, buf, n) + return ok +} diff --git a/elp/src/surface-profile.el b/elp/src/surface-profile.el new file mode 100644 index 0000000..71b0053 --- /dev/null +++ b/elp/src/surface-profile.el @@ -0,0 +1,153 @@ +// surface-profile.el - Surface profile data and accessors. +// +// THE NATIVE EFFERENT SEAM: surface = a pluggable PROFILE, using the exact same +// slot-map mechanism as language-profile.el. A language profile tells the +// realizer HOW to shape a natural-language surface (word order, morphology); a +// SURFACE profile tells the realizer WHICH surface to project meaning onto +// (markdown, docx, html, plain, or a non-text medium like symbolic music). +// +// The generalization is exact: realize_lang(form, profile) already renders a +// SemForm parameterized by a [String] profile read via lang_get. Surface is one +// more axis of that same profile vector. One frame (sem_frame), one plan step +// (sem_to_spec), one render (realize) — the surface is DATA, not a code path, +// precisely as language is data. Adding a surface means adding a profile, no +// engine change. This is the multimodal projector, native: geometry -> any +// surface, the efferent twin of ingest. +// +// Surface slot keys: +// surface - "markdown" | "docx" | "html" | "plain" | "midi" | "image" +// modality - "text" | "audio" | "image" | "video" +// media_type - MIME type of the emitted surface +// head_open - string prepended to a heading (e.g. "## " for markdown) +// head_close - string appended to a heading (e.g. "" for markdown, "" for html) +// emph_open - string opening emphasis (e.g. "*") +// emph_close - string closing emphasis (e.g. "*") +// item_mark - list-item marker (e.g. "- ") +// para_sep - paragraph separator (e.g. "\n\n") +// +// For a TEXT modality the render composes these markers around the surface that +// the EXISTING realizer produces (realize_lang / sem_realize). For a non-text +// modality (audio/image) the profile declares modality + media_type and the +// render dispatches to the medium projector, which reads the SAME frame's +// geometry (its intent/affect/structure) and projects it onto sound or pixels — +// deterministic-from-meaning, nothing invented. That dispatch point is where a +// music profile or image profile conforms, native, no parallel layer. + +// -- Constructor ------------------------------------------------------------- + +fn surface_profile(surface: String, modality: String, media_type: String, head_open: String, head_close: String, emph_open: String, emph_close: String, item_mark: String, para_sep: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "surface") + let r = native_list_append(r, surface) + let r = native_list_append(r, "modality") + let r = native_list_append(r, modality) + let r = native_list_append(r, "media_type") + let r = native_list_append(r, media_type) + let r = native_list_append(r, "head_open") + let r = native_list_append(r, head_open) + let r = native_list_append(r, "head_close") + let r = native_list_append(r, head_close) + let r = native_list_append(r, "emph_open") + let r = native_list_append(r, emph_open) + let r = native_list_append(r, "emph_close") + let r = native_list_append(r, emph_close) + let r = native_list_append(r, "item_mark") + let r = native_list_append(r, item_mark) + let r = native_list_append(r, "para_sep") + let r = native_list_append(r, para_sep) + return r +} + +// -- Accessor (same convention as lang_get; standalone so this is a leaf) ----- + +fn surface_get(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn surface_is_text(profile: [String]) -> Bool { + return str_eq(surface_get(profile, "modality"), "text") +} + +// -- Built-in TEXT surface profiles ------------------------------------------ + +// Markdown: headings with "## ", emphasis with "*", "- " list items. +fn surface_profile_markdown() -> [String] { + return surface_profile("markdown", "text", "text/markdown", "## ", "", "*", "*", "- ", "\n\n") +} + +// Plain text: no markup at all — headings become bare uppercase-free lines. +fn surface_profile_plain() -> [String] { + return surface_profile("plain", "text", "text/plain", "", "", "", "", " - ", "\n\n") +} + +// HTML: block-level heading/emphasis tags. +fn surface_profile_html() -> [String] { + return surface_profile("html", "text", "text/html", "

", "

", "", "", "
  • ", "\n") +} + +// docx: WordprocessingML is structural, not inline-markup; the head/emph slots +// carry the run/style intent that the OOXML emitter maps to . Declared +// here so docx is a first-class surface on the same seam. +fn surface_profile_docx() -> [String] { + return surface_profile("docx", "text", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "Heading2:", "", "b:", "", "bullet:", "\n") +} + +// -- Built-in NON-TEXT surface profiles (the multimodal seam) ---------------- + +// Symbolic music (MIDI): modality=audio. The render dispatches to the music +// projector, which reads the SAME frame's intent/affect and projects it to +// pitch/rhythm — deterministic-from-meaning. head/emph slots are empty because +// the medium is not textual; media_type names the surface. A music profile +// (scale/mode/instrument) is layered onto this by the audio agent, native. +fn surface_profile_midi() -> [String] { + return surface_profile("midi", "audio", "audio/midi", "", "", "", "", "", "") +} + +// Synthesized audio (WAV): modality=audio, peer to midi. The richer audio +// surface — the render SUPERPOSES ingested tonal primitives (sine at f0*n per an +// ingested instrument signature) into PCM, own-core, exactly as midi writes an +// SMF via struct. A music profile (scale/mode/instrument/adsr) layers onto this +// as its own [String] slot-map read by the same getter. Same frame -> midi OR +// audio, interchangeable; this is the audio agent's native conforming point. +fn surface_profile_audio() -> [String] { + return surface_profile("audio", "audio", "audio/wav", "", "", "", "", "", "") +} + +// Image (raster): modality=image. Documented seam — the render dispatches to the +// image projector, the efferent twin of image ingest, reading the same frame. +fn surface_profile_image() -> [String] { + return surface_profile("image", "image", "image/png", "", "", "", "", "", "") +} + +// -- Composition helpers: wrap realized TEXT with the surface's markers ------- +// +// These take text the EXISTING realizer already produced and shape it for the +// surface. They add NO content — pure surface typography over faithful text, +// exactly as the language profile adds no content, only linguistic form. + +fn surface_heading(profile: [String], text: String) -> String { + let o: String = surface_get(profile, "head_open") + let c: String = surface_get(profile, "head_close") + return o + text + c +} + +fn surface_emph(profile: [String], text: String) -> String { + let o: String = surface_get(profile, "emph_open") + let c: String = surface_get(profile, "emph_close") + return o + text + c +} + +// A section: a heading + a paragraph separator + the (already realized) body. +fn surface_section(profile: [String], heading: String, body: String) -> String { + let sep: String = surface_get(profile, "para_sep") + return surface_heading(profile, heading) + sep + body +} diff --git a/elp/tests/examples/surface-profile-demo.el b/elp/tests/examples/surface-profile-demo.el new file mode 100644 index 0000000..2fe6822 --- /dev/null +++ b/elp/tests/examples/surface-profile-demo.el @@ -0,0 +1,26 @@ +// surface-profile-demo.el - ONE SemFrame, realized ONCE, projected to THREE +// surfaces via surface profiles. Proves surface-as-profile natively: the frame +// and the realized sentence are identical; only the surface PROFILE differs. + +fn demo() -> String { + // 1. The shared frame (meaning-geometry): assert(Neuron, contain, the memory). + let frame: [String] = sem_frame("assert", "Neuron", "the memory", "") + + // 2. REALIZE once via the EXISTING native realizer (language = a profile). + let sentence: String = sem_realize(frame) + + // 3. PROJECT the same realized sentence onto three surfaces (surface = a + // profile). Same frame, same sentence, different surface — one render. + let heading: String = "Memory" + let md: String = surface_section(surface_profile_markdown(), heading, sentence) + let html: String = surface_section(surface_profile_html(), heading, sentence) + let plain: String = surface_section(surface_profile_plain(), heading, sentence) + + // 4. Report the non-text seam: a surface profile can declare an audio/image + // medium; the render dispatches to the medium projector on the SAME frame. + let midi_media: String = surface_get(surface_profile_midi(), "media_type") + + return "MD=[" + md + "] HTML=[" + html + "] PLAIN=[" + plain + "] MIDI_MEDIA=" + midi_media +} + +println(demo())