473 lines
14 KiB
Python
473 lines
14 KiB
Python
import json
|
|
|
|
import requests
|
|
from fastapi import FastAPI
|
|
from pydantic import BaseModel
|
|
|
|
from config import DEVICE, CHECKPOINT_DIR
|
|
from data.tokenizer import Tokenizer
|
|
from makescript import json_string_to_py
|
|
from model.builder_model import BuilderModel
|
|
from utils.checkpoint import latest_checkpoint, load_checkpoint
|
|
|
|
import subprocess
|
|
import sys
|
|
import os
|
|
import base64
|
|
|
|
app = FastAPI(title="Minecraft Builder AI")
|
|
|
|
if DEVICE == "cuda":
|
|
print("Cuda Mode: true")
|
|
else:
|
|
print("Cuda Mode: false")
|
|
|
|
print("Loading tokenizer...")
|
|
tokenizer = Tokenizer()
|
|
tokenizer.load("data")
|
|
|
|
print("Loading model...")
|
|
model = BuilderModel(tokenizer).to(DEVICE)
|
|
|
|
checkpoint = latest_checkpoint(CHECKPOINT_DIR)
|
|
load_checkpoint(
|
|
checkpoint,
|
|
model,
|
|
optimizer=None,
|
|
map_location=DEVICE,
|
|
)
|
|
|
|
model.eval()
|
|
|
|
|
|
class BuildRequest(BaseModel):
|
|
prompt: str
|
|
|
|
|
|
@app.post("/generate")
|
|
def generate(req: BuildRequest):
|
|
|
|
blocks, tokens = model.generate(req.prompt)
|
|
|
|
return {
|
|
"prompt": req.prompt,
|
|
"tokens": tokens,
|
|
"blocks": blocks,
|
|
}
|
|
|
|
class RunRequest(BaseModel):
|
|
script: str
|
|
|
|
@app.post("/run_script")
|
|
def runscript(req: RunRequest):
|
|
result = subprocess.run([
|
|
sys.executable,
|
|
"builders/" + req.script + ".py"
|
|
])
|
|
|
|
if result.returncode != 0:
|
|
return {
|
|
"success": False,
|
|
"schem": None
|
|
}
|
|
|
|
schem_path = "schematics/" + req.script + ".schem"
|
|
|
|
if not os.path.exists(schem_path):
|
|
return {
|
|
"success": False,
|
|
"schem": None
|
|
}
|
|
|
|
with open(schem_path, "rb") as f:
|
|
schem = base64.b64encode(f.read()).decode("ascii")
|
|
|
|
return {
|
|
"success": True,
|
|
"schem": schem
|
|
}
|
|
|
|
class AIScriptRequest(BaseModel):
|
|
url: str
|
|
model: str
|
|
api_key: str
|
|
prompt: str
|
|
context_length: int
|
|
temperature: float
|
|
reasoning: str
|
|
|
|
@app.post("/aiscript")
|
|
def aiscript(req: AIScriptRequest):
|
|
systemprompt = r"""
|
|
You are a Minecraft Building AI.
|
|
You build with the mcschematic python library.
|
|
Your task is to return a valid script in the following format:
|
|
{
|
|
"file_name": FILE_NAME,
|
|
"script: SCRIPT,
|
|
"response": RESPONSE
|
|
}
|
|
|
|
Rules:
|
|
- Only use valid Minecraft blocks
|
|
- The output folder is called "schematics"
|
|
- The schematic name must match with the FILE_NAME
|
|
- In settings there always needs to be a variable called "SEED"
|
|
|
|
Documentation:
|
|
mcschematic is a Python library for programmatically creating Minecraft Java Edition schematic files. The basic workflow is:
|
|
Create an MCSchematic object.
|
|
Place Minecraft blocks at (x, y, z) coordinates with setBlock().
|
|
Optionally manipulate blocks/entities/data.
|
|
Save the resulting structure as a .schem file with save().
|
|
The core conceptual model is:
|
|
import mcschematic
|
|
schematic = mcschematic.MCSchematic()
|
|
schematic.setBlock((x, y, z), "minecraft:oak_log")
|
|
schematic.setBlock((x, y, z), "minecraft:oak_leaves")
|
|
schematic.save(
|
|
"output_directory",
|
|
"my_structure",
|
|
mcschematic.Version.JE_1_20_1
|
|
)
|
|
Important concepts
|
|
MCSchematic
|
|
This is the main container representing the Minecraft structure.
|
|
schem = mcschematic.MCSchematic()
|
|
Think of it as an in-memory 3D voxel structure.
|
|
Coordinates
|
|
Blocks are addressed using:
|
|
(x, y, z)
|
|
For procedural generation, treat these as a normal 3D Cartesian coordinate system. Pick a clear origin convention—for example, (0, 0, 0) as the bottom-center of a tree—and build everything relative to that.
|
|
Placing blocks
|
|
The primary operation is:
|
|
schem.setBlock((x, y, z), "minecraft:oak_log")
|
|
Block IDs should use Minecraft's namespaced IDs:
|
|
minecraft:stone
|
|
minecraft:dirt
|
|
minecraft:oak_log
|
|
minecraft:oak_leaves
|
|
minecraft:glass
|
|
Block states can be specified as part of the block string when supported, e.g.:
|
|
"minecraft:oak_leaves[persistent=true]"
|
|
For a procedural generator, create helper functions around setBlock() rather than scattering raw calls throughout the code.
|
|
Example:
|
|
def block(schem, x, y, z, block_id):
|
|
schem.setBlock((x, y, z), block_id)
|
|
Then your generation code can operate at a higher level:
|
|
block(schem, x, y, z, "minecraft:oak_log")
|
|
Procedural generation
|
|
mcschematic isn't a tree/terrain generator itself. It provides the Minecraft schematic representation and file output.
|
|
The procedural geometry should be implemented by the agent.
|
|
For example:
|
|
Tree generator
|
|
│
|
|
├── trunk()
|
|
│ └── places log blocks
|
|
│
|
|
├── branch()
|
|
│ └── calculates a 3D line and places logs
|
|
│
|
|
├── roots()
|
|
│ └── creates radial structures
|
|
│
|
|
└── canopy()
|
|
└── creates randomized leaf volumes
|
|
│
|
|
└── setBlock()
|
|
Useful geometric primitives to implement yourself include:
|
|
line between two 3D points
|
|
cylinder
|
|
sphere / ellipsoid
|
|
cone
|
|
tapered branch
|
|
radial distribution
|
|
noise/randomized volume
|
|
distance-based falloff
|
|
For example, a spherical leaf cluster can be generated by checking:
|
|
distance = (
|
|
(x - cx) ** 2 +
|
|
(y - cy) ** 2 +
|
|
(z - cz) ** 2
|
|
)
|
|
if distance <= radius ** 2:
|
|
schem.setBlock(
|
|
(x, y, z),
|
|
"minecraft:oak_leaves[persistent=true]"
|
|
)
|
|
Minecraft version
|
|
The library has Minecraft-version constants under mcschematic.Version. The agent should inspect the installed library rather than hard-code a version based on memory.
|
|
For example:
|
|
print(dir(mcschematic.Version))
|
|
and choose the appropriate Java Edition version available in the installed package.
|
|
Likewise, the agent should inspect the actual installed API:
|
|
import mcschematic
|
|
print(mcschematic.version)
|
|
print(dir(mcschematic))
|
|
If there is uncertainty about an API call, inspect the package source/signatures instead of inventing an API.
|
|
Saving
|
|
The schematic is eventually serialized with:
|
|
schem.save(
|
|
output_folder,
|
|
schematic_name,
|
|
version
|
|
)
|
|
The resulting file is intended to be usable by Minecraft schematic-compatible tools.
|
|
Recommended architecture for a tree generator
|
|
A good design would be:
|
|
tree = TreeGenerator(
|
|
height=20,
|
|
trunk_radius=2,
|
|
seed=1234,
|
|
)
|
|
tree.generate()
|
|
tree.export(
|
|
"output/tree.schem"
|
|
)
|
|
Internally:
|
|
TreeGenerator
|
|
↓
|
|
generate_trunk()
|
|
↓
|
|
generate_branches()
|
|
↓
|
|
generate_roots()
|
|
↓
|
|
generate_canopy()
|
|
↓
|
|
MCSchematic.setBlock()
|
|
↓
|
|
MCSchematic.save()
|
|
|
|
Helper Methods:
|
|
def set_block(x, y, z, block):
|
|
schem.setBlock((x, y, z), block)
|
|
|
|
def fill_sphere(cx, cy, cz, radius, block, irregularity=0.0):
|
|
# Create a branch between two points.
|
|
for x in range(cx - radius, cx + radius + 1):
|
|
for y in range(cy - radius, cy + radius + 1):
|
|
for z in range(cz - radius, cz + radius + 1):
|
|
|
|
distance = math.sqrt(
|
|
(x - cx) ** 2 +
|
|
(y - cy) ** 2 +
|
|
(z - cz) ** 2
|
|
)
|
|
|
|
# Make the sphere slightly irregular.
|
|
wobble = random.uniform(-irregularity, irregularity)
|
|
|
|
if distance <= radius + wobble:
|
|
set_block(x, y, z, block)
|
|
|
|
|
|
def cylinder(cx, cz, y_start, y_end, radius, block):
|
|
# Create a vertical cylinder.
|
|
for y in range(y_start, y_end + 1):
|
|
for x in range(cx - radius, cx + radius + 1):
|
|
for z in range(cz - radius, cz + radius + 1):
|
|
|
|
if (x - cx) ** 2 + (z - cz) ** 2 <= radius ** 2:
|
|
set_block(x, y, z, block)
|
|
|
|
|
|
def branch(x1, y1, z1, x2, y2, z2, radius=1):
|
|
# Create a branch between two points.
|
|
steps = max(
|
|
abs(x2 - x1),
|
|
abs(y2 - y1),
|
|
abs(z2 - z1)
|
|
)
|
|
|
|
for i in range(steps + 1):
|
|
t = i / max(steps, 1)
|
|
|
|
x = round(x1 + (x2 - x1) * t)
|
|
y = round(y1 + (y2 - y1) * t)
|
|
z = round(z1 + (z2 - z1) * t)
|
|
|
|
cylinder(x, z, y, y, radius, "minecraft:oak_log")
|
|
|
|
These are example helper methods. Also try to make your own when needed.
|
|
|
|
This is how the schematic will be saved:
|
|
schem.save(
|
|
OUTPUT_FOLDER,
|
|
OUTPUT_NAME,
|
|
MC_VERSION
|
|
)
|
|
|
|
print(f"Saved {OUTPUT_FOLDER}/{OUTPUT_NAME}.schem")
|
|
|
|
Coding Rules ONLY APPLY THESE IN THE SCRIPT NOT THE ENTIRE JSON:
|
|
file_name:
|
|
Use only letters, numbers, underscores, and hyphens.
|
|
Do not include .py.
|
|
Do not include directories or paths.
|
|
script:
|
|
Must contain the complete, executable Python source code.
|
|
The script must use mcschematic.
|
|
The script must save the generated schematic using schem.save(...).
|
|
Use Minecraft Java Edition 1.20.1 unless the user specifies another version.
|
|
The script must be self-contained and must not require the user to manually modify it.
|
|
CRITICAL JSON REQUIREMENT:
|
|
The value of script is a JSON string.
|
|
You MUST properly JSON-escape the Python source code.
|
|
Every double quote (") INSIDE the Python script must be escaped as \".
|
|
Every newline inside the Python script must be represented as \n.
|
|
Never output literal unescaped newlines inside the JSON script string.
|
|
Never produce invalid JSON.
|
|
Do NOT wrap the response in Markdown code fences.
|
|
Do NOT output json. Do NOT output python.
|
|
Output ONLY the JSON object.
|
|
Do not add explanations, comments outside the Python script, or any text before or after the JSON object.
|
|
Before responding, internally verify that your entire response is valid JSON and could be successfully parsed with Python's json.loads().
|
|
The resulting value of:
|
|
json.loads(response)["script"]
|
|
must be directly writable to a .py file and must produce valid Python source code.
|
|
In "response" give a short description what you build.
|
|
Example of the REQUIRED output format:
|
|
{
|
|
"file_name": "small_oak_tree",
|
|
"script": "import mcschematic\nimport math\n\nMC_VERSION = mcschematic.Version.JE_1_20_1\nschem = mcschematic.MCSchematic()\n\nschem.setBlock((0, 0, 0), \"minecraft\")\n\nschem.save(\"schematics\", \"small_oak_tree\", MC_VERSION)\n",
|
|
"response": "I build your small oak tree."
|
|
}
|
|
Remember: the example above demonstrates the JSON encoding requirements. Your actual response must contain ONLY one valid JSON object.
|
|
"""
|
|
|
|
payload = {
|
|
"model": req.model,
|
|
"messages": [
|
|
{
|
|
"role": "system",
|
|
"content": systemprompt
|
|
},
|
|
{
|
|
"role": "user",
|
|
"content": req.prompt
|
|
}
|
|
],
|
|
"max_completion_tokens": req.context_length,
|
|
"reasoning_effort": req.reasoning,
|
|
"temperature": req.temperature,
|
|
"stream": True,
|
|
}
|
|
|
|
headers = {
|
|
"Authorization": f"Bearer {req.api_key}",
|
|
"Content-Type": "application/json"
|
|
}
|
|
|
|
try:
|
|
response = requests.post(
|
|
req.url + "/chat/completions",
|
|
json=payload,
|
|
headers=headers,
|
|
timeout=600,
|
|
stream=True
|
|
)
|
|
response.raise_for_status()
|
|
|
|
reasoning = ""
|
|
answer = ""
|
|
|
|
for line in response.iter_lines(decode_unicode=True):
|
|
if not line or not line.startswith("data: "):
|
|
continue
|
|
|
|
data = line[6:]
|
|
|
|
if data == "[DONE]":
|
|
break
|
|
|
|
try:
|
|
chunk = json.loads(data)
|
|
except json.JSONDecodeError:
|
|
print("Could not decode SSE chunk:", data)
|
|
continue
|
|
|
|
# Some chunks contain no choices.
|
|
choices = chunk.get("choices", [])
|
|
if not choices:
|
|
continue
|
|
|
|
choice = choices[0]
|
|
delta = choice.get("delta", {})
|
|
|
|
# Reasoning tokens
|
|
reasoning_token = delta.get("reasoning_content")
|
|
if reasoning_token:
|
|
reasoning += reasoning_token
|
|
print(reasoning_token, end="", flush=True)
|
|
|
|
# Normal answer tokens
|
|
content_token = delta.get("content")
|
|
if content_token:
|
|
answer += content_token
|
|
print(content_token, end="", flush=True)
|
|
|
|
print("\n\nFINAL ANSWER:")
|
|
print(answer)
|
|
|
|
# Parse the AI's JSON
|
|
try:
|
|
result = json.loads(answer)
|
|
except json.JSONDecodeError as e:
|
|
print("AI did not return valid JSON!")
|
|
print("JSON error:", e)
|
|
print("Raw answer:")
|
|
print(repr(answer))
|
|
|
|
return {
|
|
"success": False,
|
|
"file_name": "failed",
|
|
"response": "AI returned invalid JSON"
|
|
}
|
|
|
|
# Validate expected fields
|
|
if "file_name" not in result:
|
|
raise ValueError("AI JSON is missing 'file_name'")
|
|
|
|
if "script" not in result:
|
|
raise ValueError("AI JSON is missing 'script'")
|
|
|
|
if "response" not in result:
|
|
raise ValueError("AI JSON is missing 'response'")
|
|
|
|
file_name = result["file_name"]
|
|
script = result["script"]
|
|
remessage = result["response"]
|
|
|
|
# Write the actual Python script
|
|
with open(file_name + ".py", "w", encoding="utf-8") as f:
|
|
f.write(script)
|
|
|
|
return {
|
|
"success": True,
|
|
"file_name": file_name,
|
|
"response": remessage
|
|
}
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
print("API request failed!")
|
|
print("Error:", e)
|
|
|
|
if e.response is not None:
|
|
print("Status code:", e.response.status_code)
|
|
print("Response:", e.response.text)
|
|
|
|
return {
|
|
"success": False,
|
|
"file_name": "failed",
|
|
"response": "failed"
|
|
}
|
|
|
|
except Exception as e:
|
|
print("Unexpected error:", e)
|
|
|
|
return {
|
|
"success": False,
|
|
"file_name": "failed",
|
|
"response": str(e)
|
|
} |