Accéder au contenu principal

Tutoriel sur les petits modèles Qwen 3.5 : créez un générateur vidéo‑vers‑jeu avec Ollama

Apprenez à utiliser localement les petits modèles Qwen 3.5 via Ollama pour transformer un court extrait de gameplay en un jeu HTML jouable.
Actualisé 19 sept. 2026  · 14 min lire

Explorer avec l’IA

ChatGPTClaudePerplexity

Qwen 3.5 est une famille de modèles multimodaux open source conçus pour traiter à la fois des entrées texte et visuelles. Les modèles suivent une architecture image‑texte‑vers‑texte, ce qui leur permet de raisonner sur des images ou des images vidéo en complément d'invites en langage naturel. 

Dans ce tutoriel, nous allons créer une application Streamlit qui convertit une vidéo de gameplay en un jeu jouable dans le navigateur à l'aide du petit modèle local Qwen3.5, qwen3.5:9b, hébergé via Ollama. L'application extrait des images de la vidéo téléversée, déduit les mécaniques du jeu, les convertit en une spécification structurée, puis génère un jeu complet en HTML/CSS/JavaScript pouvant être prévisualisé directement dans l'interface.

Au passage, vous apprendrez à :

  • Extraire des images représentatives de vidéos pour le raisonnement multimodal
  • Exécuter Qwen 3.5 en local avec Ollama pour de l'inférence image‑texte
  • Concevoir un pipeline de prompts en deux étapes pour le raisonnement et la génération de code
  • Convertir des observations visuelles de gameplay en une spécification de jeu structurée
  • Générer un jeu HTML5 jouable à partir des mécaniques inférées
  • Construire une interface Streamlit interactive pour exécuter et prévisualiser le pipeline

À la fin, vous disposerez d'une démo fonctionnelle qui transforme un court extrait de gameplay en un jeu simple et jouable dans le navigateur, entièrement généré en local. Vous pouvez voir une version abrégée du flux de travail dans la vidéo ci‑dessous :

Qu'est‑ce que Qwen 3.5 Small ?

Qwen 3.5 est une famille de modèles multimodaux conçue pour couvrir un large éventail de scénarios de déploiement, des modèles légers en périphérie aux systèmes de raisonnement de pointe. 

La gamme officielle Qwen 3.5 comprend actuellement des variantes 0.8B, 2B, 4B, 9B, 27B, 35B‑A3B, 122B‑A10B et 397B‑A17B, ainsi que des checkpoints de base et quantifiés pour certains modèles. 

Le choix du bon petit modèle dépend du type de flux de travail que vous souhaitez construire. 

  • Les variantes 0.8B et 2B sont idéales pour les appareils en périphérie, le prototypage rapide et les environnements matériels contraints. 
  • Le modèle 4B constitue un palier supérieur et convient bien aux petits agents locaux ou aux tâches légères de raisonnement visuel. 
  • Le modèle 9B est mieux adapté aux applications multimodales locales, car il reste suffisamment compact pour tourner en local tout en étant assez puissant pour déduire des mécaniques de jeu et générer un code HTML exploitable. 

Pour ce tutoriel, nous avons utilisé le modèle qwen3.5:9b, qui offre un excellent équilibre entre capacités, déployabilité locale et raisonnement multimodal, ce qui le rend idéal pour bâtir un pipeline vidéo‑vers‑jeu dans le navigateur sans dépendre d'API externes.

Vous pouvez aussi consulter notre guide sur l'exécution locale de Qwen3.5 sur un seul GPU

Pourquoi utiliser un petit modèle local pour cela ?

J'ai également testé cette démo avec le modèle qwen/qwen3.5-397b-a17b. Bien que ce modèle soit nettement plus grand et généralement plus performant que la variante locale 9B, il nécessite une inférence via API, ce qui introduit des dépendances externes et une latence potentielle.

En pratique, si le temps d'exécution n'est pas une contrainte stricte, le temps total de génération différait d'environ ~3 minutes entre les deux modèles lors de mes tests. Le modèle plus grand produisait une logique de jeu légèrement plus fluide et une prévisualisation plus soignée, mais l'amélioration n'était pas spectaculaire.

Compte tenu du fait que le modèle 9B fonctionne entièrement en local, les résultats sont étonnamment compétitifs. Malgré sa taille bien moindre, il est capable de générer des jeux jouables dans le navigateur avec une baisse de qualité modérée par rapport au modèle 397B beaucoup plus volumineux.

Vous pouvez lire notre guide complet sur le fine‑tuning des petits modèles Qwen3.5 pour apprendre à tirer le meilleur parti de la variante 0.8B. 

Projet exemple Qwen 3.5 : créer un générateur vidéo‑vers‑jeu

Dans cette section, nous allons créer une application Streamlit qui :

  • prend un court extrait de gameplay
  • extrait un petit nombre d'images représentatives
  • envoie ces images à qwen3.5:9b via Ollama
  • demande au modèle d'inférer une spécification de jeu structurée
  • lui demande de générer un jeu HTML monofichier
  • prévisualise le jeu généré dans l'application Streamlit

Cette configuration met en avant les capacités multimodales de Qwen, utiles pour des tâches comme la rétro‑ingénierie de la logique à partir d'images de gameplay et sa transformation en code

Étape 1 : installer les dépendances

Avant de construire l'application, nous devons d'abord préparer un environnement local avec les bibliothèques nécessaires pour le traitement vidéo, l'inférence multimodale et l'interface interactive. 

pip install streamlit opencv-python ollama python-dotenv pydantic

La démo utilise un petit ensemble de bibliothèques Python :

  • Streamlit pour construire l'interface web interactive où les utilisateurs téléversent des vidéos de gameplay et prévisualisent les jeux générés.
  • OpenCV pour décoder la vidéo téléversée et extraire des images représentatives.
  • Ollama pour exécuter localement le modèle Qwen 3.5 pour le raisonnement multimodal et la génération de code.
  • python-dotenv pour gérer les variables d'environnement si nécessaire.
  • Pydantic pour imposer un schéma structuré à la spécification de jeu inférée avant la génération de code.

Ensuite, nous devons télécharger le modèle lui‑même. Ollama simplifie cela en récupérant directement le modèle depuis son registre :

ollama run qwen3.5:9b

La première exécution télécharge le modèle et l'enregistre en local. Le qwen3.5:9B pèse environ 6,6 Go, prend en charge les entrées texte et image, et offre une fenêtre de contexte de 256K tokens, ce qui le rend bien adapté aux flux de travail multimodaux.

Étape 2 : imports

Maintenant que l'environnement local est prêt, la prochaine étape consiste à importer les bibliothèques utilisées dans l'application et à créer un dossier pour stocker les artefacts générés. 

import json
import base64
import tempfile
from pathlib import Path
from typing import List
import cv2
import ollama
import streamlit as st
import streamlit.components.v1 as components
from pydantic import BaseModel, Field, ValidationError
OUTPUT_DIR = Path("outputs_local")
OUTPUT_DIR.mkdir(exist_ok=True)

Ce bloc réunit la pile principale du projet. Nous utilisons json pour formater la spécification de jeu structurée, base64 pour encoder les images extraites avant de les envoyer au modèle, et tempfile pour enregistrer en toute sécurité les vidéos téléversées pendant une session. Path de pathlib nous offre une manière propre de gérer les chemins de fichiers, tandis que List de typing sert aux annotations de type dans les définitions de schémas.

Ensuite, nous importons les bibliothèques principales de l'application : cv2 d'OpenCV s'occupe de lire l'extrait de gameplay et d'extraire les images représentatives, ollama est l'interface utilisée pour communiquer avec le modèle local qwen3.5:9b. streamlit fournit l'UI web, et streamlit.components.v1 permet d'intégrer directement le jeu HTML généré dans l'application pour une prévisualisation interactive.

Enfin, nous importons BaseModel, Field et ValidationError depuis Pydantic. Ils servent à définir et valider le schéma JSON structuré représentant le game design inféré. Plutôt que de faire confiance à une sortie non structurée, nous contraignons le modèle à respecter un schéma prévisible avant de transmettre ce résultat au générateur de jeu HTML.

Les deux dernières lignes créent un répertoire outputs_local s'il n'existe pas. Ce dossier est l'endroit où nous sauvegardons les artefacts générés, comme la spécification de jeu inférée et le fichier HTML final, ce qui facilite l'inspection ou le téléchargement après chaque exécution.

À l'étape suivante, nous allons définir les classes de schéma Pydantic décrivant la spécification de jeu que le modèle doit produire à partir des images extraites.

Étape 3 : définir le schéma

À ce stade, nous définissons le schéma structuré que le modèle doit suivre pour décrire les mécaniques de jeu inférées à partir des images vidéo. 

Au lieu de demander au modèle de générer directement du code HTML à partir d'images, nous exigeons d'abord une spécification de jeu structurée. Cette représentation intermédiaire rend le pipeline bien plus simple à déboguer, valider et étendre.

class Entity(BaseModel):
    name: str
    role: str
    behavior: str
class Physics(BaseModel):
    gravity: str = ""
    jump_or_impulse: str = ""
    collision_style: str = ""
    movement_style: str = ""
class VisualStyle(BaseModel):
    perspective: str = ""
    palette: str = ""
    background: str = ""
    ui_elements: List[str] = Field(default_factory=list)
class GameSpec(BaseModel):
    title: str
    genre: str
    objective: str
    player_controls: List[str]
    gameplay_loop: List[str]
    entities: List[Entity]
    scoring_rules: List[str]
    win_condition: str
    lose_condition: str
    physics: Physics
    visual_style: VisualStyle
    assumptions: List[str]
    confidence_notes: List[str]

Ce schéma définit la structure centrale de la spécification de jeu que le modèle doit générer.

  • La classe Entity représente tout objet présent dans le jeu : joueur, ennemis, obstacles ou éléments d'interface. Chaque entité comporte un nom, son rôle et une brève description de son comportement.
  • La classe Physics décrit la manière dont les objets se déplacent et interagissent dans le monde du jeu. Ces champs capturent des mécaniques de base comme la gravité, l'impulsion/saut, la gestion des collisions et les styles de déplacement.
  • La classe VisualStyle capture la présentation visuelle du jeu : perspective de caméra (vue du dessus, défilement latéral, etc.), palette de couleurs, style d'arrière‑plan et éléments d'interface visibles (compteur de score, jauge de vie...).
  • Enfin, la classe GameSpec rassemble le tout. Elle représente le design complet inféré à partir des images : objectif, contrôles, boucle de gameplay, entités, système de score, conditions de victoire/défaite, ainsi que les hypothèses formulées par le modèle.

En validant la sortie du modèle contre ce schéma, nous garantissons que le design inféré suit toujours une structure prévisible. Cela facilite aussi la transmission de la spécification à l'étape suivante du pipeline.

Étape 4 : prompts pour une génération en deux étapes

Une fois le schéma en place, l'étape suivante consiste à définir les prompts guidant le modèle à travers les deux étapes de notre pipeline. 

La première étape porte sur la rétro‑ingénierie des mécaniques à partir des images extraites et la production d'une spécification structurée. 

La seconde étape prend cette spécification et génère un jeu jouable en HTML, CSS et JavaScript.

Étape 4.1 : définir les prompts système

Le premier composant de notre stratégie d'invite est le prompt système, qui définit le rôle que le modèle doit jouer à chaque étape du pipeline.

SPEC_SYSTEM_PROMPT = """
You are an expert arcade game designer and gameplay reverse-engineering analyst.
You observe a few frames from a gameplay video and infer the smallest possible playable browser-game clone.
The gameplay is likely a simple arcade game like Pong, Breakout, Snake, or Flappy Bird.
Prefer these mechanics if uncertain.
Rules:
- Only infer mechanics that are strongly supported by visual evidence.
- Prefer a minimal playable prototype over a complex clone.
- Do not invent advanced systems unless clearly visible.
- Keep the result suitable for plain HTML5 canvas and JavaScript.
- Return strict JSON only.
- No markdown.
- No code fences.
""".strip()
CODE_SYSTEM_PROMPT = """
You are a senior JavaScript game developer.
Generate a complete single-file HTML game.
Requirements:
- Return raw HTML only.
- No markdown.
- No code fences.
- Use plain HTML, CSS, and JavaScript only.
- Use HTML5 canvas unless there is a strong reason not to.
- Keep the game simple, playable, and self-contained.
- No external libraries, no CDNs, no external assets.
- Use simple shapes/colors instead of images.
- Include score.
- Include restart support.
- Include instructions on screen.
- Keep keyboard controls simple.
- Make the game playable inside an iframe preview.
- IMPORTANT: the canvas must have tabindex="0".
- IMPORTANT: keyboard input must work inside an embedded iframe.
- IMPORTANT: focus the canvas automatically on load, on canvas click, and after pressing Start/Restart buttons.
- IMPORTANT: prevent default browser behavior for arrow keys and spacebar.
""".strip()

Le premier prompt système demande au modèle d'adopter le rôle d'analyste de gameplay. Il met l'accent sur des hypothèses minimales et l'encourage à n'inférer que les mécaniques clairement visibles dans les images. 

Comme le modèle ne voit qu'un petit nombre d'images échantillonnées, le prompt le biaise explicitement vers des jeux d'arcade simples qui peuvent être implémentés avec une logique légère côté navigateur.

Le second prompt système fait évoluer le modèle vers un rôle de développeur JavaScript. À ce stade, le modèle reçoit une spécification structurée et doit la convertir en un jeu HTML complet. 

Étape 4.2 : construire le prompt de spécification

Nous définissons ensuite une fonction utilitaire qui construit le prompt utilisateur pour la première étape du pipeline. Ce prompt demande au modèle d'analyser les images extraites et de produire une spécification de jeu structurée.

def build_spec_user_prompt(game_hint: str, extra_constraints: str, max_frames: int) -> str:
    return f"""
You are given a few still frames extracted from a gameplay video.
From these frames, infer a minimal, playable browser game design.
Use very simple 2D arcade-style mechanics.
If you are unsure, bias toward Pong / Breakout / Snake / Flappy Bird style games.
Output valid JSON with exactly this schema:
{{
  "title": "string",
  "genre": "string",
  "objective": "string",
  "player_controls": ["string"],
  "gameplay_loop": ["string"],
  "entities": [
    {{
      "name": "string",
      "role": "player|enemy|obstacle|projectile|ui|environment",
      "behavior": "string"
    }}
  ],
  "scoring_rules": ["string"],
  "win_condition": "string",
  "lose_condition": "string",
  "physics": {{
    "gravity": "string",
    "jump_or_impulse": "string",
    "collision_style": "string",
    "movement_style": "string"
  }},
  "visual_style": {{
    "perspective": "string",
    "palette": "string",
    "background": "string",
    "ui_elements": ["string"]
  }},
  "assumptions": ["string"],
  "confidence_notes": ["string"]
}}
Important:
- Only infer the simplest mechanics needed for a playable clone.
- Prefer a very simple game that can be rebuilt in one HTML file.
- Avoid menus, accounts, audio, networking, cutscenes, or multi-level progression.
Optional game hint from user:
{game_hint or "None"}
Extra constraints:
{extra_constraints or "None"}
You are seeing at most {max_frames} frames from the video, so stay simple and conservative.
Return JSON only.
""".strip()

Cette fonction construit le prompt envoyé au modèle avec les images extraites. Elle inclut plusieurs éléments clés :

  • Un rappel que le modèle analyse des images fixes et non la vidéo complète
  • Un biais vers des mécaniques d'arcade simples
  • Le schéma JSON complet, garantissant une sortie conforme à notre modèle GameSpec
  • La possibilité d'ajouter des indices et contraintes pour orienter le modèle vers un style de jeu particulier

En intégrant le schéma directement dans le prompt, on incite le modèle à produire une sortie validable par Pydantic à l'étape suivante du pipeline.

Étape 4.3 : construire le prompt de génération de code

Une fois la spécification de jeu générée et validée, nous avons besoin d'un second prompt pour la convertir en un jeu jouable.

def build_code_user_prompt(game_spec: dict) -> str:
    spec_json = json.dumps(game_spec, indent=2)
    return f"""
Using the following game specification, generate a complete single-file HTML game.
Requirements:
- One self-contained HTML file.
- Inline CSS and inline JavaScript.
- Must be playable in a browser.
- Must render correctly in an iframe.
- Show score on screen.
- Show controls on screen.
- Add restart support using a button or key.
- Keep visuals simple and robust.
- Use requestAnimationFrame.
- Use canvas for gameplay rendering.
- No external assets.
- Make the game small but genuinely playable.
Game specification:
{spec_json}
Return raw HTML only.
""".strip()

La fonction build_code_user_prompt convertit le dictionnaire GameSpec en JSON formaté et l'insère dans le prompt. Le modèle utilise ensuite cette description structurée comme cahier des charges pour générer le jeu HTML final.

Comme la spécification définit déjà mécaniques, entités, contrôles et objectifs, la tâche du modèle est simplifiée : il n'a plus qu'à traduire le design en logique JavaScript avec une boucle de rendu sur canvas.

À l'étape suivante, nous allons implémenter l'extraction d'images avec OpenCV, qui convertit la vidéo téléversée en un petit ensemble d'images représentatives analysables par le modèle Qwen 3.5.

Étape 5 : extraire des images représentatives

Les modèles locaux Qwen 3.5 d'Ollama acceptent du texte et des images, mais pas de la vidéo brute. Plutôt que d'envoyer la vidéo directement, nous extrayons un petit ensemble d'images régulièrement espacées et les passons au modèle. 

def save_uploaded_video(uploaded_file) -> Path:
    suffix = Path(uploaded_file.name).suffix or ".mp4"
    with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
        tmp.write(uploaded_file.read())
        return Path(tmp.name)
def extract_frames(video_path: Path, max_frames: int = 6):
    cap = cv2.VideoCapture(str(video_path))
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    interval = max(1, total_frames // max_frames) if total_frames > 0 else 1
    frames = []
    frame_id = 0
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        if frame_id % interval == 0:
            _, buffer = cv2.imencode(".jpg", frame)
            frames.append(base64.b64encode(buffer).decode())
        frame_id += 1
        if len(frames) >= max_frames:
            break
    cap.release()
    return frames
def extract_json_block(text: str) -> str:
    start = text.find("{")
    end = text.rfind("}")
    if start == -1 or end == -1 or end <= start:
        raise ValueError("No valid JSON object found in model response.")
    return text[start : end + 1]
def cleanup_html_response(text: str) -> str:
    html = text.strip()
    if html.startswith("```"):
        lines = html.splitlines()
        if lines and lines[0].startswith("```"):
            lines = lines[1:]
        if lines and lines[-1].startswith("```"):
            lines = lines[:-1]
        html = "\n".join(lines).strip()
    return html
def patch_html_for_iframe_keyboard(html: str) -> str:
    if 'tabindex="0"' not in html and "<canvas" in html:
        html = html.replace("<canvas", '<canvas tabindex="0"', 1)
    focus_patch = """
<script>
(function() {
  const canvas = document.querySelector("canvas");
  if (!canvas) return;
  if (!canvas.hasAttribute("tabindex")) {
    canvas.setAttribute("tabindex", "0");
  }
  function focusGame() {
    try { canvas.focus(); } catch (e) {}
  }
  window.addEventListener("load", focusGame);
  canvas.addEventListener("click", focusGame);
  document.addEventListener("keydown", function(e) {
    if (["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"," "].includes(e.key)) {
      e.preventDefault();
    }
  }, { passive: false });
  const buttons = document.querySelectorAll("button");
  buttons.forEach(btn => {
    btn.addEventListener("click", () => {
      setTimeout(focusGame, 50);
    });
  });
})();
</script>
"""
    if "</body>" in html:
        html = html.replace("</body>", focus_patch + "\n</body>")
    else:
        html += focus_patch
    return html

Ces utilitaires définissent cinq composants clés pour extraire des images représentatives de la vidéo téléversée :

  • Gestion du téléversement : la fonction save_uploaded_video() enregistre la vidéo de gameplay téléversée comme fichier temporaire sur le disque. Comme Streamlit charge les fichiers en mémoire, l'écriture sur le disque permet à OpenCV de les lire efficacement tout en préservant l'extension d'origine.
  • Échantillonnage des images : la fonction extract_frames() charge la vidéo avec OpenCV et prélève un petit nombre d'images uniformément réparties sur sa durée. Chaque image est encodée en JPEG puis convertie en chaîne base64 afin d'être transmise directement comme entrée image au modèle multimodal Qwen.
  • Extraction de JSON structuré : l'utilitaire extract_json_block() garantit que la sortie du modèle peut être analysée correctement. Le contenu extrait peut ensuite être validé contre le schéma GameSpec.
  • Nettoyage de la réponse HTML : la fonction cleanup_html_response() retire un éventuel formatage Markdown que le modèle pourrait inclure lors de la génération de code. Si le modèle encadre le HTML avec des triples backticks, l'utilitaire les supprime pour permettre l'affichage direct dans la prévisualisation Streamlit.
  • Interaction clavier en iframe : la fonction patch_html_for_iframe_keyboard() injecte un petit script JavaScript pour s'assurer que les contrôles clavier fonctionnent correctement dans l'iframe Streamlit. Elle focalise automatiquement le canvas, lui attribue un tabindex si besoin, et empêche le navigateur d'intercepter les flèches et la barre d'espace.

Ensemble, ces utilitaires constituent la couche de prétraitement vidéo et de post‑traitement des réponses du pipeline.

Étape 6 : inférer la spécification du jeu 

Maintenant que nous avons les images extraites et les modèles de prompt, nous pouvons implémenter la première étape du pipeline de raisonnement : convertir les images de gameplay en une spécification de jeu structurée.

def infer_game_spec(video_path: Path, game_hint: str, extra_constraints: str, max_frames: int) -> dict:
    frames = extract_frames(video_path, max_frames=max_frames)
    prompt = build_spec_user_prompt(game_hint, extra_constraints, max_frames)
    response = ollama.chat(
        model="qwen3.5:9b",
        messages=[
            {
                "role": "system",
                "content": SPEC_SYSTEM_PROMPT,
            },
            {
                "role": "user",
                "content": prompt,
                "images": frames,
            },
        ],
        options={
            "temperature": 0.1,
            "num_ctx": 8192,
        },
    )
    text = response["message"]["content"]
    parsed = json.loads(extract_json_block(text))
    validated = GameSpec(**parsed)
    return validated.model_dump()

Cette fonction définit quatre étapes clés dans le pipeline d'inférence de spécification :

  • Préparation des images : l'appel à extract_frames() échantillonne quelques images représentatives de la vidéo téléversée. Ces images servent de preuves visuelles au modèle pour inférer les mécaniques.
  • Construction du prompt : la fonction build_spec_user_prompt() crée dynamiquement le prompt utilisateur à partir de l'indice optionnel, d'éventuelles contraintes et du nombre maximal d'images. Ce prompt demande explicitement une spécification conforme au schéma.
  • Appel au modèle multimodal : la requête ollama.chat() envoie à la fois le prompt texte et les images extraites au modèle local qwen3.5:9b. Les paramètres utilisent une temperature faible pour un résultat plus déterministe et num_ctx=8192 pour laisser suffisamment de contexte au prompt et à la réponse structurée.
  • Validation du schéma : une fois la réponse reçue, la fonction extrait le bloc JSON, l'analyse et le valide contre le schéma Pydantic GameSpec. Enfin, la spécification validée est renvoyée sous forme de dictionnaire Python.

Nous disposons maintenant d'une représentation structurée du jeu inféré à partir des images échantillonnées. À l'étape suivante, nous utiliserons cette spécification validée comme entrée pour la génération du jeu HTML.

Étape 7 : générer le jeu HTML

À ce stade, le modèle n'a plus à inférer des mécaniques depuis des images. Nous demandons à qwen3.5:9b de produire un fichier HTML autonome incluant mise en page, styles, boucle de jeu, contrôles et logique de rendu.

def generate_game_html(game_spec: dict) -> str:
    response = ollama.chat(
        model="qwen3.5:9b",
        messages=[
            {
                "role": "system",
                "content": CODE_SYSTEM_PROMPT,
            },
            {
                "role": "user",
                "content": build_code_user_prompt(game_spec),
            },
        ],
        options={
            "temperature": 0.2,
            "num_ctx": 8192,
        },
    )
    text = response["message"]["content"]
    html = cleanup_html_response(text)
    if "<html" not in html.lower():
        raise ValueError("Model did not return HTML")
    return patch_html_for_iframe_keyboard(html)

Cette fonction effectue quatre opérations pour générer et préparer le jeu HTML final :

  • Envoi d'un prompt structuré : la fonction prend le dictionnaire game_spec validé et le passe à build_code_user_prompt(). Cela convertit la spécification en un prompt d'implémentation détaillé pour générer un jeu HTML monofichier.
  • Génération de code avec Qwen : l'appel ollama.chat() envoie le prompt système de génération de code et le prompt utilisateur au modèle local qwen3.5:9b. La tâche étant désormais purement textuelle, le modèle ne reçoit que du texte. 
  • Nettoyage et validation HTML : la réponse brute du modèle est passée à cleanup_html_response() pour supprimer tout formatage superflu. Nous vérifions ensuite que la sortie contient bien un document HTML.
  • Correctif clavier pour iframe : le HTML final est transmis à patch_html_for_iframe_keyboard(), qui injecte un petit correctif JavaScript afin de rendre le jeu jouable dans l'iframe intégrée de Streamlit. C'est important, car les jeux web ne capturent pas toujours correctement les flèches et la barre d'espace sans canvas focalisable et focus automatique.

Nous avons maintenant un jeu HTML jouable dans le navigateur, généré entièrement à partir de la spécification inférée.

Étape 8 : interface Streamlit

Pour finir, encapsulons le tout dans une interface Streamlit afin que les utilisateurs puissent essayer le système sans toucher au code. L'interface expose trois capacités principales :

  • Téléverser une vidéo de gameplay
  • Configurer les paramètres de génération (indices, échantillonnage d'images)
  • Prévisualiser et télécharger le jeu généré
st.set_page_config(page_title="Qwen 3.5 9B (Ollama) - Video to Game", layout="wide")
st.title("Qwen 3.5 9B: Gameplay Video to Playable HTML Game")
st.write(
    "This local demo uses Ollama with qwen3.5:9b to turn a short gameplay clip "
    "into a minimal browser game."
)
with st.sidebar:
    st.header("Local generation settings")
    game_hint = st.text_input("Optional game hint", value="Simple Pong-like arcade game")
    extra_constraints = st.text_area(
        "Extra constraints",
        value="Keep the game extremely simple and easy to play with arrow keys or space bar.",
        height=120,
    )
    max_frames = st.slider("Frames extracted from video", 3, 8, 5)
uploaded_video = st.file_uploader(
    "Upload gameplay video",
    type=["mp4", "mov", "avi", "mkv", "webm"],
)
if "local_game_spec" not in st.session_state:
    st.session_state.local_game_spec = None
if "local_game_html" not in st.session_state:
    st.session_state.local_game_html = None
if uploaded_video is not None:
    video_path = save_uploaded_video(uploaded_video)
    col1, col2 = st.columns([1, 1])
    with col1:
        st.subheader("Uploaded video")
        st.video(str(video_path))
    with col2:
        st.subheader("Run local pipeline")
        if st.button("Generate game", type="primary"):
            try:
                with st.spinner("Extracting frames and inferring game mechanics (local model)..."):
                    game_spec = infer_game_spec(
                        video_path=video_path,
                        game_hint=game_hint,
                        extra_constraints=extra_constraints,
                        max_frames=max_frames,
                    )
                    st.session_state.local_game_spec = game_spec
                with st.spinner("Generating HTML game code with qwen3.5:9b..."):
                    game_html = generate_game_html(game_spec)
                    st.session_state.local_game_html = game_html
                    (OUTPUT_DIR / "local_game_spec.json").write_text(
                        json.dumps(game_spec, indent=2),
                        encoding="utf-8",
                    )
                    (OUTPUT_DIR / "local_generated_game.html").write_text(
                        game_html,
                        encoding="utf-8",
                    )
                st.success("Local game generated.")
            except ValidationError as e:
                st.error(f"Schema validation failed: {e}")
            except Exception as e:
                st.error(f"Local generation failed: {e}")
if st.session_state.local_game_spec:
    st.subheader("Inferred game spec")
    st.json(st.session_state.local_game_spec)
if st.session_state.local_game_html:
    tab1, tab2, tab3 = st.tabs(["Preview", "HTML Code", "Downloads"])
    with tab1:
        st.subheader("Playable preview")
        st.caption(
            "Click START GAME, then click once inside the game canvas so it captures keyboard input."
        )
        components.html(
            st.session_state.local_game_html,
            height=760,
            scrolling=False,
        )
    with tab2:
        st.subheader("Generated HTML (local)")
        st.code(st.session_state.local_game_html, language="html")
    with tab3:
        st.subheader("Download files (local)")
        st.download_button(
            label="Download HTML game (local)",
            data=st.session_state.local_game_html,
            file_name="local_generated_game.html",
            mime="text/html",
        )
        st.download_button(
            label="Download JSON spec (local)",
            data=json.dumps(st.session_state.local_game_spec, indent=2),
            file_name="local_game_spec.json",
            mime="application/json",
        )

Ce code définit comment l'utilisateur téléverse un extrait de gameplay, fournit éventuellement un indice sur le type de jeu et choisit le nombre d'images à extraire pour l'analyse. Une fois la vidéo téléversée, l'application l'affiche à côté d'un bouton Generate game qui lance l'ensemble du pipeline.

Lorsque l'utilisateur exécute le pipeline, l'application extrait d'abord des images et infère les mécaniques de jeu avec le modèle local qwen3.5:9b, produisant une spécification structurée. Dans un second temps, le modèle génère un jeu HTML complet à partir de cette spécification. Les résultats sont stockés dans l'état de session Streamlit pour persister entre les mises à jour de l'UI.

Après la génération, l'interface affiche la spécification inférée, un aperçu jouable du jeu, le code source HTML brut et des boutons pour télécharger le jeu HTML et la spécification JSON.

Pour l'essayer vous‑même, enregistrez le code sous app.py et lancez :

streamlit run app.py

Demo

Conclusion

Qwen 3.5 est l'une des familles de modèles multimodaux les plus intéressantes du moment. Dans ce tutoriel, nous avons utilisé qwen3.5:9b via Ollama pour construire un générateur entièrement local, de la vidéo de gameplay au jeu. L'application extrait des images d'un court extrait, infère un design structuré, génère un jeu monofichier pour le navigateur et le prévisualise dans Streamlit.

Le modèle hébergé qwen/qwen3.5-397b-a17b reste l'option la plus rapide et la plus performante pour cette tâche, mais le résultat en local avec 9B est impressionnant en soi. Il prouve qu'un petit modèle multimodal peut désormais réaliser un raisonnement visuel et une génération de code pertinents sur du matériel grand public.

Pour aller plus loin, comparez 9B aux modèles 4B et 27B, ajoutez une boucle de raffinement, ou transformez la sortie en mini‑moteur de jeu prenant en charge plusieurs itérations à partir du même extrait.

Qwen3.5 Small : FAQ

Puis‑je exécuter cette démo avec `qwen3.5:4b` au lieu de 9B ?

Oui, mais les résultats seront généralement moins fiables. Le modèle 4B reste utile pour des expérimentations locales légères, mais la variante 9B offre un meilleur compromis entre vitesse et qualité. Ollama expose les deux comme modèles locaux texte‑et‑image.

Pourquoi ne pas utiliser `qwen3.5:0.8b` ou 2B ?

Les modèles 2B sont mieux adaptés à des tâches multimodales plus simples ou à des déploiements en périphérie qu'à ce pipeline complet.

Quel est le meilleur modèle Qwen 3.5 local pour ce cas d'usage ?

Le qwen3.5:9b est le meilleur point de départ. Il reste compact (6,6 Go dans Ollama), prend en charge l'entrée texte‑et‑image, et surpasse largement les très petites variantes pour ce type de tâche de génération exigeant en raisonnement.

Puis‑je aussi utiliser localement les modèles Qwen 3.5 plus grands ?

Oui. Ollama propose actuellement des variantes 27b, 35b et 122b également, toutes avec prise en charge texte‑et‑image. Le compromis porte sur la mémoire et la latence.


Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

Je suis experte Google Developers en ML (Gen AI), triple experte Kaggle et ambassadrice Women Techmakers, avec plus de trois ans d’expérience dans la tech. J’ai cofondé une startup dans le domaine de la santé en 2020 et je poursuis actuellement un master en informatique à Georgia Tech, avec une spécialisation en apprentissage automatique.

Sujets
Intelligence artificielle
Grands modèles linguistiques

Top DataCamp Courses

Cours

Développement logiciel avec Cursor

1 h 30 min
4.2K
Développez du code prêt pour la production avec Cursor. Découvrez les invites IA, la refactorisation, les tests et les workflows avancés.
Afficher les détailsRight Arrow
Commencer Le Cours
Voir plusRight Arrow