# Kartoon — documentation complète (générée le 2026-10-10) # Démarrage rapide Kartoon transforme une idée écrite en **vidéo animée prête à poster** : histoire, personnages, images, animation, voix, doublage et montage. Tout ce que fait le site se fait aussi **par API**, avec une clé. ## 1. Créer une clé Sur **kartoon.io/api** (offre Business, ou administrateur) : *Créer une clé*, choisir ses autorisations, copier la clé. Elle commence par `kx_live_` et ne s'affiche **qu'une fois**. ```bash export KARTOON_API_KEY="kx_live_…" ``` ## 2. Vérifier le compte ```bash curl https://kartoon.io/api/v1/account -H "Authorization: Bearer $KARTOON_API_KEY" ``` ```json { "id": "…", "plan": "studio", "credits_remaining": 202500, "credits_per_second_by_engine": { "2": 100, "1.5": 100, "2.5": 320 }, "max_parallel": 10, "max_quality": "4k" } ``` ## 3. Choisir un modèle ```bash curl https://kartoon.io/api/v1/models -H "Authorization: Bearer $KARTOON_API_KEY" ``` | `model` | Moteur | Prix | Pour | |---|---|---|---| | `seedance-2.5` *(défaut)* | Kartoon 2.5 | 320 crédits/s | le meilleur jeu d'acteur, les pubs, les plans longs | | `kling-3` | Kartoon 2.0 | 100 crédits/s | moins cher, image très nette (4K selon l'offre) | | `wan-3-prime` | Kartoon 1.5 | 100 crédits/s | pipeline 2.5 avec clips Wan 3.0 Prime | ## 4. Lancer une vidéo ```bash curl -X POST https://kartoon.io/api/v1/videos \ -H "Authorization: Bearer $KARTOON_API_KEY" -H "Content-Type: application/json" \ -d '{"prompt":"Un chat qui découvre la neige pour la première fois","duration":20,"language":"French","model":"seedance-2.5"}' ``` Réponse `202` en quelques secondes : ```json { "id": "g1791700000000api3f2a1c", "kind": "video", "status": "queued", "model": "seedance-2.5", "resolution": "1080p", "credits_estimated": 6400, "status_url": "/api/v1/videos/g1791700000000api3f2a1c" } ``` ## 5. Suivre, puis télécharger Une génération prend en général **13 à 27 minutes**. Interroger toutes les 30 à 60 s : ```bash curl https://kartoon.io/api/v1/videos/g1791700000000api3f2a1c -H "Authorization: Bearer $KARTOON_API_KEY" ``` Quand `status` vaut `done`, `video.mp4_url` est un MP4 1080p H.264 signé (valable 3 h au moins), prêt pour Instagram, TikTok ou un site. La vidéo est aussi dans **Mes vidéos** sur le site. ## 6. Une pub pour un produit Ajoutez `product` (nom, description, 1 à 4 photos https) ou `product_id` (produit déjà dans la bibliothèque) : c'est une pub du **Marketing Studio**. ```json { "prompt": "Voix off : français. Une maman débordée découvre le produit.", "duration": 30, "product": { "name": "Somnia", "description": "Gummies sommeil", "photos": ["https://…/somnia.jpg"] } } ``` ## Pour aller plus loin - [Référence de l'API](#/api) — chaque route, chaque champ, chaque erreur. - [Guide pour agents IA](#/agent) — tout ce qu'un agent doit savoir pour piloter Kartoon seul. - [Tout le site par API](#/site) — les 81 routes du site, au nom du compte. - [Architecture](#/architecture) — comment Kartoon est construit, de bout en bout. - Pour un LLM : [llms-full.txt](/llms-full.txt) — toute la doc en un seul fichier texte. # API Kartoon v1 Lancer et suivre des vidéos Kartoon par programme (agent de croissance, intégrations des clients Business). Mise en service le 6 octobre 2026. Code : `app/api/v1/`, clés : `lib/cles-api.ts`, tables : migration `0034_api_v1.sql`. - **Adresse** : `https://kartoon.io/api/v1` - **Authentification** : `Authorization: Bearer kx_live_…` (ou `x-api-key: kx_live_…`) - **Format** : JSON. Une erreur a toujours la forme `{ "error": { "code": "…", "message": "…" } }`. - **Coût** : une génération lancée par l'API coûte exactement comme sur le site, au tarif du modèle choisi (`seedance-2.5` : 320 crédits par seconde ; `kling-3` et `wan-3-prime` : 100), réservés au lancement, réglés à la durée réelle, rendus si la génération échoue. ## Tout le site : `/api/v1/site/` Au-delà des routes ci-dessous, **toute action du site** (projets, scripts, plans, régénération, doublage, export, persos, éléments, univers, produits, pubs de référence, likes, remix, séries, compte…) s'appelle par `https://kartoon.io/api/v1/site/` avec la même clé, au nom du compte, exactement comme ses clics. Catalogue complet, généré depuis le code : [Tout le site par API](#/site). ## Clés Créées depuis **Compte → API** (offre Business et administrateurs). La clé n'est affichée **qu'une fois** ; Kartoon n'en garde que l'empreinte. Une clé ouvre le compte et ses crédits : la garder côté serveur, la révoquer au moindre doute (effet en moins d'une minute). 10 clés actives au plus par compte. Routes de gestion (session du site connectée, **jamais** une clé — c'est la page kartoon.io/api qui les appelle) : | Route | Rôle | |---|---| | `GET /api/v1/keys` | les clés du compte (préfixe, 4 derniers caractères, autorisations, expiration, dernière utilisation) | | `POST /api/v1/keys` `{ "name", "scopes"?, "expires_in_days"? }` | crée une clé ; `scopes` absent = toutes les autorisations ; `expires_in_days` 1-3650 ou `"never"` | | `POST /api/v1/keys/{id}/rotate` `{ "grace_hours"? }` | nouvelle valeur (rendue une fois), mêmes réglages ; l'ancienne reste valable `grace_hours` (0-72, défaut 24) | | `DELETE /api/v1/keys/{id}` | révoque (effet en moins d'une minute) ; `?remove=1` supprime une clé déjà révoquée | ### Autorisations (`scopes`) | Autorisation | Ouvre | |---|---| | `generations:write` | `POST /videos`, et via `/site` : produce, projets, régénérations, doublage, séries, remix, scripts, plans | | `library:read` | toutes les lectures (`GET`) : vidéos, modèles, persos, produits, Instagram, `/site` en lecture | | `library:write` | les écritures de bibliothèque (likes, Instagram, WhatsApp, le reste de `/site`) | | `assets:write` | `POST /products`, et via `/site` : persos, éléments, univers, produits, références, envois de fichiers | | `credits:read` | `GET /account`, et via `/site` : crédits | Une requête hors des autorisations de la clé reçoit `403 scope_required` avec `scope` = l'autorisation qui manque. ## Limites | Opération | Limite par clé | |---|---| | `POST /videos` | 10 par minute | | lectures (`GET`) | 120 par minute | | générations simultanées | celles de l'offre (Business : 10) — au-delà, elles attendent leur tour | | `POST /instagram/accounts/{id}/reels` | 10 par minute (et le quota d'Instagram : publications par API sur 24 h glissantes) | | `GET /instagram/accounts/{id}`, `…/media` | 60 par minute ; `…/media?insights=1` : 6 par minute | | `GET /instagram/posts/{id}/insights` | 30 par minute | ## `GET /account` Le compte de la clé. ```json { "id": "…", "plan": "studio", "credits_remaining": 202500, "credits_per_second": 100, "max_parallel": 10, "max_quality": "4k" } ``` ## `GET /models` — les modèles au choix ```json { "default": "seedance-2.5", "data": [ { "id": "seedance-2.5", "engine": "2.5", "name": "Kartoon 2.5 — Seedance 2.5", "video_model": "bytedance/seedance-2.5/reference-to-video", "resolutions": ["720p", "1080p"], "default_resolution": "1080p", "native_audio": true, "credits_per_second": 320, "best_for": "…" }, { "id": "kling-3", "engine": "2", "credits_per_second": 100, "…": "…" }, { "id": "wan-3-prime", "engine": "1.5", "credits_per_second": 100, "…": "…" } ] } ``` - **`seedance-2.5`** (défaut) — Kartoon 2.5 : clips Seedance 2.5 en mode références (fiches des persos jointes à chaque clip), plans enchaînés jusqu'à 30 s en une seule prise, meilleur jeu et synchro labiale. Les pubs tournent ici. - **`kling-3`** — Kartoon 2.0 : clips Kling 3 de 3 à 10 s ; en `1080p`, 4K native pour les offres Pro et Business ; `720p` = le plus rapide et le moins cher. - **`wan-3-prime`** — Kartoon 1.5 : le pipeline 2.5 avec des clips Wan 3.0 Prime, dialogues dans la langue d'origine. ## `GET /characters?universe=…` Les persos de la bibliothèque du compte (créés sur le site, ou repris de la communauté) : `id`, `name` (le nom exact à donner dans `POST /videos` → `characters`), `universe`, `source` (`own` / `community`), `description`, `videos` (nombre de vidéos où il a joué), `image_url`. Plusieurs persos du même nom : `POST /videos` prend celui qui a joué dans le plus de vidéos (la réponse 202 liste les persos retenus sous `characters`) ; donner l'`id` pour en viser un précis. ## `POST /products` — ajouter un produit à la bibliothèque `{ "name": "…", "description": "…", "url": "https://…", "photos": ["https://…"] }` (1 à 4 photos) → `201` avec son `id`. Le produit rejoint « Produits » du Marketing Studio et se réutilise ensuite dans `POST /videos` par son id ou son nom exact (`product_id`). 20 par minute. ## `GET /products` Les produits de la bibliothèque (page Marketing Studio). Leur `id` sert à lancer une pub. ```json { "data": [ { "id": "5de3c66b040b", "name": "Fitmo", "description": "…", "photos": 2 } ] } ``` ## `POST /videos` — lancer | Champ | Type | Défaut | Rôle | |---|---|---|---| | `prompt` | texte (3-3000) | — | la demande, comme dans le studio | | `duration` | entier 3-300 (s) | 30 | durée visée (et facturée) | | `aspect` | `9:16`, `16:9`, `1:1`, `4:5`, … | `9:16` | format | | `language` | `auto`, `French`, `English`, … | `auto` | langue (auto = celle du prompt) | | `style` | `pixar`, `clay`, `animal`, … | `pixar` | univers | | `model` | `seedance-2.5`, `kling-3`, `wan-3-prime` | `seedance-2.5` | modèle vidéo (`GET /models`) | | `engine` | `2.5`, `2`, `1.5` | — | ancien nom du choix de moteur (`2.5` = seedance-2.5, `2` = kling-3, `1.5` = wan-3-prime) ; avec `model`, ils doivent concorder | | `resolution` | `1080p`, `720p` | `1080p` | définition | | `voice` | `narrator`, `dialogue`, `mix`, `none` | lu dans le prompt, sinon choisi par l'IA | **pubs** : voix off, persos qui parlent, les deux (scènes narrées + scènes dialoguées), ou aucune parole | | `characters` | liste de noms (1-6) | — | **persos de ta bibliothèque** (`GET /characters`), par leur **nom exact** (ou id) ; la vidéo prend leur univers (`style` ignoré) ; 2 au plus dans une pub. Nom inconnu : `404 character_not_found` avec `available` | | `product_id` | texte | — | **pub** pour un produit de la bibliothèque : son **id ou son nom exact** (`GET /products`) | | `product` | `{ name, description, photos: [https…] }` | — | **pub** pour un produit fourni (1-4 photos) | Sans produit : une vidéo (histoire). Avec `product_id` ou `product` : une pub (Marketing Studio). Réponse **202** dès que la génération est partie (quelques secondes) : ```json { "id": "ad1791300000000api3f2a1c", "kind": "ad", "status": "queued", "model": "seedance-2.5", "resolution": "1080p", "credits_estimated": 9600, "status_url": "/api/v1/videos/ad1791300000000api3f2a1c" } ``` **La voix d'une pub, sans le champ `voice`** : le moteur lit le prompt. « Voix off : français » (ou des lignes `VO : "…"`) → voix off, dite **mot pour mot** si le texte est écrit ; « les persos parlent », « que des dialogues » ou des répliques écrites `Nom : "…"` → dialogues ; voix off **et** persos qui parlent → `mix` ; « pub muette », « sans parole », « Voix off : aucune » → rien. Sinon l'IA choisit ce qui vend le mieux le produit. Refus (rien n'est débité) : `400 invalid_request`, `402 insufficient_credits` (avec `credits_remaining` et `credits_needed`), `403 style_reserved`, `404 product_not_found`, `429 rate_limited`, `503 voices_unavailable`, `422 generation_refused`. ## `GET /videos/{id}` — suivre ```json { "id": "…", "status": "running", "progress": 46, "step": "🎬 Animation 2/5", "created_at": "…", "video_id": null, "error": null, "warning": null, "video": null } ``` `status` : `queued` (en file), `running`, `done`, `error`. Une génération dure en général 13 à 27 minutes : interroger toutes les 30 à 60 s suffit. Terminée : ```json { "id": "…", "status": "done", "progress": 100, "video_id": "a8e2e46f-…", "video": { "id": "a8e2e46f-…", "title": "…", "duration_s": 21.7, "width": 1080, "height": 1920, "mp4_url": "https://…", "original_url": "https://…", "thumbnail_url": "https://…", "urls_expire_at": "…" } } ``` - `mp4_url` : copie **1080p H.264** (≤ 8 Mbit/s, démarrage rapide) — celle à donner à Instagram, TikTok ou un site. Si elle n'est pas encore prête, c'est l'original (la copie se fabrique en fond). - `original_url` : le fichier d'origine (jusqu'en 4K). - Adresses signées, valables **3 h au moins** (`urls_expire_at`) : redemander l'état pour en avoir de nouvelles. - Échec : `status: "error"`, `error.message`, et `error.credits_refunded` quand les crédits ont été rendus. - `warning` : la vidéo est publiée **mais** le moteur signale un défaut — par exemple un doublage raté (voix restées en anglais). À vérifier avant de publier la vidéo ailleurs. `null` sinon. La vidéo apparaît aussi dans **Mes vidéos** du compte, comme une génération faite sur le site. ## `GET /videos?limit=20` — historique Les générations lancées par l'API (100 au plus, les plus récentes d'abord) : `id`, `status`, `progress`, `video_id`, `error`, `created_at`. Les adresses de téléchargement : `GET /videos/{id}`. ## Instagram Publier les vidéos Kartoon en Reels sur des comptes Instagram, et lire ce qui marche. Mise en service le 7 octobre 2026. Code : `app/api/v1/instagram/`, `lib/instagram.ts`, `lib/instagram-boucles.ts` ; tables : migration `0037_instagram.sql`. - **Connexion** : sur le site, **Compte → Instagram** (offre Business et administrateurs), par la page d'autorisation d'Instagram (OAuth « Instagram Login »). Compte **professionnel** exigé (Créateur ou Business). L'API ne voit jamais un jeton Instagram : il est stocké chiffré et rafraîchi par le serveur (60 jours, renouvelé 15 jours avant la fin). - **Tant que l'app Meta n'est pas validée par Meta** (accès standard), seuls les comptes ajoutés à l'app dans son tableau de bord (rôle « testeur Instagram ») peuvent se connecter. - Un compte dont Instagram refuse le jeton passe en `reconnect_required` (code `409`) : le reconnecter depuis la page. - **Par Ayrshare (depuis le 7 octobre 2026, dès que `AYRSHARE_API_KEY` est posée)** : la connexion passe par la page de liaison d'Ayrshare (on s'y connecte avec ses identifiants Instagram ; pas d'app Meta à créer). Un compte Instagram = un « profil » Ayrshare (offre Launch : 10). `provider` vaut alors `ayrshare` (sinon `meta`). Même API, mêmes routes ; en plus : les statistiques du compte (`/analytics`) et les « trial reels » (`trial`). `lib/ayrshare.ts`, `lib/instagram-fournisseur.ts`, migration `0038`. ### `GET /instagram/accounts` Les comptes connectés : `id` (à utiliser dans les routes), `instagram_user_id`, `provider` (`meta` ou `ayrshare`), `username`, `name`, `account_type`, `profile_picture_url`, `followers_count`, `status` (`active` ou `reconnect_required`), `token_expires_at`, `last_error`, `connected_at`. ### `GET /instagram/accounts/{id}` Le compte lu **en direct** chez Instagram (abonnés recopiés), plus `media_count` et `publishing_quota` : `{ "used": 3, "limit": 100, "window_hours": 24 }` (`null` si Instagram ne l'a pas donné). ### `POST /instagram/accounts/{id}/reels` — publier ```json { "video_id": "…", "caption": "Le chat découvre la neige ❄️ #chat", "share_to_feed": true, "cover_offset_ms": 1500 } ``` | Champ | Défaut | Valeurs | |---|---|---| | `video_id` | — (obligatoire) | une vidéo de ce compte Kartoon (`video.id` de `GET /videos/{id}`), de 3 s à 15 min | | `caption` | `""` | 2 200 caractères, 30 hashtags et 20 mentions au plus (limites d'Instagram) | | `share_to_feed` | `true` | le Reel apparaît aussi dans la grille du profil | | `cover_offset_ms` | Instagram choisit | image de couverture : instant de la vidéo, en millisecondes | | `allow_duplicate` | `false` | `true` pour republier une vidéo déjà publiée sur ce compte (sinon `409 already_posted`) | | `trial` | `false` | « Trial reel » : montré d'abord aux **non-abonnés** seulement (A/B tests). Comptes `ayrshare` seulement (sinon `422 trial_not_supported`) | | `trial_graduation` | `auto` | `auto` : Instagram le partage seul aux abonnés s'il marche (dans les 72 h) ; `manual` : à faire dans l'app | **Plafond** : 20 publications par compte sur 24 h glissantes (`KARTOON_IG_POSTS_JOUR`), en cours et publiées comptées — au-delà, `429 daily_post_limit` avec `used` et `limit`. Instagram, lui, plafonne à 50. Réponse immédiate `202` : le post (forme ci-dessous) avec `status: "publishing"` et `status_url`. La publication tourne sur le serveur : copie 1080p H.264 de la vidéo, envoi à Instagram avec l'étiquette **« contenu IA »** d'Instagram, traitement par Instagram (1 à 15 min), publication. Un redémarrage du serveur ne la perd pas et ne la publie jamais deux fois. Refus : `404 video_not_found`, `409 already_posted` (avec `post_id`), `422 video_not_allowed`, `429 instagram_quota_reached`, `409 reconnect_required`. ### `GET /instagram/posts/{id}` — suivre un Reel ```json { "id": "…", "account_id": "…", "video_id": "…", "status": "published", "caption": "…", "share_to_feed": true, "cover_offset_ms": 1500, "ai_label": true, "instagram_media_id": "1789…", "permalink": "https://www.instagram.com/reel/…", "published_at": "2026-10-07T…", "error": null, "warning": null, "video_duration_s": 20, "metrics": { "views": 12840, "reach": 9120, "likes": 611, "comments": 23, "shares": 88, "saved": 41, "total_interactions": 763, "ig_reels_avg_watch_time": 7400, "ig_reels_video_view_total_time": 95016000, "reels_skip_rate": 38.5 }, "metrics_at": "2026-10-08T…", "indicators": { "hook_rate": 61.5, "avg_watch_ratio": 37, "views_per_reach": 1.41, "shares_per_1000_views": 6.9, "saves_per_1000_views": 3.2 }, "snapshots": [ { "age_hours": 24, "taken_at": "…", "metrics": { … }, "indicators": { … } } ], "snapshot_schedule_hours": [24, 48, 168, 720], "created_at": "2026-10-07T…" } ``` - `status` : `publishing` → `published`, ou `error` (`error.message` dit pourquoi : vidéo refusée par Instagram, traitement trop long, compte à reconnecter…). - `ai_label` : l'étiquette « contenu IA » a-t-elle été posée (`null` avant l'envoi à Instagram). - **Relevés automatiques** : 24 h, 48 h, 7 jours et 30 jours après la publication (`snapshots`). Instagram consolide ses chiffres jusqu'à 48 h. - **Indicateurs** (l'API d'Instagram ne donne pas de courbe de rétention) : - `hook_rate` ≈ % de vues **non** passées avant 3 s (`100 − reels_skip_rate`) ; - `avg_watch_ratio` ≈ temps moyen regardé ÷ durée de la vidéo, en % (au-delà de 100 % : relectures) ; - `views_per_reach` : vues par compte touché (relectures, partages) ; - `shares_per_1000_views`, `saves_per_1000_views`. - Une métrique qu'Instagram ne fournit pas (encore) pour ce compte est simplement absente de `metrics`. ⚠️ L'unité de `ig_reels_avg_watch_time` n'est pas documentée par Meta : millisecondes supposées, à confirmer sur les premiers relevés réels. ### `GET /instagram/posts/{id}/insights` Relit **maintenant** les statistiques chez Instagram ; rend le post à jour. `409 not_published` avant la publication. ### `GET /instagram/posts?account_id=…&status=…&limit=20&before=…` Les Reels publiés par Kartoon (100 au plus par page, les plus récents d'abord), même forme que ci-dessus sans `snapshots`. `status` : `publishing`, `published` ou `error`. Page suivante : `before` = `created_at` du dernier. ### `GET /instagram/accounts/{id}/media?limit=25&after=…&insights=1` Toutes les publications du compte, y compris celles faites **hors de Kartoon** (la matière de l'analyse des anciennes vidéos) : `id`, `caption`, `media_type`, `media_product_type` (`REELS`, `FEED`…), `permalink`, `timestamp`, `like_count`, `comments_count`, `thumbnail_url`, et `kartoon_post_id` / `kartoon_video_id` quand c'est Kartoon qui l'a publiée. `insights=1` : `metrics` et `indicators` de chaque Reel, lus en direct (25 par page au plus ; `metrics_error` si Instagram n'a pas répondu pour l'un d'eux). Page suivante : `after` = `next_cursor` (`null` à la fin). ### `GET /instagram/accounts/{id}/analytics?daily=1&history=30` Comptes `ayrshare` seulement. Les statistiques du **compte**, lues en direct : `followersCount`, `followsCount`, `mediaCount`, `reachCount`, `viewsCount`, `shareCount`, `likeCount`, `commentsCount`, et la démographie (`audienceGenderAge`, `audienceCountry`, `audienceCity`, et leurs variantes `…EngagedAudienceDemographics` — Instagram ne les donne qu'à partir de 100 abonnés). `daily=1` : séries jour par jour. `history=N` : les N derniers relevés quotidiens gardés par Kartoon (un par jour et par compte, automatiquement) — la courbe de croissance. 6 appels par minute au plus. ### `POST /instagram/accounts/{id}/ayrshare` — tout le reste d'Ayrshare Comptes `ayrshare` seulement. Relais vers l'API d'Ayrshare (https://www.ayrshare.com/docs) pour ce compte : `{ "method": "GET", "path": "/comments/…", "query": {…}, "body": {…} }` → `{ "ok": true, "ayrshare": }`. Chemins permis : `/comments`, `/messages`, `/analytics`, `/history`, `/media`, `/hashtags`, `/brand` (écoute de marques : comptes concurrents), `/generate` (transcription, sentiment…), `/links`, `/validate`, et en lecture seule `/user`, `/post`, `/automations`. Publier passe toujours par `…/reels` (plafond, suivi) ; `/profiles` et l'écriture sur `/post` sont refusés (`403 path_not_allowed`). Une erreur d'Ayrshare revient en `ayrshare_error` avec `ayrshare_code`. 60 appels par minute. ### `GET /instagram/accounts/{id}/archive?type=…&since=…&before=…&limit=100` Comptes `ayrshare`. **Tout ce qu'Ayrshare a rendu sur le compte, recopié chez Kartoon** (rien n'est perdu, même si Ayrshare ou Instagram l'oublient) : `type` = `message` (messages privés reçus et envoyés, toutes les heures), `commentaire` (commentaires et réponses des publications de moins de 30 jours, toutes les 6 h), `media` (toutes les publications du compte, même hors Kartoon, toutes les 6 h), `stats_media` (statistiques COMPLÈTES de chaque publication de moins de 30 jours, une par jour : clé `:`). Réponse : `{ type, data: [{ key, at, first_seen_at, updated_at, data }] }`, des plus récents aux plus anciens ; page suivante : `before` = `at` du dernier. 500 par page au plus. Les relevés automatiques d'un Reel (`GET /instagram/posts/{id}` → `snapshots`) portent aussi `raw` : la réponse complète d'Ayrshare à ce moment-là et les commentaires du Reel. Les relevés quotidiens du compte (`/analytics?history=N`) portent en plus `_quotidien` (séries jour par jour), `_abonnes_en_ligne` (heures où les abonnés sont connectés) et `_compte` (état du compte, quota). ### `POST /agent/whatsapp` — écrire dans le groupe WhatsApp de l'équipe `{ "account_id": "", "text": "…" }` → le texte part dans le groupe WhatsApp relié à ce compte (page admin `/admin-whatsapp.html`, passerelle `lib/whatsapp.ts`). `404 no_group` si aucun groupe n'est relié, `503 whatsapp_unavailable` si le numéro est déconnecté. 20 par minute. C'est la route du bref de l'agent de croissance et de ses réponses à l'équipe (les messages de l'équipe le réveillent : `lib/agent-pont.ts`). ### Erreurs propres à Instagram | Code | Statut | Sens | |---|---|---| | `account_not_found` | 404 | compte inconnu, déconnecté, ou d'un autre compte Kartoon | | `reconnect_required` | 409 | Instagram refuse le jeton : reconnecter le compte depuis la page Compte → Instagram | | `instagram_rate_limited` | 429 | Instagram limite les appels pour ce compte : réessayer plus tard | | `instagram_refused` | 422 | Instagram a refusé la demande (`message` et `instagram_code` d'Instagram) | | `instagram_unavailable` | 503 | Instagram ne répond pas : réessayer | ## Exemple ```bash curl -X POST https://kartoon.io/api/v1/videos \ -H "Authorization: Bearer $KARTOON_API_KEY" -H "Content-Type: application/json" \ -d '{"prompt":"Un chat qui découvre la neige pour la première fois","duration":20,"language":"French"}' ``` ## À venir Webhook de fin de génération (plus besoin d'interroger), analyse d'une vidéo (transcription, hook, structure). # Guide pour agents IA Ce guide s'adresse à un **agent** (Claude, GPT, un script autonome…) qui reçoit une clé Kartoon et doit produire, suivre et publier des vidéos **sans humain**. Il dit ce que Kartoon sait faire, comment le lui demander, ce que ça coûte, combien de temps ça prend, et comment réagir à chaque réponse. La référence exhaustive des champs est dans [Référence de l'API](#/api). > **Version texte pour un LLM** : `https://docs.kartoon.io/llms-full.txt` contient toute cette documentation en un > seul fichier. Donnez-la en contexte à votre agent. ## 1. Le modèle mental - **Une génération = un film fini.** Vous donnez une idée (le *prompt*) ; Kartoon écrit le script, crée les persos, dessine les images de départ, anime chaque plan, enregistre les voix, double, monte, et publie la vidéo dans **Mes vidéos** du compte. Vous ne pilotez pas les étapes : vous pilotez l'**intention**. - **Deux sortes** : une **vidéo** (une histoire, un sketch, un épisode) — ou une **pub** dès qu'un produit est donné (`product` ou `product_id`) : le Marketing Studio écrit alors une publicité pour ce produit. - **C'est long et ça coûte** : 13 à 27 minutes par génération ; de 100 à 320 crédits par seconde de vidéo. Ne lancez jamais « pour voir » en boucle. - **Asynchrone** : `POST /videos` répond en quelques secondes (`202` + un `id`), puis vous interrogez `GET /videos/{id}` jusqu'à `done` ou `error`. - **Tout le site est appelable** : au-delà des routes `/api/v1/*` documentées, `/api/v1/site/` relaie n'importe quelle action du site au nom du compte (régénérer un plan, doubler une vidéo, créer un perso…). Voir [Tout le site](#/site). ## 2. Ce que l'agent peut faire | Besoin | Appel | |---|---| | Connaître son budget, son offre, ses générations en parallèle | `GET /account` | | Choisir un modèle et son prix | `GET /models` | | Lancer une vidéo ou une pub | `POST /videos` | | Suivre et récupérer le MP4 | `GET /videos/{id}` | | Retrouver ce qui a été lancé (après un redémarrage de l'agent) | `GET /videos?limit=50` | | Lister / créer les produits | `GET /products`, `POST /products` | | Utiliser des persos récurrents (mascotte, héros de série) | `GET /characters` puis `characters: ["Nom exact"]` | | Publier en Reel Instagram, lire les statistiques | `/instagram/*` | | Tout le reste du site (régénérer un plan, doubler, séries, persos…) | `/site/` | ## 3. La boucle de base ```js const API = "https://kartoon.io/api/v1"; const H = { Authorization: `Bearer ${process.env.KARTOON_API_KEY}`, "Content-Type": "application/json" }; // 1. budget const compte = await (await fetch(`${API}/account`, { headers: H })).json(); // 2. lancer const r = await fetch(`${API}/videos`, { method: "POST", headers: H, body: JSON.stringify({ prompt: "Un chat qui découvre la neige pour la première fois. Voix off : français, ton tendre.", duration: 20, language: "French", model: "seedance-2.5", aspect: "9:16", }) }); const lancement = await r.json(); if (r.status !== 202) throw new Error(`${lancement.error.code} : ${lancement.error.message}`); // 3. attendre (30-60 s entre deux lectures) let etat; do { await new Promise((ok) => setTimeout(ok, 45_000)); etat = await (await fetch(`${API}/videos/${lancement.id}`, { headers: H })).json(); } while (etat.status === "queued" || etat.status === "running"); // 4. résultat if (etat.status === "done") console.log(etat.video.mp4_url, etat.warning); else console.log("échec", etat.error); // crédits rendus : etat.error.credits_refunded ``` ## 4. Budget et coût - Coût = **durée × tarif du modèle** : `seedance-2.5` 320 crédits/s, `kling-3` et `wan-3-prime` 100 crédits/s. Une vidéo de 30 s en `seedance-2.5` = 9 600 crédits. - Les crédits sont **réservés au lancement** (`credits_estimated`), puis **réglés à la durée réelle** (quelques secondes de plus ou de moins), et **rendus** si la génération échoue. - Avant de lancer : `credits_remaining ≥ durée × credits_per_second`. Sinon `402 insufficient_credits` (avec `credits_needed`) — rien n'est débité. - **Parallèle** : `max_parallel` générations en même temps (Business : 10). Au-delà, elles attendent leur tour (`status: "queued"`), elles ne sont pas refusées. ## 5. Bien écrire le prompt Le prompt est lu par un scénariste IA (Claude). Il suit le client **à la lettre** : tout ce qui est écrit est respecté, tout ce qui ne l'est pas est inventé au mieux. **Les bons réflexes** - **Dire l'histoire, pas la technique** : qui, où, ce qui arrive, la chute. « Une mamie qui se bat contre son robot aspirateur et finit par l'adopter » vaut mieux que « plan large, travelling ». - **Donner le ton** : drôle, tendre, absurde, dramatique, confession intime… - **Découper si vous avez une idée précise** : `[0-4s — HOOK] …`, `[4-14s] …` — chaque temps garde sa scène. - **Écrire les mots s'ils comptent** : une voix off écrite (`VO : "…"`) est dite **mot pour mot** ; une réplique `Léa : "…"` est dite par Léa. - **La langue** : `language: "French"` (ou laisser `auto` : la langue du prompt). Les voix sont tournées puis doublées dans la langue demandée. **La voix d'une pub** (le champ `voice` force le choix ; sinon le prompt décide) : | Vous écrivez | Résultat | |---|---| | `Voix off : français` ou des lignes `VO : "…"` | voix off (personne ne parle à l'écran) | | « les persos parlent », « que des dialogues », des répliques `Nom : "…"` | dialogues joués à l'image | | voix off **et** persos qui parlent | `mix` : scènes narrées + scènes dialoguées | | « pub muette », « sans parole », `Voix off : aucune` | aucune parole, seulement l'ambiance | **Durée** : 3 à 300 s. Une pub de 20-60 s est le format qui marche ; une vidéo histoire de 20-90 s. **Format** : `9:16` (Reels, TikTok, Shorts) par défaut ; `16:9`, `1:1`, `4:5`… disponibles. **Univers** (`style`) : `pixar` (film animé 3D, défaut), `clay` (pâte à modeler), `disney2d` (conte de fées), `yarn` (tout en laine), `barbie` (poupées glamour), `toybrick` (briques à emboîter), `blockverse` (Minecraft), `lowpoly` (low poly cinéma). Des univers personnalisés du compte s'ajoutent (voir `/site/studio/universes`). ## 6. Pubs : le produit - **Produit fourni** : `product: { name, description, photos: ["https://…"] }` (1 à 4 photos, https, accessibles publiquement). Le produit est dessiné **d'après ses photos** (forme, couleurs, étiquette). - **Produit de la bibliothèque** : `POST /products` une fois, puis `product_id: ""` à chaque pub. - **Une photo avec un visage humain** (mannequin, influenceur) peut être refusée par le modèle vidéo : préférez des photos du produit seul. - La description doit dire **ce que fait le produit et pour qui** : c'est la matière de l'argumentaire. ## 7. Persos récurrents `GET /characters` liste les persos du compte (créés sur le site ou repris de la communauté). Donnez leur **nom exact** dans `characters` (6 au plus ; 2 dans une pub) : ils jouent dans la vidéo, avec leur visage et leur univers (le `style` est alors ignoré). Un nom inconnu → `404 character_not_found` avec la liste `available`. Plusieurs persos du même nom → celui qui a joué dans le plus de vidéos ; donnez l'`id` pour en viser un précis. ## 8. Suivre une génération `GET /videos/{id}` → `status` : `queued` → `running` (`progress` 0-100, `step` lisible : « 🖼️ Scènes 3/9 », « 🎬 Animation 2/5 », « 🎬 Montage ») → `done` ou `error`. - **Cadence** : toutes les 30 à 60 s. Pas plus souvent (120 lectures/min par clé au plus). - **`done`** : `video.mp4_url` (1080p H.264, ≤ 8 Mbit/s — à donner aux réseaux), `video.original_url` (jusqu'en 4K), `thumbnail_url`. Adresses **signées 3 h au moins** : relisez l'état pour en avoir de fraîches, ne les stockez pas. - **`warning`** non nul : la vidéo est livrée mais un défaut est signalé (ex. doublage raté, voix restées en anglais). À vérifier avant de publier ailleurs. - **`error`** : `error.message` dit pourquoi ; `error.credits_refunded` si les crédits sont rendus. - **Une mise à jour du serveur ne perd pas la génération** : elle reprend seule. Un `running` qui stagne quelques minutes n'est pas un échec. ## 9. Erreurs et quoi faire | Statut / code | Sens | Réaction de l'agent | |---|---|---| | `400 invalid_request` | champ invalide (`message` dit lequel) | corriger la requête, ne pas réessayer telle quelle | | `401 unauthorized` | clé absente, inconnue, révoquée ou expirée | s'arrêter, prévenir un humain | | `402 insufficient_credits` | budget insuffisant | réduire la durée, passer à `kling-3`, ou attendre une recharge | | `403 scope_required` | la clé n'a pas l'autorisation (`scope`) | s'arrêter, demander une clé avec cette autorisation | | `403 style_reserved` | univers réservé | prendre un univers public | | `404 product_not_found` / `character_not_found` | nom inexact | choisir dans `available` | | `409 conflict` | identifiant déjà pris | relancer (nouvel identifiant) | | `422 generation_refused` | le moteur a refusé la demande (contenu, prompt vide…) | reformuler | | `429 rate_limited` | trop d'appels | attendre une minute | | `503 voices_unavailable` / `unavailable` | service momentanément indisponible | réessayer dans 5-10 min | **Jamais de double lancement** : si `POST /videos` expire côté agent (réseau), ne relancez pas à l'aveugle — lisez `GET /videos?limit=10` : la génération est peut-être partie. ## 10. Publier sur Instagram 1. Un humain connecte le compte Instagram une fois (kartoon.io → Compte → Instagram). 2. `GET /instagram/accounts` → l'`id` du compte. 3. `POST /instagram/accounts/{id}/reels` `{ "video_id": "", "caption": "…" }` → `202`, puis `GET /instagram/posts/{postId}` jusqu'à `published` (1 à 15 min). 4. Statistiques relevées seules à 24 h, 48 h, 7 j, 30 j (`snapshots`) ; `hook_rate`, `avg_watch_ratio`, `shares_per_1000_views` pour juger ce qui marche. 20 Reels par compte et par 24 h au plus. L'étiquette « contenu IA » d'Instagram est posée automatiquement. ## 11. Aller plus loin avec `/site` Tout ce qu'un utilisateur fait en cliquant : `https://kartoon.io/api/v1/site/` (même méthode, même corps que le site). Exemples utiles : - régénérer un plan raté d'une vidéo : `/site/plans/{id}/regenerate` ; - doubler une vidéo existante dans une autre langue : `/site/studio/dub` ; - créer un perso à partir d'une description ou de photos : `/site/characters/generate` ; - lister les univers disponibles : `/site/studio/universes`. Le catalogue complet, généré depuis le code, est dans [Tout le site par API](#/site). Les routes sensibles (mot de passe, paiement, suppression du compte, administration) n'y sont pas. ## 12. Règles d'or pour un agent autonome 1. **Lire le budget avant chaque lancement**, et ne jamais dépasser ce qu'un humain a fixé. 2. **Un lancement = une intention claire.** Mieux vaut une vidéo bien briefée que trois essais vagues. 3. **Attendre `done`** avant de publier ; vérifier `warning`. 4. **Garder les `id`** (génération, vidéo, post) : tout se retrouve avec eux. 5. **Ne jamais exposer la clé** (logs, prompts, messages) : elle ouvre le compte et ses crédits. 6. **Réagir aux codes, pas aux messages** : les `message` sont en français et peuvent changer ; les `code` sont stables. # API Kartoon — tout le site (`/api/v1/site/`) Généré par `scripts/generer-doc-api-site.mjs` le 2026-10-10 — ne pas éditer à la main. Chaque action du site s'appelle par l'API, **au nom du compte de la clé**, exactement comme ses clics : mêmes règles, mêmes crédits, même flux en direct (SSE) pour les générations. Même authentification que l'API v1 (`Authorization: Bearer kx_live_…`). La méthode, les paramètres d'adresse et le corps passent tels quels ; la réponse revient telle quelle. Les routes dédiées et stables de l'API v1 ([Référence de l'API](#/api)) restent à préférer quand elles existent (`/videos`, `/characters`, `/products`, `/instagram/…`). Exemple : `GET https://kartoon.io/api/v1/site/characters/library` = la page « Mes personnages ». Bloquées : mot de passe et sessions, paiements et plan, RIB et versements d'affiliation, suppression du compte, administration, routes internes du moteur. 120 appels par minute. ## Routes (81) ### `POST` /api/v1/site/account/avatar > POST /api/account/avatar — bouton « Changer la photo » de /compte. > La photo va dans le bucket public, et son URL est ecrite dans `profiles.avatar_url`. > POURQUOI LA BASE ET PLUS SEULEMENT LES METADONNEES : `user_metadata.avatar_url` est REECRIT > par Google a CHAQUE connexion OAuth. La photo televersee y survivait jusqu'a la deconnexion, > puis disparaissait au profit de la photo du compte Google. La colonne `profiles.avatar_url` > (migration 0015), elle, n'appartient qu'a nous. Les metadonnees restent mises a jour en > second : elles n'ont plus valeur de verite, mais elles evitent un clignotement le temps que > /api/me bascule sur la colonne. ### `PATCH · DELETE` /api/v1/site/account > (pas de description dans le code) ### `GET · POST` /api/v1/site/affiliation/conditions > (pas de description dans le code) ### `GET` /api/v1/site/affiliation > (pas de description dans le code) ### `PATCH · DELETE` /api/v1/site/characters/{id} > PATCH /api/characters/:id — édite un perso CUSTOM (RLS bloque les persos univers). > La modif n'impacte que les futures vidéos : les anciennes gardent leur snapshot. > > DELETE /api/characters/:id — supprime un perso CUSTOM (univers protégés par RLS). */ ### `GET` /api/v1/site/characters/casting > GET /api/characters/casting[?univers=] — items SÉLECTIONNABLES pour le casting, À PLAT. > Chaque perso = 1 item, + 1 item par variante (posés juste après leur perso de base → > le front les affiche côte à côte). Inclut les persos commu piqués. Filtre univers optionnel. ### `POST · GET · DELETE` /api/v1/site/characters/generate > POST /api/characters/generate — lance la génération d'un perso EN TÂCHE DE FOND (survit au départ > de la page). Body : { id, mode, name, desc, style, gender, photos, refImages, count? }. > Renvoie { id, status, kind } immédiatement — le job continue côté serveur. > • count absent ou 1 (comportement historique) : 1 image, puis le perso est ENREGISTRÉ en base. > • count 2..3 (page Personnages, sept. 2026 : « 3 propositions, tu gardes la bonne ») : le job > génère `count` PROPOSITIONS et n'enregistre RIEN ; la page enregistre celle que l'utilisateur > garde via POST /api/characters/save, puis retire le job (DELETE ?id=). > Débit identique dans les deux cas : CREDIT_COST.characterCreate par job, remboursé si le job > échoue (_charJob.ts). 402 → { error, code:"NO_CREDITS", balance, cost }. > L'id du body identifie le JOB (dédup double-clic, poll, DELETE) ; la clé du DÉBIT, elle, est > tirée par le serveur à chaque lancement : un id déjà utilisé est refusé (409), il ne peut plus > servir à rejouer une génération à 0 crédit. > GET /api/characters/generate?cost=1 → { cost, proposals } (public : c'est un tarif). > GET /api/characters/generate?list=1 → { jobs:[{id, meta}], proposals:[{id, meta, proposals}], > failed:[{id, meta, error, creditsRembourses}] } : jobs ENCORE en cours (reconnexion au > (re)chargement), propositions terminées pas encore choisies, et ÉCHECS pas encore retirés — > sans ces derniers, un perso raté page fermée ne laissait aucune trace au retour. > GET /api/characters/generate?id= → { status, kind, character?, proposals?, error? } — poll. > DELETE /api/characters/generate?id= → retire un job TERMINÉ de ce compte (propositions traitées). ### `PATCH · DELETE` /api/v1/site/characters/library/{id} > PATCH /api/characters/library/[id] — renomme un perso de MA librairie (user_characters). > DELETE /api/characters/library/[id] — le supprime (ses variantes suivent en cascade FK). > > Créées pour la page Personnages (design v2) : les cartes offrent Renommer / Supprimer. > NB : la route legacy /api/characters/[id] vise l'ancienne table `characters` et ne > touche PAS la librairie — d'où ces routes dédiées. RLS (uc_owner_all) borne déjà > l'accès au propriétaire ; le .eq("user_id") est une ceinture supplémentaire. ### `GET` /api/v1/site/characters/library > GET /api/characters/library[?univers=] — « Mes personnages » : persos créés + persos > piqués de la commu, FUSIONNÉS. Chaque carte : portrait, nom, univers, compteurs > (versions · vidéos), et un flag source ("own" | "community") pour le badge commu. > Filtre univers OPTIONNEL (pas de tri imposé). ### `GET · POST` /api/v1/site/characters > GET /api/characters — persos univers (read-only) + persos custom de l'user. */ > > POST /api/characters — crée un perso custom (mutable, owner only). */ ### `POST` /api/v1/site/characters/save > POST /api/characters/save { job, index, name } — ENREGISTRE dans « Mes personnages » la proposition que l'utilisateur > garde parmi celles d'un job « propose » (POST /api/characters/generate avec count 2-3). Annoncée par > app/api/characters/generate/route.ts et _charJob.ts depuis septembre, jamais écrite : aucune page n'utilisait encore > les propositions. Le Marketing Studio d'Amir (01/10/2026, fenêtre « Nouvel avatar ») en montre trois, on en garde une > (ou plusieurs) avec « Enregistrer » puis un nom. > > Rien n'est débité ici : le job a été payé à son lancement. On n'enregistre que ce que CE compte a généré (charJob filtre > par propriétaire), et une même proposition ne crée qu'un seul perso : la renvoyer renomme celui déjà créé. > Les propositions vivent une heure (KEEP_PROPOSALS_MS) ; au-delà, le job est purgé et la demande échoue proprement. ### `GET` /api/v1/site/characters/surprise > GET /api/characters/surprise[?style=] — « Surprends-moi » : description de perso aléatoire > pour débloquer la création « inventer de zéro ». Renvoie { name, desc } (look pur, pas de personnalité). ### `GET` /api/v1/site/credits > GET /api/credits — solde de crédits du user (pour l'anneau autour de l'avatar). > Renvoie le solde + le plan + le plafond du plan (pour calculer la jauge de l'anneau). ### `GET` /api/v1/site/explorer/feed > GET /api/explorer/feed — payload de la page d'accueil Explorer. > Renvoie : à la une (featured), récentes, plus likées, et par univers. > VIDÉOS PRIVÉES (18/09/2026, demande du propriétaire) : le fil ne montre QUE les vidéos du compte connecté. > Avant, c'était le fil de toute la communauté (RLS « using true ») ; la politique videos_read_owner > verrouille aussi côté base, ce filtre serveur est la ceinture avec les bretelles. Sans compte : fil vide. ### `GET` /api/v1/site/explorer/my-personas > GET /api/explorer/my-personas — persos de la communauté que J'AI sauvegardés. > Renvoyés au MÊME format que /api/studio/library (le picker studio peut les fusionner) : > views.front = URL Storage publique → utilisable directement comme image + comme réf > (le pipeline produce sait télécharger une réf en http://). ### `POST · DELETE` /api/v1/site/explorer/personas/{id}/save > POST /api/explorer/personas/[id]/save → 410 (import retiré le 18/09/2026) > DELETE /api/explorer/personas/[id]/save → le retire > Par-utilisateur (RLS). On NE touche PAS à la library.json globale du studio. ### `GET` /api/v1/site/explorer/personas/stars > GET /api/explorer/personas/stars — « stars de la commu » RETIRÉES le 18/09/2026 : les persos sont > privés, chaque compte ne voit que les siens (migration 0026). Liste vide gardée pour les pages en cache. ### `GET` /api/v1/site/explorer/series/{id} > GET /api/explorer/series/[id] — une série + ses épisodes (ordre croissant), > avec le DERNIER épisode (latest) et le PLUS VU (biggest) → points d'entrée du lien viral. > ⚠️ Plus « publique » depuis les vidéos privées (18/09/2026, 0025 : RLS videos_read_owner) : on ne lit > que SES séries ; celle d'un autre compte répond « introuvable ». ### `POST` /api/v1/site/explorer/videos/{id}/like > POST /api/explorer/videos/[id]/like — like / unlike (toggle) au nom de l'utilisateur. > Utilise le RPC atomique toggle_video_like (pose/retire la ligne + maj le compteur). ### `GET` /api/v1/site/explorer/videos/{id}/remix > GET /api/explorer/videos/[id]/remix — données pour REMIXER une vidéo (lien viral). > Renvoie le brief d'origine + le casting → le studio peut pré-remplir l'idée et les persos. > ⚠️ Plus « publique » depuis les vidéos privées (18/09/2026, 0025 : RLS videos_read_owner) : on ne > remixe que SES vidéos ; le lien d'une vidéo d'un autre compte répond « introuvable ». ### `GET · PATCH · DELETE` /api/v1/site/explorer/videos/{id} > GET /api/explorer/videos/[id] — détail d'une vidéo publiée + casting + état « liké ». > ⚠️ Plus « publique » depuis les vidéos privées (18/09/2026, 0025 : RLS videos_read_owner) : le détail > n'est servi qu'à son propriétaire (et aux admins, cf. GET). Si l'utilisateur est connecté, on renvoie aussi `liked`. ### `POST` /api/v1/site/explorer/videos/{id}/view > POST /api/explorer/videos/[id]/view — incrémente le compteur de vues. > PUBLIC (les vues comptent aussi pour les visiteurs non connectés). RPC bump_video_view. ### `GET` /api/v1/site/explorer/videos > GET /api/explorer/videos — liste filtrable + paginée (pour « voir tout » / scroll infini). > Params : sort=recent|likes|views, univers=fruit|pixar|animal|clay, q=, > limit (≤50, défaut 24 ; ≤1000 en vue admin), offset (défaut 0), mine=1 (MES vidéos, filtrées SERVEUR). > Réponse : { videos, count, limit, offset, has_more, next_offset, … } — chaque vidéo porte `own` (elle est au compte > qui lit ; false seulement dans la vue admin, sur les vidéos des AUTRES comptes : ni renommables ni supprimables). > VIDÉOS PRIVÉES (18/09/2026) : avec ou sans mine=1, on ne liste JAMAIS les vidéos des autres comptes — > seulement celles du compte connecté (l'admin garde sa vue d'exploitation via mine=1). Sans compte : liste vide. ### `GET` /api/v1/site/explorer/vitrine > GET /api/explorer/vitrine?videos=8&persos=5 — la VITRINE publique (06/10/2026, page des tarifs : « des vidéos random en > bas, des personnages random en haut »). > > PUBLIQUE : aucune session. Depuis le 18/09 tout est privé (vidéos, persos, bucket) et /api/explorer/videos répond vide à un > visiteur ; ici on lit par le client service_role et on FILTRE nous-mêmes : seulement les vidéos et les persos des > FONDATEURS (les adresses admin, lib/admin.ts), jamais une pub (is_ad : les photos du produit d'un client), jamais un compte > client, jamais un perso fait d'après une PHOTO (origin « photo » : le visage d'une vraie personne), jamais un univers masqué > ou réservé aux admins. Rien d'identifiant ne sort (ni creator_id ni nom de compte), seulement des adresses SIGNÉES du > stockage (copies légères : mini-copie 8 s pour la tuile, copie 1080p pour la visionneuse, vignette), comme > /api/explorer/videos — et JAMAIS l'original. Tirage AU HASARD à chaque appel ; le lot (≤ 150 vidéos, ≤ 150 persos) est > relu toutes les 2 minutes et GARDÉ tel quel si la relecture échoue (une panne passagère ne vide pas la page). Aucune copie > n'est fabriquée depuis ici (un visiteur anonyme ne déclenche pas de ffmpeg) : une vidéo sans mini-copie ni vignette est > simplement écartée du tirage. ### `DELETE` /api/v1/site/instagram/comptes/{id} > DELETE /api/instagram/comptes/{id} — déconnecte un compte Instagram de CE compte Kartoon : le jeton est effacé, la ligne > reste (historique des posts). Session obligatoire. Pour retirer aussi l'accès côté Instagram : Paramètres → Apps et > sites web, dans l'application Instagram. ### `GET` /api/v1/site/instagram/comptes > GET /api/instagram/comptes — les comptes Instagram connectés au compte Kartoon de la session (page Compte, onglet > Instagram), sans aucun jeton. `configured` : l'app Meta est-elle branchée ; `allowed` : offre Business ou admin. ### `GET` /api/v1/site/me > GET /api/me — profil de l'user courant (crédits, plan, identité). */ ### `GET` /api/v1/site/media/{path…} > GET /api/media/ — fichier du bucket, réservé à son propriétaire (18/09/2026). > Vérifie la session puis renvoie (302) une URL signée courte : lecture et seek vidéo passent > par le CDN de Supabase, pas par la machine Fly. ?w= = vignette redimensionnée, > ?download= = téléchargement, ?lecture=1 = copie légère de lecture d'une vidéo (lib/lecture.ts), > ?vignette=1 = mini-copie des vignettes qui jouent dans les grilles (8 s, 360 px, muette). > univers/ reste public (couvertures du catalogue). ### `POST` /api/v1/site/plans/{id}/regenerate > POST /api/plans/:id/regenerate — re-roll de la vidéo d'un SHOT. > Phase 1 : mock (la vraie génération Kling vient en Phase 4). ### `POST` /api/v1/site/plans/{id}/reorder > POST /api/plans/:id/reorder — monte/descend un SHOT dans sa scène (swap shot_number). */ ### `PUT · DELETE` /api/v1/site/plans/{id} > PUT /api/plans/:id — édite un SHOT (cadrage, réplique, durée, prompt visuel). */ > > DELETE /api/plans/:id — supprime un SHOT. */ ### `POST` /api/v1/site/plans/{id}/startframe > POST /api/plans/:id/startframe — (re)génère la start frame d'un SHOT. > Accepte un refinement optionnel (panel "Détails"). Passe le shot en "framed". ### `POST` /api/v1/site/projects/{id}/frames > POST /api/projects/:id/frames > 1 start frame PAR SHOT (+ end frame si needs_end_frame), via planShotFrame qui > consomme les MASTERS (P2). PAS de vidéo (P4b). > Ordre des références (documenté, cf. planShotFrame) : > [ décor master (ANCRE) → master(s) perso présents → frame du shot N-1 ]. > SEED déterministe par shot, nourri par les seeds des masters → frame rejouable. > Garde-fou coût : compte les appels nano + coût estimé (~0,15 $/image) en live. ### `POST` /api/v1/site/projects/{id}/generate > POST /api/projects/:id/generate > Phase 1 : STUB d'orchestration par SHOT. La vraie génération vidéo (Kling v3 pro, > end frame, slot audio) est branchée en Phase 4. Ici, en mock, chaque shot reçoit > un placeholder et passe son cycle de vie generating → generated → selected. ### `GET · DELETE` /api/v1/site/projects/{id} > GET /api/projects/:id — projet + script + scenes (→ shots) + casting dispo. */ > > DELETE /api/projects/:id — supprime le projet (cascade scripts → scenes → shots). */ ### `POST` /api/v1/site/projects/{id}/script > POST /api/projects/:id/script > Cerveau scène → SHOTS : génère le script découpé, PERSISTE scenes + shots. > Les start frames ne sont PAS générées ici (Phase 3) → shots en statut "pending". > Crée un job d'orchestration "frames" (queued). Débite les crédits. ### `GET · POST` /api/v1/site/projects > GET /api/projects — liste des projets de l'user (récents d'abord). */ > > POST /api/projects — création d'un projet. > Le mode (pub/histoire) est DÉDUIT de la présence d'un produit (pas de toggle). ### `POST` /api/v1/site/script/plot-twist > POST /api/script/plot-twist — réécrit le plot twist via une instruction (IA). > Body : { plotTwist, instruction }. Réutilise askClaude. (L'édition 100% manuelle = côté front.) ### `POST` /api/v1/site/script/save > POST /api/script/save — sauvegarde les modifs du script (plot twist + dialogues édités à la main). > Met à jour engine/script.json ; si on édite une unité de projet (.current_part.json présent), > répercute aussi dans story_scripts. NE régénère RIEN (édition manuelle directe). > > Éditeur plan par plan (refonte du 11/09/2026) : `remove:[n…]` SUPPRIME des plans (scène retirée > du script + frame.png / scene.mp4 effacés), puis les plans restants sont renumérotés 1..N > avec le même renommage en 2 phases que le réordonnancement. Le montage final ne change qu'au > prochain produce?resume=1 (régénération d'un plan) : cette route ne relance rien. > 26/09/2026 (audit) : les médias d'un plan changent de numéro — ou disparaissent — AVEC leurs caches (vignettes, aperçu, > repérage des répliques : _apercu.ts) ; l'ordre et les suppressions ne sont refusés que si CETTE vidéo est occupée > (génération, régénération, export), plus pour une génération d'une autre vidéo du compte. ### `POST` /api/v1/site/studio/ad-product > POST /api/studio/ad-product — dépose le PRODUIT d'une pub (Marketing Studio) AVANT de lancer > la génération. Les photos (data URLs) ne passent pas dans la query GET du SSE → on les écrit ICI > sur disque (refs de l'espace du compte : engine/u//refs/ad--*.jpg) + un manifest > engine/ad-products/.json que le job de production (produce?mode=ad&gid=…) lira à SON > démarrage (fichiers scopés par gid → deux pubs en file ne se clobbent pas). > Body : { gid, name, desc?, photos: [dataURL | /api/media/… | https…], media?: [idem] } > ou { gid, produitId } (01/10/2026) : un produit de la bibliothèque du compte (/api/studio/produits), copié tel quel. > /api/media/ (26/09/2026, audit) : forme des images de la bibliothèque depuis le bucket privé (un portrait mis > dans la barre par « Ajouter comme référence ») — lu par la clé serveur UNIQUEMENT si le fichier est au compte > (../ref-images/_images-du-compte.ts, mêmes règles que GET /api/media). Avant, l'URL publique du bucket d'un AUTRE compte > passait par fetchMedia et sa photo était copiée ; une adresse extérieure partait en fetch nu depuis la machine de prod. ### `POST` /api/v1/site/studio/cast-from-library > POST /api/studio/cast-from-library — caste une vidéo depuis la librairie DB. > Body : { items: [{ characterId, variantId?, source? ("own"|"community") }] }. > Résout chaque perso (image du perso ou de la variante choisie) et écrit le characters.json > de l'espace du compte (ref = URL publique, téléchargée par le pipeline produce). → le casting > devient « plug ». > > NB : on renvoie la liste des univers présents pour info, mais on n'IMPOSE PAS la contrainte > d'univers (volontairement non bloquante, comme décidé). ### `POST` /api/v1/site/studio/charge > POST /api/studio/charge — DÉBITE réellement les crédits au moment du « confirmer ». > Le front l'appelle juste avant de lancer la prod, ou avant un doublage en langues payantes. > (Le coût est d'abord affiché via /api/studio/cost et /api/studio/dub-cost.) > > Body production : { kind:"production", units?, duree? } > Body doublage : { kind:"dub", langs? } ou { kind:"dub", extra? } > > Débit via le RPC spend_credits (SECURITY DEFINER) : impossible de s'auto-créditer, > solde insuffisant → 402. Ne touche PAS au pipeline (prod/doublage = leurs routes). ### `GET` /api/v1/site/studio/charimg > GET /api/studio/charimg?f= — sert une image de perso importé depuis > engine/refs (symlinké sur /data en prod). Sanitize : basename seul, pas de "..". > Sert au runtime (contrairement à /public qui n'est figé qu'au build). ### `GET` /api/v1/site/studio/cost > GET /api/studio/cost?duree=&units=&format= — coût en crédits AVANT lancement. > Renvoie : coût par unité + total, le solde du user, et s'il peut se le permettre. > PUBLIC (26/09/2026, audit) : c'est un TARIF, le même pour tous — un visiteur recevait 401 et les pages n'avaient plus > de prix à afficher. Sans session : le coût seul, `balance`/`affordable` à null et `connecte: false` (la page s'en sert > pour proposer la connexion au moment de lancer, comme elle le faisait sur le 401). ### `GET` /api/v1/site/studio/dub-cost > GET /api/studio/dub-cost?langs=3 (ou ?extra=2) — coût des langues SUP de doublage. > La langue principale est incluse ; chaque langue en plus est payante. ### `GET` /api/v1/site/studio/dub > GET /api/studio/dub?video=... — SSE. > Lance `node dub.js /