Skip to content

Commit 90451af

Browse files
johnebgoodclaude
andcommitted
Add React/TypeScript rewrite of tts.bot with full feature parity
- Vite + React + TypeScript SPA replacing static webfront - Zustand stores for auth, voices, chatters, settings, appearance - AWS SDK v3 (Polly, Translate, Cognito) replacing monolithic aws-sdk.js - AudioPlayer class with full queue management (Speak, Skip, Dump, Ban flow) - useTwitchChat hook wrapping tmi.js - ChatProcessor port of doChat() pipeline (emote strip, Levenshtein dedup, translation) - Backend API integration: loadVoice/saveTtsConfig via api.tts.bot - Chat commands: !setvoice, !setspoken, !voices, !poof, !ttsdump, !ttsban, !tts-pause - Voice Recognition hook: pause-on-speech, STS, ban hammer, poof regex - Viewer page (/viewer) for chat viewers to configure their own voice - SettingsPanel with tabs: general, voices, translation, filters, appearance, websocket, vr, cct - CCT overlay page (/cct) with postMessage listener - SPA nginx config with try_files fallback and asset caching - CLAUDE.md documenting architecture, routes, and dev commands Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent cd56ba6 commit 90451af

40 files changed

Lines changed: 9059 additions & 0 deletions

CLAUDE.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
tts.bot is a Twitch Text-to-Speech bot for streamers. It connects to Twitch chat via TMI.js, synthesizes speech using AWS Polly, and supports real-time translation via AWS Translate. The app runs entirely client-side (no backend).
8+
9+
The **original** static HTML/JS app lives in `webfront/` (served by nginx, kept for reference).
10+
The **React rewrite** lives in `react-app/` and is the active codebase.
11+
12+
## Development (react-app/)
13+
14+
```bash
15+
cd react-app
16+
npm install
17+
npm run dev # Vite dev server at localhost:5173
18+
npm run build # Produces dist/ — point nginx root here
19+
npm run typecheck # tsc -b with no emit
20+
```
21+
22+
**Nginx**: point the server root at `react-app/dist/`. The `react-app/nginx.conf` file contains the SPA config with `try_files $uri /index.html` fallback. Run `npm run build` and reload nginx after changes.
23+
24+
## Architecture (react-app/src/)
25+
26+
### Auth flow
27+
1. `/login` — user picks voice/language settings, clicks "Authorize on Twitch"
28+
2. Twitch redirects to `/callback` with `#access_token=…` in URL hash
29+
3. `CallbackPage` extracts token → writes to Zustand (persisted in localStorage) → redirects to `/app`
30+
4. `/app` is protected — redirects to `/login` if no token
31+
32+
### Pages
33+
| Route | File | Purpose |
34+
|---|---|---|
35+
| `/login` | `pages/LoginPage.tsx` | Voice setup + Twitch OAuth |
36+
| `/callback` | `pages/CallbackPage.tsx` | Extracts OAuth token from URL hash |
37+
| `/app` | `pages/StreamerDashboard.tsx` | Main TTS dashboard (protected) |
38+
| `/cct` | `pages/CCTOverlay.tsx` | Closed Captions/Translation standalone popup |
39+
40+
### State (Zustand stores — `store/index.ts`)
41+
All stores use `zustand/middleware/persist` to localStorage except session stores.
42+
43+
| Store | Persisted | Contents |
44+
|---|---|---|
45+
| `useAuthStore` || `accessToken`, `twitchUsername` |
46+
| `useVoiceStore` || All voice/language selections |
47+
| `useChattersStore` || Per-user voice configs (`Record<username, ChatterConfig>`) |
48+
| `useSettingsStore` || All checkboxes (`cb*`) and text inputs (`txt*`) |
49+
| `useAppearanceStore` || CCT overlay visual settings |
50+
| `useConnectionStore` || `channel`, `connectionStatus` |
51+
| `useChatUiStore` || `messages[]`, `queueCount`, `isPaused`, `currentSpeakingId` |
52+
| `useVoicesDataStore` || `voices`, `voicesDesc`, `awsInitialized` |
53+
54+
### AudioPlayer (`audio/AudioPlayer.ts`)
55+
A plain TypeScript class — **not** React state. Created once as a singleton in `hooks/useAudioPlayer.ts` via `useRef`. Key methods: `Speak`, `SpeakNow`, `SpeakNext`, `SpeakCustom`, `SpeakGame2TTS`, `Pause`, `Continue`, `Skip`, `SkipByID`, `Dump`, `DumpByUser`, `PopLastMessage`. Calls `synthesizeSpeech()` from `services/awsService.ts`.
56+
57+
Configure the player after AWS is initialized:
58+
```ts
59+
player.configure({ getSystemVoice, getSystemVoiceOption, getVoices, onQueueCount, onSpeakingId });
60+
```
61+
62+
### TMI.js (`hooks/useTwitchChat.ts`)
63+
`useTwitchChat` hook owns the Twitch chat connection lifecycle. The connection only starts when `enabled: true`. TMI client is stored in a `useRef` to avoid stale closures in handlers. All event handlers are forwarded through a stable `handlers` ref.
64+
65+
### AWS (`services/awsService.ts`)
66+
- `initializeAWS()` — Cognito unauthenticated identity → credentials
67+
- `buildVoiceLookup()` — calls `DescribeVoices`, returns normalized `voices` and `voicesDesc`
68+
- `synthesizeSpeech()` — calls Polly, returns `Uint8Array`
69+
- `translateText()` — calls Translate, returns `{ translatedText, sourceLangCode }`
70+
71+
AWS clients are module-level singletons (not React state). `useAWSServices` hook initializes them once.
72+
73+
### Chat pipeline (`services/chatProcessor.ts`)
74+
`doChat()` is the main async function — ported from the original `doChat()` in `webfront/js/script.js`. It handles:
75+
- Emote stripping (BTTV, FFZ, Twitch)
76+
- SSML detection
77+
- Permission checks (everyone/mod/sub/VIP)
78+
- AWS Translate
79+
- Levenshtein dedup (per-user and chat-wide)
80+
- @username replacement with spoken names
81+
- Link filtering (TLD regex built in `StreamerDashboard`)
82+
- Calling `onSpeak``AudioPlayer.Speak()`
83+
84+
### WebSocket support
85+
Two optional WebSocket connections managed in `StreamerDashboard.tsx`:
86+
- `websocketCustomRef` — custom URL, handles `TTS`, `game2tts`, `GPT-Moderated` topics
87+
- `websocketProdRef` — AWS WebSocket backend
88+
89+
### CCT cross-window communication
90+
The original app called `window.cctPopup.processResults()` directly. The React version uses `window.postMessage` with `{ type: 'cct-result', text, isFinal }`. `CCTOverlay` also exposes `window.processResults` for legacy compatibility.
91+
92+
## Key Constants (`constants/index.ts`)
93+
- `TWITCH_CLIENT_ID``dan71ek0pct1u7b8ht5u4h55zlcxvq`
94+
- `COGNITO_IDENTITY_POOL_ID``us-east-1:e9babc40-c043-4729-91be-de6c1d22b919`
95+
- `AWS_REGION``us-east-1`
96+
97+
## Original app (`webfront/`)
98+
Static HTML/JS, no build step. `webfront/js/script.js` (4,300+ lines) is the reference implementation. Do not modify — it serves as documentation for the React rewrite.

react-app/.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

react-app/README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# React + TypeScript + Vite
2+
3+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4+
5+
Currently, two official plugins are available:
6+
7+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9+
10+
## React Compiler
11+
12+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13+
14+
## Expanding the ESLint configuration
15+
16+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17+
18+
```js
19+
export default defineConfig([
20+
globalIgnores(['dist']),
21+
{
22+
files: ['**/*.{ts,tsx}'],
23+
extends: [
24+
// Other configs...
25+
26+
// Remove tseslint.configs.recommended and replace with this
27+
tseslint.configs.recommendedTypeChecked,
28+
// Alternatively, use this for stricter rules
29+
tseslint.configs.strictTypeChecked,
30+
// Optionally, add this for stylistic rules
31+
tseslint.configs.stylisticTypeChecked,
32+
33+
// Other configs...
34+
],
35+
languageOptions: {
36+
parserOptions: {
37+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
38+
tsconfigRootDir: import.meta.dirname,
39+
},
40+
// other options...
41+
},
42+
},
43+
])
44+
```
45+
46+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47+
48+
```js
49+
// eslint.config.js
50+
import reactX from 'eslint-plugin-react-x'
51+
import reactDom from 'eslint-plugin-react-dom'
52+
53+
export default defineConfig([
54+
globalIgnores(['dist']),
55+
{
56+
files: ['**/*.{ts,tsx}'],
57+
extends: [
58+
// Other configs...
59+
// Enable lint rules for React
60+
reactX.configs['recommended-typescript'],
61+
// Enable lint rules for React DOM
62+
reactDom.configs.recommended,
63+
],
64+
languageOptions: {
65+
parserOptions: {
66+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
67+
tsconfigRootDir: import.meta.dirname,
68+
},
69+
// other options...
70+
},
71+
},
72+
])
73+
```

react-app/eslint.config.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import js from '@eslint/js'
2+
import globals from 'globals'
3+
import reactHooks from 'eslint-plugin-react-hooks'
4+
import reactRefresh from 'eslint-plugin-react-refresh'
5+
import tseslint from 'typescript-eslint'
6+
import { defineConfig, globalIgnores } from 'eslint/config'
7+
8+
export default defineConfig([
9+
globalIgnores(['dist']),
10+
{
11+
files: ['**/*.{ts,tsx}'],
12+
extends: [
13+
js.configs.recommended,
14+
tseslint.configs.recommended,
15+
reactHooks.configs.flat.recommended,
16+
reactRefresh.configs.vite,
17+
],
18+
languageOptions: {
19+
ecmaVersion: 2020,
20+
globals: globals.browser,
21+
},
22+
},
23+
])

react-app/index.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!doctype html>
2+
<html lang="en" data-bs-theme="dark">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>tts.bot by Security_Live</title>
7+
<link
8+
rel="stylesheet"
9+
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
10+
/>
11+
<link
12+
rel="stylesheet"
13+
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.0/css/all.min.css"
14+
/>
15+
</head>
16+
<body class="bg-dark text-white">
17+
<div id="root"></div>
18+
<script type="module" src="/src/main.tsx"></script>
19+
</body>
20+
</html>

react-app/nginx.conf

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Drop this into /etc/nginx/sites-available/tts.bot (or include it from your server block).
2+
# Adjust `root` to wherever you place the built `dist/` folder.
3+
4+
server {
5+
listen 80;
6+
server_name tts.bot www.tts.bot local.tts.bot dev.tts.bot uat.tts.bot;
7+
8+
root /home/security_live/Projects/webroot/tts.bot/react-app/dist;
9+
index index.html;
10+
11+
# SPA fallback — all routes serve index.html
12+
location / {
13+
try_files $uri $uri/ /index.html;
14+
}
15+
16+
# Cache hashed assets aggressively
17+
location /assets/ {
18+
expires 1y;
19+
add_header Cache-Control "public, immutable";
20+
}
21+
22+
# Don't cache index.html
23+
location = /index.html {
24+
add_header Cache-Control "no-cache, no-store, must-revalidate";
25+
}
26+
}

0 commit comments

Comments
 (0)