Building SSR-Friendly Avatars with In-Browser AI: How I Trained Python Models and Ported Them to TensorFlow.js
Source: Dev.to

The Problem
Avatar libraries typically fall into two camps:
- Canvas-based – Fast, but breaks SSR and accessibility
- SVG-as-image – SSR‑friendly, but no dynamic theming or component composition
I wanted both: true SSR compatibility and intelligent avatar generation from user photos.
Native SVG = First‑Class SSR
Every avatar in Avatune renders as a real SVG element, not a canvas or base64 image. This means:
- Zero hydration mismatch – Server renders identical markup to client
- Accessibility built‑in – Screen readers can access SVG semantics
- CSS styling works – Target elements with selectors, use CSS variables
- Inspectable in DevTools – Debug like any DOM element
import { Avatar } from '@avatune/react';
import theme from '@avatune/pacovqzz-theme/react';
// This SSR renders as clean SVG markup
function UserCard({ seed }: { seed: string }) {
return ;
}
Experimental In‑Browser ML Predictors
I trained several CNN models in Python using TensorFlow/Keras on the CelebA and FairFace datasets, then converted them to TensorFlow.js for browser inference:
- Hair Color Predictor – black, brown, blond, gray
- Hair Length Predictor – short, medium, long
- Skin Tone Predictor – light, medium, dark
- Facial Hair Predictor – clean‑shaven vs facial hair
The training pipeline uses Marimo notebooks (reactive Jupyter). Models are quantized to uint8 and served via CDN. Total bundle size per predictor: up to ~2 MB.
Type‑Safe Themes
Each theme exports strongly‑typed color enums and layer configurations:
import type { ReactAvatarItem } from '@avatune/types';
import { createTheme, fromHead } from '@avatune/theme-builder';
const theme = createTheme()
.withStyle({ size: 500, borderRadius: '100%' })
.addColors('hair', [HairColors.Black, HairColors.Brown, HairColors.Blond])
.addColors('skin', [SkinTones.Light, SkinTones.Medium, SkinTones.Dark])
.connectColors('head', ['ears', 'nose']) // ears + nose inherit head's skin color
.setOptional('glasses')
.mapPrediction('hairColor', 'brown', [HairColors.Brown, HairColors.Auburn])
.toFramework()
.build();
TypeScript knows exactly which colors and items are valid for each theme—no more runtime surprises.
Custom Rsbuild Plugins for SVG → Component
When handling 500+ SVG files across multiple frameworks, ID and mask collisions become a problem. Two Rsbuild plugins solve this:
They transform SVG files into proper framework components with:
- Auto‑prefixed IDs via SVGO (no collisions)
- Preserved
viewBoxfor responsive scaling - Query‑based imports for flexibility
import Icon from './icon.svg?svelte';
import Icon from './icon.svg?vue';
The Stack
- Turborepo – Monorepo orchestration with caching
- Bun – Package manager (≈2× faster installs)
- Rspack / Rslib – Build tooling (≈10× faster than webpack)
- Biome – Linting + formatting (replaces ESLint + Prettier)
- uv – Python package manager (10–100× faster than pip)
The monorepo contains 10 themes, 5 framework renderers (React, Vue, Svelte, Vanilla, React Native), and 5 ML predictors.
Try It
- Website + Docs:
- GitHub:
- Playground:
npm install @avatune/react @avatune/pacovqzz-theme
The ML models are experimental—feedback on accuracy and performance is welcome. If you’re interested in training better attribute predictors, PRs are encouraged.