akhaliq/anycoder
3.3k
1"""2Standalone deployment utilities for publishing to HuggingFace Spaces.3No Gradio dependencies - can be used in backend API.4"""5import os6import re7import json8import uuid9import tempfile10import shutil11import ast12from typing import Dict, List, Optional, Tuple13from pathlib import Path14 15from huggingface_hub import HfApi16from backend_models import get_inference_client, get_real_model_id17from backend_parsers import (18 parse_transformers_js_output,19 parse_html_code,20 parse_python_requirements,21 parse_multi_file_python_output,22 parse_react_output,23 strip_tool_call_markers,24 remove_code_block,25 extract_import_statements,26 generate_requirements_txt_with_llm,27 enforce_critical_versions28)29 30 31def prettify_comfyui_json_for_html(json_content: str) -> str:32 """Convert ComfyUI JSON to stylized HTML display with download button"""33 try:34 # Parse and prettify the JSON35 parsed_json = json.loads(json_content)36 prettified_json = json.dumps(parsed_json, indent=2, ensure_ascii=False)37 38 # Create Apple-style HTML wrapper39 html_content = f"""<!DOCTYPE html>40<html lang="en">41<head>42 <meta charset="UTF-8">43 <meta name="viewport" content="width=device-width, initial-scale=1.0">44 <title>ComfyUI Workflow</title>45 <style>46 * {{47 margin: 0;48 padding: 0;49 box-sizing: border-box;50 }}51 body {{52 font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif;53 background-color: #000000;54 color: #f5f5f7;55 line-height: 1.6;56 padding: 20px;57 min-height: 100vh;58 }}59 .container {{60 max-width: 1200px;61 margin: 0 auto;62 }}63 .header {{64 text-align: center;65 margin-bottom: 40px;66 padding: 40px 20px;67 }}68 .header h1 {{69 font-size: 48px;70 font-weight: 600;71 color: #ffffff;72 margin-bottom: 12px;73 letter-spacing: -0.02em;74 }}75 .header p {{76 font-size: 18px;77 color: #86868b;78 font-weight: 400;79 }}80 .controls {{81 display: flex;82 gap: 12px;83 margin-bottom: 24px;84 justify-content: center;85 }}86 .btn {{87 padding: 12px 24px;88 border: none;89 border-radius: 24px;90 font-size: 14px;91 font-weight: 500;92 cursor: pointer;93 transition: all 0.2s;94 font-family: inherit;95 }}96 .btn-primary {{97 background: #ffffff;98 color: #000000;99 }}100 .btn-primary:hover {{101 background: #f5f5f7;102 transform: scale(0.98);103 }}104 .btn-secondary {{105 background: #1d1d1f;106 color: #f5f5f7;107 border: 1px solid #424245;108 }}109 .btn-secondary:hover {{110 background: #2d2d2f;111 transform: scale(0.98);112 }}113 .json-container {{114 background-color: #1d1d1f;115 border-radius: 16px;116 padding: 32px;117 overflow-x: auto;118 border: 1px solid #424245;119 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);120 }}121 pre {{122 margin: 0;123 font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;124 font-size: 13px;125 line-height: 1.6;126 white-space: pre-wrap;127 word-wrap: break-word;128 }}129 .json-key {{130 color: #9cdcfe;131 }}132 .json-string {{133 color: #ce9178;134 }}135 .json-number {{136 color: #b5cea8;137 }}138 .json-boolean {{139 color: #569cd6;140 }}141 .json-null {{142 color: #569cd6;143 }}144 .success {{145 color: #30d158;146 }}147 @media (max-width: 768px) {{148 .header h1 {{149 font-size: 32px;150 }}151 .controls {{152 flex-direction: column;153 }}154 .json-container {{155 padding: 20px;156 }}157 }}158 </style>159</head>160<body>161 <div class="container">162 <div class="header">163 <h1>ComfyUI Workflow</h1>164 <p>View and download your workflow JSON</p>165 </div>166 167 <div class="controls">168 <button class="btn btn-primary" onclick="downloadJSON()">Download JSON</button>169 <button class="btn btn-secondary" onclick="copyToClipboard()">Copy to Clipboard</button>170 </div>171 172 <div class="json-container">173 <pre id="json-content">{prettified_json}</pre>174 </div>175 </div>176 177 <script>178 function copyToClipboard() {{179 const jsonContent = document.getElementById('json-content').textContent;180 navigator.clipboard.writeText(jsonContent).then(() => {{181 const btn = event.target;182 const originalText = btn.textContent;183 btn.textContent = 'Copied!';184 btn.classList.add('success');185 setTimeout(() => {{186 btn.textContent = originalText;187 btn.classList.remove('success');188 }}, 2000);189 }}).catch(err => {{190 alert('Failed to copy to clipboard');191 }});192 }}193 194 function downloadJSON() {{195 const jsonContent = document.getElementById('json-content').textContent;196 const blob = new Blob([jsonContent], {{ type: 'application/json' }});197 const url = URL.createObjectURL(blob);198 const a = document.createElement('a');199 a.href = url;200 a.download = 'comfyui_workflow.json';201 document.body.appendChild(a);202 a.click();203 document.body.removeChild(a);204 URL.revokeObjectURL(url);205 206 const btn = event.target;207 const originalText = btn.textContent;208 btn.textContent = 'Downloaded!';209 btn.classList.add('success');210 setTimeout(() => {{211 btn.textContent = originalText;212 btn.classList.remove('success');213 }}, 2000);214 }}215 216 // Add syntax highlighting217 function highlightJSON() {{218 const content = document.getElementById('json-content');219 let html = content.innerHTML;220 221 // Highlight different JSON elements222 html = html.replace(/"([^"]+)":/g, '<span class="json-key">"$1":</span>');223 html = html.replace(/: "([^"]*)"/g, ': <span class="json-string">"$1"</span>');224 html = html.replace(/: (-?\\d+\\.?\\d*)/g, ': <span class="json-number">$1</span>');225 html = html.replace(/: (true|false)/g, ': <span class="json-boolean">$1</span>');226 html = html.replace(/: null/g, ': <span class="json-null">null</span>');227 228 content.innerHTML = html;229 }}230 231 // Apply syntax highlighting after page load232 window.addEventListener('load', highlightJSON);233 </script>234</body>235</html>"""236 return html_content237 except json.JSONDecodeError:238 # If it's not valid JSON, return as-is wrapped in basic HTML239 return f"""<!DOCTYPE html>240<html lang="en">241<head>242 <meta charset="UTF-8">243 <meta name="viewport" content="width=device-width, initial-scale=1.0">244 <title>ComfyUI Workflow</title>245 <style>246 body {{247 font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;248 background-color: #000000;249 color: #f5f5f7;250 padding: 40px;251 }}252 pre {{253 background: #1d1d1f;254 padding: 24px;255 border-radius: 12px;256 overflow-x: auto;257 }}258 </style>259</head>260<body>261 <h1>ComfyUI Workflow</h1>262 <p>Error: Invalid JSON format</p>263 <pre>{json_content}</pre>264</body>265</html>"""266 except Exception as e:267 print(f"Error prettifying ComfyUI JSON: {e}")268 return json_content269 270 271# Note: parse_transformers_js_output, parse_python_requirements, strip_tool_call_markers,272# remove_code_block, extract_import_statements, generate_requirements_txt_with_llm,273# and parse_multi_file_python_output are now imported from backend_parsers.py274 275 276def is_streamlit_code(code: str) -> bool:277 """Check if code is Streamlit"""278 return 'import streamlit' in code or 'streamlit.run' in code279 280 281def is_gradio_code(code: str) -> bool:282 """Check if code is Gradio or Daggr"""283 return 'import gradio' in code or 'gr.' in code or 'import daggr' in code or 'from daggr' in code284 285 286def detect_sdk_from_code(code: str, language: str) -> str:287 """Detect the appropriate SDK from code and language"""288 if language == "html":289 return "static"290 elif language == "transformers.js":291 return "static"292 elif language == "comfyui":293 return "static"294 elif language == "react":295 return "docker"296 elif language == "streamlit" or is_streamlit_code(code):297 return "docker"298 elif language == "gradio" or language == "daggr" or is_gradio_code(code):299 return "gradio"300 else:301 return "gradio" # Default302 303 304def add_anycoder_tag_to_readme(api, repo_id: str, app_port: Optional[int] = None, sdk: Optional[str] = None) -> None:305 """306 Download existing README, add anycoder tag and app_port if needed, and upload back.307 Preserves all existing README content and frontmatter.308 309 Args:310 api: HuggingFace API client311 repo_id: Repository ID (username/space-name)312 app_port: Optional port number to set for Docker spaces (e.g., 7860)313 sdk: Optional SDK type (e.g., 'gradio', 'streamlit', 'docker', 'static')314 """315 try:316 import tempfile317 import re318 319 # Download the existing README320 readme_path = api.hf_hub_download(321 repo_id=repo_id,322 filename="README.md",323 repo_type="space"324 )325 326 # Read the existing README content327 with open(readme_path, 'r', encoding='utf-8') as f:328 content = f.read()329 330 # Parse frontmatter and content331 if content.startswith('---'):332 # Split frontmatter and body333 parts = content.split('---', 2)334 if len(parts) >= 3:335 frontmatter = parts[1].strip()336 body = parts[2] if len(parts) > 2 else ""337 338 # Check if tags already exist339 if 'tags:' in frontmatter:340 # Add anycoder to existing tags if not present341 if '- anycoder' not in frontmatter:342 frontmatter = re.sub(r'(tags:\s*\n(?:\s*-\s*[^\n]+\n)*)', r'\1- anycoder\n', frontmatter)343 else:344 # Add tags section with anycoder345 frontmatter += '\ntags:\n- anycoder'346 347 # Add app_port if specified and not already present348 if app_port is not None and 'app_port:' not in frontmatter:349 frontmatter += f'\napp_port: {app_port}'350 351 # For Gradio spaces, always set sdk_version to 6.0.2352 if sdk == 'gradio':353 if 'sdk_version:' in frontmatter:354 # Update existing sdk_version355 frontmatter = re.sub(r'sdk_version:\s*[^\n]+', 'sdk_version: 6.0.2', frontmatter)356 print(f"[README] Updated sdk_version to 6.0.2 for Gradio space")357 else:358 # Add sdk_version359 frontmatter += '\nsdk_version: 6.0.2'360 print(f"[README] Added sdk_version: 6.0.2 for Gradio space")361 362 # Reconstruct the README363 new_content = f"---\n{frontmatter}\n---{body}"364 else:365 # Malformed frontmatter, just add tags at the end of frontmatter366 new_content = content.replace('---', '---\ntags:\n- anycoder\n---', 1)367 else:368 # No frontmatter, add it at the beginning369 app_port_line = f'\napp_port: {app_port}' if app_port else ''370 sdk_version_line = '\nsdk_version: 6.0.2' if sdk == 'gradio' else ''371 new_content = f"---\ntags:\n- anycoder{app_port_line}{sdk_version_line}\n---\n\n{content}"372 373 # Upload the modified README374 with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding='utf-8') as f:375 f.write(new_content)376 temp_path = f.name377 378 api.upload_file(379 path_or_fileobj=temp_path,380 path_in_repo="README.md",381 repo_id=repo_id,382 repo_type="space"383 )384 385 os.unlink(temp_path)386 387 except Exception as e:388 print(f"Warning: Could not modify README.md to add anycoder tag: {e}")389 390 391def create_dockerfile_for_streamlit(space_name: str) -> str:392 """Create Dockerfile for Streamlit app"""393 return f"""FROM python:3.11-slim394 395WORKDIR /app396 397COPY requirements.txt .398RUN pip install --no-cache-dir -r requirements.txt399 400COPY . .401 402EXPOSE 7860403 404CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]405"""406 407 408def create_dockerfile_for_react(space_name: str) -> str:409 """Create Dockerfile for React app"""410 return f"""FROM node:18-slim411 412# Use existing node user413USER node414ENV HOME=/home/node415ENV PATH=/home/node/.local/bin:$PATH416 417WORKDIR /home/node/app418 419COPY --chown=node:node package*.json ./420RUN npm install421 422COPY --chown=node:node . .423RUN npm run build424 425EXPOSE 7860426 427CMD ["npm", "start", "--", "-p", "7860"]428"""429 430 431def extract_space_id_from_history(history: Optional[List], username: Optional[str] = None) -> Optional[str]:432 """433 Extract existing space ID from chat history (for updates after followups/imports)434 435 Args:436 history: Chat history (list of lists [[role, content], ...] or list of dicts)437 username: Current username (to verify ownership of imported spaces)438 439 Returns:440 Space ID (username/space-name) if found, None otherwise441 """442 if not history:443 return None444 445 import re446 existing_space = None447 448 # Look through history for previous deployments or imports449 for msg in history:450 # Handle both list format [[role, content], ...] and dict format [{'role': ..., 'content': ...}, ...]451 if isinstance(msg, list) and len(msg) >= 2:452 role = msg[0]453 content = msg[1]454 elif isinstance(msg, dict):455 role = msg.get('role', '')456 content = msg.get('content', '')457 else:458 continue459 460 # Check assistant messages for deployment confirmations461 if role == 'assistant':462 # Look for various deployment success patterns (case-insensitive)463 content_lower = content.lower()464 has_deployment_indicator = (465 "deployed" in content_lower or 466 "updated" in content_lower or467 "✅" in content # Check mark often indicates deployment success468 )469 470 if has_deployment_indicator:471 # Look for space URL pattern472 match = re.search(r'huggingface\.co/spaces/([^/\s\)]+/[^/\s\)]+)', content)473 if match:474 existing_space = match.group(1)475 print(f"[Extract Space] Found existing space: {existing_space}")476 break477 478 # Check user messages for imports479 elif role == 'user':480 if "import" in content.lower() and "space" in content.lower():481 # Extract space name from import message482 match = re.search(r'huggingface\.co/spaces/([^/\s\)]+/[^/\s\)]+)', content)483 if match:484 imported_space = match.group(1)485 # Only use imported space if user owns it (can update it)486 if username and imported_space.startswith(f"{username}/"):487 existing_space = imported_space488 break489 # If user doesn't own the imported space, we'll create a new one490 # (existing_space remains None, triggering new deployment)491 492 return existing_space493 494 495def deploy_to_huggingface_space(496 code: str,497 language: str,498 space_name: Optional[str] = None,499 token: Optional[str] = None,500 username: Optional[str] = None,501 description: Optional[str] = None,502 private: bool = False,503 existing_repo_id: Optional[str] = None,504 commit_message: Optional[str] = None,505 history: Optional[List[Dict]] = None506) -> Tuple[bool, str, Optional[str]]:507 """508 Deploy code to HuggingFace Spaces (create new or update existing)509 510 Args:511 code: Generated code to deploy512 language: Target language/framework (html, gradio, streamlit, react, transformers.js, comfyui)513 space_name: Name for the space (auto-generated if None, ignored if existing_repo_id provided)514 token: HuggingFace API token515 username: HuggingFace username516 description: Space description517 private: Whether to make the space private (only for new spaces)518 existing_repo_id: If provided (username/space-name), updates this space instead of creating new one519 commit_message: Custom commit message (defaults to "Deploy from anycoder" or "Update from anycoder")520 history: Chat history (list of dicts with 'role' and 'content') - used to detect followups/imports521 522 Returns:523 Tuple of (success: bool, message: str, space_url: Optional[str])524 """525 if not token:526 token = os.getenv("HF_TOKEN")527 if not token:528 return False, "No HuggingFace token provided", None529 530 try:531 api = HfApi(token=token)532 533 # Get username if not provided (needed for history tracking)534 if not username:535 try:536 user_info = api.whoami()537 username = user_info.get("name") or user_info.get("preferred_username") or "user"538 except Exception as e:539 pass # Will handle later if needed540 541 # Check history for existing space if not explicitly provided542 # This enables automatic updates for followup prompts and imported spaces543 if not existing_repo_id and history:544 existing_repo_id = extract_space_id_from_history(history, username)545 if existing_repo_id:546 print(f"[Deploy] Detected existing space from history: {existing_repo_id}")547 548 # Determine if this is an update or new deployment549 is_update = existing_repo_id is not None550 551 print(f"[Deploy] ========== DEPLOYMENT DECISION ==========")552 print(f"[Deploy] existing_repo_id provided: {existing_repo_id}")553 print(f"[Deploy] history provided: {history is not None} (length: {len(history) if history else 0})")554 print(f"[Deploy] username: {username}")555 print(f"[Deploy] is_update: {is_update}")556 print(f"[Deploy] language: {language}")557 print(f"[Deploy] ============================================")558 559 # For React space updates (followup changes), handle SEARCH/REPLACE blocks560 if is_update and language == "react":561 print(f"[Deploy] React space update - checking for search/replace blocks")562 563 # Import search/replace utilities564 from backend_search_replace import has_search_replace_blocks, parse_file_specific_changes, apply_search_replace_changes565 from huggingface_hub import hf_hub_download566 567 # Check if code contains search/replace blocks568 if has_search_replace_blocks(code):569 print(f"[Deploy] Detected SEARCH/REPLACE blocks - applying targeted changes")570 571 # Parse file-specific changes from code572 file_changes = parse_file_specific_changes(code)573 574 # Download existing files from the space575 try:576 print(f"[Deploy] Downloading existing files from space: {existing_repo_id}")577 578 # Get list of files in the space579 space_files = api.list_repo_files(repo_id=existing_repo_id, repo_type="space")580 print(f"[Deploy] Found {len(space_files)} files in space: {space_files}")581 582 # Download relevant files (React/Next.js files)583 react_file_patterns = ['.js', '.jsx', '.ts', '.tsx', '.css', '.json', 'Dockerfile']584 existing_files = {}585 586 for file_path in space_files:587 # Skip non-code files588 if any(file_path.endswith(ext) or ext in file_path for ext in react_file_patterns):589 try:590 downloaded_path = hf_hub_download(591 repo_id=existing_repo_id,592 filename=file_path,593 repo_type="space",594 token=token595 )596 with open(downloaded_path, 'r', encoding='utf-8') as f:597 existing_files[file_path] = f.read()598 print(f"[Deploy] Downloaded: {file_path} ({len(existing_files[file_path])} chars)")599 except Exception as e:600 print(f"[Deploy] Warning: Could not download {file_path}: {e}")601 602 if not existing_files:603 print(f"[Deploy] Warning: No React files found in space, falling back to full deployment")604 else:605 # Apply search/replace changes to the appropriate files606 updated_files = []607 608 # Check if changes are file-specific or global609 if "__all__" in file_changes:610 # Global changes - try to apply to all files611 changes_text = file_changes["__all__"]612 print(f"[Deploy] Applying global search/replace changes")613 614 # Try to apply to each file615 for file_path, original_content in existing_files.items():616 modified_content = apply_search_replace_changes(original_content, changes_text)617 if modified_content != original_content:618 print(f"[Deploy] Modified {file_path}")619 success, msg = update_space_file(620 repo_id=existing_repo_id,621 file_path=file_path,622 content=modified_content,623 token=token,624 commit_message=commit_message or f"Update {file_path} from anycoder"625 )626 if success:627 updated_files.append(file_path)628 else:629 print(f"[Deploy] Warning: Failed to update {file_path}: {msg}")630 else:631 # File-specific changes632 for filename, changes_text in file_changes.items():633 # Find the file in existing files (handle both with/without directory prefix)634 matching_file = None635 for file_path in existing_files.keys():636 if file_path == filename or file_path.endswith('/' + filename):637 matching_file = file_path638 break639 640 if matching_file:641 original_content = existing_files[matching_file]642 modified_content = apply_search_replace_changes(original_content, changes_text)643 644 print(f"[Deploy] Applying changes to {matching_file}")645 success, msg = update_space_file(646 repo_id=existing_repo_id,647 file_path=matching_file,648 content=modified_content,649 token=token,650 commit_message=commit_message or f"Update {matching_file} from anycoder"651 )652 653 if success:654 updated_files.append(matching_file)655 else:656 print(f"[Deploy] Warning: Failed to update {matching_file}: {msg}")657 else:658 print(f"[Deploy] Warning: File {filename} not found in space")659 660 if updated_files:661 space_url = f"https://huggingface.co/spaces/{existing_repo_id}"662 files_list = ", ".join(updated_files)663 return True, f"✅ Updated {len(updated_files)} file(s): {files_list}! View at: {space_url}", space_url664 else:665 return False, "No files were updated", None666 667 except Exception as e:668 print(f"[Deploy] Error applying search/replace changes: {e}")669 import traceback670 traceback.print_exc()671 # Fall through to normal deployment672 else:673 print(f"[Deploy] No SEARCH/REPLACE blocks detected, proceeding with full file update")674 # Fall through to normal React deployment below675 676 # For Gradio space updates (import/redesign), update .py files and upload all new files677 if is_update and language in ["gradio", "daggr"]:678 print(f"[Deploy] Gradio space update - updating .py files and uploading any new files")679 680 # Parse the code to get all files681 files = parse_multi_file_python_output(code)682 683 # Fallback if no files parsed684 if not files:685 print(f"[Deploy] No file markers found, using entire code as app.py")686 cleaned_code = remove_code_block(code)687 files['app.py'] = cleaned_code688 689 if not files:690 return False, "Error: No files found in generated code", None691 692 print(f"[Deploy] Generated {len(files)} file(s): {list(files.keys())}")693 694 # For redesign operations, ONLY update app.py to preserve other helper files695 # Detect redesign from commit message OR from history (user prompt contains "redesign")696 is_redesign = False697 if commit_message and "redesign" in commit_message.lower():698 is_redesign = True699 elif history:700 # Check last user message for "redesign" keyword701 for role, content in reversed(history):702 if role == "user" and content and "redesign" in content.lower():703 is_redesign = True704 break705 706 if is_redesign:707 print(f"[Deploy] Redesign operation detected - filtering to ONLY app.py")708 app_py_content = files.get('app.py')709 if not app_py_content:710 return False, "Error: No app.py found in redesign output", None711 files = {'app.py': app_py_content}712 print(f"[Deploy] Will only update app.py ({len(app_py_content)} chars)")713 714 # Upload all generated files (the LLM is instructed to only output .py files,715 # but if it creates new assets/data files, we should upload those too)716 # This approach updates .py files and adds any new files without touching717 # existing non-.py files that weren't generated718 updated_files = []719 for file_path, content in files.items():720 print(f"[Deploy] Uploading {file_path} ({len(content)} chars)")721 success, msg = update_space_file(722 repo_id=existing_repo_id,723 file_path=file_path,724 content=content,725 token=token,726 commit_message=commit_message or f"Update {file_path} from anycoder"727 )728 729 if success:730 updated_files.append(file_path)731 else:732 print(f"[Deploy] Warning: Failed to update {file_path}: {msg}")733 734 if updated_files:735 space_url = f"https://huggingface.co/spaces/{existing_repo_id}"736 files_list = ", ".join(updated_files)737 return True, f"✅ Updated {len(updated_files)} file(s): {files_list}! View at: {space_url}", space_url738 else:739 return False, "Failed to update any files", None740 741 if is_update:742 # Use existing repo743 repo_id = existing_repo_id744 space_name = existing_repo_id.split('/')[-1]745 if '/' in existing_repo_id:746 username = existing_repo_id.split('/')[0]747 elif not username:748 # Get username if still not available749 try:750 user_info = api.whoami()751 username = user_info.get("name") or user_info.get("preferred_username") or "user"752 except Exception as e:753 return False, f"Failed to get user info: {str(e)}", None754 else:755 # Get username if not provided756 if not username:757 try:758 user_info = api.whoami()759 username = user_info.get("name") or user_info.get("preferred_username") or "user"760 except Exception as e:761 return False, f"Failed to get user info: {str(e)}", None762 763 # Generate space name if not provided or empty764 if not space_name or space_name.strip() == "":765 space_name = f"anycoder-{uuid.uuid4().hex[:8]}"766 print(f"[Deploy] Auto-generated space name: {space_name}")767 768 # Clean space name (no spaces, lowercase, alphanumeric + hyphens)769 space_name = re.sub(r'[^a-z0-9-]', '-', space_name.lower())770 space_name = re.sub(r'-+', '-', space_name).strip('-')771 772 # Ensure space_name is not empty after cleaning773 if not space_name:774 space_name = f"anycoder-{uuid.uuid4().hex[:8]}"775 print(f"[Deploy] Space name was empty after cleaning, regenerated: {space_name}")776 777 repo_id = f"{username}/{space_name}"778 print(f"[Deploy] Using repo_id: {repo_id}")779 780 # Detect SDK781 sdk = detect_sdk_from_code(code, language)782 783 # Create temporary directory for files784 with tempfile.TemporaryDirectory() as temp_dir:785 temp_path = Path(temp_dir)786 787 # Parse code based on language788 app_port = None # Track if we need app_port for Docker spaces789 use_individual_uploads = False # Flag for transformers.js790 791 if language == "transformers.js":792 try:793 files = parse_transformers_js_output(code)794 print(f"[Deploy] Parsed transformers.js files: {list(files.keys())}")795 796 # Log file sizes for debugging797 for fname, fcontent in files.items():798 if fcontent:799 print(f"[Deploy] {fname}: {len(fcontent)} characters")800 else:801 print(f"[Deploy] {fname}: EMPTY")802 803 # Validate all three files are present in the dict804 required_files = {'index.html', 'index.js', 'style.css'}805 missing_from_dict = required_files - set(files.keys())806 807 if missing_from_dict:808 error_msg = f"Failed to parse required files: {', '.join(sorted(missing_from_dict))}. "809 error_msg += f"Parsed files: {', '.join(files.keys()) if files else 'none'}. "810 error_msg += "Transformers.js apps require all three files (index.html, index.js, style.css). Please regenerate using the correct format."811 print(f"[Deploy] {error_msg}")812 return False, error_msg, None813 814 # Validate files have actual content (not empty or whitespace-only)815 empty_files = [name for name in required_files if not files.get(name, '').strip()]816 if empty_files:817 error_msg = f"Empty file content detected: {', '.join(sorted(empty_files))}. "818 error_msg += "All three files must contain actual code. Please regenerate with complete content."819 print(f"[Deploy] {error_msg}")820 return False, error_msg, None821 822 # Write transformers.js files to temp directory823 for filename, content in files.items():824 file_path = temp_path / filename825 print(f"[Deploy] Writing {filename} ({len(content)} chars) to {file_path}")826 # Use text mode - Python handles encoding automatically827 if filename == "requirements.txt":828 content = enforce_critical_versions(content)829 file_path.write_text(content, encoding='utf-8')830 # Verify the write was successful831 written_size = file_path.stat().st_size832 print(f"[Deploy] Verified {filename}: {written_size} bytes on disk")833 834 # For transformers.js, we'll upload files individually (not via upload_folder)835 use_individual_uploads = True836 837 except Exception as e:838 print(f"[Deploy] Error parsing transformers.js: {e}")839 import traceback840 traceback.print_exc()841 return False, f"Error parsing transformers.js output: {str(e)}", None842 843 elif language == "html":844 html_code = parse_html_code(code)845 (temp_path / "index.html").write_text(html_code, encoding='utf-8')846 847 elif language == "comfyui":848 # ComfyUI is JSON, wrap in stylized HTML viewer with download button849 html_code = prettify_comfyui_json_for_html(code)850 (temp_path / "index.html").write_text(html_code, encoding='utf-8')851 852 elif language in ["gradio", "streamlit", "daggr"]:853 files = parse_multi_file_python_output(code)854 855 # Fallback: if no files parsed (missing === markers), treat entire code as app.py856 if not files:857 print(f"[Deploy] No file markers found in {language} code, using entire code as app.py")858 # Clean up code blocks if present859 cleaned_code = remove_code_block(code)860 # Determine app filename based on language861 app_filename = "streamlit_app.py" if language == "streamlit" else "app.py"862 files[app_filename] = cleaned_code863 864 # Write Python files (create subdirectories if needed)865 for filename, content in files.items():866 file_path = temp_path / filename867 file_path.parent.mkdir(parents=True, exist_ok=True)868 if filename == "requirements.txt":869 content = enforce_critical_versions(content)870 file_path.write_text(content, encoding='utf-8')871 872 # Ensure requirements.txt exists - generate from imports if missing873 if "requirements.txt" not in files:874 # Get the main app file (app.py for gradio, streamlit_app.py or app.py for streamlit)875 main_app = files.get('streamlit_app.py') or files.get('app.py', '')876 if main_app:877 print(f"[Deploy] Generating requirements.txt from imports in {language} app")878 import_statements = extract_import_statements(main_app)879 requirements_content = generate_requirements_txt_with_llm(import_statements)880 (temp_path / "requirements.txt").write_text(requirements_content, encoding='utf-8')881 print(f"[Deploy] Generated requirements.txt with {len(requirements_content.splitlines())} lines")882 else:883 # Fallback to minimal requirements if no app file found884 if language == "gradio":885 (temp_path / "requirements.txt").write_text("gradio>=4.0.0\n", encoding='utf-8')886 elif language == "streamlit":887 (temp_path / "requirements.txt").write_text("streamlit>=1.30.0\n", encoding='utf-8')888 elif language == "daggr":889 (temp_path / "requirements.txt").write_text("daggr>=0.5.4\ngradio>=6.0.2\n", encoding='utf-8')890 891 # Create Dockerfile if needed892 if sdk == "docker":893 if language == "streamlit":894 dockerfile = create_dockerfile_for_streamlit(space_name)895 (temp_path / "Dockerfile").write_text(dockerfile, encoding='utf-8')896 app_port = 7860 # Set app_port for Docker spaces897 use_individual_uploads = True # Streamlit uses individual file uploads898 899 elif language == "react":900 # Parse React output to get all files (uses === filename === markers)901 files = parse_react_output(code)902 903 if not files:904 return False, "Error: Could not parse React output", None905 906 # If Dockerfile is missing, use template907 if 'Dockerfile' not in files:908 dockerfile = create_dockerfile_for_react(space_name)909 files['Dockerfile'] = dockerfile910 911 # Write all React files (create subdirectories if needed)912 for filename, content in files.items():913 file_path = temp_path / filename914 file_path.parent.mkdir(parents=True, exist_ok=True)915 file_path.write_text(content, encoding='utf-8')916 917 app_port = 7860 # Set app_port for Docker spaces918 use_individual_uploads = True # React uses individual file uploads919 920 else:921 # Default: treat as Gradio app922 files = parse_multi_file_python_output(code)923 924 # Fallback: if no files parsed (missing === markers), treat entire code as app.py925 if not files:926 print(f"[Deploy] No file markers found in default (gradio) code, using entire code as app.py")927 # Clean up code blocks if present928 cleaned_code = remove_code_block(code)929 files['app.py'] = cleaned_code930 931 # Write files (create subdirectories if needed)932 for filename, content in files.items():933 file_path = temp_path / filename934 file_path.parent.mkdir(parents=True, exist_ok=True)935 if filename == "requirements.txt":936 content = enforce_critical_versions(content)937 file_path.write_text(content, encoding='utf-8')938 939 # Generate requirements.txt from imports if missing940 if "requirements.txt" not in files:941 main_app = files.get('app.py', '')942 if main_app:943 print(f"[Deploy] Generating requirements.txt from imports in default app")944 import_statements = extract_import_statements(main_app)945 requirements_content = generate_requirements_txt_with_llm(import_statements)946 (temp_path / "requirements.txt").write_text(requirements_content, encoding='utf-8')947 print(f"[Deploy] Generated requirements.txt with {len(requirements_content.splitlines())} lines")948 else:949 # Fallback to minimal requirements if no app file found950 if language == "daggr":951 (temp_path / "requirements.txt").write_text("daggr>=0.5.4\ngradio>=6.0.2\n", encoding='utf-8')952 else:953 (temp_path / "requirements.txt").write_text("gradio>=4.0.0\n", encoding='utf-8')954 955 # Don't create README - HuggingFace will auto-generate it956 # We'll add the anycoder tag after deployment957 958 # ONLY create repo for NEW deployments of non-Docker, non-transformers.js spaces959 # Docker and transformers.js handle repo creation separately below960 # This matches the Gradio version logic (line 1256 in ui.py)961 if not is_update and sdk != "docker" and language not in ["transformers.js"]:962 print(f"[Deploy] Creating NEW {sdk} space: {repo_id}")963 try:964 api.create_repo(965 repo_id=repo_id,966 repo_type="space",967 space_sdk=sdk,968 private=private,969 exist_ok=True970 )971 except Exception as e:972 return False, f"Failed to create space: {str(e)}", None973 elif is_update:974 print(f"[Deploy] UPDATING existing space: {repo_id} (skipping create_repo)")975 976 # Handle transformers.js spaces (create repo via duplicate_space)977 if language == "transformers.js":978 if not is_update:979 print(f"[Deploy] Creating NEW transformers.js space via template duplication")980 print(f"[Deploy] space_name value: '{space_name}' (type: {type(space_name)})")981 982 # Safety check for space_name983 if not space_name:984 return False, "Internal error: space_name is None after generation", None985 986 try:987 from huggingface_hub import duplicate_space988 989 # duplicate_space expects just the space name (not full repo_id)990 # Use strip() to clean the space name991 clean_space_name = space_name.strip()992 print(f"[Deploy] Attempting to duplicate template space to: {clean_space_name}")993 994 duplicated_repo = duplicate_space(995 from_id="static-templates/transformers.js",996 to_id=clean_space_name,997 token=token,998 exist_ok=True999 )1000 print(f"[Deploy] Template duplication result: {duplicated_repo} (type: {type(duplicated_repo)})")1001 except Exception as e:1002 print(f"[Deploy] Exception during duplicate_space: {type(e).__name__}: {str(e)}")1003 1004 # Check if space actually exists (success despite error)1005 space_exists = False1006 try:1007 if api.space_info(repo_id):1008 space_exists = True1009 except:1010 pass1011 1012 # Handle RepoUrl object "errors"1013 error_msg = str(e)1014 if ("'url'" in error_msg or "RepoUrl" in error_msg) and space_exists:1015 print(f"[Deploy] Space exists despite RepoUrl error, continuing with deployment")1016 else:1017 # Fallback to regular create_repo1018 print(f"[Deploy] Template duplication failed, attempting fallback to create_repo: {e}")1019 try:1020 api.create_repo(1021 repo_id=repo_id,1022 repo_type="space",1023 space_sdk="static",1024 private=private,1025 exist_ok=True1026 )1027 print(f"[Deploy] Fallback create_repo successful")1028 except Exception as e2:1029 return False, f"Failed to create transformers.js space (both duplication and fallback failed): {str(e2)}", None1030 else:1031 # For updates, verify we can access the existing space1032 try:1033 space_info = api.space_info(repo_id)1034 if not space_info:1035 return False, f"Could not access space {repo_id} for update", None1036 except Exception as e:1037 return False, f"Cannot update space {repo_id}: {str(e)}", None1038 1039 # Handle Docker spaces (React/Streamlit) - create repo separately1040 elif sdk == "docker" and language in ["streamlit", "react"]:1041 if not is_update:1042 print(f"[Deploy] Creating NEW Docker space for {language}: {repo_id}")1043 try:1044 from huggingface_hub import create_repo as hf_create_repo1045 hf_create_repo(1046 repo_id=repo_id,1047 repo_type="space",1048 space_sdk="docker",1049 token=token,1050 exist_ok=True1051 )1052 except Exception as e:1053 return False, f"Failed to create Docker space: {str(e)}", None1054 1055 # Upload files1056 if not commit_message:1057 commit_message = "Update from anycoder" if is_update else "Deploy from anycoder"1058 1059 try:1060 if language == "transformers.js":1061 # Special handling for transformers.js - create NEW temp files for each upload1062 # This matches the working pattern in ui.py1063 import time1064 1065 # Get the parsed files from earlier1066 files_to_upload = [1067 ("index.html", files.get('index.html')),1068 ("index.js", files.get('index.js')),1069 ("style.css", files.get('style.css'))1070 ]1071 1072 max_attempts = 31073 for file_name, file_content in files_to_upload:1074 if not file_content:1075 return False, f"Missing content for {file_name}", None1076 1077 success = False1078 last_error = None1079 1080 for attempt in range(max_attempts):1081 temp_file_path = None1082 try:1083 # Create a NEW temp file for this upload (matches Gradio version approach)1084 print(f"[Deploy] Creating temp file for {file_name} with {len(file_content)} chars")1085 # Use text mode "w" - lets Python handle encoding automatically (better emoji support)1086 with tempfile.NamedTemporaryFile("w", suffix=f".{file_name.split('.')[-1]}", delete=False) as f:1087 f.write(file_content)1088 temp_file_path = f.name1089 # File is now closed and flushed, safe to upload1090 1091 # Upload the file without commit_message (HF handles this for spaces)1092 api.upload_file(1093 path_or_fileobj=temp_file_path,1094 path_in_repo=file_name,1095 repo_id=repo_id,1096 repo_type="space"1097 )1098 success = True1099 print(f"[Deploy] Successfully uploaded {file_name}")1100 break1101 1102 except Exception as e:1103 last_error = e1104 error_str = str(e)1105 print(f"[Deploy] Upload error for {file_name}: {error_str}")1106 if "403" in error_str or "Forbidden" in error_str:1107 return False, f"Permission denied uploading {file_name}. Check your token has write access to {repo_id}.", None1108 1109 if attempt < max_attempts - 1:1110 time.sleep(2) # Wait before retry1111 print(f"[Deploy] Retry {attempt + 1}/{max_attempts} for {file_name}")1112 finally:1113 # Clean up temp file1114 if temp_file_path and os.path.exists(temp_file_path):1115 os.unlink(temp_file_path)1116 1117 if not success:1118 return False, f"Failed to upload {file_name} after {max_attempts} attempts: {last_error}", None1119 1120 elif use_individual_uploads:1121 # For React, Streamlit: upload each file individually1122 import time1123 1124 # Get list of files to upload from temp directory1125 files_to_upload = []1126 for file_path in temp_path.rglob('*'):1127 if file_path.is_file():1128 # Get relative path from temp directory (use forward slashes for repo paths)1129 rel_path = file_path.relative_to(temp_path)1130 files_to_upload.append(str(rel_path).replace('\\', '/'))1131 1132 if not files_to_upload:1133 return False, "No files to upload", None1134 1135 print(f"[Deploy] Uploading {len(files_to_upload)} files individually: {files_to_upload}")1136 1137 max_attempts = 31138 for filename in files_to_upload:1139 # Convert back to Path for filesystem operations1140 file_path = temp_path / filename.replace('/', os.sep)1141 if not file_path.exists():1142 return False, f"Failed to upload: {filename} not found", None1143 1144 # Upload with retry logic1145 success = False1146 last_error = None1147 1148 for attempt in range(max_attempts):1149 try:1150 # Upload without commit_message - HF API handles this for spaces1151 api.upload_file(1152 path_or_fileobj=str(file_path),1153 path_in_repo=filename,1154 repo_id=repo_id,1155 repo_type="space"1156 )1157 success = True1158 print(f"[Deploy] Successfully uploaded {filename}")1159 break1160 except Exception as e:1161 last_error = e1162 error_str = str(e)1163 print(f"[Deploy] Upload error for {filename}: {error_str}")1164 if "403" in error_str or "Forbidden" in error_str:1165 return False, f"Permission denied uploading {filename}. Check your token has write access to {repo_id}.", None1166 if attempt < max_attempts - 1:1167 time.sleep(2) # Wait before retry1168 print(f"[Deploy] Retry {attempt + 1}/{max_attempts} for {filename}")1169 1170 if not success:1171 return False, f"Failed to upload {filename} after {max_attempts} attempts: {last_error}", None1172 else:1173 # For other languages, use upload_folder1174 print(f"[Deploy] Uploading folder to {repo_id}")1175 api.upload_folder(1176 folder_path=str(temp_path),1177 repo_id=repo_id,1178 repo_type="space"1179 )1180 except Exception as e:1181 return False, f"Failed to upload files: {str(e)}", None1182 1183 # After successful upload, modify the auto-generated README to add anycoder tag1184 # For new spaces: HF auto-generates README, wait and modify it1185 # For updates: README should already exist, just add tag if missing1186 try:1187 import time1188 if not is_update:1189 time.sleep(2) # Give HF time to generate README for new spaces1190 add_anycoder_tag_to_readme(api, repo_id, app_port, sdk)1191 except Exception as e:1192 # Don't fail deployment if README modification fails1193 print(f"Warning: Could not add anycoder tag to README: {e}")1194 1195 # For transformers.js updates, trigger a space restart to ensure changes take effect1196 if is_update and language == "transformers.js":1197 try:1198 api.restart_space(repo_id=repo_id)1199 print(f"[Deploy] Restarted space after update: {repo_id}")1200 except Exception as restart_error: