Qwen 3.5 Small là gì?
Qwen 3.5 là họ mô hình đa phương thức được thiết kế để bao phủ nhiều kịch bản triển khai, từ các mô hình gọn nhẹ trên thiết bị biên đến các hệ thống suy luận quy mô tiên tiến.
Danh mục chính thức Qwen 3.5 hiện gồm các biến thể 0.8B, 2B, 4B, 9B, 27B, 35B-A3B, 122B-A10B và 397B-A17B, cùng các checkpoint base và lượng tử hóa cho một số mô hình.
Việc chọn mô hình nhỏ phù hợp phụ thuộc vào loại quy trình làm việc bạn muốn xây dựng.
- Các biến thể 0.8B và 2B phù hợp nhất cho thiết bị biên, thử nghiệm nhanh và môi trường phần cứng hạn chế.
- Mô hình 4B là bước nâng cấp và hoạt động tốt cho các agent cục bộ nhỏ hoặc tác vụ suy luận thị giác nhẹ.
- Mô hình 9B phù hợp hơn cho ứng dụng đa phương thức cục bộ vì vẫn đủ gọn để chạy tại chỗ nhưng đủ mạnh để suy luận cơ chế gameplay và tạo mã game HTML dùng được.
Trong hướng dẫn này, chúng tôi dùng mô hình qwen3.5:9b, mang lại cân bằng tốt giữa năng lực, khả năng triển khai cục bộ và suy luận đa phương thức, rất thích hợp để xây pipeline từ khung hình video đến game trên trình duyệt mà không cần API bên ngoài.
Bạn cũng có thể đọc hướng dẫn của chúng tôi về cách chạy Qwen3.5 cục bộ trên một GPU.
Vì sao dùng mô hình nhỏ cục bộ cho bài toán này?
Tôi cũng đã thử demo này với mô hình qwen/qwen3.5-397b-a17b. Dù mô hình này lớn hơn đáng kể và nói chung mạnh hơn biến thể 9B cục bộ, nó yêu cầu suy luận qua API, kéo theo phụ thuộc bên ngoài và độ trễ tiềm ẩn.
Trong thực tế, nếu thời gian chạy không quá khắt khe, tổng thời gian tạo khác nhau khoảng ~3 phút giữa hai mô hình trong các thử nghiệm của tôi. Mô hình lớn cho ra logic gameplay mượt hơn đôi chút và trải nghiệm xem trước trau chuốt hơn, nhưng mức cải thiện không quá lớn.
Xét rằng mô hình 9B chạy hoàn toàn cục bộ, kết quả là rất cạnh tranh. Dù nhỏ hơn nhiều, nó vẫn có thể tạo ra trò chơi trình duyệt có thể chơi được với mức giảm chất lượng chỉ nhỏ so với mô hình 397B lớn hơn rất nhiều.
Bạn có thể đọc toàn bộ hướng dẫn về fine-tuning Qwen3.5 small để tìm hiểu thêm cách khai thác tối đa biến thể 0.8B.
Dự án mẫu Qwen 3.5: Xây bộ tạo game từ video
Trong phần này, chúng ta sẽ xây dựng một ứng dụng Streamlit:
- nhận vào một đoạn clip gameplay ngắn
- trích xuất một số khung hình đại diện
- gửi các khung hình đó đến
qwen3.5:9bqua Ollama - yêu cầu mô hình suy luận đặc tả game có cấu trúc
- yêu cầu mô hình tạo một trò chơi HTML một tệp
- xem trước trò chơi được tạo trong ứng dụng Streamlit
Thiết lập này làm nổi bật năng lực đa phương thức của Qwen, hỗ trợ các tác vụ như đảo ngược logic từ cảnh quay gameplay và biến nó thành mã
Bước 1: Cài đặt phụ thuộc
Trước khi xây ứng dụng, chúng ta cần chuẩn bị môi trường cục bộ với các thư viện cần cho xử lý video, suy luận đa phương thức và giao diện tương tác.
pip install streamlit opencv-python ollama python-dotenv pydantic
Bản demo dùng một tập nhỏ thư viện Python:
Streamlitđể xây giao diện web tương tác nơi người dùng tải video gameplay và xem trước game được tạo.OpenCVđể giải mã video tải lên và trích xuất khung hình đại diện.Ollamađể chạy mô hình Qwen 3.5 cục bộ cho suy luận đa phương thức và sinh mã.python-dotenvđể quản lý biến môi trường khi cần.Pydanticđể áp đặt lược đồ có cấu trúc cho đặc tả game suy luận trước khi sinh mã.
Tiếp theo, chúng ta cần tải chính mô hình. Ollama giúp việc này trở nên đơn giản bằng cách tải trực tiếp từ registry của họ:
ollama run qwen3.5:9b
Lần chạy đầu sẽ tải mô hình và lưu cục bộ. qwen3.5:9B khoảng 6,6GB, hỗ trợ đầu vào văn bản và hình ảnh, và cung cấp cửa sổ ngữ cảnh 256K token, rất phù hợp cho quy trình làm việc đa phương thức.
Bước 2: Import
Khi môi trường cục bộ đã sẵn sàng, bước tiếp theo là import các thư viện dùng xuyên suốt ứng dụng và tạo thư mục lưu hiện vật được sinh ra.
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)
Khối trên tập hợp ngăn xếp lõi cho dự án. Chúng ta dùng json để định dạng đặc tả game có cấu trúc, base64 để mã hóa các khung hình video trích xuất trước khi gửi tới mô hình, và tempfile để lưu an toàn video tải lên trong phiên. Path từ pathlib giúp quản lý đường dẫn tệp gọn gàng, còn List từ typing dùng cho gợi ý kiểu trong định nghĩa lược đồ.
Tiếp đó, chúng ta import các thư viện ứng dụng chính như cv2 từ OpenCV chịu trách nhiệm đọc clip gameplay tải lên và trích xuất khung hình đại diện, ollama là giao diện để giao tiếp với mô hình qwen3.5:9b chạy cục bộ. streamlit cung cấp giao diện web, và streamlit.components.v1 cho phép nhúng trò chơi HTML được tạo trực tiếp vào ứng dụng để xem trước và tương tác trực tiếp.
Cuối cùng, chúng ta import BaseModel, Field và ValidationError từ Pydantic. Chúng được dùng để định nghĩa và kiểm định lược đồ JSON có cấu trúc biểu diễn thiết kế game suy luận được. Thay vì tin tưởng mô hình xuất ra văn bản định dạng lỏng lẻo, chúng ta buộc nó theo một lược đồ có thể dự đoán trước khi chuyển kết quả đó vào bộ sinh game HTML.
Hai dòng cuối tạo thư mục outputs_local nếu chưa tồn tại. Thư mục này là nơi lưu các hiện vật được tạo, như đặc tả game suy luận và tệp trò chơi HTML cuối cùng, giúp dễ kiểm tra hoặc tải xuống sau mỗi lần chạy.
Ở bước tiếp theo, chúng ta sẽ định nghĩa các lớp lược đồ Pydantic mô tả đặc tả game mà mô hình phải tạo từ các khung hình gameplay được trích xuất.
Bước 3: Định nghĩa lược đồ
Ở giai đoạn này, chúng ta định nghĩa lược đồ có cấu trúc mà mô hình phải tuân theo khi mô tả cơ chế gameplay suy luận từ các khung hình video.
Thay vì yêu cầu mô hình tạo trực tiếp mã HTML từ ảnh, trước hết chúng ta yêu cầu nó tạo đặc tả game có cấu trúc. Biểu diễn trung gian này giúp pipeline dễ gỡ lỗi, kiểm định và mở rộng hơn nhiều.
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]
Lược đồ này định nghĩa cấu trúc nòng cốt của đặc tả game mà mô hình phải tạo.
- Lớp
Entityđại diện cho mọi đối tượng trong game như người chơi, kẻ địch, chướng ngại vật hoặc phần tử UI. Mỗi entity gồm tên, vai trò trong game và mô tả ngắn về hành vi. - Lớp
Physicsmô tả cách các đối tượng chuyển động và tương tác trong thế giới game. Các trường này nêu các cơ chế cơ bản như trọng lực, xung/lực nhảy, xử lý va chạm và kiểu chuyển động. - Lớp
VisualStyleghi lại cách trình bày thị giác của game. Bao gồm góc nhìn (như top-down hoặc cuộn ngang), bảng màu, phong cách nền và mọi phần tử UI hiển thị như điểm số hoặc thanh máu. - Cuối cùng, lớp
GameSpectổng hợp tất cả. Nó biểu diễn thiết kế game đầy đủ suy luận từ các khung hình, gồm mục tiêu, điều khiển, vòng lặp gameplay, entity, hệ thống tính điểm, điều kiện thắng/thua và các giả định mà mô hình đưa ra khi diễn giải các khung hình.
Bằng cách kiểm định đầu ra của mô hình theo lược đồ này, chúng ta đảm bảo thiết kế suy luận luôn theo cấu trúc có thể dự đoán. Điều này cũng giúp dễ dàng chuyển đặc tả sang bước tiếp theo của pipeline.
Bước 4: Prompt cho sinh hai giai đoạn
Khi đã có lược đồ, bước tiếp theo là định nghĩa các prompt dẫn dắt mô hình qua hai giai đoạn của pipeline.
Giai đoạn đầu tập trung đảo ngược kỹ thuật (reverse-engineering) cơ chế gameplay từ các khung hình trích xuất và tạo đặc tả game có cấu trúc.
Giai đoạn hai lấy đặc tả đó và tạo trò chơi trên trình duyệt có thể chơi bằng HTML, CSS và JavaScript.
Bước 4.1: Định nghĩa system prompt
Thành phần đầu tiên trong chiến lược nhắc lệnh là các system prompt, định nghĩa vai trò mà mô hình nên đảm nhiệm trong từng giai đoạn của 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()
System prompt đầu tiên hướng dẫn mô hình hành xử như một nhà phân tích gameplay. Nó nhấn mạnh việc tối thiểu giả định và khuyến khích mô hình chỉ suy luận những cơ chế được hỗ trợ mạnh bởi các khung hình.
Vì mô hình chỉ nhìn thấy một số ít ảnh mẫu, prompt chủ ý thiên về các game arcade đơn giản có thể triển khai bằng logic trình duyệt gọn nhẹ.
System prompt thứ hai chuyển vai trò của mô hình thành nhà phát triển JavaScript. Ở giai đoạn này, mô hình nhận đặc tả game có cấu trúc và chịu trách nhiệm chuyển nó thành trò chơi HTML hoàn chỉnh.
Bước 4.2: Xây prompt cho đặc tả game
Tiếp theo, chúng ta định nghĩa một hàm tiện ích xây dựng user prompt cho giai đoạn đầu của pipeline. Prompt này yêu cầu mô hình phân tích các khung hình đã trích xuất và tạo đặc tả game có cấu trúc.
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()
Hàm này tạo prompt sẽ gửi tới mô hình cùng với các khung hình trích xuất. Nó bao gồm vài yếu tố quan trọng:
- Lời nhắc rằng mô hình đang phân tích ảnh tĩnh chứ không phải toàn bộ video
- Thiên hướng về cơ chế arcade đơn giản
- Cấu trúc lược đồ JSON đầy đủ, đảm bảo đầu ra khớp với mô hình
GameSpeccủa chúng ta - Tùy chọn cho phép người dùng thêm gợi ý và ràng buộc để giao diện định hướng mô hình tới phong cách game nhất định
Bằng cách nhúng trực tiếp lược đồ vào prompt, chúng ta khuyến khích mô hình tạo đầu ra có thể được Pydantic kiểm định ở bước kế tiếp của pipeline.
Bước 4.3: Xây prompt sinh mã
Khi đặc tả game đã được tạo và kiểm định, chúng ta cần prompt thứ hai để chuyển đặc tả đó thành trò chơi có thể chơi thực sự.
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()
Hàm build_code_user_prompt chuyển đổi từ điển GameSpec thành JSON định dạng và chèn vào prompt. Mô hình sau đó dùng mô tả có cấu trúc này như bản tóm tắt thiết kế để tạo trò chơi HTML cuối cùng.
Vì đặc tả đã định nghĩa sẵn cơ chế, entity, điều khiển và mục tiêu, nhiệm vụ của mô hình trở nên đơn giản hơn nhiều. Nó chỉ cần dịch thiết kế đó thành logic game JavaScript và vòng lặp dựng hình dựa trên canvas.
Ở bước tiếp theo, chúng ta sẽ triển khai pipeline trích xuất khung hình bằng OpenCV, chuyển video gameplay tải lên thành tập nhỏ ảnh đại diện có thể được mô hình Qwen 3.5 phân tích.
Bước 5: Trích xuất khung hình đại diện
Các mô hình Qwen 3.5 cục bộ của Ollama chấp nhận văn bản và ảnh, không phải video thô. Vì vậy thay vì gửi trực tiếp clip tải lên, chúng ta trích xuất một tập nhỏ khung hình cách đều và gửi chúng tới mô hình.
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
Các tiện ích trên định nghĩa năm thành phần then chốt để trích xuất khung hình đại diện từ video tải lên:
- Xử lý tải video: Hàm
save_uploaded_video()lưu video gameplay tải lên thành tệp tạm trên đĩa. Vì Streamlit tải tệp vào bộ nhớ, ghi ra đĩa cho phép OpenCV đọc hiệu quả trong lúc trích xuất khung hình đồng thời giữ nguyên phần mở rộng gốc. - Lấy mẫu khung hình từ video: Hàm
extract_frames()tải video bằng OpenCV và lấy mẫu một số ít khung hình cách đều suốt thời lượng. Thay vì xử lý mọi khung, hàm tính khoảng lấy mẫu và trích xuất các khung hình đại diện nắm bắt gameplay. Mỗi khung mã hóa thànhJPEGvà chuyển sang chuỗibase64để có thể gửi trực tiếp làm đầu vào ảnh cho mô hình Qwen đa phương thức. - Trích JSON có cấu trúc: Hàm tiện ích
extract_json_block()đảm bảo đầu ra mô hình có thể được phân tích cú pháp chính xác. Nội dung trích xuất sau đó có thể kiểm định an toàn với lược đồGameSpec. - Dọn dẹp phản hồi HTML: Hàm
cleanup_html_response()loại bỏ định dạng markdown mà mô hình có thể thêm khi sinh mã. Nếu mô hình bọc HTML trong khung mã ba dấu nháy, tiện ích này sẽ loại bỏ các dấu đó để HTML có thể render trực tiếp trong khung xem trước của Streamlit. - Tương tác bàn phím cho game nhúng: Hàm
patch_html_for_iframe_keyboard()chèn một đoạn JavaScript nhỏ vào HTML tạo ra để đảm bảo điều khiển bàn phím hoạt động đúng trong iframe của Streamlit. Nó tự động focus phần tử canvas, gántabindexnếu thiếu và ngăn trình duyệt chặn phím mũi tên và phím cách để dùng cho điều khiển gameplay.
Kết hợp lại, các tiện ích này tạo thành lớp tiền xử lý video và xử lý phản hồi của pipeline.
Bước 6: Suy luận đặc tả game
Giờ khi đã có các khung hình trích xuất và mẫu prompt sẵn sàng, chúng ta có thể triển khai giai đoạn đầu của pipeline suy luận, tức là chuyển các khung hình gameplay thành đặc tả game có cấu trúc.
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()
Hàm trên định nghĩa bốn giai đoạn chính trong pipeline suy luận đặc tả game:
- Chuẩn bị khung hình: Lời gọi
extract_frames()lấy mẫu tập nhỏ khung hình đại diện từ video gameplay tải lên. Những ảnh này là bằng chứng trực quan để mô hình suy luận cơ chế game. - Xây prompt: Hàm
build_spec_user_prompt()động tạo user prompt sử dụng gợi ý tùy chọn, mọi ràng buộc bổ sung và số khung hình tối đa được trích xuất. Prompt này cung cấp hướng dẫn rõ ràng để mô hình tạo đặc tả game phù hợp lược đồ. - Gọi mô hình đa phương thức: Yêu cầu
ollama.chat()gửi cả prompt văn bản và các khung hình trích xuất đến mô hìnhqwen3.5:9bcục bộ. Thiết lập sinh dùngtemperaturethấp để đầu ra mang tính quyết định hơn vànum_ctx=8192để có đủ không gian ngữ cảnh cho prompt và phản hồi có cấu trúc. - Kiểm định lược đồ: Khi mô hình phản hồi, hàm trích khối JSON, phân tích và kiểm định theo lược đồ Pydantic
GameSpec. Cuối cùng, đặc tả đã kiểm định được trả về dạng từ điển Python.
Giờ chúng ta đã có biểu diễn có cấu trúc của trò chơi mà mô hình suy luận từ các khung hình mẫu. Ở bước tiếp theo, chúng ta sẽ dùng đặc tả đã kiểm định này làm đầu vào cho giai đoạn sinh trò chơi HTML.
Bước 7: Sinh trò chơi HTML
Tại thời điểm này, mô hình không còn phải suy luận cơ chế từ ảnh nữa. Thay vào đó, chúng ta yêu cầu qwen3.5:9b tạo một tệp HTML tự chứa duy nhất gồm bố cục, kiểu, vòng lặp game, điều khiển và logic dựng hình.
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)
Hàm trên thực hiện bốn bước sau để tạo và chuẩn bị trò chơi HTML cuối cùng:
- Gửi prompt có cấu trúc: Hàm nhận từ điển
game_specđã kiểm định và truyền vàobuild_code_user_prompt(). Việc này chuyển đặc tả game thành prompt triển khai chi tiết hướng dẫn mô hình tạo trò chơi HTML một tệp hoàn chỉnh. - Sinh mã với mô hình Qwen: Lời gọi
ollama.chat()gửi system prompt sinh mã và user prompt tới mô hìnhqwen3.5:9bcục bộ. Vì tác vụ giờ là sinh mã thuần tuý thay vì suy luận đa phương thức, mô hình chỉ nhận văn bản. - Dọn dẹp và kiểm tra HTML: Phản hồi thô của mô hình được chuyển qua
cleanup_html_response()để loại bỏ markdown hoặc định dạng thừa. Sau đó, chúng ta kiểm tra đơn giản để xác nhận đầu ra chứa tài liệu HTML. - Vá bàn phím cho iframe: HTML cuối cùng được chuyển qua
patch_html_for_iframe_keyboard(), chèn bản vá JavaScript nhỏ giúp game có thể chơi trong iframe nhúng của Streamlit. Bước này quan trọng vì game trên trình duyệt thường không bắt được phím mũi tên và phím cách đúng cách trừ khi canvas có thể focus và được focus tự động.
Giờ chúng ta đã có trò chơi HTML có thể chơi trên trình duyệt, được tạo hoàn toàn từ đặc tả game suy luận.
Bước 8: Giao diện Streamlit
Bước cuối cùng, hãy gói mọi thứ trong một giao diện Streamlit đơn giản để người dùng có thể thử hệ thống mà không cần đụng vào mã nguồn. Giao diện cung cấp ba khả năng chính:
- Tải lên video gameplay
- Cấu hình tham số sinh như gợi ý và lấy mẫu khung hình
- Xem trước và tải xuống trò chơi đã tạo
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",
)
Đoạn mã trên định nghĩa cách người dùng tải một clip gameplay, tùy chọn cung cấp gợi ý về loại game, và điều chỉnh số khung hình cần trích xuất để phân tích. Khi video đã được tải lên, ứng dụng hiển thị nó cùng với nút Generate game để kích hoạt toàn bộ pipeline.
Khi người dùng chạy pipeline, ứng dụng trước hết trích xuất khung hình và suy luận cơ chế game bằng mô hình qwen3.5:9b cục bộ, tạo ra đặc tả game có cấu trúc. Ở giai đoạn thứ hai, mô hình tạo một trò chơi HTML hoàn chỉnh từ đặc tả đó. Kết quả được lưu trong session state của Streamlit để không mất khi giao diện cập nhật.
Sau khi tạo xong, giao diện hiển thị đặc tả game suy luận, bản xem trước có thể chơi của trò chơi đã tạo, mã nguồn HTML thô và các nút tải xuống để lưu cả trò chơi HTML và đặc tả JSON.
Để tự thử, lưu mã thành app.py và chạy:
streamlit run app.py

Kết luận
Qwen 3.5 là một trong những họ mô hình đa phương thức thú vị nhất hiện nay. Trong hướng dẫn này, chúng tôi dùng qwen3.5:9b qua Ollama để xây một bộ tạo trò chơi từ video gameplay hoàn toàn cục bộ. Ứng dụng trích xuất khung hình từ một đoạn clip ngắn, suy luận thiết kế game có cấu trúc, tạo trò chơi trình duyệt một tệp và xem trước ngay trong Streamlit.
Mô hình lưu trữ qwen/qwen3.5-397b-a17b vẫn là lựa chọn nhanh và mạnh hơn cho tác vụ này, nhưng kết quả từ bản 9B cục bộ cũng rất ấn tượng. Nó cho thấy mô hình đa phương thức nhỏ nay có thể thực hiện suy luận thị giác và sinh mã có ý nghĩa trên phần cứng người dùng.
Nếu muốn tiến xa hơn, hãy so sánh 9B với các mô hình 4B và 27B, thêm vòng tinh chỉnh, hoặc biến đầu ra thành một mini game engine hỗ trợ nhiều lần lặp từ cùng một clip.
Qwen3.5 Small FAQs
Can I run this demo with `qwen3.5:4b` instead of 9B?
Có, nhưng kết quả thường kém ổn định hơn. Mô hình 4B vẫn hữu ích cho thử nghiệm cục bộ gọn nhẹ, nhưng biến thể 9B cân bằng tốt hơn giữa tốc độ và chất lượng. Ollama cung cấp cả hai dưới dạng mô hình văn bản–hình ảnh cục bộ.
Why not use `qwen3.5:0.8b` or 2B?
Các mô hình 2B phù hợp hơn với các tác vụ đa phương thức đơn giản hoặc triển khai kiểu edge hơn là toàn bộ pipeline này.
What is the best local Qwen 3.5 model for this use case?
qwen3.5:9b là điểm khởi đầu tốt nhất. Mô hình vẫn gọn (6,6GB trong Ollama), hỗ trợ đầu vào văn bản–hình ảnh và cho hiệu năng tốt hơn nhiều so với các biến thể rất nhỏ cho dạng tác vụ tạo–suy luận nặng như thế này.
Can I use the larger Qwen 3.5 models locally, too?
Có. Ollama hiện cũng liệt kê các biến thể 27b, 35b và 122b, tất cả đều hỗ trợ văn bản–hình ảnh. Đổi lại là yêu cầu bộ nhớ và độ trễ.
Tôi là Chuyên gia Google Developers trong lĩnh vực ML (Gen AI), Chuyên gia Kaggle 3x và Đại sứ Women Techmakers với hơn 3 năm kinh nghiệm trong ngành công nghệ. Tôi đồng sáng lập một startup công nghệ y tế vào năm 2020 và hiện đang theo học thạc sĩ khoa học máy tính tại Georgia Tech, chuyên sâu về học máy.

Qwen 3.5 là họ mô hình đa phương thức mã nguồn mở, được thiết kế để xử lý cả đầu vào văn bản và hình ảnh. Các mô hình này theo kiến trúc image-text-to-text, cho phép suy luận trên ảnh hoặc khung hình video song song với lời nhắc ngôn ngữ tự nhiên.
Trong hướng dẫn này, chúng ta sẽ xây dựng một ứng dụng Streamlit chuyển đổi video gameplay thành trò chơi trên trình duyệt có thể chơi được bằng cách dùng mô hình nhỏ Qwen3.5 cục bộ,
qwen3.5:9b, được lưu trữ qua Ollama. Ứng dụng sẽ trích xuất khung hình từ video tải lên, suy luận cơ chế gameplay, chuyển chúng thành đặc tả có cấu trúc, rồi tạo ra một trò chơi HTML/CSS/JavaScript hoàn chỉnh có thể xem trước trực tiếp trong giao diện.Trong quá trình thực hiện, bạn sẽ học cách:
Kết thúc, bạn sẽ có một bản demo hoạt động có thể biến một đoạn clip gameplay ngắn thành trò chơi trình duyệt đơn giản, có thể chơi được, được tạo hoàn toàn trên máy cục bộ. Bạn có thể xem phiên bản tóm tắt của quy trình trong video dưới đây: