iArt.ai
AI 모션 그래픽 소프트웨어, 누구나 사용할 수 있는 After Effects 대안, 텍스트 프롬프트로 몇 분 안에 애니메이션·설명 영상 제작
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Shema — Live Korean Sermon → English Translation
Real-time pipeline: Korean mic → Deepgram STT → Claude translation → ElevenLabs TTS → English audio on listener device.
Target end-to-end latency: 2–5 seconds.
Names and terms the pipeline must get right — the church's own name, member
names, song titles. Two scopes, one table:
Terms reach the pipeline in two places:README 전문 보기 ↗Real-time pipeline: Korean mic → Deepgram STT → Claude translation → ElevenLabs TTS → English audio on listener device.
Target end-to-end latency: 2–5 seconds.
Laptop A (Broadcaster) Backend (Node.js) Laptop B (Listener)
───────────────────── ───────────────── ───────────────────
Mic → PCM chunks
│
▼ WebSocket (binary)
──────────► Deepgram STT WebSocket
↓ Korean transcript
KoreanChunker
↓ semantic chunks
Claude API
↓ { direct, sermon }
ElevenLabs TTS HTTP
↓ MP3 audio
◄────────── WebSocket (base64 audio + text) ──►
Korean transcript ◄────── Audio queue → playback
Direct EN ◄────── Sermon text display
Sermon EN ◄──────
Debug panel ◄────
Names and terms the pipeline must get right — the church's own name, member names, song titles. Two scopes, one table:
| Scope | Lives for | Added from |
|---|---|---|
| Church | Permanently, per church | /glossary, or the desk with scope "Church" |
| Service | One broadcast | The desk's Terms card (the default) |
Terms reach the pipeline in two places:
src/stt.ts and capped at the
provider's limit (100 terms / 500 tokens). Deepgram bakes keyterms into the
connection URL, so a term added mid-sermon improves translation immediately
but only biases recognition from the next broadcast — reconnecting mid-
sermon would drop whatever the preacher was part-way through saying.Storage is Postgres (DATABASE_URL), with migrations applied at boot. If no
database is configured or it is unreachable, the glossary falls back to the
CHURCH_GLOSSARY / CHURCH_GLOSSARY_<SLUG> env vars, read-only, and the
service still goes on air. To import those vars into the database:
cd backend
npx tsx scripts/seed-glossary.ts --dry-run # show what would be imported
npx tsx scripts/seed-glossary.ts # idempotent; safe to re-run
git clone <repo-url>
cd shema
# Install backend dependencies
cd backend && npm install && cd ..
# Install frontend dependencies
cd frontend && npm install && cd ..
# Create backend/.env from the example
cp backend/.env.example backend/.env
Edit backend/.env:
ANTHROPIC_API_KEY=sk-ant-your-key-here
DEEPGRAM_API_KEY=your-deepgram-key-here
ELEVENLABS_API_KEY=your-elevenlabs-key-here
ELEVENLABS_VOICE_ID=pNInz6obpgDQGcFmaJgB # or your preferred voice ID
PORT=3001
For local development, no frontend .env.local is needed — backend URLs are
derived from the page's hostname (ws://<host>:3001/ws, see
frontend/lib/backend-config.ts), which also keeps the two-laptop LAN setup
working with zero config.
In production these come from NEXT_PUBLIC_BACKEND_WS_URL /
NEXT_PUBLIC_BACKEND_HTTP_URL (see frontend/.env.example and the
Deploy section).
cd backend
npm run dev
You should see:
🎙️ Shema backend running on port 3001
WebSocket: ws://localhost:3001/ws?role=broadcaster|listener
Health: http://localhost:3001/health
In a separate terminal:
cd frontend
npm run dev
Open http://localhost:3000 in your browser.
The server laptop runs the backend + frontend and also plays the English audio (Listener). The broadcaster laptop only needs a browser — it captures the Korean mic. Both laptops must be on the same Wi-Fi network.
First, find the server laptop's local IP:
# macOS
ipconfig getifaddr en0 # e.g. 192.168.1.242
# Linux
hostname -I | awk '{print $1}'
Substitute that address for SERVER_IP below.
Terminal 1 — backend:
cd backend
npm run dev
# → 🎙️ Shema backend running on port 3001
Terminal 2 — frontend:
cd frontend
npm run dev
# → ready on http://localhost:3000
Browser (this laptop):
Browsers block microphone access on a plain-http:// address unless it's
localhost. Since this laptop loads the page over the network, allow the mic
once in Chrome:
chrome://flags/#unsafely-treat-insecure-origin-as-securehttp://SERVER_IP:3000 to the box, set to Enabled, click RelaunchThen start broadcasting:
http://SERVER_IP:3001/healthhttp://SERVER_IP:3000
and that you relaunchedSingle-laptop test: run both roles on the server laptop using
http://localhost:3000/speakandhttp://localhost:3000/listenin two tabs —localhostneeds no Chrome flag.
| Mode | Latency | Quality |
|---|---|---|
| Fast | ~2–3 s | Flushes on any final STT segment; rougher at sentence edges |
| Smooth | ~3–5 s | Waits for natural pauses; cleaner sentences |
Toggle during a live broadcast — the change takes effect immediately.
ANTHROPIC_API_KEY in backend/.envModel used: claude-3-5-haiku-20241022 (fastest, lowest cost).
Swap to claude-3-5-sonnet-20241022 in backend/src/translation.ts for higher quality.
DEEPGRAM_API_KEYELEVENLABS_API_KEYELEVENLABS_VOICE_IDRecommended voices for sermon delivery:
pNInz6obpgDQGcFmaJgB) — deep, authoritativeonwK4e9ZLuTAKqWW03F9) — clear, naturalOne Next.js app (marketing + product) on Vercel, the WebSocket backend on Railway, and Cloudflare as registrar/DNS + host of the small contact-form Worker. Follow the steps in order; the app stays runnable locally throughout (local dev needs none of these env vars).
tryshema.app → DNS at Cloudflare (points at everything below)
├─ / → marketing bundle ┐
├─ /speak /listen/[church] → product routes ├─ ONE Next.js app on VERCEL
├─ /api/contact → Next rewrite → contact Worker on CLOUDFLARE
└─ api.tryshema.app → Node WebSocket backend on RAILWAY (all API keys live here)
Secrets rule: API keys live ONLY on Railway. The frontend carries nothing
but public NEXT_PUBLIC_* URLs.
Vercel and Railway both deploy straight from the GitHub repo — make sure the branch you want live is pushed.
backend. Railway then auto-detects Node and runs
npm install → npm run build → npm start (already wired: build
compiles to dist/, start runs node dist/index.js).ANTHROPIC_API_KEYDEEPGRAM_API_KEYELEVENLABS_API_KEYELEVENLABS_VOICE_IDFRONTEND_URL=https://tryshema.app,https://www.tryshema.appAUTH_USERS + AUTH_SECRET — locks broadcasting behind staff login (see
Staff login below)NODE_ENV=production (usually set automatically; setting it explicitly
is what arms the CORS/WebSocket origin allowlist)PORT — Railway injects its own and the server reads it.something.up.railway.app URL and check https://<it>/health returns JSON.api.tryshema.app.
Railway shows you a CNAME target — you'll create that record in
Cloudflare in step 4.frontend (framework auto-detects as Next.js).NEXT_PUBLIC_BACKEND_WS_URL=wss://api.tryshema.app/wsNEXT_PUBLIC_BACKEND_HTTP_URL=https://api.tryshema.appCONTACT_WORKER_ORIGIN=<the contact Worker's own origin> — its
https://….workers.dev URL from step 4. ⚠️ This must be the Worker's OWN
origin: not https://tryshema.app (that's Vercel itself — the
rewrite would loop forever) and not api.tryshema.app (that's the
Railway backend, which has no contact endpoint). If you don't have the
Worker URL yet, deploy without it and add it after step 4 (then
Redeploy — env vars only take effect on a fresh build).*.vercel.app URL: / shows the marketing
page, /speak and /listen load (they'll connect once DNS is live).tryshema.app and
www.tryshema.app. Vercel will display the exact DNS records it wants —
keep that page open for step 4.Deploy the contact-form Worker:
The Worker deploys as-is to its own *.workers.dev origin — it claims no
custom domain (tryshema.app belongs to Vercel via DNS):
cd marketing-worker
npx wrangler deploy # log in with your Cloudflare account when prompted
Note the printed https://tryshema.<your-account>.workers.dev URL → that is
CONTACT_WORKER_ORIGIN for Vercel (step 3.3; set it and redeploy).
Email delivery uses Cloudflare Email Routing on the tryshema.app zone —
it was already configured for the old site; verify the destination address is
still verified under Cloudflare → Email → Email Routing.
DNS records (Cloudflare dashboard → tryshema.app → DNS):
tryshema.app at the
old Cloudflare-hosted site.A record, name @ (tryshema.app) → Vercel's IP (e.g. 76.76.21.21)CNAME, name www → cname.vercel-dns.comCNAME, name api → the target Railway displayedhttps://tryshema.app → marketing page; https://api.tryshema.app/health → JSONhttps://tryshema.app/listen/<church> on a phone on cellular (not your
Wi-Fi) — it should connect and, during a broadcast from /speak in the same
church, play audiodig tryshema.app /
dig api.tryshema.app to watch it flipPhase A auth: staff log in with a username + password (POST /login) and
receive a 12-hour session token; starting a broadcast requires that token.
Listening never needs auth. No accounts database — users live in backend env
vars:
# JSON map of username → bcrypt password hash. Generate a hash:
# cd backend && node -e "console.log(require('bcryptjs').hashSync('your-password', 10))"
AUTH_USERS={"hanmaum":"$2b$10$...your-hash..."}
# Signs the session tokens — any long random string (openssl rand -hex 32):
AUTH_SECRET=pick-a-long-random-string
/login; a wrong or expired session
gets a clear error and the connection is closed.AUTH_USERS, redeploy the backend (Railway → Variables → edit → Apply).Real accounts + billing (Phase B) replace this once a second church signs on.
Three URLs run the whole service. <church> is your church's slug
(e.g. grace-church) — same one everywhere.
https://tryshema.app/listen/<church> — put the QR in the bulletin, on a
slide, or on a sign by the door. It's the same every week; print it once.For churches feeding translation into an existing transmitter/receiver system:
| Symptom | Fix |
|---|---|
| Phone: no sound | They skipped the tap — reload and press Tap to listen; check the phone's silent switch + volume |
| Phone: iPhone stops when locked | Known iOS limitation — keep the screen on, or use the kiosk + receivers |
| /speak: "Invalid host key" | Wrong or missing key — re-enter it (keys are per-church; rotate in Railway → Variables) |
| /speak: no Korean appearing | Wrong Input device — re-pick the board/interface; check the STT pill; make sure the board channel is unmuted |
| /speak: Korean appears, no English ever | Translation API issue — check Anthropic credits (console.anthropic.com → Plans & Billing) |
| Kiosk: test tone silent | Laptop output routed wrong — macOS System Settings → Sound → Output; then test tone again |
| Nothing connects at church | Check https://api.tryshema.app/health in a browser. If it loads but the app won't connect, church Wi-Fi may block WebSockets — hotspot the staff laptop or ask IT to allow wss to api.tryshema.app |
| Mid-service page weirdness | Reload the page — broadcast state recovers by itself; listeners auto-reconnect |
Weekly pre-service check (2 min): open /speak, start a 10-second test
broadcast in a test room (e.g. <church>-test), confirm English audio on your
phone, stop. Confirms keys, credits, and audio path in one go.
| Stage | Typical Duration | Notes |
|---|---|---|
| Audio capture → backend | < 50 ms | 200ms chunks via WebSocket |
| ElevenLabs STT | 300–800 ms | WebSocket streaming, near real-time |
| Korean chunking | 200–500 ms | Waits for natural pause or max length |
| Claude translation | 400–900 ms | Haiku model, ~512 output tokens |
| ElevenLabs TTS | 600–1500 ms | Streaming HTTP, depends on text length |
| Audio delivery → listener | < 100 ms | WebSocket + browser decode |
| Total | ~2–5 s | Varies with network and text length |
modelId in tts.ts)chunkIntervalMs in AudioCapture (already 200ms)Instead of the browser mic, you can feed the backend a direct audio line:
AudioCapture with a Node.js audio input library (e.g., node-audiorecord or sox) that streams PCM to the backend STT module directlystt.ts → chunker.ts → translation.ts → tts.ts) remains unchangedThis eliminates laptop-microphone background noise entirely.
# Backend
cd backend
npm run dev # Development (ts-node-dev, hot reload)
npm run build # Compile TypeScript → dist/
npm start # Run compiled output
npm test # Tier A: deterministic unit tests (no API key, no network)
npm run eval # Tier B: scored ko-en translation evals (needs ANTHROPIC_API_KEY)
# add -- --judge for LLM-graded faithfulness/fluency
npm run eval:en-ko # Tier B for en-ko: register (하십시오체) / honorific / Hangul-script
# / Korean-book-name checks + EN→KO→EN round-trip similarity;
# add -- --judge for LLM-graded faithfulness/naturalness/register.
# Catches register & gross errors — a Korean speaker still
# signs off a full sermon before a real en-ko service.
# Frontend
cd frontend
npm run dev # Next.js dev server on :3000
npm run build # Production build
npm start # Serve production build
shema/
├── backend/
│ ├── src/
│ │ ├── index.ts # Express + WebSocket server
│ │ ├── session.ts # In-memory session state
│ │ ├── broadcaster.ts # Broadcaster WS handler (audio in → pipeline)
│ │ ├── listener.ts # Listener WS handler (audio/text out)
│ │ ├── stt.ts # ElevenLabs STT WebSocket client
│ │ ├── translation.ts # Claude translation (returns JSON)
│ │ ├── tts.ts # ElevenLabs TTS (HTTP streaming → Buffer)
│ │ └── chunker.ts # Korean text chunking logic
│ ├── package.json
│ └── tsconfig.json
├── frontend/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── globals.css
│ │ ├── page.tsx # Landing page (/)
│ │ ├── speak/page.tsx # Broadcaster UI (/speak) + QR share
│ │ ├── listen/page.tsx # Church-code entry (/listen, /listen?church=)
│ │ ├── listen/[church]/ # Listener UI (/listen/<church>, the QR link)
│ │ └── play/[church]/ # Kiosk output (placeholder until Step 6)
│ ├── lib/
│ │ ├── ws-client.ts # Typed WebSocket client
│ │ ├── audio-capture.ts # Mic → PCM 16kHz chunks
│ │ └── audio-playback.ts # MP3 queue → Web Audio API
│ ├── next.config.js
│ ├── package.json
│ └── tsconfig.json
├── .env.example
└── README.md