diff --git a/DESIGN.md b/DESIGN.md index bb92604..58156f7 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -46,7 +46,7 @@ stage presence, and the streamer is the director. The visuals moved with the met | 👻 | app identity / a ghost | | 🧠 | brain / model / API | | 📷 | stream & OBS | - | 🎙 | voice / mic transcription | + | 🎙️ | voice / mic transcription | | 💬 | chat behavior & cadence | | 🎨 | look / theming | | 🔧 | app plumbing | diff --git a/README.md b/README.md index 13b4ea1..89d8437 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ Quit Hype Ghost. | `app.autoUpdate` | Check GitHub for new versions at launch and update automatically (default true). | | `app.setupComplete` | Set by the setup wizard when you finish it (even via the brain step's "skip"). Once true, `/` opens the dashboard — without a brain that's **preview mode**: everything explorable, cast quiet, $0. | | `app.costMeter` | Show the live session cost readout in the deck's top bar (default true). | +| `app.fontScale` | UI text size for the deck/settings/wizard: `1` default, `1.1` large, `1.25` extra large (Settings → App, applies live). Never shrinks below default, and the on-stream overlay is unaffected. | | `app.ttsOutputDevice` | Which speaker/headphone spoken cast messages play on (Settings → App → 🔊, with a test button). Picking a specific device keeps chat out of your stream mix and off the VOD; that path speaks with Windows (SAPI) voices. Blank = system default device with browser voices. | | `bot.language` | Language the whole cast chats in (default English). | | `stream.context` | Standing context about your stream (game, format, vibe) the cast always knows. It also sees your current OBS scene name automatically. | diff --git a/config.example.json b/config.example.json index bb2f76b..e187635 100644 --- a/config.example.json +++ b/config.example.json @@ -42,6 +42,7 @@ "ttsRate": 1, "ttsOutputDevice": "", "uiLanguage": "en", + "fontScale": 1, "setupComplete": false }, "stream": { diff --git a/public/dashboard.html b/public/dashboard.html index 8485c31..3a9d100 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -440,7 +440,7 @@ function setOrb(id, cls, text) { const el = $(id); el.className = 'orb ' + cls; el.querySelector('.lbl').textContent = text; } - let appliedLang = null, appliedAccent = null; + let appliedLang = null, appliedAccent = null, appliedScale = null; function renderState() { // Language + accent apply only when they actually change — applyI18n // sweeps the whole DOM and state broadcasts arrive constantly. @@ -450,6 +450,10 @@ applyAccent(state.accent); applyI18n(); } + if (state.fontScale !== appliedScale) { + appliedScale = state.fontScale; + document.documentElement.style.setProperty('--font-scale', Math.min(1.5, Math.max(1, Number(state.fontScale) || 1))); + } if (JSON.stringify(cast) !== JSON.stringify(state.cast || [])) { cast = state.cast || []; renderStage(); } const si = state.streamInfo, np = $('nowplaying'); diff --git a/public/settings.html b/public/settings.html index be798f8..425951d 100644 --- a/public/settings.html +++ b/public/settings.html @@ -69,7 +69,7 @@

⚙ Settings

- + @@ -189,7 +189,7 @@

🎮 Stream info for the cast

-

🎙 Voice awareness

+

🎙️ Voice awareness

Local mic transcription via the LocalVocal OBS plugin, so answering out loud is replying. It's third-party — keep it updated to match your OBS version (an outdated build can crash OBS, e.g. when switching Scene Collections). Optional: leave this Off to reply by typing.

+ + + + +

Scales the text across the deck, settings, and wizard (preview is instant; Save keeps it). The on-stream overlay is untouched — its look lives on the Stream tab.

@@ -538,6 +545,7 @@

Danger zone

$('aboutVersion').textContent = 'v' + (data.version || '?'); $('aboutConfigPath').textContent = data.configPath || ''; initStorage(); + applyFontScale(cfg.app && cfg.app.fontScale); if (apiKeySaved) $('apiKeyField').placeholder = 'saved ✓ — leave blank to keep the current key'; const saved = 'saved ✓ — leave blank to keep'; if (data.openaiKeySaved) fields.find((f) => f.dataset.path === 'brain.openaiApiKey').placeholder = saved; @@ -668,6 +676,12 @@

Danger zone

$('browseFile').onclick = () => pickInto('transcript.file', 'transcript.mode'); $('browseFile2').onclick = () => pickInto('transcript2.file', 'transcript2.mode'); + // ---- font scale (instant preview; Save persists + hot-applies) ---- + function applyFontScale(v) { + document.documentElement.style.setProperty('--font-scale', Math.min(1.5, Math.max(1, Number(v) || 1))); + } + $('fontScale').onchange = () => applyFontScale($('fontScale').value); + // ---- storage (data folder) ---- // Desktop only: the server fronts Electron's folder dialog + file migration, // then relaunches. Headless/dev just shows where the data lives. @@ -719,6 +733,7 @@

Danger zone

for (const f of fields) { let v; if (f.dataset.type === 'bool') v = f.checked; + else if (f.dataset.type === 'num') v = Number(f.value) || 0; // selects with numeric values else if (f.type === 'number') { if (f.value === '') continue; v = Number(f.value); } else v = f.value.trim ? f.value.trim() : f.value; setPath(cfg, f.dataset.path, v); diff --git a/public/setup.html b/public/setup.html index df7cb68..0b40e83 100644 --- a/public/setup.html +++ b/public/setup.html @@ -123,7 +123,7 @@

Let's build your room

Optional extras
- 🎙 Voice awareness — the cast hears what you say (recommended) + 🎙️ Voice awareness — the cast hears what you say (recommended)

Install the free LocalVocal OBS plugin (speech-to-text runs 100% locally), add its filter to your mic, and point it at a file or text source. Keep LocalVocal updated to match your OBS version — an outdated build can crash OBS on newer OBS releases (it's third-party, not part of Hype Ghost). Voice awareness is optional; leave this Off to reply by typing.

@@ -170,6 +170,7 @@

Your room is ready!

fetch('/api/config').then((r) => r.json()).then((data) => { cfg = data.config; apiKeySaved = data.apiKeySaved; canRestart = data.canRestart; + document.documentElement.style.setProperty('--font-scale', Math.min(1.5, Math.max(1, Number(cfg.app?.fontScale) || 1))); archetypes = data.archetypes || []; palette = data.palette || {}; $('model').innerHTML = data.models.map((m) => ``).join(''); // archetype cards diff --git a/public/theme.css b/public/theme.css index c1306c6..4a58105 100644 --- a/public/theme.css +++ b/public/theme.css @@ -48,12 +48,16 @@ --r-full: 999px; /* --- type scale --- */ - --fs-xs: 11px; - --fs-sm: 13px; - --fs-base: 15px; - --fs-lg: 18px; - --fs-xl: 22px; - --fs-2xl: 30px; + /* Every size scales with the font-size setting (Settings → App): pages set + --font-scale from app.fontScale (1 / 1.1 / 1.25). The overlay never sets + it — the on-stream look (and the watermark) stays as designed. */ + --font-scale: 1; + --fs-xs: calc(12px * var(--font-scale)); + --fs-sm: calc(14px * var(--font-scale)); + --fs-base: calc(15px * var(--font-scale)); + --fs-lg: calc(18px * var(--font-scale)); + --fs-xl: calc(22px * var(--font-scale)); + --fs-2xl: calc(30px * var(--font-scale)); /* --- depth --- */ --shadow: 0 18px 44px -20px rgba(0, 0, 0, 0.75); @@ -192,7 +196,7 @@ input:focus:not(:focus-visible), textarea:focus:not(:focus-visible), select:focu /* the AI label — the ethics promise made visible; identical on every surface */ .badge-ai { - font-size: 10px; + font-size: calc(10px * var(--font-scale)); /* grows with the UI; --font-scale never goes below 1 */ font-weight: 800; letter-spacing: .6px; padding: 2px 7px; @@ -218,7 +222,7 @@ input:focus:not(:focus-visible), textarea:focus:not(:focus-visible), select:focu /* ---------- layout helpers ---------- */ .row { display: flex; gap: 14px; } .row > div { flex: 1; } -.check { display: flex; align-items: flex-start; gap: 10px; margin-top: 14px; font-size: 14px; } +.check { display: flex; align-items: flex-start; gap: 10px; margin-top: 14px; font-size: var(--fs-sm); } .check input { width: auto; margin-top: 3px; flex: none; } .check label { margin: 0; color: var(--text); font-weight: 450; } .withbtn { display: flex; gap: 8px; } diff --git a/src/server.js b/src/server.js index e1985ce..5d8c9e7 100644 --- a/src/server.js +++ b/src/server.js @@ -78,6 +78,9 @@ export function startServer(opts = {}) { outputDevice: config.app.ttsOutputDevice || '', engine: sapiTts.available() ? 'sapi' : 'browser', }); + // UI font scale (Settings → App). Never below 1: text can grow, not shrink — + // and the 🤖 AI badge inherits the same guarantee. + const fontScale = () => Math.min(1.5, Math.max(1, Number(config.app.fontScale) || 1)); const brain = new Brain({ brain: config.brain, anthropic: config.anthropic, @@ -114,6 +117,7 @@ export function startServer(opts = {}) { costMeter: config.app.costMeter !== false, tts: ttsState(), uiLanguage: config.app.uiLanguage || 'en', + fontScale: fontScale(), usage: { messages: 0, inputTokens: 0, outputTokens: 0, cost: 0, costKnown: true }, }; const history = []; // {id, author, role: 'bot'|'streamer', text, ts} @@ -229,7 +233,7 @@ export function startServer(opts = {}) { // transcripts, Twitch, port) still restarts. const HOT_PATHS = [ 'energy', 'talkingPoints', 'theme.', 'overlay.', 'moments.', 'memory.', 'stream.', - 'app.costMeter', 'app.uiLanguage', 'app.autoPause', 'app.autoPauseMinutes', 'app.autoUpdate', + 'app.costMeter', 'app.uiLanguage', 'app.fontScale', 'app.autoPause', 'app.autoPauseMinutes', 'app.autoUpdate', 'app.ttsVoice', 'app.ttsRate', 'app.ttsOutputDevice', 'cadence.soloSeconds', 'cadence.quietSeconds', 'cadence.jitter', 'cadence.burstChance', 'cadence.lullChance', 'cadence.replyDelaySeconds', 'cadence.minVoiceReplyGapSeconds', @@ -272,6 +276,7 @@ export function startServer(opts = {}) { state.accent = config.theme?.accent || 'violet'; state.costMeter = config.app.costMeter !== false; state.uiLanguage = config.app.uiLanguage || 'en'; + state.fontScale = fontScale(); state.tts = ttsState(); // voice/rate/output device are hot; the on/off toggle is not broadcast({ type: 'config' }); // overlay clients re-fetch /api/overlay-config broadcastState();