# Cadence Tabata — format de lien de séance
> Une séance d'entraînement complète est encodée dans une URL. Aucune API, aucun
> compte, aucune base de données. Ouvrir le lien affiche la séance prête à démarrer.
> Ce document est la spécification complète. Il est autosuffisant : tout ce qui est
> nécessaire pour générer un lien valide est ici.
Public : agents et outils qui génèrent des liens vers cette application.
Version du format : 1. Stable.
---
## 1. GRAMMAIRE
Un seul paramètre de query string, `w`.
?w=~~~[~...]
= ::
= "s" → secondes (l'exercice se termine tout seul)
| "x" → répétitions (l'exercice attend un appui manuel)
Séparateurs : `~` entre les segments, `:` entre les champs d'un exercice.
Minimum 4 segments : nom, rounds, repos_round, et au moins un exercice.
Aucune limite haute sur le nombre d'exercices.
Déroulé résultant : pour chaque round, les exercices sont enchaînés dans l'ordre,
chacun suivi de son repos ; `repos_round` s'intercale entre deux rounds.
---
## 2. RÈGLE LA PLUS IMPORTANTE
Le suffixe `x` est ce qui distingue répétitions et secondes. Il n'est PAS optionnel.
Pompes:15x:20 → 15 RÉPÉTITIONS, puis 20 s de repos ✅
Pompes:15:20 → 15 SECONDES, puis 20 s de repos ❌ si vous vouliez des reps
C'est l'erreur la plus fréquente. Vérifiez chaque exercice en répétitions.
Différence de comportement :
- `30s` : décompte automatique, annonce vocale à 30/20/10 s restantes,
bips sur les 3 dernières secondes, passage auto au repos.
- `15x` : PAS de décompte. Le chrono compte vers le haut et attend indéfiniment
que l'utilisateur appuie sur « SUIVANT ». Aucune durée n'est imposée.
---
## 3. ENCODAGE
À l'intérieur d'un nom (nom de séance ou nom d'exercice), échapper :
~ → %7E (sinon interprété comme séparateur de segment)
: → %3A (sinon interprété comme séparateur de champ)
Les séparateurs `~` et `:` que VOUS insérez entre les segments restent bruts.
Algorithme d'échappement d'un nom :
1. Appliquer un encodage URL composant standard (encodeURIComponent / quote(safe="")).
2. Remplacer les `~` restants par `%7E` — la plupart des implémentations
considèrent le tilde comme non réservé et ne l'encodent pas.
3. Optionnel, pour la lisibilité : remplacer `%20` par `+`.
Accents, emoji, `&`, `#`, `%`, `=`, `+` : gérés par l'encodage URL standard de l'étape 1.
---
## 4. CHAMPS ET BORNES
Les valeurs hors bornes sont ramenées dans l'intervalle, jamais rejetées.
| Champ | Position | Bornes | Absent / invalide |
|-----------------|-------------------|-------------|------------------------|
| nom | segment 1 | texte libre | `Workout` |
| rounds | segment 2 | 1 – 99 | 1 |
| repos_round | segment 3 | 0 – 900 s | 0 |
| nom_exercice | exercice, champ 1 | texte libre | exercice ignoré |
| durée (`30s`) | exercice, champ 2 | 1 – 999 s | 20s |
| reps (`15x`) | exercice, champ 2 | 1 – 999 | 20x (reste en reps) |
| repos | exercice, champ 3 | 0 – 999 s | 0 |
Tolérances acceptées :
Ex:30:5 → 30 secondes, 5 s de repos (le "s" est optionnel)
Ex:15X:5 → 15 répétitions (le "x" est insensible à la casse)
Ex:30s → 30 secondes, 0 s de repos (repos omis)
Ex → 20 secondes, 0 s de repos (tout omis)
Ex:0x:5 → 20 RÉPÉTITIONS (piège : 0 est invalide, la valeur
retombe sur 20 ; le suffixe x reste)
---
## 5. ALGORITHME DE GÉNÉRATION
fonction construire_lien(base, nom, rounds, repos_round, exercices):
segments = [échapper(nom), rounds, repos_round]
pour chaque ex dans exercices:
si ex a des répétitions:
durée = ex.reps + "x"
sinon:
durée = ex.secondes + "s"
segments.ajouter(échapper(ex.nom) + ":" + durée + ":" + (ex.repos ou 0))
retourner base + "?w=" + joindre(segments, "~")
Implémentation JavaScript de référence :
const enc = s => encodeURIComponent(String(s))
.replace(/~/g, '%7E')
.replace(/%20/g, '+');
function buildWorkoutUrl(baseUrl, workout) {
const parts = [enc(workout.name), workout.rounds, workout.roundRest];
for (const ex of workout.exercises) {
const dur = ex.reps ? `${ex.reps}x` : `${ex.seconds}s`;
parts.push(`${enc(ex.name)}:${dur}:${ex.rest ?? 0}`);
}
return `${baseUrl}?w=${parts.join('~')}`;
}
Implémentation Python de référence :
from urllib.parse import quote
def enc(s):
return quote(str(s), safe="").replace("~", "%7E").replace("%20", "+")
def build_workout_url(base_url, name, rounds, round_rest, exercises):
parts = [enc(name), str(rounds), str(round_rest)]
for ex in exercises:
dur = f"{ex['reps']}x" if ex.get("reps") else f"{ex['seconds']}s"
parts.append(f"{enc(ex['name'])}:{dur}:{ex.get('rest', 0)}")
return f"{base_url}?w=" + "~".join(parts)
---
## 6. EXEMPLES VÉRIFIÉS
Chaque exemple ci-dessous a été décodé par l'implémentation réelle de l'application.
EX1 — Tabata classique, uniquement au temps
?w=Tabata+Classique~8~60~Squats:20s:10~Pompes:20s:10~Burpees:20s:10
→ « Tabata Classique », 8 rounds, 60 s entre rounds
Squats 20 s (+10 s) | Pompes 20 s (+10 s) | Burpees 20 s (+10 s)
EX2 — Mixte temps et répétitions
?w=Full+Body~3~45~Crunchs:30s:10~Push-Ups+Classic:15x:20~Skater+Jumps:45s:15
→ « Full Body », 3 rounds, 45 s entre rounds
Crunchs 30 s (+10 s) | Push-Ups Classic 15 reps (+20 s) | Skater Jumps 45 s (+15 s)
EX3 — Uniquement des répétitions
?w=Force+Haut+du+Corps~4~90~Pull-ups:8x:30~Dips:12x:30~Pike+Push-ups:15x:45
→ « Force Haut du Corps », 4 rounds, 90 s entre rounds
Pull-ups 8 reps (+30 s) | Dips 12 reps (+30 s) | Pike Push-ups 15 reps (+45 s)
EX4 — Nom contenant des caractères à échapper
?w=S%C3%A9ance+%C2%AB+Jambes+%C2%BB+100%25~2~30~D%C3%A9velopp%C3%A9+couch%C3%A9+%7E+tempo+2%3A1:12x:60~Fentes+arri%C3%A8re:40s:20
→ « Séance « Jambes » 100% », 2 rounds, 30 s entre rounds
« Développé couché ~ tempo 2:1 » 12 reps (+60 s) | « Fentes arrière » 40 s (+20 s)
EX5 — Un seul exercice, un seul round
?w=Gainage~1~0~Plank:60s:0
→ « Gainage », 1 round, 0 s entre rounds
Plank 60 s (+0 s)
---
## 7. DURÉES ET ESTIMATIONS
Une séance uniquement au temps a une durée exacte :
total = (Σ(durée + repos)) × rounds + repos_round × (rounds − 1)
Dès qu'un exercice est en répétitions, la durée devient une ESTIMATION : personne
ne sait combien de temps prendra une série. L'application estime 2 secondes par
répétition (minimum 10 s par exercice) et préfixe les totaux affichés d'un `~`.
N'annoncez jamais une durée exacte pour une séance contenant des répétitions.
---
## 8. NOMS D'EXERCICES ET ILLUSTRATIONS
N'importe quel nom fonctionne : la séance se déroule normalement quel que soit
l'intitulé. Le nom détermine uniquement si une ILLUSTRATION est affichée pendant
le repos qui précède l'exercice.
La correspondance est approximative (coefficient de Dice ≥ 0.6, casse, accents et
ponctuation ignorés). Comportement observé :
« push ups classic » → Push-Ups Classic ✅ casse ignorée
« Pushups Classic » → Push-Ups Classic ✅ ponctuation ignorée
« Crunches » → Crunchs ✅ pluriel toléré
« burpee » → Burpees ✅ singulier toléré
« Goblet Squat » → Dumbbell Goblet Squat ✅ correspondance partielle
« Push-Ups » → Pike Push-ups ⚠️ MAUVAIS exercice
« Pompes » → aucune ❌ le français n'est pas reconnu
« Squats » → aucune ❌ absent du catalogue
« Mountain Climbers » → aucune ❌ absent du catalogue
RECOMMANDATION : utilisez les noms EXACTS du catalogue ci-dessous. C'est le seul
moyen d'être certain de l'illustration. Les noms hors catalogue restent valides,
simplement sans image.
### Catalogue (35 noms exacts)
Bicycle Crunches
Burpees
Concentration Curl
Crunchs
Donkey Kicks
Dumbbell Bent-Over Row
Dumbbell Curl
Dumbbell Goblet Squat
Dumbbell Lateral Raise
Dumbbell Romanian Deadlift
Dumbbell Shoulder Press
Dumbell Curl
Flutter Kicks
Glute Bridge
Hammer Curl
High Knees
Hip Thrust
Inchworm Walk
Lunges
Overhead Tricep Extension
Pike Push-ups
Plank
Pull-ups
Push Ups Wide Grip
Push-Ups Classic
Push-Ups Wide Grip
Reverse Crunches
Reverse Snow Angels
Russian Twist
Shoulder taps
Side Plank
Skater Jumps
Superman
Tricep Kickback
V-ups
Note : `Dumbell Curl` et `Push Ups Wide Grip` sont des variantes orthographiques
de `Dumbbell Curl` et `Push-Ups Wide Grip`. Les deux graphies fonctionnent ;
préférez la forme correcte.
---
## 9. LIENS INVALIDES
Aucun lien ne produit d'erreur visible. Quand le lien n'est pas exploitable,
l'application s'ouvre simplement sur son écran d'accueil.
?w= → accueil (vide)
?w=MaSéance → accueil (segments manquants)
?w=A~8~60 → accueil (aucun exercice)
?w=A~8~60~:30s:10 → accueil (nom d'exercice vide)
?w=A~8~60~:30s:10~B:20s:5 → séance à 1 exercice (l'anonyme est ignoré, B est gardé)
?w=A~500~99999~Ex:5000s:-9 → séance valide (bornée à 99 rounds, 900 s, 999 s, 0 s)
Les autres paramètres de query string sont ignorés : `utm_source` et consorts
passent sans gêner. L'ordre des paramètres n'a pas d'importance.
---
## 10. CE QUE VOIT L'UTILISATEUR
1. Écran de la séance : nom, rounds, liste des exercices, durée. Trois actions —
Démarrer, Ajouter à mes séries, Modifier.
2. Le lien N'AUTO-DÉMARRE JAMAIS. Un appui est requis, notamment parce que les
navigateurs bloquent l'audio tant qu'aucun geste utilisateur n'a eu lieu.
3. Compte à rebours de 3 secondes, puis la séance s'enchaîne.
4. L'application est une PWA : après une première visite, les liens fonctionnent
hors ligne, tout étant décodé côté client.
---
## 11. LISTE DE VÉRIFICATION AVANT DE PRODUIRE UN LIEN
- [ ] Chaque exercice en répétitions porte bien le suffixe `x`.
- [ ] Les `~` et `:` présents dans les noms sont échappés en `%7E` et `%3A`.
- [ ] rounds entre 1 et 99 ; repos_round entre 0 et 900.
- [ ] Durées et répétitions entre 1 et 999 ; aucune valeur à 0 (0 devient 20).
- [ ] Au moins un exercice, et chaque exercice a un nom non vide.
- [ ] Les noms viennent du catalogue si l'illustration est souhaitée.
- [ ] Aucune durée totale exacte annoncée si la séance contient des répétitions.