diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..f543f2f3f9cf6b30147cbb06fad0d5e4b1b938bf 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ +<<<<<<< HEAD *.7z filter=lfs diff=lfs merge=lfs -text *.arrow filter=lfs diff=lfs merge=lfs -text *.bin filter=lfs diff=lfs merge=lfs -text @@ -33,3 +34,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +======= +*.png filter=lfs diff=lfs merge=lfs -text +>>>>>>> master diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..4ffe2dda7713773dd8cd2a583cd7758c94f9151b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +output/ +image_temp/ +MerchantBot CLI/ +seed_images diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..ed59f2fd85bc5cf4a88789f934e2a7e1b296e381 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# Stage 1: Build Cuda toolkit +FROM drakosfire/cuda-base:latest as base-layer + +# Llama.cpp requires the ENV variable be set to signal the CUDA build and be built with the CMAKE variables from pip for python use +ENV LLAMA_CUBLAS=1 +RUN apt-get update && \ + apt-get install -y python3 python3-pip python3-venv && \ + pip install gradio && \ + CMAKE_ARGS="-DLLAMA_CUBLAS=on" pip install llama-cpp-python && \ + pip install pillow && \ + pip install diffusers && \ + pip install accelerate && \ + pip install transformers && \ + pip install peft + +FROM base-layer as final-layer + +RUN useradd -m -u 1000 user + + # mkdir -p /home/user/.cache && \ + # chmod 777 /home/user/.cache && \ + # chown -R user:user /home/user/app/ +# Set environment variables for copied builds of cuda and flash-attn in /venv + + +ENV PATH=/usr/local/cuda-12.4/bin:/venv/bin:${PATH} +ENV LD_LIBRARY_PATH=/usr/local/cuda-12.4/lib64:${LD_LIBRARY_PATH} + +ENV VIRTUAL_ENV=/venv +RUN python3 -m venv $VIRTUAL_ENV +ENV PATH="$VIRTUAL_ENV/bin:$PATH" +# Set working directory and user +WORKDIR /home/user/app + +USER user + +# Set the entrypoint +EXPOSE 8000 + +ENTRYPOINT ["python", "main.py"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..bb4e4750b26d03eb3f8ad900b89504dd897ad752 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Drakosfire + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MerchanBot CLI/LICENSE b/MerchanBot CLI/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..bb4e4750b26d03eb3f8ad900b89504dd897ad752 --- /dev/null +++ b/MerchanBot CLI/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Drakosfire + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MerchanBot CLI/README.md b/MerchanBot CLI/README.md new file mode 100644 index 0000000000000000000000000000000000000000..fe0c1e099a34c6efb2b3c858997b325c945bac59 --- /dev/null +++ b/MerchanBot CLI/README.md @@ -0,0 +1,2 @@ +# CardGenerator +Takes user input and generates a collectible card with custom or LLM generated text and image generation diff --git a/MerchanBot CLI/card_generator.py b/MerchanBot CLI/card_generator.py new file mode 100755 index 0000000000000000000000000000000000000000..1bcb201a584d7c19d4625558c73dd38fa3b6c966 --- /dev/null +++ b/MerchanBot CLI/card_generator.py @@ -0,0 +1,109 @@ +import render_card_text as rend +import inventory as inv +from PIL import Image +import utilities as u +import os +from PIL import ImageFilter + + +def save_image(image,item_key): + image.save(f"{item_key['Name']}.png") + + +# Import Inventory +#shop_inventory = inv.inventory +#purchased_item_key = shop_inventory['Shortsword'] +#border_path = './card_templates/Shining Sunset Border.png' + +sticker_path_dictionary = {'Default': './card_parts/Sizzek Sticker.png','Common': './card_parts/Common.png', 'Uncommon': './card_parts/Uncommon.png','Rare': './card_parts/Rare.png','Very Rare':'./card_parts/Very Rare.png','Legendary':'./card_parts/Legendary.png'} +blank_overlay_path = "./card_parts/white-fill-title-detail-value-transparent.png" +value_overlay_path = "./card_parts/Value_box_transparent.png" +test_item = {'Name': 'Pustulent Raspberry', 'Type': 'Fruit', 'Value': '1 cp', 'Properties': ['Unusual Appearance', 'Rare Taste'], 'Weight': '0.2 lb', 'Description': 'This small fruit has a pustulent appearance, with bumps and irregular shapes covering its surface. Its vibrant colors and strange texture make it an oddity among other fruits.', 'Quote': 'A fruit that defies expectations, as sweet and sour as life itself.', 'SD Prompt': 'A small fruit with vibrant colors and irregular shapes, bumps covering its surface.'} + +# Function that takes in an image path and a dictionary and uses the values to print onto a card. +def paste_image_and_resize(base_image,sticker_path, x_position, y_position,img_width, img_height, purchased_item_key = None): + # Load the image to paste + + if purchased_item_key: + if sticker_path[purchased_item_key]: + sticker_path = sticker_path[purchased_item_key] + else: sticker_path = sticker_path['Default'] + + + image_to_paste = Image.open(sticker_path) + + # Define the new size (scale) for the image you're pasting + + new_size = (img_width, img_height) + + # Resize the image to the new size + image_to_paste_resized = image_to_paste.resize(new_size) + + # Specify the top-left corner where the resized image will be pasted + paste_position = (x_position, y_position) # Replace x and y with the coordinates + + # Paste the resized image onto the base image + base_image.paste(image_to_paste_resized, paste_position, image_to_paste_resized) + +def render_text_on_card(image_path, purchased_item_key) : + # Card Properties + print(list(purchased_item_key.keys())) + output_image_path = f"./{purchased_item_key['Name']}.png" + print(f"Saving image to {output_image_path}") + font_path = "./fonts/Balgruf.ttf" + italics_font_path = './fonts/BalgrufItalic.ttf' + initial_font_size = 50 + + # Title Properties + title_center_position = (395, 55) + title_area_width = 600 # Maximum width of the text box + title_area_height = 60 # Maximum height of the text box + + # Type box properties + type_center_position = (384, 540) + type_area_width = 600 + type_area_height = 50 + type_text = purchased_item_key['Type'] + if 'Weight' in list(purchased_item_key.keys()) : + type_text = type_text + ' '+ purchased_item_key['Weight'] + + if 'Damage' in list(purchased_item_key.keys()) : + type_text = type_text + ' '+ purchased_item_key['Damage'] + + # Description box properties + description_position = (105, 630) + description_area_width = 590 + description_area_height = 215 + + # Value box properties (This is good, do not change unless underlying textbox layout is changing) + value_position = (660,905) + value_area_width = 125 + value_area_height = 50 + + # Quote test properties + quote_position = (110,885) + quote_area_width = 470 + quote_area_height = 60 + + # open image and render text + image = Image.open(image_path) + #paste_image_and_resize(image, blank_overlay_path,x_position= 0,y_position=0, img_width= 768, img_height= 1024) + image = rend.render_text_with_dynamic_spacing(image, purchased_item_key['Name'], title_center_position, title_area_width, title_area_height,font_path,initial_font_size) + + image = rend.render_text_with_dynamic_spacing(image,type_text , type_center_position, type_area_width, type_area_height,font_path,initial_font_size) + image = rend.render_text_with_dynamic_spacing(image, '', description_position, description_area_width, description_area_height,font_path,initial_font_size,purchased_item_key, description = True) + paste_image_and_resize(image, value_overlay_path,x_position= 0,y_position=0, img_width= 768, img_height= 1024) + image = rend.render_text_with_dynamic_spacing(image, purchased_item_key['Value'], value_position, value_area_width, value_area_height,font_path,initial_font_size) + image = rend.render_text_with_dynamic_spacing(image, purchased_item_key['Quote'], quote_position, quote_area_width, quote_area_height,italics_font_path,initial_font_size, quote = True) + #Paste Sizzek Sticker + paste_image_and_resize(image, sticker_path_dictionary,x_position= 0,y_position=909, img_width= 115, img_height= 115, purchased_item_key= purchased_item_key['Rarity']) + #save_image(image, purchased_item_key) + image = image.filter(ImageFilter.GaussianBlur(.5)) + image = image.save(f"./output/{purchased_item_key['Name']}.png") + + +#render_text_on_card('./card_templates/Shining Sunset Border.png',test_item ) + + + + diff --git a/MerchanBot CLI/image_gen.py b/MerchanBot CLI/image_gen.py new file mode 100755 index 0000000000000000000000000000000000000000..78890b50dd2f1cbf13761d519f6ee59e90f19ef7 --- /dev/null +++ b/MerchanBot CLI/image_gen.py @@ -0,0 +1,50 @@ +#This independent from streamlit runs full speed ~ 5it/s /w StableDiffusionXLPipeline +from diffusers import StableDiffusionXLPipeline, StableDiffusionPipeline +import torch +import time +import inventory as inv +import utilities as u + +start_time = time.time() +card_pre_prompt = " blank magic card,high resolution, detailed high quality intricate border, decorated textbox, high quality magnum opus cgi drawing of" +torch.backends.cuda.matmul.allow_tf32 = True +image_list = [] +item = inv.inventory['Shortsword'] +def generate_image(num_img, prompt, item) : + prompt = card_pre_prompt + prompt + print(prompt) + model_path = ("../models/stable-diffusion/SDXLFaetastic_v20.safetensors") + lora_path = ("../models/stable-diffusion/Loras/blank-card-template.safetensors") + pipe = StableDiffusionXLPipeline.from_single_file(model_path, + custom_pipeline="low_stable_diffusion", + torch_dtype=torch.float16, + variant="fp16" ).to("cuda") + pipe.load_lora_weights(lora_path) + pipe.enable_vae_slicing() + + + for x in range(num_img): + img_start = time.time() + image = pipe(prompt=prompt,num_inference_steps=50, height = 1024, width = 768).images[0] + image = image.save(str(x) + f"{item}.png") + img_time = time.time() - img_start + img_its = 50/img_time + print(f"image gen time = {img_time} and {img_its} it/s") + print(f"Memory after image {x} = {torch.cuda.memory_allocated()}") + image_path = str(os.path.abspath(image)) + # image_list.append(image_path) + del image + del pipe + u.reclaim_mem() + + print(f"Memory after del {torch.cuda.memory_allocated()}") + print(image_list) + total_time = time.time() - start_time + + print(f"Total Time to generate{x} images = {total_time} ") + return image_path + + + + + \ No newline at end of file diff --git a/MerchanBot CLI/img2img.py b/MerchanBot CLI/img2img.py new file mode 100755 index 0000000000000000000000000000000000000000..94265f9d669942fea3b899f2227aa1713dd6a771 --- /dev/null +++ b/MerchanBot CLI/img2img.py @@ -0,0 +1,77 @@ +from diffusers import StableDiffusionXLPipeline, StableDiffusionXLImg2ImgPipeline +from diffusers.utils import load_image +import torch +import time +import utilities as u +import card_generator as card +from PIL import Image + +start_time = time.time() +torch.backends.cuda.matmul.allow_tf32 = True +model_path = ("../../models/stable-diffusion/SDXLFaetastic_v24.safetensors") +lora_path = "../../models/stable-diffusion/Loras/blank-card-template-5.safetensors" +detail_lora_path = "../../models/stable-diffusion/Loras/add-detail-xl.safetensors" +mimic_lora_path = "../../models/stable-diffusion/Loras/EnvyMimicXL01.safetensors" + +card_pre_prompt = " blank magic card,high resolution, detailed intricate high quality border, textbox, high quality magnum opus drawing of a " +negative_prompts = "text, words, numbers, letters" +image_list = [] + +def generate_image(num_img, prompt, item, user_input_template, mimic = None) : + prompt = card_pre_prompt + item + ' ' + prompt + print(prompt) + image_path = f"card_templates/{user_input_template}" + init_image = load_image(image_path).convert("RGB") + + pipe = StableDiffusionXLImg2ImgPipeline.from_single_file(model_path, + custom_pipeline="low_stable_diffusion", + torch_dtype=torch.float16, + variant="fp16", + local_files_only = True).to("cuda") + # Load LoRAs for controlling image + pipe.load_lora_weights(lora_path, weight_name = "blank-card-template-5.safetensors",adapter_name = 'blank-card-template') + pipe.load_lora_weights(detail_lora_path, weight_name = "add-detail-xl.safetensors", adapter_name = "add-detail-xl") + + # If mimic keyword has been detected, load the mimic LoRA and set adapter values + if mimic: + pipe.load_lora_weights(mimic_lora_path, weight_name = "EnvyMimicXL01.safetensors", adapter_name = "EnvyMimicXL") + pipe.set_adapters(['blank-card-template', "add-detail-xl", "EnvyMimicXL"], adapter_weights = [0.9,0.9,1.0]) + else : + pipe.set_adapters(['blank-card-template', "add-detail-xl"], adapter_weights = [0.9,0.9]) + pipe.enable_vae_slicing() + + for x in range(num_img): + img_start = time.time() + image = pipe(prompt=prompt, + strength = .9, + guidance_scale = 5, + image= init_image, + negative_promt = negative_prompts, + num_inference_steps=50, + height = 1024, width = 768).images[0] + image = image.save(str(x) + f"{item}.png") + output_image_path = str(x) + f"{item}.png" + img_time = time.time() - img_start + img_its = 50/img_time + print(f"image gen time = {img_time} and {img_its} it/s") + print(f"Memory after image {x} = {torch.cuda.memory_allocated()}") + + image_list.append(output_image_path) + + # Delete the image variable to keep VRAM open to load the LLM + del image + print(f"Memory after del {torch.cuda.memory_allocated()}") + print(image_list) + total_time = time.time() - start_time + + print(f"Total Time to generate{x} images = {total_time} ") + del pipe + u.reclaim_mem() + return image_list + + + + + + + \ No newline at end of file diff --git a/MerchanBot CLI/inventory.py b/MerchanBot CLI/inventory.py new file mode 100755 index 0000000000000000000000000000000000000000..15f077ae3481948e52a177c13b39eee1a4d03d58 --- /dev/null +++ b/MerchanBot CLI/inventory.py @@ -0,0 +1,52 @@ + + +inventory = { + 'Shortsword': { + 'Name' : 'Shortsword', + 'Type' : 'Melee Weapon (martial, sword)', + 'Value': '10 gp', + 'Properties': ['Finesse, Light '], + 'Damage': '1d6 + proficiency + Dex or Str', + 'Weight': '2 lb', + 'Description': 'Gleaming with a modest radiance, the shortsword boasts a keen edge and a leather-wrapped hilt, promising both grace and reliability in the heat of combat.', + 'Quote': 'In the heart of battle, the shortsword proves not just a weapon, but a steadfast companion, whispering paths of valor to those who wield it.', + 'SD Description' : 'high resolution, blank magic card,detailed high quality intricate border, decorated textbox, high quality magnum opus cgi drawing of a steel shortsword' + }, + + 'Health Potion': { + 'Name' : 'Health Portion', + 'Type' : 'Potion', + 'Value': '50 gp', + 'Properties': ['Quafable', 'Restores 1d4 + 2 HP upon consumption'], + 'Weight': '0.5 lb', + 'Description': 'Contained within this small vial is a crimson liquid that sparkles when shaken, a life-saving elixir for those who brave the unknown.', + 'Quote': 'To the weary, a drop of hope; to the fallen, a chance to stand once more.' + }, + + + 'Wooden Shield': { + 'Name' : 'Wooden Shield', + 'Type' : 'Armor, Shield', + 'Value': '15 gp', + 'Properties': ['+2 AC'], + 'Weight': '6 lb', + 'Description': 'Sturdy and reliable, this wooden shield is a simple yet effective defense against the blows of adversaries.', + 'Quote': 'In the rhythm of battle, it dances - a barrier between life and defeat.' + }, + + 'Magical Helmet': { + 'Name' : 'Magical Helmet of Perception', + 'Type' : 'Magical Item (armor, helmet)', + 'Value': '120 gp', + 'Properties': ['+ 1 to AC', 'Grants the wearer enhanced perception'], + 'Weight': '3 lb', + 'Description': 'Forged from mystic metals and enchanted with ancient spells, this helmet offers protection beyond the physical realm.', + 'Quote': 'A crown not of royalty, but of unyielding vigilance, warding off the unseen threats that lurk in the shadows.' + } + } + +{'id': 'cmpl-5b0ed6c7-2326-473f-8f11-32d3f079edc2', + 'object': 'text_completion', + 'created': 1709094107, + 'model': '../models/starling-lm-7b-alpha.Q8_0.gguf', + 'choices': [{'text': ' Here\'s an example of a structured inventory entry for a Mimic Treasure Chest as per your request:\n\n```python\n{\n \'Mimic Treasure Chest\': {\n \'Name\': \'Mimic Treasure Chest\',\n \'Type\': \'Trap\',\n \'Rarity\': \'Rare\',\n \'Value\': \'1000 gp\', \n \'Properties\': [\n \'Deceptively inviting\', \n \'Springs to life when interacted with\', \n \'Capable of attacking unwary adventurers\'\n ],\n \'Weight\': \'50 lb\', \n \'Description\': \'At first glance, this chest appears to be laden with treasure, beckoning to all who gaze upon it. However, it harbors a deadly secret: it is a Mimic, a cunning and dangerous creature that preys on the greed of adventurers. With its dark magic, it can perfectly imitate a treasure chest, only to reveal its true, monstrous form when approached. Those who seek to plunder its contents might find themselves in a fight for their lives.\',\n \'Quote\': \'"Beneath the guise of gold and riches lies a predator, waiting with bated breath for its next victim."\',\n \'SD Prompt\': \'A seemingly ordinary treasure chest that glimmers with promise. Upon closer inspection, sinister, almost living edges move with malice, revealing its true nature as a Mimic, ready to unleash fury on the unwary.\'\n }\n}\n```\n\nKeep in mind that mimics are typically found in dungeons and are known to take on the form of doors and chests. This example follows that theme while also providing information on the mimic\'s rarity, value, properties, and weight.', 'index': 0, 'logprobs': None, 'finish_reason': 'stop'}], 'usage': {'prompt_tokens': 4287, 'completion_tokens': 405, 'total_tokens': 4692}} \ No newline at end of file diff --git a/MerchanBot CLI/item_dict_gen.py b/MerchanBot CLI/item_dict_gen.py new file mode 100755 index 0000000000000000000000000000000000000000..9dc0f1d2c8450f872310190a4ace4086f25511fc --- /dev/null +++ b/MerchanBot CLI/item_dict_gen.py @@ -0,0 +1,329 @@ +from llama_cpp import Llama +import ast +import gc +import torch + +model_path = "../models/starling-lm-7b-alpha.Q8_0.gguf" +# Set gpu_layers to the number of layers to offload to GPU. Set to 0 if no GPU acceleration is available on your system. + + +# Simple inference example +def load_llm(user_input): + llm = Llama( + model_path=model_path, + n_ctx=8192, # The max sequence length to use - note that longer sequence lengths require much more resources + n_threads=8, # The number of CPU threads to use, tailor to your system and the resulting performance + n_gpu_layers=-1 # The number of layers to offload to GPU, if you have GPU acceleration available +) + return llm( + f"GPT4 User: {prompt_instructions} the item is {user_input}: <|end_of_turn|>GPT4 Assistant:", # Prompt + max_tokens=512, # Generate up to 512 tokens + stop=[""], # Example stop token - not necessarily correct for this specific model! Please check before using. + echo=False # Whether to echo the prompt + ) + +def call_llm_and_cleanup(user_input): + # Call the LLM and store its output + llm_output = load_llm(user_input) + print(llm_output['choices'][0]['text']) + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() # Clear VRAM allocated by PyTorch + + # llm_output is still available for use here + + return llm_output + +def convert_to_dict(string): + # Evaluate if string is dictionary literal + try: + result = ast.literal_eval(string) + if isinstance(result, dict): + print("Item dictionary is valid") + return result + # If not, modify by attempting to add brackets to where they tend to fail to generate. + else: + modified_string = '{' + string + if isinstance(modified_string, dict): + return modified_string + modified_string = string + '}' + if isinstance(modified_string, dict): + return modified_string + modified_string = '{' + string + '}' + if isinstance(modified_string, dict): + return modified_string + except (ValueError, SyntaxError) : + print("Dictionary not valid") + return None + + +# Instructions past 4 are not time tested and may need to be removed. +### Meta prompted : +prompt_instructions = """ **Purpose**: Generate a structured inventory entry for a specific item as a hashmap. Follow the format provided in the examples below. + +**Instructions**: +1. Replace `{item}` with the name of your item, enclosed in single quotes (e.g., `'Magic Wand'`). +2. Ensure your request is formatted as a hashmap. Do not add quotation marks around the dictionary's `Quote` value. +3. The quote should be strange and interesting and from the perspective of someone commenting on the impact of the {item} on their life +4. Value should be assigned as an integer of copper pieces (cp), silver pieces (sp), electrum pieces (ep), gold pieces (gp), and platinum pieces (pp). . +5. Use this table for reference on value : +1 cp 1 lb. of wheat +2 cp 1 lb. of flour or one chicken +5 cp 1 lb. of salt +1 sp 1 lb. of iron or 1 sq. yd. of canvas +5 sp 1 lb. of copper or 1 sq. yd. of cotton cloth +1 gp 1 lb. of ginger or one goat +2 gp 1 lb. of cinnamon or pepper, or one sheep +3 gp 1 lb. of cloves or one pig +5 gp 1 lb. of silver or 1 sq. yd. of linen +10 gp 1 sq. yd. of silk or one cow +15 gp 1 lb. of saffron or one ox +50 gp 1 lb. of gold +500 gp 1 lb. of platinum + +6. Examples of Magical Scroll Value: + Common: 50-100 gp + Uncommon: 101-500 gp + Rare: 501-5000 gp + Very rare: 5001-50000 gp + Legendary: 50001+ gp +A scroll's rarity depends on the spell's level: + Cantrip-1: Common + 2-3: Uncommon + 4-5: Rare + 6-8: Very rare + 9: Legendary + +7. Explanation of Mimics: +Mimics are shapeshifting predators able to take on the form of inanimate objects to lure creatures to their doom. In dungeons, these cunning creatures most often take the form of doors and chests, having learned that such forms attract a steady stream of prey. +Imitative Predators. Mimics can alter their outward texture to resemble wood, stone, and other basic materials, and they have evolved to assume the appearance of objects that other creatures are likely to come into contact with. A mimic in its altered form is nearly unrecognizable until potential prey blunders into its reach, whereupon the monster sprouts pseudopods and attacks. +When it changes shape, a mimic excretes an adhesive that helps it seize prey and weapons that touch it. The adhesive is absorbed when the mimic assumes its amorphous form and on parts the mimic uses to move itself. +Cunning Hunters. Mimics live and hunt alone, though they occasionally share their feeding grounds with other creatures. Although most mimics have only predatory intelligence, a rare few evolve greater cunning and the ability to carry on simple conversations in Common or Undercommon. Such mimics might allow safe passage through their domains or provide useful information in exchange for food. + +8. +**Format Example**: +- **Dictionary Structure**: + + {'{item}': { + 'Name': '{item name}', + 'Type': '{item type}', + 'Rarity': '{item rarity}, + 'Value': '{item value}', + 'Properties': ['{property1}', '{property2}', ...], + 'Damage': '{damage formula} , 'damage type}', + 'Weight': '{weight}', + 'Description': '{item description}', + 'Quote': '{item quote}', + 'SD Prompt': '{special description for the item}' + } } + +- **Input Placeholder**: + - `{item}`: Replace with the item name, ensuring it's wrapped in single quotes. + +**Output Examples**: +1. Cloak of Whispering Shadows Entry: + + {'Cloak of Whispering Shadows': { + 'Name': 'Cloak of Whispering Shadows', + 'Type': 'Cloak', + 'Rarity': 'Very Rare', + 'Value': '10000 gp', + 'Properties': ['Grants invisibility in dim light or darkness','Allows communication with shadows for gathering information'], + 'Weight': '1 lb', + 'Description': 'A cloak woven from the essence of twilight, blending its wearer into the shadows. Whispers of the past and present linger in its folds, offering secrets to those who listen.', + 'Quote': 'In the embrace of night, secrets surface in the silent whispers of the dark.', + 'SD Prompt': ' decorated with shimmering threads that catch the light to mimic stars.' + } } + +2. Health Potion Entry: + + {'Health Potion': { + 'Name' : 'Health Portion', + 'Type' : 'Potion', + 'Rarity' : 'Common', + 'Value': '50 gp', + 'Properties': ['Quafable', 'Restores 1d4 + 2 HP upon consumption'], + 'Weight': '0.5 lb', + 'Description': 'Contained within this small vial is a crimson liquid that sparkles when shaken, a life-saving elixir for those who brave the unknown.', + 'Quote': 'To the weary, a drop of hope; to the fallen, a chance to stand once more.', + 'SD Prompt' : ' high quality magnum opus drawing of a vial of bubling red liquid' + } } + +3. Wooden Shield Entry: + + {'Wooden Shield': { + 'Name' : 'Wooden Shield', + 'Type' : 'Armor, Shield', + 'Rarity': 'Common', + 'Value': '10 gp', + 'Properties': ['+2 AC'], + 'Weight': '6 lb', + 'Description': 'Sturdy and reliable, this wooden shield is a simple yet effective defense against the blows of adversaries.', + 'Quote': 'In the rhythm of battle, it dances - a barrier between life and defeat.', + 'SD Prompt': ' high quality magnum opus drawing of a wooden shield strapped with iron and spikes' + } } + +4. Magical Helmet of Perception Entry: + + {'Magical Helmet': { + 'Name' : 'Magical Helmet of Perception', + 'Type' : 'Magical Item (armor, helmet)', + 'Rarity': 'Very Rare', + 'Value': '3000 gp', + 'Properties': ['+ 1 to AC', 'Grants the wearer advantage on perception checks', '+5 to passive perception'], + 'Weight': '3 lb', + 'Description': 'Forged from mystic metals and enchanted with ancient spells, this helmet offers protection beyond the physical realm.', + 'Quote': 'A crown not of royalty, but of unyielding vigilance, warding off the unseen threats that lurk in the shadows.', + 'SD Prompt': 'high quality magnum opus drawing of an ancient elegant helm with a shimmer of magic' + } } + +5. Longbow Entry: + + {'Longbow': { + 'Name': 'Longbow', + 'Type': 'Ranged Weapon (martial, longbow)', + 'Rarity': 'Common', + 'Value': '50 gp', + 'Properties': ['2-handed', 'Range 150/600', 'Loading'], + 'Damage': '1d8 + Dex, piercing', + 'Weight': '6 lb', + 'Description': 'With a sleek and elegant design, this longbow is crafted for speed and precision, capable of striking down foes from a distance.', + 'Quote': 'From the shadows it emerges, a silent whisper of steel that pierces the veil of darkness, bringing justice to those who dare to trespass.', + 'SD Prompt' : 'high quality magnum opus drawing of a longbow with a quiver attached' + } } + + +6. Mace Entry: + + {'Mace': { + 'Name': 'Mace', + 'Type': 'Melee Weapon (martial, bludgeoning)', + 'Rarity': 'Common', + 'Value': '25 gp', 'Properties': ['Bludgeoning', 'One-handed'], + 'Damage': '1d6 + str, bludgeoning', + 'Weight': '6 lb', + 'Description': 'This mace is a fearsome sight, its head a heavy and menacing ball of metal designed to crush bone and break spirits.', + 'Quote': 'With each swing, it sings a melody of pain and retribution, an anthem of justice to those who wield it.', + 'SD Prompt': 'high quality magnum opus drawing of a mace with intricate detailing and an ominous presence' + } } + +7. Flying Carpet Entry: + + {'Flying Carpet': { + 'Name': 'Flying Carpet', + 'Type': 'Magical Item (transportation)', + 'Rarity': 'Very Rare' + 'Value': '12000 gp', + 'Properties': ['Flying', 'Personal Flight', 'Up to 2 passengers', Speed : 60 ft], + 'Weight': '50 lb', + 'Description': 'This enchanted carpet whisks its riders through the skies, providing a swift and comfortable mode of transport across great distances.', 'Quote': 'Soar above the mundane, and embrace the winds of adventure with this magical gift from the heavens.', + 'SD Prompt': 'high quality magnum opus drawing of an elegant flying carpet with intricate patterns and colors' + } } + +8. Tome of Endless Stories Entry: + + {'Tome of Endless Stories': { + 'Name': 'Tome of Endless Stories', + 'Type': 'Book', + 'Rarity': 'Uncommon' + 'Value': '500 gp', + 'Properties': [ + 'Generates a new story or piece of lore each day', + 'Reading a story grants insight or a hint towards solving a problem or puzzle' + ], + 'Weight': '3 lbs', + 'Description': 'An ancient tome bound in leather that shifts colors like the sunset. Its pages are never-ending, filled with tales from worlds both known and undiscovered.', + 'Quote': 'Within its pages lie the keys to a thousand worlds, each story a doorway to infinite possibilities.', + 'SD Prompt': 'leather-bound with gold and silver inlay, pages appear aged but are incredibly durable, magical glyphs shimmer softly on the cover.' + } } + +9. Ring of Miniature Summoning Entry: + + {'Ring of Miniature Summoning': { + 'Name': 'Ring of Miniature Summoning', + 'Type': 'Ring', + 'Rarity': 'Rare', + 'Value': '1500 gp', + 'Properties': ['Summons a miniature beast ally once per day', 'Beast follows commands and lasts for 1 hour', 'Choice of beast changes with each dawn'], + 'Weight': '0 lb', + 'Description': 'A delicate ring with a gem that shifts colors. When activated, it brings forth a small, loyal beast companion from the ether.', + 'Quote': 'Not all companions walk beside us. Some are summoned from the depths of magic, small in size but vast in heart.', + 'SD Prompt': 'gemstone with changing colors, essence of companionship and versatility.' + } } + + +10. Spoon of Tasting Entry: + + {'Spoon of Tasting': { + 'Name': 'Spoon of Tasting', + 'Type': 'Spoon', + 'Rarity': 'Uncommon', + 'Value': '200 gp', + 'Properties': ['When used to taste any dish, it can instantly tell you all the ingredients', 'Provides exaggerated compliments or critiques about the dish'], + 'Weight': '0.2 lb', + 'Description': 'A culinary critic’s dream or nightmare. This spoon doesn’t hold back its opinions on any dish it tastes.', + 'Quote': 'A spoonful of sugar helps the criticism go down.', + 'SD Prompt': 'Looks like an ordinary spoon, but with a mouth that speaks more than you’d expect.' + } } + +11. Infinite Scroll Entry: + + {'Infinite Scroll': { + 'Name': 'Infinite Scroll', + 'Type': 'Magical Scroll', + 'Rarity': 'Legendary', + 'Value': '25000', + 'Properties': [ + 'Endlessly Extends with New Knowledge', + 'Reveals Content Based on Reader’s Need or Desire', + 'Cannot be Fully Transcribed' + ], + 'Weight': '0.5 lb', + 'Description': 'This scroll appears to be a standard parchment at first glance. However, as one begins to read, it unrolls to reveal an ever-expanding tapestry of knowledge, lore, and spells that seems to have no end. The content of the scroll adapts to the reader’s current quest for knowledge or need, always offering just a bit more beyond what has been revealed.', + 'Quote': 'In the pursuit of knowledge, the horizon is ever receding. So too is the content of this scroll, an endless journey within a parchment’s bounds.', + 'SD Prompt': 'A seemingly ordinary scroll that extends indefinitely, ' + } } + +12. Mimic Treasure Chest Entry: + + {'Mimic Treasure Chest': { + 'Name': 'Mimic Treasure Chest', + 'Type': 'Trap', + 'Rarity': 'Rare', + 'Value': '1000 gp', # Increased value reflects its dangerous and rare nature + 'Properties': [ + 'Deceptively inviting', + 'Springs to life when interacted with', + 'Capable of attacking unwary adventurers' + ], + 'Weight': '50 lb', # Mimics are heavy due to their monstrous nature + 'Description': 'At first glance, this chest appears to be laden with treasure, beckoning to all who gaze upon it. However, it harbors a deadly secret: it is a Mimic, a cunning and dangerous creature that preys on the greed of adventurers. With its dark magic, it can perfectly imitate a treasure chest, only to reveal its true, monstrous form when approached. Those who seek to plunder its contents might find themselves in a fight for their lives.', + 'Quote': '"Beneath the guise of gold and riches lies a predator, waiting with bated breath for its next victim."', + 'SD Prompt': 'A seemingly ordinary treasure chest that glimmers with promise. Upon closer inspection, sinister, almost living edges move with malice, revealing its true nature as a Mimic, ready to unleash fury on the unwary.' + } } + +13. Hammer of Thunderbolts Entry: + + {'Hammer of Thunderbolts': { + 'Name': 'Hammer of Thunderbolts', + 'Type': 'Melee Weapon (maul, bludgeoning)', + 'Rarity': 'Legendary', + 'Value': '16000', + 'Properties': [ + 'requires attunement', + 'Giant's Bane', + 'must be wearing a belt of giant strength and gauntlets of ogre power', + 'Str +4', + 'Can excees 20 but not 30', + '20 against giant, DC 17 save against death', + '5 charges, expend 1 to make a range attack 20/60', + 'ranged attack releases thunderclap on hit, DC 17 save against stunned 30 ft', + 'regain 1d4+1 charges at dawn' + ], + 'Weight': 15 lb', + 'Description': 'Forged by the gods and bound by the storms themselves, the Hammer of Thunderbolts is a weapon of unparalleled might. Its head is etched with ancient runes that glow with a fierce light whenever its power is called upon. This maul is not just a tool of destruction but a symbol of the indomitable force of nature, capable of leveling mountains and commanding the elements with each swing.', + 'Quote': 'When the skies rage and the earth trembles, know that the Hammer of Thunderbolts has found its mark. It is not merely a weapon, but the embodiment of the storm\'s wrath wielded by those deemed worthy.', + 'SD Prompt': 'It radiates with electric energy, its rune-etched head and storm-weathered leather grip symbolizing its dominion over storms. In its grasp, it pulses with the potential to summon the heavens' fury, embodying the tempest's raw power.' + } } + +""" diff --git a/MerchanBot CLI/main.py b/MerchanBot CLI/main.py new file mode 100644 index 0000000000000000000000000000000000000000..ce142ef48c1427eacfa6d76063c5ee1af51323f6 --- /dev/null +++ b/MerchanBot CLI/main.py @@ -0,0 +1,25 @@ +import item_dict_gen as igen +import img2img +import card_generator as card +import utilities as u +import ctypes +import user_input as uinput +import os + +# This is a fix for the way that python doesn't release system memory back to the OS and it was leading to locking up the system +libc = ctypes.cdll.LoadLibrary("libc.so.6") +M_MMAP_THRESHOLD = -3 + +# Set malloc mmap threshold. +libc.mallopt(M_MMAP_THRESHOLD, 2**20) + +uinput.prompt_user_input() + + + + + + + + + diff --git a/MerchanBot CLI/render_card_text.py b/MerchanBot CLI/render_card_text.py new file mode 100755 index 0000000000000000000000000000000000000000..2a3c3bf1b0c6e8851a65b83aaa63f2fb2baaeb02 --- /dev/null +++ b/MerchanBot CLI/render_card_text.py @@ -0,0 +1,102 @@ +from PIL import Image, ImageDraw, ImageFont + + # Function for managing longer bodies of text and breaking into a list of lines to be printed based on input arguments +def split_text_into_lines(text, font, max_width, draw): + blocks = text.split('\n') + lines = [] + for block in blocks: + words = block.split() + current_line = '' + + for word in words: + # Check width with new word added + test_line = f"{current_line} {word}".strip() + test_width = draw.textlength(text = test_line, font=font) + if test_width <= max_width: + current_line = test_line + else: + #If the line with the new word exceeds the max width, start a new line + lines.append(current_line) + current_line = word + # add the last line + lines.append(current_line) + return lines +# Function for calculating the height of the text at the current font setting + + +def adjust_font_size_lines_and_spacing(text, font_path, initial_font_size, max_width, area_height, image) : + font_size = initial_font_size + optimal_font_size = font_size + optimal_lines = [] + line_spacing_factor = 1.2 # multiple of font size that will get added between each line + + while font_size > 10: # Set minimum font size + font = ImageFont.truetype(font_path, font_size) + draw = ImageDraw.Draw(image) + # Fitting text into box dimensions + lines = split_text_into_lines(text, font, max_width, draw) + # Calculate total height with dynamic line spacing + single_line_height = draw.textbbox((0, 0), "Ay", font=font)[3] - draw.textbbox((0, 0), "Ay", font=font)[1] # Height of 'Ay' + line_spacing = int(single_line_height * line_spacing_factor) - single_line_height + total_text_height = len(lines) * single_line_height + (len(lines) - 1) * line_spacing # Estimate total height of all lines by multiplying number of lines by font height plus number of lines -1 times line spacing + + if total_text_height <= area_height : + optimal_font_size = font_size + optimal_lines = lines + break # Exit loop font fits in contraints + + else: + font_size -= 1 # Reduce font by 1 to check if it fits + + return optimal_font_size, optimal_lines, line_spacing +# Function that takes in an image,text and properties for textfrom card_generator +def render_text_with_dynamic_spacing(image, text, center_position, max_width, area_height,font_path, initial_font_size, item_key = None, description = None, quote = None): + if item_key: + text = write_description(item_key) + + optimal_font_size, optimal_lines, line_spacing = adjust_font_size_lines_and_spacing( + text, font_path, initial_font_size, max_width, area_height, image) + # create an object to draw on + + font = ImageFont.truetype(font_path, optimal_font_size) + draw = ImageDraw.Draw(image) + + # Shadow settings + shadow_offset = (1, 1) # X and Y offset for shadow + shadow_color = 'grey' # Shadow color + + # Unsure about the following line, not sure if I want y_offset to be dynamic + y_offset = center_position[1] + + if description or quote : + for line in optimal_lines: + line_width = draw.textlength(text = line, font=font) + x = center_position[0] + # Draw Shadow first + shadow_position = (x + shadow_offset[0], y_offset + shadow_offset[1]) + draw.text(shadow_position, line, font=font, fill=shadow_color) + #Draw text + draw.text((x, y_offset), line, font=font, fill = 'black', align = "left" ) + y_offset += optimal_font_size + line_spacing # Move to next line + return image + + for line in optimal_lines: + line_width = draw.textlength(text = line, font=font) + x = center_position[0] - (line_width / 2) + # Draw Shadow first + shadow_position = (x + shadow_offset[0], y_offset + shadow_offset[1]) + draw.text(shadow_position, line, font=font, fill=shadow_color) + #Draw text + draw.text((x, y_offset), line, font=font, fill = 'black', align = "left" ) + y_offset += optimal_font_size + line_spacing # Move to next line + return image + +# Function to put the description objects together, this will be the complicated bit, I think iterate through keys excluding title, type and cost +def write_description(item_key): + skip_list = ['Name', 'Type', 'Value', 'Weight', 'Damage', 'SD Prompt', 'Quote', 'Rarity'] + description_list = ['\n'.join(value) for key, value in item_key.items() if key not in skip_list and type(value) == list] + description_list += [value if key not in skip_list else '' for key, value in item_key.items() if type(value) != list] + return '\n'.join(filter(None, description_list)) + + + diff --git a/MerchanBot CLI/user_input.py b/MerchanBot CLI/user_input.py new file mode 100644 index 0000000000000000000000000000000000000000..94cac145a915ed0183a86d2d7d3dba65f7bf33bc --- /dev/null +++ b/MerchanBot CLI/user_input.py @@ -0,0 +1,85 @@ +import item_dict_gen as igen +import img2img +import card_generator as card +import utilities as u +import sys + +image_path = str +end_phrase = """<|end_of_turn|>""" +card_template_path = "./card_templates" +list_of_card_templates = u.directory_contents(card_template_path) + +user_pick_template_prompt = "Pick a template number from this list : " +user_pick_image_prompt = "Select an image : " + +# Check if the user wants to exit the chatbot + +def user_exit_question(user_input): + if user_input.lower() in ['exit', 'quit']: + print("Chatbot session ended.") + sys.exit() +# Process the list of files in the card_template directory and print with corresponding numbers to index +def process_list_for_user_response(list_of_items): + x = 0 + for item in list_of_items: + print(f"{x} : {item}") + x += 1 + +def user_pick_item(user_prompt,list_of_items): + process_list_for_user_response(list_of_items) + user_input = input(user_prompt) + # Check if the user wants to exit the chatbot + user_exit_question(user_input) + return list_of_items[int(user_input)] + +def call_llm(user_input): + # Process the query and get the response + llm_call = igen.call_llm_and_cleanup(user_input) + response = llm_call['choices'][0]['text'] + + # Find the index of the phrase + index = response.find(end_phrase) + print(f"index = {index}") + if index != -1: + # Slice the string from the end of the phrase onwards + response = response[index + len(end_phrase):] + else: + # Phrase not found, optional handling + response = response + + response = response.replace("GPT4 Assistant: ", "") + response = igen.convert_to_dict(response) + if not response: + response = call_llm(user_input) + del llm_call + return response + +def prompt_user_input(): + mimic = None + while True: + user_input_item = input("Provide an item : ") + user_exit_question(user_input_item) + + if 'mimic' in user_input_item.lower(): + mimic = True + + #user_input_template = input(f"Pick a template number from this list : {process_list_for_user_response(list_of_card_templates)}") + user_input_template = user_pick_item(user_pick_template_prompt,list_of_card_templates) + response = call_llm(user_input_item) + print(response[u.keys_list(response,0)]) + output_dict = response[u.keys_list(response,0)] + u.reclaim_mem() + item_name = response[u.keys_list(response,0)]['Name'] + sd_prompt = response[u.keys_list(response,0)]['SD Prompt'] + image_path = img2img.generate_image(4,sd_prompt,item_name,user_input_template, mimic) + user_card_image = user_pick_item(user_pick_image_prompt, image_path) + + print(image_path) + + card.render_text_on_card(user_card_image, output_dict) + u.delete_files(img2img.image_list) + + + +print(list_of_card_templates) + diff --git a/MerchanBot CLI/utilities.py b/MerchanBot CLI/utilities.py new file mode 100644 index 0000000000000000000000000000000000000000..0324f7387cbd6589d6957e7f2c26dd37eda9529d --- /dev/null +++ b/MerchanBot CLI/utilities.py @@ -0,0 +1,40 @@ +# Create a list of hashmap key values . +import torch +import time +import gc +import os + +# Utility Functions to be called from all modules + +# Function to return a list of keys of a nested dictionary using it's key value (item or creature) +def keys_list(dict, index): + keys_list=list(dict.keys()) + return keys_list[index] + +def reclaim_mem(): + + print(f"Memory before del {torch.cuda.memory_allocated()}") + torch.cuda.ipc_collect() + gc.collect() + torch.cuda.empty_cache() + time.sleep(0.01) + print(f"Memory after del {torch.cuda.memory_allocated()}") + +def del_object(object): + del object + gc.collect() + +def directory_contents(directory_path): + if os.path.isdir(directory_path) : + contents = os.listdir(directory_path) + return contents + else : pass + +def delete_files(file_paths): + + for file_path in file_paths: + try: + os.remove(file_path) + except OSError as e: + print(f"Error: {file_path} : {e.strerror}") + file_paths.clear() \ No newline at end of file diff --git a/README.md b/README.md index f26138e410156f6347bcd28977b1e635fa040bb4..ce59f450b66918635380c32208ccc828eea14cf1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +<<<<<<< HEAD --- title: CollectibleCardGenerator emoji: 🐠 @@ -9,3 +10,7 @@ license: mit --- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +======= +# CardGenerator +Takes user input and generates a collectible card with custom or LLM generated text and image generation +>>>>>>> master diff --git a/__pycache__/card_generator.cpython-310.pyc b/__pycache__/card_generator.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9759a065d985fb3f1d23282a7d552220a1acc2db Binary files /dev/null and b/__pycache__/card_generator.cpython-310.pyc differ diff --git a/__pycache__/image_gen.cpython-310.pyc b/__pycache__/image_gen.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..153f0e00f5ee8dfc0845c3f9bd7b5e893f840202 Binary files /dev/null and b/__pycache__/image_gen.cpython-310.pyc differ diff --git a/__pycache__/img2img.cpython-310.pyc b/__pycache__/img2img.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d2949870576f87308e3fb93d341ae3aa90a80d6 Binary files /dev/null and b/__pycache__/img2img.cpython-310.pyc differ diff --git a/__pycache__/inventory.cpython-310.pyc b/__pycache__/inventory.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2c3b4382eca60404c522953a99dda8a4e9d7f04 Binary files /dev/null and b/__pycache__/inventory.cpython-310.pyc differ diff --git a/__pycache__/item_dict_gen.cpython-310.pyc b/__pycache__/item_dict_gen.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab51ec8de22ed7b37f6e45ea641adcb43aa9d0c2 Binary files /dev/null and b/__pycache__/item_dict_gen.cpython-310.pyc differ diff --git a/__pycache__/main.cpython-310.pyc b/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bf8c41d8c5ffe4722277b128542739253ccbbd4c Binary files /dev/null and b/__pycache__/main.cpython-310.pyc differ diff --git a/__pycache__/render_card_text.cpython-310.pyc b/__pycache__/render_card_text.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c4bea72479cdf57f61f092e817a94d398bcd904c Binary files /dev/null and b/__pycache__/render_card_text.cpython-310.pyc differ diff --git a/__pycache__/template_builder.cpython-310.pyc b/__pycache__/template_builder.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f17ad841ccf3e16a91ca8291e756bbf4894455b Binary files /dev/null and b/__pycache__/template_builder.cpython-310.pyc differ diff --git a/__pycache__/user_input.cpython-310.pyc b/__pycache__/user_input.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..113280806e50e7c940da845eb24b068351074411 Binary files /dev/null and b/__pycache__/user_input.cpython-310.pyc differ diff --git a/__pycache__/utilities.cpython-310.pyc b/__pycache__/utilities.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f920680faecad69570cea505b369e39414a24f1 Binary files /dev/null and b/__pycache__/utilities.cpython-310.pyc differ diff --git a/card_generator.py b/card_generator.py new file mode 100755 index 0000000000000000000000000000000000000000..3b755e1774e7e284b49d68cb4e633f359f024bef --- /dev/null +++ b/card_generator.py @@ -0,0 +1,124 @@ +import render_card_text as rend +from PIL import Image, ImageFilter +import utilities as u +import ast + + +def save_image(image,item_key): + image.save(f"{item_key['Name']}.png") + + +# Import Inventory +#shop_inventory = inv.inventory +#purchased_item_key = shop_inventory['Shortsword'] +#border_path = './card_templates/Shining Sunset Border.png' + +sticker_path_dictionary = {'Default': './card_parts/Sizzek Sticker.png','Common': './card_parts/Common.png', 'Uncommon': './card_parts/Uncommon.png','Rare': './card_parts/Rare.png','Very Rare':'./card_parts/Very Rare.png','Legendary':'./card_parts/Legendary.png'} +blank_overlay_path = "./card_parts/white-fill-title-detail-value-transparent.png" +value_overlay_path = "./card_parts/Value_box_transparent.png" +test_item = {'Name': 'Pustulent Raspberry', 'Type': 'Fruit', 'Value': '1 cp', 'Properties': ['Unusual Appearance', 'Rare Taste'], 'Weight': '0.2 lb', 'Description': 'This small fruit has a pustulent appearance, with bumps and irregular shapes covering its surface. Its vibrant colors and strange texture make it an oddity among other fruits.', 'Quote': 'A fruit that defies expectations, as sweet and sour as life itself.', 'SD Prompt': 'A small fruit with vibrant colors and irregular shapes, bumps covering its surface.'} + + + +# Function that takes in an image url and a dictionary and uses the values to print onto a card. +def paste_image_and_resize(base_image,sticker_path, x_position, y_position,img_width, img_height, purchased_item_key = None): + + # Check for if item has a Rarity string that is a in the dictionary of sticket paths + if purchased_item_key: + if sticker_path[purchased_item_key]: + sticker_path = sticker_path[purchased_item_key] + else: sticker_path = sticker_path['Default'] + + # Load the image to paste + image_to_paste = Image.open(sticker_path) + + # Define the new size (scale) for the image you're pasting + + new_size = (img_width, img_height) + + # Resize the image to the new size + image_to_paste_resized = image_to_paste.resize(new_size) + + # Specify the top-left corner where the resized image will be pasted + paste_position = (x_position, y_position) # Replace x and y with the coordinates + + # Paste the resized image onto the base image + base_image.paste(image_to_paste_resized, paste_position, image_to_paste_resized) + +def render_text_on_card(image_path, item_name, + item_type, + item_rarity, + item_value, + item_properties, + item_damage, + item_weight, + item_description, + item_quote) : + # Card Properties + image_list = [] + item_properties = ast.literal_eval(item_properties) + item_properties = '\n'.join(item_properties) + output_image_path = f"./{item_name}.png" + print(f"Saving image to {output_image_path}") + font_path = "./fonts/Balgruf.ttf" + italics_font_path = './fonts/BalgrufItalic.ttf' + initial_font_size = 50 + + # Title Properties + title_center_position = (395, 55) + title_area_width = 600 # Maximum width of the text box + title_area_height = 60 # Maximum height of the text box + + # Type box properties + type_center_position = (384, 545) + type_area_width = 600 + type_area_height = 45 + type_text = item_type + if len(item_weight) >= 1: + type_text = type_text + ' '+ item_weight + + if len(item_damage) >= 1 : + type_text = type_text + ' '+ item_damage + + # Description box properties + description_position = (105, 630) + description_area_width = 590 + description_area_height = 215 + + # Value box properties (This is good, do not change unless underlying textbox layout is changing) + value_position = (660,905) + value_area_width = 125 + value_area_height = 50 + + # Quote test properties + quote_position = (110,885) + quote_area_width = 470 + quote_area_height = 60 + + # open image and render text + image = u.open_image_from_url(image_path) + image = rend.render_text_with_dynamic_spacing(image, item_name, title_center_position, title_area_width, title_area_height,font_path,initial_font_size) + image = rend.render_text_with_dynamic_spacing(image,type_text , type_center_position, type_area_width, type_area_height,font_path,initial_font_size) + image = rend.render_text_with_dynamic_spacing(image, item_description + '\n\n' + item_properties, description_position, description_area_width, description_area_height,font_path,initial_font_size, description = True) + paste_image_and_resize(image, value_overlay_path,x_position= 0,y_position=0, img_width= 768, img_height= 1024) + image = rend.render_text_with_dynamic_spacing(image, item_value, value_position, value_area_width, value_area_height,font_path,initial_font_size) + image = rend.render_text_with_dynamic_spacing(image, item_quote, quote_position, quote_area_width, quote_area_height,italics_font_path,initial_font_size, quote = True) + #Paste Sizzek Sticker + paste_image_and_resize(image, sticker_path_dictionary,x_position= 0,y_position=909, img_width= 115, img_height= 115, purchased_item_key= item_rarity) + + # Add blur, gives it a less artificial look, put into list and return the list since gallery requires lists + image = image.filter(ImageFilter.GaussianBlur(.5)) + image_list.append(image) + + image = image.save(f"./output/{item_name}.png") + + + + return image_list + + +#render_text_on_card('./card_templates/Shining Sunset Border.png',test_item ) + + + + diff --git a/card_parts/Common.png b/card_parts/Common.png new file mode 100644 index 0000000000000000000000000000000000000000..84fba34181901787880b548bd7186be7f8051dff --- /dev/null +++ b/card_parts/Common.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34c5e1d977377ef156a2f146dbd6a49a870bbe4c5fc06948e9026f0b8340c029 +size 1351641 diff --git a/card_parts/Legendary.png b/card_parts/Legendary.png new file mode 100644 index 0000000000000000000000000000000000000000..9b55606ce7782763a671c4312b3974f73c409665 --- /dev/null +++ b/card_parts/Legendary.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e35e4bdbbcff19e9c0c625460128c4a13c5b9ed5d4e663c35a4357f951be0bc1 +size 1403991 diff --git a/card_parts/Rare.png b/card_parts/Rare.png new file mode 100644 index 0000000000000000000000000000000000000000..46c419e3e480039f38bdced71f5cb706289b9ca3 --- /dev/null +++ b/card_parts/Rare.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d95030a73ca3985e5c61044459ba99f624ff667507ea67042822efbd1d2b421 +size 1297367 diff --git a/card_parts/Sizzek Sticker.png b/card_parts/Sizzek Sticker.png new file mode 100644 index 0000000000000000000000000000000000000000..08709b84df293633c51a7beac023cb92aa593340 --- /dev/null +++ b/card_parts/Sizzek Sticker.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:863b193d8f637c343a83d2b8942fe5c9715888843bef3e595f3a927789e0917a +size 963192 diff --git a/card_parts/Uncommon.png b/card_parts/Uncommon.png new file mode 100644 index 0000000000000000000000000000000000000000..cf2797e44ab52ea8bddc0d854ac9495820f7658f --- /dev/null +++ b/card_parts/Uncommon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e772c8dc34ac80ab87cc1b6b1f220c3ecfb15db844d76a913e16190841cc9a3 +size 1329398 diff --git a/card_parts/Value_box_transparent.png b/card_parts/Value_box_transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..9cc501a5216924cf1539bef6c347cd5caace3995 --- /dev/null +++ b/card_parts/Value_box_transparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea2ffa23ddcc6e77ee5d56f6963920dfa74bf33b4925c7afbaa077b0e61acae3 +size 27784 diff --git a/card_parts/Very Rare.png b/card_parts/Very Rare.png new file mode 100644 index 0000000000000000000000000000000000000000..d820997333bff9e264f2fb4d592183b689a5827d --- /dev/null +++ b/card_parts/Very Rare.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64c7765ee4b0e872a7b71b97cb398d8e4d57d5e53e9e07a6d15ac29d405e1ac4 +size 1229394 diff --git a/card_parts/white-fill-title-detail-value-transparent.png b/card_parts/white-fill-title-detail-value-transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..bb49e899c49ef83cd96c2f0699ec85e1c1cbfd17 --- /dev/null +++ b/card_parts/white-fill-title-detail-value-transparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3fb9d83290603ac2c8e4a07030d2696c6f144dadc371e11ea0c4e097cba9045 +size 19272 diff --git a/fonts/Balgruf.ttf b/fonts/Balgruf.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1cb462f8cbca2a6da23df944d368de27dd4c5239 Binary files /dev/null and b/fonts/Balgruf.ttf differ diff --git a/fonts/BalgrufItalic.ttf b/fonts/BalgrufItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..12d953c9981323102b65d380d6712191af59767f Binary files /dev/null and b/fonts/BalgrufItalic.ttf differ diff --git a/fonts/Goudy Medieval Medieval.ttf b/fonts/Goudy Medieval Medieval.ttf new file mode 100755 index 0000000000000000000000000000000000000000..c35d69bd8001db3222434483fdcd4a5a08af176e Binary files /dev/null and b/fonts/Goudy Medieval Medieval.ttf differ diff --git a/fonts/balgruf-font/Balgruf.ttf b/fonts/balgruf-font/Balgruf.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1cb462f8cbca2a6da23df944d368de27dd4c5239 Binary files /dev/null and b/fonts/balgruf-font/Balgruf.ttf differ diff --git a/fonts/balgruf-font/BalgrufItalic.ttf b/fonts/balgruf-font/BalgrufItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..12d953c9981323102b65d380d6712191af59767f Binary files /dev/null and b/fonts/balgruf-font/BalgrufItalic.ttf differ diff --git a/fonts/balgruf-font/info.txt b/fonts/balgruf-font/info.txt new file mode 100644 index 0000000000000000000000000000000000000000..9f81da2b0c2ce0ee96a351eaaf291d565f170cec --- /dev/null +++ b/fonts/balgruf-font/info.txt @@ -0,0 +1,2 @@ +license: SIL Open Font License (OFL) +link: https://www.fontspace.com/balgruf-font-f59539 \ No newline at end of file diff --git a/fonts/balgruf-font/misc/Balgruf-31cd.woff2 b/fonts/balgruf-font/misc/Balgruf-31cd.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..fbda01f786bcee1b6754018bfd99bf31fee909ac Binary files /dev/null and b/fonts/balgruf-font/misc/Balgruf-31cd.woff2 differ diff --git a/fonts/balgruf-font/misc/Balgruf-d256.woff b/fonts/balgruf-font/misc/Balgruf-d256.woff new file mode 100644 index 0000000000000000000000000000000000000000..b5527652a0c60f9424dc92e56f0ad0a3fad6df21 Binary files /dev/null and b/fonts/balgruf-font/misc/Balgruf-d256.woff differ diff --git a/fonts/balgruf-font/misc/Balgruf_Italic-52d6.woff b/fonts/balgruf-font/misc/Balgruf_Italic-52d6.woff new file mode 100644 index 0000000000000000000000000000000000000000..a83e0a1a4d470c14a4d94300b05e3ca96bed8a5a Binary files /dev/null and b/fonts/balgruf-font/misc/Balgruf_Italic-52d6.woff differ diff --git a/fonts/balgruf-font/misc/Balgruf_Italic-e184.woff2 b/fonts/balgruf-font/misc/Balgruf_Italic-e184.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..e22702082f80823152f06aa3fa0cae9f1fc81b00 Binary files /dev/null and b/fonts/balgruf-font/misc/Balgruf_Italic-e184.woff2 differ diff --git a/img2img.py b/img2img.py new file mode 100755 index 0000000000000000000000000000000000000000..2dc7f3fb408ae2dd3775bc9beb2d9569ae92f5fa --- /dev/null +++ b/img2img.py @@ -0,0 +1,77 @@ +from diffusers import (StableDiffusionXLImg2ImgPipeline, AutoencoderKL) +from diffusers.utils import load_image +import torch +import time +import utilities as u +import card_generator as card +from PIL import Image + +pipe = None +start_time = time.time() +torch.backends.cuda.matmul.allow_tf32 = True +model_path = ("./models/stable-diffusion/card-generator-v1.safetensors") +lora_path = "./models/stable-diffusion/Loras/blank-card-template-5.safetensors" +detail_lora_path = "./models/stable-diffusion/Loras/add-detail-xl.safetensors" +mimic_lora_path = "./models/stable-diffusion/Loras/EnvyMimicXL01.safetensors" +temp_image_path = "./image_temp/" +card_pre_prompt = " blank magic card,high resolution, detailed intricate high quality border, textbox, high quality detailed magnum opus drawing of a " +negative_prompts = "text, words, numbers, letters" +image_list = [] + + +def load_img_gen(prompt, item, mimic = None): + prompt = card_pre_prompt + item + ' ' + prompt + print(prompt) + # image_path = f"{user_input_template}" + # init_image = load_image(image_path).convert("RGB") + + pipe = StableDiffusionXLImg2ImgPipeline.from_single_file(model_path, + custom_pipeline="low_stable_diffusion", + torch_dtype=torch.float16, + variant="fp16").to("cuda") + # Load LoRAs for controlling image + #pipe.load_lora_weights(lora_path, weight_name = "blank-card-template-5.safetensors",adapter_name = 'blank-card-template') + pipe.load_lora_weights(detail_lora_path, weight_name = "add-detail-xl.safetensors", adapter_name = "add-detail-xl") + + # If mimic keyword has been detected, load the mimic LoRA and set adapter values + if mimic: + pipe.load_lora_weights(mimic_lora_path, weight_name = "EnvyMimicXL01.safetensors", adapter_name = "EnvyMimicXL") + pipe.set_adapters(['blank-card-template', "add-detail-xl", "EnvyMimicXL"], adapter_weights = [0.9,0.9,1.0]) + else : + pipe.set_adapters([ "add-detail-xl"], adapter_weights = [0.9]) + pipe.enable_vae_slicing() + return pipe, prompt + +def preview_and_generate_image(x,pipe, prompt, user_input_template, item): + img_start = time.time() + image = pipe(prompt=prompt, + strength = .9, + guidance_scale = 5, + image= user_input_template, + negative_promt = negative_prompts, + num_inference_steps=40, + height = 1024, width = 768).images[0] + + image = image.save(temp_image_path+str(x) + f"{item}.png") + output_image_path = temp_image_path+str(x) + f"{item}.png" + img_time = time.time() - img_start + img_its = 50/img_time + print(f"image gen time = {img_time} and {img_its} it/s") + + # Delete the image variable to keep VRAM open to load the LLM + del image + print(f"Memory after del {torch.cuda.memory_allocated()}") + print(image_list) + total_time = time.time() - start_time + print(total_time) + + return output_image_path + + + + + + + + + \ No newline at end of file diff --git a/inventory.py b/inventory.py new file mode 100755 index 0000000000000000000000000000000000000000..15f077ae3481948e52a177c13b39eee1a4d03d58 --- /dev/null +++ b/inventory.py @@ -0,0 +1,52 @@ + + +inventory = { + 'Shortsword': { + 'Name' : 'Shortsword', + 'Type' : 'Melee Weapon (martial, sword)', + 'Value': '10 gp', + 'Properties': ['Finesse, Light '], + 'Damage': '1d6 + proficiency + Dex or Str', + 'Weight': '2 lb', + 'Description': 'Gleaming with a modest radiance, the shortsword boasts a keen edge and a leather-wrapped hilt, promising both grace and reliability in the heat of combat.', + 'Quote': 'In the heart of battle, the shortsword proves not just a weapon, but a steadfast companion, whispering paths of valor to those who wield it.', + 'SD Description' : 'high resolution, blank magic card,detailed high quality intricate border, decorated textbox, high quality magnum opus cgi drawing of a steel shortsword' + }, + + 'Health Potion': { + 'Name' : 'Health Portion', + 'Type' : 'Potion', + 'Value': '50 gp', + 'Properties': ['Quafable', 'Restores 1d4 + 2 HP upon consumption'], + 'Weight': '0.5 lb', + 'Description': 'Contained within this small vial is a crimson liquid that sparkles when shaken, a life-saving elixir for those who brave the unknown.', + 'Quote': 'To the weary, a drop of hope; to the fallen, a chance to stand once more.' + }, + + + 'Wooden Shield': { + 'Name' : 'Wooden Shield', + 'Type' : 'Armor, Shield', + 'Value': '15 gp', + 'Properties': ['+2 AC'], + 'Weight': '6 lb', + 'Description': 'Sturdy and reliable, this wooden shield is a simple yet effective defense against the blows of adversaries.', + 'Quote': 'In the rhythm of battle, it dances - a barrier between life and defeat.' + }, + + 'Magical Helmet': { + 'Name' : 'Magical Helmet of Perception', + 'Type' : 'Magical Item (armor, helmet)', + 'Value': '120 gp', + 'Properties': ['+ 1 to AC', 'Grants the wearer enhanced perception'], + 'Weight': '3 lb', + 'Description': 'Forged from mystic metals and enchanted with ancient spells, this helmet offers protection beyond the physical realm.', + 'Quote': 'A crown not of royalty, but of unyielding vigilance, warding off the unseen threats that lurk in the shadows.' + } + } + +{'id': 'cmpl-5b0ed6c7-2326-473f-8f11-32d3f079edc2', + 'object': 'text_completion', + 'created': 1709094107, + 'model': '../models/starling-lm-7b-alpha.Q8_0.gguf', + 'choices': [{'text': ' Here\'s an example of a structured inventory entry for a Mimic Treasure Chest as per your request:\n\n```python\n{\n \'Mimic Treasure Chest\': {\n \'Name\': \'Mimic Treasure Chest\',\n \'Type\': \'Trap\',\n \'Rarity\': \'Rare\',\n \'Value\': \'1000 gp\', \n \'Properties\': [\n \'Deceptively inviting\', \n \'Springs to life when interacted with\', \n \'Capable of attacking unwary adventurers\'\n ],\n \'Weight\': \'50 lb\', \n \'Description\': \'At first glance, this chest appears to be laden with treasure, beckoning to all who gaze upon it. However, it harbors a deadly secret: it is a Mimic, a cunning and dangerous creature that preys on the greed of adventurers. With its dark magic, it can perfectly imitate a treasure chest, only to reveal its true, monstrous form when approached. Those who seek to plunder its contents might find themselves in a fight for their lives.\',\n \'Quote\': \'"Beneath the guise of gold and riches lies a predator, waiting with bated breath for its next victim."\',\n \'SD Prompt\': \'A seemingly ordinary treasure chest that glimmers with promise. Upon closer inspection, sinister, almost living edges move with malice, revealing its true nature as a Mimic, ready to unleash fury on the unwary.\'\n }\n}\n```\n\nKeep in mind that mimics are typically found in dungeons and are known to take on the form of doors and chests. This example follows that theme while also providing information on the mimic\'s rarity, value, properties, and weight.', 'index': 0, 'logprobs': None, 'finish_reason': 'stop'}], 'usage': {'prompt_tokens': 4287, 'completion_tokens': 405, 'total_tokens': 4692}} \ No newline at end of file diff --git a/item_dict_gen.py b/item_dict_gen.py new file mode 100755 index 0000000000000000000000000000000000000000..e2aaab6601d49d2391d366dd55c16dbdc03e517b --- /dev/null +++ b/item_dict_gen.py @@ -0,0 +1,371 @@ +from llama_cpp import Llama +import ast +import gc +import torch + +model_path = "./models/starling-lm-7b-alpha.Q8_0.gguf" +# Set gpu_layers to the number of layers to offload to GPU. Set to 0 if no GPU acceleration is available on your system. + + +# Simple inference example +def load_llm(user_input): + llm = Llama( + model_path=model_path, + n_ctx=8192, # The max sequence length to use - note that longer sequence lengths require much more resources + n_threads=8, # The number of CPU threads to use, tailor to your system and the resulting performance + n_gpu_layers=-1 # The number of layers to offload to GPU, if you have GPU acceleration available +) + return llm( + f"GPT4 User: {prompt_instructions} the item is {user_input}: <|end_of_turn|>GPT4 Assistant:", # Prompt + max_tokens=512, # Generate up to 512 tokens + stop=[""], # Example stop token - not necessarily correct for this specific model! Please check before using. + echo=False # Whether to echo the prompt + ) + +def call_llm_and_cleanup(user_input): + # Call the LLM and store its output + llm_output = load_llm(user_input) + print(llm_output['choices'][0]['text']) + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() # Clear VRAM allocated by PyTorch + + # llm_output is still available for use here + + return llm_output + +def convert_to_dict(string): + # Evaluate if string is dictionary literal + try: + result = ast.literal_eval(string) + if isinstance(result, dict): + print("Item dictionary is valid") + return result + # If not, modify by attempting to add brackets to where they tend to fail to generate. + else: + modified_string = '{' + string + if isinstance(modified_string, dict): + return modified_string + modified_string = string + '}' + if isinstance(modified_string, dict): + return modified_string + modified_string = '{' + string + '}' + if isinstance(modified_string, dict): + return modified_string + except (ValueError, SyntaxError) : + print("Dictionary not valid") + return None + + +# Instructions past 4 are not time tested and may need to be removed. +### Meta prompted : +prompt_instructions = """ **Purpose**: Generate a structured inventory entry for a specific item as a hashmap. Follow the format provided in the examples below. + +**Instructions**: +1. Replace `{item}` with the name of the user item, DO NOT CHANGE THE USER ITEM NAME enclosed in single quotes (e.g., `'Magic Wand'`). +2. Ensure your request is formatted as a hashmap. +3. Weapons MUST have a key 'Damage' +4. The description should be brief and puncy, or concise and thoughtful. +5. The quote and SD Prompt MUST be inside double quotations ie " ". +6. The quote is from the perspective of someone commenting on the impact of the {item} on their life +7. Value should be assigned as an integer of copper pieces (cp), silver pieces (sp), electrum pieces (ep), gold pieces (gp), and platinum pieces (pp). +8. Use this table for reference on value : +1 cp 1 lb. of wheat +2 cp 1 lb. of flour or one chicken +5 cp 1 lb. of salt +1 sp 1 lb. of iron or 1 sq. yd. of canvas +5 sp 1 lb. of copper or 1 sq. yd. of cotton cloth +1 gp 1 lb. of ginger or one goat +2 gp 1 lb. of cinnamon or pepper, or one sheep +3 gp 1 lb. of cloves or one pig +5 gp 1 lb. of silver or 1 sq. yd. of linen +10 gp 1 sq. yd. of silk or one cow +15 gp 1 lb. of saffron or one ox +50 gp 1 lb. of gold +500 gp 1 lb. of platinum + + +300 gp +1 Melee or Ranged Weapon +2000 gp +2 Melee or Ranged Weapon +10000 gp +3 Melee or Ranged Weapon +300 gp +1 Armor Uncommon +2000 gp +2 Armor Rare +10000 gp +3 Armor Very Rare +300 gp +1 Shield Uncommon +2000 gp +2 Shield Rare +10000 gp +3 Shield Very Rare + +9. Examples of Magical Scroll Value: + Common: 50-100 gp + Uncommon: 101-500 gp + Rare: 501-5000 gp + Very rare: 5001-50000 gp + Legendary: 50001+ gp + +A scroll's rarity depends on the spell's level: + Cantrip-1: Common + 2-3: Uncommon + 4-5: Rare + 6-8: Very rare + 9: Legendary + +10. Explanation of Mimics: +Mimics are shapeshifting predators able to take on the form of inanimate objects to lure creatures to their doom. In dungeons, these cunning creatures most often take the form of doors and chests, having learned that such forms attract a steady stream of prey. +Imitative Predators. Mimics can alter their outward texture to resemble wood, stone, and other basic materials, and they have evolved to assume the appearance of objects that other creatures are likely to come into contact with. A mimic in its altered form is nearly unrecognizable until potential prey blunders into its reach, whereupon the monster sprouts pseudopods and attacks. +When it changes shape, a mimic excretes an adhesive that helps it seize prey and weapons that touch it. The adhesive is absorbed when the mimic assumes its amorphous form and on parts the mimic uses to move itself. +Cunning Hunters. Mimics live and hunt alone, though they occasionally share their feeding grounds with other creatures. Although most mimics have only predatory intelligence, a rare few evolve greater cunning and the ability to carry on simple conversations in Common or Undercommon. Such mimics might allow safe passage through their domains or provide useful information in exchange for food. + +11. +**Format Example**: +- **Dictionary Structure**: + + {"{item}": { + 'Name': "{item name}", + 'Type': '{item type}', + 'Rarity': '{item rarity}, + 'Value': '{item value}', + 'Properties': ["{property1}", "{property2}", ...], + 'Damage': '{damage formula} , '{damage type}', + 'Weight': '{weight}', + 'Description': "{item description}", + 'Quote': "{item quote}", + 'SD Prompt': "{special description for the item}" + } } + +- **Input Placeholder**: + - "{item}": Replace with the item name, ensuring it's wrapped in single quotes. + +**Output Examples**: +1. Cloak of Whispering Shadows Entry: + + {"Cloak of Whispering Shadows": { + 'Name': 'Cloak of Whispering Shadows', + 'Type': 'Cloak', + 'Rarity': 'Very Rare', + 'Value': '7500 gp', + 'Properties': ["Grants invisibility in dim light or darkness","Allows communication with shadows for gathering information"], + 'Weight': '1 lb', + 'Description': "A cloak woven from the essence of twilight, blending its wearer into the shadows. Whispers of the past and present linger in its folds, offering secrets to those who listen.", + 'Quote': "In the embrace of night, secrets surface in the silent whispers of the dark.", + 'SD Prompt': " Cloak of deep indigo almost black, swirling patterns that shift and move with every step. As it drapes over one's shoulders, an eerie connection forms between the wearer and darkness itself." + } } + +2. Health Potion Entry: + + {"Health Potion": { + 'Name' : "Health Portion", + 'Type' : 'Potion', + 'Rarity' : 'Common', + 'Value': '50 gp', + 'Properties': ["Quafable", "Restores 1d4 + 2 HP upon consumption"], + 'Weight': '0.5 lb', + 'Description': "Contained within this small vial is a crimson liquid that sparkles when shaken, a life-saving elixir for those who brave the unknown.", + 'Quote': "To the weary, a drop of hope; to the fallen, a chance to stand once more.", + 'SD Prompt' : " a small, delicate vial containing a sparkling crimson liquid. Emit a soft glow, suggesting its restorative properties. The vial is set against a dark, ambiguous background." + } } + +3. Wooden Shield Entry: + + {"Wooden Shield": { + 'Name' : "Wooden Shield", + 'Type' : 'Armor, Shield', + 'Rarity': 'Common', + 'Value': '10 gp', + 'Properties': ["+2 AC"], + 'Weight': '6 lb', + 'Description': "Sturdy and reliable, this wooden shield is a simple yet effective defense against the blows of adversaries.", + 'Quote': "In the rhythm of battle, it dances - a barrier between life and defeat.", + 'SD Prompt': " a sturdy wooden shield, a symbol of defense, with a simple yet solid design. The shield, has visible grain patterns and a few battle scars. It stands as a steadfast protector, embodying the essence of a warrior's resilience in the face of adversity." + } } + +4. Helmet of Perception Entry: + + {"Helmet of Perception": { + 'Name' : "Helmet of Perception", + 'Type' : 'Magical Item (armor, helmet)', + 'Rarity': 'Very Rare', + 'Value': '3000 gp', + 'Properties': ["+ 1 to AC", "Grants the wearer advantage on perception checks", "+5 to passive perception"], + 'Weight': '3 lb', + 'Description': "Forged from mystic metals and enchanted with ancient spells, this helmet offers protection beyond the physical realm.", + 'Quote': "A crown not of royalty, but of unyielding vigilance, warding off the unseen threats that lurk in the shadows.", + 'SD Prompt': " a mystical helmet crafted from enchanted metals, glowing with subtle runes. imbued with spells, radiates a mystical aura, symbolizing enhanced perception and vigilance,elegant,formidable" + } } + +5. Longbow Entry: + + {"Longbow": { + 'Name': "Longbow", + 'Type': 'Ranged Weapon (martial, longbow)', + 'Rarity': 'Common', + 'Value': '50 gp', + 'Properties': ["2-handed", "Range 150/600", "Loading"], + 'Damage': '1d8 + Dex, piercing', + 'Weight': '6 lb', + 'Description': "With a sleek and elegant design, this longbow is crafted for speed and precision, capable of striking down foes from a distance.", + 'Quote': "From the shadows it emerges, a silent whisper of steel that pierces the veil of darkness, bringing justice to those who dare to trespass.", + 'SD Prompt' : "a longbow with intricate carvings and stone inlay with a black string" + } } + + +6. Mace Entry: + + {"Mace": { + 'Name': "Mace", + 'Type': 'Melee Weapon (martial, bludgeoning)', + 'Rarity': 'Common', + 'Value': '25 gp', + 'Properties': ["Bludgeoning", "One-handed"], + 'Damage': '1d6 + str, bludgeoning', + 'Weight': '6 lb', + 'Description': "This mace is a fearsome sight, its head a heavy and menacing ball of metal designed to crush bone and break spirits.", + 'Quote': "With each swing, it sings a melody of pain and retribution, an anthem of justice to those who wield it.", + 'SD Prompt': "a menacing metal spike ball mace, designed for bludgeoning, with a heavy, intimidating head, embodying a tool for bone-crushing and spirit-breaking." + } } + +7. Flying Carpet Entry: + + {"Flying Carpet": { + 'Name': "Flying Carpet", + 'Type': 'Magical Item (transportation)', + 'Rarity': 'Very Rare', + 'Value': '3000 gp', + 'Properties': ["Flying", "Personal Flight", "Up to 2 passengers", Speed : 60 ft], + 'Weight': '50 lb', + 'Description': "This enchanted carpet whisks its riders through the skies, providing a swift and comfortable mode of transport across great distances.", + 'Quote': "Soar above the mundane, and embrace the winds of adventure with this magical gift from the heavens.", + 'SD Prompt': "a vibrant, intricately patterned flying carpet soaring high in the sky, with clouds and a clear blue backdrop, emphasizing its magical essence and freedom of flight" + } } + +8. Tome of Endless Stories Entry: + + {"Tome of Endless Stories": { + 'Name': "Tome of Endless Stories", + 'Type': 'Book', + 'Rarity': 'Uncommon' + 'Value': '500 gp', + 'Properties': [ + "Generates a new story or piece of lore each day", + "Reading a story grants insight or a hint towards solving a problem or puzzle" + ], + 'Weight': '3 lbs', + 'Description': "An ancient tome bound in leather that shifts colors like the sunset. Its pages are never-ending, filled with tales from worlds both known and undiscovered.", + 'Quote': "Within its pages lie the keys to a thousand worlds, each story a doorway to infinite possibilities.", + 'SD Prompt': "leather-bound with gold and silver inlay, pages appear aged but are incredibly durable, magical glyphs shimmer softly on the cover." + } } + +9. Ring of Miniature Summoning Entry: + + {"Ring of Miniature Summoning": { + 'Name': "Ring of Miniature Summoning", + 'Type': 'Ring', + 'Rarity': 'Rare', + 'Value': '1500 gp', + 'Properties': ["Summons a miniature beast ally once per day", "Beast follows commands and lasts for 1 hour", "Choice of beast changes with each dawn"], + 'Weight': '0 lb', + 'Description': "A delicate ring with a gem that shifts colors. When activated, it brings forth a small, loyal beast companion from the ether.", + 'Quote': "Not all companions walk beside us. Some are summoned from the depths of magic, small in size but vast in heart.", + 'SD Prompt': "gemstone with changing colors, essence of companionship and versatility." + } } + + +10. Spoon of Tasting Entry: + + {"Spoon of Tasting": { + 'Name': "Spoon of Tasting", + 'Type': 'Spoon', + 'Rarity': 'Uncommon', + 'Value': '200 gp', + 'Properties': ["When used to taste any dish, it can instantly tell you all the ingredients", "Provides exaggerated compliments or critiques about the dish"], + 'Weight': '0.2 lb', + 'Description': "A culinary critic’s dream or nightmare. This spoon doesn’t hold back its opinions on any dish it tastes.", + 'Quote': "A spoonful of sugar helps the criticism go down.", + 'SD Prompt': "Looks like an ordinary spoon, but with a mouth that speaks more than you’d expect." + } } + +11. Infinite Scroll Entry: + + {"Infinite Scroll": { + 'Name': "Infinite Scroll", + 'Type': 'Magical Scroll', + 'Rarity': 'Legendary', + 'Value': '25000', + 'Properties': [ + "Endlessly Extends with New Knowledge","Reveals Content Based on Reader’s Need or Desire","Cannot be Fully Transcribed"], + 'Weight': '0.5 lb', + 'Description': "This scroll appears to be a standard parchment at first glance. However, as one begins to read, it unrolls to reveal an ever-expanding tapestry of knowledge, lore, and spells that seems to have no end.", + 'Quote': "In the pursuit of knowledge, the horizon is ever receding. So too is the content of this scroll, an endless journey within a parchment’s bounds.", + 'SD Prompt': "A seemingly ordinary scroll that extends indefinitely" + } } + +12. Mimic Treasure Chest Entry: + + {"Mimic Treasure Chest": { + 'Name': "Mimic Treasure Chest", + 'Type': 'Trap', + 'Rarity': 'Rare', + 'Value': '1000 gp', # Increased value reflects its dangerous and rare nature + 'Properties': ["Deceptively inviting","Springs to life when interacted with","Capable of attacking unwary adventurers"], + 'Weight': '50 lb', # Mimics are heavy due to their monstrous nature + 'Description': "This enticing treasure chest is a deadly Mimic, luring adventurers with the promise of riches only to unleash its monstrous true form upon those who dare to approach, turning their greed into a fight for survival.", + 'SD Prompt': "A seemingly ordinary treasure chest that glimmers with promise. Upon closer inspection, sinister, almost living edges move with malice, revealing its true nature as a Mimic, ready to unleash fury on the unwary." + } } + +13. Hammer of Thunderbolts Entry: + + {'Hammer of Thunderbolts': { + 'Name': 'Hammer of Thunderbolts', + 'Type': 'Melee Weapon (maul, bludgeoning)', + 'Rarity': 'Legendary', + 'Value': '16000', + 'Damage': '2d6 + 1 (martial, bludgeoning)', + 'Properties': ["requires attunement","Giant's Bane","must be wearing a belt of giant strength and gauntlets of ogre power","Str +4","Can excees 20 but not 30","20 against giant, DC 17 save against death","5 charges, expend 1 to make a range attack 20/60","ranged attack releases thunderclap on hit, DC 17 save against stunned 30 ft","regain 1d4+1 charges at dawn"], + 'Weight': 15 lb', + 'Description': "God-forged and storm-bound, a supreme force, its rune-etched head blazing with power. More than a weapon, it's a symbol of nature's fury, capable of reshaping landscapes and commanding elements with every strike.", + 'Quote': "When the skies rage and the earth trembles, know that the Hammer of Thunderbolts has found its mark. It is not merely a weapon, but the embodiment of the storm\'s wrath wielded by those deemed worthy.", + 'SD Prompt': "It radiates with electric energy, its rune-etched head and storm-weathered leather grip symbolizing its dominion over storms. In its grasp, it pulses with the potential to summon the heavens' fury, embodying the tempest's raw power." + } } + +14. Shadow Lamp Entry: + + {'Shadow Lamp': { + 'Name': 'Shadow Lamp', + 'Type': 'Magical Item', + 'Rarity': 'Uncommon', + 'Value': '500 gp', + 'Properties': ["Provides dim light in a 20-foot radius", "Invisibility to darkness-based senses", "Can cast Darkness spell once per day"], + 'Weight': '1 lb', + 'Description': "A small lamp carved from obsidian and powered by a mysterious force, it casts an eerie glow that illuminates its surroundings while making the wielder invisible to those relying on darkness-based senses.", + 'Quote': "In the heart of shadow lies an unseen light, casting away darkness and revealing what was once unseen.", + 'SD Prompt': "Glass lantern filled with inky swirling shadows, black gaseous clouds flow out, blackness flows from it, spooky, sneaky" + } } + +15. Dark Mirror: + + {'Dark Mirror': { + 'Name': 'Dark Mirror', + 'Type': 'Magical Item', + 'Rarity': 'Rare', + 'Value': '600 gp', + 'Properties': ["Reflects only darkness when viewed from one side", "Grants invisibility to its reflection", "Can be used to cast Disguise Self spell once per day"], + 'Weight': '2 lb', + 'Description': "An ordinary-looking mirror with a dark, almost sinister tint. It reflects only darkness and distorted images when viewed from one side, making it an ideal tool for spies and those seeking to hide their true identity.", + 'Quote': "A glass that hides what lies within, a surface that reflects only darkness and deceit.", + 'SD Prompt': "Dark and mysterious black surfaced mirror with an obsidian flowing center with a tint of malice, its surface reflecting nothing but black and distorted images, swirling with tendrils, spooky, ethereal" + } } + +16. Moon-Touched Greatsword Entry: + + {'Moon-Touched Greatsword':{ + 'Name': 'Moontouched Greatsword', + 'Type': 'Melee Weapon (greatsword, slashing)', + 'Rarity': 'Very Rare', + 'Value': '8000 gp', + 'Damage': '2d6 + Str slashing', + 'Properties': ["Adds +2 to attack and damage rolls while wielder is under the effects of Moonbeam or Daylight spells", "Requires attunement"], + 'Weight': '6 lb', + 'Description': "Forged from lunar metal and imbued with celestial magic, this greatsword gleams like a silver crescent moon, its edge sharp enough to cut through the darkest shadows.", + 'Quote': "With each swing, it sings a melody of light that pierces the veil of darkness, a beacon of hope and justice.", + 'SD Prompt': "A silver greatsword with a crescent moon-shaped blade that reflects a soft glow, reminiscent of the moon's radiance. The hilt is wrapped in silvery leather, and the metal seems to shimmer and change with the light, reflecting the lunar cycles." + } } +""" diff --git a/main.py b/main.py new file mode 100644 index 0000000000000000000000000000000000000000..c28415dfbe2456ab28b06c55459557ab76c3cbd4 --- /dev/null +++ b/main.py @@ -0,0 +1,330 @@ + +import img2img +import card_generator as card +import utilities as u +import ctypes +import user_input as useri +import gradio as gr +import template_builder as tb +import threading +import time + + + +# This is a fix for the way that python doesn't release system memory back to the OS and it was leading to locking up the system +libc = ctypes.cdll.LoadLibrary("libc.so.6") +M_MMAP_THRESHOLD = -3 + +# Set malloc mmap threshold. +libc.mallopt(M_MMAP_THRESHOLD, 2**20) +initial_name = "A Crowbar" + + + + + +with gr.Blocks() as demo: + + # Functions and State Variables + # Build functions W/in the Gradio format, because it only allows modification within it's context + # Define inputs to match what is called on click, and output of the function as a list that matches the list of outputs + textbox_default_dict = {'Name':'', \ + 'Type': '', + 'Rarity':'', + 'Value':'', + 'Properties':'', + 'Damage':'', + 'Weight':'', + 'Description':'', + 'Quote':'', + 'SD Prompt':'' + } + + item_name_var = gr.State() + item_type_var = gr.State() + item_rarity_var = gr.State() + item_value_var = gr.State() + item_properties_var = gr.State() + item_damage_var = gr.State() + item_weight_var = gr.State() + item_description_var = gr.State() + item_quote_var = gr.State() + item_sd_prompt_var = gr.State('') + + selected_border_image = gr.State('./card_templates/Moonstone Border.png') + num_image_to_generate = gr.State(4) + generated_image_list = gr.State([]) + selected_generated_image = gr.State() + selected_seed_image = gr.State() + built_template = gr.State() + mimic = None + + def set_textbox_defaults(textbox_default_dict, key): + item_name = textbox_default_dict[key] + return item_name + + + + # Function called when user generates item info, then assign values of dictionary to variables, output once to State, twice to textbox + def generate_text_update_textboxes(user_input, progress = gr.Progress()): + u.reclaim_mem() + + # Define a function to update progress + def update_progress(duration, progress): + for i in range(10): + time.sleep(duration / 10) # Wait for a fraction of the total duration + progress((i + 1) / 10, desc="Thinking...") # Update progress + # Start the progress update in a separate thread, passing `progress` explicitly + threading.Thread(target=update_progress, args=(10, progress)).start() + + llm_output=useri.call_llm(user_input) + item_key = list(llm_output.keys()) + + item_key_values = list(llm_output[item_key[0]].keys()) + item_name = llm_output[item_key[0]]['Name'] + item_type = llm_output[item_key[0]]['Type'] + item_rarity = llm_output[item_key[0]]['Rarity'] + item_value = llm_output[item_key[0]]['Value'] + item_properties = llm_output[item_key[0]]['Properties'] + + if 'Damage' in item_key_values: + item_damage = llm_output[item_key[0]]['Damage'] + else: item_damage = '' + item_weight = llm_output[item_key[0]]['Weight'] + item_description = llm_output[item_key[0]]['Description'] + item_quote = llm_output[item_key[0]]['Quote'] + + sd_prompt = llm_output[item_key[0]]['SD Prompt'] + return [item_name, item_name, + item_type, item_type, + item_rarity, item_rarity, + item_value, item_value, + item_properties, item_properties, + item_damage, item_damage, + item_weight, item_weight, + item_description, item_description, + item_quote, item_quote, + sd_prompt, sd_prompt] + + # Called on user selecting an image from the gallery, outputs the path of the image + def assign_img_path(evt: gr.SelectData): + img_dict = evt.value + print(img_dict) + selected_image_path = img_dict['image']['url'] + print(selected_image_path) + return selected_image_path + + # Make a list of files in image_temp and delete them + def delete_temp_images(): + image_list = u.directory_contents('./image_temp') + u.delete_files(image_list) + img2img.image_list.clear() + + # Called when pressing button to generate image, updates gallery by returning the list of image URLs + def generate_image_update_gallery(num_img, sd_prompt,item_name, built_template): + delete_temp_images() + print(type(built_template)) + image_list = [] + img_gen, prompt = img2img.load_img_gen(sd_prompt, item_name) + for x in range(num_img): + preview = img2img.preview_and_generate_image(x,img_gen, prompt, built_template, item_name) + image_list.append(preview) + yield image_list + #generate_gallery.change(image_list) + del preview + u.reclaim_mem() + + #generated_image_list = img2img.generate_image(num_img,sd_prompt,item_name,selected_border) + return image_list + + def build_template(selected_border, selected_seed_image): + image_list = tb.build_card_template(selected_border, selected_seed_image) + return image_list, image_list + + + # Beginning of page format + # Title + gr.HTML("""
+

Item Card Generator

+

+ With this AI driven tool you will build a collectible style card of a fantasy flavored item with details. +

+
""") + gr.HTML("""
+

First: Build a Card Template

+
""") + with gr.Row(): + + # Template Gallery instructions + gr.HTML("""
+

1. Click a border from the 'Card Template Gallery'

+
""") + + border_gallery = gr.Gallery(label = "Card Template Gallery", + scale = 2, + value = useri.index_image_paths("./seed_images/card_templates/", "card_templates/"), + show_label = True, + columns = [3], rows = [3], + object_fit = "contain", + height = "auto", + elem_id = "Template Gallery") + gr.HTML("""
+

2. Click a image from the Seed Image Gallery


+
""") + border_gallery.select(assign_img_path, outputs = selected_border_image) + + seed_image_gallery = gr.Gallery(label= " Image Seed Gallery", + scale = 2, + value = useri.index_image_paths("./seed_images/item_seeds/","item_seeds/"), + show_label = True, + columns = [3], rows = [3], + object_fit = "contain", + height = "auto", + elem_id = "Template Gallery", + interactive=True) + + + gr.HTML("""

-Or- Upload your own seed image, by dropping it into the 'Generated Template Gallery'


+

3. Click 'Generate Card Template'


+
""") + + + built_template_gallery = gr.Gallery(label= "Generated Template Gallery", + scale = 1, + value = None, + show_label = True, + columns = [4], rows = [4], + object_fit = "contain", + height = "auto", + elem_id = "Template Gallery", + interactive=True) + + seed_image_gallery.select(assign_img_path, outputs = selected_seed_image) + built_template_gallery.upload(u.receive_upload, inputs=built_template_gallery, outputs= selected_seed_image) + + build_card_template_button = gr.Button(value = "Generate Card Template") + build_card_template_button.click(build_template, inputs = [selected_border_image, selected_seed_image], outputs = [built_template_gallery, built_template]) + + gr.HTML("""
+

Second: Generate Item Text

+
""") + gr.HTML("""
+

1. Use a few words to describe the item then click 'Generate Text'

+
""") + with gr.Row(): + + + user_input = gr.Textbox(label = 'Item', lines =1, placeholder= "Flaming Magical Sword", elem_id= "Item", scale =4) + item_text_generate = gr.Button(value = "Generate item text", scale=1) + + gr.HTML("""
+

2. Review and Edit the text

+
""") + with gr.Row(): + # Build text boxes for the broken up item dictionary values + + with gr.Column(scale = 1): + + + + item_name_output = gr.Textbox(value = set_textbox_defaults(textbox_default_dict, 'Name'),label = 'Name', lines = 1, interactive=True, elem_id='Item Name') + item_type_output = gr.Textbox(value = set_textbox_defaults(textbox_default_dict, 'Type'),label = 'Type', lines = 1, interactive=True, elem_id='Item Type') + item_rarity_output = gr.Textbox(value = set_textbox_defaults(textbox_default_dict, 'Rarity'),label = 'Rarity : [Common, Uncommon, Rare, Very Rare, Legendary]', lines = 1, interactive=True, elem_id='Item Rarity') + item_value_output = gr.Textbox(value = set_textbox_defaults(textbox_default_dict, 'Value'),label = 'Value', lines = 1, interactive=True, elem_id='Item Value') + + # Pass the user input and border template to the generator + with gr.Column(scale = 1): + item_damage_output = gr.Textbox(value = set_textbox_defaults(textbox_default_dict, 'Damage'),label = 'Damage', lines = 1, interactive=True, elem_id='Item Damage') + item_weight_output = gr.Textbox(value = set_textbox_defaults(textbox_default_dict, 'Weight'),label = 'Weight', lines = 1, interactive=True, elem_id='Item Weight') + item_description_output = gr.Textbox(value = set_textbox_defaults(textbox_default_dict, 'Description'),label = 'Description', lines = 1, interactive=True, elem_id='Item Description') + item_quote_output = gr.Textbox(value = set_textbox_defaults(textbox_default_dict, 'Quote'),label = 'Quote', lines = 1, interactive=True, elem_id='Item quote') + item_properties_output = gr.Textbox(value = set_textbox_defaults(textbox_default_dict, 'Properties'),label = 'Properties : [List of comma seperated values]', lines = 1, interactive=True, elem_id='Item Properties') + gr.HTML("""
+

3. This text will be used to generate the card's image.

+
""") + item_sd_prompt_output = gr.Textbox(label = 'Putting words or phrases in parenthesis adds weight. Example: (Flaming Magical :1.0) Sword.', value = set_textbox_defaults(textbox_default_dict, 'SD Prompt'), lines = 1, interactive=True, elem_id='SD Prompt') + + gr.HTML("""
+

Third: Click 'Generate Cards' to generate 4 cards to choose from.

+
""") + card_gen_button = gr.Button(value = "Generate Cards", elem_id="Generate Card Button") + + # No longer Row Context, in context of entire Block + gr.HTML("""
+

Fourth: Click your favorite card then add text, or click 'Generate Four Card Options' again.
+

+
""") + + with gr.Row(): + generate_gallery = gr.Gallery(label = "Generated Cards", + value = [], + show_label= True, + scale= 5, + columns =[2], rows = [2], + object_fit= "fill", + height = "768", + elem_id = "Generated Cards Gallery" + ) + generate_final_item_card = gr.Button(value = "Add Text", elem_id = "Generate user card") + + + card_gen_button.click(fn = generate_image_update_gallery, inputs =[num_image_to_generate,item_sd_prompt_output,item_name_output,built_template], outputs= generate_gallery) + generate_gallery.select(assign_img_path, outputs = selected_generated_image) + + # Button logice calls function when button object is pressed, passing inputs and passing output to components + llm_output = item_text_generate.click(generate_text_update_textboxes, + inputs = [user_input], + outputs= [item_name_var, + item_name_output, + item_type_var, + item_type_output, + item_rarity_var, + item_rarity_output, + item_value_var, + item_value_output, + item_properties_var, + item_properties_output, + item_damage_var, + item_damage_output, + item_weight_var, + item_weight_output, + item_description_var, + item_description_output, + item_quote_var, + item_quote_output, + item_sd_prompt_var, + item_sd_prompt_output]) + + + + generate_final_item_card.click(card.render_text_on_card, inputs = [selected_generated_image, + item_name_output, + item_type_output, + item_rarity_output, + item_value_output, + item_properties_output, + item_damage_output, + item_weight_output, + item_description_output, + item_quote_output + ], + outputs = generate_gallery ) + + + +if __name__ == '__main__': + demo.launch(server_name = "0.0.0.0", server_port = 8000, share = False, allowed_paths = ["/media/drakosfire/Shared/","/media/drakosfire/Shared/MerchantBot/card_templates"]) + + + + + + + + + + + + + + diff --git a/render_card_text.py b/render_card_text.py new file mode 100755 index 0000000000000000000000000000000000000000..2ddeee6e802c1a96050c10513bb1bfee2857cd4f --- /dev/null +++ b/render_card_text.py @@ -0,0 +1,97 @@ +from PIL import Image, ImageDraw, ImageFont + + # Function for managing longer bodies of text and breaking into a list of lines to be printed based on input arguments +def split_text_into_lines(text, font, max_width, draw): + blocks = text.split('\n') + lines = [] + for block in blocks: + words = block.split() + current_line = '' + + for word in words: + # Check width with new word added + test_line = f"{current_line} {word}".strip() + test_width = draw.textlength(text = test_line, font=font) + if test_width <= max_width: + current_line = test_line + else: + #If the line with the new word exceeds the max width, start a new line + lines.append(current_line) + current_line = word + # add the last line + lines.append(current_line) + return lines +# Function for calculating the height of the text at the current font setting + + +def adjust_font_size_lines_and_spacing(text, font_path, initial_font_size, max_width, area_height, image) : + font_size = initial_font_size + optimal_font_size = font_size + optimal_lines = [] + line_spacing_factor = 1.2 # multiple of font size that will get added between each line + + while font_size > 10: # Set minimum font size + font = ImageFont.truetype(font_path, font_size) + draw = ImageDraw.Draw(image) + # Fitting text into box dimensions + lines = split_text_into_lines(text, font, max_width, draw) + # Calculate total height with dynamic line spacing + single_line_height = draw.textbbox((0, 0), "Ay", font=font)[3] - draw.textbbox((0, 0), "Ay", font=font)[1] # Height of 'Ay' + line_spacing = int(single_line_height * line_spacing_factor) - single_line_height + total_text_height = len(lines) * single_line_height + (len(lines) - 1) * line_spacing # Estimate total height of all lines by multiplying number of lines by font height plus number of lines -1 times line spacing + + if total_text_height <= area_height : + optimal_font_size = font_size + optimal_lines = lines + break # Exit loop font fits in contraints + + else: + font_size -= 1 # Reduce font by 1 to check if it fits + + return optimal_font_size, optimal_lines, line_spacing +# Function that takes in an image,text and properties for textfrom card_generator +def render_text_with_dynamic_spacing(image, text, center_position, max_width, area_height,font_path, initial_font_size,description = None, quote = None): + + + optimal_font_size, optimal_lines, line_spacing = adjust_font_size_lines_and_spacing( + text, font_path, initial_font_size, max_width, area_height, image) + # create an object to draw on + + font = ImageFont.truetype(font_path, optimal_font_size) + draw = ImageDraw.Draw(image) + + # Shadow settings + shadow_offset = (1, 1) # X and Y offset for shadow + shadow_color = 'grey' # Shadow color + + # Unsure about the following line, not sure if I want y_offset to be dynamic + y_offset = center_position[1] + + if description or quote : + for line in optimal_lines: + line_width = draw.textlength(text = line, font=font) + x = center_position[0] + # Draw Shadow first + shadow_position = (x + shadow_offset[0], y_offset + shadow_offset[1]) + draw.text(shadow_position, line, font=font, fill=shadow_color) + #Draw text + draw.text((x, y_offset), line, font=font, fill = 'black', align = "left" ) + y_offset += optimal_font_size + line_spacing # Move to next line + return image + + for line in optimal_lines: + line_width = draw.textlength(text = line, font=font) + x = center_position[0] - (line_width / 2) + # Draw Shadow first + shadow_position = (x + shadow_offset[0], y_offset + shadow_offset[1]) + draw.text(shadow_position, line, font=font, fill=shadow_color) + #Draw text + draw.text((x, y_offset), line, font=font, fill = 'black', align = "left" ) + y_offset += optimal_font_size + line_spacing # Move to next line + return image + +# Function to put the description objects together, this will be the complicated bit, I think iterate through keys excluding title, type and cost + + + + diff --git a/template_builder.py b/template_builder.py new file mode 100644 index 0000000000000000000000000000000000000000..07ce182d2626324c18a46c216f35319fbf6a26e6 --- /dev/null +++ b/template_builder.py @@ -0,0 +1,73 @@ +from PIL import Image, ImageDraw, ImageFont +import utilities as u + +# Function to initialize image canvas + +# Function to scale the seed image + +# Paste seed image onto blank canvas at specific location + +# Paste the selected border on top + +# Save and return + +# Function that takes in an image url and a dictionary and uses the values to print onto a card. + +# Seed Image starting x,y +seed_x = 56 +seed_y = 128 +seed_width = 657 +seed_height = 422 + +def paste_image_and_resize(base_image,sticker, x_position, y_position,img_width, img_height): + + # Load the image to paste + # image_to_paste = Image.open(sticker_path) + + # Define the new size (scale) for the image you're pasting + + new_size = (img_width, img_height) + + # Resize the image to the new size + sticker = sticker.resize(new_size) + + # Specify the top-left corner where the resized image will be pasted + paste_position = (x_position, y_position) # Replace x and y with the coordinates + + # Paste the resized image onto the base image + base_image.paste(sticker, paste_position) + + return base_image + +def build_card_template(selected_border, selected_seed_image): + print(selected_seed_image) + print(type(selected_seed_image)) + selected_border = u.open_image_from_url(selected_border) + if type(selected_seed_image) == str: + print(f"String : {selected_seed_image}") + selected_seed_image = u.open_image_from_url(selected_seed_image) + + mask = selected_border.split()[3] + + image_list = [] + + # Image size parameters + width = 768 + height = 1024 + + # Set canvas as transparent + + background_color = (0,0,0,0) + + #initialize canvas + canvas = Image.new('RGB', (width, height), background_color) + + canvas = paste_image_and_resize(canvas, selected_seed_image,seed_x,seed_y, seed_width, seed_height) + + canvas.paste(selected_border,(0,0), mask = mask) + + image_list.append(canvas) + + return image_list + + \ No newline at end of file diff --git a/user_input.py b/user_input.py new file mode 100644 index 0000000000000000000000000000000000000000..05e7453d75225a5fb197a749bcfb0848002fcaa0 --- /dev/null +++ b/user_input.py @@ -0,0 +1,97 @@ +import item_dict_gen as igen +import img2img +import card_generator as card +import utilities as u +import sys +import tempfile +from PIL import Image + +image_path = str +end_phrase = """<|end_of_turn|>""" +# Indexing the contents of Card templates and temp images +card_template_path = "./card_templates/" +temp_image_path = "./image_temp" +def index_image_paths(directory_path, github_path): + list_temp_files = [] + list_of_image_paths = u.directory_contents(directory_path) + for image_path in list_of_image_paths: + image_path = f"https://raw.githubusercontent.com/Drakosfire/CardGenerator/alpha-templates/seed_images/{github_path}{image_path}" + print(image_path) + list_temp_files.append(image_path) + return list_temp_files + + +user_pick_template_prompt = "Pick a template number from this list : " +user_pick_image_prompt = "Select an image : " + +# Check if the user wants to exit the chatbot + +def user_exit_question(user_input): + if user_input.lower() in ['exit', 'quit']: + print("Chatbot session ended.") + sys.exit() +# Process the list of files in the card_template directory and print with corresponding numbers to index +def process_list_for_user_response(list_of_items): + x = 0 + for item in list_of_items: + print(f"{x} : {item}") + x += 1 + +def user_pick_item(user_prompt,list_of_items): + process_list_for_user_response(list_of_items) + user_input = input(user_prompt) + # Check if the user wants to exit the chatbot + user_exit_question(user_input) + return list_of_items[int(user_input)] + +def call_llm(user_input): + # Process the query and get the response + llm_call = igen.call_llm_and_cleanup(user_input) + response = llm_call['choices'][0]['text'] + + # Find the index of the phrase + index = response.find(end_phrase) + print(f"index = {index}") + if index != -1: + # Slice the string from the end of the phrase onwards + response = response[index + len(end_phrase):] + else: + # Phrase not found, optional handling + response = response + + response = response.replace("GPT4 Assistant: ", "") + response = igen.convert_to_dict(response) + if not response: + response = call_llm(user_input) + del llm_call + return response + +def prompt_user_input(): + mimic = None + while True: + user_input_item = input("Provide an item : ") + user_exit_question(user_input_item) + + if 'mimic' in user_input_item.lower(): + mimic = True + + #user_input_template = input(f"Pick a template number from this list : {process_list_for_user_response(list_of_card_templates)}") + user_input_template = user_pick_item(user_pick_template_prompt,list_of_card_templates) + response = call_llm(user_input_item) + print(response[u.keys_list(response,0)]) + output_dict = response[u.keys_list(response,0)] + u.reclaim_mem() + item_name = response[u.keys_list(response,0)]['Name'] + sd_prompt = response[u.keys_list(response,0)]['SD Prompt'] + image_path = img2img.generate_image(4,sd_prompt,item_name,user_input_template, mimic) + user_card_image = user_pick_item(user_pick_image_prompt, image_path) + + print(image_path) + + card.render_text_on_card(user_card_image, output_dict) + u.delete_files(img2img.image_list) + + + + + diff --git a/utilities.py b/utilities.py new file mode 100644 index 0000000000000000000000000000000000000000..dd25804b3f44f78bd525c27662f72ed9eeb8dcc2 --- /dev/null +++ b/utilities.py @@ -0,0 +1,62 @@ +# Create a list of hashmap key values . +import torch +import time +import gc +from io import BytesIO +import requests +import os +from PIL import Image +from pathlib import Path +# Utility Functions to be called from all modules + +# Function to return a list of keys of a nested dictionary using it's key value (item or creature) +def keys_list(dict, index): + keys_list=list(dict.keys()) + return keys_list[index] + +# Function to clear model from VRAM to make space for other model +def reclaim_mem(): + + print(f"Memory before del {torch.cuda.memory_allocated()}") + torch.cuda.ipc_collect() + gc.collect() + torch.cuda.empty_cache() + time.sleep(0.01) + print(f"Memory after del {torch.cuda.memory_allocated()}") + +#def del_object(object): + # del object + # gc.collect() + +# Create a list of a directory if directory exists +def directory_contents(directory_path): + if os.path.isdir(directory_path) : + contents = os.listdir(directory_path) + return contents + else : pass + +# Delete a list of file +def delete_files(file_paths): + + for file_path in file_paths: + try: + os.remove(f"./image_temp/{file_path}") + print(f"Remove : ./image_temp/{file_path}") + except OSError as e: + print(f"Error: {file_path} : {e.strerror}") + file_paths.clear() + + +def open_image_from_url(image_url): + response = requests.get(image_url) + image_data = BytesIO(response.content) + image = Image.open(image_data) + return image + +def receive_upload(image_file): + + image = Image.open(image_file[0][0]) + + print(image) + return image +