scaffold: 12 Hobbies Studio site + OpenScribe project page

Initial static site (no framework, no build) for the 12 Hobbies Studio maker
studio, featuring OpenScribe as the flagship project.

What changed:
- index.html: studio home - hero, featured OpenScribe block with sync diagram,
  projects grid, about. British spelling, dark warm theme.
- projects/openscribe.html: OpenScribe product page. Leads with the "bring your
  own AI" differentiator, an interactive provider picker, the feature set, a
  four-step build guide, and licensing.
- assets/styles.css: all styling (palette, cards, featured layout, picker, code blocks).
- assets/app.js: dependency-free provider picker that swaps a sample server .env
  snippet across Ollama / OpenAI / Anthropic / Groq / LM Studio - on message with
  OpenScribe's any-AI design.
- README, LICENSE (MIT for site code), .gitignore.

Why:
- The user asked to develop a site on 12 Hobbies Studio; this is the first cut,
  showcasing OpenScribe and the connect-to-any-AI capability. Branding uses sensible
  defaults and is trivially editable (noted in README).

Notes:
- Pure static files; deploy to any static host or Forgejo Pages. Validated: app.js
  passes node --check; HTML section tags balanced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Laurence 2026-07-03 19:00:36 +01:00
commit e1bf5a2d7a
7 changed files with 497 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.DS_Store
Thumbs.db
*.swp
*~
node_modules/

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 12 Hobbies Studio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

39
README.md Normal file
View file

@ -0,0 +1,39 @@
# 12 Hobbies Studio - website
A small, dependency-free static site for **12 Hobbies Studio**, a maker studio that builds
open-source, self-hostable projects. The flagship project featured here is
[OpenScribe](https://git.discworld.casa/laurence/openscribe), an open AI voice recorder you
can connect to any AI.
> Note on branding: this is an initial site with sensible defaults (name, tagline, colours,
> copy). All of it is easy to change - edit `assets/styles.css` for the look and the HTML for
> the words. Nothing here is locked in.
## What's here
```
index.html Studio home: hero, featured project, projects grid, about
projects/openscribe.html OpenScribe product page + interactive "bring your own AI" picker
assets/styles.css All styling (no framework, no build)
assets/app.js Provider picker that swaps a sample server .env snippet
```
## Preview locally
It is plain HTML/CSS/JS - open `index.html` in a browser, or serve the folder:
```bash
python -m http.server 8080 # then visit http://localhost:8080
```
## Deploy
Any static host works (it is just files). For Forgejo Pages, publish this repo per your
instance's Pages setup; for a plain web server, copy the files to the web root. There is no
build step and no server-side code.
## Licence
Site code (HTML/CSS/JS) is MIT (see `LICENSE`). Prose content is the studio's own.
OpenScribe is an independent project inspired by the Plaud class of device and is not
affiliated with Plaud.

86
assets/app.js Normal file
View file

@ -0,0 +1,86 @@
// SPDX-License-Identifier: MIT
// Interactive AI-provider picker for the OpenScribe page. Swaps a sample server .env
// snippet as you choose a provider - the whole point of OpenScribe: bring your own AI.
(function () {
var PROVIDERS = {
ollama: {
label: "Ollama (local)",
lines: [
["c", "# Fully local and self-hosted - no cloud, no key"],
["k", "OPENSCRIBE_LLM_PROVIDER", "ollama"],
["k", "OPENSCRIBE_OLLAMA_URL", "http://localhost:11434"],
["k", "OPENSCRIBE_OLLAMA_MODEL", "llama3.1"],
],
},
openai: {
label: "OpenAI",
lines: [
["c", "# Any OpenAI-compatible endpoint works the same way"],
["k", "OPENSCRIBE_LLM_PROVIDER", "openai_compatible"],
["k", "OPENSCRIBE_LLM_BASE_URL", "https://api.openai.com/v1"],
["k", "OPENSCRIBE_LLM_API_KEY", "sk-..."],
["k", "OPENSCRIBE_LLM_MODEL", "gpt-4o-mini"],
],
},
anthropic: {
label: "Anthropic (Claude)",
lines: [
["c", "# Commercial Claude via the official Anthropic API"],
["k", "OPENSCRIBE_LLM_PROVIDER", "anthropic"],
["k", "OPENSCRIBE_LLM_API_KEY", "sk-ant-..."],
["k", "OPENSCRIBE_LLM_MODEL", "claude-opus-4-8"],
],
},
groq: {
label: "Groq",
lines: [
["c", "# Fast + cheap, OpenAI-compatible"],
["k", "OPENSCRIBE_LLM_PROVIDER", "openai_compatible"],
["k", "OPENSCRIBE_LLM_BASE_URL", "https://api.groq.com/openai/v1"],
["k", "OPENSCRIBE_LLM_API_KEY", "gsk_..."],
["k", "OPENSCRIBE_LLM_MODEL", "llama-3.3-70b-versatile"],
],
},
lmstudio: {
label: "LM Studio (local)",
lines: [
["c", "# A local model server on your LAN, OpenAI-compatible"],
["k", "OPENSCRIBE_LLM_PROVIDER", "openai_compatible"],
["k", "OPENSCRIBE_LLM_BASE_URL", "http://localhost:1234/v1"],
["k", "OPENSCRIBE_LLM_MODEL", "local-model"],
],
},
};
function esc(s) { return s.replace(/&/g, "&amp;").replace(/</g, "&lt;"); }
function render(key) {
var p = PROVIDERS[key];
var html = p.lines.map(function (l) {
if (l[0] === "c") return '<span class="c">' + esc(l[1]) + "</span>";
return '<span class="k">' + esc(l[1]) + "</span>=" + esc(l[2]);
}).join("\n");
document.getElementById("env-out").innerHTML = html;
}
function init() {
var picker = document.getElementById("provider-picker");
if (!picker) return;
Object.keys(PROVIDERS).forEach(function (key, i) {
var b = document.createElement("button");
b.type = "button";
b.textContent = PROVIDERS[key].label;
b.setAttribute("aria-pressed", i === 0 ? "true" : "false");
b.addEventListener("click", function () {
picker.querySelectorAll("button").forEach(function (x) { x.setAttribute("aria-pressed", "false"); });
b.setAttribute("aria-pressed", "true");
render(key);
});
picker.appendChild(b);
});
render(Object.keys(PROVIDERS)[0]);
}
if (document.readyState !== "loading") init();
else document.addEventListener("DOMContentLoaded", init);
})();

128
assets/styles.css Normal file
View file

@ -0,0 +1,128 @@
/* SPDX-License-Identifier: MIT
12 Hobbies Studio - site styles. No framework, no build step. */
:root {
--ink: #10131a;
--ink-2: #171b24;
--panel: #1e2330;
--line: #2c3444;
--text: #e7ecf3;
--muted: #9aa6b8;
--accent: #f0a35e; /* warm amber */
--accent-2: #6ec7c0; /* teal */
--radius: 14px;
--wrap: 1080px;
--sans: ui-sans-serif, system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", "Cascadia Code", "Fira Code", Consolas, monospace;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: var(--sans);
color: var(--text);
background:
radial-gradient(1200px 600px at 80% -10%, rgba(240,163,94,0.10), transparent 60%),
radial-gradient(1000px 500px at -10% 10%, rgba(110,199,192,0.08), transparent 55%),
var(--ink);
line-height: 1.6;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 20px; }
/* Header */
.site-header {
position: sticky; top: 0; z-index: 10;
backdrop-filter: blur(8px);
background: rgba(16,19,26,0.72);
border-bottom: 1px solid var(--line);
}
.site-header .wrap { display: flex; align-items: center; justify-content: space-between; height: 62px; }
.brand { display: flex; align-items: center; gap: 10px; font-weight: 700; letter-spacing: 0.2px; color: var(--text); }
.brand .mark {
display: grid; place-items: center; width: 30px; height: 30px; border-radius: 8px;
background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: #0c0e13; font-weight: 800;
}
.nav a { color: var(--muted); margin-left: 22px; font-weight: 500; }
.nav a:hover { color: var(--text); text-decoration: none; }
/* Hero */
.hero { padding: 84px 0 40px; }
.eyebrow { color: var(--accent-2); font-family: var(--mono); font-size: 0.82rem; letter-spacing: 2px; text-transform: uppercase; }
.hero h1 { font-size: clamp(2.1rem, 5vw, 3.4rem); line-height: 1.08; margin: 12px 0 14px; }
.hero p.lead { font-size: 1.18rem; color: var(--muted); max-width: 640px; }
.hero .accent { color: var(--accent); }
/* Buttons */
.btn {
display: inline-block; padding: 11px 18px; border-radius: 10px; font-weight: 600;
border: 1px solid var(--line); color: var(--text); background: var(--panel);
}
.btn:hover { text-decoration: none; border-color: var(--accent); }
.btn.primary { background: linear-gradient(135deg, var(--accent), #e8925a); color: #14100a; border: none; }
.btn-row { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 26px; }
/* Sections */
section { padding: 46px 0; }
h2 { font-size: 1.7rem; margin: 0 0 6px; }
.section-sub { color: var(--muted); margin: 0 0 26px; }
/* Cards / grid */
.grid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
.card {
background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
padding: 20px; transition: border-color .15s, transform .15s;
}
.card:hover { border-color: var(--accent); transform: translateY(-2px); }
.card h3 { margin: 0 0 6px; font-size: 1.12rem; }
.card p { color: var(--muted); margin: 0; }
.card .tag { display: inline-block; font-family: var(--mono); font-size: 0.72rem; color: var(--accent-2);
border: 1px solid var(--line); border-radius: 999px; padding: 2px 10px; margin-bottom: 10px; }
.card.soon { opacity: 0.72; }
/* Featured */
.featured {
display: grid; grid-template-columns: 1.2fr 1fr; gap: 26px; align-items: center;
background: linear-gradient(180deg, var(--ink-2), var(--panel));
border: 1px solid var(--line); border-radius: 18px; padding: 28px;
}
.featured h2 { font-size: 1.9rem; }
.featured .kicker { color: var(--accent); font-family: var(--mono); font-size: .8rem; letter-spacing: 1.5px; text-transform: uppercase; }
.featured ul { margin: 14px 0 0; padding-left: 18px; color: var(--muted); }
.diagram {
font-family: var(--mono); font-size: 0.8rem; color: var(--muted);
background: #0c0f16; border: 1px solid var(--line); border-radius: 12px; padding: 16px; white-space: pre; overflow-x: auto;
}
@media (max-width: 780px) { .featured { grid-template-columns: 1fr; } }
/* Provider picker (openscribe page) */
.picker { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 16px; }
.picker button {
font-family: var(--sans); font-size: .92rem; cursor: pointer;
background: var(--panel); color: var(--text); border: 1px solid var(--line);
border-radius: 999px; padding: 8px 15px;
}
.picker button[aria-pressed="true"] { border-color: var(--accent); background: rgba(240,163,94,0.14); color: var(--text); }
pre.code {
font-family: var(--mono); font-size: 0.86rem; color: #d7e2f0;
background: #0c0f16; border: 1px solid var(--line); border-radius: 12px; padding: 16px; overflow-x: auto;
}
pre.code .c { color: var(--muted); }
pre.code .k { color: var(--accent-2); }
/* Feature list */
.features { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(220px,1fr)); }
.feature { border-left: 2px solid var(--accent); padding-left: 14px; }
.feature h4 { margin: 0 0 3px; }
.feature p { margin: 0; color: var(--muted); font-size: 0.96rem; }
/* Footer */
.site-footer { border-top: 1px solid var(--line); padding: 30px 0; color: var(--muted); }
.site-footer .wrap { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px; }
.pill { font-family: var(--mono); font-size: .75rem; color: var(--accent-2); }

115
index.html Normal file
View file

@ -0,0 +1,115 @@
<!doctype html>
<!-- SPDX-License-Identifier: MIT -->
<html lang="en-GB">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>12 Hobbies Studio - open, self-hosted maker projects</title>
<meta name="description" content="12 Hobbies Studio builds open-source, self-hosted maker projects. Flagship: OpenScribe, an AI voice recorder you can connect to any AI." />
<link rel="stylesheet" href="assets/styles.css" />
</head>
<body>
<header class="site-header">
<div class="wrap">
<a class="brand" href="index.html"><span class="mark">12</span> Hobbies Studio</a>
<nav class="nav">
<a href="#projects">Projects</a>
<a href="#about">About</a>
<a href="projects/openscribe.html">OpenScribe</a>
</nav>
</div>
</header>
<main>
<section class="hero">
<div class="wrap">
<div class="eyebrow">A maker studio</div>
<h1>Twelve hobbies, one habit:<br /><span class="accent">build it open, run it yourself.</span></h1>
<p class="lead">12 Hobbies Studio makes small, open-source, self-hostable projects across
hardware, software and everything in between. No lock-in, no subscriptions you cannot leave,
no data you do not own.</p>
<div class="btn-row">
<a class="btn primary" href="projects/openscribe.html">Meet OpenScribe</a>
<a class="btn" href="#projects">Browse projects</a>
</div>
</div>
</section>
<section>
<div class="wrap">
<div class="featured">
<div>
<div class="kicker">Flagship project</div>
<h2>OpenScribe</h2>
<p class="section-sub">An open-source, self-hosted AI voice recorder in the spirit of a
Plaud device - but you bring the AI. Record on a small ESP32-S3, sync over BLE or WiFi,
transcribe and summarise with <strong>any AI you choose</strong>: open-standard, local,
or a commercial API.</p>
<ul>
<li>Open hardware, open firmware, open API - build it and own it.</li>
<li>Connect to OpenAI, Anthropic, Groq, a local model, or your own server.</li>
<li>Self-hosted transcription and summaries. No mandatory cloud.</li>
</ul>
<div class="btn-row">
<a class="btn primary" href="projects/openscribe.html">Explore OpenScribe</a>
<a class="btn" href="https://git.discworld.casa/laurence/openscribe">Source</a>
</div>
</div>
<div class="diagram">mic
-&gt; ESP32-S3 (record to SD)
| BLE / WiFi sync
v
self-hosted server
transcribe -&gt; summarise
| (any AI provider)
v
phone app + open API</div>
</div>
</div>
</section>
<section id="projects">
<div class="wrap">
<h2>Projects</h2>
<p class="section-sub">One shipped, more in the workshop. Everything here is open source.</p>
<div class="grid">
<a class="card" href="projects/openscribe.html">
<span class="tag">hardware + AI</span>
<h3>OpenScribe</h3>
<p>Self-hosted AI voice recorder. Bring your own AI, open or commercial.</p>
</a>
<div class="card soon">
<span class="tag">in the workshop</span>
<h3>More soon</h3>
<p>New hobbies, new builds. This grid grows as projects ship.</p>
</div>
<div class="card soon">
<span class="tag">idea</span>
<h3>Your idea?</h3>
<p>Open a discussion on the forge. The studio likes a good excuse to build.</p>
</div>
</div>
</div>
</section>
<section id="about">
<div class="wrap">
<h2>About the studio</h2>
<p class="section-sub" style="max-width:680px">
12 Hobbies Studio is a home for maker projects that are worth doing properly and worth
sharing. The through-line: build in the open, keep the data with the person who made it,
and let anyone reproduce the whole thing from the repo alone. Firmware, servers and apps
are copyleft; hardware is open-hardware licensed; docs are share-alike.
</p>
</div>
</section>
</main>
<footer class="site-footer">
<div class="wrap">
<div>&copy; 12 Hobbies Studio. Built open. <span class="pill">self-hosted</span></div>
<div><a href="https://git.discworld.casa/laurence/openscribe">OpenScribe source</a></div>
</div>
</footer>
</body>
</html>

103
projects/openscribe.html Normal file
View file

@ -0,0 +1,103 @@
<!doctype html>
<!-- SPDX-License-Identifier: MIT -->
<html lang="en-GB">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenScribe - open, self-hosted AI voice recorder | 12 Hobbies Studio</title>
<meta name="description" content="OpenScribe is an open-source, self-hosted AI voice recorder. Record on an ESP32-S3, sync over BLE or WiFi, and transcribe and summarise with any AI you choose - open or commercial." />
<link rel="stylesheet" href="../assets/styles.css" />
</head>
<body>
<header class="site-header">
<div class="wrap">
<a class="brand" href="../index.html"><span class="mark">12</span> Hobbies Studio</a>
<nav class="nav">
<a href="../index.html#projects">Projects</a>
<a href="#any-ai">Any AI</a>
<a href="https://git.discworld.casa/laurence/openscribe">Source</a>
</nav>
</div>
</header>
<main>
<section class="hero">
<div class="wrap">
<div class="eyebrow">Project - hardware + AI</div>
<h1>OpenScribe</h1>
<p class="lead">An open-source, self-hosted AI voice recorder in the spirit of a Plaud device.
Record on a small ESP32-S3, sync to your phone and your own server, and turn recordings into
transcripts and summaries with <span class="accent">any AI you choose</span>.</p>
<div class="btn-row">
<a class="btn primary" href="https://git.discworld.casa/laurence/openscribe">Get the source</a>
<a class="btn" href="#build">How to build</a>
</div>
</div>
</section>
<section id="any-ai">
<div class="wrap">
<h2>Bring your own AI</h2>
<p class="section-sub">This is the difference. A commercial recorder locks you to its cloud.
OpenScribe lets you point transcription and summaries at whatever you want - a local model,
an open-standard endpoint, or a commercial API. Pick one and drop it in your server config:</p>
<div id="provider-picker" class="picker" aria-label="Choose an AI provider"></div>
<pre class="code"><code id="env-out"></code></pre>
<p class="section-sub">Transcription is just as flexible: local <code>faster-whisper</code>, or
any OpenAI-compatible audio endpoint (OpenAI, Groq, or your own whisper server).</p>
</div>
</section>
<section>
<div class="wrap">
<h2>What it does</h2>
<p class="section-sub">The expected recorder features, without the lock-in.</p>
<div class="features">
<div class="feature"><h4>One-button recording</h4><p>Capture to microSD on a wearable ESP32-S3. No length limit.</p></div>
<div class="feature"><h4>Three ways to sync</h4><p>BLE for control, WiFi to the app, and independent WiFi upload when on charge.</p></div>
<div class="feature"><h4>Transcribe + summarise</h4><p>Server-side, with the AI provider you chose. Overview, key points, action items.</p></div>
<div class="feature"><h4>Completely open API</h4><p>List, download and export recordings (audio, TXT, SRT, VTT, Markdown, JSON).</p></div>
<div class="feature"><h4>Self-hosted storage</h4><p>Upload to your own S3-compatible store, WebDAV or NAS. Your data stays yours.</p></div>
<div class="feature"><h4>Mobile app</h4><p>Flutter app for Android and iOS: provisioning, library, playback, summaries.</p></div>
</div>
</div>
</section>
<section id="build">
<div class="wrap">
<h2>Build one</h2>
<p class="section-sub">Off-the-shelf parts, a printed case, and a server you run.</p>
<div class="grid">
<div class="card"><span class="tag">hardware</span><h3>1. Get the parts</h3><p>ESP32-S3 with PSRAM, an I2S MEMS mic, a microSD card, a LiPo and a charge IC. Full BOM in the repo.</p></div>
<div class="card"><span class="tag">firmware</span><h3>2. Flash it</h3><p>PlatformIO builds two board profiles. Record to WAV, then sync over WiFi and the on-device REST API.</p></div>
<div class="card"><span class="tag">case</span><h3>3. Print the case</h3><p>Parametric OpenSCAD enclosure you can re-tune to your exact board and battery.</p></div>
<div class="card"><span class="tag">server</span><h3>4. Run the server</h3><p>FastAPI ingest + transcription + summaries. Point it at your chosen AI and storage.</p></div>
</div>
<div class="btn-row">
<a class="btn primary" href="https://git.discworld.casa/laurence/openscribe">Open the repository</a>
</div>
</div>
</section>
<section>
<div class="wrap">
<h2>Open all the way down</h2>
<p class="section-sub" style="max-width:680px">
Firmware, server and app are GPL-3.0. The hardware design is CERN-OHL-S. The case and docs
are CC-BY-SA. Anyone can build it, improve it, and keep the result open. OpenScribe is an
independent project inspired by the Plaud class of device; it is not affiliated with Plaud.
</p>
</div>
</section>
</main>
<footer class="site-footer">
<div class="wrap">
<div>&copy; 12 Hobbies Studio. Built open. <span class="pill">bring your own AI</span></div>
<div><a href="../index.html">Back to the studio</a></div>
</div>
</footer>
<script src="../assets/app.js"></script>
</body>
</html>