Skip to content

Migrate from Shiny to glinty - #1

Closed
TroyHernandez wants to merge 2 commits into
mainfrom
migrate-glinty
Closed

Migrate from Shiny to glinty#1
TroyHernandez wants to merge 2 commits into
mainfrom
migrate-glinty

Conversation

@TroyHernandez

Copy link
Copy Markdown
Contributor

Drops shiny and bslib for glinty 0.4.0.1, which pulls only jsonlite and digest. Same features, same layout, no Bootstrap. Companion to earshot#3.

🔴 Rotate both API keys

passwordInput("openai_key", value = Sys.getenv("OPENAI_API_KEY", "")) and the ElevenLabs equivalent rendered both keys into the page source as plaintext value= attributes. type="password" only masks on screen; View Source showed them, and the app binds all interfaces.

This was true of the Shiny version, so it has been live on main. Treat both keys as compromised regardless of this PR.

Neither field is prefilled now. configure_backend() already reads both environment variables, so the inputs exist only to override them — empty means "use the environment", and the placeholder says which variable is in play without echoing it. A test asserts neither key appears in the rendered page.

conditional_panel, and why it isn't render_ui

All 11 conditionalPanels became conditional_panel() with condition objects:

conditional_panel(
    condition = cond_not(cond_and(
        input_is("backend", "qwen3"),
        input_is("use_voice_design", TRUE)
    )),
    ui_output("voice_select")
)

No JavaScript expression string, no eval.

This matters beyond tidiness. conditional_panel() only toggles display, so the parameter sliders keep their values when you switch backend. Building the same thing on render_ui() would rebuild the subtree and silently reset Speed, Exaggeration and CFG Weight on every backend change — a bug that would have been maddening to track down.

Server

input$backendinput$backend() throughout. Inputs are NULL until set, and anything built inside render_ui() stays NULL until touched, so optional reads go through new opt_str() / opt_num() helpers instead of bare nzchar() and is.na(), both of which throw on NULL.

Two things got restructured rather than transliterated:

  • generate_speech(). The generate handler was a 150-line inline block doing parameter assembly, four-way dispatch across tts/speech_clone/speech_design, and history bookkeeping. It's a function now that returns the file and the info record, so the observer reads as the sequence of steps it is.
  • container_panel(). The qwen3 and chatterbox container widgets were near-identical 30-line renderUI blocks. One function now, parameterised by service name and button ids.

Also: the five hardcoded ~/.cornfab/voices paths collapse into voices_dir(), which shrinks the pending ~/.cornball migration in CLAUDE.md from "5+ refs" to one line.

Icons

shiny::icon() is gone. R/icons.R is a local seven-shape inline SVG set — a framework has no business carrying FontAwesome's attribution for eight glyphs, and glinty deliberately ships none.

This turned up a real glinty bug (glinty#19, merged): buildTagNode used document.createElement(), which puts SVG in the HTML namespace as an HTMLUnknownElement that parses fine and never renders. Icons in static UI worked; the identical icons on the dynamically-rendered container buttons would have silently vanished. Hence the >= 0.4.0.1 floor.

Smaller things

The main textarea and voice-description box were raw tags$textarea relying on Shiny's auto-binding; they're textarea_input() now. History rows carry their own id on a click bind rather than inline onclick strings. get_vram_usage() suppresses warnings — no GPU is the normal case and nvidia-smi warns rather than errors, which was leaking into the console every 5 seconds.

Verification

tinypkgr::install(); tinytest::test_package("cornfab")

All ok, 67 results. inst/tinytest/test_server_wiring.R runs app_server() against a headless glinty session: the server starts and seeds its outputs, char_count reacts to the textarea, the download registers and names files by the format select, every icon resolves and survives the wire format, and generation_details() renders per-backend without leaking one backend's parameters into another's report.

Served live and checked over HTTP: page renders, assets serve, both key fields are empty.

Not yet verified — needs a browser. Tab switching, all 11 conditional panels toggling, the two modals, icon rendering, the download, and any actual generation. This wants a click-through before it merges.

Drops shiny and bslib for glinty, which pulls only jsonlite and
digest. Same features, same layout, no Bootstrap.

UI: navset_underline becomes tabset(); the layout was already plain
divs over the app's own CSS, so it converted directly. All 11
conditionalPanels became conditional_panel() with condition objects
(input_is, cond_and, cond_not) instead of JavaScript expression
strings handed to eval.

That last one matters beyond tidiness. conditional_panel() only
toggles display, so the parameter sliders keep their values across a
backend switch. Doing it with render_ui() would have rebuilt the
subtree and silently reset Speed, Exaggeration and CFG Weight every
time the user changed backend.

Server: inputs are called now, and inputs created inside render_ui()
stay NULL until touched, so optional reads go through new opt_str()
and opt_num() helpers rather than bare nzchar()/is.na(), which throw
on NULL. The generate handler was a 150-line inline block; it is now
generate_speech(), which returns the file and the info record, so the
observer reads as the sequence of steps it actually is. The five
hardcoded ~/.cornfab/voices paths collapse into voices_dir(), which
also shrinks the pending ~/.cornball migration to one line.

The two container panels were near-identical 30-line renderUI blocks;
they share container_panel() now, differing only in service name and
button ids.

shiny::icon() is replaced by R/icons.R, a local seven-shape inline SVG
set. glinty deliberately ships no icons, and a framework has no
business carrying FontAwesome's attribution for eight glyphs.

The main textarea and the voice-description box were raw tags$textarea
relying on Shiny's auto-binding; they are textarea_input() now.
History rows carry their own id on a click bind instead of inline
onclick strings.

get_vram_usage() suppresses warnings: no GPU is the normal case, and
nvidia-smi warns rather than erroring, which was leaking into the
console on every 5-second refresh.

SECURITY: the OpenAI and ElevenLabs key fields are no longer prefilled
from the environment. A value= attribute is rendered into page source
in plain text -- type="password" only masks on screen -- and glinty
listens on all interfaces, so both keys were readable by anyone who
could reach the port. They were equally exposed in the Shiny version.
configure_backend() already reads both env vars; the fields now only
override them, and a test asserts neither key appears in the page.

Dockerfile moves off rocker/shiny to rocker/r-ver. run_app() loses its
host argument, since glinty always binds all interfaces.
0.4.0.1 was the floor for the SVG namespace fix the icons need.
0.4.1 adds the secret guard: password_input() drops its value
argument and run_app() refuses to serve a page containing an
environment secret. Both are what keep the OpenAI and ElevenLabs keys
out of the page source.
@TroyHernandez

Copy link
Copy Markdown
Contributor Author

Parking this rather than merging.

The migration is complete and tested — it isn't being closed because anything is wrong with it. It's being closed because glinty is about to change underneath it.

Why: glinty's wire protocol is DOM-shaped. Update messages say things like "set textContent on this element", and the initial UI never crosses the wire at all — the browser gets server-rendered HTML. That's fine for one frontend and wrong for three. glinty is moving to a semantic protocol (v3) so a Flutter/Dart client can be a peer implementation rather than a DOM emulator.

This branch is built against the old shape, and leans hard on raw tag() for <details>/<summary> panels, <small>, and the audio player — all of which become browser-only under v3. Merging it would mean migrating twice.

The work is preserved:

  • Branch migrate-glinty, not deleted
  • Tag parked/glinty-migration-v1
  • Documented in the cornelius vault

main stays on Shiny and keeps working.

Worth carrying forward when this is redone:

  • Both the OpenAI and ElevenLabs keys were rendered into page source as plaintext value= attributes, and still are on main. Exposure required someone on the local network, so nothing is known to have leaked, but the pattern should not come back. glinty 0.4.1 now refuses to serve a page containing an environment secret.
  • conditional_panel() must toggle display rather than rebuild, or the parameter sliders reset on every backend switch. That distinction is worth preserving in whatever v3 looks like.
  • generate_speech() and container_panel() extracted a 150-line inline block and two near-identical 30-line renderUI blocks. Both are worth keeping.
  • The five hardcoded ~/.cornfab/voices paths collapsed into voices_dir(), which shrinks the pending ~/.cornball migration to one line.
  • Icons live in R/icons.R as inline SVG. This is what surfaced the glinty SVG-namespace bug: createElement() puts SVG in the HTML namespace, where it parses and never renders.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant