Lewati ke konten utama

Tutorial Model Kecil Qwen 3.5: Bangun Generator Video-ke-Gim dengan Ollama

Pelajari cara menggunakan model kecil Qwen 3.5 secara lokal melalui Ollama untuk mengubah cuplikan gameplay pendek menjadi gim HTML yang dapat dimainkan.
Diperbarui 28 Sep 2026  · 14 mnt Baca

Jelajahi bersama AI

ChatGPTClaudePerplexity

Qwen 3.5 adalah keluarga model multimodal sumber terbuka yang dirancang untuk menangani masukan teks dan visual. Model-model ini mengikuti arsitektur image-text-to-text, memungkinkan penalaran atas gambar atau bingkai video bersamaan dengan prompt bahasa alami. 

Dalam tutorial ini, kita akan membangun aplikasi Streamlit yang mengonversi video gameplay menjadi gim peramban yang dapat dimainkan menggunakan model kecil Qwen3.5 lokal, qwen3.5:9b, yang di-host melalui Ollama. Aplikasi ini mengekstrak bingkai dari video yang diunggah, menyimpulkan mekanika gim, mengonversinya menjadi spesifikasi terstruktur, lalu menghasilkan gim HTML/CSS/JavaScript lengkap yang dapat dipratinjau langsung di dalam antarmuka.

Sepanjang proses, Anda akan mempelajari cara:

  • Mengekstrak bingkai representatif dari video untuk penalaran multimodal
  • Menjalankan Qwen 3.5 secara lokal dengan Ollama untuk inferensi gambar-teks
  • Merancang pipeline prompting dua tahap untuk penalaran dan pembuatan kode
  • Mengonversi pengamatan gameplay visual menjadi spesifikasi gim terstruktur
  • Menghasilkan gim peramban HTML5 yang dapat dimainkan dari mekanika yang disimpulkan
  • Membangun antarmuka Streamlit interaktif untuk menjalankan dan mempratinjau pipeline

Pada akhirnya, Anda akan memiliki demo yang berfungsi untuk mengubah cuplikan gameplay pendek menjadi gim peramban sederhana yang dapat dimainkan, yang sepenuhnya dihasilkan pada mesin lokal. Anda dapat melihat versi ringkas alur kerjanya pada video di bawah ini:

Apa itu Qwen 3.5 Small?

Qwen 3.5 adalah keluarga model multimodal yang dirancang untuk mencakup berbagai skenario penyebaran, dari model edge yang ringan hingga sistem penalaran skala terdepan. 

Lini resmi Qwen 3.5 saat ini mencakup varian 0.8B, 2B, 4B, 9B, 27B, 35B-A3B, 122B-A10B, dan 397B-A17B, bersama dengan checkpoint base dan terkuantisasi untuk beberapa model. 

Memilih model kecil yang tepat bergantung pada jenis alur kerja yang ingin Anda bangun. 

  • Varian 0.8B dan 2B paling cocok untuk perangkat edge, pembuatan prototipe cepat, dan lingkungan perangkat keras yang terbatas. 
  • Model 4B memberikan peningkatan dan bekerja baik untuk agen lokal kecil atau tugas penalaran visual yang ringan. 
  • Model 9B lebih cocok untuk aplikasi multimodal lokal karena tetap cukup ringkas untuk dijalankan secara lokal namun cukup kuat untuk menyimpulkan mekanika gameplay dan menghasilkan kode gim HTML yang dapat digunakan. 

Untuk tutorial ini, kami menggunakan model qwen3.5:9b, yang memberikan keseimbangan kuat antara kapabilitas, kemudahan penyebaran lokal, dan penalaran multimodal, sehingga sangat cocok untuk membangun pipeline dari bingkai video ke gim peramban tanpa bergantung pada API eksternal.

Anda juga dapat membaca panduan kami tentang cara menjalankan Qwen3.5 secara lokal pada satu GPU. 

Mengapa menggunakan model kecil lokal untuk ini?

Saya juga menguji demo ini dengan model qwen/qwen3.5-397b-a17b. Meskipun model ini jauh lebih besar dan umumnya lebih mumpuni daripada varian 9B lokal, model ini memerlukan inferensi berbasis API, yang memperkenalkan ketergantungan eksternal dan potensi latensi.

Dalam praktiknya, jika waktu eksekusi bukan kendala ketat, waktu pembuatan keseluruhan berbeda sekitar ~3 menit antara kedua model dalam pengujian saya. Model yang lebih besar memang menghasilkan logika gameplay yang sedikit lebih mulus dan pengalaman pratinjau yang lebih rapi, tetapi peningkatannya tidak dramatis.

Mengingat model 9B berjalan sepenuhnya secara lokal, hasilnya sangat kompetitif. Terlepas dari ukurannya yang jauh lebih kecil, model ini mampu menghasilkan gim peramban yang dapat dimainkan dengan penurunan kualitas yang relatif kecil dibandingkan model 397B yang jauh lebih besar.

Anda dapat membaca panduan lengkap kami tentang fine-tuning Qwen3.5 small untuk mempelajari lebih lanjut cara memaksimalkan varian 0.8B. 

Proyek Contoh Qwen 3.5: Bangun Generator Video-ke-Gim

Pada bagian ini, kita akan membangun aplikasi Streamlit yang:

  • menerima cuplikan gameplay pendek
  • mengekstrak sejumlah kecil bingkai representatif
  • mengirim bingkai tersebut ke qwen3.5:9b melalui Ollama
  • meminta model menyimpulkan spesifikasi gim terstruktur
  • meminta model menghasilkan gim HTML satu berkas
  • mempratinjau gim yang dihasilkan di dalam aplikasi Streamlit

Pengaturan ini menyoroti kapabilitas multimodal Qwen yang mendukung tugas seperti merekayasa balik logika dari rekaman gameplay dan mengubahnya menjadi kode

Langkah 1: Instal dependensi

Sebelum membangun aplikasi, kita perlu menyiapkan lingkungan lokal dengan pustaka yang diperlukan untuk pemrosesan video, inferensi multimodal, dan antarmuka interaktif. 

pip install streamlit opencv-python ollama python-dotenv pydantic

Demo ini menggunakan sejumlah kecil pustaka Python:

  • Streamlit untuk membangun antarmuka web interaktif tempat pengguna mengunggah video gameplay dan mempratinjau gim yang dihasilkan.
  • OpenCV untuk mendekode video yang diunggah dan mengekstrak bingkai representatif.
  • Ollama untuk menjalankan model Qwen 3.5 lokal guna penalaran multimodal dan pembuatan kode.
  • python-dotenv untuk mengelola variabel lingkungan jika diperlukan.
  • Pydantic untuk menerapkan skema terstruktur bagi spesifikasi gim yang disimpulkan sebelum menghasilkan kode.

Selanjutnya, kita perlu mengunduh modelnya. Ollama mempermudah hal ini dengan menarik model langsung dari registry modelnya:

ollama run qwen3.5:9b

Jalankan pertama kali akan mengunduh model dan menyimpannya secara lokal. qwen3.5:9B berukuran sekitar 6,6GB, mendukung masukan teks dan gambar, serta menyediakan jendela konteks 256K token, yang membuatnya sangat cocok untuk alur kerja multimodal.

Langkah 2: Impor

Setelah lingkungan lokal siap, langkah berikutnya adalah mengimpor pustaka yang akan kita gunakan di seluruh aplikasi dan membuat folder untuk menyimpan artefak yang dihasilkan. 

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)

Blok di atas mengumpulkan tumpukan inti untuk proyek. Kita menggunakan json untuk memformat spesifikasi gim terstruktur, base64 untuk menyandikan bingkai video yang diekstrak sebelum mengirimkannya ke model, dan tempfile untuk menyimpan video yang diunggah dengan aman selama sesi. Path dari pathlib memberi kita cara yang rapi untuk mengelola path berkas, sementara List dari typing digunakan untuk petunjuk tipe dalam definisi skema.

Berikutnya, kita mengimpor pustaka aplikasi utama seperti cv2 dari OpenCV yang bertanggung jawab membaca cuplikan gameplay yang diunggah dan mengekstrak bingkai representatif, ollama adalah antarmuka untuk berkomunikasi dengan model qwen3.5:9b yang berjalan secara lokal. streamlit menyediakan UI web, dan streamlit.components.v1 memungkinkan kita menyematkan gim HTML yang dihasilkan langsung ke dalam aplikasi untuk pratinjau dan interaksi langsung.

Terakhir, kita mengimpor BaseModel, Field, dan ValidationError dari Pydantic. Ini digunakan untuk mendefinisikan dan memvalidasi skema JSON terstruktur yang merepresentasikan desain gim yang disimpulkan. Alih-alih mempercayai model untuk mengeluarkan teks dengan format longgar, kita memaksanya ke dalam skema yang dapat diprediksi sebelum meneruskan hasil tersebut ke generator gim HTML.

Dua baris terakhir membuat direktori outputs_local jika belum ada. Folder ini adalah tempat kita menyimpan artefak yang dihasilkan seperti spesifikasi gim yang disimpulkan dan berkas gim HTML final, sehingga lebih mudah untuk memeriksa atau mengunduh hasil setelah setiap proses.

Pada langkah berikutnya, kita akan mendefinisikan kelas skema Pydantic yang mendeskripsikan spesifikasi gim yang harus dihasilkan model dari bingkai gameplay yang diekstrak.

Langkah 3: Definisikan skema

Pada tahap ini, kita mendefinisikan skema terstruktur yang harus diikuti model saat mendeskripsikan mekanika gameplay yang disimpulkan dari bingkai video. 

Alih-alih meminta model langsung menghasilkan kode HTML dari gambar, kita terlebih dahulu mengharuskannya menghasilkan spesifikasi gim terstruktur. Representasi antara ini membuat pipeline jauh lebih mudah untuk dibedah, divalidasi, dan diperluas.

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]

Skema ini mendefinisikan struktur inti spesifikasi gim yang harus dihasilkan model.

  • Kelas Entity merepresentasikan objek apa pun di dalam gim, seperti pemain, musuh, rintangan, atau elemen UI. Setiap entitas menyertakan nama, perannya dalam gim, dan deskripsi singkat perilakunya.
  • Kelas Physics mendeskripsikan bagaimana objek bergerak dan berinteraksi di dunia gim. Bidang-bidang ini menangkap mekanika dasar seperti gravitasi, dorongan/lompatan, penanganan tumbukan, dan pola pergerakan.
  • Kelas VisualStyle menangkap penyajian visual gim. Ini mencakup perspektif kamera (seperti top-down atau side-scrolling), palet warna, gaya latar, dan elemen UI yang terlihat seperti penghitung skor atau indikator kesehatan.
  • Terakhir, kelas GameSpec menyatukan semuanya. Ini merepresentasikan desain gim lengkap yang disimpulkan dari bingkai video, termasuk tujuan, kontrol, loop gameplay, entitas, sistem penilaian, kondisi menang/kalah, dan asumsi yang dibuat model saat menafsirkan bingkai.

Dengan memvalidasi keluaran model terhadap skema ini, kita memastikan bahwa desain yang disimpulkan selalu mengikuti struktur yang dapat diprediksi. Ini juga memudahkan untuk meneruskan spesifikasi ke tahap berikutnya dalam pipeline.

Langkah 4: Prompt untuk pembuatan dua tahap

Dengan skema yang telah siap, langkah selanjutnya adalah mendefinisikan prompt yang memandu model melalui dua tahap pipeline kita. 

Tahap pertama berfokus pada rekayasa balik mekanika gameplay dari bingkai yang diekstrak dan menghasilkan spesifikasi gim terstruktur. 

Tahap kedua mengambil spesifikasi tersebut dan menghasilkan gim peramban yang dapat dimainkan menggunakan HTML, CSS, dan JavaScript.

Langkah 4.1: Definisikan system prompt

Komponen pertama dari strategi prompting kita adalah system prompt, yang mendefinisikan peran yang harus dimainkan model selama setiap tahap 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 pertama menginstruksikan model untuk berperilaku seperti analis gameplay. Ini menekankan asumsi minimal dan mendorong model untuk hanya menyimpulkan mekanika yang benar-benar didukung oleh bingkai. 

Karena model hanya melihat sejumlah kecil gambar yang diambil sampelnya, prompt secara eksplisit membiasakannya ke gim gaya arcade sederhana yang dapat diimplementasikan dengan logika peramban yang ringan.

System prompt kedua mengalihkan peran model menjadi pengembang JavaScript. Pada tahap ini, model menerima spesifikasi gim terstruktur dan bertanggung jawab mengonversinya menjadi gim HTML yang lengkap. 

Langkah 4.2: Bangun prompt game-spec

Selanjutnya, kita mendefinisikan fungsi pembantu yang menyusun user prompt untuk tahap pertama pipeline. Prompt ini meminta model menganalisis bingkai yang diekstrak dan menghasilkan spesifikasi gim terstruktur.

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

Fungsi ini menyusun prompt yang akan dikirim ke model beserta bingkai video yang diekstrak. Isinya mencakup beberapa elemen penting:

  • Pengingat bahwa model menganalisis bingkai statis, bukan video penuh
  • Kecenderungan ke mekanika arcade sederhana
  • Struktur skema JSON lengkap, memastikan keluaran cocok dengan model GameSpec kita
  • Secara opsional, pengguna dapat menambahkan petunjuk dan batasan yang memungkinkan antarmuka mengarahkan model ke gaya gim tertentu

Dengan menyematkan skema langsung di dalam prompt, kita mendorong model untuk menghasilkan keluaran yang dapat divalidasi oleh Pydantic pada tahap berikutnya dari pipeline.

Langkah 4.3: Bangun prompt pembuatan kode

Setelah spesifikasi gim dihasilkan dan divalidasi, kita memerlukan prompt kedua untuk mengonversi spesifikasi tersebut menjadi gim yang benar-benar dapat dimainkan.

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

Fungsi build_code_user_prompt mengonversi kamus GameSpec menjadi JSON terformat dan memasukkannya ke dalam prompt. Model kemudian menggunakan deskripsi terstruktur ini sebagai brief desain untuk menghasilkan gim HTML final.

Karena spesifikasi sudah mendefinisikan mekanika, entitas, kontrol, dan tujuan, tugas model menjadi jauh lebih sederhana. Model hanya perlu menerjemahkan desain itu ke logika gim JavaScript dan loop rendering berbasis canvas.

Pada langkah berikutnya, kita akan menerapkan pipeline ekstraksi bingkai menggunakan OpenCV, yang mengonversi video gameplay yang diunggah menjadi sejumlah kecil gambar representatif yang dapat dianalisis oleh model Qwen 3.5.

Langkah 5: Ekstrak bingkai representatif

Model Qwen 3.5 lokal milik Ollama menerima teks dan gambar, bukan video mentah. Jadi alih-alih mengirim klip yang diunggah secara langsung, kita mengekstrak sejumlah kecil bingkai yang tersebar merata dan meneruskannya ke model. 

def save_uploaded_video(uploaded_file) -> Path:
    suffix = Path(uploaded_file.name).suffix or ".mp4"
    with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
        tmp.write(uploaded_file.read())
        return Path(tmp.name)
def extract_frames(video_path: Path, max_frames: int = 6):
    cap = cv2.VideoCapture(str(video_path))
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    interval = max(1, total_frames // max_frames) if total_frames > 0 else 1
    frames = []
    frame_id = 0
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        if frame_id % interval == 0:
            _, buffer = cv2.imencode(".jpg", frame)
            frames.append(base64.b64encode(buffer).decode())
        frame_id += 1
        if len(frames) >= max_frames:
            break
    cap.release()
    return frames
def extract_json_block(text: str) -> str:
    start = text.find("{")
    end = text.rfind("}")
    if start == -1 or end == -1 or end <= start:
        raise ValueError("No valid JSON object found in model response.")
    return text[start : end + 1]
def cleanup_html_response(text: str) -> str:
    html = text.strip()
    if html.startswith("```"):
        lines = html.splitlines()
        if lines and lines[0].startswith("```"):
            lines = lines[1:]
        if lines and lines[-1].startswith("```"):
            lines = lines[:-1]
        html = "\n".join(lines).strip()
    return html
def patch_html_for_iframe_keyboard(html: str) -> str:
    if 'tabindex="0"' not in html and "<canvas" in html:
        html = html.replace("<canvas", '<canvas tabindex="0"', 1)
    focus_patch = """
<script>
(function() {
  const canvas = document.querySelector("canvas");
  if (!canvas) return;
  if (!canvas.hasAttribute("tabindex")) {
    canvas.setAttribute("tabindex", "0");
  }
  function focusGame() {
    try { canvas.focus(); } catch (e) {}
  }
  window.addEventListener("load", focusGame);
  canvas.addEventListener("click", focusGame);
  document.addEventListener("keydown", function(e) {
    if (["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"," "].includes(e.key)) {
      e.preventDefault();
    }
  }, { passive: false });
  const buttons = document.querySelectorAll("button");
  buttons.forEach(btn => {
    btn.addEventListener("click", () => {
      setTimeout(focusGame, 50);
    });
  });
})();
</script>
"""
    if "</body>" in html:
        html = html.replace("</body>", focus_patch + "\n</body>")
    else:
        html += focus_patch
    return html

Utilitas di atas mendefinisikan lima komponen kunci untuk mengekstrak bingkai representatif dari video yang diunggah:

  • Penanganan unggah video: Fungsi save_uploaded_video() menyimpan video gameplay yang diunggah sebagai berkas sementara di disk. Karena Streamlit mengunggah berkas di memori, menulis berkas ke disk memungkinkan OpenCV membacanya secara efisien selama ekstraksi bingkai sambil mempertahankan ekstensi berkas asli.
  • Sampel bingkai dari video: Fungsi extract_frames() memuat video menggunakan OpenCV dan mengambil sampel sejumlah kecil bingkai yang tersebar merata di sepanjang durasinya. Alih-alih memproses setiap bingkai, fungsi menghitung interval sampling dan mengekstrak bingkai representatif yang menangkap gameplay. Setiap bingkai dikodekan sebagai JPEG dan diubah menjadi string base64 sehingga dapat dikirim langsung sebagai masukan gambar ke model Qwen multimodal.
  • Ekstraksi JSON terstruktur: Fungsi pembantu extract_json_block() memastikan keluaran model dapat diurai dengan benar. Konten yang diekstrak kemudian dapat divalidasi dengan aman terhadap skema GameSpec.
  • Pembersihan respons HTML: Fungsi cleanup_html_response() menghapus pemformatan markdown yang mungkin disertakan model saat menghasilkan kode. Jika model membungkus keluaran HTML dalam pagar kode backtick tiga, pembantu ini menghapus penanda tersebut sehingga HTML dapat dirender langsung di dalam pratinjau Streamlit.
  • Interaksi papan ketik untuk gim sematan: Fungsi patch_html_for_iframe_keyboard() menyuntikkan patch JavaScript kecil ke dalam HTML yang dihasilkan untuk memastikan kontrol papan ketik berfungsi dengan benar di dalam iframe Streamlit. Fungsi ini otomatis memfokuskan elemen canvas, menetapkan tabindex jika belum ada, dan mencegah peramban mencegat tombol panah dan spasi agar dapat digunakan untuk kontrol gameplay.

Bersama-sama, utilitas ini membentuk lapisan prapemrosesan video dan penanganan respons pada pipeline.

Langkah 6: Simpulkan spesifikasi gim 

Sekarang setelah kita memiliki bingkai yang diekstrak dan templat prompt siap, kita dapat menerapkan tahap pertama dari pipeline penalaran, yaitu mengonversi bingkai gameplay menjadi spesifikasi gim terstruktur.

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

Fungsi di atas mendefinisikan empat tahap kunci dalam pipeline inferensi spesifikasi gim:

  • Persiapan bingkai: Pemanggilan extract_frames() mengambil sampel sejumlah kecil bingkai representatif dari video gameplay yang diunggah. Gambar-gambar ini bertindak sebagai bukti visual yang akan digunakan model untuk menyimpulkan mekanika gim.
  • Konstruksi prompt: Fungsi build_spec_user_prompt() secara dinamis membuat user prompt menggunakan petunjuk opsional, batasan tambahan, dan jumlah maksimum bingkai yang diekstrak. Prompt ini memberi model instruksi yang jelas untuk menghasilkan spesifikasi gim yang patuh skema.
  • Pemanggilan model multimodal: Permintaan ollama.chat() mengirimkan prompt teks dan bingkai yang diekstrak ke model lokal qwen3.5:9b. Pengaturan generasi menggunakan temperature rendah untuk menjaga keluaran deterministik dan num_ctx=8192 untuk menyediakan ruang konteks yang cukup bagi prompt dan respons terstruktur.
  • Validasi skema: Setelah model merespons, fungsi mengekstrak blok JSON, mengurainya, dan memvalidasinya terhadap skema Pydantic GameSpec. Terakhir, spesifikasi yang telah divalidasi dikembalikan sebagai kamus Python.

Kini kita memiliki representasi terstruktur dari gim yang disimpulkan model dari bingkai video yang diambil sampelnya. Pada langkah berikutnya, kita akan menggunakan spesifikasi tervalidasi ini sebagai masukan untuk tahap pembuatan gim HTML.

Langkah 7: Hasilkan gim HTML

Pada titik ini, model tidak lagi harus menyimpulkan mekanika dari gambar. Sebagai gantinya, kita meminta qwen3.5:9b menghasilkan satu berkas HTML mandiri yang mencakup tata letak, gaya, game loop, kontrol, dan logika 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)

Fungsi di atas melakukan empat langkah berikut untuk menghasilkan dan menyiapkan gim HTML final:

  • Penyampaian prompt terstruktur: Fungsi di atas mengambil kamus game_spec yang tervalidasi dan meneruskannya ke fungsi build_code_user_prompt(). Ini mengonversi spesifikasi gim menjadi prompt implementasi terperinci yang menginstruksikan model untuk menghasilkan gim HTML satu berkas yang lengkap.
  • Pembuatan kode dengan model Qwen: Pemanggilan ollama.chat() mengirimkan system prompt pembuatan kode dan user prompt ke model lokal qwen3.5:9b. Karena tugasnya kini murni pembuatan kode bukan penalaran multimodal, model hanya menerima teks. 
  • Pembersihan dan validasi HTML: Respons mentah model diteruskan melalui cleanup_html_response() untuk menghapus markdown atau pemformatan tambahan. Lalu kita melakukan pemeriksaan validasi sederhana untuk memastikan keluaran memuat dokumen HTML.
  • Patch papan ketik pada iframe: HTML final diteruskan melalui patch_html_for_iframe_keyboard(), yang menyuntikkan perbaikan JavaScript kecil agar gim dapat dimainkan di dalam iframe sematan Streamlit. Langkah ini penting karena gim peramban sering gagal menangkap tombol panah dan spasi dengan benar kecuali canvas dapat difokuskan secara eksplisit dan difokuskan secara otomatis.

Sekarang kita memiliki gim HTML yang dapat dimainkan di peramban, yang sepenuhnya dihasilkan dari spesifikasi gim yang disimpulkan.

Langkah 8: UI Streamlit

Sebagai langkah terakhir, mari bungkus semuanya di dalam antarmuka Streamlit sederhana sehingga pengguna dapat mencoba sistem ini tanpa menyentuh kode dasarnya. Antarmuka ini menyediakan tiga kemampuan utama:

  • Mengunggah video gameplay
  • Konfigurasi parameter pembuatan seperti petunjuk dan pengambilan sampel bingkai
  • Pratinjau dan unduh gim yang dihasilkan
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",
        )

Kode di atas mendefinisikan bagaimana pengguna mengunggah cuplikan gameplay, secara opsional memberikan petunjuk tentang jenis gim, dan mengontrol berapa banyak bingkai yang harus diekstrak dari video untuk analisis. Setelah video diunggah, aplikasi menampilkannya bersama tombol Generate game yang memicu seluruh pipeline.

Saat pengguna menjalankan pipeline, aplikasi terlebih dahulu mengekstrak bingkai dan menyimpulkan mekanika gim menggunakan model qwen3.5:9b lokal, menghasilkan spesifikasi gim terstruktur. Pada tahap kedua, model menghasilkan gim HTML lengkap dari spesifikasi tersebut. Hasil disimpan dalam session state Streamlit sehingga tetap ada saat UI diperbarui.

Setelah proses selesai, antarmuka menampilkan spesifikasi gim yang disimpulkan, pratinjau langsung gim yang dihasilkan, sumber HTML mentah, serta tombol unduh untuk menyimpan gim HTML dan spesifikasi JSON.

Untuk mencobanya sendiri, simpan kodenya sebagai app.py dan jalankan:

streamlit run app.py

Demo

Kesimpulan

Qwen 3.5 adalah salah satu keluarga model multimodal paling menarik saat ini. Dalam tutorial ini, kita menggunakan qwen3.5:9b melalui Ollama untuk membangun generator video gameplay-ke-gim yang sepenuhnya lokal. Aplikasi ini mengekstrak bingkai dari cuplikan pendek, menyimpulkan desain gim terstruktur, menghasilkan gim peramban satu berkas, dan mempratinjaunya di dalam Streamlit.

Model hosted qwen/qwen3.5-397b-a17b tetap menjadi opsi yang lebih cepat dan lebih kuat untuk tugas ini, namun hasil 9B lokal sudah mengesankan. Ini membuktikan bahwa model multimodal kecil kini dapat melakukan penalaran visual dan pembuatan kode yang bermakna pada perangkat konsumen.

Jika Anda ingin melangkah lebih jauh, bandingkan 9B dengan model 4B dan 27B, tambahkan loop penyempurnaan, atau ubah keluarannya menjadi mesin gim mini yang mendukung beberapa iterasi dari klip yang sama.

Qwen3.5 Small FAQs

Bisakah saya menjalankan demo ini dengan `qwen3.5:4b` alih-alih 9B?

Bisa, tetapi hasilnya biasanya kurang andal. Model 4B tetap berguna untuk eksperimen lokal yang ringan, namun varian 9B adalah keseimbangan yang lebih baik antara kecepatan dan kualitas. Ollama menyediakan keduanya sebagai model lokal teks-dan-gambar.

Mengapa tidak menggunakan `qwen3.5:0.8b` atau 2B?

Model 2B lebih cocok untuk tugas multimodal yang lebih sederhana atau penyebaran bergaya edge daripada pipeline penuh ini.

Apa model Qwen 3.5 lokal terbaik untuk kasus penggunaan ini?

qwen3.5:9b adalah titik awal terbaik. Ukurannya tetap ringkas di 6,6GB di Ollama, mendukung masukan teks-dan-gambar, dan berkinerja jauh lebih baik daripada varian yang sangat kecil untuk tugas pembuatan yang sangat bergantung pada penalaran seperti ini.

Bisakah saya menggunakan model Qwen 3.5 yang lebih besar secara lokal juga?

Ya. Ollama saat ini juga mencantumkan varian 27b, 35b, dan 122b, semuanya dengan dukungan teks-dan-gambar. Pertukarannya adalah memori dan latensi.


Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

Saya adalah Google Developers Expert di ML (Gen AI), Kaggle 3x Expert, dan Women Techmakers Ambassador dengan pengalaman lebih dari 3 tahun di bidang teknologi. Saya ikut mendirikan startup health-tech pada 2020 dan sedang menempuh studi magister ilmu komputer di Georgia Tech dengan spesialisasi machine learning.

Topik
Kecerdasan Buatan
Large Language Models

Kursus Teratas DataCamp

Kursus

Pengembangan Perangkat Lunak dengan Cursor

1 jam 30 menit
4.3K
Bangun kode yang siap produksi dengan Cursor. Pelajari prompt AI, refactoring, pengujian, dan alur kerja lanjutan.
Lihat DetailRight Arrow
Mulai Kursus
Lihat SelengkapnyaRight Arrow
Terkait

blogs

12 Alternatif ChatGPT Terbaik yang Bisa Anda Coba pada 2026

Artikel ini menyajikan daftar alternatif ChatGPT yang akan meningkatkan produktivitas Anda.
Javier Canales Luna's photo

Javier Canales Luna

14 mnt

blogs

40 Pertanyaan Wawancara DBMS Teratas di 2026

Kuasai pertanyaan wawancara basis data, dari konsep SQL dasar hingga skenario desain sistem tingkat lanjut. Panduan mendalam ini mencakup semua yang Anda perlukan untuk sukses di wawancara DBMS dan meraih peran berikutnya.
Dario Radečić's photo

Dario Radečić

15 mnt

blogs

Spaghetti Plot dan Jalur Badai

Temukan alasan mengapa Anda sebaiknya (tidak) menggunakan spaghetti plot untuk menyampaikan ketidakpastian jalur prediksi badai serta dampaknya terhadap interpretasi.
Hugo Bowne-Anderson's photo

Hugo Bowne-Anderson

13 mnt

blogs

Tutorial Korelasi di R

Dapatkan pengenalan dasar-dasar korelasi di R: pelajari lebih lanjut tentang koefisien korelasi, matriks korelasi, plotting korelasi, dan sebagainya.
David Woods's photo

David Woods

13 mnt

Lihat SelengkapnyaLihat Selengkapnya