// pages.jsx — Otherworld.dev page components // ─── Shared bits ──────────────────────────────────────────────────────── function Brand({ size = 26 }) { return ( otherworld/adam morgan ); } function Nav({ route, go }) { const links = [ { id: "home", label: "Index", path: "/" }, { id: "markaloud", label: "Markaloud", path: "/markaloud" }, { id: "work", label: "Work", path: "/work" }, { id: "about", label: "About", path: "/about" }, { id: "contact", label: "Contact", path: "/contact" }, ]; const [open, setOpen] = React.useState(false); const onLink = (id) => (e) => { e.preventDefault(); setOpen(false); go(id); }; return ( ); } function Footer({ go }) { return ( ); } // Random-but-stable waveform values function makeWave(n, seed = 7) { const out = []; let s = seed; for (let i = 0; i < n; i++) { s = (s * 9301 + 49297) % 233280; const r = s / 233280; // shape so middle bars are taller const center = 1 - Math.abs(i - n / 2) / (n / 2); const h = 12 + r * 48 * (0.6 + 0.4 * center); out.push(Math.round(h)); } return out; } function Waveform({ progress = 0.42, live = 0.46 }) { const bars = React.useMemo(() => makeWave(56, 5), []); return ( ); } // ─── Markaloud product mock (audio + AI-structured feedback) ──────────── function MarkaloudMock() { return (
M
Final Viva — Eleanor Hughes
PHIL3402 · Submitted Tue 14:22 · 12:48 long
Reviewing
00:00 05:54 12:48
05:54
Strength Clear framing of Kant's transcendental argument — pair this with a counter-example next time to score the top band.
07:21
Action Citation drift around minute 7. Re-listen and tighten attribution; rubric criterion B2.
11:02
Next Strong recovery in the conclusion. Suggest extending this thread into the written commentary.
05:54 / 12:48
Rubric · QAA-aligned
); } // ─── Markaloud feature (three treatments) ─────────────────────────────── function MarkaloudFeature({ treatment, go }) { if (treatment === "manifesto") { return (
Featured product · Markaloud

Marking a viva should take minutes, not hours —
and feedback should feel like coaching, not a verdict.

Markaloud is an academic audio-marking tool. Educators upload a viva or oral assignment; our model timestamps strengths, flags rubric gaps, and drafts constructive comments students can actually act on.

Status
Closed beta · 2026
Sector
UK higher education
Approach
Constructive · rubric-aligned
Type
SaaS · Web app
{ e.preventDefault(); go("markaloud"); }}> Read the case study markaloud.cloud
); } if (treatment === "split") { return (
● Featured product

Markaloud.
Audio marking,{" "} without the dread.

Markers record once. Markaloud transcribes, timestamps, and structures constructive feedback against your rubric — so students get coaching, not just a grade.

Hourssaved
per marker, per cohort
Pilots
UK higher-ed · 2026
QAA
Aligned rubrics
{ e.preventDefault(); go("markaloud"); }}> Explore Markaloud Visit site
); } // spotlight (default) return (
● Featured product · Live

Markaloud turns viva recordings into constructive feedback.

An academic audio-marking tool for assignments and vivas. Upload once; Markaloud timestamps the moments that matter and drafts feedback that's aligned with your rubric — and your students.

Hourssaved
Per marker, per cohort
Pilots
UK higher-ed · 2026
QAA
Aligned rubrics
); } // ─── Hero ──────────────────────────────────────────────────────────────── function Hero({ go }) { return (
Now — shipping Markaloud beta

Engineering
for the messy,
physical world.

I'm Adam Morgan. I run Otherworld Dev Ltd — a software, AI & robotics engineering practice. Currently Lecturer in Robotics at Swansea University; going full-time independent from July 2026.

{ e.preventDefault(); go("markaloud"); }}> See Markaloud { e.preventDefault(); go("work"); }}> Selected work
Disciplines
Software · AI · Robotics · Hardware
Available
● From July 2026
Research
5 published · 1 in progress
Based
Swansea, UK
arm-04 · pick + place · cycle 003 ● LIVE
); } // ─── Work cards (used on home + work page) ─────────────────────────────── const WORKS = [ { n: "01", ttl: "Markaloud", desc: "The featured product — academic audio-marking for vivas and oral assessments. Whisper-based transcription, rubric-aligned feedback, time-pinned comments.", tags: ["AI", "SaaS", "Featured"], href: "/markaloud", vis: ( {Array.from({ length: 32 }).map((_, i) => { const h = 12 + ((i * 13) % 70); const lit = i > 10 && i < 18; return ; })} 05:54 · STRENGTH ), }, { n: "02", ttl: "Shopping List", desc: "A shared shopping list for Nextcloud — Vue 3 + TypeScript + PHP. Real-time sync, offline mode, ingredient parsing, auto-area detection.", tags: ["Software", "Vue", "Nextcloud"], href: "https://github.com/otherworld-dev/Shopping-List", vis: ( {/* Cards: each row = a shopping item with a category color bar */} {[ { y: 14, w: 130, c: "var(--accent)", chk: true, cat: "PRODUCE" }, { y: 32, w: 110, c: "var(--accent)", chk: true, cat: "PRODUCE" }, { y: 50, w: 144, c: "#a78bfa", chk: false, cat: "DAIRY" }, { y: 68, w: 122, c: "#a78bfa", chk: false, cat: "DAIRY" }, { y: 86, w: 136, c: "#f59e0b", chk: false, cat: "BAKERY" }, ].map((row, i) => ( {/* Checkbox */} {row.chk && ( )} {/* Item name + qty */} ))} 5 ITEMS · 3 AREAS ), }, { n: "03", ttl: "Bifrost", desc: "PyQt5 control GUI for a 6-axis robot arm — forward & inverse kinematics, 3D visualisation, sequence recorder. Talks RepRapFirmware over USB serial.", tags: ["Robotics", "Python", "PyQt5"], href: "https://github.com/otherworld-dev/Bifrost", vis: ( {[[40,100],[40,78],[78,50],[118,44],[142,58],[156,50]].map(([x,y], i) => ( ))} TCP ), }, { n: "04", ttl: "RSIPI", desc: "Real-time KUKA control over the Robot Sensor Interface protocol — Python at 250 Hz, UDP/XML, trajectory generation, safety limits. MIT-licensed library.", tags: ["Robotics", "Python", "Real-time"], href: "https://github.com/otherworld-dev/rsi-pi", vis: ( PC KR CTRL RKorr RIst · IPOC 4ms · 250Hz {[80,90,100,110,120].map((x, i) => ( ))} ), }, { n: "05", ttl: "Drone tracker", desc: "Real-time drone detection & tracking with a custom YOLOv11n model and a global-shutter USB camera. Two-state FSM: scanning → tracking, with offset logging.", tags: ["AI", "Computer Vision", "Python"], href: "https://github.com/otherworld-dev/Drone-Tracking", vis: ( drone 0.89 ● TRACKING ), }, { n: "06", ttl: "ThorRR", desc: "An open-source 6-DOF printable robot arm, forked from Thor and redesigned around RepRapFirmware + the Octopus Pro board. Stretches to 625 mm, ≤ £350 BOM.", tags: ["Hardware", "3D Print", "Open Source"], href: "https://github.com/otherworld-dev/ThorRR", vis: ( {[ { y: 96, w: 70, label: "BASE" }, { y: 80, w: 56, label: "J1" }, { y: 66, w: 44, label: "J2" }, { y: 52, w: 38, label: "J3" }, { y: 38, w: 30, label: "WRIST" }, { y: 24, w: 22, label: "TOOL" }, ].map((s, i) => ( {s.label} ))} 625 mm · 6-DOF ), }, { n: "07", ttl: "DrillCam", desc: "High-speed camera app for monitoring drilling — 309 fps on a global-shutter OV9281, lossless FFV1 capture, FFT vibration analysis, sub-pixel motion tracking.", tags: ["AI", "Imaging", "Raspberry Pi"], href: "https://github.com/otherworld-dev/Drill-Cam", vis: ( {Array.from({ length: 38 }).map((_, i) => { const x = 14 + i * 4.6; const phase = i * 0.62; const yMid = 40; const a = 16 * (0.5 + 0.5 * Math.sin(i * 0.18)); const y1 = yMid - a * Math.sin(phase); const y2 = yMid - a * Math.sin(phase + 0.62); return 8 && i < 22 ? "var(--accent)" : "var(--fg-2)"} strokeWidth="1.2"/>; })} VIBRATION · 187Hz 309 fps OV9281 · global shutter ), }, ]; function WorkGrid({ limit }) { const items = limit ? WORKS.slice(0, limit) : WORKS; const [stats, setStats] = React.useState({}); React.useEffect(() => { let cancelled = false; const ghRepos = items .map(w => w.href || "") .filter(href => href.startsWith("https://github.com/")) .map(href => href.replace("https://github.com/", "").replace(/\/$/, "")); if (!ghRepos.length) return; Promise.all(ghRepos.map(async repo => { try { const r = await fetch(`https://api.github.com/repos/${repo}`); if (!r.ok) return null; const data = await r.json(); return [repo, { stars: data.stargazers_count, lang: data.language, updated: data.pushed_at, }]; } catch { return null; } })).then(results => { if (cancelled) return; const map = {}; for (const r of results) if (r) map[r[0]] = r[1]; setStats(map); }); return () => { cancelled = true; }; }, []); function formatAge(iso) { if (!iso) return null; const d = Math.max(0, (Date.now() - new Date(iso).getTime()) / 86400000); if (d < 1) return "today"; if (d < 2) return "yesterday"; if (d < 30) return `${Math.round(d)}d ago`; if (d < 365) return `${Math.round(d / 30)}mo ago`; return `${Math.round(d / 365)}y ago`; } return (
{items.map((w, i) => { const isExternal = w.href && w.href.startsWith("http"); const isGh = w.href && w.href.startsWith("https://github.com/"); const repoKey = isGh ? w.href.replace("https://github.com/", "").replace(/\/$/, "") : null; const s = repoKey && stats[repoKey]; const inner = (
{w.n} / {WORKS.length.toString().padStart(2, "0")} {w.badge && ( {w.badge} )} {s && ( ★ {s.stars} {s.updated ? · {formatAge(s.updated)} : null} )}
{w.vis}
{w.ttl} {isExternal ? "↗" : "→"}
{w.desc}
{w.tags.map(t => {t})}
); const className = "work-card fade-in"; const delay = Math.min(i + 1, 5); const style = { textDecoration: "none", color: "inherit", cursor: "pointer" }; if (w.href) { return ( {inner} ); } return
{inner}
; })}
); } // ─── Clients ───────────────────────────────────────────────────────────── const CLIENTS = ["Thor Community", "ThorRR", "Bifrost", "Nextcloud", "KUKA RSI", "OV9281"]; function Clients() { return (
{CLIENTS.map((c, i) => (
{c.toUpperCase()}
))}
); } // ─── Contact CTA (home strip) ──────────────────────────────────────────── function ContactCTA({ go }) { return (
Open to work

Have a hard, weird,
cross-discipline problem?

I take on a small number of consulting and build engagements each year. If yours bridges hardware, software, and people — I'd love to hear about it.

{ e.preventDefault(); go("contact"); }}> Start a conversation contact@otherworld.dev
EngagementBuild · Advise · Diligence
DisciplinesSoftware · AI · Robotics
ResponseWithin 48 hours
BasedSwansea, UK · remote / hybrid
); } // Export to window for cross-file access Object.assign(window, { Nav, Footer, Hero, MarkaloudFeature, MarkaloudMock, WorkGrid, Clients, ContactCTA, Brand, WORKS, CLIENTS, Waveform, });