Spaces:
Running
on
Zero
Running
on
Zero
| import os | |
| import subprocess | |
| import os | |
| from pathlib import Path | |
| BASE_DIR = Path("/home/user/app") | |
| commands = [ | |
| ("python -V", BASE_DIR), | |
| ("pip install -r my_requirements.txt", BASE_DIR) | |
| ] | |
| def run_command(cmd, cwd=None): | |
| result = subprocess.run( | |
| cmd, # ๆณจๆ๏ผ่ฟ้ไธๅไฝฟ็จ shlex.split() | |
| cwd=str(cwd) if cwd else None, | |
| shell=True, # ้่ฆ shell=True ๆฅๆฏๆ && ็ญๆไฝ็ฌฆ | |
| check=True, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| text=True | |
| ) | |
| print(f"[SUCCESS] {cmd}") | |
| if result.stdout: print(result.stdout) | |
| return True | |
| for cmd, cwd in commands: | |
| run_command(cmd, cwd) | |
| import re | |
| import gradio as gr | |
| import argparse | |
| import json | |
| import time | |
| from PIL import Image | |
| from gradio_image_annotation import image_annotator | |
| from utils.system_prompt import SHORT_SYSTEM_PROMPT_WITH_THINKING | |
| from utils.lua_converter import LuaConverter | |
| from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor | |
| from qwen_vl_utils import process_vision_info | |
| import torch | |
| from utils.lua2lrt import lua_to_lrtemplate | |
| from huggingface_hub import snapshot_download | |
| import spaces | |
| def extract_json_from_answer(answer): | |
| """ | |
| Extract configuration data from the answer string and convert to JSON | |
| Args: | |
| answer (str): The answer string containing configuration data | |
| Returns: | |
| list: List with exactly one configuration object | |
| """ | |
| import ast | |
| import re | |
| print(f"๐ Extracting configuration from answer...") | |
| def find_complete_dict(text, start_pos=0): | |
| """Find complete dictionary, handling nested cases""" | |
| brace_count = 0 | |
| start_found = False | |
| start_idx = 0 | |
| for i in range(start_pos, len(text)): | |
| char = text[i] | |
| if char == '{': | |
| if not start_found: | |
| start_idx = i | |
| start_found = True | |
| brace_count += 1 | |
| elif char == '}': | |
| brace_count -= 1 | |
| if brace_count == 0 and start_found: | |
| return text[start_idx:i+1] | |
| return None | |
| # Method 1: Find complete dictionary structure | |
| dict_start = answer.find('{') | |
| if dict_start != -1: | |
| complete_dict_str = find_complete_dict(answer, dict_start) | |
| if complete_dict_str: | |
| print(f"Found complete dictionary, length: {len(complete_dict_str)}") | |
| config_dict = ast.literal_eval(complete_dict_str) | |
| if isinstance(config_dict, dict) and len(config_dict) > 0: | |
| print(f"โ Successfully extracted configuration with {len(config_dict)} parameters") | |
| print(f"๐ฆ Config keys: {list(config_dict.keys())[:10]}...") # Show first 10 keys | |
| return [config_dict] | |
| # Method 2: Fallback to original method (if new method fails) | |
| print("๐ Falling back to regex pattern matching...") | |
| # Find Python dict pattern in answer | |
| # Look for patterns like "{'key': value, ...}" | |
| dict_pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}' | |
| matches = re.findall(dict_pattern, answer, re.DOTALL) | |
| print(f"Found {len(matches)} potential matches") | |
| # Find the largest match | |
| largest_match = None | |
| largest_size = 0 | |
| for match in matches: | |
| # Try to parse as Python dict using ast.literal_eval | |
| config_dict = ast.literal_eval(match) | |
| if isinstance(config_dict, dict) and len(config_dict) > largest_size: | |
| largest_match = config_dict | |
| largest_size = len(config_dict) | |
| print(f"๐ฆ Found larger config with {len(config_dict)} parameters") | |
| if largest_match: | |
| print(f"โ Successfully extracted configuration with {len(largest_match)} parameters") | |
| print(f"๐ฆ Config keys: {list(largest_match.keys())[:10]}...") # Show first 10 keys | |
| return [largest_match] | |
| print("โ No valid configuration data found in answer") | |
| return [] | |
| def json_to_lua(json_data, save_folder, filename="config.lua"): | |
| """ | |
| Convert JSON data to Lua format and save to file | |
| Args: | |
| json_data (dict or str): JSON data to convert | |
| save_folder (str): Folder to save the Lua file | |
| filename (str): Filename for the Lua file | |
| Returns: | |
| tuple: (file_path, error_message) | |
| """ | |
| try: | |
| # Ensure save folder exists | |
| os.makedirs(save_folder, exist_ok=True) | |
| # Parse JSON if it's a string | |
| if isinstance(json_data, str): | |
| try: | |
| json_obj = json.loads(json_data) | |
| except: | |
| return None, f"Error parsing JSON: Invalid JSON format" | |
| else: | |
| json_obj = json_data | |
| save_path = os.path.join(save_folder, filename) | |
| # Convert to Lua format using LuaConverter | |
| try: | |
| lua_content = LuaConverter.to_lua(json_obj) | |
| with open(save_path, "w", encoding="utf-8") as f: | |
| f.write('return %s' % lua_content) | |
| return save_path, None | |
| except Exception as e: | |
| return None, f"Error writing Lua file: {str(e)}" | |
| except Exception as e: | |
| return None, f"Error in json_to_lua: {str(e)}" | |
| # Model downloader | |
| def download_tools_ckpts(target_dir, url): | |
| from huggingface_hub import snapshot_download | |
| import os | |
| import shutil | |
| tmp_dir = "hf_temp_download" | |
| os.makedirs(tmp_dir, exist_ok=True) | |
| snapshot_download( | |
| repo_id="JarvisArt/JarvisArt-Preview", | |
| repo_type="model", | |
| local_dir=tmp_dir, | |
| allow_patterns=os.path.join(url, "**"), | |
| local_dir_use_symlinks=False, | |
| ) | |
| src_dir = os.path.join(tmp_dir, url) | |
| shutil.copytree(src_dir, target_dir) | |
| shutil.rmtree(tmp_dir) | |
| def download_model(model_path): | |
| """ | |
| Download model from HuggingFace if not exists locally | |
| Args: | |
| model_path (str): Path to save the model | |
| """ | |
| if not os.path.exists(model_path): | |
| download_tools_ckpts(model_path, "") | |
| else: | |
| print(f"โ Model already exists at {model_path}") | |
| # Local model client class | |
| class LocalModelClient: | |
| def __init__(self, model_path): | |
| """ | |
| Initialize local model client | |
| Args: | |
| model_path (str): Path to the model directory | |
| """ | |
| self.model_path = model_path | |
| self.model = None | |
| self.processor = None | |
| self.model_loaded = False | |
| # Download model if needed | |
| download_model(model_path) | |
| # Load model | |
| self._load_model() | |
| def _load_model(self): | |
| """ | |
| Load the model and processor | |
| """ | |
| print(f"๐ Loading model from {self.model_path}...") | |
| # Model configuration | |
| min_pixels = 256 * 28 * 28 | |
| max_pixels = 1280 * 28 * 28 | |
| # Load model | |
| self.model = Qwen2_5_VLForConditionalGeneration.from_pretrained( | |
| self.model_path, | |
| torch_dtype="auto", | |
| device_map="auto" | |
| ) | |
| # Load processor | |
| self.processor = AutoProcessor.from_pretrained(self.model_path) | |
| print(f"โ Model loaded successfully from {self.model_path}") | |
| self.model_loaded = True | |
| def chat(self, messages, system=None, images=None, **kwargs): | |
| """ | |
| Chat with model using local inference | |
| Args: | |
| messages (list): Message list | |
| system (str): System prompt | |
| images (list): Image path list | |
| **kwargs: Other parameters | |
| Returns: | |
| list: List containing Response objects | |
| """ | |
| if not self.model_loaded: | |
| class Response: | |
| def __init__(self, text): | |
| self.response_text = text | |
| return [Response("โ Model not loaded. Please check model path and try again.")] | |
| # Prepare message format | |
| formatted_messages = [] | |
| # Add system message | |
| if system: | |
| formatted_messages.append({ | |
| "role": "system", | |
| "content": [{"type": "text", "text": system}] | |
| }) | |
| # Process user messages and images | |
| for msg in messages: | |
| if images and msg["role"] == "user": | |
| # Format message with image and text | |
| content = [] | |
| # Add images | |
| for img_path in images: | |
| content.append({ | |
| "type": "image", | |
| "image": f"file://{img_path}" | |
| }) | |
| # Add text | |
| content.append({ | |
| "type": "text", | |
| "text": msg["content"] | |
| }) | |
| formatted_messages.append({ | |
| "role": msg["role"], | |
| "content": content | |
| }) | |
| else: | |
| # Regular text message | |
| formatted_messages.append({ | |
| "role": msg["role"], | |
| "content": msg["content"] | |
| }) | |
| # Apply chat template | |
| text = self.processor.apply_chat_template( | |
| formatted_messages, tokenize=False, add_generation_prompt=True | |
| ) | |
| # Process vision info | |
| image_inputs, video_inputs = process_vision_info(formatted_messages) | |
| # Prepare inputs | |
| inputs = self.processor( | |
| text=[text], | |
| images=image_inputs, | |
| videos=video_inputs, | |
| padding=True, | |
| return_tensors="pt", | |
| ) | |
| # Move inputs to device | |
| device = next(self.model.parameters()).device | |
| inputs = inputs.to(device) | |
| # Generate response | |
| generated_ids = self.model.generate(**inputs, max_new_tokens=10240) | |
| generated_ids_trimmed = [ | |
| out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids) | |
| ] | |
| output_text = self.processor.batch_decode( | |
| generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False | |
| ) | |
| # Create Response object for compatibility | |
| class Response: | |
| def __init__(self, text): | |
| self.response_text = text | |
| # Return list containing Response object | |
| return [Response(output_text[0])] | |
| # Parse command line arguments | |
| def parse_args(): | |
| parser = argparse.ArgumentParser(description="JarvisArt Gradio Demo") | |
| parser.add_argument( | |
| "--model_path", | |
| type=str, | |
| default="./models/JarvisArt-Preview", | |
| help="Path to the local model directory" | |
| ) | |
| parser.add_argument( | |
| "--server_port", | |
| type=int, | |
| default=7860, # Change to standard Gradio port | |
| help="Port for the Gradio server" | |
| ) | |
| parser.add_argument( | |
| "--server_name", | |
| type=str, | |
| default="0.0.0.0", | |
| help="Server name/IP for the Gradio server" | |
| ) | |
| parser.add_argument( | |
| "--share", | |
| action="store_true", | |
| help="Enable public sharing via Gradio tunnel (creates public URL)" | |
| ) | |
| return parser.parse_args() | |
| # Get command line arguments | |
| args = parse_args() | |
| # Avatar file path configuration | |
| SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| USER_AVATAR_PATH = os.path.join(SCRIPT_DIR, "assets", "user_avatar.svg") | |
| AI_AVATAR_PATH = os.path.join(SCRIPT_DIR, "assets", "ai_avatar.svg") | |
| # System prompt - pre-designed system prompt | |
| system_prompt = SHORT_SYSTEM_PROMPT_WITH_THINKING | |
| # Default user prompt | |
| default_user_prompt = "I want a dreamy, romantic sunset vibe with soft, warm colors and gentle glow." | |
| # Initialize local model client | |
| print(f"Initializing local model from {args.model_path}...") | |
| print("๐ง Model configuration:") | |
| print(f" ๐ Model Path: {args.model_path}") | |
| chat_model = LocalModelClient(model_path=args.model_path) | |
| # Check model loading status | |
| if hasattr(chat_model, 'model_loaded') and chat_model.model_loaded: | |
| print("โ Model loaded successfully!") | |
| else: | |
| print("โ ๏ธ Model loading failed or incomplete") | |
| print("๐ก Common solutions:") | |
| print(f" 1. Check if model exists at {args.model_path}") | |
| print(" 2. Ensure sufficient GPU memory") | |
| print(" 3. Check internet connection for model download") | |
| print(" 4. Try using --model_path parameter to specify different path") | |
| print("="*60) | |
| def load_examples_data(): | |
| """ | |
| Load examples data from examples folder | |
| Returns list of example data dictionaries | |
| """ | |
| examples_data = [] | |
| examples_dir = os.path.join(os.path.dirname(__file__), "examples") | |
| if not os.path.exists(examples_dir): | |
| return examples_data | |
| # Get all subdirectories | |
| subdirs = [d for d in os.listdir(examples_dir) if os.path.isdir(os.path.join(examples_dir, d))] | |
| subdirs.sort(key=lambda x: int(x) if x.isdigit() else float('inf')) | |
| for subdir in subdirs: | |
| subdir_path = os.path.join(examples_dir, subdir) | |
| example_data = { | |
| 'id': subdir, | |
| 'original_image': None, | |
| 'processed_image': None, | |
| 'user_prompt': None, | |
| 'config_lua': None, | |
| 'config_path': None | |
| } | |
| # Check for required files | |
| original_path = os.path.join(subdir_path, "original.jpg") | |
| processed_path = os.path.join(subdir_path, "processed.jpg") | |
| prompt_path = os.path.join(subdir_path, "user_prompt.txt") | |
| config_path = os.path.join(subdir_path, "config_1.lua") | |
| if os.path.exists(original_path): | |
| example_data['original_image'] = original_path | |
| if os.path.exists(processed_path): | |
| example_data['processed_image'] = processed_path | |
| if os.path.exists(prompt_path): | |
| with open(prompt_path, 'r', encoding='utf-8') as f: | |
| example_data['user_prompt'] = f.read().strip() | |
| if os.path.exists(config_path): | |
| with open(config_path, 'r', encoding='utf-8') as f: | |
| config_content = f.read().strip() | |
| # Remove "return {" from the beginning and "}" from the end | |
| origin_content = config_content | |
| config_content = re.sub(r'^return\s*\{', ' ', config_content) | |
| config_content = re.sub(r'\}$', '', config_content.strip()) | |
| example_data['config_lua'] = config_content | |
| example_data['origin'] = origin_content | |
| example_data['config_path'] = config_path | |
| examples_data.append(example_data) | |
| return examples_data | |
| def download_example_preset(config_lua_content, example_id): | |
| """ | |
| Convert lua content to lrtemplate and return file for download | |
| """ | |
| if not config_lua_content: | |
| return None | |
| try: | |
| # Create temporary lua file | |
| import tempfile | |
| with tempfile.NamedTemporaryFile(mode='w', suffix='.lua', delete=False, encoding='utf-8') as temp_lua: | |
| temp_lua.write(config_lua_content) | |
| temp_lua_path = temp_lua.name | |
| # Convert to lrtemplate | |
| lrtemplate_path = lua_to_lrtemplate(temp_lua_path) | |
| # Clean up temp lua file | |
| os.unlink(temp_lua_path) | |
| return lrtemplate_path | |
| except Exception as e: | |
| print(f"Error converting example {example_id} to lrtemplate: {e}") | |
| return None | |
| def get_box_coordinates(annotated_image_dict, prompt_original): | |
| """ | |
| Processes the output from the image_annotator to extract | |
| and format the bounding box coordinates. | |
| Only keeps the latest box when multiple boxes are drawn. | |
| """ | |
| global local_dict | |
| if annotated_image_dict and annotated_image_dict.get("boxes") and len(annotated_image_dict["boxes"]) > 0: | |
| # Get the last drawn box | |
| input_image = annotated_image_dict["image"] | |
| # Handle both PIL Image and file path cases | |
| if isinstance(input_image, str): | |
| # If it's a file path | |
| pil_image = Image.open(input_image) | |
| image_key = input_image | |
| else: | |
| # If it's a PIL Image object | |
| pil_image = input_image | |
| image_key = str(input_image) # Use string representation as key | |
| last_box = annotated_image_dict["boxes"][0] | |
| height, width = pil_image.shape[:2] | |
| # Normalize coordinates | |
| xmin = last_box["xmin"] / width | |
| ymin = last_box["ymin"] / height | |
| xmax = last_box["xmax"] / width | |
| ymax = last_box["ymax"] / height | |
| local_dict[image_key] = [xmin, ymin, xmax, ymax] | |
| # Check if prompt has <box> tag and modify accordingly | |
| if "<box>" in prompt_original and "</box>" in prompt_original: | |
| # Replace the content inside existing <box> tags | |
| modified_prompt = re.sub(r'<box>.*?</box>', f'<box>{str([xmin, ymin, xmax, ymax])}</box>', prompt_original) | |
| else: | |
| # Add <box> tag at the beginning | |
| modified_prompt = f"In the region <box>{str([xmin, ymin, xmax, ymax])}</box>, {prompt_original}" | |
| return str([xmin, ymin, xmax, ymax]), modified_prompt | |
| return "No box drawn", prompt_original | |
| def process_image_analysis_stream(image_dict, user_prompt, max_new_tokens, top_k, top_p, temperature): | |
| """ | |
| ๆตๅผๅพๅๅๆๅค็ๅฝๆฐ๏ผๅ่mm_qwen2vl.py็TextIteratorStreamerๅฎ็ฐ | |
| Args: | |
| image_dict: ๅพๅๅญๅ ธ | |
| user_prompt: ็จๆทๆ็คบ่ฏ | |
| max_new_tokens, top_k, top_p, temperature: ็ๆๅๆฐ | |
| Yields: | |
| tuple: (่ๅคฉๅๅฒ, ไธ่ฝฝๆไปถๅ่กจ) | |
| """ | |
| import html # ็งปๅฐๅฝๆฐๅผๅคด | |
| global local_dict, chat_model | |
| # ๆฃๆฅๅพๅ่พๅ ฅ | |
| if image_dict is None: | |
| yield [{"role": "user", "content": "่ฏทๅ ไธไผ ๅพ็๏ผ"}, {"role": "assistant", "content": "ๆ้่ฆๅพ็ๆ่ฝ่ฟ่กๅๆใ"}], None | |
| return | |
| # ๅค็ๅพๅ | |
| image = image_dict | |
| if not isinstance(image, str): | |
| import tempfile | |
| from PIL import Image as PILImage | |
| temp_path = os.path.join(tempfile.gettempdir(), f"temp_image_{hash(str(image))}.png") | |
| # ๅฐ numpy ๆฐ็ป่ฝฌๆขไธบ PIL Image | |
| if hasattr(image, 'shape'): # ็กฎ่ฎคๆฏ numpy ๆฐ็ป | |
| pil_img = PILImage.fromarray(image) | |
| pil_img.save(temp_path) | |
| else: | |
| # ๅฆๆๅทฒ็ปๆฏ PIL Image | |
| image.save(temp_path) | |
| image = temp_path | |
| # ๅค็ๆ็คบ่ฏ | |
| if not user_prompt.strip(): | |
| user_prompt = default_user_prompt | |
| # ๆๅปบๆถๆฏๆ ผๅผ๏ผๅ่mm_qwen2vl.py็ๆ ผๅผ | |
| messages = [{ | |
| "role": "user", | |
| "content": [ | |
| {"type": "image", "image": f"file://{image}"}, | |
| {"type": "text", "text": user_prompt} | |
| ] | |
| }] | |
| if system_prompt: | |
| formatted_messages = [ | |
| {"role": "system", "content": [{"type": "text", "text": system_prompt}]} | |
| ] + messages | |
| else: | |
| formatted_messages = messages | |
| print("======debug=====") | |
| print(formatted_messages) | |
| # ๅบ็จ่ๅคฉๆจกๆฟ | |
| text = chat_model.processor.apply_chat_template( | |
| formatted_messages, tokenize=False, add_generation_prompt=True | |
| ) | |
| # ๅค็่ง่งไฟกๆฏ | |
| image_inputs, video_inputs = process_vision_info(formatted_messages) | |
| # ้ขๅค็่พๅ ฅ | |
| inputs = chat_model.processor( | |
| text=[text], | |
| images=image_inputs, | |
| videos=video_inputs, | |
| padding=True, | |
| return_tensors="pt", | |
| ) | |
| # ็งปๅจๅฐ่ฎพๅค | |
| device = next(chat_model.model.parameters()).device | |
| inputs = inputs.to(device) | |
| # ๅๅปบๆตๅผ่พๅบๅจ๏ผๅ่mm_qwen2vl.py | |
| from transformers import TextIteratorStreamer, GenerationConfig | |
| streamer = TextIteratorStreamer( | |
| chat_model.processor.tokenizer, | |
| timeout=20.0, | |
| skip_prompt=True, | |
| skip_special_tokens=True | |
| ) | |
| # ็ๆ้ ็ฝฎ | |
| gen_kwargs = ( | |
| dict(temperature=temperature, top_p=top_p, top_k=top_k) | |
| if temperature > 0 | |
| else dict(do_sample=False) | |
| ) | |
| gen_config = GenerationConfig(max_new_tokens=max_new_tokens, **gen_kwargs) | |
| # ไฝฟ็จ็บฟ็จๅฏๅจๆจกๅๆจ็๏ผๅ่mm_qwen2vl.py | |
| from threading import Thread | |
| thread = Thread( | |
| target=chat_model.model.generate, | |
| kwargs=dict( | |
| **inputs, | |
| generation_config=gen_config, | |
| streamer=streamer, | |
| ), | |
| ) | |
| thread.start() | |
| # ๅๅงๅ่ๅคฉๅๅฒ | |
| chat_history = [{"role": "user", "content": user_prompt}, {"role": "assistant", "content": ""}] | |
| yield chat_history, None | |
| # ๆตๅผๆฅๆถ่พๅบ๏ผๅ่mm_qwen2vl.py็botๅฝๆฐ | |
| full_response = "" | |
| for new_token in streamer: | |
| full_response += new_token | |
| # ๆ ผๅผๅๆพ็คบๅ ๅฎน๏ผ็กฎไฟ<think>ๅ<answer>้ฝ่ฝๆญฃ็กฎๆพ็คบ | |
| display_content = full_response | |
| # ๆฃๆฅๆฏๅฆๅ ๅซthinkๆ ็ญพ๏ผ่ฟ่กๆ ผๅผๅๆพ็คบ | |
| if "<think>" in display_content: | |
| think_match = re.search(r'<think>(.*?)</think>', display_content, re.DOTALL) | |
| if think_match: | |
| # ๅฎๆด็thinkๆ ็ญพ | |
| think_content = think_match.group(1).strip() | |
| # HTML่ฝฌไนๅ ๅฎน | |
| think_content_escaped = html.escape(think_content).replace('\n', '<br/>') | |
| formatted_content = f'<div style="background: linear-gradient(135deg, #e3f2fd, #f3e5f5); border-left: 4px solid #2196f3; padding: 12px; margin: 8px 0; border-radius: 8px;"><strong style="color: #1976d2; font-size: 1.1em;">๐ค Thinking</strong><br/><span style="color: #424242; line-height: 1.5;">{think_content_escaped}</span></div>\n\n' | |
| # ๆฃๆฅๆฏๅฆ่ฟๆanswer้จๅ | |
| if "<answer>" in display_content: | |
| answer_match = re.search(r'<answer>(.*?)</answer>', display_content, re.DOTALL) | |
| if answer_match: | |
| answer_content = answer_match.group(1).strip() | |
| formatted_content += f'**๐ฏAnswer๏ผ**\n{answer_content}' | |
| else: | |
| # answerๆ ็ญพๆชๅฎๆด๏ผๆพ็คบๅฝๅๅ ๅฎน | |
| answer_start = display_content.find("<answer>") | |
| if answer_start != -1: | |
| partial_answer = display_content[answer_start + 8:].strip() | |
| if partial_answer: | |
| formatted_content += f'**๐ฏAnswer๏ผ**\n{partial_answer}' | |
| display_content = formatted_content | |
| else: | |
| # ๆ<think>ๆ ็ญพไฝๆชๅฎๆด๏ผไป็ถๆ ผๅผๅๆพ็คบ | |
| think_start = display_content.find("<think>") | |
| if think_start != -1: | |
| partial_think = display_content[think_start + 7:].strip() | |
| partial_think_escaped = html.escape(partial_think).replace('\n', '<br/>') | |
| formatted_content = f'<div style="background: linear-gradient(135deg, #e3f2fd, #f3e5f5); border-left: 4px solid #2196f3; padding: 12px; margin: 8px 0; border-radius: 8px;"><strong style="color: #1976d2; font-size: 1.1em;">๐ค Thinking</strong><br/><span style="color: #424242; line-height: 1.5;">{partial_think_escaped}</span></div>' | |
| # ๆฃๆฅๆฏๅฆ่ฟๆanswer้จๅ | |
| if "<answer>" in display_content: | |
| answer_start = display_content.find("<answer>") | |
| if answer_start != -1: | |
| partial_answer = display_content[answer_start + 8:].strip() | |
| if partial_answer: | |
| formatted_content += f'\n\n**๐ฏAnswer๏ผ**\n{partial_answer}' | |
| display_content = formatted_content | |
| else: | |
| display_content = full_response | |
| # ๅฆๆๆฒกๆthinkๆ ็ญพ๏ผๆพ็คบๅๅงๅ ๅฎน๏ผๅ้้ป่พ๏ผ | |
| else: | |
| display_content = full_response | |
| chat_history[-1]["content"] = display_content | |
| yield chat_history, None | |
| # ๅค็ๅฎๆๅ็ๆไปถ็ๆ | |
| thread.join() # ็ญๅพ ็บฟ็จๅฎๆ | |
| # ๆฃๆฅ็ๆๆฏๅฆๅฎๆด | |
| print(f"๐ ็ๆๅฎๆ๏ผๅๅบ้ฟๅบฆ: {len(full_response)}") | |
| print(f"๐ ๆฏๅฆๅ ๅซ<think>: {'<think>' in full_response}") | |
| print(f"๐ ๆฏๅฆๅ ๅซ</think>: {'</think>' in full_response}") | |
| print(f"๐ ๆฏๅฆๅ ๅซ<answer>: {'<answer>' in full_response}") | |
| print(f"๐ ๆฏๅฆๅ ๅซ</answer>: {'</answer>' in full_response}") | |
| # ๅฆๆๅๅบไธๅฎๆด๏ผๅจ่ๅคฉ็้ขๆพ็คบ่ญฆๅ | |
| if '<think>' in full_response and '</think>' not in full_response: | |
| chat_history[-1]["content"] += "\n\nโ ๏ธ ๅๅบๅฏ่ฝไธๅฎๆด๏ผๆ่้จๅๆช็ปๆ" | |
| yield chat_history, None | |
| elif '<answer>' in full_response and '</answer>' not in full_response: | |
| chat_history[-1]["content"] += "\n\nโ ๏ธ ๅๅบๅฏ่ฝไธๅฎๆด๏ผ็ญๆก้จๅๆช็ปๆ" | |
| yield chat_history, None | |
| # ๆๅJSON้ ็ฝฎๅนถ่ฝฌๆขไธบLightroom้ข่ฎพ | |
| answer_match = re.search(r'<answer>(.*?)</answer>', full_response, re.DOTALL) | |
| if answer_match: | |
| answer_content = answer_match.group(1).strip() | |
| else: | |
| answer_content = full_response | |
| json_objects = extract_json_from_answer(answer_content) | |
| download_files = [] | |
| if json_objects: | |
| # ๅๅปบ็ปๆ็ฎๅฝ | |
| script_dir = os.path.dirname(os.path.abspath(__file__)) | |
| results_dir = os.path.join(script_dir, "results") | |
| os.makedirs(results_dir, exist_ok=True) | |
| # ๅๅปบไผ่ฏ็ฎๅฝ | |
| timestamp = int(time.time()) | |
| session_folder_name = f"session_{timestamp}" | |
| session_dir = os.path.join(results_dir, session_folder_name) | |
| os.makedirs(session_dir, exist_ok=True) | |
| # ไฟๅญ้ ็ฝฎๆไปถ | |
| for i, json_obj in enumerate(json_objects): | |
| filename = f"config_{i+1}.lua" | |
| lua_path, error = json_to_lua(json_obj, session_dir, filename) | |
| if lua_path: | |
| lrtemplate_path = lua_to_lrtemplate(lua_path) | |
| # ไฝฟ็จpreset_name้ๅฝๅๆไปถ | |
| preset_name_match = re.search(r'title = "([^"]+)"', open(lrtemplate_path, 'r', encoding='utf-8').read()) | |
| if preset_name_match: | |
| preset_name = preset_name_match.group(1) | |
| new_filename = f"{preset_name}.lrtemplate" | |
| new_path = os.path.join(os.path.dirname(lrtemplate_path), new_filename) | |
| os.rename(lrtemplate_path, new_path) | |
| lrtemplate_path = new_path | |
| download_files.append(lrtemplate_path) | |
| print(f"โ ็ๆLightroom้ข่ฎพ: {lrtemplate_path}") | |
| # ๆ็ป่พๅบ | |
| yield chat_history, download_files if download_files else None | |
| # Create Gradio interface | |
| def create_interface(): | |
| """ | |
| Create and configure the Gradio web interface similar to example.py style | |
| Returns: | |
| gr.Blocks: Configured Gradio interface | |
| """ | |
| with gr.Blocks(title="JarvisArt: AI-Powered Photo Retouching Assistant", theme=gr.themes.Soft()) as demo: | |
| # Header with title | |
| gr.HTML(""" | |
| <div style="display: flex; align-items: center; justify-content: center; gap: 15px; margin-bottom: 20px;"> | |
| <h1 style="margin: 0; font-size: 2.5em;">๐จ JarvisArt: AI-Powered Photo Retouching Assistant</h1> | |
| <a href="https://github.com/LYL1015/JarvisArt" target="_blank" style="text-decoration: none;"> | |
| <div style="display: inline-flex; align-items: center; height: 20px; border-radius: 3px; overflow: hidden; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 11px; font-weight: 400; box-shadow: 0 1px 2px rgba(0,0,0,0.1); transition: all 0.2s ease;"> | |
| <div style="background-color: #24292e; color: white; padding: 3px 6px; display: flex; align-items: center; justify-content: center; height: 100%;"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="white" style="margin-right: 3px;"> | |
| <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/> | |
| </svg> | |
| </div> | |
| <div style="background-color: #0366d6; color: white; padding: 3px 8px; height: 100%; display: flex; align-items: center;"> | |
| Code | |
| </div> | |
| </div> | |
| </a> | |
| </div> | |
| <p style="text-align: center; color: #666; margin-top: 10px;">Upload an image, describe your vision, and get professional Lightroom presets!</p> | |
| """) | |
| # Important notices - collapsible | |
| with gr.Accordion("โ ๏ธ Important Notice", open=False): | |
| gr.HTML(""" | |
| <div style="background: linear-gradient(135deg, #fff3cd, #ffeaa7); border: 1px solid #ffc107; border-radius: 8px; padding: 16px; margin: 8px 0;"> | |
| <h4 style="color: #856404; margin-top: 0;">๐ Important Information:</h4> | |
| <ul style="color: #856404; margin-bottom: 0;"> | |
| <li><strong>Preview Version:</strong> This is currently a preview version. We will be releasing an updated version with enhanced features and support for localized editing capabilities in the future.</li> | |
| <li><strong>Image Copyright:</strong> Some example images are collected from the internet and related communities. If there are any copyright concerns, please contact us for removal.</li> | |
| </ul> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| # Input image upload component | |
| input_image = gr.Image( | |
| label="๐ธ Upload Your Image", | |
| type="pil", | |
| height=500 | |
| ) | |
| # Prompt input | |
| user_prompt = gr.Textbox( | |
| label="๐ฌ Describe Your Vision", | |
| placeholder="Describe your desired retouching style... (e.g., 'I want a dreamy, romantic sunset vibe with soft, warm colors')", | |
| lines=8, | |
| max_lines=10 | |
| ) | |
| # Process button | |
| process_btn = gr.Button( | |
| "โจ Generate Recommendations & Presets", | |
| variant="primary", | |
| size="lg" | |
| ) | |
| # Add some spacing to match right column height | |
| gr.Markdown("") # Empty space | |
| gr.Markdown("") # Empty space | |
| with gr.Column(scale=1): | |
| # Chat interface to show analysis | |
| chatbot = gr.Chatbot( | |
| label="๐ฌ AI Analysis & Recommendations", | |
| height=400, | |
| show_label=True, | |
| type="messages" | |
| ) | |
| # Download section | |
| download_files = gr.File( | |
| label="๐พ Download Lightroom Preset Files", | |
| file_count="multiple", | |
| visible=True, | |
| interactive=False | |
| ) | |
| # Lightroom import guide (collapsible) | |
| with gr.Accordion("๐จ How to use Lightroom Presets:", open=True): | |
| gr.Markdown(""" | |
| ### How to Import Lightroom Presets: | |
| 1. Open Adobe Lightroom | |
| 2. Go to the **Presets** panel | |
| 3. Click on the **+** icon | |
| 4. Select **Import Presets** | |
| 5. Choose the `.lrtemplate` file(s) you downloaded and click **Import** | |
| The imported presets will now be available in your Presets panel for use on your photos. | |
| """) | |
| # Quick examples | |
| gr.Markdown("### ๐ก Example Prompts:") | |
| examples = [ | |
| "Warm, vintage style with soft tones and dreamy haze for a nostalgic feel.", | |
| "I desire a Melancholy Blues style to evoke a deeper, more reflective mood in the scene.", | |
| "I aim for a striking, high-contrast image with sharp details and a cold, immersive feel. Bold style.", | |
| "Minimal Ethereal style, clean composition, soft filters for tranquil, airy feel.", | |
| "Make it edgy with high contrast and a modern, urban feel, please." | |
| ] | |
| example_buttons = [] | |
| with gr.Row(): | |
| for i, example in enumerate(examples): | |
| btn = gr.Button( | |
| example[:30] + "...", | |
| size="sm", | |
| scale=1 | |
| ) | |
| example_buttons.append(btn) | |
| # Bind click event to set prompt | |
| btn.click( | |
| lambda ex=example: ex, | |
| outputs=user_prompt | |
| ) | |
| # Example pairs with card-style layout | |
| gr.Markdown("### ๐ Example Pairs") | |
| gr.HTML(""" | |
| <style> | |
| .example-card { | |
| background: linear-gradient(145deg, #ffffff, #f8f9fa); | |
| border: 1px solid #e9ecef; | |
| border-radius: 16px; | |
| padding: 24px; | |
| margin: 16px 0; | |
| box-shadow: 0 4px 16px rgba(0,0,0,0.1); | |
| transition: all 0.3s ease; | |
| } | |
| .example-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 24px rgba(0,0,0,0.15); | |
| } | |
| .card-header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white !important; | |
| padding: 12px 20px; | |
| border-radius: 12px; | |
| margin-bottom: 20px; | |
| text-align: center; | |
| font-weight: bold; | |
| font-size: 1.1em; | |
| } | |
| .instruction-box { | |
| background: linear-gradient(135deg, #667eea20, #764ba220); | |
| border-left: 4px solid #667eea; | |
| padding: 16px; | |
| border-radius: 8px; | |
| margin-top: 12px; | |
| font-style: italic; | |
| } | |
| .config-container { | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| padding: 12px; | |
| border: 1px solid #dee2e6; | |
| } | |
| .card-header-btn { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| color: white !important; | |
| border: none !important; | |
| border-radius: 12px !important; | |
| margin-bottom: 20px !important; | |
| font-weight: bold !important; | |
| font-size: 1.1em !important; | |
| width: 100% !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .card-header-btn:hover { | |
| transform: translateY(-1px) !important; | |
| box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3) !important; | |
| } | |
| </style> | |
| """) | |
| # Load examples data | |
| examples_data = load_examples_data() | |
| if examples_data: | |
| # Store click buttons for event binding | |
| example_buttons = [] | |
| # Create card-style layout for each example | |
| for i, example in enumerate(examples_data): | |
| # Card container - make it clickable | |
| with gr.Column(elem_classes="example-card"): | |
| # Card header with click functionality | |
| card_click_btn = gr.Button( | |
| f"๐จ Example {example['id']} - Click to Use", | |
| variant="secondary", | |
| size="lg", | |
| elem_classes="card-header-btn" | |
| ) | |
| # Store button and example data for later binding | |
| example_buttons.append((card_click_btn, example)) | |
| # Card content | |
| with gr.Row(): | |
| # Left column - Original Image & Instruction | |
| with gr.Column(scale=2): | |
| gr.Markdown("#### ๐ธ Original Image") | |
| if example['original_image']: | |
| gr.Image( | |
| example['original_image'], | |
| label=None, | |
| height=280, | |
| interactive=False, | |
| show_label=False, | |
| container=False, | |
| show_download_button=False, | |
| elem_classes="card-image" | |
| ) | |
| else: | |
| gr.Markdown("*No image available*") | |
| # Instruction with card styling | |
| if example['user_prompt']: | |
| gr.HTML(f''' | |
| <div class="instruction-box"> | |
| <strong>๐ฌ Instruction:</strong><br/> | |
| "{example['user_prompt']}" | |
| </div> | |
| ''') | |
| else: | |
| gr.Markdown("*No instruction available*") | |
| # Middle column - Retouching Settings | |
| with gr.Column(scale=3): | |
| gr.Markdown("#### โ๏ธ Retouching Settings") | |
| if example['config_lua']: | |
| with gr.Column(elem_classes="config-container"): | |
| config_preview = example['config_lua'][:450] + "..." if len(example['config_lua']) > 450 else example['config_lua'] | |
| gr.Code( | |
| config_preview, | |
| label="Configuration Preview", | |
| language="javascript", # Use javascript for better syntax highlighting | |
| lines=14, | |
| max_lines=14, | |
| interactive=False | |
| ) | |
| # Download section | |
| preset_file = download_example_preset(example['origin'], example['id']) | |
| if preset_file: | |
| gr.File( | |
| value=preset_file, | |
| label=f"๐ฅ Download Lightroom Preset", | |
| interactive=False, | |
| visible=True | |
| ) | |
| else: | |
| gr.Markdown("*No configuration available*") | |
| # Right column - Processed Image | |
| with gr.Column(scale=2): | |
| gr.Markdown("#### โจ Processed Image") | |
| if example['processed_image']: | |
| gr.Image( | |
| example['processed_image'], | |
| label=None, | |
| height=280, | |
| interactive=False, | |
| show_label=False, | |
| container=False, | |
| show_download_button=False, | |
| elem_classes="card-image" | |
| ) | |
| else: | |
| gr.Markdown("*No result image available*") | |
| else: | |
| gr.Markdown("*No examples found in examples folder*") | |
| # Advanced parameter control panel (collapsible) | |
| with gr.Accordion("โ๏ธ Advanced Generation Parameters", open=False): | |
| gr.Markdown("### Generation Parameter Controls") | |
| with gr.Row(): | |
| max_new_tokens = gr.Slider( | |
| minimum=512, | |
| maximum=20480, | |
| value=10240, | |
| step=256, | |
| label="Max New Tokens", | |
| info="Maximum number of tokens to generate" | |
| ) | |
| with gr.Row(): | |
| top_k = gr.Slider( | |
| minimum=1, | |
| maximum=100, | |
| value=50, | |
| step=1, | |
| label="Top-K", | |
| info="Sample from top K tokens with highest probability" | |
| ) | |
| top_p = gr.Slider( | |
| minimum=0.1, | |
| maximum=1.0, | |
| value=0.8, | |
| step=0.05, | |
| label="Top-P (Nucleus Sampling)", | |
| info="Cumulative probability threshold, controls generation diversity" | |
| ) | |
| with gr.Row(): | |
| temperature = gr.Slider( | |
| minimum=0.1, | |
| maximum=2.0, | |
| value=0.7, | |
| step=0.1, | |
| label="Temperature", | |
| info="Generation randomness, higher values mean more random" | |
| ) | |
| with gr.Row(): | |
| reset_params_btn = gr.Button( | |
| "๐ Reset to Default", | |
| variant="secondary", | |
| size="sm" | |
| ) | |
| # Preset parameter buttons | |
| conservative_btn = gr.Button( | |
| "๐ฏ Conservative", | |
| variant="secondary", | |
| size="sm" | |
| ) | |
| creative_btn = gr.Button( | |
| "๐จ Creative", | |
| variant="secondary", | |
| size="sm" | |
| ) | |
| balanced_btn = gr.Button( | |
| "โ๏ธ Balanced", | |
| variant="secondary", | |
| size="sm" | |
| ) | |
| # Instructions section at the bottom | |
| gr.Markdown("### ๐ How to Use:") | |
| gr.Markdown(""" | |
| 1. **Upload an image** you'd like to enhance (any photo format) | |
| 2. **Describe your vision** - what style or mood you want to achieve | |
| 3. **Click 'Generate'** to get AI-powered retouching recommendations | |
| 4. **Download** the generated Lightroom preset files (.lrtemplate) | |
| 5. **Import** the presets into Adobe Lightroom (see guide above) | |
| """) | |
| # Event binding - simplified to match test1.py working pattern | |
| # ไธปๅค็ๆ้ฎ - ๆตๅผ่พๅบ | |
| process_btn.click( | |
| fn=process_image_analysis_stream, | |
| inputs=[input_image, user_prompt, max_new_tokens, top_k, top_p, temperature], | |
| outputs=[chatbot, download_files] | |
| ) | |
| # ๅ่ฝฆๆไบค - ๆตๅผ่พๅบ | |
| user_prompt.submit( | |
| fn=process_image_analysis_stream, | |
| inputs=[input_image, user_prompt, max_new_tokens, top_k, top_p, temperature], | |
| outputs=[chatbot, download_files] | |
| ) | |
| # Parameter control events | |
| # Reset parameters | |
| reset_params_btn.click( | |
| lambda: (10240, 50, 0.8, 0.7), | |
| outputs=[max_new_tokens, top_k, top_p, temperature] | |
| ) | |
| # Conservative mode: more deterministic output | |
| conservative_btn.click( | |
| lambda: (8192, 30, 0.6, 0.3), | |
| outputs=[max_new_tokens, top_k, top_p, temperature] | |
| ) | |
| # Creative mode: more diverse output | |
| creative_btn.click( | |
| lambda: (12288, 80, 0.9, 1.0), | |
| outputs=[max_new_tokens, top_k, top_p, temperature] | |
| ) | |
| # Balanced mode: balance determinism and creativity | |
| balanced_btn.click( | |
| lambda: (10240, 50, 0.8, 0.7), | |
| outputs=[max_new_tokens, top_k, top_p, temperature] | |
| ) | |
| # Bind example card click events | |
| if 'example_buttons' in locals() and example_buttons: | |
| for card_btn, example in example_buttons: | |
| def create_click_handler(ex): | |
| def handler(): | |
| image_path = ex['original_image'] if ex['original_image'] else None | |
| prompt_text = ex['user_prompt'] if ex['user_prompt'] else "" | |
| return image_path, prompt_text | |
| return handler | |
| card_btn.click( | |
| fn=create_click_handler(example), | |
| outputs=[input_image, user_prompt] | |
| ) | |
| return demo | |
| if __name__ == "__main__": | |
| local_dict={} | |
| latest_session_dir = None # Track latest session for downloads | |
| print("="*60) | |
| print("๐จ Starting JarvisArt Image Retouching Recommendation Assistant") | |
| print("="*60) | |
| print("๐ง System Configuration Information:") | |
| print(f" ๐ Model Path: {args.model_path}") | |
| print(f" ๐ Web Interface: {args.server_name}:{args.server_port}") | |
| print(f" ๐ CUDA Device: {os.environ.get('CUDA_VISIBLE_DEVICES', 'Auto')}") | |
| print("="*60) | |
| # Setup tips | |
| print("๐ Setup Check Tips:") | |
| print(" If you encounter issues, please check:") | |
| print(" 1. Is the model downloaded successfully?") | |
| print(" 2. Is there sufficient GPU memory available?") | |
| print(" 3. Are the required dependencies installed?") | |
| print(" 4. Check internet connection for model download") | |
| print() | |
| print("๐ก Custom Parameter Examples:") | |
| print(" python gradio_demo.py --model_path ./models/custom-model-path") | |
| print(" pip install gradio_image_annotation # Install required dependency") | |
| print("="*60) | |
| demo = create_interface() | |
| # Launch the Gradio app on specified host and port | |
| print(f"๐ Starting Web Interface...") | |
| demo.launch( | |
| server_name=args.server_name, | |
| server_port=args.server_port, | |
| share=args.share, # Enable public sharing if requested | |
| show_error=True | |
| ) | |
| print(f"โ JarvisArt interface started successfully!") | |
| print(f"๐ Local access: http://{args.server_name}:{args.server_port}") | |
| if args.share: | |
| print(f"๐ Public sharing enabled! Check the 'public URL' link in the console output above") | |
| print(f" Share link valid for: 72 hours") | |
| print(f" โ ๏ธ Note: Anyone with the share link can access your app") | |
| else: | |
| print(f"โน๏ธ Public sharing disabled, accessible only locally/within LAN") | |
| print(f" Enable sharing: Add --share parameter") | |
| if args.server_name == "0.0.0.0": | |
| print(f"๐ Local access: http://localhost:{args.server_port}") | |
| print(f"๐ LAN access: http://<your IP address>:{args.server_port}") | |