Public demo: gormigskansk.jardenberg.se
This repo contains the standalone browser app behind Gormigskånsk: a small iPhone-friendly demo that takes spoken input in any language, rewrites it into a more Skånska-flavoured Swedish version, and returns audio in a Skånska target voice.
The repo name stayed dialektlab, but the public-facing product and branding are now Gormigskånsk.
This project is intentionally separate from 2026GPT.
- records speech in the browser
- also accepts direct text input in the transcript section
- transcribes the audio with OpenAI
- rewrites the utterance into a Skånska-oriented Swedish version while trying to preserve meaning and length
- renders the result with ElevenLabs using the configured target voice
- plays the audio back in the browser
- tap once to start recording
- tap again to stop
- or skip the microphone and type text directly in the transcript section
- first interaction may trigger the browser microphone permission dialog
- recording is placed first in the UI so people can start immediately
- processing feedback is shown with staged status text and a progress bar
- source speech can be auto-detected instead of being locked to Swedish
- localized landing pages are available at
/,/en,/es,/pl, and/fr
https://gormigskansk.jardenberg.se/for Swedishhttps://gormigskansk.jardenberg.se/enfor Englishhttps://gormigskansk.jardenberg.se/esfor Spanishhttps://gormigskansk.jardenberg.se/plfor Polishhttps://gormigskansk.jardenberg.se/frfor French
The chrome and metadata are localized per route. The voice pipeline stays the same: any supported spoken input is turned into Swedish with a Skånska flavour on output.
- Node + Express
- plain browser frontend in
public/ - OpenAI for transcription and dialect rewrite
- ElevenLabs for the default TTS path
- Cloudflare for the public custom domain, DNS, TLS, and edge delivery in front of the app
- Railway for deployment
Current defaults bias for latency while keeping decent rewrite quality:
DIALECTLAB_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe
DIALECTLAB_TEXT_MODEL=gpt-5.4-mini
ELEVENLABS_MODEL_ID=eleven_turbo_v2_5
DIALECTLAB_SPEECH_SPEED=0.95Notes:
gpt-4o-mini-transcribehandles speech-to-textgpt-5.4-miniis the current default rewrite modeleleven_turbo_v2_5is now the default TTS model for the current quality/speed balance- if you want to compare lower latency, switch
ELEVENLABS_MODEL_IDtoeleven_flash_v2_5 - if you prefer slightly higher TTS fidelity and can tolerate more delay, switch
ELEVENLABS_MODEL_IDtoeleven_multilingual_v2
- Copy
.env.exampleto.env - Set at least:
OPENAI_API_KEY=...
ELEVENLABS_API_KEY=...
ELEVENLABS_VOICE_ID=...
ELEVENLABS_JJ_VOICE_ID=...- Install dependencies:
npm install- Start the app:
npm run devCommonly useful variables:
OPENAI_API_KEY=...
DIALECTLAB_OPENAI_API_KEY=
ELEVENLABS_API_KEY=...
ELEVENLABS_VOICE_ID=...
ELEVENLABS_JJ_VOICE_ID=...
ELEVENLABS_MODEL_ID=eleven_turbo_v2_5
DIALECTLAB_AUDIO_BACKEND=elevenlabs_tts
DIALECTLAB_INPUT_LANGUAGE=
DIALECTLAB_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe
DIALECTLAB_TEXT_MODEL=gpt-5.4-mini
DIALECTLAB_SPEECH_SPEED=0.95
DIALECTLAB_TARGET_DIALECT=Skanska
DIALECTLAB_IP_MAX=12Notes:
DIALECTLAB_OPENAI_API_KEYis optional and only overridesOPENAI_API_KEYDIALECTLAB_AUDIO_BACKEND=elevenlabs_ttsis optional but makes the intent explicit- leave
DIALECTLAB_INPUT_LANGUAGEempty for auto-detection; only set it if you want to force one source language HOSTandPORTdo not need to be set on RailwayDIALECTLAB_TTS_MODELandDIALECTLAB_TTS_VOICEonly matter on the OpenAI TTS fallback path
Two audio rendering paths exist:
openai_ttselevenlabs_tts
Selection rules:
- if
DIALECTLAB_AUDIO_BACKENDis set, that wins - otherwise, if
ELEVENLABS_API_KEYexists, the app useselevenlabs_tts - otherwise, it falls back to
openai_tts
Microphone access in mobile Safari generally requires a secure context. That means:
http://localhostworks on the same device- a deployed HTTPS URL works
- a tunnel also works
Plain local-network http://192.168.x.x:8787 may not allow microphone capture on iPhone.
The app is already shaped correctly for Railway:
- binds to
0.0.0.0 - respects Railway
PORT - exposes
GET /health
Recommended Railway variables:
OPENAI_API_KEY=...
ELEVENLABS_API_KEY=...
ELEVENLABS_VOICE_ID=CuaAIFbkzX2kaNH5EtHZ
ELEVENLABS_JJ_VOICE_ID=Z2ic84BAXoRvhU0mvJIa
ELEVENLABS_MODEL_ID=eleven_turbo_v2_5
DIALECTLAB_AUDIO_BACKEND=elevenlabs_tts
DIALECTLAB_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe
DIALECTLAB_TEXT_MODEL=gpt-5.4-mini
DIALECTLAB_SPEECH_SPEED=0.95
DIALECTLAB_TARGET_DIALECT=Skanska
DIALECTLAB_IP_MAX=12The public deployment is not just Railway. In production, gormigskansk.jardenberg.se is exposed through Cloudflare in front of the Railway service. In practice that means:
- Cloudflare handles the public custom domain, DNS, TLS termination, and edge-facing delivery
- Railway runs the Node app itself
- the app still controls its own HTML freshness with
Cache-Control: no-storeon locale-rendered pages
- favicon:
public/favicon.svg - social preview source:
public/og-card.svg - social preview PNG:
public/og-card.png
Page metadata, canonical URLs, and hreflang tags are rendered from lib/locales.js and lib/renderPage.js.
When ElevenLabs is active, the demo currently exposes two configured voice buttons:
JJ-Skånska(default)ElevenLabs-Skånska
The default uses ELEVENLABS_JJ_VOICE_ID. The secondary voice uses ELEVENLABS_VOICE_ID.
server.js: Express server and API routeslib/locales.js: locale bundles, route mapping, metadatalib/renderPage.js: server-rendered HTML shell per localelib/openaiDialectDemo.js: OpenAI + ElevenLabs pipelinepublic/app.js: tap-to-start/tap-to-stop browser logicpublic/styles.css: stylingCHANGELOG.md: project history
See CHANGELOG.md for the build history from this project thread.