Ga naar hoofdinhoud

Qwen 3.5 Small-modellen tutorial: bouw een video-naar-gamegenerator met Ollama

Leer hoe je Qwen 3.5 small-modellen lokaal gebruikt via Ollama om een korte gameplayclip om te zetten in een speelbare HTML-game.
Bijgewerkt 2 okt 2026  · 14 min lezen

Verken met AI

ChatGPTClaudePerplexity

Qwen 3.5 is een familie van open-source multimodale modellen die zowel tekst- als visuele input aankunnen. De modellen volgen een image-text-to-text-architectuur, waardoor ze over afbeeldingen of videoframes kunnen redeneren naast prompts in natuurlijke taal. 

In deze tutorial bouwen we een Streamlit-app die een gameplayvideo omzet in een speelbare browsergame met behulp van het lokale Qwen3.5 small-, qwen3.5:9b-model, gehost via Ollama. De applicatie extraheert frames uit de geüploade video, leidt de gamemechanieken af, zet deze om in een gestructureerde specificatie en genereert vervolgens een complete HTML/CSS/JavaScript-game die je direct in de interface kunt bekijken.

Onderweg leer je hoe je:

  • Representatieve frames uit video’s extraheert voor multimodaal redeneren
  • Qwen 3.5 lokaal draait met Ollama voor image-text-inferentie
  • Een tweefasige prompting-pipeline ontwerpt voor redeneren en codegeneratie
  • Visuele gameplayobservaties omzet in een gestructureerde gamespecificatie
  • Een speelbare HTML5-browsergame genereert op basis van de afgeleide mechanieken
  • Een interactieve Streamlit-interface bouwt om de pipeline te draaien en te bekijken

Aan het einde heb je een werkende demo die een korte gameplayclip omzet in een eenvoudige, speelbare browsergame die volledig op een lokale machine is gegenereerd. Hieronder zie je een verkorte versie van de workflow in de video:

Wat is Qwen 3.5 Small?

Qwen 3.5 is een multimodale modellenfamilie die is ontworpen voor uiteenlopende implementatiescenario’s, van lichte edge-modellen tot reasoning-systemen op frontier-schaal. 

De officiële Qwen 3.5-reeks omvat momenteel 0.8B, 2B, 4B, 9B, 27B, 35B-A3B, 122B-A10B en 397B-A17B varianten, plus base- en gequantiseerde checkpoints voor sommige modellen. 

De juiste small-variant kiezen hangt af van het type workflow dat je wilt bouwen. 

  • De 0.8B- en 2B-varianten zijn het meest geschikt voor edge-apparaten, snelle prototyping en krappe hardwareomgevingen. 
  • Het 4B-model is een stapje hoger en werkt goed voor kleine lokale agents of lichte visuele redeneertaken. 
  • Het 9B-model is beter geschikt voor lokale multimodale applicaties, omdat het compact genoeg blijft om lokaal te draaien maar krachtig genoeg is om gamemechanieken af te leiden en bruikbare HTML-gamecode te genereren. 

Voor deze tutorial gebruikten we het qwen3.5:9b-model, dat een sterke balans biedt tussen capaciteit, lokale inzetbaarheid en multimodaal redeneren. Hierdoor is het zeer geschikt om een pipeline van videoframes naar browsergame te bouwen zonder externe API’s.

Je kunt ook onze gids lezen over Qwen3.5 lokaal draaien op één GPU. 

Waarom hiervoor een lokaal small-model gebruiken?

Ik heb deze demo ook getest met het qwen/qwen3.5-397b-a17b-model. Hoewel dit model aanzienlijk groter en over het algemeen capabeler is dan de lokale 9B-variant, vereist het inferentie via een API, wat externe afhankelijkheden en mogelijke latency introduceert.

In de praktijk, als runtime niet strikt begrensd is, verschilde de totale generatietijd in mijn tests met ongeveer ~3 minuten tussen de twee modellen. Het grotere model leverde iets vloeiendere gameplaylogica en een meer gepolijste preview-ervaring op, maar de verbetering was niet dramatisch.

Gezien het feit dat het 9B-model volledig lokaal draait, zijn de resultaten verrassend competitief. Ondanks de veel kleinere omvang is het in staat speelbare browsergames te genereren met slechts een bescheiden kwaliteitsverlies vergeleken met het veel grotere 397B-model.

Je kunt onze volledige gids lezen over Qwen3.5 small fine-tunen om meer te leren over het maximale uit de 0.8B-variant halen. 

Qwen 3.5 Voorbeeldproject: bouw een video-naar-gamegenerator

In deze sectie bouwen we een Streamlit-app die:

  • een korte gameplayclip inneemt
  • een klein aantal representatieve frames extraheert
  • die frames via Ollama naar qwen3.5:9b stuurt
  • het model vraagt om een gestructureerde gamespecificatie af te leiden
  • het model vraagt om een HTML-game in één bestand te genereren
  • de gegenereerde game in de Streamlit-app toont

Deze setup benadrukt de multimodale mogelijkheden van Qwen die taken ondersteunen zoals het reverse-engineeren van logica uit gameplaybeelden en dit omzetten naar code

Stap 1: Installeer dependencies

Voordat we de applicatie bouwen, moeten we eerst een lokale omgeving voorbereiden met de libraries die nodig zijn voor videobewerking, multimodale inferentie en de interactieve interface. 

pip install streamlit opencv-python ollama python-dotenv pydantic

De demo gebruikt een kleine set Python-libraries:

  • Streamlit om de interactieve webinterface te bouwen waar gebruikers gameplayvideo’s uploaden en gegenereerde games bekijken.
  • OpenCV om de geüploade video te decoderen en representatieve frames te extraheren.
  • Ollama om het lokale Qwen 3.5-model te draaien voor multimodaal redeneren en codegeneratie.
  • python-dotenv om indien nodig omgevingsvariabelen te beheren.
  • Pydantic om een gestructureerd schema af te dwingen voor de afgeleide gamespecificatie voordat we code genereren.

Vervolgens moeten we het model zelf downloaden. Ollama maakt dit eenvoudig door het model direct uit zijn modelregistry te pullen:

ollama run qwen3.5:9b

De eerste run downloadt het model en slaat het lokaal op. De qwen3.5:9B is ongeveer 6,6 GB groot, ondersteunt tekst- en beeldinvoer en biedt een contextvenster van 256K tokens, wat het zeer geschikt maakt voor multimodale workflows.

Stap 2: Imports

Nu de lokale omgeving klaar is, is de volgende stap het importeren van de libraries die we in de hele applicatie gebruiken en het aanmaken van een map om gegenereerde artefacten op te slaan. 

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)

Het bovenstaande blok brengt de kernstack voor het project samen. We gebruiken json om de gestructureerde gamespecificatie te formatteren, base64 om geëxtraheerde videoframes te encoderen voordat we ze naar het model sturen, en tempfile om geüploade video’s veilig op te slaan tijdens een sessie. Path uit pathlib geeft ons een nette manier om paden te beheren, terwijl List uit typing wordt gebruikt voor typehints in de schemadefinities.

Vervolgens importeren we de belangrijkste applicatielibraries, zoals cv2 van OpenCV dat verantwoordelijk is voor het lezen van de geüploade gameplayclip en het extraheren van representatieve frames, ollama is de interface die we gebruiken om te communiceren met het lokaal draaiende qwen3.5:9b-model. streamlit levert de web-UI, en streamlit.components.v1 laat ons de gegenereerde HTML-game direct in de app insluiten voor live preview en interactie.

Tot slot importeren we BaseModel, Field en ValidationError van Pydantic. Deze worden gebruikt om het gestructureerde JSON-schema te definiëren en te valideren dat het afgeleide gameontwerp vertegenwoordigt. In plaats van het model te vertrouwen om losjes geformatteerde tekst uit te voeren, dwingen we het in een voorspelbaar schema voordat we dat resultaat doorgeven aan de HTML-gamegenerator.

De laatste twee regels maken een outputs_local-map aan als die nog niet bestaat. Deze map is waar we gegenereerde artefacten opslaan, zoals de afgeleide gamespecificatie en het definitieve HTML-gamebestand, zodat je na elke run resultaten makkelijker kunt inspecteren of downloaden.

In de volgende stap definiëren we de Pydantic-schemaklassen die de gamespecificatie beschrijven die het model moet genereren op basis van de geëxtraheerde gameplayframes.

Stap 3: Definieer het schema

In deze fase definiëren we het gestructureerde schema dat het model moet volgen bij het beschrijven van de gamemechanieken die uit de videoframes zijn afgeleid. 

In plaats van het model te vragen om direct HTML-code uit afbeeldingen te genereren, laten we het eerst een gestructureerde gamespecificatie produceren. Deze tussenrepresentatie maakt de pipeline veel eenvoudiger te debuggen, valideren en uit te breiden.

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]

Dit schema definieert de kernstructuur van de gamespecificatie die het model moet genereren.

  • De Entity-klasse vertegenwoordigt elk object in de game, zoals de speler, vijanden, obstakels of UI-elementen. Elke entiteit bevat een naam, zijn rol binnen de game en een korte beschrijving van het gedrag.
  • De Physics-klasse beschrijft hoe objecten bewegen en interageren in de gamewereld. Deze velden leggen basismechanieken vast zoals zwaartekracht, impuls/spronggedrag, botsingsafhandeling en bewegingspatronen.
  • De VisualStyle-klasse legt de visuele presentatie van de game vast. Dit omvat de cameraperspectief (zoals top-down of side-scrolling), het kleurenpalet, de achtergrondstijl en zichtbare UI-elementen zoals scoretellers of gezondheidsindicatoren.
  • Ten slotte brengt de GameSpec-klasse alles samen. Deze vertegenwoordigt het volledige gameontwerp dat is afgeleid uit de videoframes, inclusief het doel, de besturing, de gameplayloop, entiteiten, scoresysteem, win-/verliesvoorwaarden en aannames die het model maakte bij het interpreteren van de frames.

Door de uitvoer van het model te valideren tegen dit schema, zorgen we ervoor dat het afgeleide ontwerp altijd een voorspelbare structuur volgt. Dit maakt het ook veel eenvoudiger om de specificatie door te geven aan de volgende fase van de pipeline.

Stap 4: Prompts voor tweefasige generatie

Met het schema op zijn plek is de volgende stap het definiëren van de prompts die het model door de twee fasen van onze pipeline begeleiden. 

De eerste fase richt zich op het reverse-engineeren van de gamemechanieken uit de geëxtraheerde frames en het produceren van een gestructureerde gamespecificatie. 

De tweede fase neemt die specificatie en genereert een speelbare browsergame met HTML, CSS en JavaScript.

Stap 4.1: Definieer de systeemprompts

Het eerste onderdeel van onze prompting-strategie zijn de systeemprompts, die de rol definiëren die het model moet spelen tijdens elke fase van de 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()

De eerste systeemprompt instrueert het model om zich te gedragen als een gameplay-analist. Het benadrukt minimale aannames en moedigt het model aan alleen mechanieken af te leiden die sterk worden ondersteund door de frames. 

Omdat het model slechts een klein aantal gesamplede beelden ziet, stuurt de prompt het expliciet in de richting van eenvoudige arcadegames die met lichte browserlogica kunnen worden geïmplementeerd.

De tweede systeemprompt schakelt de rol van het model om naar JavaScript-ontwikkelaar. In deze fase ontvangt het model een gestructureerde gamespecificatie en is het verantwoordelijk voor het omzetten daarvan in een complete HTML-game. 

Stap 4.2: Bouw de game-spec-prompt

Vervolgens definiëren we een helperfunctie die de user prompt voor de eerste fase van de pipeline opstelt. Deze prompt vraagt het model om de geëxtraheerde frames te analyseren en een gestructureerde gamespecificatie te produceren.

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()

Deze functie bouwt de prompt die samen met de geëxtraheerde videoframes naar het model wordt gestuurd. De prompt bevat een aantal belangrijke elementen:

  • Een herinnering dat het model stilstaande beelden analyseert en niet de volledige video
  • Een voorkeur voor eenvoudige arcade-mechanieken
  • De volledige JSON-schemastructuur, zodat de output overeenkomt met ons GameSpec-model
  • Optioneel kunnen gebruikers hints en constraints toevoegen waarmee de interface het model naar een bepaalde gamestijl stuurt

Door het schema direct in de prompt op te nemen, stimuleren we het model om output te produceren die door Pydantic in de volgende fase van de pipeline kan worden gevalideerd.

Stap 4.3: Bouw de codegeneratieprompt

Zodra de gamespecificatie is gegenereerd en gevalideerd, hebben we een tweede prompt nodig om die specificatie om te zetten in een daadwerkelijk speelbare game.

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()

De functie build_code_user_prompt zet de GameSpec-dictionary om in geformatteerde JSON en plaatst die in de prompt. Het model gebruikt deze gestructureerde beschrijving vervolgens als ontwerpopdracht om de uiteindelijke HTML-game te genereren.

Omdat de specificatie de mechanieken, entiteiten, besturing en doelen al definieert, wordt de taak van het model veel eenvoudiger. Het hoeft die opzet alleen nog te vertalen naar JavaScript-gamelogica en een canvas-gebaseerde renderlus.

In de volgende stap implementeren we de frame-extractiepipeline met OpenCV, die de geüploade gameplayvideo omzet in een kleine set representatieve afbeeldingen die door het Qwen 3.5-model kunnen worden geanalyseerd.

Stap 5: Extraheer representatieve frames

De lokale Qwen 3.5-modellen van Ollama accepteren tekst en afbeeldingen, geen ruwe video. In plaats van de geüploade clip direct te sturen, extraheren we dus een kleine set gelijkmatig verdeelde frames en geven die door aan het model. 

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

De bovenstaande utilities definiëren vijf kernelementen om de representatieve frames uit de geüploade video te extraheren:

  • Afhandeling van videoupload: De functie save_uploaded_video() slaat de geüploade gameplayvideo op als een tijdelijk bestand op schijf. Omdat Streamlit bestanden in het geheugen uploadt, maakt het schrijven naar schijf het mogelijk voor OpenCV om het efficiënt te lezen tijdens het extraheren van frames, met behoud van de originele bestandsextensie.
  • Frames samplen uit video: De functie extract_frames() laadt de video met OpenCV en sampelt een klein aantal gelijkmatig verdeelde frames over de duur. In plaats van elk frame te verwerken, berekent de functie een sample-interval en extraheert representatieve frames die de gameplay vastleggen. Elk frame wordt als JPEG gecodeerd en omgezet naar een base64-string zodat het direct als afbeeldingsinput naar het multimodale Qwen-model kan worden gestuurd.
  • Gestructureerde JSON-extractie: De helperfunctie extract_json_block() zorgt ervoor dat de modeluitvoer correct kan worden geparseerd. De geëxtraheerde inhoud kan vervolgens veilig worden gevalideerd tegen het GameSpec-schema.
  • HTML-respons opschonen: De functie cleanup_html_response() verwijdert markdown-opmaak die het model mogelijk toevoegt bij het genereren van code. Als het model de HTML-uitvoer omwikkelt met triple backticks, stript deze helper die markeringen zodat de HTML direct in de Streamlit-preview kan worden gerenderd.
  • Toetsenbordinteractie voor ingesloten games: De functie patch_html_for_iframe_keyboard() voegt een kleine JavaScript-patch in de gegenereerde HTML in om te zorgen dat toetsenbordbediening goed werkt binnen de Streamlit-iframe. Hij focust automatisch het canvas-element, kent een tabindex toe als die ontbreekt en voorkomt dat de browser pijltoetsen en de spatiebalk onderschept, zodat deze voor gameplaybesturing gebruikt kunnen worden.

Samen vormen deze utilities de videopreprocessing en responsafhandeling van de pipeline.

Stap 6: Leid de gamespecificatie af 

Nu we de geëxtraheerde frames en de prompttemplates klaar hebben, kunnen we de eerste fase van de reasoning-pipeline implementeren: het omzetten van de gameplayframes naar een gestructureerde gamespecificatie.

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()

De bovenstaande functie definieert vier belangrijke stappen in de game-spec-inferentiepipeline:

  • Voorbereiden van frames: De aanroep extract_frames() sampelt een kleine set representatieve frames uit de geüploade gameplayvideo. Deze afbeeldingen vormen het visuele bewijs dat het model gebruikt om de gamemechanieken af te leiden.
  • Opbouw van de prompt: De functie build_spec_user_prompt() maakt dynamisch de user prompt met de optionele hint, eventuele extra constraints en het maximum aantal geëxtraheerde frames. Deze prompt geeft het model een duidelijke instructie om een schema-conforme gamespecificatie te produceren.
  • Multimodale modelaanroep: Het verzoek ollama.chat() stuurt zowel de tekstprompt als de geëxtraheerde frames naar het lokale qwen3.5:9b-model. De generatiesettings gebruiken een lage temperature om de uitvoer deterministisch te houden en num_ctx=8192 om voldoende contextruimte te bieden voor de prompt en de gestructureerde respons.
  • Schemavalidatie: Zodra het model reageert, extraheert de functie het JSON-blok, parseert het en valideert het tegen het Pydantic-schema GameSpec. Ten slotte wordt de gevalideerde specificatie als een Python-dictionary geretourneerd.

We hebben nu een gestructureerde representatie van de game die het model uit de gesamplede videoframes heeft afgeleid. In de volgende stap gebruiken we deze gevalideerde specificatie als input voor de HTML-gamegeneratiefase.

Stap 7: Genereer de HTML-game

Op dit punt hoeft het model geen mechanieken meer uit afbeeldingen af te leiden. In plaats daarvan vragen we qwen3.5:9b om een enkel, self-contained HTML-bestand te produceren dat de layout, stijlen, gameloop, besturing en renderlogica bevat.

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)

De bovenstaande functie voert de volgende vier stappen uit om de uiteindelijke HTML-game te genereren en voor te bereiden:

  • Gestructureerde promptlevering: De bovenstaande functie neemt de gevalideerde game_spec-dictionary en geeft deze door aan de functie build_code_user_prompt(). Dit zet de gamespecificatie om in een gedetailleerde implementatieprompt die het model instrueert om een complete HTML-game in één bestand te genereren.
  • Codegeneratie met Qwen-model: De aanroep ollama.chat() stuurt de codegeneratie-systeemprompt en de user prompt naar het lokale qwen3.5:9b-model. Omdat de taak nu pure codegeneratie is in plaats van multimodaal redeneren, ontvangt het model alleen tekst. 
  • HTML-opschoning en validatie: De ruwe respons van het model gaat door cleanup_html_response() om eventuele markdown of extra opmaak te verwijderen. We voeren vervolgens een eenvoudige validatie uit om te bevestigen dat de output een HTML-document bevat.
  • Iframe-toetsenbordpatch: De uiteindelijke HTML gaat door patch_html_for_iframe_keyboard(), dat een kleine JavaScript-fix injecteert om de game speelbaar te maken binnen de ingesloten iframe van Streamlit. Deze stap is belangrijk omdat browsergames vaak de pijltoetsen en spatiebalk niet correct vastleggen, tenzij het canvas expliciet focusbaar is en automatisch focus krijgt.

We hebben nu een in de browser speelbare HTML-game die volledig is gegenereerd op basis van de afgeleide gamespecificatie.

Stap 8: Streamlit-UI

Als laatste stap wikkelen we alles in een eenvoudige Streamlit-interface, zodat gebruikers het systeem kunnen proberen zonder de onderliggende code aan te raken. De interface biedt drie hoofdfuncties:

  • Een gameplayvideo uploaden
  • Generatieparameters configureren, zoals hints en frame-sampling
  • De gegenereerde game bekijken en downloaden
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",
        )

De bovenstaande code definieert hoe de gebruiker een gameplayclip uploadt, optioneel een hint geeft over het type game en bepaalt hoeveel frames uit de video moeten worden geëxtraheerd voor analyse. Zodra een video is geüpload, toont de app deze naast een knop Generate game die de volledige pipeline start.

Wanneer de gebruiker de pipeline draait, extraheert de app eerst frames en leidt de gamemechanieken af met het lokale qwen3.5:9b-model, wat een gestructureerde gamespecificatie oplevert. In de tweede fase genereert het model daaruit een complete HTML-game. De resultaten worden opgeslagen in de Streamlit session state zodat ze behouden blijven bij UI-updates.

Na afloop toont de interface de afgeleide gamespecificatie, een live speelbare preview van de gegenereerde game, de ruwe HTML-bron en downloadknoppen om zowel de HTML-game als de JSON-specificatie op te slaan.

Om het zelf te proberen, sla de code op als app.py en start:

streamlit run app.py

Demo

Conclusie

Qwen 3.5 is een van de interessantste multimodale modellenfamilies van dit moment. In deze tutorial gebruikten we qwen3.5:9b via Ollama om een volledig lokale gameplay video-naar-gamegenerator te bouwen. De app extraheert frames uit een korte clip, leidt een gestructureerd gameontwerp af, genereert een single-file browsergame en toont die in Streamlit.

Het gehoste qwen/qwen3.5-397b-a17b-model is nog steeds de snellere en sterkere optie voor deze taak, maar het lokale 9B-resultaat is op zichzelf indrukwekkend. Het bewijst dat een klein multimodaal model nu zinvol visueel kan redeneren en code kan genereren op consumentenhardware.

Wil je verder gaan, vergelijk dan 9B met 4B- en 27B-modellen, voeg een verfijningslus toe of verander de output in een mini-game-engine die meerdere iteraties van dezelfde clip ondersteunt.

Qwen3.5 Small FAQ's

Kan ik deze demo draaien met `qwen3.5:4b` in plaats van 9B?

Ja, maar de resultaten zijn meestal minder betrouwbaar. Het 4B-model is nog steeds nuttig voor lichte lokale experimenten, maar de 9B-variant biedt een betere balans tussen snelheid en kwaliteit. Ollama stelt beide bloot als lokale tekst-en-beeldmodellen.

Waarom niet `qwen3.5:0.8b` of 2B gebruiken?

De 2B-modellen zijn beter geschikt voor eenvoudigere multimodale taken of edge-achtige deployments dan voor deze volledige pipeline.

Wat is het beste lokale Qwen 3.5-model voor deze use case?

De qwen3.5:9b is het beste startpunt. Het is nog steeds compact (6,6 GB in Ollama), ondersteunt tekst-en-beeldinput en presteert veel beter dan de allerkleinste varianten voor dit soort redeneer-intensieve generatietaken.

Kan ik de grotere Qwen 3.5-modellen ook lokaal gebruiken?

Ja. Ollama vermeldt momenteel ook 27b-, 35b- en 122b-varianten, allemaal met tekst-en-beeldondersteuning. De trade-off is geheugen en latency.


Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

Ik ben een Google Developers Expert in ML (Gen AI), een Kaggle 3x Expert en een Women Techmakers Ambassador met meer dan 3 jaar ervaring in tech. In 2020 heb ik een healthtech-startup mee opgericht en ik volg een master computer science aan Georgia Tech, met als specialisatie machine learning.

Onderwerpen
Kunstmatige intelligentie
Large Language Models

Topcursussen op DataCamp

Cursus

Softwareontwikkeling met Cursor

1 uur 30 min
4.3K
Maak code die klaar is voor productie met Cursor. Leer over AI-prompts, refactoring, testen en geavanceerde workflows.
Bekijk detailsRight Arrow
Start Cursus
Meer bekijkenRight Arrow
Gerelateerd

blog

AI vanaf nul leren in 2026: een complete gids van de experts

Ontdek alles wat je moet weten om in 2026 AI te leren, van tips om te beginnen tot handige resources en inzichten van industrie-experts.
Adel Nehme's photo

Adel Nehme

15 min

Meer BekijkenMeer Bekijken