Ir al contenido principal

Tutorial de Qwen 3.5 Small: crea un generador de vídeo a juego con Ollama

Aprende a usar los modelos Qwen 3.5 small de forma local con Ollama para convertir un clip corto de gameplay en un juego HTML jugable.
Actualizado 17 sept 2026  · 14 min leer

Explorar con IA

ChatGPTClaudePerplexity

Qwen 3.5 es una familia de modelos multimodales de código abierto diseñada para trabajar con entradas de texto e imagen. Los modelos siguen una arquitectura image-text-to-text, lo que les permite razonar sobre imágenes o fotogramas de vídeo junto con indicaciones en lenguaje natural. 

En este tutorial, crearemos una app con Streamlit que convierte un vídeo de gameplay en un juego para navegador usando el modelo local Qwen3.5 small, qwen3.5:9b, alojado a través de Ollama. La aplicación extrae fotogramas del vídeo subido, infiere la mecánica del juego, la convierte en una especificación estructurada y después genera un juego completo en HTML/CSS/JavaScript que se puede previsualizar directamente en la interfaz.

Por el camino, aprenderás a:

  • Extraer fotogramas representativos de vídeos para razonamiento multimodal
  • Ejecutar Qwen 3.5 en local con Ollama para inferencia texto-imagen
  • Diseñar una canalización de prompts en dos etapas para razonamiento y generación de código
  • Convertir observaciones visuales del gameplay en una especificación de juego estructurada
  • Generar un juego HTML5 jugable a partir de las mecánicas inferidas
  • Crear una interfaz interactiva con Streamlit para ejecutar y previsualizar la canalización

Al final tendrás una demo funcional que convierte un clip corto de gameplay en un juego sencillo y jugable en el navegador, generado íntegramente en una máquina local. Puedes ver una versión resumida del flujo de trabajo en el vídeo de abajo:

¿Qué es Qwen 3.5 Small?

Qwen 3.5 es una familia de modelos multimodales diseñada para cubrir un amplio rango de escenarios de despliegue, desde modelos ligeros para el edge hasta sistemas de razonamiento de frontera. 

La gama oficial de Qwen 3.5 incluye actualmente variantes 0.8B, 2B, 4B, 9B, 27B, 35B-A3B, 122B-A10B y 397B-A17B, además de checkpoints base y cuantizados para algunos modelos. 

Elegir el modelo small adecuado depende del tipo de flujo de trabajo que quieras construir. 

  • Las variantes 0.8B y 2B encajan mejor en dispositivos edge, prototipado rápido y entornos con hardware limitado. 
  • El modelo 4B supone un salto y funciona bien para agentes locales pequeños o tareas ligeras de razonamiento visual. 
  • El modelo 9B es más adecuado para aplicaciones multimodales locales porque sigue siendo lo bastante compacto para ejecutarse en local, pero lo bastante potente para inferir mecánicas de juego y generar código HTML utilizable. 

Para este tutorial hemos usado el modelo qwen3.5:9b, que ofrece un buen equilibrio entre capacidad, desplegado local y razonamiento multimodal, lo que lo hace ideal para construir una canalización de fotogramas de vídeo a juego en navegador sin depender de APIs externas.

También puedes leer nuestra guía sobre cómo ejecutar Qwen3.5 en local con una sola GPU

¿Por qué usar un modelo small local para esto?

También probé esta demo con el modelo qwen/qwen3.5-397b-a17b. Aunque este modelo es significativamente mayor y en general más capaz que la variante local de 9B, requiere inferencia vía API, lo que introduce dependencias externas y posible latencia.

En la práctica, si el tiempo de ejecución no es una restricción estricta, el tiempo total de generación difirió en torno a ~3 minutos entre ambos modelos en mis pruebas. El modelo más grande sí produjo una lógica de juego algo más fluida y una experiencia de previsualización más pulida, pero la mejora no fue drástica.

Teniendo en cuenta que el modelo 9B se ejecuta completamente en local, los resultados son sorprendentemente competitivos. A pesar de su tamaño mucho menor, es capaz de generar juegos jugables en el navegador con solo una ligera caída de calidad respecto al modelo 397B, mucho mayor.

Puedes leer nuestra guía completa sobre fine-tuning de Qwen3.5 small para aprender a sacarle más partido a la variante 0.8B. 

Proyecto de ejemplo con Qwen 3.5: crea un generador de vídeo a juego

En esta sección, construiremos una app de Streamlit que:

  • recibe un clip corto de gameplay
  • extrae un pequeño número de fotogramas representativos
  • envía esos fotogramas a qwen3.5:9b a través de Ollama
  • pide al modelo que infiera una especificación de juego estructurada
  • pide al modelo que genere un juego HTML en un único archivo
  • previsualiza el juego generado dentro de la app de Streamlit

Esta configuración pone en valor las capacidades multimodales de Qwen para tareas como deducir la lógica a partir de metraje de juego y convertirla en código

Paso 1: instala las dependencias

Antes de crear la aplicación, primero debemos preparar un entorno local con las librerías necesarias para procesado de vídeo, inferencia multimodal y la interfaz interactiva. 

pip install streamlit opencv-python ollama python-dotenv pydantic

La demo usa un conjunto pequeño de librerías de Python:

  • Streamlit para construir la interfaz web interactiva donde los usuarios suben vídeos de gameplay y previsualizan los juegos generados.
  • OpenCV para decodificar el vídeo subido y extraer fotogramas representativos.
  • Ollama para ejecutar el modelo local Qwen 3.5 y realizar razonamiento multimodal y generación de código.
  • python-dotenv para gestionar variables de entorno si hacen falta.
  • Pydantic para imponer un esquema estructurado a la especificación de juego inferida antes de generar código.

A continuación, necesitamos descargar el propio modelo. Ollama lo pone fácil tirando del registro de modelos directamente:

ollama run qwen3.5:9b

La primera ejecución descarga el modelo y lo guarda en local. El qwen3.5:9B ocupa aproximadamente 6,6 GB, admite entradas de texto e imagen y ofrece una ventana de contexto de 256K tokens, lo que lo hace muy adecuado para flujos de trabajo multimodales.

Paso 2: imports

Con el entorno local listo, el siguiente paso es importar las librerías que usaremos en toda la aplicación y crear una carpeta para guardar los artefactos generados. 

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)

El bloque anterior reúne la pila principal del proyecto. Usamos json para formatear la especificación estructurada del juego, base64 para codificar los fotogramas extraídos antes de enviárselos al modelo y tempfile para guardar de forma segura los vídeos subidos durante la sesión. Path de pathlib nos da una forma clara de gestionar rutas, mientras que List de typing se usa para las anotaciones de tipo en las definiciones de esquema.

Después importamos las librerías principales de la aplicación: cv2 de OpenCV se encarga de leer el clip de gameplay y extraer fotogramas representativos; ollama es la interfaz para comunicar con el modelo qwen3.5:9b ejecutándose en local; streamlit aporta la interfaz web, y streamlit.components.v1 nos permite incrustar el juego HTML generado directamente en la app para su previsualización e interacción en vivo.

Por último, importamos BaseModel, Field y ValidationError de Pydantic. Se usan para definir y validar el esquema JSON estructurado que representa el diseño del juego inferido. En vez de confiar en que el modelo devuelva texto poco estructurado, lo forzamos a un esquema predecible antes de pasar ese resultado al generador de juego HTML.

Las dos últimas líneas crean un directorio outputs_local si aún no existe. Aquí guardamos artefactos generados como la especificación inferida y el archivo HTML final del juego, lo que facilita inspeccionar o descargar resultados tras cada ejecución.

En el siguiente paso definiremos las clases de esquema Pydantic que describen la especificación de juego que el modelo debe generar a partir de los fotogramas extraídos.

Paso 3: define el esquema

En esta fase definimos el esquema estructurado que el modelo debe seguir al describir las mecánicas de juego inferidas a partir de los fotogramas. 

En lugar de pedir al modelo que genere directamente código HTML a partir de imágenes, primero le exigimos que produzca una especificación de juego estructurada. Esta representación intermedia hace que la canalización sea mucho más fácil de depurar, validar y ampliar.

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]

Este esquema define la estructura principal de la especificación de juego que el modelo debe generar.

  • La clase Entity representa cualquier objeto presente en el juego, como el jugador, enemigos, obstáculos o elementos de UI. Cada entidad incluye un nombre, su rol dentro del juego y una breve descripción de su comportamiento.
  • La clase Physics describe cómo se mueven e interactúan los objetos en el mundo del juego. Estos campos capturan mecánicas básicas como la gravedad, el impulso/salto, el manejo de colisiones y los patrones de movimiento.
  • La clase VisualStyle recoge la presentación visual del juego: la perspectiva de cámara (por ejemplo, cenital o de desplazamiento lateral), la paleta de colores, el estilo del fondo y los elementos de UI visibles como marcadores de puntuación o indicadores de salud.
  • Por último, la clase GameSpec lo integra todo. Representa el diseño completo del juego inferido a partir de los fotogramas, incluyendo el objetivo, los controles, el bucle de juego, las entidades, el sistema de puntuación, las condiciones de victoria/derrota y las asunciones del modelo al interpretar los fotogramas.

Al validar la salida del modelo contra este esquema, nos aseguramos de que el diseño inferido siga siempre una estructura predecible. Esto también facilita pasar la especificación a la siguiente etapa de la canalización.

Paso 4: prompts para generación en dos etapas

Con el esquema listo, el siguiente paso es definir los prompts que guían al modelo a través de las dos etapas de nuestra canalización. 

La primera etapa se centra en reconstruir las mecánicas de juego a partir de los fotogramas extraídos y producir una especificación estructurada. 

La segunda etapa toma esa especificación y genera un juego jugable para navegador usando HTML, CSS y JavaScript.

Paso 4.1: define los system prompts

El primer componente de nuestra estrategia de prompting son los system prompts, que definen el rol que el modelo debe desempeñar en cada etapa de la canalización.

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

El primer system prompt instruye al modelo para que actúe como analista de gameplay. Hace hincapié en minimizar las suposiciones y le anima a inferir solo las mecánicas que estén claramente respaldadas por los fotogramas. 

Como el modelo solo ve un número pequeño de imágenes muestreadas, el prompt lo sesga explícitamente hacia juegos arcade sencillos que se puedan implementar con lógica ligera en el navegador.

El segundo system prompt cambia el rol del modelo a desarrollador JavaScript. En esta fase, el modelo recibe una especificación estructurada del juego y es responsable de convertirla en un juego HTML completo. 

Paso 4.2: crea el prompt de especificación

Ahora definimos una función auxiliar que construye el prompt de usuario para la primera etapa de la canalización. Este prompt pide al modelo que analice los fotogramas extraídos y produzca una especificación de juego estructurada.

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

Esta función construye el prompt que se enviará al modelo junto con los fotogramas extraídos del vídeo. Incluye varios elementos importantes:

  • Un recordatorio de que el modelo analiza fotogramas fijos y no el vídeo completo
  • Un sesgo hacia mecánicas arcade sencillas
  • La estructura completa del esquema JSON, asegurando que la salida coincida con nuestro modelo GameSpec
  • Opcionalmente, el usuario puede añadir pistas y restricciones para orientar al modelo hacia un estilo de juego concreto

Al incrustar el esquema directamente en el prompt, fomentamos que el modelo produzca una salida que pueda validarse con Pydantic en la siguiente etapa de la canalización.

Paso 4.3: crea el prompt de generación de código

Una vez generada y validada la especificación del juego, necesitamos un segundo prompt para convertirla en un juego realmente jugable.

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 función build_code_user_prompt convierte el diccionario GameSpec a JSON formateado y lo inserta en el prompt. El modelo usa esta descripción estructurada como briefing de diseño para generar el juego HTML final.

Como la especificación ya define mecánicas, entidades, controles y objetivos, la tarea del modelo se simplifica: solo tiene que traducir ese diseño a lógica de juego en JavaScript y a un bucle de renderizado con canvas.

En el siguiente paso implementaremos la extracción de fotogramas con OpenCV, que convierte el vídeo subido en un conjunto reducido de imágenes representativas que el modelo Qwen 3.5 puede analizar.

Paso 5: extrae fotogramas representativos

Los modelos locales Qwen 3.5 de Ollama aceptan texto e imágenes, no vídeo crudo. Así que, en lugar de enviar el clip directamente, extraemos un conjunto pequeño de fotogramas espaciados uniformemente y se los pasamos al modelo. 

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

Estas utilidades definen cinco componentes clave para extraer fotogramas representativos del vídeo subido:

  • Gestión de la subida de vídeo: la función save_uploaded_video() guarda el vídeo de gameplay subido como archivo temporal en disco. Como Streamlit sube los archivos en memoria, escribirlo en disco permite que OpenCV lo lea eficientemente durante la extracción de fotogramas, conservando la extensión original.
  • Muestreo de fotogramas del vídeo: la función extract_frames() carga el vídeo con OpenCV y toma un pequeño número de fotogramas espaciados uniformemente a lo largo de su duración. En lugar de procesar cada fotograma, calcula un intervalo de muestreo y extrae fotogramas representativos que capturan el gameplay. Cada fotograma se codifica como JPEG y se convierte a una cadena base64 para poder enviarlo directamente como entrada de imagen al modelo multimodal Qwen.
  • Extracción de JSON estructurado: la función auxiliar extract_json_block() garantiza que la salida del modelo se pueda analizar correctamente. El contenido extraído se valida después contra el esquema GameSpec.
  • Limpieza de la respuesta HTML: la función cleanup_html_response() elimina formato markdown que el modelo puede incluir al generar código. Si el modelo envuelve la salida HTML en triple backticks, este helper los elimina para que el HTML pueda renderizarse directamente en la previsualización de Streamlit.
  • Interacción por teclado en juegos incrustados: la función patch_html_for_iframe_keyboard() inyecta un pequeño parche JavaScript en el HTML generado para asegurar que los controles de teclado funcionen correctamente dentro del iframe de Streamlit. Enfoca automáticamente el canvas, asigna un tabindex si falta y evita que el navegador intercepte las flechas y la barra espaciadora para usarlas como controles del juego.

En conjunto, estas utilidades forman la capa de preprocesado de vídeo y manejo de respuestas de la canalización.

Paso 6: infiere la especificación del juego 

Con los fotogramas extraídos y las plantillas de prompts listas, podemos implementar la primera etapa de la canalización de razonamiento: convertir los fotogramas de gameplay en una especificación de juego estructurada.

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 función anterior define cuatro etapas clave en la canalización de inferencia de la especificación del juego:

  • Preparación de fotogramas: la llamada a extract_frames() toma un conjunto reducido de fotogramas representativos del vídeo de gameplay subido. Estas imágenes actúan como evidencia visual para que el modelo infiera las mecánicas del juego.
  • Construcción del prompt: la función build_spec_user_prompt() crea dinámicamente el prompt de usuario usando la pista opcional, cualquier restricción extra y el número máximo de fotogramas extraídos. Este prompt da instrucciones claras al modelo para producir una especificación conforme al esquema.
  • Llamada al modelo multimodal: la petición ollama.chat() envía tanto el prompt de texto como los fotogramas al modelo local qwen3.5:9b. La configuración usa una temperature baja para mantener la salida determinista y num_ctx=8192 para disponer de suficiente contexto para el prompt y la respuesta estructurada.
  • Validación del esquema: cuando el modelo responde, la función extrae el bloque JSON, lo analiza y lo valida contra el esquema Pydantic GameSpec. Por último, devuelve la especificación validada como diccionario de Python.

Ya tenemos una representación estructurada del juego que el modelo ha inferido a partir de los fotogramas. En el siguiente paso usaremos esta especificación validada como entrada para la generación del juego HTML.

Paso 7: genera el juego en HTML

En este punto, el modelo ya no tiene que inferir mecánicas a partir de imágenes. En su lugar, pedimos a qwen3.5:9b que produzca un único archivo HTML autocontenido que incluya maquetación, estilos, bucle del juego, controles y lógica de renderizado.

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 función anterior realiza cuatro pasos para generar y preparar el juego HTML final:

  • Entrega estructurada del prompt: la función toma el diccionario game_spec validado y lo pasa a build_code_user_prompt(). Esto convierte la especificación del juego en un prompt de implementación detallado que instruye al modelo para generar un juego completo en un único archivo HTML.
  • Generación de código con Qwen: la llamada ollama.chat() envía el system prompt de generación de código y el prompt de usuario al modelo local qwen3.5:9b. Como la tarea ahora es solo generación de código, no de razonamiento multimodal, el modelo solo recibe texto. 
  • Limpieza y validación de HTML: la respuesta bruta del modelo pasa por cleanup_html_response() para eliminar cualquier markdown o formato adicional. Luego comprobamos de forma sencilla que la salida contenga un documento HTML.
  • Parche para teclado en iframe: el HTML final pasa por patch_html_for_iframe_keyboard(), que inyecta un pequeño ajuste JavaScript para que el juego sea jugable dentro del iframe incrustado de Streamlit. Este paso es importante porque los juegos en navegador a menudo no capturan correctamente las flechas y la barra espaciadora si el canvas no es enfocables y no se enfoca automáticamente.

Ya tenemos un juego HTML jugable en el navegador generado íntegramente a partir de la especificación inferida.

Paso 8: interfaz en Streamlit

Como último paso, vamos a envolverlo todo en una interfaz sencilla de Streamlit para que cualquiera pueda probar el sistema sin tocar el código. La interfaz expone tres capacidades principales:

  • Subir un vídeo de gameplay
  • Configurar parámetros de generación como pistas y muestreo de fotogramas
  • Previsualizar y descargar el juego generado
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",
        )

El código anterior define cómo el usuario sube un clip de gameplay, aporta opcionalmente una pista sobre el tipo de juego y controla cuántos fotogramas deben extraerse del vídeo para su análisis. Una vez subido el vídeo, la app lo muestra junto a un botón Generate game que lanza toda la canalización.

Cuando el usuario ejecuta la canalización, la app primero extrae fotogramas e infiere las mecánicas con el modelo local qwen3.5:9b, produciendo una especificación de juego estructurada. En la segunda etapa, el modelo genera a partir de esa especificación un juego HTML completo. Los resultados se guardan en el estado de sesión de Streamlit para que persistan entre actualizaciones de la UI.

Cuando termina la generación, la interfaz muestra la especificación inferida, una previsualización jugable del juego generado, el HTML en bruto y botones para descargar tanto el juego HTML como la especificación JSON.

Para probarlo tú mismo, guarda el código como app.py y lanza:

streamlit run app.py

Demo

Conclusión

Qwen 3.5 es una de las familias de modelos multimodales más interesantes ahora mismo. En este tutorial, usamos qwen3.5:9b a través de Ollama para crear un generador totalmente local que convierte un vídeo de gameplay en un juego. La app extrae fotogramas de un clip corto, infiere un diseño de juego estructurado, genera un juego en un único archivo y lo previsualiza en Streamlit.

El modelo alojado qwen/qwen3.5-397b-a17b sigue siendo la opción más rápida y potente para esta tarea, pero el resultado con el 9B local es muy meritorio. Demuestra que un modelo multimodal pequeño ya puede hacer razonamiento visual y generación de código útiles en hardware de consumo.

Si quieres ir más allá, compara 9B con los modelos 4B y 27B, añade un bucle de refinamiento o convierte la salida en un mini motor que admita múltiples iteraciones a partir del mismo clip.

Qwen3.5 Small: preguntas frecuentes

¿Puedo ejecutar esta demo con `qwen3.5:4b` en lugar de 9B?

Sí, pero los resultados suelen ser menos fiables. El modelo 4B sigue siendo útil para experimentos locales ligeros, pero la variante 9B ofrece un mejor equilibrio entre velocidad y calidad. Ollama expone ambos como modelos locales de texto e imagen.

¿Por qué no usar `qwen3.5:0.8b` o 2B?

Los modelos 2B se adaptan mejor a tareas multimodales más simples o a despliegues de tipo edge que a esta canalización completa.

¿Cuál es el mejor modelo local de Qwen 3.5 para este caso de uso?

El qwen3.5:9b es el mejor punto de partida. Sigue siendo compacto con 6,6 GB en Ollama, admite entrada de texto e imagen y rinde mucho mejor que las variantes muy pequeñas en este tipo de tareas de generación con fuerte componente de razonamiento.

¿Puedo usar también los modelos Qwen 3.5 más grandes en local?

Sí. Ollama también lista variantes 27b, 35b y 122b, todas con soporte de texto e imagen. El compromiso está en memoria y latencia.


Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

Soy experta Google Developers en ML (Gen AI), triple experta en Kaggle y embajadora de Women Techmakers, con más de tres años de experiencia en el sector tecnológico. Cofundé una startup de salud en 2020 y actualmente curso un máster en informática en Georgia Tech, con especialización en aprendizaje automático.

Temas
Inteligencia Artificial
Grandes modelos lingüísticos

Top DataCamp Courses

Curso

Desarrollo de software con Cursor

1 h 30 min
4.2K
Crea código listo para producción con Cursor. Aprende sobre indicaciones de IA, refactorización, pruebas y flujos de trabajo avanzados.
Ver detallesRight Arrow
Iniciar Curso
Ver másRight Arrow
Relacionado
An AI juggles tasks

blog

Cinco proyectos que puedes crear con modelos de IA generativa (con ejemplos)

Aprende a utilizar modelos de IA generativa para crear un editor de imágenes, un chatbot similar a ChatGPT con pocos recursos y una aplicación clasificadora de aprobación de préstamos y a automatizar interacciones PDF y un asistente de voz con GPT.
Abid Ali Awan's photo

Abid Ali Awan

10 min

Tutorial

RAG Con Llama 3.1 8B, Ollama y Langchain: Tutorial

Aprende a crear una aplicación RAG con Llama 3.1 8B utilizando Ollama y Langchain, configurando el entorno, procesando documentos, creando incrustaciones e integrando un recuperador.
Ryan Ong's photo

Ryan Ong

12 min

Tutorial

Tutorial FLAN-T5: Guía y puesta a punto

Una guía completa para afinar un modelo FLAN-T5 para una tarea de respuesta a preguntas utilizando la biblioteca de transformadores, y ejecutando la inferencia optmizada en un escenario del mundo real.
Zoumana Keita 's photo

Zoumana Keita

15 min

Tutorial

Ajuste fino de LLaMA 2: Guía paso a paso para personalizar el modelo de lenguaje grande

Aprende a ajustar Llama-2 en Colab utilizando nuevas técnicas para superar las limitaciones de memoria y computación y hacer más accesibles los grandes modelos lingüísticos de código abierto.
Abid Ali Awan's photo

Abid Ali Awan

12 min

Tutorial

Introducción a Q-learning: tutorial para principiantes

Conoce el algoritmo de aprendizaje por refuerzo sin modelo más popular con un tutorial en Python.
Abid Ali Awan's photo

Abid Ali Awan

11 min

Tutorial

Guía para principiantes de LlaMA-Factory WebUI: Ajuste de los LLM

Aprende a afinar los LLM en conjuntos de datos personalizados, evaluar el rendimiento y exportar y servir modelos sin problemas utilizando el marco de trabajo de bajo/ningún código de LLaMA-Factory.
Abid Ali Awan's photo

Abid Ali Awan

12 min

Ver MásVer Más