See 400 years of opera as a living, breathing map.
Click any dot to discover an opera, its composer, when it premiered, and how it connects to everything else.
New to opera? You don't need to know anything about opera to use Violetta. Click the Discover tab for guided tours, a glossary of every term you'll hear, and curated paths through 400 years of music history. If there's a performance near you, the Events tab will show you what's on.
Imagine every opera ever written as a dot on a map. Now draw lines between them: this one was composed by the same person as that one, this one was inspired by that one, this one sounds like that one. That's Violetta.
It pulls real data from Wikidata, MusicBrainz, Open Opus, IMSLP, and RISM, then renders it as two interactive views you can explore in your browser:
- Network Graph -- a force-directed web where operas cluster around their composers and lines show relationships like "composed by", "inspired by", and "similar to"
- Timeline Scatter -- every opera plotted by its premiere year, so you can literally watch opera evolve from Monteverdi's Baroque experiments through Verdi's Romantic dramas to today's contemporary works
- Discover -- guided tours for opera newcomers ("The Big 5", "Mozart for Beginners", "Italian Romance"), a glossary of opera terms, and an era-by-era history guide
- Events -- local opera performances scraped from venue websites, filtered by region
Click any node. Filter by era. Search by name. Everything is linked -- selecting a dot in the timeline highlights it in the network, and vice versa.
Wikidata + MusicBrainz + Open Opus + IMSLP + RISM
|
Swift CLI pulls it all down
|
Merges, deduplicates, builds graph.json
|
Core ML on your Neural Engine computes
sentence embeddings (384-dim vectors)
|
Cosine similarity finds "sounds like" links
UMAP projects everything to 2D
|
sigma.js + deck.gl render it in your browser
(WebGL on your GPU, 60fps)
No cloud. No API keys. Everything runs locally on your Mac, using Apple Silicon hardware acceleration end-to-end.
This project squeezes everything it can out of your Mac's hardware. Here's what runs where:
| Chip Component | What it does in Violetta |
|---|---|
| Neural Engine (16-core ANE) | Runs the all-MiniLM-L6-v2 sentence transformer via Core ML. Converts opera descriptions into 384-dimensional vectors at ~1000 embeddings/sec. This is what powers the "similar to" edges -- operas with similar descriptions cluster together. |
| GPU (Metal) | WebGL rendering through sigma.js (network graph, thousands of nodes/edges at 60fps) and deck.gl (timeline scatter with GPU-accelerated point rendering). The ForceAtlas2 layout algorithm runs in a web worker but the final rendering is all GPU. |
| CPU (Performance cores) | Swift's structured concurrency (async/await with TaskGroup) fetches from 6 APIs in parallel. Go's Playwright-based scraper runs headless Chromium for venue pages. UMAP dimensionality reduction runs on CPU. |
| Unified Memory | The entire pipeline -- fetched data, graph structure, embeddings, Core ML model weights -- lives in unified memory. No copying between CPU and GPU. A 384-dim embedding computed on the ANE is immediately available to the GPU for rendering. |
The embedding pipeline specifically uses MLComputeUnits.all, which tells Core ML to use the Neural Engine when available, falling back to GPU, then CPU. On M1+ chips, the Neural Engine handles the transformer inference while the GPU stays free for rendering.
Opera didn't appear out of nowhere. It evolved over 400 years, and Violetta color-codes every work by its era so you can see the evolution at a glance:
Composer nodes appear in gray (#6c757d). Node size reflects how many connections a node has -- Verdi's node is big because he composed a lot of operas.
Violetta scrapes real venue websites to show you what's playing in your area. Click the Events tab to browse upcoming performances, filtered by region.
Currently configured regions:
| Region | Venues |
|---|---|
| Southern California | LA Opera, Long Beach Opera, Pacific Opera Project, San Diego Opera, Mission Opera, Pacific Symphony |
| Northern California | San Francisco Opera, Opera San Jose |
| New Mexico | Santa Fe Opera |
| Atlanta | The Atlanta Opera |
To fetch the latest event listings:
make scrape-regional-all # scrape all configured venues
make scrape-socal # or just one regionEvents appear automatically in the Events tab after scraping. Opera titles are fuzzy-matched against the graph, so clicking an event card can jump you straight to that opera's node.
Got a favorite opera company website? Drop the URL into the Scraper page and Violetta will try to extract event data automatically.
- Click the Scraper button in the header
- Paste any URL that lists opera performances
- Optionally add a label (e.g., "Chicago Lyric Opera")
- Click Scrape -- Violetta renders the page in a headless browser and runs three extraction strategies:
| Strategy | How it works | Best for |
|---|---|---|
| JSON-LD / Schema.org | Reads structured <script type="application/ld+json"> data |
Major venues with modern websites |
| Heuristic DOM | Scans for .event, .performance, article, [datetime] patterns |
Most event listing pages |
| Meta fallback | Extracts from OpenGraph and <meta> tags |
Single-event pages |
Extracted opera titles are fuzzy-matched against known operas in the graph using Levenshtein distance. Matched events link directly to graph nodes.
All scraped data is saved locally to data/raw/custom/ -- no cloud, no API keys.
Or use the API directly:
curl -X POST http://localhost:8080/api/scrape-url \
-H "Content-Type: application/json" \
-d '{"url": "https://www.sfopera.com/on-stage/", "label": "SF Opera"}'# Clone and enter the repo
git clone <repo-url> && cd machu-picchu
# Install all dependencies (Swift, Go, Node)
make setup
# Pull opera data from live APIs
make fetch
# One-command deploy -- builds web UI + starts server
make serve
# --> http://localhost:8080 (web UI + API, single binary)Or for development with hot reload:
./start.sh --dev
# --> http://localhost:5173 (Vite dev server, proxies API to :8080)Or run the full pipeline including AI embeddings:
make all # setup -> fetch -> embed -> build -> serve| Tool | Version | Install |
|---|---|---|
| macOS | 15+ (Sequoia) | -- |
| Xcode | 26+ | Mac App Store |
| Swift | 6.2+ | Included with Xcode |
| Go | 1.25+ | brew install go |
| Node.js | 20+ | brew install node |
Captured automatically with Playwright-Go (make screenshots).
| View | Description |
|---|---|
| Network Graph | Force-directed graph with ForceAtlas2 layout. Operas clustered around composers, colored by era. |
| Timeline (Scatter) | Every opera plotted by premiere year with connecting lines showing relationships. Zoomed in on the Late Romantic golden age. |
| Timeline (Decades) | Decade-bucketed view with jitter for visual separation. |
| Discover | Guided tours, glossary, and era history for opera newcomers. |
| Events | Local opera performances scraped from venue websites. |
To regenerate screenshots: make screenshots
The full visualization is deployed automatically to GitHub Pages on every push to main:
https://parkwardrr.github.io/Violetta-Opera-Graph-Relationship-Maps/
No backend needed -- the graph, timeline, and Discover tabs work entirely in the browser. Only the Events/Scraper features require the Go server.
The deployment is handled by a GitHub Actions workflow (.github/workflows/deploy-pages.yml) that builds the Vite app with graph.json bundled from web/public/.
Violetta can also be deployed to S3 or any static host with no backend required.
make build-s3
# Output: web/dist/ with graph.json bundled in./s3/deploy.sh my-violetta-bucket --region us-east-1This will:
- Build the web UI with Vite
- Bundle
graph.jsonandprojections.jsonintodist/ - Create the S3 bucket with website hosting
- Set public read access
- Upload with cache headers (1 year for assets, 5 minutes for HTML/JSON)
The SPA routing is handled by setting index.html as both the index and error document.
make build-s3
aws s3 sync web/dist/ s3://my-bucket --delete
aws s3api put-bucket-website --bucket my-bucket \
--website-configuration file://s3/website-config.jsongraph TB
subgraph Data["Data Pipeline (offline)"]
WD["Wikidata SPARQL"]
OO["Open Opus API"]
MB["MusicBrainz API"]
IM["IMSLP API"]
RI["RISM Online"]
VN["Regional Venues"]
WD & OO & MB & IM & RI -->|"Swift async/await"| DF["data_fetch CLI"]
VN -->|"Go + Playwright"| SC["scraper CLI"]
DF -->|CSV + JSON| RAW["data/raw/"]
SC -->|JSON| RAW
RAW -->|"swift run opera-fetch process"| GJ["graph.json"]
GJ -->|"swift run opera-embed"| EMB["embeddings.json"]
EMB -->|"node compute-projections.mjs"| PROJ["projections.json"]
end
subgraph ML["Apple Silicon ML Pipeline"]
GJ -->|"Text repr per opera"| TOK["SentencePiece Tokenizer"]
TOK -->|"Neural Engine (ANE)"| COREML["Core ML\nall-MiniLM-L6-v2"]
COREML -->|"384-dim vectors"| SIM["Cosine Similarity\n(Accelerate vDSP)"]
SIM -->|"threshold > 0.7"| EDGES["similar_to edges"]
COREML -->|"UMAP projection"| PROJ2["2D coordinates"]
end
subgraph Server["Go Server (port 8080)"]
API["/api/events\n/api/scrape-url\n/api/sources"]
STATIC["Static file serving\nSPA fallback"]
end
subgraph Web["Web UI (runtime)"]
GJ -->|fetch /graph.json| WEB["React + Vite"]
WEB --> SIG["sigma.js Network View\n(WebGL / Metal GPU)"]
WEB --> DGL["deck.gl Timeline View\n(WebGL / Metal GPU)"]
WEB --> DISC["Discover + Events"]
SIG <-->|"Zustand selectionStore"| DGL
DISC <-->|"eventsStore"| API
end
SC -->|"serves API"| API
STATIC -->|"serves web/dist/"| WEB
style Data fill:#1e293b,stroke:#334155,color:#e2e8f0
style ML fill:#2d1b4e,stroke:#8b5cf6,color:#e2e8f0
style Web fill:#0f172a,stroke:#3b82f6,color:#e2e8f0
flowchart LR
subgraph Fetch["Phase 1: Fetch"]
A["Wikidata\n57 operas\n55 composers"] --> D["data/raw/"]
B["Open Opus\n3.3MB dump"] --> D
C["MusicBrainz\n2000 works"] --> D
E["IMSLP\n500 pages"] --> D
end
subgraph Process["Phase 2: Process"]
D -->|"merge + dedupe"| F["graph.json\n88 nodes\n53 edges"]
end
subgraph Embed["Phase 3: Embed (Neural Engine)"]
F -->|"all-MiniLM-L6-v2\nCore ML ANE"| G["embeddings.json\n384-dim vectors"]
G -->|"cosine > 0.7"| H["similar_to edges"]
G -->|"UMAP"| I["projections.json\n2D coords"]
end
subgraph Render["Phase 4: Render (Metal GPU)"]
F --> J["sigma.js\nForceAtlas2 WebGL"]
I --> K["deck.gl\nScatterplot WebGL"]
end
style Fetch fill:#1e3a5f,stroke:#3b82f6,color:#e2e8f0
style Process fill:#1e3a5f,stroke:#3b82f6,color:#e2e8f0
style Embed fill:#2d1b4e,stroke:#8b5cf6,color:#e2e8f0
style Render fill:#1a2e1a,stroke:#22c55e,color:#e2e8f0
flowchart TD
A["graph.json\nopera nodes"] -->|"Build text repr"| B["'{title} by {composer},\n{year}, {genre}, {language}, {era}'"]
B -->|"SentencePiece tokenize"| T["Token IDs\n(max 128 tokens)"]
T -->|"Batch size 32\nMLComputeUnits.all"| C["all-MiniLM-L6-v2\nCore ML .mlpackage\n16-core Neural Engine"]
C --> D["384-dim vectors\nFloat16 (ANE-native)"]
D -->|"vDSP cosine similarity\n(Accelerate framework)"| E{"cosine > 0.7?"}
E -->|Yes| F["similar_to edge\nwith similarity weight"]
E -->|No| G["No edge"]
D -->|"UMAP projection\n(CPU, ~2 sec)"| H["2D coordinates\nprojX, projY"]
H --> I["Timeline scatter\nX = premiereYear\nY = UMAP axis"]
style C fill:#2d1b4e,stroke:#8b5cf6,color:#e2e8f0
style T fill:#1e3a5f,stroke:#3b82f6,color:#e2e8f0
graph TD
App["App"]
App --> Header["Header + Tabs + ThemeToggle + ExportMenu"]
App --> FB["FilterBar"]
App --> Main["Main Content"]
Main --> NV["NetworkView"]
Main --> TV["TimelineView"]
Main --> DP["DiscoverPage"]
Main --> EV["EventsView"]
Main --> ND["NodeDetail Sidebar"]
App --> Pages["Full Pages"]
Pages --> PP["PreferencesPage"]
Pages --> SP["ScraperPage"]
Pages --> AP["AdminPage"]
NV --> SC["SigmaContainer"]
SC --> GE["GraphEvents"]
SC --> FA["ForceAtlas2Layout"]
SC --> GF["GraphFilters"]
TV --> DGL["DeckGL + OrthographicView"]
DGL --> SPL["ScatterplotLayer"]
DP --> Tours["Guided Tours"]
DP --> Glossary["Opera Glossary"]
DP --> Eras["Era Guide"]
FB --> Search["Search Input"]
FB --> Comp["Composer Dropdown"]
FB --> Era["Era Chips"]
FB --> Dec["Decade Range"]
Main --> EL["EraLegend"]
subgraph Stores["Zustand Stores"]
SS["selectionStore"]
FS["filterStore"]
GS["graphStore"]
ES["eventsStore"]
PS["preferencesStore"]
end
GE <-->|"click/hover"| SS
GF <-->|"filter state"| FS
SC <-->|"graph instance"| GS
SPL <-->|"click/hover"| SS
FB <-->|"filter state"| FS
ND <-->|"selected node"| SS
EV <-->|"events data"| ES
SP <-->|"scraping"| ES
style Stores fill:#2d1b4e,stroke:#8b5cf6,color:#e2e8f0
sequenceDiagram
participant User
participant NetworkView
participant selectionStore
participant TimelineView
participant NodeDetail
User->>NetworkView: Click node "La Traviata"
NetworkView->>selectionStore: setSelected({Q1350}, 'network')
selectionStore-->>TimelineView: selectedNodeKeys changed
TimelineView->>TimelineView: Highlight Q1350 dot
selectionStore-->>NodeDetail: Show La Traviata details
selectionStore-->>NetworkView: Dim non-selected nodes
User->>TimelineView: Click dot "Carmen"
TimelineView->>selectionStore: setSelected({Q2226}, 'timeline')
selectionStore-->>NetworkView: Highlight Carmen, dim others
selectionStore-->>NodeDetail: Show Carmen details
graph LR
subgraph Repo["Repo (machu-picchu/)"]
DF["data_fetch/\nSwift CLI"]
EM["embeddings/\nSwift CLI + Core ML"]
SC["scraper/\nGo CLI"]
SR["scripts/\nNode.js"]
WB["web/\nReact + TS"]
MK["Makefile"]
CF["config.yaml"]
end
subgraph Static["~/Violetta-Opera-Graph-Relationship-Maps/"]
DR["data/raw/\nCSV, JSON, HTML"]
DP["data/processed/\ngraph.json\nembeddings.json\nprojections.json"]
SM["static/models/\nCore ML .mlpackage"]
end
DF -->|"writes"| DR
SC -->|"writes"| DR
DF -->|"reads raw, writes"| DP
EM -->|"reads graph.json, writes"| DP
EM -->|"downloads"| SM
SR -->|"reads embeddings, writes"| DP
WB -->|"serves"| DP
style Repo fill:#1e293b,stroke:#334155,color:#e2e8f0
style Static fill:#1a2e1a,stroke:#22c55e,color:#e2e8f0
| Target | Description |
|---|---|
make serve |
One-command deploy: build web UI + start Go server on port 8080 |
make server |
Start Go API server only (serves web/dist/ + API endpoints) |
make setup |
Install all dependencies (Swift build, Go modules, npm install) |
make fetch |
Pull data from all APIs + scrape regional venues |
make embed |
Generate Core ML sentence embeddings + UMAP 2D projections |
make build |
Build web UI for production (web/dist/) |
make dev |
Start Vite dev server at localhost:5173 |
make all |
Full pipeline: setup -> fetch -> embed -> build |
make process |
Re-process raw data into graph.json (no re-fetching) |
make scrape-socal |
Scrape Southern California venues only |
make scrape-norcal |
Scrape Northern California venues only |
make scrape-nm |
Scrape New Mexico venues only |
make scrape-atl |
Scrape Atlanta venues only |
make scrape-regional-all |
Scrape all regional venues |
make build-s3 |
Build self-contained static site for S3/CDN deployment |
make screenshots |
Capture UI screenshots with Playwright-Go for README |
make test-web |
Run Playwright-Go e2e smoke tests |
make clean |
Remove build artifacts (preserves fetched data) |
| Source | Type | Rate Limit | Data |
|---|---|---|---|
| Wikidata | SPARQL | 2 req/s | Opera metadata, composers, relationships |
| Open Opus | REST JSON | 5 req/s | Composer bios, work catalogs |
| MusicBrainz | REST JSON | 1 req/s | Work hierarchy, recordings, releases |
| IMSLP | MediaWiki API | 1 req/s | Score metadata, publication dates |
| RISM Online | REST JSON | 2 req/s | Historical music source records |
| Regional Venues | HTML Scraping | Configurable | Upcoming performances by venue |
erDiagram
OPERA {
string key "Wikidata Q-ID"
string label "Opera title"
string composerId "Composer Q-ID"
string composerName "Composer name"
int premiereYear "Year of premiere"
string premiereLocation "City"
string language "Sung language"
string genre "Opera subgenre"
string eraBucket "Baroque|Classical|..."
string decade "1850s|1860s|..."
float x "Graph X position"
float y "Graph Y position"
float size "Node radius"
string color "Hex color by era"
}
COMPOSER {
string key "Wikidata Q-ID"
string label "Full name"
int birthYear "Born"
int deathYear "Died"
string nationality "Country"
float x "Graph X position"
float y "Graph Y position"
}
OPERA }|--|| COMPOSER : "composed_by"
OPERA ||--o{ OPERA : "based_on"
OPERA ||--o{ OPERA : "inspired_by"
OPERA ||--o{ OPERA : "similar_to"
graph TD
subgraph socal["Southern California"]
LA["LA Opera"]
LB["Long Beach Opera"]
POP["Pacific Opera Project"]
SD["San Diego Opera"]
MO["Mission Opera"]
PS["Pacific Symphony"]
end
subgraph norcal["Northern California"]
SF["San Francisco Opera"]
SJ["Opera San Jose"]
end
subgraph nm["New Mexico"]
SANTA["Santa Fe Opera"]
end
subgraph atl["Atlanta"]
ATL["The Atlanta Opera"]
end
style socal fill:#1e3a5f,stroke:#3b82f6,color:#e2e8f0
style norcal fill:#1e3a5f,stroke:#3b82f6,color:#e2e8f0
style nm fill:#2d1b4e,stroke:#8b5cf6,color:#e2e8f0
style atl fill:#1a2e1a,stroke:#22c55e,color:#e2e8f0
The web UI supports three export formats from the header menu:
| Format | Description |
|---|---|
| PNG | Rasterized screenshot of the current sigma.js canvas |
| SVG | Vector graphic generated from node/edge positions |
| JSON | Raw graph.json download (Graphology format) |
Violetta includes a built-in URL scraper and admin UI.
- Scraper page: Click the Scraper button in the header to drop in any URL and extract opera events. The smart parser tries JSON-LD structured data first, then heuristic DOM extraction, then meta tag fallback. Extracted titles are fuzzy-matched to graph nodes.
- Admin page: Manage data ingestion, trigger scrapes, and inspect
config.yaml.
Start the server with make serve or make server, then open http://localhost:8080.
All scraping limits, rate limits, and regional venue configs live in config.yaml.
See STATIC_FILES.md for the full data directory layout at ~/Violetta-Opera-Graph-Relationship-Maps/.
- Edit
config.yamland add a new venue under the appropriate region - Run
make scrape-regional-allto scrape all venues
- name: "Your Opera Company"
code: "youropera"
official_url: "https://www.youropera.org"
calendar_url: "https://www.youropera.org/events"
city: "Your City"
state: "ST"mindmap
root((Violetta))
Data Ingestion
Swift 6.2
async/await URLSession
Actor-based concurrency
TaskGroup parallel fetching
SPM packages
Go 1.25
Playwright browser automation
Domain-aware rate limiting
ML Pipeline
Core ML
all-MiniLM-L6-v2 transformer
MLComputeUnits.all
Neural Engine first, GPU fallback
Float16 ANE-native precision
384-dim sentence embeddings
Accelerate
vDSP cosine similarity
SIMD vector operations
UMAP
umap-js
2D dimensionality reduction
Visualization
sigma.js 3
WebGL rendering via Metal
ForceAtlas2 web worker
Graphology data model
deck.gl 9
GPU-accelerated scatter
OrthographicView
Metal-backed WebGL
React 19
Vite 6
TypeScript 5.7
Zustand state management
Tailwind CSS 4
- Multi-source data ingestion (Wikidata, Open Opus, MusicBrainz, IMSLP, RISM)
- Core ML sentence embeddings with Neural Engine acceleration
- Cosine similarity "similar_to" edge discovery
- UMAP 2D projection for timeline layout
- sigma.js force-directed network graph (WebGL)
- deck.gl timeline scatter plot (WebGL)
- Linked selection across both views (Zustand)
- Filter by composer, era, decade range, and text search
- PNG / SVG / JSON export
- Regional venue scraping (Go + Playwright)
- Era legend with color-coded date ranges
- Detail sidebar with related operas and connections
- Discover tab -- guided tours, opera glossary, era guide
- Events tab -- local performances from scraped venue data
- Smart URL scraper -- JSON-LD, heuristic DOM, meta fallback with fuzzy matching
- Single-binary deployment -- Go server serves web UI + API on one port
- Dark/light theme with system detection
- Preferences page with customizable display options
- iOS app -- SwiftUI native client with the same graph, running Core ML on-device
- Public deployment -- Vercel/Fly.io hosting, custom domain, SEO optimization
- User accounts -- favorites, personal notes, "my opera list" with CloudKit sync
- Richer "similar_to" edges -- use Apple's
NaturalLanguageframework for multilingual semantic similarity alongside MiniLM - GPU-accelerated layout -- port ForceAtlas2 to Metal compute shaders for instant graph layout (no 3-second settle wait)
- Live venue integration -- real-time "what's playing near me" overlay with MapKit and Core Location
- Aria-level granularity -- break operas into individual arias/scenes as sub-nodes, with audio fingerprint similarity via
AudioToolbox - Core ML speech-to-search -- use Apple's on-device
Speechframework to search the graph by speaking an opera name or humming a melody - Create ML recommendations -- train a tabular classifier on user interaction patterns to recommend "operas you might like" based on what you've explored
- VisionOS spatial graph -- render the network graph in 3D using RealityKit on Apple Vision Pro, walking through opera history in spatial computing
- MLX fine-tuning -- fine-tune a small language model (via Apple MLX) on opera synopses for natural-language Q&A ("What operas are about jealousy?")
- Offline-first sync -- Core Data + CloudKit for persistent user annotations and favorites across devices
- OperaBase / Operissimo integration -- additional data sources for cast, recording, and review data
- Performance analytics -- track which operas are performed most frequently worldwide using scraped season data