""" HuggingFace to ModelScope Migration Tool This Gradio app enables migration of models and datasets from HuggingFace to ModelScope. """ import os import shutil import tempfile from pathlib import Path from typing import Tuple, Optional import argparse import gradio as gr from huggingface_hub import snapshot_download, HfApi from modelscope.hub.api import HubApi from modelscope.hub.constants import Licenses, ModelVisibility, DatasetVisibility import sys import io import threading import queue import time import re # Set ModelScope domain to use the international site os.environ.setdefault("MODELSCOPE_DOMAIN", "modelscope.ai") # Regex to match ANSI escape codes (like [A, [2K, etc.) ANSI_ESCAPE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') class MigrationTool: """Handles migration of models and datasets between HuggingFace and ModelScope.""" def __init__(self): self.temp_dir = None def download_from_hf( self, repo_id: str, repo_type: str = "model", token: Optional[str] = None ) -> Tuple[bool, str, Optional[str]]: """Download a repository from HuggingFace. Args: repo_id: HuggingFace repository ID (e.g., 'username/repo-name') repo_type: Type of repository ('model' or 'dataset') token: HuggingFace authentication token Returns: Tuple of (success, message, local_path) """ try: # Create temporary directory self.temp_dir = tempfile.mkdtemp(prefix="hf2ms_") # Download the repository local_path = snapshot_download( repo_id=repo_id, repo_type=repo_type, local_dir=self.temp_dir, local_dir_use_symlinks=False, token=token ) return True, f"βœ“ Successfully downloaded {repo_type} from HuggingFace", local_path except Exception as e: return False, f"βœ— Download failed: {str(e)}", None def upload_to_ms( self, local_path: str, repo_id: str, token: str, repo_type: str = "model", visibility: str = "public", license_type: str = "apache-2.0", chinese_name: Optional[str] = None ) -> Tuple[bool, str]: """Upload a repository to ModelScope. Args: local_path: Local path to the repository repo_id: ModelScope repository ID (e.g., 'username/repo-name') token: ModelScope authentication token repo_type: Type of repository ('model' or 'dataset') visibility: Repository visibility ('public' or 'private') license_type: License type chinese_name: Optional Chinese name for the repository Returns: Tuple of (success, message) """ try: # Clean and validate token token = token.strip() if not token: return False, "βœ— ModelScope token is empty" # Create HubApi instance and login explicitly api = HubApi() try: api.login(token) except Exception as login_error: return False, f"βœ— ModelScope Login failed: {str(login_error)}\n\nπŸ’‘ Tip: Ensure you are using an 'SDK Token' from https://www.modelscope.ai/my/myaccesstoken. The token usually starts with 'ms-'." # Map license types license_map = { "apache-2.0": Licenses.APACHE_V2, "mit": Licenses.MIT, "gpl-2.0": Licenses.GPL_V2, "gpl-3.0": Licenses.GPL_V3, "lgpl-2.1": Licenses.LGPL_V2_1, "lgpl-3.0": Licenses.LGPL_V3, "afl-3.0": Licenses.AFL_V3, "ecl-2.0": Licenses.ECL_V2, "other": None, } lic = license_map.get(license_type.lower(), Licenses.APACHE_V2) # Check if repository exists repo_exists = api.repo_exists(repo_id=repo_id, repo_type=repo_type, token=token) # Create repository if it doesn't exist # Important: We must create with the correct visibility BEFORE upload_folder, # because upload_folder will create a public repo by default if repo doesn't exist if not repo_exists: try: if repo_type == "model": # Determine visibility for models (1=private, 5=public) vis = ModelVisibility.PUBLIC if visibility == "public" else ModelVisibility.PRIVATE # Build parameters, only include license if not None create_params = { "model_id": repo_id, "visibility": vis, "token": token, } if lic is not None: create_params["license"] = lic if chinese_name: create_params["chinese_name"] = chinese_name api.create_model(**create_params) else: # Determine visibility for datasets (1=private, 5=public) vis = DatasetVisibility.PUBLIC if visibility == "public" else DatasetVisibility.PRIVATE # For datasets, need to split repo_id into namespace and name parts = repo_id.split('/') if len(parts) != 2: return False, f"βœ— Invalid dataset ID format: {repo_id}. Must be 'namespace/name'" namespace, dataset_name = parts # Build parameters, only include license if not None create_params = { "dataset_name": dataset_name, "namespace": namespace, "visibility": vis, } if lic is not None: create_params["license"] = lic if chinese_name: create_params["chinese_name"] = chinese_name api.create_dataset(**create_params) except Exception as create_error: error_msg = str(create_error) # Only ignore if repo already exists (race condition) if "already exists" not in error_msg.lower(): return False, f"βœ— Failed to create repository: {error_msg}" # Push the model/dataset if repo_type == "model": api.upload_folder( repo_id=repo_id, folder_path=local_path, token=token, ) else: # For datasets, use upload_folder with repo_type='dataset' api.upload_folder( repo_id=repo_id, folder_path=local_path, token=token, repo_type="dataset" ) return True, f"βœ“ Successfully uploaded {repo_type} to ModelScope" except Exception as e: return False, f"βœ— Upload failed: {str(e)}" def cleanup(self): """Clean up temporary files.""" if self.temp_dir and os.path.exists(self.temp_dir): try: shutil.rmtree(self.temp_dir) self.temp_dir = None except Exception as e: print(f"Warning: Failed to clean up temporary directory: {e}") def migrate( self, hf_token: str, ms_token: str, hf_repo_id: str, ms_repo_id: str, repo_type: str, visibility: str, license_type: str, chinese_name: Optional[str] = None, progress=None ) -> str: """Perform the complete migration process with real-time console log capture.""" # If no progress tracker is provided (CLI mode), we just skip progress updates # Gradio will pass its own tracker when called from the UI def update_progress(val, desc=""): if progress: progress(val, desc=desc) log_queue = queue.Queue() output_lines = [] # Helper to capture console output and send it to the queue class StreamToQueue(io.StringIO): def __init__(self, original_stream, q): super().__init__() self.original_stream = original_stream self.q = q def write(self, s): if s: # Write to original stream (console) and our queue (Gradio) self.original_stream.write(s) self.original_stream.flush() self.q.put(s) def flush(self): self.original_stream.flush() def update_output(): """Gather all pending messages from the queue and return the full status.""" new_data = False while not log_queue.empty(): try: raw_msg = log_queue.get_nowait() # 1. Strip ANSI escape codes (those [A, [m, etc.) msg = ANSI_ESCAPE.sub('', raw_msg) if not msg: continue # 2. Process the message line by line # We handle \r by treating it as a signal to potentially overwrite the last line # We handle \n as a signal to start a new line for line in msg.replace('\r', '\n').split('\n'): clean_line = line.strip() if not clean_line: continue # 3. Smart Progress Bar Handling # Identify if this line is a progress bar update # Progress bars usually look like: "Label: 45%|### | ..." is_progress = '%' in clean_line and '|' in clean_line and ('[' in clean_line or ']' in clean_line) if is_progress: # Extract the label (everything before the progress bar/percentage) # This helps us identify WHICH progress bar to update label = clean_line.split('|')[0].split('%')[0].strip() # If the label ends with a number (like '45'), try to get the text before it label = re.sub(r'\d+$', '', label).strip() found = False # Look at the last few lines to see if we're updating an existing bar # We only look back ~10 lines to keep it fast for i in range(len(output_lines) - 1, max(-1, len(output_lines) - 11), -1): if label and label in output_lines[i] and ('%' in output_lines[i] or '|' in output_lines[i]): output_lines[i] = clean_line found = True new_data = True break if not found: output_lines.append(clean_line) new_data = True else: # Regular log message output_lines.append(clean_line) new_data = True except queue.Empty: break # Keep the output box from growing infinitely (last 1000 lines) if len(output_lines) > 1000: output_lines[:] = output_lines[-1000:] return "\n".join(output_lines), new_data # Thread-safe migration execution storage results = {"success": False, "message": "", "finished": False} def run_migration(): try: # Clean inputs update_progress(0, desc="Validating inputs...") nonlocal hf_token, ms_token, hf_repo_id, ms_repo_id hf_token = hf_token.strip() if hf_token else "" ms_token = ms_token.strip() if ms_token else "" hf_repo_id = hf_repo_id.strip() if hf_repo_id else "" ms_repo_id = ms_repo_id.strip() if ms_repo_id else "" if not hf_token or not ms_token or not hf_repo_id or not ms_repo_id: results["message"] = "βœ— Error: All tokens and repository IDs are required" results["finished"] = True return if "/" not in ms_repo_id: results["message"] = "βœ— Error: ModelScope Repo ID must include your namespace (e.g., 'username/repo-name')" results["finished"] = True return # 1. Download update_progress(0.1, desc="Downloading from HuggingFace...") print(f"⬇️ Starting download from HuggingFace: {hf_repo_id}...") success, msg, local_path = self.download_from_hf(hf_repo_id, repo_type, hf_token) print(msg) if not success: results["message"] = msg results["finished"] = True return # 2. Upload update_progress(0.4, desc="Uploading to ModelScope...") print(f"\n⬆️ Starting upload to ModelScope: {ms_repo_id}...") success, msg = self.upload_to_ms( local_path, ms_repo_id, ms_token, repo_type, visibility, license_type, chinese_name ) print(msg) results["success"] = success results["message"] = msg except Exception as e: results["message"] = f"βœ— Unexpected error: {str(e)}" finally: print("\n🧹 Cleaning up temporary files...") self.cleanup() print("βœ“ Cleanup complete") results["finished"] = True # Redirect stdout and stderr to our queue old_stdout = sys.stdout old_stderr = sys.stderr sys.stdout = StreamToQueue(sys.stdout, log_queue) sys.stderr = StreamToQueue(sys.stderr, log_queue) try: # Start the migration in a background thread thread = threading.Thread(target=run_migration) thread.start() # Continuously yield updates until the migration thread completes while not results["finished"]: current_status, updated = update_output() if updated: yield current_status time.sleep(0.1) # Final capture of any remaining logs final_status, _ = update_output() # Append final results if results["success"]: update_progress(1.0, desc="Completed") final_status += f"\n\nβœ… Migration completed successfully!" final_status += f"\nYour {repo_type} is available at: https://www.modelscope.ai/models/{ms_repo_id}" else: update_progress(1.0, desc="Failed") final_status += f"\n\n❌ Migration failed: {results['message']}" yield final_status finally: # CRITICAL: Restore original streams so we don't break the whole app sys.stdout = old_stdout sys.stderr = old_stderr def create_interface(): """Create the Gradio interface.""" migration_tool = MigrationTool() with gr.Blocks(title="HuggingFace to ModelScope Migration Tool") as app: gr.Markdown(""" # πŸš€ HuggingFace to ModelScope Migration Tool Easily migrate your models and datasets from HuggingFace to ModelScope. ## πŸ“‹ Instructions: 1. Get your **HuggingFace token** from: https://huggingface.co/settings/tokens 2. Get your **ModelScope SDK token** from: https://www.modelscope.ai/my/myaccesstoken 3. Fill in the repository details below 4. Click "Start Migration" """) with gr.Row(): with gr.Column(): gr.Markdown("### πŸ”‘ Authentication") hf_token = gr.Textbox( label="HuggingFace Token", type="password", placeholder="hf_...", info="Your HuggingFace access token" ) ms_token = gr.Textbox( label="ModelScope Token", type="password", placeholder="ms_...", info="Your SDK token from modelscope.ai (usually starts with 'ms-')" ) with gr.Column(): gr.Markdown("### πŸ“¦ Repository Details") repo_type = gr.Radio( choices=["model", "dataset"], label="Repository Type", value="model", info="Select what you want to migrate" ) visibility = gr.Radio( choices=["public", "private"], label="Visibility", value="public", info="Visibility of the repository on ModelScope" ) with gr.Row(): with gr.Column(): hf_repo_id = gr.Textbox( label="Source HuggingFace Repo ID", placeholder="username/repo-name", info="e.g., bert-base-uncased or username/my-model" ) with gr.Column(): ms_repo_id = gr.Textbox( label="Destination ModelScope Repo ID", placeholder="username/repo-name", info="Your ModelScope username/repo-name" ) with gr.Row(): with gr.Column(): license_type = gr.Dropdown( choices=[ "apache-2.0", "mit", "gpl-2.0", "gpl-3.0", "lgpl-2.1", "lgpl-3.0", "afl-3.0", "ecl-2.0", "other" ], label="License", value="apache-2.0", info="License for the repository" ) with gr.Column(): chinese_name = gr.Textbox( label="Chinese Name (Optional)", placeholder="ζ¨‘εž‹δΈ­ζ–‡εη§°", info="Optional Chinese name for the repository" ) migrate_btn = gr.Button("πŸš€ Start Migration", variant="primary", size="lg") output = gr.Textbox( label="Migration Status", lines=15, interactive=False ) migrate_btn.click( fn=migration_tool.migrate, inputs=[ hf_token, ms_token, hf_repo_id, ms_repo_id, repo_type, visibility, license_type, chinese_name ], outputs=output ) gr.Markdown(""" --- ### πŸ“ Notes: - Large models may take some time to download and upload - Make sure you have enough disk space for temporary storage - Private repositories require appropriate token permissions - The tool will create the repository on ModelScope if it doesn't exist ### πŸ”— Resources: - [HuggingFace Hub](https://huggingface.co/) - [ModelScope](https://www.modelscope.ai/) - [HuggingFace Token Settings](https://huggingface.co/settings/tokens) - [ModelScope Token Settings](https://www.modelscope.ai/my/myaccesstoken) """) return app if __name__ == "__main__": parser = argparse.ArgumentParser(description="HuggingFace to ModelScope Migration Tool") # Mode selection parser.add_argument("--cli", action="store_true", help="Run in CLI mode instead of Gradio UI") # Gradio options parser.add_argument("--host", type=str, default="0.0.0.0", help="Host for Gradio app") parser.add_argument("--port", type=int, default=7860, help="Port for Gradio app") parser.add_argument("--share", action="store_true", help="Create a public link for Gradio app") # CLI options (only used if --cli is set) parser.add_argument("--hf-token", type=str, help="HuggingFace access token") parser.add_argument("--ms-token", type=str, help="ModelScope access token") parser.add_argument("--hf-repo", type=str, help="Source HuggingFace repo ID") parser.add_argument("--ms-repo", type=str, help="Destination ModelScope repo ID") parser.add_argument("--type", type=str, choices=["model", "dataset"], default="model", help="Repository type") parser.add_argument("--visibility", type=str, choices=["public", "private"], default="public", help="Repo visibility") parser.add_argument("--license", type=str, default="apache-2.0", help="Repo license") parser.add_argument("--chinese-name", type=str, help="Optional Chinese name for the repo") args = parser.parse_args() if args.cli: if not all([args.hf_token, args.ms_token, args.hf_repo, args.ms_repo]): print("βœ— Error: CLI mode requires --hf-token, --ms-token, --hf-repo, and --ms-repo") sys.exit(1) tool = MigrationTool() print(f"πŸš€ Starting CLI Migration: {args.hf_repo} -> {args.ms_repo}") # In CLI mode, we just consume the generator last_status = "" for status in tool.migrate( args.hf_token, args.ms_token, args.hf_repo, args.ms_repo, args.type, args.visibility, args.license, args.chinese_name ): last_status = status print("\n" + "="*50) print("Final Status:") print(last_status) else: app = create_interface() app.launch( server_name=args.host, server_port=args.port, share=args.share )