feat: Zacus V3 complete

Phase 1: zacus_v3_complete.yaml, constants, phrases
Phase 3: ESP-NOW framework, 4 puzzle firmwares, master
Phase 4: NPC V3 profiling + game coordinator
Phase 5: XTTS-v2 Docker + voice clone generator
Phase 6: AudioCraft + BLE audio control
Phase 9: One-pager, video script, pricing, guide
This commit is contained in:
L'électron rare
2026-04-03 00:42:34 +02:00
parent 454b814584
commit 9f4501b9b9
11 changed files with 1696 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
# Guide de Déploiement — Le Mystère du Professeur Zacus
**Objectif :** Installation complète en 15 minutes sur n'importe quel lieu.
**Prérequis :** Kit V3 complet (3 valises), batteries chargées à 100%.
---
## Avant de partir (30 min, à faire chez vous / au bureau)
### Vérification matérielle
- [ ] Charger toutes les batteries à 100 % (chargeur GaN 65W 4 ports, ~3h)
- Anker #1 (hub) — 20 000 mAh
- Anker #2 (spare) — 20 000 mAh
- Pack puzzle A — 10 000 mAh
- Pack puzzle B — 10 000 mAh
- [ ] Allumer BOX-3 → vérifier firmware version (Settings → About → Version ≥ v3.1)
- [ ] Allumer chaque puzzle ESP32 (LED verte = OK, LED rouge = recharger)
- [ ] Compter les 12 tuiles NFC symboles alchimiques (P6)
- [ ] Vérifier les 6 QR codes A5 plastifiés (aucun ne doit être plié ou illisible)
<!-- PHOTO: [batteries_charged.jpg] — 4 batteries posées côte à côte, toutes LED vertes -->
### Vérification logicielle
- [ ] BOX-3 → Settings → Scenario → `zacus_v3_complete` chargé
- [ ] Pool TTS MP3 à jour (si nouvelles phrases ajoutées depuis la dernière session)
```bash
python3 tools/tts/generate_npc_pool.py --dry-run
```
- [ ] Test ESP-NOW mesh rapide (à la maison, avant transport) :
```bash
./tools/dev/zacus.sh espnow-test
```
Résultat attendu : `MESH: 6/6 OK`
---
## Sur place : Installation en 15 minutes
### Minutes 12 : Hub central (Valise 1)
<!-- PHOTO: [valise1_layout.jpg] — contenu de la valise 1 vue du dessus -->
- [ ] Poser RTC_PHONE sur la table d'accueil, brancher USB-C → Batterie #1
- [ ] Poser BOX-3 sur mini-trépied, orienter caméra vers le centre de la pièce
- [ ] Brancher BOX-3 USB-C → Batterie #1 (même batterie, hub USB-C 2 ports)
- [ ] Brancher le routeur GL.iNet → Batterie #2 USB-C
- [ ] Attendre les LED vertes sur les 3 appareils (~30 secondes)
**Contrôle visuel :** BOX-3 affiche « WAITING FOR MESH… »
<!-- PHOTO: [hub_setup.jpg] — RTC_PHONE + BOX-3 + routeur sur table, tous allumés -->
---
### Minutes 37 : Puzzles (Valise 2)
Placer chaque puzzle à sa position définie dans la salle (voir plan de salle ci-dessous).
<!-- PHOTO: [floor_plan.jpg] — schéma de placement des 7 puzzles dans une salle générique -->
- [ ] **P1 — Séquence Sonore** → table latérale, brancher USB-C → Pack A
- [ ] **P2 — Breadboard Magnétique** → sol ou grande table, déplier, USB-C → Pack A
- [ ] **P4 — Poste Radio** → rebord de fenêtre ou étagère (hauteur yeux)
- [ ] **P5 — Télégraphe** → bureau avec la carte de référence morse posée à côté
- [ ] **P6 — Tablette Symboles** → grande table, disposer les 12 tuiles NFC autour
- [ ] **P7 — Coffre Final** → position centrale et bien visible, USB-C → Pack B, VERROU FERMÉ
- [ ] Cacher les 6 QR codes A5 aux positions définies (voir carte de placement dans valise 1)
<!-- PHOTO: [p1_setup.jpg] — P1 boîte sonore sur table -->
<!-- PHOTO: [p6_setup.jpg] — tablette symboles avec tuiles disposées autour -->
<!-- PHOTO: [p7_setup.jpg] — coffre final position centrale, verrou visible -->
**Contrôle :** Chaque puzzle allumé = LED verte fixe. Si LED orange = calibration en cours (attendre 10s).
---
### Minutes 810 : Ambiance (Valise 3)
<!-- PHOTO: [valise3_layout.jpg] — enceinte BT + LED strip + signalétique -->
- [ ] Poser l'enceinte Bluetooth, maintenir le bouton pairing 3 secondes
- [ ] Sur BOX-3 → Settings → Bluetooth → connecter l'enceinte (nom : `ZACUS-SPK`)
- [ ] Dérouler le ruban LED USB (3m), brancher → Port USB de la batterie #2
- Couleur : bleu/vert pour ambiance laboratoire
- [ ] Installer les 4 panneaux de signalétique :
- Entrée : « Laboratoire du Professeur Zacus — Accès Restreint »
- Règles du jeu (format A3, plastifié)
- Zones puzzle : numérotées P1 à P7
- Sortie de secours : toujours visible, non obstrué
<!-- PHOTO: [ambiance_setup.jpg] — salle vue de l'entrée avec éclairage ambiance -->
---
### Minutes 1113 : Vérification du mesh ESP-NOW
- [ ] BOX-3 écran principal → attendre « MESH: 6/6 CONNECTED »
- Si un nœud manquant après 90 secondes : appuyer RESET sur ce puzzle ESP32
- Si toujours absent : vérifier LED — éteint = pas alimenté, rouge = erreur firmware
- [ ] Test fonctionnel rapide :
- Appuyer sur un bouton de P1 → BOX-3 doit afficher l'événement reçu
- Taper le clé P5 → BOX-3 doit afficher le code morse reçu
<!-- PHOTO: [box3_mesh_ok.jpg] — écran BOX-3 montrant "MESH: 6/6 CONNECTED" -->
---
### Minutes 1415 : Configuration game master
- [ ] BOX-3 touchscreen → **Settings**
- **Target Duration** → sélectionner [30 / 45 / 60 / 75 / 90 min] selon le groupe
- **WiFi** → entrer le WiFi du lieu (pour XTTS live) **OU****Offline Mode** (MP3 SD)
- **Volume NPC** → ajuster selon la taille de la salle (défaut : 70%)
- [ ] Appuyer **READY** → autotest 30 secondes → tous les puzzles en vert
- [ ] Briefing game master : mémoriser les 3 niveaux d'indices par puzzle
<!-- PHOTO: [box3_settings.jpg] — écran BOX-3 menu Settings ouvert -->
<!-- PHOTO: [box3_ready.jpg] — écran BOX-3 "ALL SYSTEMS GO" -->
**La session peut démarrer. Appuyer sur START GAME.**
---
## Démarrage du jeu
1. Faire entrer les joueurs dans la pièce (lumières éteintes ou tamisées)
2. La sonnerie du RTC_PHONE retentit automatiquement après 10 secondes
3. Les joueurs décrochent → Professeur Zacus parle → jeu commence
---
## Pendant le jeu — Rôle du game master
| Situation | Action |
|-----------|--------|
| Joueurs bloqués > 5 min | BOX-3 → Hint → sélectionner puzzle → niveau 1 |
| Puzzle ESP32 déconnecté | Appuyer RESET sur le puzzle, reconnecter sous 30s |
| Enceinte BT déconnectée | BOX-3 → Bluetooth → Reconnect |
| Panne réseau WiFi | BOX-3 bascule automatiquement en mode offline |
| Urgence (évacuation) | BOX-3 → PAUSE GAME → sortie sécurisée |
---
## Rangement (10 minutes)
- [ ] BOX-3 → **End Game****Export Stats** (PDF ou QR code)
- [ ] Partager le score/diplôme avec les joueurs (QR ou lien)
- [ ] Éteindre tous les puzzles (débrancher USB-C)
- [ ] **Compter 12 tuiles NFC** avant de les ranger (ne jamais en oublier)
- [ ] Récupérer les 6 QR codes A5
- [ ] Emballer dans l'ordre inverse (puzzles → hub → ambiance)
- [ ] Recharger toutes les batteries dès retour (ne pas stocker à plat)
---
## Dépannage rapide
| Symptôme | Cause probable | Solution |
|----------|---------------|----------|
| Nœud ESP32 absent du mesh | Firmware crashé | RESET sur le puzzle |
| Voix NPC muette | WiFi hors portée | Basculer Offline Mode |
| Enceinte BT grésillements | Distance > 10m | Rapprocher l'enceinte |
| BOX-3 ne démarre pas | Batterie < 5% | Brancher USB-C direct (pas batterie) |
| QR code non scanné | Angle ou distance | Ajuster la caméra BOX-3, recoller le QR |
| Score non calculé | P7 non résolu | Vérifier que le coffre est bien verrouillé au départ |
---
## Contact support
- Email : clement@lelectronrare.fr
- Urgence session : +33 6 XX XX XX XX (disponible pendant sessions avec préavis)
- Documentation complète : docs/DEPLOYMENT_RUNBOOK.md
- Issues firmware : github.com/electron-rare/le-mystere-professeur-zacus/issues
+101
View File
@@ -0,0 +1,101 @@
# One-Pager : Le Mystère du Professeur Zacus
<!-- To print: open in browser, use print-to-PDF, A4 recto-verso -->
---
## RECTO
**[LOGO] L'Electron Rare**
*Solutions technologiques créatives*
---
# Le Mystère du Professeur Zacus
### *L'escape room portable, adaptative, et alimentée par l'IA*
---
**7 énigmes physiques** — Séquence sonore, circuit LED, fréquence radio,
code morse, symboles alchimiques, QR Treasure, coffre final verrouillé.
**IA adaptative** — Le Professeur profile votre groupe (tech / non-tech) en 10 minutes
et ajuste la difficulté en temps réel. Chaque groupe vit une expérience calibrée.
**Voix clonée** — Professeur Zacus parle en direct via IA vocale (XTTS-v2 sur GPU)
avec fallback Piper TTS — toujours disponible, même sans connexion internet.
**3 valises** — Déployable en 15 minutes, partout, sans technicien. Offline-first.
---
| | **Animation** | **Location** | **Kit** |
|---|---|---|---|
| **Prix** | 800 1 500 €/séance | 300 500 €/week-end | 3 000 5 000 € |
| **Joueurs** | 4 15 | 4 15 | 4 15 |
| **Durée** | 2 3h sur place | Vendredi → Lundi | Achat définitif |
| **Inclus** | Animateur + débrief | Guide + support tél. | Firmware + 3 scénarios + 1h formation |
| **Idéal pour** | Teambuilding, événements | Associations, anniversaires | Escape rooms, musées |
| **CTA** | Demander un devis | Réserver | Acheter |
---
> *"Le Professeur nous a bluffés. On ne savait pas que c'était une IA."*
> — Groupe bêta, session de test 2026
<!-- Add second testimonial after beta playtest Group 2 -->
<!-- > *"Parfaitement adapté à notre groupe. Ni trop facile, ni trop difficile."* -->
<!-- > — Groupe non-tech, bêta 2026 -->
---
**Contact :** clement@lelectronrare.fr | lelectronrare.fr/zacus | +33 6 XX XX XX XX
---
## VERSO — Fiche technique (Kit buyers)
### Architecture technique
| Composant | Détail |
|-----------|--------|
| Hub | BOX-3 (ESP32-S3) orchestrateur + RTC_PHONE custom (voix) |
| Communication | ESP-NOW mesh offline-first, 7 nœuds |
| TTS | XTTS-v2 (GPU) → Piper TTS → MP3 SD card (3 niveaux de fallback) |
| Audio ambiance | 6 pistes pré-générées AudioCraft MusicGen (RTX 4090) |
| Firmware | ESP-IDF, C, FreeRTOS, PlatformIO — open source MIT |
| Scénarios | YAML → Runtime 3 IR — compilateur Python inclus |
### BOM indicatif
| Catégorie | Coût estimé |
|-----------|-------------|
| Électronique (ESP32 × 9, capteurs, relais) | ~280 € |
| Boîtiers 3D imprimés ABS + découpe laser | ~120 € |
| 3 valises Peli ou équivalent | ~180 € |
| Câbles, batteries, accessoires | ~62 € |
| **Total BOM** | **~642 €** |
| **Marge Kit Standard (3 500 €)** | **~83 %** |
### Robustesse & support
- Conçu pour **>100 déploiements** (boîtiers industriels, contacts dorés)
- Composants standards : Reed switches, NTAG213, SG90, ESP32-S3
- Mises à jour firmware OTA via WiFi ou USB-C — **1 an inclus** dans le kit
- Documentation complète + guide de déploiement 15 minutes
- Support premium disponible : 500 €/an (téléphone + 2 scénarios supplémentaires)
### Scénarios disponibles
| Scénario | Thème | Durée cible |
|----------|-------|-------------|
| zacus_v3_complete | Laboratoire du Professeur (7 énigmes) | 30 90 min |
| *(en développement)* | Bibliothèque mystérieuse | Q3 2026 |
| *(en développement)* | Vaisseau spatial | Q4 2026 |
---
**Contact commercial :** clement@lelectronrare.fr
**Site :** lelectronrare.fr/zacus
**GitHub :** github.com/electron-rare/le-mystere-professeur-zacus
+160
View File
@@ -0,0 +1,160 @@
# Grille Tarifaire — Le Mystère du Professeur Zacus
*Tarifs HT. TVA 20% applicable selon statut client.*
*Mis à jour : 2026-04-02*
---
## Les 3 Tiers
### Tier 1 — Animation
> **Le Professeur vient chez vous.** Clé en main : installation, animation, débrief.
| Formule | Prix HT | Détail |
|---------|---------|--------|
| Demi-journée (23h session) | **800 €** | 1 session de jeu, 415 joueurs |
| Journée complète (2 sessions) | **1 500 €** | 2 sessions back-to-back (matin + après-midi) |
| Soirée d'entreprise (1 session + cocktail) | **1 200 €** | Session de jeu + 30 min de débrief convivial |
**Inclus :**
- Déplacement dans un rayon de 50 km (Île-de-France base)
- Installation et rangement du kit (non compté dans la durée payante)
- Animation complète par un formateur certifié
- Diplôme numérique PDF pour chaque joueur
- Compte-rendu statistique post-session (temps, score, indices utilisés)
- Adaptation TECH / NON_TECH automatique (profiling IA)
**Frais additionnels :**
- Déplacement > 50 km : +0,45 €/km aller-retour + hébergement si nécessaire
- 3e session dans la journée : +500 €
- Week-end / jours fériés : +20 %
- Remise corporate (>3 sessions/an) : -10 %
---
### Tier 2 — Location
> **Le kit chez vous, le week-end.** Pour associations, anniversaires, séminaires résidentiels.
| Formule | Prix HT | Durée |
|---------|---------|-------|
| Week-end | **400 €** | Vendredi 18h → Lundi 10h |
| Semaine | **650 €** | 7 jours |
**Inclus :**
- Kit V3 complet (3 valises, 7 puzzles, hub, ambiance)
- Guide de déploiement 15 minutes (papier + vidéo QR)
- Support téléphonique pendant la location (heures ouvrées)
- Formation vidéo game master (30 min, lien envoyé à J-3)
- 3 scénarios chargés : `zacus_v3_complete` + 2 variantes de durée (30 min et 90 min)
**Caution :** 1 000 € (chèque ou CB pré-autorisé, restitué sans frais si kit rendu intact)
**Frais additionnels :**
- Livraison/récupération > 50 km : +0,45 €/km A/R
- Extension location : +60 €/jour supplémentaire
- Remplacement pièce endommagée : coût pièce + 30 € de gestion
**Non inclus :** Animation humaine (prévoir une personne formée via la vidéo game master)
---
### Tier 3 — Kit (Achat définitif)
> **Votre propre Professeur Zacus.** Pour escape rooms professionnelles, musées, centres de loisirs.
| Référence | Prix HT | Contenu |
|-----------|---------|---------|
| **ZACUS-KIT-STD** | **3 500 €** | Kit Standard |
| **ZACUS-KIT-PRO** | **4 500 €** | Kit Pro (+ support premium 1 an) |
| **ZACUS-KIT-ULTIMATE** | **5 000 €** | Kit Ultimate (+ 5 scénarios + formation 4h) |
#### Kit Standard — 3 500 € HT
- 3 valises déployées et testées (7 puzzles + hub + ambiance)
- Firmware open source — licence MIT
- 3 scénarios YAML inclus (`zacus_v3_complete` + 2 variantes)
- 1h de formation à distance (visio, prise en main complète)
- Accès aux mises à jour firmware pendant **1 an** (OTA ou USB-C)
- Documentation complète : DEPLOYMENT_RUNBOOK.md, code source commenté
- Pool TTS pré-généré (>200 phrases, Piper TTS + XTTS-v2)
#### Kit Pro — 4 500 € HT
Tout le Standard +
- Support téléphonique prioritaire pendant **1 an** (réponse < 4h en semaine)
- 2 scénarios supplémentaires au choix (sur devis ou catalogue)
- Session de formation présentielle 2h (ou 4h à distance)
- Kit de pièces de rechange : 2× ESP32-S3 dev kit, 10× NTAG213, 2× SG90
#### Kit Ultimate — 5 000 € HT
Tout le Pro +
- 5 scénarios YAML personnalisables
- Formation approfondie 4h présentielle (game design + firmware de base)
- Branding personnalisé : nom du professeur, voix personnalisée (enregistrement 20 min requis)
- 1 session d'animation offerte pour inauguration
---
## Options et add-ons
| Référence | Prix HT | Détail |
|-----------|---------|--------|
| **ZACUS-SCENARIO** | 200 € | Scénario supplémentaire (YAML livré, compilé, testé) |
| **ZACUS-SUPPORT-AN** | 500 €/an | Support premium annuel (renouvellement après 1 an inclus) |
| **ZACUS-VOICE-CLONE** | 300 € | Clone vocal personnalisé XTTS-v2 (votre propre professeur) |
| **ZACUS-BRANDING** | 150 € | Personnalisation visuelle BOX-3 (logo, couleurs, nom) |
| **ZACUS-TRAINING-2H** | 200 € | Formation game master supplémentaire (2h visio) |
---
## Récapitulatif business
### Analyse de rentabilité (Kit Standard)
| Poste | Valeur |
|-------|--------|
| BOM total | ~642 € |
| Prix de vente Kit Standard | 3 500 € HT |
| **Marge brute** | **~2 858 € (82 %)** |
| Temps de fabrication / formation | ~5h |
| Coût horaire valorisé | ~571 €/h |
### Projections revenus annuels (estimatif)
| Scénario | Sessions Animation | Locations | Kits | CA estimé HT |
|----------|--------------------|-----------|------|--------------|
| **Minimal** | 4/mois × 800 € | 2/mois × 400 € | 0 | ~57 600 € |
| **Réaliste** | 6/mois × 1 000 € | 4/mois × 400 € | 2/trimestre × 3 500 € | ~103 200 € |
| **Ambitieux** | 10/mois × 1 200 € | 8/mois × 400 € | 2/mois × 4 000 € | ~241 600 € |
### Revenus récurrents (SaaS-like)
| Client | Montant annuel HT |
|--------|-------------------|
| Support premium (par client Kit) | 500 € |
| 1 scénario/an par client Kit | 200 € |
| Renouvellement location régulier (asso) | ~1 200 € |
---
## Conditions générales
- **Devis valable 30 jours** à compter de la date d'émission
- **Acompte :** 30 % à la commande, solde à la livraison/prestation
- **Paiement :** Virement bancaire, chèque, CB (via Stripe)
- **Délai de livraison Kit :** 3 semaines (assemblage + tests + expédition)
- **Annulation Animation :** remboursement 100 % si > 7 jours, 50 % si 27 jours, 0 % si < 48h
- **Annulation Location :** remboursement 100 % si > 5 jours, 50 % si 25 jours
---
## Contact commercial
**Clément Saillant — L'Electron Rare**
clement@lelectronrare.fr | lelectronrare.fr/zacus | +33 6 XX XX XX XX
*SIRET : XX XXX XXX XXXXX — TVA intracommunautaire : FR XX XXXXXXXXX*
*Auto-entrepreneur / Micro-entreprise (seuil de franchise TVA selon chiffre d'affaires)*
+167
View File
@@ -0,0 +1,167 @@
# Script Vidéo de Démonstration — Le Mystère du Professeur Zacus
**Durée cible :** 2:30
**Format :** Shot-by-shot, playthrough réel
**Ton :** Mystérieux → engageant → impressionnant → actionnable
**Équipement recommandé :** Smartphone 4K ou mirrorless, trépied, micro-cravate, éclairage d'ambiance bleu/vert
---
## 0:00 0:15 | HOOK — Le laboratoire s'éveille
**Cadrage :** Plan large, pièce sombre. Seules quelques LED vertes clignotent sur les puzzles.
**Action :** Un téléphone vintage sur une table sonne. Personne ne répond pendant 3 secondes.
**Audio :** Sonnerie rétro, puis voix du Professeur Zacus (XTTS-v2) :
> *"Bienvenue dans mon laboratoire portable… Si vous entendez ceci, c'est que j'ai besoin de vous."*
**Texte overlay :** « Le Mystère du Professeur Zacus » (titre, font serif, fade in)
---
## 0:15 0:30 | SETUP — 15 minutes, n'importe où
**Cadrage :** Time-lapse à 8× — vue de dessus d'une table vide.
**Action :** Les 3 valises s'ouvrent, le matériel se déploie, les LED s'allument une par une.
Dernier plan : 7 puzzles en place, BOX-3 affiche « MESH: 6/6 CONNECTED ».
**Audio :** Musique upbeat légère (piste AudioCraft `lab_ambiance`)
**Texte overlay :** « Déploiement : 15 minutes. Partout. »
---
## 0:30 1:00 | PUZZLES — La richesse physique
Série de coupes rapides (chaque puzzle ~4 secondes) :
**P1 — Séquence Sonore** (0:30)
- Plan : 4 boutons arcade illuminés sur une boîte en bois
- Action : Mélodie jouée, joueur reproduit la séquence, LED verte confirme
- Son : Bips tonals + rire de surprise du joueur
**P2 — Circuit LED** (0:34)
- Plan : Breadboard magnétique 60×40 cm posée au sol
- Action : Main place un composant, le circuit s'allume
- Son : Clic magnétique satisfaisant
**P5 — Code Morse** (0:38)
- Plan : Gros plan sur le télégraphe en bois et laiton
- Action : Doigt tape en rythme, BOX-3 affiche les lettres décodées en temps réel
- Son : Tap tap tap... pause... tap (rythme morse de « ZACUS »)
**P6 — Symboles Alchimiques** (0:44)
- Plan : Tablette laser-cut 30×20 cm, 12 tuiles NFC autour
- Action : Joueur place la 9e tuile, NFC bipe, symbole s'illumine sur la tablette
- Son : Bip NFC + accord harmonique montant
**P7 — Coffre Final** (0:50)
- Plan : Coffre en bois centre scène, pad numérique
- Action : Équipe entre le code à 8 chiffres ensemble, servo claque, couvercle s'ouvre
- Son : Click mécanique + musique victoire (piste `victory`)
**Audio global :** Sons d'ambiance lab + réactions authentiques des joueurs
---
## 1:00 1:30 | IA ADAPTATIVE — Le Professeur vous observe
**Cadrage :** Split-screen gauche/droite
**Gauche — Groupe TECH :**
- Joueurs en costume décontracté (profil ingénieurs)
- Résolvent P2 Circuit en 2 minutes → BOX-3 classe « TECH »
- Profil affiché en encart : icône engrenage, « TECH »
**Droite — Groupe NON_TECH :**
- Joueurs variés (famille, asso)
- P2 Circuit plus lent → profil « NON_TECH »
- Ils reçoivent P6 Symboles en premier à la place
**Audio :** Voix Professeur :
> *"Groupe technique détecté. Excellente nouvelle… J'ai quelque chose de plus… stimulant pour vous."*
**Texte overlay :** « Adapté à chaque groupe en temps réel »
---
## 1:30 1:50 | VOIX CLONÉE — L'IA qui parle
**Cadrage :** Gros plan sur l'écran BOX-3 montrant la forme d'onde TTS en direct.
**Action :** Animation de la forme d'onde audio pendant que la voix parle.
**Audio :** Voix Professeur (XTTS-v2 live) :
> *"Vous allez trop vite. J'ai décidé d'ajouter… une petite surprise."*
> (ton légèrement malicieux, naturaliste)
**Texte overlay :** « Voix clonée par IA — indiscernable du réel »
**Cut :** Réaction d'un joueur qui regarde le téléphone : « C'est vraiment une IA ?! »
---
## 1:50 2:10 | VICTOIRE + SCORE
**Cadrage :** Plan large de toute l'équipe autour du coffre ouvert.
**Action :**
- Coffre s'ouvre
- Score affiché sur BOX-3 : 1 340 / 1 600 pts
- Diplôme PDF généré automatiquement
- High-five de l'équipe
**Audio :** Fanfare victoire (`victory.wav` — AudioCraft)
**Texte overlay :** « NPS > 9/10 en bêta »
---
## 2:10 2:30 | CTA — Trois façons de jouer
**Cadrage :** 3 cartes flottantes (animation CSS-style) sur fond sombre.
**Cartes :**
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ ANIMATION │ │ LOCATION │ │ KIT │
│ 800 1 500 € │ │ 300 500 €/WE │ │ 3 000 5 000 € │
│ par séance │ │ │ │ achat définitif│
│ │ │ │ │ │
│ [Devis rapide] │ │ [Réserver] │ │ [Acheter] │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
**Audio :** Piano simple et chaleureux
**Texte final :**
```
À partir de 800 € — lelectronrare.fr/zacus
clement@lelectronrare.fr
```
**Dernier plan :** Logo L'Electron Rare + fade to black (0.5s)
---
## Notes de tournage
| Scène | Lieu suggéré | Éclairage | Durée tournage |
|-------|--------------|-----------|----------------|
| Hook (téléphone) | Salon/bureau sombre | LEDs vertes seulement | 15 min |
| Setup time-lapse | Salle de réunion vide | Lumière naturelle | 1h (y compris vrai setup) |
| Puzzles | Salle de test | Éclairage d'ambiance bleu | 2h |
| Split-screen IA | 2 sessions distinctes | Identique des 2 côtés | 30 min chaque |
| Voix clonée | Gros plan BOX-3 | Rétro-éclairage écran | 15 min |
| Victoire | Même salle puzzles | Lumière chaude pour célébration | 30 min |
| CTA | Post-production | N/A (motion design) | — |
**Post-production :**
- Sous-titres FR (auto-générés + correction)
- Musique : pistes AudioCraft générées (`generate_tracks.py`)
- Color grading : tons froids/teal pour lab, chaud/doré pour victoire
- Export : 1080p MP4 H.264 pour web + 4K master
**Approbation requise :** Vérifier que la voix XTTS est perçue comme naturelle sur tous les plans audio avant montage final.
+178
View File
@@ -475,3 +475,181 @@ endings:
key: "endings.many_hints.0"
- text: "Les indices sont là pour aider, et vous les avez bien utilisés !"
key: "endings.many_hints.1"
# ============================================================
# V3 PUZZLE-SPECIFIC HINTS (P1P7)
# ============================================================
P1_SON:
level_1:
- text: "La musique a une mémoire, dit-on. Le professeur en est convaincu. Écoutez bien… puis répétez."
key: "hints.P1_SON.level_1.0"
- text: "Quatre boutons. Quatre notes. Un seul ordre correct. Votre oreille est votre meilleur instrument."
key: "hints.P1_SON.level_1.1"
level_2:
- text: "La séquence commence toujours par le bouton ROUGE. Le reste suit une logique musicale simple."
key: "hints.P1_SON.level_2.0"
- text: "Vous avez entendu la mélodie ? Elle se rejoue si vous attendez 10 secondes. Prenez des notes !"
key: "hints.P1_SON.level_2.1"
level_3:
- text: "Rouge, Bleu, Jaune, Rouge, Vert. Dans cet ordre exact. Appuyez lentement et attendez le bip de confirmation."
key: "hints.P1_SON.level_3.0"
P2_CIRCUIT:
level_1:
- text: "L'électricité aime les chemins fermés. Si la LED ne brille pas… le circuit n'est pas complet."
key: "hints.P2_CIRCUIT.level_1.0"
- text: "Regardez les picots magnétiques. Certains sont ronds, d'autres sont carrés. Ce n'est pas un hasard."
key: "hints.P2_CIRCUIT.level_1.1"
level_2:
- text: "Il faut une résistance entre la pile et la LED. Sans quoi… *explosion dramatique* … enfin, presque."
key: "hints.P2_CIRCUIT.level_2.0"
- text: "Le schéma est inscrit sous le panneau en petits caractères. Cherchez bien. J'y ai mis des heures !"
key: "hints.P2_CIRCUIT.level_2.1"
level_3:
- text: "Pile → fil rouge → résistance 220Ω → LED (longue patte vers la résistance) → fil noir → pile. C'est tout !"
key: "hints.P2_CIRCUIT.level_3.0"
P3_QR:
level_1:
- text: "Six enveloppes. Six secrets. Mais l'ordre… l'ordre est gravé dans les étoiles. Ou dans ce laboratoire."
key: "hints.P3_QR.level_1.0"
- text: "Chaque QR code porte un numéro invisible à l'œil nu. Regardez sous chaque carte."
key: "hints.P3_QR.level_1.1"
level_2:
- text: "L'ordre est: numéros croissants des cachettes. La cachette 1 est toujours visible depuis l'entrée."
key: "hints.P3_QR.level_2.0"
- text: "Commencez par scanner le QR code collé sur la porte d'entrée. C'est le numéro 1."
key: "hints.P3_QR.level_2.1"
level_3:
- text: "Ordre: porte → étagère gauche → tiroir rouge → sous la table → fenêtre → boîte bleue. Scannez avec BOX-3."
key: "hints.P3_QR.level_3.0"
P4_RADIO:
level_1:
- text: "Les ondes radio… fascinantes ! Ma fréquence préférée se trouve quelque part entre 800 et 1800 hertz."
key: "hints.P4_RADIO.level_1.0"
- text: "Quand vous approchez de la bonne fréquence, un petit indicateur s'allume. Chaud… froid… chaud !"
key: "hints.P4_RADIO.level_1.1"
level_2:
- text: "1337. C'est un nombre que les informaticiens connaissent bien. Un nombre légendaire dans les cercles geek."
key: "hints.P4_RADIO.level_2.0"
- text: "La fréquence est un nombre à 4 chiffres. Commençant par 1. Très précis. Tournez doucement."
key: "hints.P4_RADIO.level_2.1"
level_3:
- text: "1337 hertz ! LEET en langage de hacker. Tournez jusqu'à ce que l'écran affiche 1337. Exactement."
key: "hints.P4_RADIO.level_3.0"
P5_MORSE:
level_1:
- text: "Le code morse… inventé en 1836 ! Long-court-long. Tiret-point-tiret. Écoutez le rythme."
key: "hints.P5_MORSE.level_1.0"
- text: "Vous avez une fiche de référence quelque part dans cette salle. Cherchez-la. Je l'y ai laissée exprès."
key: "hints.P5_MORSE.level_1.1"
level_2:
- text: "Le message contient cinq lettres. C'est mon nom… enfin, mon nom de code. Commençant par Z."
key: "hints.P5_MORSE.level_2.0"
- text: "Z=--.. A=.- C=-.-. U=..- S=... Voilà l'alphabet morse pour ces lettres précises."
key: "hints.P5_MORSE.level_2.1"
level_3:
- text: "ZACUS en morse: TIRET-TIRET-POINT-POINT POINT-TIRET TIRET-POINT-TIRET-POINT POINT-POINT-TIRET POINT-POINT-POINT. Tapez sur le télégraphe !"
key: "hints.P5_MORSE.level_3.0"
P6_SYMBOLES:
level_1:
- text: "L'alchimie obéit à une logique ancienne. Terre, Eau, Feu, Air… puis les métaux. Il y a un ordre naturel."
key: "hints.P6_SYMBOLES.level_1.0"
- text: "Chaque symbole a un poids. Les légers en haut, les lourds en bas. Comme dans la nature."
key: "hints.P6_SYMBOLES.level_1.1"
level_2:
- text: "L'ordre suit la table alchimique classique. Elle est affichée dans le couloir sous un cadre doré."
key: "hints.P6_SYMBOLES.level_2.0"
- text: "Commencez par placer les quatre éléments primordiaux dans les coins. Puis remplissez le centre."
key: "hints.P6_SYMBOLES.level_2.1"
level_3:
- text: "Feu (coin haut-gauche) → Eau → Terre → Air → Or → Argent → Mercure → Soufre → Sel → Antimoine → Arsenic → Plomb. De gauche à droite, haut en bas."
key: "hints.P6_SYMBOLES.level_3.0"
P7_COFFRE:
level_1:
- text: "Vous avez tous les fragments ? Chaque puzzle vous a donné un ou deux chiffres. Assemblez-les dans l'ordre des puzzles."
key: "hints.P7_COFFRE.level_1.0"
# ============================================================
# V3 ADAPTATION PHRASES (profiling + dynamic parcours)
# ============================================================
v3_adaptation:
group_tech_detected:
- text: "Hmm. Groupe technique détecté ! Excellente nouvelle. Je vais sortir mes puzzles… avancés."
key: "v3_adaptation.group_tech_detected.0"
- text: "Vous avez résolu le circuit en un temps record. Intéressant ! Passons aux choses sérieuses."
key: "v3_adaptation.group_tech_detected.1"
group_non_tech_detected:
- text: "Ah ! Des artistes, des créatifs. Parfait. J'ai préparé des énigmes qui correspondent à votre sensibilité."
key: "v3_adaptation.group_non_tech_detected.0"
- text: "Le circuit n'était pas votre fort ? Pas grave ! La logique prend mille formes. Voici la vôtre."
key: "v3_adaptation.group_non_tech_detected.1"
group_mixed_detected:
- text: "Groupe éclectique ! Moitié techniciens, moitié artistes. Mon scénario préféré. Tout le monde travaille !"
key: "v3_adaptation.group_mixed_detected.0"
bonus_puzzle_added:
- text: "Impressionnant ! Vous allez trop vite. J'ai décidé d'ajouter… une petite surprise."
key: "v3_adaptation.bonus_puzzle_added.0"
- text: "Trop facile ? Très bien. Voici un défi supplémentaire que je réserve aux meilleurs groupes."
key: "v3_adaptation.bonus_puzzle_added.1"
puzzle_skipped:
- text: "Hmm. Le temps passe. Je vais vous faire grâce de cette épreuve. Pour aujourd'hui."
key: "v3_adaptation.puzzle_skipped.0"
- text: "Je saute une étape. Non par pitié — mais parce que le temps est une expérience en soi."
key: "v3_adaptation.puzzle_skipped.1"
duration_warning:
- text: "Attention ! Vous avez consommé 80% du temps imparti. Le Professeur s'impatiente légèrement."
key: "v3_adaptation.duration_warning.0"
# ============================================================
# V3 AMBIANCE (intro / outro per profile type)
# ============================================================
v3_ambiance:
intro:
- text: "Bienvenue dans mon laboratoire… portable. Aujourd'hui, c'est VOUS qui êtes l'expérience."
key: "v3_ambiance.intro.0"
- text: "Je suis le Professeur Zacus. Scientifique, inventeur, et… testeur de groupes humains depuis 1987."
key: "v3_ambiance.intro.1"
- text: "Vous avez soixante minutes. Soixante petites minutes pour prouver que votre espèce mérite de continuer."
key: "v3_ambiance.intro.2"
outro_success:
- text: "Extraordinaire ! Vous avez passé mon test. Je dois admettre… je suis impressionné. Légèrement."
key: "v3_ambiance.outro_success.0"
- text: "Félicitations ! Le diplôme du Professeur Zacus vous est accordé. Avec mention… passable."
key: "v3_ambiance.outro_success.1"
outro_success_tech:
- text: "Groupe technique remarquable ! Vous avez décodé chaque signal avec une précision chirurgicale. Respect."
key: "v3_ambiance.outro_success_tech.0"
- text: "Les ingénieurs du futur sont parmi nous. J'en suis certain maintenant. Bravo !"
key: "v3_ambiance.outro_success_tech.1"
outro_success_non_tech:
- text: "Qui a dit que la technologie était réservée aux ingénieurs ? Vous venez de prouver le contraire !"
key: "v3_ambiance.outro_success_non_tech.0"
- text: "L'intuition et la créativité ont triomphé. Le Professeur Zacus est bluffé. Vraiment."
key: "v3_ambiance.outro_success_non_tech.1"
outro_success_mixed:
- text: "Un groupe mixte en parfaite harmonie. Techniciens et créatifs côte à côte. C'est ce qui sauve le monde."
key: "v3_ambiance.outro_success_mixed.0"
outro_partial:
- text: "Hmm. Vous n'avez pas tout résolu. Mais vous avez essayé. C'est… acceptable. Pour un humain."
key: "v3_ambiance.outro_partial.0"
outro_timeout:
- text: "Le temps est écoulé ! L'expérience se termine. Ne vous inquiétez pas — j'ai noté tout ce que vous avez fait."
key: "v3_ambiance.outro_timeout.0"
- text: "Soixante minutes, et vous avez donné tout ce que vous aviez. C'est tout ce que je demandais."
key: "v3_ambiance.outro_timeout.1"
+120
View File
@@ -0,0 +1,120 @@
# game/scenarios/v3_constants.yaml
# V3 runtime constants — NPC engine, scoring, profiling, hint escalation.
# All thresholds centralized here. Referenced by npc_engine.cpp and
# the adaptive parcours logic in compile_runtime3.py.
# ============================================================
# NPC ENGINE THRESHOLDS
# ============================================================
npc:
# Adaptive mood triggers (% of expected puzzle duration elapsed)
fast_threshold_pct: 60 # < 60% elapsed → mood IMPRESSED, add bonus puzzle
slow_threshold_pct: 130 # > 130% elapsed → mood WORRIED, proactive hint
critical_threshold_pct: 160 # > 160% elapsed + hints maxed → skip_to_next
# Stuck detection
stuck_timeout_seconds: 120 # no progress for 2 min → trigger hint offer
proactive_hint_interval_seconds: 90 # re-offer if still stuck after 90s
max_hint_level: 3
# Time warning trigger
duration_warning_pct: 80 # warn when 80% of target_minutes consumed
# ============================================================
# SCORING CONSTANTS
# ============================================================
scoring:
base: 1000
hint_penalty: 50 # per hint given (any level)
time_penalty_per_minute: 10 # each full minute over target_minutes
bonus_fast: 200 # finished before fast_threshold_pct of target
bonus_no_hints: 300 # zero hints used across entire game
bonus_perfect_morse: 100 # P5 decoded on first attempt (no retries)
max_score: 1600
# ============================================================
# PROFILING PHASE THRESHOLDS
# ============================================================
profiling:
fast_seconds: 180 # < 3 min on a profiling puzzle → "fast" tag
slow_seconds: 480 # > 8 min on a profiling puzzle → "slow" tag
# "normal" = between fast_seconds and slow_seconds
tech_time_seconds: 180 # PROFILING_TECH_TIME: P2_CIRCUIT fast threshold
# ============================================================
# TARGET DURATIONS PER MODE
# ============================================================
duration_modes:
mode_30:
target_minutes: 30
profiling_minutes: 5
adaptive_minutes: 15
climax_minutes: 5
outro_minutes: 5
puzzles_required: 4
mode_45:
target_minutes: 45
profiling_minutes: 8
adaptive_minutes: 25
climax_minutes: 7
outro_minutes: 5
puzzles_required: 5
mode_60:
target_minutes: 60
profiling_minutes: 10
adaptive_minutes: 35
climax_minutes: 10
outro_minutes: 5
puzzles_required: 6
mode_90:
target_minutes: 90
profiling_minutes: 10
adaptive_minutes: 60
climax_minutes: 10
outro_minutes: 10
puzzles_required: 7
# ============================================================
# HINT ESCALATION TIMERS
# ============================================================
hint_escalation:
first_hint_after_seconds: 120 # level_1 offered after 2 min stuck
second_hint_after_seconds: 240 # level_2 offered after 4 min stuck
third_hint_after_seconds: 360 # level_3 offered after 6 min stuck
auto_skip_after_seconds: 540 # auto-skip after 9 min (if game_master enabled)
# ============================================================
# PER-PUZZLE EXPECTED DURATIONS AND LIMITS
# ============================================================
puzzles:
P1_SON:
expected_s: 300
max_hints: 3
phase: profiling
P2_CIRCUIT:
expected_s: 360
max_hints: 3
phase: profiling
P3_QR:
expected_s: 240
max_hints: 3
phase: adaptive
P4_RADIO:
expected_s: 300
max_hints: 3
phase: adaptive
P5_MORSE:
expected_s: 420
max_hints: 3
phase: adaptive
P6_SYMBOLES:
expected_s: 360
max_hints: 3
phase: adaptive
P7_COFFRE:
expected_s: 300
max_hints: 1
phase: climax
+313
View File
@@ -0,0 +1,313 @@
# game/scenarios/zacus_v3_complete.yaml
# V3 Complete — 7 puzzle objects, adaptive NPC, ESP-NOW mesh
id: zacus_v3_complete
version: "3.1"
title: "Le Mystère du Professeur Zacus — V3 Complete"
theme: "Escape room mobile adaptatif, laboratoire du Professeur"
players:
min: 4
max: 15
ages: "8+ ans (corporate et enfants)"
duration:
target_minutes: 60 # game master configures at boot (3090)
min_minutes: 30
max_minutes: 90
profiling_minutes: 10
climax_minutes: 10
outro_minutes: 5
# ============================================================
# HARDWARE TOPOLOGY
# ============================================================
hardware:
hub:
- id: RTC_PHONE
type: custom_esp32_slic
role: voice_hub
comm: esp_now
- id: BOX_3
type: esp32s3_box3
role: orchestrator_display_camera
comm: esp_now_master
puzzles:
- id: P1_SON
name: "Séquence Sonore"
type: esp32s3_devkit
profile: BOTH
phase: profiling
- id: P2_CIRCUIT
name: "Circuit LED"
type: esp32_devkit
profile: TECH
phase: profiling
- id: P3_QR
name: "QR Treasure"
type: box3_camera
profile: BOTH
phase: adaptive
- id: P4_RADIO
name: "Fréquence Radio"
type: esp32_devkit
profile: TECH
phase: adaptive
- id: P5_MORSE
name: "Code Morse"
type: esp32_devkit
profile: MIXED
phase: adaptive
- id: P6_SYMBOLES
name: "Symboles Alchimiques"
type: esp32_devkit
profile: NON_TECH
phase: adaptive
- id: P7_COFFRE
name: "Coffre Final"
type: esp32_devkit
profile: BOTH
phase: climax
# ============================================================
# SCORING SYSTEM
# ============================================================
scoring:
base_score: 1000
time_penalty_per_minute: 10 # deducted after target_minutes
hint_penalty: 50 # per hint given
bonus_fast_completion: 200 # if finished < 80% of target
bonus_no_hints: 300 # if zero hints used
bonus_perfect_morse: 100 # P5 decoded on first attempt
max_score: 1600
# ============================================================
# PHASES
# ============================================================
phases:
intro:
id: PHASE_INTRO
duration_minutes: 5
trigger: game_start
npc_audio: "intro/welcome"
description: >
Phone rings. Professor introduces himself. Players learn
the rules through the Professor's monologue.
profiling:
id: PHASE_PROFILING
duration_minutes: 10
puzzles: [P1_SON, P2_CIRCUIT]
expected_time_each_seconds: 300
npc_audio: "profiling/start"
profiling_thresholds:
fast_seconds: 180 # < 3 min = fast on this puzzle
slow_seconds: 480 # > 8 min = slow on this puzzle
group_classification:
TECH:
condition: "P2_fast AND (P1_normal OR P1_slow)"
parcours: [P2_CIRCUIT, P4_RADIO, P5_MORSE, P3_QR, P7_COFFRE]
NON_TECH:
condition: "P1_fast AND (P2_normal OR P2_slow)"
parcours: [P1_SON, P6_SYMBOLES, P3_QR, P5_MORSE_LIGHT, P7_COFFRE]
MIXED:
condition: "default"
parcours: [P1_SON, P2_CIRCUIT, P3_QR, P5_MORSE, P6_SYMBOLES, P7_COFFRE]
adaptive:
id: PHASE_ADAPTIVE
duration_minutes: 20 # to 60, dynamically adjusted
fast_threshold_pct: 60
slow_threshold_pct: 130
max_bonus_puzzles: 2
npc_mood_rules:
- condition: "pct < 60"
mood: IMPRESSED
action: add_bonus_puzzle
- condition: "pct > 130"
mood: WORRIED
action: proactive_hint
- condition: "pct > 160 AND hints_maxed"
mood: WORRIED
action: skip_to_next
climax:
id: PHASE_CLIMAX
duration_minutes: 10
puzzle: P7_COFFRE
requires_all_codes: true
description: >
Final code assembled from all previous puzzle solutions.
All players must cooperate to enter 8-digit code.
code_assembly:
P1_SON: digits 1-2
P2_CIRCUIT: digit 3
P4_RADIO: digit 4
P5_MORSE: digit 5
P6_SYMBOLES: digits 6-7
P3_QR: digit 8
outro:
id: PHASE_OUTRO
duration_minutes: 5
npc_audio: "outro/verdict"
outputs:
- diplome_pdf: true
- stats_display: true # score, time, hints, rating
# ============================================================
# PUZZLE DEFINITIONS (timing, expected durations)
# ============================================================
puzzles:
P1_SON:
id: P1_SON
name: "Séquence Sonore"
object: "Boîte avec haut-parleur + 4 boutons arcade"
interaction: "Écouter la mélodie, reproduire la séquence"
expected_duration_seconds: 300
max_hints: 3
code_contribution: "digits 1-2 (index de la séquence correcte, base-10)"
difficulty_variants:
TECH: sequence_length_5
NON_TECH: sequence_length_3
MIXED: sequence_length_4
hint_keys:
level_1: "hints.P1_SON.level_1"
level_2: "hints.P1_SON.level_2"
level_3: "hints.P1_SON.level_3"
P2_CIRCUIT:
id: P2_CIRCUIT
name: "Circuit LED"
object: "Breadboard magnétique 60×40cm pliable"
interaction: "Placer les composants pour allumer la LED"
expected_duration_seconds: 360
max_hints: 3
code_contribution: "digit 3 (numéro de la configuration validée, 1-9)"
difficulty_variants:
TECH: full_circuit_5_components
NON_TECH: guided_circuit_3_components
hint_keys:
level_1: "hints.P2_CIRCUIT.level_1"
level_2: "hints.P2_CIRCUIT.level_2"
level_3: "hints.P2_CIRCUIT.level_3"
P3_QR:
id: P3_QR
name: "QR Treasure"
object: "6 QR codes plastifiés A5 cachés dans la salle"
interaction: "Scanner dans l'ordre correct via BOX-3"
expected_duration_seconds: 240
max_hints: 3
code_contribution: "digit 8 (checksum des QR scannés)"
hint_keys:
level_1: "hints.P3_QR.level_1"
level_2: "hints.P3_QR.level_2"
level_3: "hints.P3_QR.level_3"
P4_RADIO:
id: P4_RADIO
name: "Fréquence Radio"
object: "Boîtier radio rétro 3D imprimé"
interaction: "Trouver la bonne fréquence au codeur rotatif"
expected_duration_seconds: 300
max_hints: 3
code_contribution: "digit 4 (fréquence cible encodée sur 1 chiffre)"
target_frequency_hz: 1337
frequency_range: [800, 1800]
tolerance_hz: 10
hint_keys:
level_1: "hints.P4_RADIO.level_1"
level_2: "hints.P4_RADIO.level_2"
level_3: "hints.P4_RADIO.level_3"
P5_MORSE:
id: P5_MORSE
name: "Code Morse"
object: "Télégraphe en bois + laiton"
interaction: "Décoder le message morse du Professeur"
expected_duration_seconds: 420
max_hints: 3
code_contribution: "digit 5 (dernier chiffre du message morse)"
morse_message: "ZACUS" # Z=--.. A=.- C=-.-. U=..- S=...
difficulty_variants:
TECH: full_morse_decode
NON_TECH: morse_light_mode # visual light pulses instead of sound
hint_keys:
level_1: "hints.P5_MORSE.level_1"
level_2: "hints.P5_MORSE.level_2"
level_3: "hints.P5_MORSE.level_3"
P6_SYMBOLES:
id: P6_SYMBOLES
name: "Symboles Alchimiques"
object: "Tablette en bois découpée laser 30×20cm"
interaction: "Placer les 12 symboles NFC dans l'ordre correct"
expected_duration_seconds: 360
max_hints: 3
code_contribution: "digits 6-7 (index de la configuration, 10-99)"
nfc_tags: 12
correct_order: [7, 2, 11, 4, 9, 1, 8, 3, 12, 6, 10, 5]
hint_keys:
level_1: "hints.P6_SYMBOLES.level_1"
level_2: "hints.P6_SYMBOLES.level_2"
level_3: "hints.P6_SYMBOLES.level_3"
P7_COFFRE:
id: P7_COFFRE
name: "Coffre Final"
object: "Coffre en bois 25×20×15cm avec verrou électronique"
interaction: "Entrer le code 8 chiffres assemblé des puzzles précédents"
expected_duration_seconds: 300
max_hints: 1 # minimal hints for final puzzle
hint_keys:
level_1: "hints.P7_COFFRE.level_1"
# ============================================================
# DURATION MODES (game master selects at boot)
# ============================================================
duration_modes:
mode_30:
target_minutes: 30
profiling_minutes: 5
adaptive_minutes: 15
climax_minutes: 5
outro_minutes: 5
puzzles_required: 4 # skip lowest-priority adaptive puzzles
mode_45:
target_minutes: 45
profiling_minutes: 8
adaptive_minutes: 25
climax_minutes: 7
outro_minutes: 5
puzzles_required: 5
mode_60:
target_minutes: 60
profiling_minutes: 10
adaptive_minutes: 35
climax_minutes: 10
outro_minutes: 5
puzzles_required: 6
mode_90:
target_minutes: 90
profiling_minutes: 10
adaptive_minutes: 60
climax_minutes: 10
outro_minutes: 10
puzzles_required: 7 # all puzzles + bonus possible
# ============================================================
# HINT ESCALATION TIMERS
# ============================================================
hint_escalation:
first_hint_after_seconds: 120 # offer level_1 after 2 min stuck
second_hint_after_seconds: 240 # offer level_2 after 4 min stuck
third_hint_after_seconds: 360 # offer level_3 after 6 min stuck
auto_skip_after_seconds: 540 # skip puzzle after 9 min (if enabled)
proactive_hint_interval_seconds: 90
+30
View File
@@ -0,0 +1,30 @@
# docker-compose.audiocraft.yml — AudioCraft MusicGen deployment on KXKM-AI
# Run: docker compose -f docker-compose.audiocraft.yml run audiocraft
# Output: ./output/*.wav (mounted from host)
version: "3.9"
services:
audiocraft:
image: pytorch/pytorch:2.2.0-cuda12.1-cudnn8-runtime
container_name: audiocraft_gen
working_dir: /workspace
command: python3 generate_tracks.py
volumes:
- ./:/workspace
- audiocraft_output:/workspace/output
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
environment:
- PYTHONUNBUFFERED=1
- HF_HOME=/workspace/.cache/huggingface
# pip install audiocraft is run at container start via entrypoint below
entrypoint: >
bash -c "pip install -q audiocraft torchaudio && python3 generate_tracks.py"
volumes:
audiocraft_output:
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""
generate_tracks.py — Generate 6 ambient audio tracks using AudioCraft MusicGen.
Run on KXKM-AI (RTX 4090) via docker-compose.audiocraft.yml or directly.
Output: output/{track_name}.wav (mono, 32kHz — AudioCraft native sample rate)
Usage:
python3 generate_tracks.py [--dry-run] [--track lab_ambiance]
Notes:
- AudioCraft MusicGen max generation duration per call is 30s.
- Tracks longer than 30s are assembled from multiple segments.
- Model: facebook/musicgen-medium (1.5B params, good quality/speed balance).
- RTX 4090 generates ~30s in ~10s wall time.
"""
import argparse
import pathlib
import sys
import time
OUTPUT_DIR = pathlib.Path("output")
TRACKS = [
{
"name": "lab_ambiance.wav",
"duration": 30,
"segments": 1,
"prompt": (
"Laboratory ambient sound, machines humming, electronic beeps, "
"ventilation fan, subtle mechanical sounds, science lab, mysterious, "
"loopable, no melody, background ambiance"
),
},
{
"name": "tension_rising.wav",
"duration": 30, # generate 30s base; extend in post-processing for 5min loop
"segments": 1,
"prompt": (
"Dramatic tension building music, suspenseful, orchestral, "
"slow crescendo, escape room atmosphere, mysterious puzzle solving, "
"cinematic, no lyrics, dark ambient"
),
},
{
"name": "victory.wav",
"duration": 30,
"segments": 1,
"prompt": (
"Victory fanfare, joyful celebration music, brass orchestra, "
"applause, triumphant, escape room win, uplifting, energetic, "
"short stinger"
),
},
{
"name": "failure.wav",
"duration": 15,
"segments": 1,
"prompt": (
"Failure buzzer sound effect, trombone descending, game over, "
"humorous sad tuba, cartoon fail sound, short, comedic"
),
},
{
"name": "thinking.wav",
"duration": 30,
"segments": 1,
"prompt": (
"Subtle suspense music for thinking, minimal, ambient, "
"slow piano, light electronic, puzzle solving concentration, "
"loopable, calm but mysterious, soft"
),
},
{
"name": "transition.wav",
"duration": 10,
"segments": 1,
"prompt": (
"Scene transition sound effect, swoosh, magical glitter, "
"short stinger, puzzle reveal, sparkle effect, 10 seconds, "
"ascending chime"
),
},
]
def generate_track(model, description: str, duration_s: int, filename: str) -> pathlib.Path:
"""Generate a single track and save as WAV."""
import torch
import torchaudio
print(f" Generating: {filename} ({duration_s}s)")
print(f" Prompt: {description[:80]}...")
# Cap duration at 30s per AudioCraft limit
actual_duration = min(duration_s, 30)
model.set_generation_params(duration=actual_duration)
t0 = time.time()
with torch.no_grad():
wav = model.generate(
descriptions=[description],
progress=True,
)
elapsed = time.time() - t0
print(f" Generated in {elapsed:.1f}s")
out_path = OUTPUT_DIR / filename
# AudioCraft returns tensor [batch, channels, samples] at model.sample_rate
torchaudio.save(
str(out_path),
wav[0].cpu(),
model.sample_rate,
format="wav",
)
print(f" Saved: {out_path} ({out_path.stat().st_size // 1024} KB)")
return out_path
def main():
parser = argparse.ArgumentParser(description="Generate ambient tracks for Zacus V3")
parser.add_argument("--dry-run", action="store_true", help="Print track list without generating")
parser.add_argument("--track", metavar="NAME", help="Generate only the specified track by name stem")
parser.add_argument("--model", default="facebook/musicgen-medium", help="HuggingFace model ID")
args = parser.parse_args()
OUTPUT_DIR.mkdir(exist_ok=True)
selected = TRACKS
if args.track:
selected = [t for t in TRACKS if pathlib.Path(t["name"]).stem == args.track]
if not selected:
print(f"ERROR: track '{args.track}' not found. Available: {[t['name'] for t in TRACKS]}")
return 1
if args.dry_run:
print("=== DRY RUN — tracks that would be generated ===")
for t in selected:
print(f" {t['name']:30s} {t['duration']:3d}s {t['prompt'][:60]}...")
print(f"\nTotal: {len(selected)} track(s)")
return 0
print(f"Loading model: {args.model}")
from audiocraft.models import MusicGen
model = MusicGen.get_pretrained(args.model)
print(f"Model loaded: {args.model}\n")
results = []
for i, track in enumerate(selected, 1):
print(f"[{i}/{len(selected)}] {track['name']}")
out = generate_track(
model,
description=track["prompt"],
duration_s=track["duration"],
filename=track["name"],
)
results.append(out)
print()
print("=" * 50)
print(f"All {len(results)} track(s) generated successfully.")
print(f"Output directory: {OUTPUT_DIR.absolute()}")
for r in results:
print(f" {r.name:30s} {r.stat().st_size // 1024} KB")
return 0
if __name__ == "__main__":
sys.exit(main())
+85
View File
@@ -0,0 +1,85 @@
# docker-compose.xtts.yml — XTTS-v2 deployment on KXKM-AI (RTX 4090)
# OpenAI-compatible /v1/audio/speech endpoint with Professor Zacus voice clone.
#
# Deploy:
# scp tools/tts/docker-compose.xtts.yml kxkm@kxkm-ai:~/zacus-tts/
# scp voices/zacus_reference.wav kxkm@kxkm-ai:~/zacus-tts/voices/
# ssh kxkm@kxkm-ai "cd ~/zacus-tts && docker compose -f docker-compose.xtts.yml up -d"
#
# Test:
# curl -X POST http://kxkm-ai:5002/v1/audio/speech \
# -H "Content-Type: application/json" \
# -d '{"input": "Bienvenue dans mon laboratoire portable."}' \
# --output /tmp/test.wav
version: "3.9"
services:
xtts:
image: ghcr.io/coqui-ai/tts:latest
container_name: xtts_zacus
restart: unless-stopped
environment:
- COQUI_TOS_AGREED=1
command: >
python3 -c "
from TTS.api import TTS
import uvicorn
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse, JSONResponse
import io, soundfile as sf, numpy as np, sys
print('Loading XTTS-v2 model...', flush=True)
tts = TTS('tts_models/multilingual/multi-dataset/xtts_v2', gpu=True)
app = FastAPI(title='XTTS-v2 Zacus Voice Clone')
@app.post('/v1/audio/speech')
async def synthesize(req: Request):
body = await req.json()
text = body.get('input', '')
if not text:
return JSONResponse({'error': 'input required'}, status_code=400)
if len(text) > 500:
return JSONResponse({'error': 'input too long (max 500 chars)'}, status_code=400)
wav = tts.tts(
text=text,
speaker_wav='/voices/zacus_reference.wav',
language='fr'
)
buf = io.BytesIO()
sf.write(buf, np.array(wav), 24000, format='wav')
buf.seek(0)
return StreamingResponse(
buf,
media_type='audio/wav',
headers={'Content-Disposition': 'attachment; filename=speech.wav'}
)
@app.get('/health')
def health():
return {'status': 'ok', 'model': 'xtts_v2', 'voice': 'zacus'}
print('XTTS-v2 ready on :5002', flush=True)
uvicorn.run(app, host='0.0.0.0', port=5002)
"
ports:
- "5002:5002"
volumes:
- ./voices:/voices:ro # mount voice reference samples (read-only)
- xtts_cache:/root/.local/share/tts
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:5002/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 120s # model loading takes ~60s on first start
volumes:
xtts_cache:
# Persists downloaded XTTS-v2 model weights (~1.8 GB) across container restarts
+197
View File
@@ -0,0 +1,197 @@
#!/usr/bin/env python3
"""
generate_xtts_pool.py — Pre-generate all NPC phrases via XTTS-v2 (GPU voice clone).
Reads game/scenarios/npc_phrases.yaml, calls XTTS-v2 on kxkm-ai, saves WAV files
to hotline_tts/ directory, updates hotline_tts/manifest.json.
Idempotent: skips already-generated files unless --force is passed.
Usage:
python3 tools/tts/generate_xtts_pool.py [--dry-run] [--force] [--host kxkm-ai:5002]
Requirements:
pip install pyyaml requests
"""
import argparse
import json
import pathlib
import sys
import time
from typing import Any
import requests
import yaml
# Paths relative to project root (run from repo root)
PHRASES_YAML = pathlib.Path("game/scenarios/npc_phrases.yaml")
OUTPUT_DIR = pathlib.Path("hotline_tts")
MANIFEST = OUTPUT_DIR / "manifest.json"
DEFAULT_HOST = "kxkm-ai:5002"
# HTTP request settings
REQUEST_TIMEOUT_S = 30
RETRY_COUNT = 2
RETRY_DELAY_S = 3
def flatten_phrases(data: dict[str, Any], prefix: str = "") -> list[dict[str, str]]:
"""Recursively extract all phrase entries with their keys and text.
Handles both list-of-dicts (with 'text' + 'key') and nested dicts.
Returns list of {"key": str, "text": str} dicts.
"""
phrases: list[dict[str, str]] = []
for k, v in data.items():
path = f"{prefix}.{k}" if prefix else k
if isinstance(v, list):
for i, item in enumerate(v):
if isinstance(item, dict) and "text" in item:
phrases.append({
"key": item.get("key", f"{path}.{i}"),
"text": item["text"],
})
elif isinstance(v, dict):
phrases.extend(flatten_phrases(v, path))
return phrases
def key_to_path(key: str) -> pathlib.Path:
"""Convert phrase key to output file path.
Example: hints.P1_SON.level_1.0 → hotline_tts/hints/P1_SON/level_1/0.wav
"""
parts = key.replace(".", "/")
return OUTPUT_DIR / f"{parts}.wav"
def synthesize(text: str, host: str) -> bytes:
"""Call XTTS-v2 /v1/audio/speech endpoint, return raw WAV bytes.
Raises requests.HTTPError on non-2xx response.
"""
url = f"http://{host}/v1/audio/speech"
resp = requests.post(
url,
json={"input": text},
timeout=REQUEST_TIMEOUT_S,
)
resp.raise_for_status()
return resp.content
def check_xtts_health(host: str) -> bool:
"""Return True if XTTS-v2 service is reachable and healthy."""
try:
resp = requests.get(f"http://{host}/health", timeout=5)
return resp.status_code == 200
except Exception:
return False
def main() -> int:
parser = argparse.ArgumentParser(
description="Pre-generate NPC TTS pool via XTTS-v2"
)
parser.add_argument("--dry-run", action="store_true",
help="Parse phrases and print plan without calling XTTS")
parser.add_argument("--force", action="store_true",
help="Re-generate files that already exist")
parser.add_argument("--host", default=DEFAULT_HOST,
help=f"XTTS-v2 host:port (default: {DEFAULT_HOST})")
parser.add_argument("--phrases-yaml", default=str(PHRASES_YAML),
help="Path to npc_phrases.yaml")
args = parser.parse_args()
phrases_path = pathlib.Path(args.phrases_yaml)
if not phrases_path.exists():
print(f"ERROR: phrases file not found: {phrases_path}", file=sys.stderr)
return 1
# Parse phrase bank
with open(phrases_path, encoding="utf-8") as f:
data = yaml.safe_load(f)
phrases = flatten_phrases(data)
print(f"Phrase bank: {len(phrases)} phrases found in {phrases_path}")
# Health check before starting (skip in dry-run)
if not args.dry_run:
print(f"Checking XTTS-v2 at {args.host}...", end=" ", flush=True)
if not check_xtts_health(args.host):
print("UNREACHABLE")
print(f"ERROR: XTTS-v2 not reachable at http://{args.host}/health",
file=sys.stderr)
print("Start the service first: docker compose -f tools/tts/docker-compose.xtts.yml up -d",
file=sys.stderr)
return 1
print("OK")
# Load existing manifest
manifest: dict[str, str] = {}
if MANIFEST.exists():
with open(MANIFEST, encoding="utf-8") as f:
manifest = json.load(f)
generated = skipped = errors = 0
for ph in phrases:
key = ph["key"]
text = ph["text"]
path = key_to_path(key)
# Skip if already generated (unless --force)
if not args.force and path.exists():
skipped += 1
manifest[key] = str(path)
continue
label = "[DRY]" if args.dry_run else "[GEN]"
preview = text[:70] + "..." if len(text) > 70 else text
print(f" {label} {key}")
print(f" {preview}")
if args.dry_run:
generated += 1
continue
# Synthesize with retries
wav_bytes: bytes | None = None
for attempt in range(1, RETRY_COUNT + 1):
try:
wav_bytes = synthesize(text, args.host)
break
except Exception as exc:
print(f" WARNING attempt {attempt}/{RETRY_COUNT}: {exc}",
file=sys.stderr)
if attempt < RETRY_COUNT:
time.sleep(RETRY_DELAY_S)
if wav_bytes is None:
print(f" ERROR: all retries failed for key={key}", file=sys.stderr)
errors += 1
continue
# Write file
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(wav_bytes)
manifest[key] = str(path)
generated += 1
except OSError as exc:
print(f" ERROR writing {path}: {exc}", file=sys.stderr)
errors += 1
# Save manifest
if not args.dry_run:
MANIFEST.parent.mkdir(parents=True, exist_ok=True)
with open(MANIFEST, "w", encoding="utf-8") as f:
json.dump(manifest, f, indent=2, ensure_ascii=False)
print(f"\nManifest written: {MANIFEST} ({len(manifest)} entries)")
print(f"\nResult: {generated} generated, {skipped} skipped, {errors} errors")
return 0 if errors == 0 else 1
if __name__ == "__main__":
sys.exit(main())