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

Published: (December 9, 2025 at 07:02 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

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

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 viewBox for 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.

Back to Blog

Related posts

Read more »