Comment je construis.

Portfolio technique · Engineering Lead chez DJUST

Architecture frontend, IA générative et rendu temps réel. Plutôt qu’une liste de mots-clés, une étude de cas que vous pouvez ouvrir et éprouver : ce site.

Stack

Frontend

  • Nuxt / Vue
  • React 19
  • Next.js 16
  • TypeScript
  • Design systems
  • SSR / SSG

IA générative

  • Agents & tool use
  • MCP
  • RAG
  • LLM integration
  • Realtime voice
  • Agents de code

Backend

  • Node.js
  • Python
  • GraphQL
  • REST
  • PostgreSQL

3D & rendu

  • Three.js
  • React Three Fiber
  • GLSL
  • Post-processing

Cloud & qualité

  • Google Cloud
  • Docker
  • CI/CD
  • Observabilité
  • Tests

Ce portfolio

Une plongée 3D jouable, un agent vocal qui parle en mon nom (et le dit), et un profil statique pour qui est pressé. Conçu par Sami et développé avec des agents de code IA (Claude Code), de l’architecture aux shaders : de l’ingénierie assistée par l’IA, dirigée, relue et testée.

Rendu
Next.js 16 · App Router · pages statiques
Client
React 19 · R3F 9 · three 0.180
Voix
OpenAI Realtime · WebRTC
Agent
outils validés
Tests
node:test · TypeScript transpilé

Architecture

  1. ③ audio + canal de données (WebRTC)
  2. appel d’outil
  3. eventFromTool : validation
  4. dispatch
  5. scène 3D · jeu
  6. function_call_output

Le navigateur parle au modèle en WebRTC ; le serveur ne fait que signer la session. Les actions du modèle reviennent comme données, sont validées, puis appliquées à la scène.

Décisions d’ingénierie

La clé ne quitte jamais le serveur

Problème
Un agent vocal temps réel exige une session côté navigateur, mais la clé d’API ne doit pas y être exposée.
Décision
Le navigateur envoie seulement son offre SDP. Le serveur fixe modèle, outils, voix et limites, puis relaie la réponse SDP.
Compromis
Un aller-retour serveur à la connexion, en échange d’une configuration non falsifiable.
voix · session (simplifié)
// The browser only sends its connection offer. Everything that matters
// (model, tools, voice, limits) is decided on the server, and the
// secret key never reaches the client.
const session = buildSession({ tools: allowlistedTools });
const answer = await relay(offer, session);   // key stays server-side
return answer;

Un appel d’outil est une donnée, pas un ordre

Problème
Le modèle pilote la scène : il ne faut ni action inventée, ni double exécution, ni action d’une réponse interrompue.
Décision
Schémas stricts (clés exactes, valeurs énumérées), déduplication par call_id, réponses annulées ignorées.
Compromis
Moins de liberté pour le modèle ; c’est voulu.
agent · validation des outils
// A tool call is data, never authority: exact keys, enumerated values.
export function eventFromTool(name: unknown, args: unknown) {
  if (!args || typeof args !== 'object' || Array.isArray(args)) return null;
  const keys = Object.keys(args);
  if (name === 'show_project' && keys.length === 1 && keys[0] === 'project')
    return parseExperienceEvent({ type: 'showProject', project: args.project });
  // … one branch per tool; anything else is rejected (null)
}

// run once, and never for a response the visitor interrupted
if (!this.cancelledResponses.has(event.response_id) && !this.handledCalls.has(event.call_id)) {
  this.handledCalls.add(event.call_id);
  const result = action ? this.callbacks.scene(action) : null;
  this.send({ type: 'conversation.item.create', item: { type: 'function_call_output', … } });
}

Un cœur de jeu pur, hors de React

Problème
Oxygène, quêtes et révélations évoluent en continu ; tout passer par l’état React re-rendrait la page à chaque tick.
Décision
Fonction step() pure et testée, petit store externe lu par le HUD (useSyncExternalStore) et par la scène (getState dans useFrame).
Compromis
Un petit store maison au lieu d’une bibliothèque ; aucune dépendance ajoutée.
jeu · cœur pur
// Pure core: (state, telemetry, events) -> new state. Testable in Node.
export function step(state, sites, input, events = []) { … }

// Tiny external store: the HUD subscribes with useSyncExternalStore,
// the 3D scene reads getState() inside useFrame — no 60 fps re-renders.
export function createGameStore(sites, storage) {
  let state = initialGame();
  const listeners = new Set<() => void>();
  return {
    getState: () => state,
    subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
    tick(input) { commit(step(state, sites, input)); },
  };
}

La physique et le shader ne divergent pas

Problème
La houle déplace l’eau de ±0,6 m dans le GPU, mais le CPU décidait « sous l’eau » à y = 0 : brouillard et objectif se trompaient.
Décision
Un miroir analytique de la houle côté TypeScript, et un test qui lit le source GLSL et échoue s’ils divergent.
Compromis
Une duplication volontaire, mais gardée par un test.
scène · houle + test
// "Under water" means under the actual wave, not under y = 0.
export function swellHeight(x, z, time, motion = 1, detail = 1) {
  const localY = -z;
  const a = (x * .93 + localY * .36) * .19 - time * .95;
  const b = (x * -.45 + localY * .89) * .39 - time * 1.35;
  return (Math.sin(a) * .42 + Math.sin(b) * .17 * detail) * motion;
}

// The test reads the GLSL source and fails if the shader drifts:
assert.match(shader, /swell\.z\+=\(sin\(a\)\*\.42\+sin\(b\)\*\.17\*swellDetail\)/);

Des effets sur mesure, bon marché

Problème
Des ondes interactives sur un océan de 1,6 km sans alourdir la géométrie.
Décision
Paquets d’ondes analytiques injectés dans le shader Water de three.js, sortie anticipée loin du front d’onde, calculs sans NaN.
Compromis
Les ondes n’affectent que l’éclairage, pas la géométrie ; invisible à l’œil.
scène · ondes
// 12 wave packets injected into three.js Water: only normals are bent,
// so ripples stay sharp whatever the mesh resolution.
for (int i = 0; i < 12; i++) {
  vec4 r = ripples[i]; float age = rippleClock - r.z;
  if (r.w <= 0. || age < 0. || age > 7.) continue;
  vec2 delta = worldPosition.xz - r.xy; float d = length(delta) + .0001;
  float x = d - age * 2.35;
  if (abs(x) > 10. && d > 1.5) continue;          // early out far from the front
  float envelope = exp(-x * x / (.55 + age * 1.25)) * exp(-age * .62) * r.w / sqrt(1. + d * .35);
  float k = 6.2 - min(age, 4.) * .7;
  rippleSlope += delta / d * cos(x * k) * k * envelope * .11;
}

La performance s’adapte à l’appareil

Problème
Le même monde 3D doit tourner sur un téléphone moyen et sur un poste puissant.
Décision
Mesure des fps en conditions réelles, bascule automatique vers un palier allégé, arbre R3F mémoïsé.
Compromis
Le palier bas sacrifie le post-traitement et une partie de la végétation.
scène · performance
// Quality adapts to the device: after 6 s below 30 fps,
// the scene switches to its low tier (no post-processing, fewer instances).
if (!low && !perf.reported && delta < .12 && clock.elapsedTime > 3) {
  perf.seconds += delta; perf.frames++;
  if (perf.seconds > 6) {
    perf.reported = true;
    if (perf.frames / perf.seconds < 30) onSlow();
  }
}

// The R3F tree is memoised: HUD updates never reconcile the 3D scene.
export default memo(function OceanScene(props) { … });

L’IA, en ingénieur

  • Les secrets et la configuration du modèle restent côté serveur.
  • Les outils sont étroits, typés et validés ; la sortie du modèle est traitée comme une entrée non fiable.
  • Budgets et limites explicites côté serveur ; la voix publique n’est ouverte qu’avec des garde-fous en place.
  • Interruptions gérées : une réponse coupée ne peut plus agir sur la scène.
  • Honnêteté : l’agent parle en mon nom mais dit qu’il est une IA, et ne revendique que des faits vérifiés. Sans service IA, une narration est annoncée comme préenregistrée, jamais simulée.

Qualité et accessibilité

  • Mobile d’abord : joystick tactile, cibles de 44 px, zones de sécurité.
  • Mouvements réduits respectés partout, contrôles au clavier, focus piégé dans les dialogues.
  • Tests automatisés sur le cœur de jeu, la voix, la navigation et la géométrie du monde.
  • Contenu public verrouillé par des tests (aucune information personnelle non souhaitée).

Au quotidien, chez DJUST

Depuis 2021 : architecture frontend d’une plateforme SaaS B2B, plateforme développeurs (composants, SDK TypeScript, API) et produits intégrant l’IA. Engineering Lead depuis janvier 2026.

Profil en 30 s →

Projets personnels