Corso
Qwen 3.5 è una famiglia di modelli multimodali open-source progettati per gestire input sia testuali sia visivi. I modelli seguono un’architettura image-text-to-text, che consente loro di ragionare su immagini o fotogrammi video insieme a prompt in linguaggio naturale.
In questo tutorial costruiremo un’app Streamlit che converte un video di gameplay in un gioco per browser giocabile usando il modello locale Qwen3.5 small, qwen3.5:9b, ospitato tramite Ollama. L’applicazione estrae fotogrammi dal video caricato, deduce le meccaniche di gioco, le converte in una specifica strutturata e poi genera un gioco completo in HTML/CSS/JavaScript che può essere visualizzato in anteprima direttamente nell’interfaccia.
Lungo il percorso imparerai a:
- Estrarre fotogrammi rappresentativi dai video per il ragionamento multimodale
- Eseguire Qwen 3.5 in locale con Ollama per l’inferenza immagine-testo
- Progettare una pipeline di prompting in due fasi per ragionamento e generazione di codice
- Convertire osservazioni visive del gameplay in una specifica di gioco strutturata
- Generare un gioco HTML5 giocabile a partire dalle meccaniche dedotte
- Creare un’interfaccia Streamlit interattiva per eseguire e visualizzare in anteprima la pipeline
Alla fine avrai una demo funzionante che trasforma una breve clip di gameplay in un semplice gioco per browser, generato interamente su una macchina locale. Puoi vedere una versione ridotta del flusso di lavoro nel video qui sotto:
Che cos’è Qwen 3.5 Small?
Qwen 3.5 è una famiglia di modelli multimodali progettata per coprire un’ampia gamma di scenari di deployment, dai modelli leggeri per l’edge a sistemi di ragionamento di frontiera.
La lineup ufficiale di Qwen 3.5 attualmente include varianti 0.8B, 2B, 4B, 9B, 27B, 35B-A3B, 122B-A10B e 397B-A17B, insieme a checkpoint base e quantizzati per alcuni modelli.
La scelta del modello small giusto dipende dal tipo di workflow che vuoi costruire.
- Le varianti 0.8B e 2B sono più adatte a dispositivi edge, prototipazione rapida e contesti hardware limitati.
- Il modello 4B rappresenta un passo in avanti e funziona bene per piccoli agenti locali o compiti leggeri di ragionamento visivo.
- Il modello 9B è più indicato per applicazioni multimodali locali perché rimane abbastanza compatto da girare in locale ma sufficientemente potente da dedurre meccaniche di gioco e generare codice HTML utilizzabile.
Per questo tutorial abbiamo usato il modello qwen3.5:9b, che offre un ottimo equilibrio tra capacità, distribuibilità locale e ragionamento multimodale, rendendolo ideale per costruire una pipeline da video frame a gioco per browser senza dipendere da API esterne.
Puoi anche leggere la nostra guida su come eseguire Qwen3.5 in locale su una singola GPU.
Perché usare un modello small locale per questo?
Ho testato anche questa demo con il modello qwen/qwen3.5-397b-a17b. Sebbene questo modello sia significativamente più grande e in generale più capace rispetto alla variante locale 9B, richiede inferenza tramite API, introducendo dipendenze esterne e potenziale latenza.
In pratica, se il tempo di esecuzione non è un vincolo rigido, il tempo totale di generazione differiva di circa ~3 minuti tra i due modelli nei miei test. Il modello più grande ha prodotto una logica di gameplay leggermente più fluida e un’anteprima più rifinita, ma il miglioramento non è stato drastico.
Considerando che il modello 9B gira completamente in locale, i risultati sono sorprendentemente competitivi. Nonostante le dimensioni molto più ridotte, è in grado di generare giochi per browser giocabili con solo un modesto calo di qualità rispetto al modello 397B molto più grande.
Puoi leggere la nostra guida completa sul fine-tuning di Qwen3.5 small per saperne di più su come ottenere il massimo dalla variante 0.8B.
Progetto d’esempio con Qwen 3.5: crea un generatore Video-to-Game
In questa sezione costruiremo un’app Streamlit che:
- riceve una breve clip di gameplay
- estrae un piccolo numero di fotogrammi rappresentativi
- invia quei fotogrammi a
qwen3.5:9btramite Ollama - chiede al modello di dedurre una specifica di gioco strutturata
- chiede al modello di generare un gioco HTML a file singolo
- mostra l’anteprima del gioco generato all’interno dell’app Streamlit
Questa configurazione mette in evidenza le capacità multimodali di Qwen che supportano attività come il reverse engineering della logica a partire da filmati di gameplay e la loro trasformazione in codice
Passaggio 1: installa le dipendenze
Prima di costruire l’applicazione, dobbiamo preparare un ambiente locale con le librerie necessarie per l’elaborazione video, l’inferenza multimodale e l’interfaccia interattiva.
pip install streamlit opencv-python ollama python-dotenv pydantic
La demo usa un piccolo set di librerie Python:
Streamlitper costruire l’interfaccia web interattiva in cui gli utenti caricano i video di gameplay e visualizzano in anteprima i giochi generati.OpenCVper decodificare il video caricato ed estrarre fotogrammi rappresentativi.Ollamaper eseguire in locale il modello Qwen 3.5 per il ragionamento multimodale e la generazione di codice.python-dotenvper gestire variabili d’ambiente se necessario.Pydanticper imporre uno schema strutturato alla specifica di gioco dedotta prima di generare il codice.
Poi dobbiamo scaricare il modello. Ollama semplifica l’operazione scaricando il modello direttamente dal suo registry:
ollama run qwen3.5:9b
La prima esecuzione scarica il modello e lo archivia in locale. Il qwen3.5:9B è di circa 6,6 GB, supporta input testuali e di immagine e offre una context window da 256K token, che lo rende adatto a flussi di lavoro multimodali.
Passaggio 2: import
Ora che l’ambiente locale è pronto, il passo successivo è importare le librerie che useremo nell’applicazione e creare una cartella per archiviare gli artefatti generati.
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)
Il blocco sopra riunisce lo stack principale del progetto. Usiamo json per formattare la specifica di gioco strutturata, base64 per codificare i fotogrammi estratti prima di inviarli al modello e tempfile per salvare in modo sicuro i video caricati durante una sessione. Path da pathlib ci offre un modo pulito per gestire i percorsi, mentre List da typing è usato come type hint nelle definizioni di schema.
Successivamente importiamo le librerie principali dell’applicazione: cv2 da OpenCV si occupa di leggere la clip di gameplay caricata ed estrarre fotogrammi rappresentativi; ollama è l’interfaccia usata per comunicare con il modello qwen3.5:9b in esecuzione locale. streamlit fornisce la UI web e streamlit.components.v1 ci permette di incorporare direttamente nell’app il gioco HTML generato per l’anteprima dal vivo e l’interazione.
Infine importiamo BaseModel, Field e ValidationError da Pydantic. Questi servono a definire e validare lo schema JSON strutturato che rappresenta il game design dedotto. Invece di fidarci di un output testuale poco formattato, costringiamo il modello a rispettare uno schema prevedibile prima di passare il risultato al generatore del gioco HTML.
Le ultime due righe creano una directory outputs_local se non esiste già. In questa cartella salviamo gli artefatti generati, come la specifica di gioco dedotta e il file HTML finale del gioco, così è più facile ispezionare o scaricare i risultati dopo ogni esecuzione.
Nel prossimo passaggio definiremo le classi di schema Pydantic che descrivono la specifica di gioco che il modello deve generare a partire dai fotogrammi di gameplay estratti.
Passaggio 3: definisci lo schema
A questo punto definiamo lo schema strutturato che il modello deve seguire quando descrive le meccaniche di gioco dedotte dai fotogrammi video.
Invece di chiedere al modello di generare direttamente codice HTML a partire dalle immagini, prima gli richiediamo di produrre una specifica di gioco strutturata. Questa rappresentazione intermedia rende la pipeline molto più facile da fare debug, validare ed estendere.
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]
Questo schema definisce la struttura principale della specifica di gioco che il modello deve generare.
- La classe
Entityrappresenta qualsiasi oggetto presente nel gioco, come il giocatore, i nemici, gli ostacoli o gli elementi UI. Ogni entità include un nome, il suo ruolo nel gioco e una breve descrizione del comportamento. - La classe
Physicsdescrive come gli oggetti si muovono e interagiscono nel mondo di gioco. Questi campi catturano meccaniche di base come gravità, impulso/salto, gestione delle collisioni e schemi di movimento. - La classe
VisualStylecattura la presentazione visiva del gioco. Include la prospettiva della camera (ad esempio top-down o side-scrolling), la palette di colori, lo stile dello sfondo e gli elementi UI visibili come contatori del punteggio o indicatori di salute. - Infine, la classe
GameSpecriunisce tutto. Rappresenta il design completo del gioco dedotto dai fotogrammi, includendo obiettivo, controlli, gameplay loop, entità, sistema di punteggio, condizioni di vittoria/sconfitta e assunzioni fatte dal modello nell’interpretare i fotogrammi.
Validando l’output del modello contro questo schema, garantiamo che il design dedotto segua sempre una struttura prevedibile. Questo rende anche molto più semplice passare la specifica alla fase successiva della pipeline.
Passaggio 4: prompt per la generazione in due fasi
Con lo schema a posto, il passo successivo è definire i prompt che guidano il modello nelle due fasi della nostra pipeline.
La prima fase si concentra sul reverse engineering delle meccaniche di gameplay a partire dai fotogrammi estratti e sulla produzione di una specifica di gioco strutturata.
La seconda fase prende quella specifica e genera un gioco per browser giocabile usando HTML, CSS e JavaScript.
Passaggio 4.1: definisci i system prompt
Il primo componente della nostra strategia di prompting sono i system prompt, che definiscono il ruolo che il modello dovrebbe svolgere durante ciascuna fase della 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()
Il primo system prompt istruisce il modello a comportarsi come un analista di gameplay. Sottolinea di fare assunzioni minime e incoraggia il modello a dedurre solo le meccaniche fortemente supportate dai fotogrammi.
Poiché il modello vede solo un piccolo numero di immagini campionate, il prompt lo orienta esplicitamente verso giochi in stile arcade semplice che possono essere implementati con logica leggera per browser.
Il secondo system prompt cambia il ruolo del modello in sviluppatore JavaScript. In questa fase, il modello riceve una specifica di gioco strutturata ed è responsabile di convertirla in un gioco HTML completo.
Passaggio 4.2: costruisci il prompt della game spec
Definiamo ora una funzione helper che costruisce il prompt utente per la prima fase della pipeline. Questo prompt chiede al modello di analizzare i fotogrammi estratti e produrre una specifica di gioco strutturata.
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()
Questa funzione costruisce il prompt che verrà inviato al modello insieme ai fotogrammi video estratti. Include diversi elementi importanti:
- Un promemoria che il modello sta analizzando fotogrammi statici e non l’intero video
- Un orientamento verso meccaniche arcade semplici
- L’intera struttura dello schema JSON, assicurando che l’output corrisponda al nostro modello
GameSpec - Facoltativamente, l’utente può aggiungere suggerimenti e vincoli che permettono all’interfaccia di indirizzare il modello verso un determinato stile di gioco
Inserendo lo schema direttamente nel prompt, incoraggiamo il modello a produrre un output che possa essere validato da Pydantic nella fase successiva della pipeline.
Passaggio 4.3: costruisci il prompt per la generazione del codice
Una volta generata e validata la specifica di gioco, ci serve un secondo prompt per convertirla in un gioco effettivamente giocabile.
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 funzione build_code_user_prompt converte il dizionario GameSpec in JSON formattato e lo inserisce nel prompt. Il modello usa quindi questa descrizione strutturata come brief di design per generare il gioco HTML finale.
Poiché la specifica definisce già meccaniche, entità, controlli e obiettivi, il compito del modello diventa molto più semplice. Deve solo tradurre quel design in logica di gioco JavaScript e in un ciclo di rendering basato su canvas.
Nel prossimo passaggio implementeremo la pipeline di estrazione dei fotogrammi con OpenCV, che converte il video di gameplay caricato in un piccolo set di immagini rappresentative analizzabili dal modello Qwen 3.5.
Passaggio 5: estrai fotogrammi rappresentativi
I modelli locali Qwen 3.5 di Ollama accettano testo e immagini, non video grezzi. Quindi, invece di inviare direttamente la clip caricata, estraiamo un piccolo set di fotogrammi equidistanti e li passiamo al modello.
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
Le utility sopra definiscono cinque componenti chiave per estrarre i fotogrammi rappresentativi dal video caricato:
- Gestione upload video: la funzione
save_uploaded_video()salva il video di gameplay caricato come file temporaneo su disco. Poiché Streamlit carica i file in memoria, scriverlo su disco consente a OpenCV di leggerlo in modo efficiente durante l’estrazione dei fotogrammi preservando l’estensione originale. - Campionamento dei fotogrammi: la funzione
extract_frames()carica il video con OpenCV e campiona un piccolo numero di fotogrammi equidistanti lungo la sua durata. Invece di elaborare ogni frame, calcola un intervallo di campionamento ed estrae fotogrammi rappresentativi che catturano il gameplay. Ogni fotogramma è codificato comeJPEGe convertito in stringabase64così da poter essere inviato direttamente come input immagine al modello multimodale Qwen. - Estrazione JSON strutturato: l’helper
extract_json_block()assicura che l’output del modello possa essere analizzato correttamente. Il contenuto estratto può poi essere validato in sicurezza contro lo schemaGameSpec. - Pulizia della risposta HTML: la funzione
cleanup_html_response()rimuove formattazioni markdown che il modello potrebbe includere quando genera il codice. Se il modello racchiude l’HTML in backtick tripli, questo helper li rimuove così che l’HTML possa essere renderizzato direttamente nell’anteprima di Streamlit. - Interazione da tastiera per giochi embedded: la funzione
patch_html_for_iframe_keyboard()inietta una piccola patch JavaScript nell’HTML generato per assicurare che i controlli da tastiera funzionino correttamente dentro l’iframe di Streamlit. Mette automaticamente a fuoco l’elemento canvas, assegna untabindexse manca e impedisce al browser di intercettare frecce e barra spaziatrice così da usarle per i controlli di gioco.
Insieme, queste utility costituiscono il livello di preprocessing video e gestione delle risposte della pipeline.
Passaggio 6: deduci la specifica di gioco
Ora che abbiamo i fotogrammi estratti e i template di prompt pronti, possiamo implementare la prima fase della pipeline di ragionamento, cioè convertire i fotogrammi di gameplay in una specifica di gioco strutturata.
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()
La funzione sopra definisce quattro fasi chiave nella pipeline di inferenza della game spec:
- Preparazione dei fotogrammi: la chiamata a
extract_frames()campiona un piccolo set di fotogrammi rappresentativi dal video caricato. Queste immagini fungono da evidenza visiva che il modello userà per dedurre le meccaniche di gioco. - Costruzione del prompt: la funzione
build_spec_user_prompt()crea dinamicamente il prompt utente usando il suggerimento opzionale, eventuali vincoli aggiuntivi e il numero massimo di fotogrammi estratti. Questo prompt fornisce al modello un’istruzione chiara per produrre una specifica di gioco conforme allo schema. - Chiamata al modello multimodale: la richiesta
ollama.chat()invia sia il prompt testuale sia i fotogrammi estratti al modello localeqwen3.5:9b. Le impostazioni di generazione usano unatemperaturebassa per mantenere l’output deterministico enum_ctx=8192per dare spazio sufficiente a prompt e risposta strutturata. - Validazione dello schema: una volta che il modello risponde, la funzione estrae il blocco JSON, lo analizza e lo valida contro lo schema Pydantic
GameSpec. Infine, la specifica validata viene restituita come dizionario Python.
Ora abbiamo una rappresentazione strutturata del gioco che il modello ha dedotto dai fotogrammi campionati. Nel prossimo passaggio useremo questa specifica validata come input per la fase di generazione del gioco HTML.
Passaggio 7: genera il gioco HTML
A questo punto, il modello non deve più dedurre meccaniche dalle immagini. Invece chiediamo a qwen3.5:9b di produrre un singolo file HTML auto-contenuto che includa layout, stili, game loop, controlli e logica di rendering.
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)
La funzione sopra esegue i seguenti quattro passaggi per generare e preparare il gioco HTML finale:
- Consegna del prompt strutturato: la funzione prende il dizionario
game_specvalidato e lo passa abuild_code_user_prompt(). Questo converte la specifica di gioco in un prompt di implementazione dettagliato che istruisce il modello a generare un gioco HTML completo a file singolo. - Generazione del codice con il modello Qwen: la chiamata
ollama.chat()invia il system prompt per la generazione di codice e il prompt utente al modello localeqwen3.5:9b. Poiché il compito ora è pura generazione di codice e non più ragionamento multimodale, il modello riceve solo testo. - Pulizia e validazione dell’HTML: la risposta grezza del modello viene passata a
cleanup_html_response()per rimuovere eventuale markdown o formattazione extra. Eseguiamo poi un semplice controllo di validazione per confermare che l’output contenga un documento HTML. - Patch per la tastiera nell’iframe: l’HTML finale viene passato a
patch_html_for_iframe_keyboard(), che inietta una piccola correzione JavaScript per rendere il gioco giocabile dentro l’iframe incorporato di Streamlit. Questo passaggio è importante perché spesso i browser game non catturano correttamente frecce e barra spaziatrice a meno che il canvas non sia esplicitamente focalizzabile e messo a fuoco automaticamente.
Ora abbiamo un gioco HTML per browser giocabile generato interamente dalla specifica di gioco dedotta.
Passaggio 8: UI in Streamlit
Come ultimo passaggio, incapsuliamo tutto dentro una semplice interfaccia Streamlit così che gli utenti possano provare il sistema senza toccare il codice sottostante. L’interfaccia espone tre funzionalità principali:
- Caricare un video di gameplay
- Configurare i parametri di generazione come suggerimenti e campionamento dei fotogrammi
- Visualizzare in anteprima e scaricare il gioco generato
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",
)
Il codice sopra definisce come l’utente carica una clip di gameplay, fornisce facoltativamente un suggerimento sul tipo di gioco e controlla quanti fotogrammi estrarre dal video per l’analisi. Una volta caricato un video, l’app lo mostra accanto a un pulsante Generate game che avvia l’intera pipeline.
Quando l’utente esegue la pipeline, l’app prima estrae i fotogrammi e deduce le meccaniche di gioco usando il modello locale qwen3.5:9b, producendo una specifica di gioco strutturata. Nella seconda fase, il modello genera da quella specifica un gioco HTML completo. I risultati sono salvati nello stato di sessione di Streamlit così da persistere tra gli aggiornamenti della UI.
Al termine della generazione, l’interfaccia mostra la specifica di gioco dedotta, un’anteprima giocabile del gioco generato, il sorgente HTML grezzo e i pulsanti per scaricare sia il gioco HTML sia la specifica JSON.
Per provarlo, salva il codice come app.py e avvia:
streamlit run app.py

Conclusione
Qwen 3.5 è una delle famiglie di modelli multimodali più interessanti oggi disponibili. In questo tutorial abbiamo usato qwen3.5:9b tramite Ollama per costruire un generatore da video di gameplay a gioco completamente locale. L’app estrae fotogrammi da una breve clip, deduce un game design strutturato, genera un gioco per browser a file singolo e lo visualizza in anteprima dentro Streamlit.
Il modello ospitato qwen/qwen3.5-397b-a17b resta l’opzione più rapida e potente per questo compito, ma il risultato locale con 9B è notevole di per sé. Dimostra che un modello multimodale small può ormai eseguire ragionamento visivo significativo e generazione di codice su hardware consumer.
Se vuoi spingerti oltre, confronta 9B con i modelli 4B e 27B, aggiungi un ciclo di raffinamento o trasforma l’output in un mini motore di gioco che supporti più iterazioni a partire dalla stessa clip.
Qwen3.5 Small FAQ
Posso eseguire questa demo con `qwen3.5:4b` invece di 9B?
Sì, ma i risultati di solito saranno meno affidabili. Il modello 4B è comunque utile per esperimenti locali leggeri, ma la variante 9B offre un equilibrio migliore tra velocità e qualità. Ollama espone entrambi come modelli locali testo+immagine.
Perché non usare `qwen3.5:0.8b` o 2B?
I modelli 2B sono più adatti a compiti multimodali più semplici o a deployment in stile edge rispetto a questa pipeline completa.
Qual è il miglior modello Qwen 3.5 locale per questo caso d’uso?
Il qwen3.5:9b è il punto di partenza migliore. È ancora compatto (6,6 GB in Ollama), supporta input testuali e di immagine e va molto meglio delle varianti piccolissime per questo tipo di task di generazione pesantemente basata sul ragionamento.
Posso usare anche i modelli Qwen 3.5 più grandi in locale?
Sì. Ollama attualmente elenca anche le varianti 27b, 35b e 122b, tutte con supporto testo+immagine. Il compromesso riguarda memoria e latenza.
Sono una Google Developers Expert in ML (Gen AI), una Kaggle 3x Expert e una Women Techmakers Ambassador con oltre 3 anni di esperienza nel tech. Ho co-fondato una startup health-tech nel 2020 e sto conseguendo un master in informatica al Georgia Tech, con specializzazione in machine learning.


