Formats

24 — Webtoon : le format toon et webtoon.recit.dev

Statut : proposé · Phase : P7 · Mis à jour : 02/11/2026.

Ce plan détaille la phase P7 de 06 · Feuille de route : le format toon (webtoon, ruban vertical de cases), son jumeau JSON, son lecteur, ses exports (tranches, PDF, film) et le site webtoon.recit.dev. Il part de la spécification de 08 · Formats §2, qu'il précise sans revenir sur les décisions acceptées ; chaque désaccord avec un plan accepté est posé en question ouverte (§12), jamais tranché ici. Il s'appuie sur le code tel qu'il est au 02/11/2026, pas sur l'ancien texte.

Il dépend de 15 · Export PDF (§3.5 et PDF2, accepté), de 09 · Langues et JSON, de 14 · IA et scénario (lot IA0 logé en P7, bible et attributs de scène), de 11 · Images et 16 · Atelier de personnages (personnages, Broc), de 03 · Packaging §6 et §8, de 07 · Domaines et de 21 · Tests. Le plan 23 (report, P6) livre la chaîne d'impression dont P7 a besoin ; le plan 25 (book, P8) partage la question des récits en plusieurs fichiers (séries ici, chapitres là-bas).

1. Pourquoi

  • Le webtoon est la forme native de la lecture au pouce. Un ruban vertical de cases se lit sur un téléphone sans zoom ni pagination. C'est le premier format de Récit où l'image porte le récit et où le texte se pose dessus.
  • Le lettrage est du texte. Dans une BD classique, les bulles sont peintes dans l'image : la traduire, c'est la redessiner. Ici les cases sont des images sans texte et les bulles sont des données typées (position, forme, texte). Le texte est donc traduisible, sélectionnable, indexé par la recherche, lu par un lecteur d'écran, et une seule série d'images sert toutes les langues.
  • Une source, quatre sorties. Le même épisode se lit au scroll sur le web, s'exporte en tranches d'images pour les plateformes, en PDF (une décision acceptée : le webtoon doit l'avoir) et devient un film vertical 9:16 par le pont toon → motion.
  • Le premier client du scénario. Le plan 14 a choisi le webtoon pour son épisode de démonstration (sites/plans/pages/annexes/broc/scenario-3.md, « Tempo a filé », 12 cases). Le format doit pouvoir l'exprimer sans exception, même si sa publication attend P16 et la licence de l'image de référence.

2. Ce qui existe déjà et ce qu'on réutilise

Relevé dans le dépôt le 02/11/2026.

ÉlémentÉtatCe que P7 en fait
packages/formats/toonstub : package.json privé (@recit/toon 0.0.0, scope:format), src/index.ts videdevient un paquet publiable, sur le modèle de packages/formats/deck (tsdown, README.md, LICENSE.md, ./styles.css)
@recit/core/schema (source.ts)ToonSourceSchema = genericSource("toon") : scènes GenericSceneSourceSchema (objet lâche), settings lâche. toon est déjà dans STORY_FORMATS, et packages/core/schema/v1/toon.json est publiéle schéma devient strict, avec des champs typés, comme DeckSourceSchema et MotionSourceSchema ; ajout compatible en v1 (règle B1)
convert.ts, parity.ts, assemble.tsconversion Markdown ⇄ JSON par format (deckScene, docsScene, motionScene, genericScene) ; parité des variantes de langue ; jumeau (kind: "story") et compagnon (kind: "sidecar") par resolveStoryFilesune fonction toonScene et son écriture inverse ; la parité compare aussi le lettrage
Séparateur de scène (split-slides.ts)attributs --- clé=valeur, valeurs entre guillemets acceptées (alt="…"), typage par SlideMetadataSchema. La forme sur une ligne :::notes texte ::: est un cas particulier de :::notes (l. 235)réutilisé tel quel pour src, alt, height, gutter, background
remark-directive dans le cœurles directives :::nom{…} sont lues par le pipeline unifié de @recit/core:::bubble, :::caption, :::sfx : noms au registre du cœur (règle B4), sens dans @recit/toon
Round-trip des exemplespackages/core/src/__tests__/source-conversion.spec.ts vérifie toute story de examples/ et sites/*/stories, que son JSON soit sur disque ou nonexamples/toon-* y entrent sans code de test en plus
@recit/motion et @recit/motion-blocksle pont storyToFilm (source/story.ts) ; huit blocs (title, statement, stat, compare-bars, chapter, list, ui-mock, ui-scene) ; aucun bloc ne montre une imagele pont toon → motion demande un bloc nouveau, panel (§5.4)
La CLIrecit dev a un aperçu propre au motion (src/motion/client.tsx, bundlé par esbuild) ; recit export sort le PDF ou le HTML du deck, sans --to ; recit render rend un récit motion ; recit convert --to json|md|filmun aperçu toon sur le même modèle ; --to images et --to pdf arrivent avec la chaîne de P6 ; render accepte un toon
@recit/paged/runnern'existe pas encore : le stub est packages/formats/paged, déplacé dans packages/tools/paged en PDF1 (P6, plan 23)prérequis de W3 (tranches) et de PDF2
@recit/site-storiesplugin.tsx choisit le lecteur par format : MotionViewer pour motion, StoryViewer (deck) sinon ; vite.ts compile les stories et tente storyToFilmune branche toon vers ToonReader, chargée paresseusement
@recit/site-kitformats.ts décrit déjà toon (« Webtoon ») ; SITES (sites.ts) ne connaît pas encore webtoonajout de webtoon (port 3005, 07 §4)
@recit/componentsuseCssVariable (valeurs dynamiques sous la loi du plan 19), composants partagéspositions des bulles en variables CSS
sharpabsent du lockfile ; allowBuilds.sharp: false dans pnpm-workspace.yamlvoir §5.2 et question 4 : la découpe peut se passer de sharp
Scénario de Brocsites/plans/pages/annexes/broc/scenario-3.md : source Markdown complète et brouillon du jumeau JSON d'un épisode de 12 cases ; image de référence examples/scenario-lebrocoli/cast/broc/reference.png (© vguerand, tous droits réservés)banc d'essai du modèle (W0) ; publication en P16 seulement, dans examples/scenario-lebrocoli

Ce qu'on ne crée pas : pas de second parseur Markdown, pas de moteur de mise en page, pas de bibliothèque de bulles, pas de lanceur de Chromium dans @recit/toon (la CLI capture par @recit/paged/runner, 03 §8), pas de schéma de type hors de @recit/core.

3. Vocabulaire

  • Une scène de toon est une case (panel) : « case » est le mot du lecteur, de l'interface et de la doc, comme « slide » pour le deck. Le modèle garde Scene, sceneIndex.
  • Un épisode est une story format: toon. Une série est un dossier d'épisodes (§4.6).
  • Le lettrage désigne les bulles, encarts et onomatopées d'une case : ce qui se traduit.
  • Une tranche (slice) est une image découpée du ruban pour une plateforme ; une page est une page du PDF. Les deux découpages sont distincts (15 §3.5).
  • Le rendu motion d'un épisode est un film, jamais un « deck ».

Ces mots sont à ajouter au glossaire de AGENTS.md par l'intégrateur (W7), ce plan ne le modifie pas.

4. Le modèle

4.1 Markdown

Même grammaire que tous les formats : frontmatter, scènes séparées par --- avec leurs attributs, directives :::. Réglages à plat dans le frontmatter (08, préambule).

MarkdownMarkdown
--- title: Lorem Ipsum — Episode 1 format: toon lang: en width: 800 gutter: 120 background: "#101014" font: lettering --- --- id=p1 src=./panels/01.webp alt="A lorem street at night, empty, under a single lamp." :::caption Somewhere, dolor sit amet. ::: :::bubble{x=62% y=18% tail=bottom-left} Lorem ipsum? ::: --- id=p2 src=./panels/02.webp height=1400 gutter=40 alt="A close-up on two ipsum figures." :::bubble{x=30% y=12% shape=shout} Ipsum! ::: :::sfx{x=70% y=60% rotate=-12} BOOM ::: :::notes Author note, never published. Becomes the voice-over in the film. :::
ÉlémentRôle
srcimage de la case (chemin relatif, partagée entre langues ; variante 01.fr.webp possible, 09 §1)
altdescription de l'image, localisable ; manquante = avertissement (15 Q6, tranché)
heighthauteur en px à la largeur de référence ; par défaut, celle de l'image mise à l'échelle
gutterespace après la case, en px ; par défaut le gutter de l'épisode
backgroundcouleur d'une case sans image (case de transition, noir de fin) ; un seul nom, jamais bg (règle B4)
:::bubble{x y w tail shape cast}bulle. x, y : centre de la bulle en % de la case ; w : largeur maximale en % (40 par défaut) ; tail ; shape : speech, thought, shout, whisper ; cast (§4.5)
:::caption{x y w}encart narratif
:::sfx{x y rotate size}onomatopée en texte vectoriel
:::notesnotes d'auteur, jamais publiées dans le lecteur ; voix off dans le film (§5.4)
beat, cast, set, shot, pose, promptattributs réservés du scénario (B4, 14 D4) : gardés dans attrs sans erreur en P7, vérifiés par @recit/scenario en P16

Règles :

  • L'ordre du Markdown est l'ordre de lecture des éléments de lettrage d'une case, et l'ordre du DOM. Rien n'est trié par position (règle d'emaki : jamais de devinette).
  • Un élément de lettrage sans x ni y est empilé en haut de la case, dans l'ordre : c'est la forme demandée par le plan 14 D6 pour les répliques venues d'un :::dialogue ou d'un pont.
  • x, y, w s'écrivent en pourcentage (62%), de 0 à 100 ; une valeur hors bornes est une erreur avec numéro de ligne. Le débordement réel (bulle trop grande pour son texte) ne se mesure qu'au rendu : avertissement du lecteur en dev et du runner à l'export.
  • Le corps d'une case ne contient que du lettrage et :::notes. Toute autre chose (paragraphe, titre, autre directive) est une erreur avec numéro de ligne, pas un texte affiché au hasard. Les blocs transverses (```pixel, plus tard) s'ajouteront par la règle B2.
  • Une case sans src ni background est dessinée comme une case de remplacement : un cadre neutre qui montre l'alt et, s'il existe, l'id de prompt=. Avertissement, erreur en --strict. C'est le point d'accroche des images de remplacement du plan 14 D7.
  • Image introuvable : erreur avec numéro de ligne (08 §2). Poids d'une image et de l'épisode au-delà du budget (budget.panelBytes, budget.episodeBytes) : avertissement.
  • La forme canonique des directives est sur plusieurs lignes (ouverture, texte, :::). La forme sur une ligne de 08 §2 (:::bubble{…} Lorem ipsum? :::) n'est pas lue par le pipeline actuel, qui ne la connaît que pour :::notes : question 2.

4.2 Le jumeau JSON

Le schéma vit dans packages/core/src/schema/source.ts, en zod/mini comme les autres ; les types sont dérivés (z.infer) et exportés depuis @recit/core/schema. Esquisse :

TypeScriptTypeScript
/** A position or a width, in percent of the panel (written `62%` in Markdown) */ const PercentSchema = z.number().check(z.gte(0), z.lte(100)); const letteringFields = { x: z.optional(PercentSchema), y: z.optional(PercentSchema), /** Inline Markdown (emphasis only); one text per language */ text: LocalizedSchema, }; export const ToonBubbleSchema = z.strictObject({ type: z.literal("bubble"), ...letteringFields, w: z.optional(PercentSchema), tail: z.optional(z.enum(TOON_TAILS)), // bottom-left, bottom, bottom-right, top-…, left, right, none shape: z.optional(z.enum(["speech", "thought", "shout", "whisper"])), /** Speaker: an id of the bible (plan 14); its name is the accessible label */ cast: z.optional(z.string().check(z.minLength(1))), }); export const ToonCaptionSchema = z.strictObject({ type: z.literal("caption"), ...letteringFields, w: z.optional(PercentSchema), }); export const ToonSfxSchema = z.strictObject({ type: z.literal("sfx"), ...letteringFields, rotate: z.optional(z.number().check(z.gte(-180), z.lte(180))), size: z.optional(z.enum(["s", "m", "l", "xl"])), }); export const ToonLetteringSchema = z.discriminatedUnion("type", [ ToonBubbleSchema, ToonCaptionSchema, ToonSfxSchema, ]); export const ToonSceneSourceSchema = z.strictObject({ id: sceneFields.id, attrs: sceneFields.attrs, src: z.optional(z.string().check(z.minLength(1))), alt: z.optional(LocalizedSchema), height: z.optional(z.int().check(z.positive())), gutter: z.optional(z.int().check(z.gte(0))), background: z.optional(z.string()), /** Bubbles, captions and sound effects, in reading order */ lettering: z.optional(z.array(ToonLetteringSchema)), notes: sceneFields.notes, }); export const ToonSettingsSourceSchema = z.looseObject({ width: z.optional(z.int().check(z.positive())), // 800 gutter: z.optional(z.int().check(z.gte(0))), // 120 background: z.optional(z.string()), /** A font role of the theme (`lettering`) or a family it ships */ font: z.optional(z.string()), direction: z.optional(z.enum(["ltr", "rtl"])), budget: z.optional(z.strictObject({ panelBytes: …, episodeBytes: … })), pdf: z.optional(ToonPdfOptionsSchema), // PDF2.1, plan 15 images: z.optional(ToonImagesOptionsSchema), // W3 }); export const ToonSourceSchema = z.strictObject({ ...storyFields, format: z.literal("toon"), settings: z.optional(ToonSettingsSourceSchema), scenes: z.array(ToonSceneSourceSchema), });
  • Un seul tableau ordonné, lettering. 09 §2.3 (accepté) écrit trois tableaux, bubbles[], captions[] et sfx[]. Trois tableaux perdent l'ordre entre un encart et une bulle, donc l'ordre de lecture, et l'aller-retour Markdown → JSON → Markdown ne rend plus le même texte (dans le scénario 3, la case c01 ouvre sur l'encart, la case c12 le met à la fin). Ce plan propose un tableau unique discriminé sur type ; c'est la question 1, à trancher avant W0. type ne désigne ici que la sorte d'élément de lettrage, à l'intérieur de l'objet ; l'attribut de scène type= reste réservé aux blocs motion (B4).
  • x et y sont des nombres en JSON et des pourcentages en Markdown (62% ⇄ 62). Le brouillon du scénario 3 gardait des chaînes "58%" en laissant le choix à W1 : le nombre se valide par le schéma (bornes), la chaîne demanderait un refine.
  • src et alt restent à plat (09 §2.3), et non un objet image: { src, alt } comme le DeckImageSchema du deck : l'alt d'une case vaut aussi sans image (case de remplacement, balisage du PDF).
  • ToonPdfOptionsSchema et ToonImagesOptionsSchema vivent dans packages/core/src/config/validate.ts avec les autres options d'export (15 §3.2), et la section toon: de recit.config.ts est ToonConfigSchema (même champs, forme imbriquée, 08 préambule). Aucun réglage ne s'appelle kind.
  • @recit/toon ne redéfinit aucun de ces types : il les importe.

4.3 Conversion et aller-retour

  • markdownToSource gagne toonScene (dans convert.ts, à côté de motionScene) : les attributs typés du séparateur passent dans leurs champs, les autres restent dans attrs ; les directives de lettrage du corps, lues par le même AST que le reste du cœur, deviennent lettering, dans l'ordre ; :::notes devient notes. Un reste dans le corps est une erreur avec sa ligne (§4.1).
  • sourceToMarkdown écrit la forme canonique : attributs dans l'ordre id src alt height gutter background, puis attrs, directives sur plusieurs lignes, attributs de directive dans l'ordre du schéma, pourcentages avec %.
  • Propriété testée : compile(md) == compile(toJson(md)) et toMd(toJson(md)) == md pour toute source canonique (09 §2.4), sur des fixtures Lorem et sur les exemples.
  • Le compagnon (kind: "sidecar") complète une case par son id : utile pour des alt ou des textes venus d'un CMS. Il ne redéfinit jamais la liste des cases (règle d'AGENTS.md).

4.4 Langues

  • episode-1.fr.md est la même page en français (09 §1) : mêmes id, mêmes images, mêmes positions, textes traduits. checkStoryParity vérifie en plus que le lettrage a le même nombre d'éléments, les mêmes type, x, y, w, shape ; le texte identique à la langue par défaut donne l'avertissement untranslated.
  • 08 §2 et le scénario 3 (§4.3) décrivent un fichier de langue qui ne contient que les textes. Ce fichier n'est ni une variante complète (la parité refuse des attributs de structure manquants), ni un compagnon. Ce plan ne le crée pas : la traduction partielle passe par episode-1.fr.json (kind: "sidecar"), et recit i18n sait déjà produire la variante complète. Question 3.
  • Dans le JSON, un texte porte toutes ses langues ({ "en": "…", "fr": "…" }) : positions écrites une fois, textes par langue. C'est la forme la plus sûre pour un outil de traduction.
  • Les bulles grandissent avec leur texte (largeur maximale w, hauteur libre) ; un débordement de la case est un avertissement, par langue.
  • direction: rtl pose dir="rtl" sur le lecteur et retourne l'interface. Il ne réordonne pas le lettrage : l'auteur écrit dans l'ordre de lecture (question 7, en lien avec 15 §3.5).

4.5 Scénario, personnages et images

  • Les attributs beat, cast, set, shot, pose, prompt sont réservés (B4) et lus par le plugin de @recit/scenario en P16. En P7, @recit/toon les garde dans attrs et ne les interprète pas, sauf prompt= dont il affiche l'id sur une case de remplacement.
  • Qui parle. Sans :::dialogue (plan 14, plus tard), le locuteur n'est donné que par la queue de la bulle, invisible pour un lecteur d'écran. Ce plan propose cast= sur :::bubble : le nom affiché au lecteur d'écran est celui de la bible quand elle existe (P16), sinon l'id lui-même. Question 5.
  • Les images ne sont jamais générées par Récit (14 D7, 11). Les panneaux viennent de l'auteur ; leur provenance et la mention « généré par IA » arrivent avec le registre d'assets du plan 14.
  • Broc. Son épisode (scénario 3) vit dans examples/scenario-lebrocoli/toon/ (règle B7, un seul dossier lebrocoli), en P16, après la licence de l'image de référence. P7 s'en sert comme banc d'essai du schéma (W0) avec une copie Lorem de sa structure, sans copier ni son texte ni ses images dans un exemple publié.
  • Les blocs ```pixel et sprite (plan 11, plan 16) pourront fournir l'image d'une case plus tard, « sur besoin réel » (11 D7) : rien en P7.

4.6 Séries

Un dossier = une série. 08 §2 propose un series.yaml ou le frontmatter du premier épisode. Tout document de Récit a un jumeau JSON typé : ce plan propose un series.json (kind: "series", schéma SeriesSchema dans @recit/core/schema, titre localisable, couverture, ordre des épisodes), ajout compatible en v1 (B1), et pas de YAML. Le plan 25 pose la même question pour les chapitres d'un livre (settings.chapters) : une seule réponse pour les deux. Question 8. Les séries sont le lot W6, après les critères de sortie de P7.

5. Le rendu

5.1 Le lecteur web

Composants de @recit/toon, tous anonymes, chacun avec son X.module.css, sa story et son play :

ComposantRôle
ToonReaderracine : ruban vertical, largeur min(100%, width), dir, langue ; sommaire de l'épisode
Panelune case : <figure>, image avec width/height intrinsèques (pas de décalage de mise en page), loading="lazy", decoding="async", gouttière après
Bubbleune bulle : texte HTML posé sur une forme SVG (speech, thought, shout, whisper) avec sa queue ; position par variables CSS (useCssVariable)
Captionun encart
Sfxune onomatopée en SVG <text>, aria-hidden sauf si l'auteur la marque comme porteuse du récit
PlaceholderPanelcase de remplacement (§4.1)
EpisodeNavépisode précédent et suivant (W6)
  • Mobile d'abord. La case suit la largeur de l'écran ; la taille du lettrage suit la largeur de la case par les unités de conteneur (cqw), en CSS simple (pas de PostCSS hors du framework de sites, plan 19). Rien ne demande de zoom à 360 px.
  • Bulles en HTML et SVG. Le texte reste sélectionnable, indexé par Pagefind (site-stories/search-index.ts), lu dans l'ordre du DOM. Chaque case est une figure dont l'alt décrit l'image et dont le lettrage suit en texte.
  • Apparition au scroll avec motion : m et LazyMotion avec domAnimation chargé à part (règle « Bundle Size » d'AGENTS.md), coupée sous prefers-reduced-motion.
  • Mode de couleur. Les cases sont des images ; la gouttière prend background de l'épisode ; le reste de l'interface suit data-mantine-color-scheme par light-dark(). Pas de .dark.
  • Poids. Images servies telles que l'auteur les fournit, au format choisi par lui (WebP ou AVIF recommandés) ; la CLI vérifie le budget. Les variantes responsives (srcset) supposent un encodeur : question 4.
  • Plus tard : un mode « case par case » au tap (lecture guidée), sur demande réelle.

Lettrage : une police de lettrage OFL servie localement par @recit/theme, avec son OFL.txt, comme Lexend, et un jeton de rôle (--recit-font-lettering). font: lettering la choisit ; une autre famille du thème reste possible (le scénario 3 utilise lexend). Le choix de la police est la question 6.

5.2 Les tranches pour les plateformes (W3)

  • recit export episode.md --to images --preset webtoon-canvas produit des PNG, JPEG ou WebP de la largeur du préréglage, coupés à hauteur fixe, lettrage compris (une série de tranches par langue, --all-locales). Les préréglages sont des données versionnées, avec leur date de vérification (08 §9) ; les valeurs des plateformes sont à vérifier au moment de W3, aucune n'est promise ici.
  • La CLI sert le build du lecteur, l'ouvre par @recit/paged/runner et capture chaque tranche par une capture découpée (clip) de la page : pas d'image géante en mémoire, pas de limite de texture. L'encodage WebP ou JPEG se fait dans Chromium (canvas.toBlob). sharp n'est alors plus nécessaire ; 08 §2 et 03 §6 le prévoient : question 4.
  • 08 §2 dit « en évitant de couper une bulle », 15 §3.5 dit « coupées à hauteur fixe sans égard aux cases ». Les plateformes recollent les tranches sans joint : une coupe ne se voit pas. Ce plan propose la hauteur fixe par défaut et une option avoidLettering (la coupe remonte au-dessus d'un élément de lettrage, dans une limite) : question 9.
  • Les fichiers sont listés dans exports.json (ExportManifestSchema, variante kind: "images", 15 §3.2) ; le panneau d'exports du site les lit.

5.3 Le PDF (PDF2, plan 15)

Tout est décidé dans 15 §3.5 et PDF2 ; ce plan ne fait que le loger :

  • pagination: "panels" par défaut : pages de la largeur du préréglage, hauteur pageHeight indépendante des tranches, coupe toujours entre deux cases (break-inside: avoid), case trop haute sur sa propre page avec un avertissement, fond de gouttière en bas de page.
  • @recit/toon fournit la vue imprimable (ToonPrintView, même composants, sans animation, variantes JPEG des cases) ; la CLI mesure la géométrie et imprime par @recit/paged/runner.
  • Les variantes JPEG sont produites dans Chromium au moment de l'impression (comme les tranches), sauf si la question 4 retient sharp.
  • maxBytes : budget par PDF, avertissement, erreur en --strict. Un PDF par langue, balisé, avec signets (un par case qui porte un caption ou par id, à fixer en PDF2.2) et lang/dir.
  • pagination: "continuous" seulement après le spike PDF2.3.

5.4 Le film (pont toon → motion, W4)

Correspondance fermée (08 §0) : une case → un plan 9:16.

CasePlan du film
src, heightbloc panel : l'image remplit la largeur, panoramique vertical du haut vers le bas de la case
lettering (dans l'ordre)bulles, encarts, sfx en apparition successive, mêmes formes que le lecteur
notesvoiceover du plan (comme :::notes en motion)
duréetemps de lecture du lettrage (wordsPerSecond) + hold, ou dur donné par l'auteur
background sans imageplan de couleur unie
case de remplacementerreur : pas de film sans images
  • Le bloc panel n'existe pas : son schéma va dans le registre de @recit/motion (schema/blocks.ts), son rendu dans @recit/motion-blocks (blocks/panel.tsx, CSS module, story et play), avec la couche par image du plan 19 (allowlist de l'animation par frame). Le chargement d'images au rendu Remotion est à vérifier en W4.1.
  • toonToFilm(story) vit dans @recit/toon/motion (sous-chemin : le lecteur n'importe jamais @recit/motion). Format → format : autorisé par 03 §8.
  • recit render episode.md reconnaît format: toon et passe par toonToFilm : pas d'option --as motion, render ne produit que des films (question 10). <StoryEmbed format="motion"> sur un épisode joue le même film, par storyMotion de site-stories/vite.ts.

6. CLI et MCP

CommandeEffetLot
recit init --format toonépisode Lorem de trois cases, images de remplacementW1
recit dev episode.mdaperçu du lecteur, rechargement à chaud (client src/toon/client.tsx, esbuild)W1
recit build episode.mdlecteur statique (HTML, images copiées, budget vérifié)W1
recit validate [--strict] episode.mdschéma, lettrage, images, budget, alt, parité des languesW0, W2
recit convert episode.md --to json|mdjumeau, forme canoniqueW0
recit export episode.md --to images --preset …tranchesW3
recit export episode.md --to pdfPDF, un par langue avec --all-localesPDF2
recit render episode.mdfilm 9:16W4

MCP (@recit/mcp), sans aucun appel de modèle :

  • describe_format accepte toon (aujourd'hui motion et deck) : syntaxe et JSON Schema du jumeau, même texte que docs.recit.dev ;
  • validate_story (nouveau, générique, utile à tous les formats) : diagnostics avec lignes ;
  • ressource recit://guide/toon : écrire un épisode, poser le lettrage, ne jamais inventer d'image ; reprise par recit skills export (plan 14, B9) ;
  • plus tard : des opérations ciblées (déplacer une bulle, changer un texte) sur le modèle d'apply_ops.

7. Le site webtoon.recit.dev

  • sites/webtoon : Markdown et config seulement (recit.config.ts avec createSiteConfig({ site: "webtoon" }), styles.css, vercel.json), comme sites/motion. Composants dans @recit/site-kit.
  • Pages : accueil (hero = un épisode lisible au scroll, sélecteur de langue sur la même planche), galerie, « une planche, deux langues », exports (panneau lu dans exports.json : tranches, PDF, film), « épisode → film ». Aucune doc propre : la doc est sur docs.recit.dev › Formats › Webtoon.
  • publish.entries pointe vers examples/toon-*, que le site déclare en devDependencies (audit ST-20). Lecteur plein écran sous /present/<slug>.
  • Port local 3005, e2e 4305 (e2e/ports.ts), projet Vercel recit-webtoon, alias 301 toon.recit.dev (07). La création du projet et du DNS reste à l'utilisateur.
  • Les fichiers générés (tranches, PDF, MP4) sont produits en CI, jamais sur Vercel (15, question 8 de ce plan-là, encore ouverte).

8. Les exemples

DossierContenuLanguesLot
examples/toon-episodeun épisode Lorem de 8 à 10 cases : toutes les formes de bulle, encarts, sfx, une case sans image, une case haute (PDF), notesen, frW5
examples/toon-seriesdeux épisodes courts et series.json, préréglage de tranchesenW6
examples/scenario-loremmicro-récit toon du lot IA0.3 (plan 14), cases de remplacement avec prompt=en, frW6
  • Contenu Lorem Ipsum, aucun nom réel, aucune image sous droits.
  • Les cases sont des SVG abstraits (formes et dégradés), écrits par un script de l'exemple (scripts/panels.ts) et commités ; deux d'entre elles ont aussi une version WebP, produite une fois par Chromium, pour que le budget de poids et les variantes JPEG du PDF soient testés sur des images matricielles. Ligne au NOTICE.md dans le même changement.
  • package.json : scope:example, scripts dev, build, export:images, export:pdf, render.
  • Les sources Markdown ne passent jamais par oxfmt.

9. Tests

NiveauCe qui est vérifiéOù
Schéma et jumeautoonScene et son inverse ; aller-retour sur des fixtures Lorem (ordre du lettrage, %, guillemets, case vide) ; erreurs avec ligne (bornes, reste dans le corps, image manquante) ; parité des languespackages/core/src/__tests__/ ; source-conversion.spec.ts pour les exemples
Modèle de renducompileToon (story → cases), budget, case de remplacement, toonToFilm (durées, ordre, voix off)packages/formats/toon/src/__tests__/
Composantsune story par composant avec play : rôles et noms (img nommée par l'alt, texte des bulles, ordre de lecture), axe en erreur ; Bubble dans ses quatre formes, ses queues, un texte long, RTLX.stories.tsx à côté de X.tsx, pnpm test:stories
Bloc motionpanel : story et play, golden de frames comme les autres blocs@recit/motion-blocks
CLIdev, build, convert, validate, export --to images (nombre et taille des tranches), render (durée du film)packages/cli/src/__tests__/
e2e, sitewebtoon.recit.dev : pages en clair et sombre, mobile (390 px) et bureau, chargement paresseux (les images hors écran ne sont pas demandées), changement de langue, recherche d'un texte de bullee2e/site-webtoon.spec.ts, captures -linux
e2e, exportsPDF : aucune case coupée, PNG des pages, budget de poids, deux langues (PDF2.4) ; tranches : largeur, hauteur, nombree2e/toon-export.spec.ts
Storybooksurface toon dans la galerie de capturese2e/storybook-toon.spec.ts

Le lecteur de PDF des tests (pdfjs-dist ou poppler) est la question 7 de 15, encore ouverte : PDF2.4 la suppose tranchée. Seuils de couverture : une ligne packages/formats/toon/** dans le vitest.config.ts racine, en autoUpdate.

10. Lots

Identifiants W0 à W7 : W1 à W5 reprennent ceux de 08 §2, W0, W6 et W7 sont nouveaux. Chaque lot est assez petit pour un agent ; les règles de travail concurrent sont celles de 20 · Exécution multi-agent.

Code
W0 ─► W1 ─┬─► W2 ─┐ ├─► W4 ─┼─► W5 ─► W7 └─► W6 ─┘ P6 (PDF1, runner) ─► W3, PDF2 ─► W5

W0 — Le jumeau (M)

Dépend de : rien dans P7 (questions 1 à 3 tranchées). Fichiers : packages/core/src/schema/source.ts, convert.ts, parity.ts, packages/core/src/config/validate.ts (ToonConfigSchema sans options d'export), packages/core/schema/v1/toon.json (régénéré), registre des directives, tests.

  • W0.1 — ToonSceneSourceSchema, ToonLetteringSchema, ToonSettingsSourceSchema, ToonSourceSchema strict ; types exportés.
  • W0.2 — toonScene et l'écriture canonique ; erreurs avec ligne.
  • W0.3 — Parité du lettrage entre langues.
  • W0.4 — Fixtures Lorem, dont une copie Lorem de la structure du scénario 3 (12 cases, lettrage mêlé, attributs de scénario) : elle valide sans perte, hors des champs que P16 doit fixer (cast: du frontmatter).
  • W0.5 — pnpm -F @recit/core schema:json ; version plan prerelease pour @recit/core.

Sortie : recit convert fait l'aller-retour sur toutes les fixtures, octet pour octet ; un toon JSON invalide échoue avec un chemin JSON, un Markdown invalide avec sa ligne.

TerminalBash
pnpm exec vitest run packages/core/src/__tests__/source-conversion.spec.ts pnpm test:related packages/core/src/schema/convert.ts && pnpm check

W1 — Le paquet et le lecteur (L)

Dépend de : W0. Fichiers : packages/formats/toon/** (paquet publiable : tsdown.config.ts, tsconfig.json, README.md, LICENSE.md, ./styles.css par scripts/build-css-modules.js), packages/theme (police de lettrage, OFL.txt), packages/cli/src/toon/, packages/cli/src/index.ts (init, dev, build), NOTICE.md (police).

  • W1.1 — compileToon(story) : cases typées, dimensions lues dans l'en-tête des images (PNG, JPEG, WebP, AVIF ; sans dépendance, question 4), budget.
  • W1.2 — Composants du §5.1, un module CSS chacun, stories avec play.
  • W1.3 — Police de lettrage et jeton de rôle dans @recit/theme.
  • W1.4 — recit init --format toon, recit dev, recit build.
  • W1.5 — Version plan preminor pour @recit/toon (premier jalon de la phase), prerelease pour @recit/theme et @recit/cli.

Sortie : recit dev d'une fixture se lit au scroll à 360 px et à 1280 px, en clair et en sombre ; node scripts/check-style-rule.js packages/formats/toon et node scripts/check-stories.js à 0 ; scripts/check-package-boundaries.js sans nouvelle tolérance.

TerminalBash
pnpm exec nx run toon:build && pnpm -F @recit/toon typecheck pnpm test:stories packages/formats/toon && pnpm check

W2 — Langues et site-stories (M)

Dépend de : W1. Fichiers : packages/integrations/site-stories/src/{plugin.tsx,vite.ts,search-index.ts}, package.json de site-stories (dépendance @recit/toon), packages/core/src/schema/parity.ts.

  • W2.1 — Branche toon de plugin.tsx vers ToonReader, chargée paresseusement ; StoryEmbed avec format="toon".
  • W2.2 — Routes par langue, sélecteur, hreflang (comme les autres formats, 09 §4).
  • W2.3 — Lettrage indexé par Pagefind.

Sortie : une fixture en/fr se lit dans examples/site-embed en deux langues ; recit validate signale une bulle non traduite.

W3 — Tranches (M)

Dépend de : W1 et @recit/paged/runner (P6, PDF1). Fichiers : ToonImagesOptionsSchema dans packages/core/src/config/validate.ts, préréglages, packages/cli/src/commands/export-*.ts, ExportManifestSchema (variante images).

  • W3.1 — Options typées (preset, width, sliceHeight, format, quality, maxBytes, avoidLettering selon la question 9).
  • W3.2 — Capture découpée par le runner, encodage dans Chromium.
  • W3.3 — exports.json, --all-locales.

Sortie : recit export examples/toon-episode/episode-1.md --to images --preset webtoon-canvas produit des tranches de la bonne largeur, sous le budget, listées dans exports.json.

PDF2 — Le PDF du webtoon (M)

Tâches PDF2.1 à PDF2.4 de 15 §6, sans changement. Dépend de : W1 et PDF1. Sortie : celle de 15, reprise dans les critères de P7.

W4 — Le pont vers motion (M)

Dépend de : W1. Fichiers : packages/formats/motion/src/schema/blocks.ts (bloc panel), packages/formats/motion-blocks/src/blocks/panel.tsx et son module, story et golden, packages/formats/toon/src/motion/, packages/cli/src/motion/source.ts (lecture d'un toon), site-stories/vite.ts (storyMotion), scripts/style-rule-allowlist.json si la couche d'animation le demande.

  • W4.1 — Spike : une image dans un plan rendu par Remotion (chemin, chargement, attente).
  • W4.2 — Bloc panel (schéma, rendu, story, golden).
  • W4.3 — toonToFilm, durées, voix off ; recit render episode.md.

Sortie : recit render examples/toon-episode/episode-1.md produit un MP4 9:16 ; le film se joue dans <StoryEmbed format="motion"> avec les mêmes plans. Version plan prerelease pour @recit/motion et @recit/motion-blocks.

W5 — Exemple, site, doc (M)

Dépend de : W2, W4, et W3 + PDF2 pour la page d'exports. Fichiers : examples/toon-episode/**, sites/webtoon/**, packages/tools/site-kit/src/sites.ts, e2e/ports.ts, scripts/e2e-projects.js, e2e/site-webtoon.spec.ts, e2e/toon-export.spec.ts, e2e/storybook-toon.spec.ts, sites/docs/pages/…/formats/webtoon.mdx, NOTICE.md.

  • W5.1 — examples/toon-episode (en, fr), panneaux du §8.
  • W5.2 — sites/webtoon, entrée webtoon de SITES.
  • W5.3 — Page docs.recit.dev › Formats › Webtoon, syntaxe et JSON Schema.
  • W5.4 — e2e et captures : pnpm test:e2e:update:linux e2e/site-webtoon.spec.ts.

Sortie : les critères de P7 ci-dessous, sauf la mise en ligne.

W6 — Séries, MCP, scénario (M)

Dépend de : W1 (et la question 8 pour les séries). Fichiers : SeriesSchema dans @recit/core, EpisodeNav, examples/toon-series, packages/mcp/src/{server.ts,guide.ts}, examples/scenario-lorem (avec le plan 14, IA0.3).

  • W6.1 — describe_format pour toon, validate_story, recit://guide/toon.
  • W6.2 — Micro-récit toon de examples/scenario-lorem, cases de remplacement.
  • W6.3 — Séries : series.json, navigation entre épisodes, examples/toon-series.

Sortie : un agent qui lit recit://guide/toon écrit un épisode Lorem qui passe recit validate --strict du premier coup ou avec les seules corrections signalées.

W7 — Clôture (S)

pnpm verify, pnpm test:e2e:linux e2e/site-webtoon.spec.ts e2e/toon-export.spec.ts e2e/storybook-toon.spec.ts, seuils de couverture, version plans relus, glossaire d'AGENTS.md proposé à l'intégrateur (case, épisode, lettrage, tranche), note datée dans journal.md.

Critères de sortie de P7

Ceux de 06 P7, inchangés :

  • examples/toon-episode se lit au scroll, en deux langues ;
  • l'export en tranches respecte les préréglages des plateformes ;
  • recit render produit un film 9:16 (la forme exacte de la commande : question 10) ;
  • examples/toon-episode produit un PDF par langue, sans case coupée, sous son budget de poids, listé dans exports.json ;
  • webtoon.recit.dev est en ligne (toon.recit.dev redirige), et la version 0.3.0-alpha est publiée (après le go de l'utilisateur).

S'y ajoutent, proposés par ce plan : le jumeau de chaque exemple fait l'aller-retour sans perte ; tous les composants ont leur story avec play et passent axe.

11. Risques

RisqueParade
P6 en retard : pas de runner, pas de --toW0, W1, W2, W4 n'en dépendent pas ; W3 et PDF2 attendent, P7 ne sort pas sans eux
Poids des images sur mobilebudget avertissant, formats modernes recommandés, chargement paresseux, dimensions intrinsèques ; srcset selon la question 4
Débordement des bulles après traduction (l'allemand allonge de 30 %)bulles extensibles, avertissement par langue au rendu, captures en deux langues
Le bloc panel ne se rend pas proprement dans Remotionspike W4.1 avant le reste ; le pont peut sortir après P7 sans bloquer le lecteur
Le tableau unique lettering contredit 09 §2.3question 1, tranchée avant W0
Préréglages de plateformes qui changentdonnées versionnées, date de vérification, e2e sur la largeur et la hauteur seulement
Captures instables (chargement paresseux, animations)?settled=1 comme le motion, animations coupées en e2e, attente des images
Lecteur d'écran : bulles sans locuteurcast= sur la bulle (question 5)
Tentation d'ajouter sharp « pour plus tard »règle « une seule stack » ; question 4

12. Questions ouvertes

  1. Lettrage en un tableau. 09 §2.3 (accepté) écrit bubbles[], captions[], sfx[]. Ce plan propose un seul tableau ordonné lettering[] discriminé sur type, parce que trois tableaux perdent l'ordre de lecture et cassent l'aller-retour. D'accord pour corriger 09 ?
  2. Directives sur une ligne. Garder la forme :::bubble{…} texte ::: de 08 §2 (il faut alors l'apprendre au parseur, comme :::notes), ou ne garder que la forme sur plusieurs lignes (recommandé : une seule forme canonique) ?
  3. Fichier de langue « textes seulement ». 08 §2 et le scénario 3 le décrivent ; il n'entre ni dans la variante complète de 09 ni dans le compagnon. Recommandation : variante complète (episode.fr.md) ou compagnon (episode.fr.json), pas de troisième forme. Sinon, il faut changer la parité de tous les formats.
  4. sharp. 08 §2 et 03 §6 le prévoient pour la découpe. Ce plan propose de s'en passer : capture découpée et encodage dans Chromium, dimensions lues dans l'en-tête des images par un petit module maison. Contrepartie : pas de srcset généré au build en P7. D'accord ?
  5. Locuteur d'une bulle. cast= sur :::bubble (nom tiré de la bible en P16), ou attendre :::dialogue ?
  6. Police de lettrage. Quelle famille OFL par défaut (par exemple Comic Neue, licence à vérifier au moment de W1.3) ?
  7. direction: rtl. 15 §3.5 dit qu'il « change l'ordre de lecture des bulles ». Ce plan propose que l'ordre du Markdown fasse toujours foi et que rtl ne change que dir et l'interface. D'accord ?
  8. Séries et récits en plusieurs fichiers. series.json (kind: "series") plutôt que series.yaml, et la même forme que les chapitres du livre (plan 25) ?
  9. Coupe des tranches. Hauteur fixe sans égard au contenu (15 §3.5) ou en évitant les bulles (08 §2) ? Proposition : fixe par défaut, avoidLettering en option.
  10. Commande du film. recit render episode.md (format reconnu) au lieu de recit render --as motion écrit dans 06 P7 et 08 §2 ?
  11. Second exemple. 06 P12 demande deux exemples par format : examples/toon-series suffit-il (W6), ou faut-il un exemple en sens rtl ?