Migrate from Shiny to glinty - #1
Conversation
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.
|
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 This branch is built against the old shape, and leans hard on raw The work is preserved:
Worth carrying forward when this is redone:
|
Drops
shinyandbslibfor glinty 0.4.0.1, which pulls onlyjsonliteanddigest. 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 plaintextvalue=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 becameconditional_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 togglesdisplay, so the parameter sliders keep their values when you switch backend. Building the same thing onrender_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$backend→input$backend()throughout. Inputs are NULL until set, and anything built insiderender_ui()stays NULL until touched, so optional reads go through newopt_str()/opt_num()helpers instead of barenzchar()andis.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 acrosstts/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-linerenderUIblocks. One function now, parameterised by service name and button ids.Also: the five hardcoded
~/.cornfab/voicespaths collapse intovoices_dir(), which shrinks the pending~/.cornballmigration in CLAUDE.md from "5+ refs" to one line.Icons
shiny::icon()is gone.R/icons.Ris 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):
buildTagNodeuseddocument.createElement(), which puts SVG in the HTML namespace as anHTMLUnknownElementthat 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.1floor.Smaller things
The main textarea and voice-description box were raw
tags$textarearelying on Shiny's auto-binding; they'retextarea_input()now. History rows carry their own id on a click bind rather than inlineonclickstrings.get_vram_usage()suppresses warnings — no GPU is the normal case andnvidia-smiwarns rather than errors, which was leaking into the console every 5 seconds.Verification
All ok, 67 results.
inst/tinytest/test_server_wiring.Rrunsapp_server()against a headless glinty session: the server starts and seeds its outputs,char_countreacts to the textarea, the download registers and names files by the format select, every icon resolves and survives the wire format, andgeneration_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.