WeatherFish
Built the full frontend and native apps for a 6-person team, and chose and integrated the text-to-speech model that reads the AI weather summary aloud.
- Role
- Frontend developer and TTS integration
- Duration
- Mar – Jul 2026
- Stack
- React, TypeScript, Capacitor
Context
WeatherFish is a weather app that doesn't just show numbers. Along with the current weather and forecast, it writes a short personalized weather summary with an LLM, and you can listen to it as audio instead of reading it.
We built it as a team of 6 during my master's at OTH Amberg-Weiden. The others worked on the backend and the LLM side, and I took the frontend. It runs as a website and also as native Android and iOS apps from the same code.
The frontend
The old client was on React 16 and hard to maintain, so I started a new one from scratch with Vite, React, TypeScript, Tailwind CSS and shadcn/ui. TanStack Query handles all the API calls, Zustand keeps the login, active location and theme, and forms use React Hook Form with Zod.
It has a landing page, login and registration, the home page with current weather, hourly and 7-day forecast, a favorite locations page, a personalization page and a profile page. Location search sits in the topbar, so you can switch city or add it to favorites from anywhere.
The AI summary is expensive to generate, so I didn't want it to be fetched on every visit. It is cached for 24 hours and only generated again when the location changes, the user updates their personalization, or they press refresh.
3
Platforms
Web, Android and iOS from one React codebase
3
TTS models compared
Kokoro-82M, Qwen 0.6B and Qwen 1.7B
24h
Summary cache
Fewer LLM calls without showing old data
Choosing the TTS model
For the audio I compared mainly three models: Kokoro-82M, and the Qwen TTS models in 0.6B and 1.7B sizes. Qwen 1.7B was clearly my favorite, the voice was very accurate and fluent, and it sounded the most natural of the three.
But we had limited hardware, and a 1.7B model was too heavy to run for every request. So we went with Kokoro-82M. I integrated it into the FastAPI backend, running in-process as an int8 ONNX model, so there is no separate TTS server to pay for. The voice is picked from the user's personalization, based on their preferred gender and energy level.
Kokoro-82M instead of Qwen 1.7B
Quality was better with Qwen 1.7B, but it needed more memory and compute than we had. Kokoro is small enough to run inside the API itself, loads once and serves every request, and the quality is still good for short weather summaries.
Trade-off: The voice is less fluent than the model I actually preferred.
The hard parts
This was the first time I worked with audio in the browser and with Capacitor, and those two were the hardest parts for me.
Playing generated audio
Generating the audio takes a moment, and browsers block playback that doesn't start directly from a click. I create and resume the audio context immediately on the click, then send the summary to the audio endpoint, decode the WAV it returns and play it with the Web Audio API. The button shows loading, playing and error states separately, and pressing it again stops the audio.
Trade-off: The audio is only requested when the user presses play, so there is a short wait.
One API layer for web and native
A native app can't always reach the API the same way a browser does, and on the Android emulator localhost doesn't even point to your machine. I wrote one apiFetch helper that uses normal fetch on the web and switches to Capacitor's native HTTP client on Android and iOS. The API URL comes from an environment variable, so the rest of the app has no platform-specific code.
Trade-off: Every request has to go through the helper, never plain fetch.
Result
WeatherFish is live at weatherfish.app, deployed on Vercel with every push, and the same code runs as Android and iOS apps. You get the weather, a personalized summary, and can listen to it with one tap.