From 7dedbef80877bc71d3ee72d05ef9d2da5c67c6ee Mon Sep 17 00:00:00 2001 From: Blockmaster29 Date: Mon, 21 Sep 2026 22:20:24 +0200 Subject: [PATCH] feat: Initial Commit --- .idea/.gitignore | 10 + .idea/misc.xml | 9 + .idea/modules.xml | 8 + .idea/vcs.xml | 13 + MineBuildAI.iml | 11 + config.py | 123 +++++++ data/dataset.json | 655 +++++++++++++++++++++++++++++++++++ data/dataset.py | 113 ++++++ data/tokenizer.py | 232 +++++++++++++ infer.py | 74 ++++ makescript.py | 53 +++ model/builder_model.py | 133 +++++++ model/positional_encoding.py | 55 +++ model/transformer.py | 196 +++++++++++ old settings.txt | 5 + prompt | 19 + pyproject.toml | 13 + requirements.txt | 21 ++ server.py | 473 +++++++++++++++++++++++++ train.py | 156 +++++++++ utils/checkpoint.py | 85 +++++ 21 files changed, 2457 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 MineBuildAI.iml create mode 100644 config.py create mode 100644 data/dataset.json create mode 100644 data/dataset.py create mode 100644 data/tokenizer.py create mode 100644 infer.py create mode 100644 makescript.py create mode 100644 model/builder_model.py create mode 100644 model/positional_encoding.py create mode 100644 model/transformer.py create mode 100644 old settings.txt create mode 100644 prompt create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 server.py create mode 100644 train.py create mode 100644 utils/checkpoint.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..383423b --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..570b307 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..c7b1ecf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/MineBuildAI.iml b/MineBuildAI.iml new file mode 100644 index 0000000..5cc3dc8 --- /dev/null +++ b/MineBuildAI.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..9719625 --- /dev/null +++ b/config.py @@ -0,0 +1,123 @@ +""" +config.py + +Global configuration for the Minecraft AI Builder. +""" + +from pathlib import Path +import torch + +# ========================================================== +# Project Paths +# ========================================================== + +ROOT_DIR = Path(__file__).parent + +DATASET_PATH = ROOT_DIR / "data" / "dataset.json" +CHECKPOINT_DIR = ROOT_DIR / "checkpoints" + +CHECKPOINT_DIR.mkdir(exist_ok=True) + +# ========================================================== +# Model +# ========================================================== + +EMBED_DIM = 512 +NUM_HEADS = 8 +NUM_ENCODER_LAYERS = 6 +NUM_DECODER_LAYERS = 6 +FEED_FORWARD_DIM = 2048 +DROPOUT = 0.1 + +# Maximum lengths + +MAX_PROMPT_LENGTH = 64 +MAX_OUTPUT_LENGTH = 4096 + +# ========================================================== +# Vocabulary +# ========================================================== + +# Filled automatically after building vocab +TEXT_VOCAB_SIZE = None +OUTPUT_VOCAB_SIZE = None + +# Special Tokens +PAD_TOKEN = "" +START_TOKEN = "" +END_TOKEN = "" +UNK_TOKEN = "" + +SPECIAL_TOKENS = [ + PAD_TOKEN, + START_TOKEN, + END_TOKEN, + UNK_TOKEN +] + +# ========================================================== +# Training +# ========================================================== + +BATCH_SIZE = 8 +LEARNING_RATE = 1e-4 + +EPOCHS = 100 + +WEIGHT_DECAY = 1e-5 + +DEVICE = "cuda" if torch.cuda.is_available() else "cpu" + +# ========================================================== +# Generation +# ========================================================== + +TEMPERATURE = 1.0 + +TOP_K = 40 + +TOP_P = 0.95 + +# ========================================================== +# Random Seed +# ========================================================== + +RANDOM_SEED = 42 + +# ========================================================== +# Coordinate Limits +# ========================================================== + +MIN_COORD = 0 +MAX_COORD = 128 + +# ========================================================== +# Supported Minecraft Blocks +# ========================================================== + +SUPPORTED_BLOCKS = [ + "minecraft:stone", + + "minecraft:oak_planks", + "minecraft:spruce_planks", + "minecraft:birch_planks", + "minecraft:jungle_planks", + "minecraft:acacia_planks", + "minecraft:dark_oak_planks", + "minecraft:mangrove_planks", + "minecraft:cherry_planks", + "minecraft:bamboo_planks", + "minecraft:crimson_planks", + "minecraft:warped_planks", + + "minecraft:oak_log", + "minecraft:spruce_log", + "minecraft:birch_log", + "minecraft:jungle_log", + "minecraft:acacia_log", + "minecraft:dark_oak_log", + "minecraft:mangrove_log", + "minecraft:cherry_log", + "minecraft:crimson_stem", + "minecraft:warped_stem", +] \ No newline at end of file diff --git a/data/dataset.json b/data/dataset.json new file mode 100644 index 0000000..c8514b8 --- /dev/null +++ b/data/dataset.json @@ -0,0 +1,655 @@ +[ + { + "prompt": "single stone", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:stone" + } + ] + }, + { + "prompt": "single oak planks", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:oak_planks" + } + ] + }, + { + "prompt": "single oak log", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:oak_log" + } + ] + }, + { + "prompt": "single spruce planks", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:spruce_planks" + } + ] + }, + { + "prompt": "single birch planks", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:birch_planks" + } + ] + }, + { + "prompt": "single jungle planks", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:jungle_planks" + } + ] + }, + { + "prompt": "single acacia planks", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:acacia_planks" + } + ] + }, + { + "prompt": "single dark oak planks", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:dark_oak_planks" + } + ] + }, + { + "prompt": "single mangrove planks", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:mangrove_planks" + } + ] + }, + { + "prompt": "single cherry planks", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:cherry_planks" + } + ] + }, + { + "prompt": "single bamboo planks", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:bamboo_planks" + } + ] + }, + { + "prompt": "single crimson planks", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:crimson_planks" + } + ] + }, + { + "prompt": "single warped planks", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:warped_planks" + } + ] + }, + { + "prompt": "single oak log", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:oak_log" + } + ] + }, + { + "prompt": "single spruce log", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:spruce_log" + } + ] + }, + { + "prompt": "single birch log", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:birch_log" + } + ] + }, + { + "prompt": "single jungle log", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:jungle_log" + } + ] + }, + { + "prompt": "single acacia log", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:acacia_log" + } + ] + }, + { + "prompt": "single dark oak log", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:dark_oak_log" + } + ] + }, + { + "prompt": "single mangrove log", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:mangrove_log" + } + ] + }, + { + "prompt": "single cherry log", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:cherry_log" + } + ] + }, + { + "prompt": "single crimson stem", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:crimson_stem" + } + ] + }, + { + "prompt": "single warped stem", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:warped_stem" + } + ] + }, + { + "prompt": "3 block tall wooden pillar", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:oak_log" + }, + { + "x": 0, + "y": 1, + "z": 0, + "block": "minecraft:oak_log" + }, + { + "x": 0, + "y": 2, + "z": 0, + "block": "minecraft:oak_log" + } + ] + }, + { + "prompt": "3 block tall wooden pillar out of oak logs", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:oak_log" + }, + { + "x": 0, + "y": 1, + "z": 0, + "block": "minecraft:oak_log" + }, + { + "x": 0, + "y": 2, + "z": 0, + "block": "minecraft:oak_log" + } + ] + }, + { + "prompt": "3 block tall wooden pillar out of spruce logs", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:spruce_log" + }, + { + "x": 0, + "y": 1, + "z": 0, + "block": "minecraft:spruce_log" + }, + { + "x": 0, + "y": 2, + "z": 0, + "block": "minecraft:spruce_log" + } + ] + }, + { + "prompt": "3 block tall wooden pillar out of birch logs", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:birch_log" + }, + { + "x": 0, + "y": 1, + "z": 0, + "block": "minecraft:birch_log" + }, + { + "x": 0, + "y": 2, + "z": 0, + "block": "minecraft:birch_log" + } + ] + }, + { + "prompt": "a 4 block wide and 3 block tall wooden wall", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 0, + "z": 1, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 0, + "z": 2, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 0, + "z": 3, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 1, + "z": 0, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 1, + "z": 1, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 1, + "z": 2, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 1, + "z": 3, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 2, + "z": 0, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 2, + "z": 1, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 2, + "z": 2, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 2, + "z": 3, + "block": "minecraft:oak_planks" + } + ] + }, + { + "prompt": "a 4 block wide and 3 block tall wooden wall out of oak planks", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 0, + "z": 1, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 0, + "z": 2, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 0, + "z": 3, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 1, + "z": 0, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 1, + "z": 1, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 1, + "z": 2, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 1, + "z": 3, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 2, + "z": 0, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 2, + "z": 1, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 2, + "z": 2, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 2, + "z": 3, + "block": "minecraft:oak_planks" + } + ] + }, + { + "prompt": "a small wooden wall", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 0, + "z": 1, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 0, + "z": 2, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 0, + "z": 3, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 1, + "z": 0, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 1, + "z": 1, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 1, + "z": 2, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 1, + "z": 3, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 2, + "z": 0, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 2, + "z": 1, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 2, + "z": 2, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 2, + "z": 3, + "block": "minecraft:oak_planks" + } + ] + }, + { + "prompt": "a small wooden wall out of oak planks", + "blocks": [ + { + "x": 0, + "y": 0, + "z": 0, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 0, + "z": 1, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 0, + "z": 2, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 0, + "z": 3, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 1, + "z": 0, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 1, + "z": 1, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 1, + "z": 2, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 1, + "z": 3, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 2, + "z": 0, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 2, + "z": 1, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 2, + "z": 2, + "block": "minecraft:oak_planks" + }, + { + "x": 0, + "y": 2, + "z": 3, + "block": "minecraft:oak_planks" + } + ] + } +] \ No newline at end of file diff --git a/data/dataset.py b/data/dataset.py new file mode 100644 index 0000000..7038559 --- /dev/null +++ b/data/dataset.py @@ -0,0 +1,113 @@ +import json + +import torch +from torch.utils.data import Dataset +from torch.nn.utils.rnn import pad_sequence + +from config import ( + PAD_TOKEN, + MAX_PROMPT_LENGTH, + MAX_OUTPUT_LENGTH, +) + + +class MinecraftDataset(Dataset): + """ + Dataset for training the Minecraft Builder AI. + """ + + def __init__(self, dataset_path, tokenizer): + self.tokenizer = tokenizer + + with open(dataset_path, "r", encoding="utf8") as f: + self.data = json.load(f) + + self.pad_text = tokenizer.text_to_id[PAD_TOKEN] + self.pad_output = tokenizer.output_to_id[PAD_TOKEN] + + def __len__(self): + return len(self.data) + + def __getitem__(self, index): + sample = self.data[index] + + prompt_ids = self.tokenizer.encode_prompt( + sample["prompt"] + ) + + output_ids = self.tokenizer.encode_blocks( + sample["blocks"] + ) + + # Limit maximum sequence lengths + prompt_ids = prompt_ids[:MAX_PROMPT_LENGTH] + output_ids = output_ids[:MAX_OUTPUT_LENGTH] + + return ( + torch.tensor(prompt_ids, dtype=torch.long), + torch.tensor(output_ids, dtype=torch.long), + ) + + +def collate_fn(batch): + """ + Pads sequences inside a batch. + """ + + prompts = [item[0] for item in batch] + outputs = [item[1] for item in batch] + + prompt_pad = prompts[0].new_tensor( + [0] + ) # placeholder (overwritten below) + + output_pad = outputs[0].new_tensor( + [0] + ) + + # These values are replaced by the DataLoader factory below. + prompt_padding_value = getattr(collate_fn, "prompt_pad", 0) + output_padding_value = getattr(collate_fn, "output_pad", 0) + + prompts = pad_sequence( + prompts, + batch_first=True, + padding_value=prompt_padding_value, + ) + + outputs = pad_sequence( + outputs, + batch_first=True, + padding_value=output_padding_value, + ) + + return prompts, outputs + + +def create_dataloader( + dataset_path, + tokenizer, + batch_size, + shuffle=True, +): + """ + Creates a DataLoader with automatic padding. + """ + + dataset = MinecraftDataset( + dataset_path, + tokenizer, + ) + + # Give the collate function the correct padding IDs + collate_fn.prompt_pad = tokenizer.text_to_id[PAD_TOKEN] + collate_fn.output_pad = tokenizer.output_to_id[PAD_TOKEN] + + loader = torch.utils.data.DataLoader( + dataset, + batch_size=batch_size, + shuffle=shuffle, + collate_fn=collate_fn, + ) + + return loader \ No newline at end of file diff --git a/data/tokenizer.py b/data/tokenizer.py new file mode 100644 index 0000000..b393658 --- /dev/null +++ b/data/tokenizer.py @@ -0,0 +1,232 @@ +import json +from collections import Counter +from pathlib import Path + +from config import ( + PAD_TOKEN, + START_TOKEN, + END_TOKEN, + UNK_TOKEN, + SPECIAL_TOKENS, +) + + +class Tokenizer: + def __init__(self): + self.text_to_id = {} + self.id_to_text = {} + + self.output_to_id = {} + self.id_to_output = {} + + # ===================================================== + # Build Vocabulary + # ===================================================== + + def build(self, dataset_path): + + dataset_path = Path(dataset_path) + + with open(dataset_path, "r", encoding="utf8") as f: + data = json.load(f) + + text_counter = Counter() + output_counter = Counter() + + for sample in data: + + # --------------------------- + # Prompt tokens + # --------------------------- + + prompt = sample["prompt"].lower().split() + + text_counter.update(prompt) + + # --------------------------- + # Output tokens + # --------------------------- + + for block in sample["blocks"]: + + x = block["x"] + y = block["y"] + z = block["z"] + b = block["block"] + + output_counter.update([ + f"X_{x}", + f"Y_{y}", + f"Z_{z}", + f"BLOCK_{b}" + ]) + + # Special tokens + + text_vocab = SPECIAL_TOKENS + sorted(text_counter.keys()) + output_vocab = SPECIAL_TOKENS + sorted(output_counter.keys()) + + self.text_to_id = { + token: i for i, token in enumerate(text_vocab) + } + + self.id_to_text = { + i: token for token, i in self.text_to_id.items() + } + + self.output_to_id = { + token: i for i, token in enumerate(output_vocab) + } + + self.id_to_output = { + i: token for token, i in self.output_to_id.items() + } + + # ===================================================== + # Prompt Encoding + # ===================================================== + + def encode_prompt(self, prompt): + + tokens = prompt.lower().split() + + ids = [ + self.text_to_id[START_TOKEN] + ] + + for token in tokens: + + ids.append( + self.text_to_id.get( + token, + self.text_to_id[UNK_TOKEN] + ) + ) + + ids.append( + self.text_to_id[END_TOKEN] + ) + + return ids + + def decode_prompt(self, ids): + + words = [] + + for idx in ids: + + token = self.id_to_text[idx] + + if token in SPECIAL_TOKENS: + continue + + words.append(token) + + return " ".join(words) + + # ===================================================== + # Structure Encoding + # ===================================================== + + def encode_blocks(self, blocks): + + ids = [ + self.output_to_id[START_TOKEN] + ] + + for block in blocks: + + ids.extend([ + self.output_to_id[f"X_{block['x']}"], + self.output_to_id[f"Y_{block['y']}"], + self.output_to_id[f"Z_{block['z']}"], + self.output_to_id[f"BLOCK_{block['block']}"], + ]) + + ids.append( + self.output_to_id[END_TOKEN] + ) + + return ids + + def decode_blocks(self, ids): + + tokens = [] + + for idx in ids: + + token = self.id_to_output[idx] + + if token in SPECIAL_TOKENS: + continue + + tokens.append(token) + + blocks = [] + + i = 0 + + while i + 3 < len(tokens): + + x = int(tokens[i][2:]) + y = int(tokens[i + 1][2:]) + z = int(tokens[i + 2][2:]) + block = tokens[i + 3][6:] + + blocks.append({ + "x": x, + "y": y, + "z": z, + "block": block + }) + + i += 4 + + return blocks + + # ===================================================== + # Save / Load + # ===================================================== + + def save(self, folder): + + folder = Path(folder) + folder.mkdir(parents=True, exist_ok=True) + + with open(folder / "text_vocab.json", "w") as f: + json.dump(self.text_to_id, f, indent=4) + + with open(folder / "output_vocab.json", "w") as f: + json.dump(self.output_to_id, f, indent=4) + + def load(self, folder): + + folder = Path(folder) + + with open(folder / "text_vocab.json", "r") as f: + self.text_to_id = json.load(f) + + with open(folder / "output_vocab.json", "r") as f: + self.output_to_id = json.load(f) + + self.id_to_text = { + int(v): k + for k, v in self.text_to_id.items() + } + + self.id_to_output = { + int(v): k + for k, v in self.output_to_id.items() + } + + # ===================================================== + # Properties + # ===================================================== + + @property + def text_vocab_size(self): + return len(self.text_to_id) + + @property + def output_vocab_size(self): + return len(self.output_to_id) \ No newline at end of file diff --git a/infer.py b/infer.py new file mode 100644 index 0000000..27b4751 --- /dev/null +++ b/infer.py @@ -0,0 +1,74 @@ +from config import ( + DEVICE, + CHECKPOINT_DIR, +) + +from data.tokenizer import Tokenizer +from model.builder_model import BuilderModel +from utils.checkpoint import latest_checkpoint, load_checkpoint + + +def main(): + 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) + + if checkpoint is None: + print("No checkpoint found!") + return + + print(f"Loading checkpoint: {checkpoint}") + + load_checkpoint( + checkpoint, + model, + optimizer=None, + map_location=DEVICE, + ) + + model.eval() + + print("\nMinecraft AI Builder") + print("Type 'exit' to quit.\n") + + while True: + + prompt = input("> ") + + if prompt.lower() in ("exit", "quit"): + break + + blocks, token_count = model.generate(prompt) + + print("\nGenerated Structure:\n") + + if len(blocks) == 0: + print("(No blocks generated)") + continue + + for block in blocks: + print( + f"{block['block']:20}" + f" x={block['x']:4}" + f" y={block['y']:4}" + f" z={block['z']:4}" + ) + + print(f"\nTotal blocks: {len(blocks)}") + print(f"Generated tokens: {token_count}\n") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/makescript.py b/makescript.py new file mode 100644 index 0000000..bd7c755 --- /dev/null +++ b/makescript.py @@ -0,0 +1,53 @@ +import json +from pathlib import Path + + +def json_string_to_py(json_text: str, output_file: str = "generated.py"): + """ + Convert an AI response containing: + { + "file_name": "...", + "script": "..." + } + + into a valid Python .py file. + """ + + # Parse the JSON response + data = json.loads(json_text) + + # Validate that the script exists + if "script" not in data: + raise ValueError("JSON does not contain a 'script' field.") + + script = data["script"] + + if not isinstance(script, str): + raise TypeError("'script' must be a string.") + + # Write the Python source code exactly as provided + output_path = Path("builders/" + output_file) + output_path.write_text(script, encoding="utf-8") + + print(f"Created: {output_path.resolve()}") + + return output_path + + +# Example usage +if __name__ == "__main__": + + ai_response = r''' +{ +"file_name": "small_oak_tree", +"script": "import random\n\nSEED = 42\nrandom.seed(SEED)\n\nprint(\"Hello, World!\")" +} +''' + + data = json.loads(ai_response) + + # Prefer the AI-provided filename + filename = data.get("file_name", "generated") + output_file = f"{filename}.py" + + json_string_to_py(ai_response, output_file) \ No newline at end of file diff --git a/model/builder_model.py b/model/builder_model.py new file mode 100644 index 0000000..a553c46 --- /dev/null +++ b/model/builder_model.py @@ -0,0 +1,133 @@ +import torch +import torch.nn as nn + +from config import ( + DEVICE, + START_TOKEN, + END_TOKEN, + MAX_OUTPUT_LENGTH, +) + +from model.transformer import MinecraftTransformer + + +class BuilderModel(nn.Module): + """ + High-level wrapper around the MinecraftTransformer. + """ + + def __init__(self, tokenizer): + super().__init__() + + self.tokenizer = tokenizer + + self.transformer = MinecraftTransformer( + tokenizer.text_vocab_size, + tokenizer.output_vocab_size, + ) + + def forward(self, src, tgt): + """ + Training forward pass. + + src = prompt tokens + tgt = decoder input tokens (already shifted by train.py) + """ + + return self.transformer( + src=src, + tgt=tgt, + ) + + @torch.no_grad() + def generate( + self, + prompt, + max_length=MAX_OUTPUT_LENGTH, + ): + """ + Generate a Minecraft structure from a prompt. + """ + + self.eval() + + device = next(self.parameters()).device + + # Encode prompt + src = torch.tensor( + [self.tokenizer.encode_prompt(prompt)], + dtype=torch.long, + device=device, + ) + + # Encode with transformer + memory = self.transformer.encode(src) + + start_id = self.tokenizer.output_to_id[START_TOKEN] + end_id = self.tokenizer.output_to_id[END_TOKEN] + + generated = [start_id] + + for _ in range(max_length): + + tgt = torch.tensor( + [generated], + dtype=torch.long, + device=device, + ) + + logits = self.transformer.decode( + tgt, + memory, + ) + + probs = torch.softmax(logits[0, -1], dim=-1) + + next_token = torch.argmax(probs).item() + + print( + next_token, + self.tokenizer.id_to_output[next_token], + f"{probs[next_token].item():.3f}" + ) + + generated.append(next_token) + + if next_token == end_id: + break + + blocks = self.tokenizer.decode_blocks(generated) + + # Don't count the token + token_count = max(0, len(generated) - 1) + + return blocks, token_count + + def save(self, path): + """ + Save model weights. + """ + + torch.save( + self.state_dict(), + path, + ) + + def load(self, path): + """ + Load model weights. + """ + + self.load_state_dict( + torch.load( + path, + map_location=DEVICE, + ) + ) + + def to_device(self): + """ + Move model to configured device. + """ + + return self.to(DEVICE) \ No newline at end of file diff --git a/model/positional_encoding.py b/model/positional_encoding.py new file mode 100644 index 0000000..8c8517a --- /dev/null +++ b/model/positional_encoding.py @@ -0,0 +1,55 @@ +import math + +import torch +import torch.nn as nn + + +class PositionalEncoding(nn.Module): + """ + Standard sinusoidal positional encoding from + "Attention Is All You Need". + """ + + def __init__(self, embed_dim, dropout=0.1, max_len=10000): + super().__init__() + + self.dropout = nn.Dropout(dropout) + + pe = torch.zeros(max_len, embed_dim) + + position = torch.arange( + 0, + max_len, + dtype=torch.float + ).unsqueeze(1) + + div_term = torch.exp( + torch.arange( + 0, + embed_dim, + 2 + ).float() * + (-math.log(10000.0) / embed_dim) + ) + + pe[:, 0::2] = torch.sin(position * div_term) + pe[:, 1::2] = torch.cos(position * div_term) + + pe = pe.unsqueeze(0) + + # Stored as a buffer so it moves with the model + # but isn't trained. + self.register_buffer("pe", pe) + + def forward(self, x): + """ + Args: + x: Tensor of shape + (batch_size, sequence_length, embed_dim) + """ + + seq_len = x.size(1) + + x = x + self.pe[:, :seq_len] + + return self.dropout(x) \ No newline at end of file diff --git a/model/transformer.py b/model/transformer.py new file mode 100644 index 0000000..417b749 --- /dev/null +++ b/model/transformer.py @@ -0,0 +1,196 @@ +import torch +import torch.nn as nn + +from config import ( + EMBED_DIM, + NUM_HEADS, + NUM_ENCODER_LAYERS, + NUM_DECODER_LAYERS, + FEED_FORWARD_DIM, + DROPOUT, +) + +from model.positional_encoding import PositionalEncoding + + +class MinecraftTransformer(nn.Module): + """ + Encoder-Decoder Transformer used by the Minecraft Builder AI. + """ + + def __init__( + self, + text_vocab_size, + output_vocab_size, + ): + super().__init__() + + self.embed_dim = EMBED_DIM + + # ---------------------------- + # Embeddings + # ---------------------------- + + self.text_embedding = nn.Embedding( + text_vocab_size, + EMBED_DIM + ) + + self.output_embedding = nn.Embedding( + output_vocab_size, + EMBED_DIM + ) + + # ---------------------------- + # Positional Encoding + # ---------------------------- + + self.text_position = PositionalEncoding( + EMBED_DIM, + DROPOUT + ) + + self.output_position = PositionalEncoding( + EMBED_DIM, + DROPOUT + ) + + # ---------------------------- + # Transformer + # ---------------------------- + + self.transformer = nn.Transformer( + d_model=EMBED_DIM, + nhead=NUM_HEADS, + num_encoder_layers=NUM_ENCODER_LAYERS, + num_decoder_layers=NUM_DECODER_LAYERS, + dim_feedforward=FEED_FORWARD_DIM, + dropout=DROPOUT, + batch_first=True, + ) + + # ---------------------------- + # Output layer + # ---------------------------- + + self.fc_out = nn.Linear( + EMBED_DIM, + output_vocab_size + ) + + # ================================================== + # Masks + # ================================================== + + def generate_square_subsequent_mask(self, size, device): + """ + Prevent the decoder from seeing future tokens. + """ + + return torch.triu( + torch.full( + (size, size), + float("-inf"), + device=device + ), + diagonal=1 + ) + + # ================================================== + # Forward + # ================================================== + + def forward( + self, + src, + tgt, + src_padding_mask=None, + tgt_padding_mask=None, + ): + """ + Parameters + ---------- + src : (batch, src_len) + + tgt : (batch, tgt_len) + + Returns + ------- + logits : (batch, tgt_len, output_vocab_size) + """ + + src = self.text_embedding(src) + tgt = self.output_embedding(tgt) + + src = self.text_position(src) + tgt = self.output_position(tgt) + + tgt_mask = self.generate_square_subsequent_mask( + tgt.size(1), + tgt.device + ) + + output = self.transformer( + src=src, + tgt=tgt, + tgt_mask=tgt_mask, + src_key_padding_mask=src_padding_mask, + tgt_key_padding_mask=tgt_padding_mask, + memory_key_padding_mask=src_padding_mask, + ) + + logits = self.fc_out(output) + + return logits + + # ================================================== + # Encoder + # ================================================== + + def encode( + self, + src, + src_padding_mask=None, + ): + + src = self.text_embedding(src) + src = self.text_position(src) + + memory = self.transformer.encoder( + src, + src_key_padding_mask=src_padding_mask + ) + + return memory + + # ================================================== + # Decoder + # ================================================== + + def decode( + self, + tgt, + memory, + tgt_padding_mask=None, + memory_padding_mask=None, + ): + + tgt = self.output_embedding(tgt) + tgt = self.output_position(tgt) + + tgt_mask = self.generate_square_subsequent_mask( + tgt.size(1), + tgt.device + ) + + output = self.transformer.decoder( + tgt=tgt, + memory=memory, + tgt_mask=tgt_mask, + tgt_key_padding_mask=tgt_padding_mask, + memory_key_padding_mask=memory_padding_mask, + ) + + logits = self.fc_out(output) + + return logits \ No newline at end of file diff --git a/old settings.txt b/old settings.txt new file mode 100644 index 0000000..25f5cdd --- /dev/null +++ b/old settings.txt @@ -0,0 +1,5 @@ +EMBED_DIM = 512 +NUM_HEADS = 8 +NUM_ENCODER_LAYERS = 6 +NUM_DECODER_LAYERS = 6 +FEED_FORWARD_DIM = 2048 \ No newline at end of file diff --git a/prompt b/prompt new file mode 100644 index 0000000..528b606 --- /dev/null +++ b/prompt @@ -0,0 +1,19 @@ +{ + "url": "https://integrate.api.nvidia.com/v1", + "model": "moonshotai/kimi-k3", + "api_key": "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", + "prompt": "Build a massive medieval fantasy castle complex measuring exactly 200 x 200 blocks overall. Use the entire area efficiently and make the castle feel like a complete, fortified royal settlement rather than a single building. CENTER AND MAIN CASTLE: Place the main castle in the center of the 200 x 200 area. Make the main castle large, imposing, symmetrical, and highly detailed, with multiple towers, battlements, stone roofs, balconies, windows, arches, staircases, and decorative stonework. Use a medieval palette of stone bricks, cracked stone bricks, cobblestone, polished stone, dark oak, spruce, and other matching materials. Add variation between blocks so the walls do not look flat or repetitive. Create a grand entrance leading into the main castle. The entrance should have a large fortified gate, heavy wooden doors, stone arches, guard posts, torches, banners, and a wide staircase. THRONE ROOM: Create a huge throne room inside the main castle as the central interior feature. Make it tall, spacious, and luxurious. Place a long red carpet running from the main entrance of the throne room directly to the throne. The red carpet should be clearly visible and bordered by decorative blocks. Place the royal throne at the far end on a raised platform with several steps. Make the throne area grand and elaborate using dark oak, gold-colored blocks, quartz, stone, and decorative blocks. Add banners, pillars, chandeliers, fireplaces, windows, armor stands, and other medieval decorations. Put symmetrical pillars along both sides of the throne room and seating areas for nobles or guards. MAIN CASTLE ROOMS: Include multiple connected rooms throughout the castle, including a grand entrance hall, throne room, royal dining hall, kitchen, storage rooms, armory, library, treasury, bedrooms, guest rooms, guard rooms, meeting room, prison/dungeon, and staircases connecting multiple floors. Make the interiors functional and detailed instead of leaving large empty spaces. TOWERS: Build several large defensive towers around the main castle. Make corner towers especially large and impressive. Each tower should have multiple floors, windows, spiral or internal staircases, battlements, lighting, and rooms for guards or storage. Add taller central towers to make the castle skyline impressive. FARM: Create a large working medieval farm inside the fortified area, positioned toward one side of the castle. Include wheat fields, carrots, potatoes, beetroot, pumpkins, melons, and other crops. Add irrigation channels, water wells, fences, gates, lanterns, barns, animal pens, hay bales, storage sheds, and a farmhouse. Include pens for cows, pigs, sheep, and chickens. Organize the farmland into neat sections while keeping it visually natural. SHOP AND MARKET: Create a detailed medieval shop district inside the walls, preferably near the main entrance so it feels like visitors can access it easily. Include several small shops or stalls selling different goods such as food, weapons, armor, tools, building materials, books, and farming supplies. Use wooden stalls, awnings, signs, barrels, crates, shelves, lanterns, and decorative details. Create a small market square with paths connecting the shops. WALLS AND FORTIFICATIONS: Surround the entire 200 x 200 block area with massive defensive walls. The outer walls should be thick, tall, and heavily fortified, with regular towers and battlements. Add a large main gate with a portcullis, gatehouse, guard rooms, bridges, and defensive platforms. Make the walls visually interesting by using stone brick variations, support pillars, arches, crenellations, windows, arrow slits, banners, torches, and occasional damaged or weathered blocks. Add a secondary gate if space allows. COURTYARD AND PATHS: Create a large courtyard between the main castle, farm, and shops. Connect all major areas with properly designed stone and dirt paths. Add fountains, statues, trees, benches, flower beds, lanterns, barrels, carts, crates, wells, and other medieval decorations. Make the paths logically connect entrances and important buildings. LAYOUT: Keep the entire construction within the exact 200 x 200 block boundary. Leave enough space between major structures so the complex does not feel overcrowded. Make the main castle the dominant centerpiece, with the farm and shops clearly visible but subordinate to it. Maintain a strong medieval architectural style throughout the entire build. DETAIL AND ATMOSPHERE: Add extensive small details everywhere. Use torches, lanterns, banners, flags, chains, trapdoors, fences, barrels, flower pots, bookshelves, armor stands, fireplaces, tables, chairs, crates, hay bales, carts, signs, windows, roof supports, and decorative blocks. Avoid huge blank walls and empty interiors. Make the castle look lived-in, functional, wealthy, fortified, and believable. Use consistent medieval architecture, strong symmetry for the main castle, and natural variation in secondary buildings. The finished result should look like a massive royal Minecraft kingdom contained inside a fully fortified 200 x 200 block castle complex. IMPORTANT BUILD REQUIREMENTS: Overall footprint must be exactly 200 x 200 blocks. Include a massive central main castle. Include a large throne room with a prominent red carpet leading to the throne. Include multiple castle towers and defensive structures. Include a large working farm with crops and animals. Include a medieval shop and market area. Surround the entire complex with fortified outer walls. Include a grand main gate and gatehouse. Connect all areas with paths. Add detailed interiors to important buildings. Use medieval stone, wood, and dark fantasy materials consistently. Maximize detail without exceeding the 200 x 200 boundary. Make the final result look like a complete, impressive royal castle settlement rather than separate disconnected buildings. Go into detail while making this. I want a minimum of 1000 lines of code.", + "context_length": 1000000, + "temperature": 1, + "reasoning": "high" +} + +{ + "url": "https://integrate.api.nvidia.com/v1", + "model": "moonshotai/kimi-k3", + "api_key": "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", + "prompt": "Create a large, highly detailed, realistic medieval-fantasy village that feels genuinely lived-in, functional, and organically developed over many years. The village should be large enough to feel like a small town, with many different districts, buildings, roads, farms, landmarks, hidden areas, and small environmental details. Avoid repetitive buildings and make every area feel unique. OVERALL LAYOUT Build the village around a main central road and village square, with smaller winding roads branching naturally into residential, farming, commercial, industrial, and wealthy areas. The village should not look perfectly planned — roads should curve around buildings, hills, trees, streams, and older structures. Place the village in a lush valley surrounded by rolling hills and dense forests, with a small river or stream running along one side. Include wooden bridges crossing the water, dirt paths leading into the countryside, and distant hills visible beyond the village. Use a mixture of: Timber-frame houses Stone houses Wooden cottages Small farmhouses Larger manor-style homes Shops and workshops Barns and storage buildings Thatched and wooden roofs Stone foundations Weathered fences and gates Buildings should have different sizes, shapes, heights, roof designs, materials, and levels of wealth. Some should look old and repaired several times, while others should be newer and better maintained. CENTRAL VILLAGE SQUARE Create a large, busy central village square. In the center place: A large stone fountain or old village well Wooden market stall Fruit and vegetable stands Bread and pastry stalls A butcher's stall A flower seller A traveling merchant's wagon Barrels and crates Benches Wooden signs Notice boards covered with papers Hanging lanterns Small trees and flower beds Surround the square with: A large village tavern Bakery General store Blacksmith Tailor Apothecary Carpenter Small inn Stable Merchant houses Make the square look like the social heart of the village, with plenty of small details suggesting that villagers gather here every day. TAVERN AND INN Create a large two-story village tavern with a sign hanging outside. Include: Outdoor wooden tables Barrels stacked beside the entrance Firewood piles A kitchen chimney producing smoke Lanterns Flower boxes A small stable behind it An upstairs living area A cellar underneath A secret storage room A back entrance leading into an alley Nearby, create a cozy traveler's inn with several rooms, a stable yard, carriage parking, and a small garden. RESIDENTIAL DISTRICT Create a large residential area containing dozens of houses, but make them visually different. Include: Small cottages for poorer villagers Medium-sized family homes Larger homes for merchants Homes with vegetable gardens Houses with chicken coops Homes with balconies Houses with porches Children's play areas Clotheslines with hanging laundry Wood piles Wells Dog houses Small sheds Fences Garden gates Flower pots Firewood racks Tool sheds Some houses should have narrow alleys between them, while others have larger yards. Add tiny signs of life everywhere: buckets, baskets, carts, shoes outside doors, tools leaning against walls, chopped wood, stacked hay, open windows, curtains, flower boxes, and repaired sections of walls. FARMING DISTRICT Surround part of the village with a huge agricultural area. Create: Large wheat fields Vegetable gardens Potato fields Pumpkin patches Herb gardens Fruit orchards Apple trees Berry bushes Beehives Irrigation channels Dirt farm paths Stone walls Wooden fences Add several farmhouses with: Large barns Hay storage Animal pens Chicken coops Pig pens Cow sheds Horse paddocks Tool sheds Grain storage Include farmers' carts traveling between the fields and village. ANIMAL AREA Create a dedicated livestock area with: Horses Cows Sheep Goats Pigs Chickens Include a large stable, fenced pastures, feeding troughs, hay piles, water troughs, and muddy areas around the animal pens. BLACKSMITH AND INDUSTRIAL AREA Create a working blacksmith forge near the edge of the village. Include: Large stone forge Chimney Anvil Hammering area Water trough Weapon racks Horseshoes Metal scraps Coal storage Firewood Barrels Covered outdoor workspace Nearby create workshops for: Carpenter Wheelwright Mason Tanner Potter Weaver Cooper Leatherworker Give every workshop its own tools, materials, worktables, storage, and signs. RIVER DISTRICT Have a river running along one side of the village. Build: A large wooden bridge A smaller stone footbridge Fishing docks Small fishing boats A riverside warehouse Fishermen's cottages Wooden piers Fishing nets Barrels Crates Fish drying racks Water wheels Add a large wooden watermill beside the river with a turning wheel and mill house. Create a small riverside market selling fish, boats, rope, nets, and other goods. CHURCH / TEMPLE Create an impressive old stone village church or fantasy temple on a slightly raised hill. Include: Stone bell tower Large wooden doors Stained-glass windows Graveyard Stone tombstones Ancient trees Small chapel Garden Memorial statues Candles Stone walls Add a winding staircase leading from the village square toward the church. Make the church look old and important, with architecture that makes it one of the village's major landmarks. VILLAGE ELDER / MANOR Create a larger village elder's manor on a hill overlooking the settlement. Include: Stone-and-timber architecture Small courtyard Garden Stable Guardhouse Storage building Meeting hall Private garden Stone walls Decorative banners Add a large oak tree beside the manor with a bench underneath it. GUARD AND DEFENSE Even though it is primarily a village, give it a modest defensive system. Create: Wooden palisade sections Stone walls in important areas Two wooden watchtowers Main entrance gate Smaller side gate Guard barracks Equipment storage Archery targets Training yard Weapon racks The defenses should look practical rather than like a giant castle. FOREST EDGE At the edge of the village, create a dense forest. Add: Hunting trails Fallen trees Mushrooms Wildflowers Berry bushes Deer Rabbits Birds Wooden hunting stands A small hunter's cabin Traps Firewood camps Hidden paths Create a mysterious ancient stone circle deep in the forest, partially covered in moss and surrounded by enormous old trees. SECRET AREAS Add several hidden locations that reward exploration. Include: A hidden underground cellar beneath an abandoned house A forgotten tunnel underneath part of the village A smugglers' hideout near the river A locked abandoned watchtower A hidden forest shrine A cave behind a waterfall An overgrown ruined building A secret passage connecting two older buildings Do not make these obvious from the main roads. They should feel like discoveries. ROADS AND PATHS Create a detailed network of: Main cobblestone roads Smaller dirt roads Narrow alleys Farm paths Forest trails Stone stairways Wooden walkways Bridges The roads should show signs of use: muddy sections, wagon wheel tracks, worn stones, puddles, footprints, scattered straw, and patches of grass growing between stones. VILLAGE LANDMARKS Add several memorable landmarks: Giant ancient oak tree Village fountain Old stone bridge Watermill Bell tower Market square Windmill on a nearby hill Old abandoned tower Large village gate Ancient statue Small waterfall Stone shrine SMALL DETAILS EVERYWHERE Fill the village with environmental storytelling. Add: Cats sleeping on roofs Dogs wandering around Chickens crossing roads Birds on rooftops Villagers' carts Wheelbarrows Barrels Crates Baskets Hay bales Firewood Tools Rope Lanterns Signs Benches Wells Flower pots Laundry Market awnings Broken fences Moss-covered stones Ivy-covered walls Puddles Mud Fallen leaves Wild grass Small gardens Birdhouses Include subtle imperfections: crooked fences, uneven roofs, patched walls, repaired roads, mismatched building materials, old foundations, leaning sheds, and buildings that have clearly been expanded over time. ATMOSPHERE The village should feel alive and inhabited, not like an empty architectural showcase. Create a sense of different social classes and occupations. The wealthier buildings should be closer to the central square, while poorer cottages and working areas should be toward the outskirts. Make the village visually interesting from every direction. Include elevation changes, hills, terraces, bridges, winding streets, and buildings partially hidden behind others. The final result should feel like a village that has grown organically over generations, with history visible in its architecture. FINAL REQUIREMENT Make the village very large, dense, detailed, and explorable, with many distinct areas and hundreds of small environmental details. Avoid repetitive houses, identical roofs, perfectly straight roads, empty spaces, or artificial-looking layouts. Every time the viewer turns a corner, there should be something interesting to discover. The overall feeling should be cozy, prosperous, adventurous, mysterious, lived-in, and believable, like a major location from a high-quality open-world fantasy game. Every building needs to be furnished. Go into detail while building this.", + "context_length": 1000000, + "temperature": 1, + "reasoning": "high" +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..492f6b4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "minebuildai" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "fastapi>=0.141.1", + "mcschematic>=11.4.4", + "noise>=1.2.2", + "numpy>=2.5.1", + "pydantic>=2.13.4", + "requests>=2.34.2", + "torch>=2.13.0", +] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a7a9af0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,21 @@ +# Numerical computing +numpy>=2.3.0 + +# Progress bars +tqdm>=4.67.0 + +# Visualization (optional, useful during training) +matplotlib>=3.10.0 + +# TensorBoard logging (optional) +tensorboard>=2.20.0 + +# API Server +fastapi>=0.139.0 +uvicorn>=0.35.0 + +# Data validation (FastAPI dependency, included automatically but pinned here) +pydantic>=2.12.0 + +# MCSchematic +mcschematic>=11.4.4 \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..e3ac384 --- /dev/null +++ b/server.py @@ -0,0 +1,473 @@ +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) + } \ No newline at end of file diff --git a/train.py b/train.py new file mode 100644 index 0000000..994653f --- /dev/null +++ b/train.py @@ -0,0 +1,156 @@ +import torch +import torch.nn as nn +import torch.optim as optim + +from config import ( + DATASET_PATH, + BATCH_SIZE, + LEARNING_RATE, + EPOCHS, + DEVICE, + CHECKPOINT_DIR, +) + +from data.tokenizer import Tokenizer +from data.dataset import create_dataloader + +from model.builder_model import BuilderModel + +from utils.checkpoint import ( + save_checkpoint, + latest_checkpoint, + load_checkpoint, +) + +def format_params(n): + if n >= 1_000_000_000: + return f"{n:,} ({n/1_000_000_000:.2f}B)" + elif n >= 1_000_000: + return f"{n:,} ({n/1_000_000:.2f}M)" + elif n >= 1_000: + return f"{n:,} ({n/1_000:.2f}K)" + return str(n) + +def train(): + + print("=" * 60) + print("Building tokenizer...") + print("=" * 60) + + tokenizer = Tokenizer() + tokenizer.build(DATASET_PATH) + tokenizer.save("data") + + print("Prompt vocabulary :", tokenizer.text_vocab_size) + print("Output vocabulary :", tokenizer.output_vocab_size) + + print("=" * 60) + print("Loading dataset...") + print("=" * 60) + + dataloader = create_dataloader( + DATASET_PATH, + tokenizer, + BATCH_SIZE, + ) + + print("Creating model...") + + model = BuilderModel(tokenizer).to(DEVICE) + + optimizer = optim.AdamW( + model.parameters(), + lr=LEARNING_RATE, + ) + + criterion = nn.CrossEntropyLoss( + ignore_index=tokenizer.output_to_id[""] + ) + + start_epoch = 1 + + checkpoint = latest_checkpoint(CHECKPOINT_DIR) + + if checkpoint: + + print("Loading checkpoint:", checkpoint) + + start_epoch, _ = load_checkpoint( + checkpoint, + model, + optimizer, + DEVICE, + ) + + start_epoch += 1 + + print("=" * 60) + print("Training") + print("=" * 60) + + for epoch in range(start_epoch, EPOCHS + 1): + + model.train() + + total_loss = 0 + + for prompts, outputs in dataloader: + + prompts = prompts.to(DEVICE) + outputs = outputs.to(DEVICE) + + decoder_input = outputs[:, :-1] + targets = outputs[:, 1:] + + optimizer.zero_grad() + + logits = model( + prompts, + decoder_input, + ) + + loss = criterion( + logits.reshape(-1, logits.size(-1)), + targets.reshape(-1), + ) + + loss.backward() + + torch.nn.utils.clip_grad_norm_( + model.parameters(), + 1.0, + ) + + optimizer.step() + + total_loss += loss.item() + + avg_loss = total_loss / len(dataloader) + + print( + f"Epoch {epoch}/{EPOCHS} | Loss: {avg_loss:.4f}" + ) + + print("\nTraining finished!") + + total_params = sum(p.numel() for p in model.parameters()) + trainable_params = sum( + p.numel() for p in model.parameters() if p.requires_grad + ) + + print("=" * 60) + print(f"Total parameters: {format_params(total_params)}") + print(f"Trainable parameters: {format_params(trainable_params)}") + print("=" * 60) + + save_checkpoint( + model, + optimizer, + EPOCHS, + avg_loss, + CHECKPOINT_DIR / "final_model.pth", + ) + + +if __name__ == "__main__": + train() \ No newline at end of file diff --git a/utils/checkpoint.py b/utils/checkpoint.py new file mode 100644 index 0000000..0b3c383 --- /dev/null +++ b/utils/checkpoint.py @@ -0,0 +1,85 @@ +import os +import torch + + +def save_checkpoint( + model, + optimizer, + epoch, + loss, + path, +): + """ + Save a training checkpoint. + """ + + os.makedirs(os.path.dirname(path), exist_ok=True) + + torch.save( + { + "epoch": epoch, + "model_state_dict": model.state_dict(), + "optimizer_state_dict": optimizer.state_dict(), + "loss": loss, + }, + path, + ) + + +def load_checkpoint( + path, + model, + optimizer=None, + map_location="cpu", +): + """ + Load a training checkpoint. + + Returns: + epoch (int) + loss (float) + """ + + checkpoint = torch.load( + path, + map_location=map_location, + ) + + model.load_state_dict( + checkpoint["model_state_dict"] + ) + + if optimizer is not None: + optimizer.load_state_dict( + checkpoint["optimizer_state_dict"] + ) + + return ( + checkpoint["epoch"], + checkpoint["loss"], + ) + + +def latest_checkpoint(folder): + """ + Returns the newest checkpoint file in a folder. + """ + + if not os.path.exists(folder): + return None + + checkpoints = [ + os.path.join(folder, file) + for file in os.listdir(folder) + if file.endswith(".pth") + ] + + if len(checkpoints) == 0: + return None + + checkpoints.sort( + key=os.path.getmtime, + reverse=True, + ) + + return checkpoints[0] \ No newline at end of file