跳至主要内容

Qwen 3.5 小型模型教程:使用 Ollama 构建“视频生成游戏”应用

学习如何通过 Ollama 在本地使用 Qwen 3.5 小型模型,将一段简短的游戏实况视频转换为可玩的 HTML 游戏。
· 14分钟 阅读

使用 AI 探索

Qwen 3.5 是一组开源多模态模型,能够同时处理文本与视觉输入。该系列采用图像-文本到文本的架构,使模型可以在自然语言提示的同时,对图像或视频帧进行推理。

在本教程中,我们将构建一个 Streamlit 应用,使用本地 Qwen3.5 小型 qwen3.5:9b 模型(通过Ollama 托管)将一段游戏玩法视频转换为可在浏览器中运行的游戏。应用会从上传的视频中提取帧,推断游戏机制,将其转化为结构化规格说明,然后生成完整的 HTML/CSS/JavaScript 游戏,并可直接在界面内预览。

在此过程中,您将学到:

  • 从视频中提取具有代表性的帧以支持多模态推理
  • 使用 Ollama 在本地运行 Qwen 3.5 进行图像-文本推理
  • 设计一个用于推理与代码生成的双阶段提示管线
  • 将视觉观察到的玩法转化为结构化的游戏规格
  • 从推断出的机制生成可玩的 HTML5 浏览器游戏
  • 构建一个交互式 Streamlit 界面来运行并预览整个流程

完成后,您将拥有一个可用的演示:将一段简短的游戏片段转换为在本地机器上完全生成、可游玩的浏览器游戏。下面的视频展示了该流程的简化版本:

什么是 Qwen 3.5 Small?

Qwen 3.5 是一个多模态模型家族,覆盖从轻量级边缘模型到前沿规模推理系统的广泛部署场景。

官方 Qwen 3.5 产品线目前包括 0.8B、2B、4B、9B、27B、35B-A3B、122B-A10B 和 397B-A17B 等变体,其中部分模型同时提供基础版和量化检查点。

选择合适的小型模型取决于您要构建的工作流类型。

  • 0.8B 与 2B 变体最适合边缘设备、快速原型开发以及受限硬件环境。
  • 4B 型号更进一步,适用于小型本地智能体或轻量级视觉推理任务。
  • 9B 型号更适合本地多模态应用:既足够紧凑,便于本地运行,又足够强大,能够推断玩法机制并生成可用的 HTML 游戏代码。

在本教程中,我们使用了 qwen3.5:9b 模型,它在能力、本地可部署性与多模态推理之间取得了良好平衡,非常适合在不依赖外部 API 的情况下,构建“视频帧到浏览器游戏”的管线。

您也可以阅读我们的指南:如何在单块 GPU 上本地运行 Qwen3.5。

为何为此选择本地小型模型?

我也用 qwen/qwen3.5-397b-a17b 模型测试了该演示。尽管该模型远大于本地 9B 变体且总体更强,但它需要基于 API 的推理,这会引入外部依赖与潜在时延。

在实际测试中,如果对运行时长不做严格限制,两者的整体生成时间大致相差约 3 分钟。更大的模型确实能生成略为流畅的玩法逻辑与更精致的预览体验,但提升并不显著。

考虑到 9B 模型可完全本地运行,其效果相当有竞争力。尽管体量小得多,它依然能够生成可玩的浏览器游戏,质量仅较更大的 397B 模型略有下降。

想进一步了解如何最大化利用 0.8B 变体,可阅读我们的完整指南:微调 Qwen3.5 Small。

Qwen 3.5 示例项目:构建“视频生成游戏”器

本节我们将构建一个 Streamlit 应用,它将:

  • 接收一段简短的游戏实况视频
  • 提取少量具有代表性的帧
  • 通过 Ollama 将这些帧发送给 qwen3.5:9b
  • 让模型推断结构化的游戏规格
  • 让模型生成单文件 HTML 游戏
  • 在 Streamlit 应用内预览生成的游戏

该设置凸显了 Qwen 的多模态能力,可支持从游戏画面逆向推导玩法逻辑并将其转化为代码等任务。

步骤 1:安装依赖

在构建应用之前,我们需要先准备本地环境,安装用于视频处理、多模态推理与交互界面的必要库。

pip install streamlit opencv-python ollama python-dotenv pydantic

该演示使用了一小组 Python 库:

  • Streamlit:构建交互式 Web 界面,用户可上传游戏视频并预览生成的游戏。
  • OpenCV:解码上传的视频并提取具有代表性的帧。
  • Ollama:运行本地 Qwen 3.5 模型,用于多模态推理与代码生成。
  • python-dotenv:在需要时管理环境变量。
  • Pydantic:在生成代码前,对推断出的游戏规格应用结构化模式约束并进行校验。

接下来需要下载模型本身。Ollama 可直接从其模型仓库拉取模型,流程非常简洁:

ollama run qwen3.5:9b

首次运行会下载模型并将其存储在本地。qwen3.5:9B 约 6.6GB,支持文本与图像输入,并提供 256K token 的上下文窗口,十分适合多模态工作流。

步骤 2:导入依赖

本地环境就绪后,下一步是在应用中导入将要用到的库,并创建一个用于存放生成产物的文件夹。

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)

上述代码汇集了项目的核心技术栈。我们使用 json 来格式化结构化游戏规格,使用 base64 在将提取的视频帧发送给模型前进行编码,并用 tempfile 在会话期间安全地保存上传视频。pathlib 中的 Path 提供了更清晰的路径管理方式,而 typing 中的 List 则用于模式定义中的类型提示。

随后导入应用的主要库,例如来自 OpenCV 的 cv2 用于读取上传的游戏片段并提取代表性帧,ollama 用于与本地运行的 qwen3.5:9b 模型通信。streamlit 提供 Web 界面,而 streamlit.components.v1 允许我们将生成的 HTML 游戏直接嵌入应用中进行实时预览与交互。

最后,我们从 Pydantic 导入 BaseModel、Field 与 ValidationError,用于定义并校验表示推断游戏设计的结构化 JSON 模式。与其相信模型输出松散格式的文本,我们更倾向在传入 HTML 游戏生成器前,将其约束为可预测的模式。

末尾两行代码会在不存在时创建 outputs_local 目录。我们会将推断出的游戏规格与最终 HTML 游戏文件等生成产物保存在该文件夹中,便于在每次运行后检查或下载结果。

下一步,我们将定义描述游戏规格的 Pydantic 模式类,模型必须基于提取的游戏帧按此模式生成结果。

步骤 3:定义模式

在这个阶段,我们定义模型在描述从视频帧中推断出的玩法机制时必须遵循的结构化模式。

我们并非直接要求模型从图像生成 HTML 代码,而是先让它产出结构化的游戏规格。这一中间表示使管线更易调试、验证和扩展。

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]

该模式定义了模型必须生成的游戏规格的核心结构。

  • Entity 类表示游戏中的任何对象,例如玩家、敌人、障碍或 UI 元素。每个实体包含名称、其在游戏中的角色,以及其行为的简要描述。
  • Physics 类描述对象在游戏世界中的运动与交互。这些字段涵盖了重力、冲量/跳跃、碰撞处理与运动模式等基础机制。
  • VisualStyle 类描述游戏的视觉呈现,包括视角(如自上而下或横向卷轴)、配色、背景风格以及可见的 UI 元素(如分数或生命值显示)。
  • 最后,GameSpec 类将一切整合,表示从视频帧推断出的完整游戏设计,包括目标、操作方式、玩法循环、实体、计分系统、胜负条件,以及模型在解读画面时做出的假设。

通过将模型输出与该模式进行校验,我们可确保推断设计始终遵循可预测的结构。这样也更便于将规格传递至管线的下一阶段。

步骤 4:双阶段生成的提示

模式已就位,下一步是定义提示,指导模型完成我们管线中的两个阶段。

第一阶段专注于从提取的帧中逆向还原玩法机制,并产出结构化的游戏规格。

第二阶段基于该规格,用 HTML、CSS 与 JavaScript 生成可玩的浏览器游戏。

步骤 4.1:定义系统提示

我们的提示策略首先是系统提示,它定义了模型在管线各阶段应扮演的角色。

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

第一个系统提示将模型设定为玩法分析师,强调尽量减少假设,只推断由画面强力支撑的机制。

由于模型仅能看到少量采样图像,提示会将其明确引导至易于用轻量浏览器逻辑实现的街机类简单玩法。

第二个系统提示将模型的角色切换为 JavaScript 开发者。在此阶段,模型接收结构化的游戏规格,并负责将其转换为完整的 HTML 游戏。

步骤 4.2:构建游戏规格提示

接下来定义一个辅助函数,用于构造第一阶段的用户提示。该提示要求模型分析提取的帧并生成结构化游戏规格。

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

该函数构建的提示会与提取的视频帧一同发送给模型,包含以下要点:

  • 提醒模型正在分析静态帧而非完整视频
  • 引导偏向简单的街机机制
  • 嵌入完整的 JSON 模式结构,确保输出与我们的 GameSpec 模型匹配
  • 用户可选地提供提示与约束,引导模型朝特定风格靠拢

通过将模式直接嵌入提示,我们鼓励模型生成可被 Pydantic 在下一阶段验证的输出。

步骤 4.3:构建代码生成提示

一旦游戏规格生成并通过验证,我们需要第二个提示,将该规格转换为真正可玩的游戏。

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

build_code_user_prompt 函数将 GameSpec 字典转为格式化 JSON,并插入到提示中。模型据此结构化描述作为设计说明书,生成最终的 HTML 游戏。

由于规格已定义了机制、实体、操作与目标,模型的任务大幅简化,仅需将该设计转化为 JavaScript 游戏逻辑与基于 canvas 的渲染循环。

下一步我们将使用 OpenCV 实现帧提取管线,把上传的游戏视频转换为一小组具有代表性的图像,供 Qwen 3.5 模型分析。

步骤 5:提取具有代表性的帧

Ollama 的 Qwen 3.5 本地模型接受文本与图像,而非原始视频。因此我们不直接发送上传的视频,而是提取一小组等间隔的帧,将其传给模型。

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

上述工具函数实现了从上传视频中提取代表性帧的五个关键环节:

  • 视频上传处理:save_uploaded_video() 将上传的游戏视频保存为磁盘上的临时文件。由于 Streamlit 在内存中上传文件,将其写入磁盘便于 OpenCV 在提帧时高效读取,同时保留原始文件扩展名。
  • 视频帧采样:extract_frames() 使用 OpenCV 加载视频,并在其时长上均匀采样少量帧。函数计算采样间隔,提取能够代表玩法的帧。每帧以 JPEG 编码,并转换为 base64 字符串,以便作为图像输入直接发送给多模态 Qwen 模型。
  • 结构化 JSON 提取:extract_json_block() 帮助函数确保模型输出可被正确解析。随后可将提取内容安全地与 GameSpec 模式进行验证。
  • HTML 响应清理:cleanup_html_response() 移除模型在生成代码时可能包含的 markdown 格式。如果模型用三反引号包裹 HTML,该函数会去除这些标记,便于在 Streamlit 预览中直接渲染。
  • 嵌入式游戏的键盘交互:patch_html_for_iframe_keyboard() 会向生成的 HTML 注入一段小型 JavaScript 补丁,确保键盘控制在 Streamlit 的 iframe 中正常工作。它会自动聚焦 canvas 元素、在缺失时添加 tabindex,并阻止浏览器拦截方向键与空格键,以便用于游戏操作。

这些工具共同构成了管线中的视频预处理与响应处理层。

步骤 6:推断游戏规格

现在我们已经准备好了提取出的帧与提示模板,可以实现推理管线的第一阶段——将玩法帧转化为结构化游戏规格。

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

上述函数定义了游戏规格推断管线中的四个关键阶段:

  • 帧准备:调用 extract_frames() 从上传的视频中采样少量代表性帧。这些图像将作为模型推断玩法机制的视觉证据。
  • 提示构建:build_spec_user_prompt() 使用可选提示、附加约束与最大帧数动态创建用户提示,明确要求模型生成符合模式的游戏规格。
  • 多模态模型调用:ollama.chat() 将文本提示与提取的帧一起发送给本地 qwen3.5:9b 模型。生成设置使用较低的 temperature 以保持输出的确定性,并将 num_ctx 设为 8192,为提示与结构化响应留出足够的上下文空间。
  • 模式验证:模型响应返回后,函数会提取 JSON 块、解析并基于 GameSpec 的 Pydantic 模式进行验证。最后将验证通过的规格以 Python 字典返回。

至此,我们已获得基于采样视频帧由模型推断的结构化游戏表示。下一步将以该验证后的规格作为输入,进入 HTML 游戏生成阶段。

步骤 7:生成 HTML 游戏

此时模型不再需要从图像中推断机制。我们改为请求 qwen3.5:9b 生成一个自包含的 HTML 文件,其中包括布局、样式、游戏循环、控制与渲染逻辑。

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)

上述函数通过以下四步生成并准备最终的 HTML 游戏:

  • 结构化提示传递:将已验证的 game_spec 字典传入 build_code_user_prompt(),把游戏规格转换为详细的实现提示,指示模型生成完整的单文件 HTML 游戏。
  • 使用 Qwen 进行代码生成:ollama.chat() 将代码生成的系统提示与用户提示一并发送给本地 qwen3.5:9b 模型。由于当前任务是纯代码生成而非多模态推理,模型仅接收文本。
  • HTML 清理与校验:将模型的原始响应传入 cleanup_html_response(),去除任何 markdown 或额外格式。随后进行简单校验,确认输出包含 HTML 文档。
  • iframe 键盘补丁:将最终 HTML 交给 patch_html_for_iframe_keyboard(),注入小型 JavaScript 修复,使游戏在 Streamlit 的嵌入式 iframe 中可正常游玩。此步骤很重要,因为浏览器游戏若未显式设置 canvas 的可聚焦与自动聚焦,往往无法正确捕获方向键与空格键。

至此,我们获得了一个完全基于推断游戏规格生成、可在浏览器中游玩的 HTML 游戏。

步骤 8:Streamlit 界面

最后一步,我们将所有内容封装进一个简洁的 Streamlit 界面,让用户无需接触底层代码即可体验系统。界面提供三项主要功能:

  • 上传游戏视频
  • 配置生成参数(如提示与提帧数量)
  • 预览与下载生成的游戏
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",
        )

上述代码定义了用户如何上传游戏片段、可选地提供游戏类型提示,并控制为分析而提取的帧数。一旦视频上传,应用会在“生成游戏”按钮旁显示该视频,点击即可运行全流程。

当用户运行管线时,应用首先使用本地 qwen3.5:9b 模型提取帧并推断玩法机制,生成结构化游戏规格。第二阶段,模型依据该规格生成完整的 HTML 游戏。结果存放于 Streamlit 的会话状态,以便在界面更新后仍然保留。

生成完成后,界面会展示推断出的游戏规格、可实时游玩的游戏预览、原始 HTML 源码,以及用于下载 HTML 游戏与 JSON 规格的按钮。

要亲自试用,请将代码保存为 app.py 并运行:

streamlit run app.py

Demo

总结

Qwen 3.5 是当下最有趣的多模态模型家族之一。在本教程中,我们通过 Ollama 使用 qwen3.5:9b 构建了一个完全本地的“游戏视频转游戏”生成器。应用会从短片中提帧、推断结构化游戏设计、生成单文件浏览器游戏,并在 Streamlit 中进行预览。

托管的 qwen/qwen3.5-397b-a17b 模型在该任务上仍然更快、更强,但本地 9B 的表现也相当令人印象深刻。这证明在消费级硬件上,小型多模态模型已能完成有意义的视觉推理与代码生成。

如果您想进一步拓展,不妨比较 9B、4B 与 27B 模型,加入迭代优化环节,或将输出构造成一个能从同一视频支持多次迭代的迷你游戏引擎。

Qwen3.5 小型模型常见问答

Can I run this demo with `qwen3.5:4b` instead of 9B?

可以,但结果通常不够稳定。4B 模型仍适合轻量本地实验,而 9B 变体在速度与质量之间更均衡。Ollama 将两者都作为本地“图文多模态”模型提供。

Why not use `qwen3.5:0.8b` or 2B?

2B 模型更适合更简单的多模态任务或边缘部署场景,而非本教程这样的完整管线。

What is the best local Qwen 3.5 model for this use case?

qwen3.5:9b 是最佳起点。它在 Ollama 中仅约 6.6GB,支持图文输入,且在这种偏重推理的生成任务中,表现远优于更小的变体。

Can I use the larger Qwen 3.5 models locally, too?

可以。Ollama 目前也提供 27b、35b 与 122b 变体,均支持图文输入。代价在于内存占用与时延。

Aashi Dutt

作者

Aashi Dutt

我是一名 Google Developers 机器学习(生成式 AI)领域的专家、Kaggle 三项专家,以及 Women Techmakers 大使,拥有 3 年以上的技术从业经验。2020 年我共同创办了一家健康科技初创公司,目前在佐治亚理工学院攻读计算机科学硕士,专攻机器学习。

主题