Last chance! 50% off DataCamp Premium
Sale ends in
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:
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:
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.
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.
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.
In deze sectie bouwen we een Streamlit-app die:
qwen3.5:9b stuurtDeze setup benadrukt de multimodale mogelijkheden van Qwen die taken ondersteunen zoals het reverse-engineeren van logica uit gameplaybeelden en dit omzetten naar code
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.
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.
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.
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.Physics-klasse beschrijft hoe objecten bewegen en interageren in de gamewereld. Deze velden leggen basismechanieken vast zoals zwaartekracht, impuls/spronggedrag, botsingsafhandeling en bewegingspatronen.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.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.
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.
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.
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:
GameSpec-modelDoor 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.
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.
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:
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.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.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.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.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.
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:
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.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.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.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.
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:
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.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. 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.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.
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:
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

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.
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.
De 2B-modellen zijn beter geschikt voor eenvoudigere multimodale taken of edge-achtige deployments dan voor deze volledige pipeline.
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.
Ja. Ollama vermeldt momenteel ook 27b-, 35b- en 122b-varianten, allemaal met tekst-en-beeldondersteuning. De trade-off is geheugen en latency.
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.
Topcursussen op DataCamp
Cursus
Cursus
Cursus

blog
Adel Nehme
15 min