diff --git a/examples/ui_role.py b/examples/ui_role.py new file mode 100644 index 000000000..42b214178 --- /dev/null +++ b/examples/ui_role.py @@ -0,0 +1,294 @@ +# -*- coding: utf-8 -*- +# @Date : 2023/7/15 16:40 +# @Author : stellahong (stellahong@fuzhi.ai) +# @Desc : +import re +import os +from importlib import import_module +from functools import wraps + +from metagpt.logs import logger +from metagpt.actions import Action, ActionOutput +from metagpt.roles import ProductManager, Role +from metagpt.schema import Message +from metagpt.const import WORKSPACE_ROOT + +from metagpt.actions import WritePRD +from metagpt.software_company import SoftwareCompany +from metagpt.tools.sd_engine import SDEngine + +PROMPT_TEMPLATE = ''' +# Context +{context} + +## Format example +{format_example} +----- +Role: You are a UserInterface Designer; the goal is to finish a UI design according to PRD, give a design description, and select specified elements and UI style. +Requirements: Based on the context, fill in the following missing information, provide detailed HTML and CSS code +Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. + +## UI Design Description:Provide as Plain text, place the design objective here +## Selected Elements:Provide as Plain text, up to 5 specified elements, clear and simple +## HTML Layout:Provide as Plain text, use standard HTML code +## CSS Styles (styles.css):Provide as Plain text,use standard css code +## Anything UNCLEAR:Provide as Plain text. Make clear here. + +''' + +FORMAT_EXAMPLE = ''' + +## UI Design Description +```Snake games are classic and addictive games with simple yet engaging elements. Here are the main elements commonly found in snake games ``` + +## Selected Elements + +Game Grid: The game grid is a rectangular... + +Snake: The player controls a snake that moves across the grid... + +Food: Food items (often represented as small objects or differently colored blocks) + +Score: The player's score increases each time the snake eats a piece of food. The longer the snake becomes, the higher the score. + +Game Over: The game ends when the snake collides with itself or an obstacle. At this point, the player's final score is displayed, and they are given the option to restart the game. + + +## HTML Layout + + + + + + Snake Game + + + +
+ +
+
+ +
+ + + +## CSS Styles (styles.css) +body { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #f0f0f0; +} + +.game-grid { + width: 400px; + height: 400px; + display: grid; + grid-template-columns: repeat(20, 1fr); /* Adjust to the desired grid size */ + grid-template-rows: repeat(20, 1fr); + gap: 1px; + background-color: #222; + border: 1px solid #555; +} + +.game-grid div { + width: 100%; + height: 100%; + background-color: #444; +} + +.snake-segment { + background-color: #00cc66; /* Snake color */ +} + +.food { + width: 100%; + height: 100%; + background-color: #cc3300; /* Food color */ + position: absolute; +} + +/* Optional styles for a simple game over message */ +.game-over { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 24px; + font-weight: bold; + color: #ff0000; + display: none; +} + +## Anything UNCLEAR +There are no unclear points. + +''' + +OUTPUT_MAPPING = { + "UI Design Description": (str, ...), + "Selected Elements": (str, ...), + "HTML Layout": (str, ...), + "CSS Styles (styles.css)": (str, ...), + "Anything UNCLEAR": (str, ...), +} + + +def load_engine(func): + """Decorator to load an engine by file name and engine name.""" + + @wraps(func) + def wrapper(*args, **kwargs): + file_name, engine_name = func(*args, **kwargs) + engine_file = import_module(file_name, package='metagpt') + ip_module_cls = getattr(engine_file, engine_name) + try: + engine = ip_module_cls() + except: + engine = None + + return engine + + return wrapper + + +def parse(func): + """Decorator to parse information using regex pattern.""" + + @wraps(func) + def wrapper(*args, **kwargs): + context, pattern = func(*args, **kwargs) + match = re.search(pattern, context, re.DOTALL) + if match: + text_info = match.group(1) + logger.info(text_info) + else: + text_info = context + logger.info("未找到匹配的内容") + + return text_info + + return wrapper + + +class UIDesign(Action): + """Class representing the UI Design action.""" + + def __init__(self, name, context=None, llm=None): + super().__init__(name, context, llm) # 需要调用LLM进一步丰富UI设计的prompt + + @parse + def parse_requirement(self, context: str): + """Parse UI Design draft from the context using regex.""" + pattern = r"## UI Design draft.*?\n(.*?)## Anything UNCLEAR" + return context, pattern + + @parse + def parse_ui_elements(self, context: str): + """Parse Selected Elements from the context using regex.""" + pattern = r"## Selected Elements.*?\n(.*?)## HTML Layout" + return context, pattern + + @parse + def parse_css_code(self, context: str): + pattern = r"```css.*?\n(.*?)## Anything UNCLEAR" + return context, pattern + + @parse + def parse_html_code(self, context: str): + pattern = r"```html.*?\n(.*?)```" + return context, pattern + + async def draw_icons(self, context, *args, **kwargs): + """Draw icons using SDEngine.""" + engine = SDEngine() + icon_prompts = self.parse_ui_elements(context) + icons = icon_prompts.split("\n") + icons = [s for s in icons if len(s.strip()) > 0] + prompts_batch = [] + for icon_prompt in icons: + # fixme: 添加icon lora + prompt = engine.construct_payload(icon_prompt + ".") + prompts_batch.append(prompt) + await engine.run_t2i(prompts_batch) + logger.info("Finish icon design using StableDiffusion API") + + async def _save(self, css_content, html_content): + save_dir = WORKSPACE_ROOT / "UI" / 'docs/' + if not os.path.exists(save_dir): + os.makedirs(save_dir, exist_ok=True) + # Save CSS and HTML content to files + css_file_path = save_dir / f"ui_design.css" + html_file_path = save_dir / f"ui_design.html" + + with open(css_file_path, 'w') as css_file: + css_file.write(css_content) + with open(html_file_path, 'w') as html_file: + html_file.write(html_content) + + async def run(self, requirements: list[Message], *args, **kwargs) -> ActionOutput: + """Run the UI Design action.""" + # fixme: update prompt (根据需求细化prompt) + context = requirements[-1].content + ui_design_draft = self.parse_requirement(context=context) + # todo: parse requirements str + prompt = PROMPT_TEMPLATE.format(context=ui_design_draft, format_example=FORMAT_EXAMPLE) + logger.info(prompt) + ui_describe = await self._aask_v1(prompt, "ui_design", OUTPUT_MAPPING) + logger.info(ui_describe.content) + logger.info(ui_describe.instruct_content) + css = self.parse_css_code(context=ui_describe.content) + html = self.parse_html_code(context=ui_describe.content) + await self._save(css_content=css, html_content=html) + await self.draw_icons(ui_describe.content) + return ui_describe + + +class UI(Role): + """Class representing the UI Role.""" + + def __init__(self, name="Catherine", profile="UI Design", + goal="Finish a workable and good User Interface design based on a product design", + constraints="Give clear layout description and use standard icons to finish the design", + skills=["SD"]): + super().__init__(name, profile, goal, constraints) + self.load_skills(skills) + self._init_actions([UIDesign]) + self._watch([WritePRD]) + + @load_engine + def load_sd_engine(self): + """Load the SDEngine.""" + file_name = ".tools.sd_engine" + engine_name = "SDEngine" + return file_name, engine_name + + def load_skills(self, skills): + """Load skills for the UI Role.""" + # todo: 添加其他出图engine + for skill in skills: + if skill == "SD": + self.sd_engine = self.load_sd_engine() + logger.info(f"load skill engine {self.sd_engine}") + + +async def startup(idea: str, investment: float = 3.0, n_round: int = 5): + """Run a startup. Be a boss.""" + company = SoftwareCompany() + company.hire([ProductManager(), UI()]) + company.invest(investment) + company.start_project(idea) + await company.run(n_round=n_round) + + +if __name__ == "__main__": + import asyncio + from metagpt.utils.common import OutputParser, CodeParser + + # ws_name = CodeParser.parse_str(block="Python package name", text=context[-1].content) + event_loop = asyncio.get_event_loop() + result = event_loop.run_until_complete(startup(idea="贪食蛇小游戏", investment=3.0, n_round=3)) + diff --git a/metagpt/tools/sd_engine.py b/metagpt/tools/sd_engine.py new file mode 100644 index 000000000..c0d6b3d44 --- /dev/null +++ b/metagpt/tools/sd_engine.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +# @Date : 2023/7/19 16:28 +# @Author : stellahong (stellahong@fuzhi.ai) +# @Desc : +import os +import asyncio +from os.path import join +from typing import List +import json +import io +import base64 + +from aiohttp import ClientSession +from PIL import Image, PngImagePlugin + +from metagpt.logs import logger +from metagpt.config import Config +from metagpt.const import WORKSPACE_ROOT + +config = Config() + +payload = { + "prompt": "", + "negative_prompt": "(easynegative:0.8),black, dark,Low resolution", + "override_settings": { + "sd_model_checkpoint": "galaxytimemachinesGTM_photoV20" + }, + "seed": -1, + "batch_size": 1, + "n_iter": 1, + "steps": 20, + "cfg_scale": 7, + "width": 512, + "height": 768, + "restore_faces": False, + "tiling": False, + "do_not_save_samples": False, + "do_not_save_grid": False, + 'enable_hr': False, + 'hr_scale': 2, + 'hr_upscaler': 'Latent', + 'hr_second_pass_steps': 0, + 'hr_resize_x': 0, + 'hr_resize_y': 0, + 'hr_upscale_to_x': 0, + 'hr_upscale_to_y': 0, + 'truncate_x': 0, + 'truncate_y': 0, + 'applied_old_hires_behavior_to': None, + "eta": None, + + "sampler_index": "DPM++ SDE Karras", + "alwayson_scripts": {} +} + +default_negative_prompt = "(easynegative:0.8),black, dark,Low resolution" + + +class SDEngine: + def __init__(self): + # Initialize the SDEngine with configuration + self.config = Config() + self.sd_url = self.config.get('SD_URL') + self.sd_t2i_url = f"{self.sd_url}{self.config.get('SD_T2I_API')}" + # Define default payload settings for SD API + self.payload = payload + logger.info(self.sd_t2i_url) + + def construct_payload(self, prompt, negtive_prompt=default_negative_prompt, width=512, height=512, + sd_model="galaxytimemachinesGTM_photoV20"): + # Configure the payload with provided inputs + self.payload["prompt"] = prompt + self.payload["negtive_prompt"] = negtive_prompt + self.payload["width"] = width + self.payload["height"] = height + self.payload["override_settings"]["sd_model_checkpoint"] = sd_model + logger.info(f"call sd payload is {self.payload}") + return self.payload + + def _save(self, imgs, save_name=""): + # fixme:寻址 + save_dir = WORKSPACE_ROOT / "SD_Output" + if not os.path.exists(save_dir): + os.makedirs(save_dir, exist_ok=True) + batch_decode_base64_to_image(imgs, save_dir, save_name=save_name) + + async def run_t2i(self, prompts: List): + # Asynchronously run the SD API for multiple prompts + session = ClientSession() + for payload_idx, payload in enumerate(prompts): + results = await self.run(url=self.sd_t2i_url, payload=payload, session=session) + self._save(results, save_name=f"output_{payload_idx}") + await session.close() + + async def run(self, url, payload, session): + # Perform the HTTP POST request to the SD API + async with session.post(url, json=payload, timeout=600) as rsp: + data = await rsp.read() + + rsp_json = json.loads(data) + imgs = rsp_json['images'] + logger.info(f"callback rsp json is {rsp_json.keys()}") + return imgs + + async def run_i2i(self): + # todo: 添加图生图接口调用 + raise NotImplementedError + + async def run_sam(self): + # todo:添加SAM接口调用 + raise NotImplementedError + + +# async def run(url, payload, session): +# # Perform the HTTP POST request to the SD API +# async with session.post(url, json=payload, timeout=600) as rsp: +# data = await rsp.read() +# +# rsp_json = json.loads(data) +# imgs = rsp_json['images'] +# logger.info(f"callback rsp json is {rsp_json}") +# return imgs + + +def decode_base64_to_image(img, save_name): + image = Image.open(io.BytesIO(base64.b64decode(img.split(",", 1)[0]))) + pnginfo = PngImagePlugin.PngInfo() + logger.info(save_name) + image.save(f"{save_name}.png", pnginfo=pnginfo) + return pnginfo, image + + +def batch_decode_base64_to_image(imgs, save_dir="", save_name=""): + for idx, _img in enumerate(imgs): + save_name = join(save_dir, save_name) + decode_base64_to_image(_img, save_name=save_name) + + +if __name__ == "__main__": + import asyncio + + engine = SDEngine() + prompt = "pixel style, game design, a game interface should be minimalistic and intuitive with the score and high score displayed at the top. The snake and its food should be easily distinguishable. The game should have a simple color scheme, with a contrasting color for the snake and its food. Complete interface boundary" + + engine.construct_payload(prompt) + + event_loop = asyncio.get_event_loop() + event_loop.run_until_complete(engine.run_t2i(prompt))