Spaces:
Sleeping
Sleeping
| import os | |
| import gradio as gr | |
| from pathlib import Path | |
| import tempfile | |
| import zipfile | |
| import shutil | |
| from typing import List, Set, Dict, Tuple | |
| class WebProjectAnalyzer: | |
| def __init__(self): | |
| # Diretórios que devem ser completamente ignorados | |
| self.ignored_dirs = { | |
| '__pycache__', '.git', '.vscode', '.idea', 'node_modules', | |
| 'vendor', 'dist', 'build', 'cache', 'logs', 'tmp', | |
| 'coverage', '.github', '.circleci', '.docker', 'venv', | |
| 'env', '.venv', 'virtualenv', 'bin', 'include', 'lib', | |
| 'lib64', 'site-packages', 'dist-packages', '.gitlab', | |
| '.svn', '.hg', '.pytest_cache', '.mypy_cache', '__MACOSX', | |
| '.next', '.nuxt', 'out', 'target', '.gradle', '.mvn', | |
| 'bower_components', 'jspm_packages', 'web_modules', | |
| '.serverless', '.fusebox', '.dynamodb', '.tern-port', | |
| '.yarn', '.pnp', 'eggs', 'wheels', 'htmlcov', '.tox', | |
| '.hypothesis', '.ruff_cache', 'var', 'sdist', 'develop-eggs', | |
| '.sass-cache', '.cache', 'public/packs', 'public/assets' | |
| } | |
| # Extensões relevantes para projetos web (PRINCIPAIS) | |
| self.relevant_extensions = { | |
| # Frontend | |
| '.html', '.htm', '.css', '.scss', '.sass', '.less', | |
| '.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte', | |
| # Backend | |
| '.py', '.rb', '.php', '.java', '.go', '.rs', | |
| # Config importantes | |
| '.json', '.yml', '.yaml', '.toml', '.xml', | |
| # Docs | |
| '.md', '.txt', | |
| # SQL | |
| '.sql', | |
| # Outros | |
| '.env.example', '.gitignore', '.dockerignore' | |
| } | |
| # Arquivos de configuração CRÍTICOS (sempre incluir) | |
| self.critical_files = { | |
| 'package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', | |
| 'requirements.txt', 'Pipfile', 'Pipfile.lock', 'pyproject.toml', | |
| 'composer.json', 'composer.lock', 'Gemfile', 'Gemfile.lock', | |
| 'go.mod', 'go.sum', 'Cargo.toml', 'Cargo.lock', | |
| 'pom.xml', 'build.gradle', 'settings.gradle', | |
| 'CMakeLists.txt', 'Makefile', 'tsconfig.json', | |
| 'webpack.config.js', 'vite.config.js', 'rollup.config.js', | |
| 'docker-compose.yml', 'Dockerfile', '.dockerignore', | |
| '.env.example', '.gitignore', '.eslintrc', '.prettierrc', | |
| 'README.md', 'LICENSE', 'setup.py', 'setup.cfg', | |
| 'manage.py', 'wsgi.py', 'asgi.py', 'app.py', 'main.py', | |
| 'index.html', 'index.js', 'index.ts', 'App.js', 'App.tsx', | |
| 'next.config.js', 'nuxt.config.js', 'vue.config.js', | |
| 'angular.json', 'nest-cli.json', 'vercel.json', 'netlify.toml' | |
| } | |
| # Padrões de nomes de arquivos a ignorar | |
| self.ignored_file_patterns = { | |
| '.min.js', '.min.css', '.map', '.log', | |
| '.pyc', '.pyo', '.pyd', '.so', '.dll', '.dylib', | |
| '.class', '.jar', '.war', '.ear', '.tar', '.gz', | |
| '.exe', '.bin', '.dat', '.db', '.sqlite', '.sqlite3', | |
| 'thumbs.db', '.DS_Store', 'desktop.ini' | |
| } | |
| def should_ignore(self, name: str, path: str, is_dir: bool) -> bool: | |
| """Determina se um arquivo/diretório deve ser ignorado""" | |
| # Ignora diretórios específicos | |
| if is_dir and name in self.ignored_dirs: | |
| return True | |
| # Ignora arquivos críticos? NÃO! | |
| if name in self.critical_files: | |
| return False | |
| # Ignora arquivos ocultos (exceto importantes) | |
| if name.startswith('.') and name not in self.critical_files: | |
| if not name.endswith('.example') and name not in ['.gitignore', '.dockerignore', '.env.example']: | |
| return True | |
| # Ignora arquivos temporários | |
| if name.endswith('~') or name.startswith('~') or name.startswith('#'): | |
| return True | |
| # Ignora padrões específicos (exceto .lock de configs) | |
| for pattern in self.ignored_file_patterns: | |
| if pattern in name and name not in self.critical_files: | |
| return True | |
| # Ignora arquivos muito grandes (>10MB) | |
| if not is_dir: | |
| try: | |
| if os.path.getsize(path) > 10 * 1024 * 1024: | |
| return True | |
| except: | |
| pass | |
| return False | |
| def is_relevant_file(self, name: str) -> bool: | |
| """Determina se um arquivo é relevante para mostrar""" | |
| # Arquivos críticos sempre são relevantes | |
| if name in self.critical_files: | |
| return True | |
| # Ignora padrões indesejados (exceto configs) | |
| for pattern in self.ignored_file_patterns: | |
| if pattern in name and name not in self.critical_files: | |
| return False | |
| # Verifica extensões relevantes | |
| _, ext = os.path.splitext(name) | |
| return ext.lower() in self.relevant_extensions | |
| def get_file_icon(self, filename: str) -> str: | |
| """Retorna ícone apropriado para o tipo de arquivo""" | |
| _, ext = os.path.splitext(filename) | |
| # Arquivos específicos | |
| if filename in ['package.json', 'package-lock.json']: | |
| return '📦' | |
| if filename in ['requirements.txt', 'Pipfile']: | |
| return '🐍' | |
| if filename in ['docker-compose.yml', 'Dockerfile']: | |
| return '🐳' | |
| if filename == 'README.md': | |
| return '📖' | |
| if filename in ['.gitignore', '.dockerignore']: | |
| return '🚫' | |
| if filename.startswith('.env'): | |
| return '🔐' | |
| # Por extensão | |
| icon_map = { | |
| '.py': '🐍', '.js': '📜', '.jsx': '⚛️', '.ts': '📘', '.tsx': '⚛️', | |
| '.html': '🌐', '.htm': '🌐', '.css': '🎨', '.scss': '🎨', '.less': '🎨', | |
| '.php': '🐘', '.java': '☕', '.rb': '💎', '.json': '📋', '.xml': '📄', | |
| '.sql': '🗃️', '.md': '📖', '.txt': '📄', '.yml': '⚙️', '.yaml': '⚙️', | |
| '.toml': '⚙️', '.vue': '💚', '.svelte': '🟢', '.rs': '🦀', '.go': '🐹' | |
| } | |
| return icon_map.get(ext, '📄') | |
| def extract_zip(self, zip_path: str) -> str: | |
| """Extrai arquivo ZIP e retorna o diretório""" | |
| try: | |
| # Cria diretório temporário | |
| temp_dir = tempfile.mkdtemp(prefix='project_') | |
| print(f"📦 Extraindo ZIP para: {temp_dir}") | |
| # Extrai ZIP | |
| with zipfile.ZipFile(zip_path, 'r') as zip_ref: | |
| zip_ref.extractall(temp_dir) | |
| # Verifica se há uma única pasta raiz (comum em ZIPs do GitHub) | |
| items = os.listdir(temp_dir) | |
| if len(items) == 1 and os.path.isdir(os.path.join(temp_dir, items[0])): | |
| root_dir = os.path.join(temp_dir, items[0]) | |
| print(f"📁 Pasta raiz encontrada: {items[0]}") | |
| return root_dir | |
| return temp_dir | |
| except Exception as e: | |
| print(f"❌ Erro ao extrair ZIP: {e}") | |
| raise | |
| def analyze_structure(self, root_path: str, max_depth: int = 4) -> Tuple[str, Dict, List[str]]: | |
| """Analisa a estrutura do projeto""" | |
| if not root_path: | |
| return "❌ Nenhuma pasta para analisar.", {"total_files": 0, "relevant_files": 0, "total_size": 0}, [] | |
| if not os.path.exists(root_path): | |
| return "❌ Pasta não encontrada.", {"total_files": 0, "relevant_files": 0, "total_size": 0}, [] | |
| if not os.path.isdir(root_path): | |
| return "❌ Caminho inválido.", {"total_files": 0, "relevant_files": 0, "total_size": 0}, [] | |
| structure_output = [] | |
| relevant_files = [] | |
| stats = { | |
| 'total_files': 0, | |
| 'relevant_files': 0, | |
| 'total_size': 0 | |
| } | |
| def analyze_directory(current_path: str, level: int = 0, prefix: str = ''): | |
| if level > max_depth: | |
| return | |
| try: | |
| items = sorted(os.listdir(current_path)) | |
| dirs = [] | |
| files = [] | |
| # Separa diretórios e arquivos | |
| for item in items: | |
| full_path = os.path.join(current_path, item) | |
| is_dir = os.path.isdir(full_path) | |
| if self.should_ignore(item, full_path, is_dir): | |
| continue | |
| if is_dir: | |
| dirs.append(item) | |
| else: | |
| stats['total_files'] += 1 | |
| if self.is_relevant_file(item): | |
| files.append(item) | |
| stats['relevant_files'] += 1 | |
| relevant_files.append(full_path) | |
| try: | |
| stats['total_size'] += os.path.getsize(full_path) | |
| except: | |
| pass | |
| # Renderiza arquivos primeiro | |
| for i, file in enumerate(files): | |
| is_last = (i == len(files) - 1) and len(dirs) == 0 | |
| connector = '└── ' if is_last else '├── ' | |
| icon = self.get_file_icon(file) | |
| structure_output.append(f"{prefix}{connector}{icon} {file}") | |
| # Renderiza diretórios depois | |
| for i, dir_name in enumerate(dirs): | |
| is_last = i == len(dirs) - 1 | |
| connector = '└── ' if is_last else '├── ' | |
| structure_output.append(f"{prefix}{connector}📁 {dir_name}/") | |
| # Recursão | |
| extension = ' ' if is_last else '│ ' | |
| full_path = os.path.join(current_path, dir_name) | |
| analyze_directory(full_path, level + 1, prefix + extension) | |
| except PermissionError: | |
| structure_output.append(f"{prefix}🚫 [Acesso negado]") | |
| except Exception as e: | |
| structure_output.append(f"{prefix}⚠️ [Erro: {str(e)}]") | |
| try: | |
| # Nome da pasta raiz | |
| project_name = os.path.basename(root_path) | |
| structure_output.append(f"📁 {project_name}/") | |
| analyze_directory(root_path, 0, '') | |
| if not structure_output or len(structure_output) == 1: | |
| return "ℹ️ Nenhum arquivo relevante encontrado.", stats, relevant_files | |
| structure_text = '\n'.join(structure_output) | |
| return structure_text, stats, relevant_files | |
| except Exception as e: | |
| return f"❌ Erro ao analisar: {str(e)}", stats, [] | |
| def create_structure_report(self, folder_name: str, structure: str, stats: Dict) -> str: | |
| """Cria relatório completo da estrutura""" | |
| total_files = stats['total_files'] | |
| relevant_files = stats['relevant_files'] | |
| total_size = stats['total_size'] | |
| from datetime import datetime | |
| current_time = datetime.now().strftime('%d/%m/%Y %H:%M:%S') | |
| report = f"""# 📊 ANÁLISE DO PROJETO: `{folder_name}` | |
| ## 📍 Informações | |
| - **Projeto**: `{folder_name}` | |
| - **Data da Análise**: {current_time} | |
| ## 📈 Estatísticas | |
| | Métrica | Valor | | |
| |---------|-------| | |
| | 📁 Total de arquivos escaneados | {total_files} | | |
| | ✅ Arquivos relevantes | {relevant_files} | | |
| | 💾 Tamanho total | {total_size / 1024 / 1024:.2f} MB | | |
| | 🎯 Taxa de relevância | {(relevant_files/total_files*100) if total_files > 0 else 0:.1f}% | | |
| --- | |
| ## 🌳 Estrutura do Projeto | |
| ``` | |
| {structure} | |
| ``` | |
| --- | |
| ## 💡 Legenda | |
| - 📁 Diretório | |
| - 🐍 Python | |
| - 📜 JavaScript | |
| - ⚛️ React (JSX/TSX) | |
| - 📘 TypeScript | |
| - 🌐 HTML | |
| - 🎨 CSS/SCSS | |
| - 📦 Package Config | |
| - 🐳 Docker | |
| - 📖 Documentação | |
| - 🚫 Git/Docker Ignore | |
| - 🔐 Environment | |
| --- | |
| **✨ Apenas arquivos estruturais e de configuração principais foram incluídos** | |
| """ | |
| return report | |
| def process_zip(self, zip_file, max_depth: int = 4): | |
| """Processa arquivo ZIP do projeto""" | |
| if not zip_file: | |
| return ( | |
| "**📁 Status**: Aguardando upload de arquivo ZIP...", | |
| "ℹ️ Faça upload do arquivo ZIP do seu projeto para análise...", | |
| None | |
| ) | |
| temp_dir = None | |
| try: | |
| # Obtém o caminho do arquivo | |
| if hasattr(zip_file, 'name'): | |
| zip_path = zip_file.name | |
| else: | |
| zip_path = str(zip_file) | |
| print(f"📦 Processando ZIP: {zip_path}") | |
| # Verifica se é um ZIP | |
| if not zip_path.lower().endswith('.zip'): | |
| return "❌ Por favor, envie um arquivo ZIP do projeto.", "", None | |
| # Extrai ZIP | |
| temp_dir = self.extract_zip(zip_path) | |
| # Analisa estrutura | |
| structure, stats, relevant_files = self.analyze_structure(temp_dir, max_depth) | |
| # Nome do projeto (do ZIP) | |
| project_name = Path(zip_path).stem | |
| # Cria relatório | |
| report = self.create_structure_report(project_name, structure, stats) | |
| # Salva relatório | |
| report_file = tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') | |
| report_file.write(report) | |
| report_file.close() | |
| return report, structure, report_file.name | |
| except zipfile.BadZipFile: | |
| return "❌ Arquivo ZIP inválido ou corrompido.", "", None | |
| except Exception as e: | |
| error_msg = f"❌ Erro ao processar: {str(e)}" | |
| import traceback | |
| traceback.print_exc() | |
| return error_msg, "", None | |
| finally: | |
| # Limpa diretório temporário | |
| if temp_dir and os.path.exists(temp_dir): | |
| try: | |
| shutil.rmtree(temp_dir) | |
| print(f"🧹 Limpeza: {temp_dir}") | |
| except: | |
| pass | |
| def create_interface(): | |
| """Cria a interface Gradio""" | |
| analyzer = WebProjectAnalyzer() | |
| with gr.Blocks( | |
| title="🔍 Web Project Structure Analyzer", | |
| theme=gr.themes.Soft(primary_hue="purple"), | |
| ) as demo: | |
| gr.Markdown(""" | |
| # 🔍 Web Project Structure Analyzer | |
| ### Analise a estrutura do seu projeto web automaticamente! | |
| **✨ Recursos:** | |
| - 📦 Upload de arquivo ZIP do projeto completo | |
| - 🎯 Filtragem inteligente (ignora node_modules, .git, cache, etc) | |
| - 📊 Estatísticas detalhadas | |
| - 💾 Download do relatório em Markdown | |
| - 🌳 Visualização em árvore ASCII | |
| --- | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 📦 Upload do Projeto (ZIP)") | |
| zip_input = gr.File( | |
| label="Arraste o arquivo ZIP ou clique para selecionar", | |
| file_count="single", | |
| file_types=[".zip"], | |
| type="filepath", | |
| height=200 | |
| ) | |
| gr.Markdown(""" | |
| **💡 Como usar:** | |
| 1. **Comprima seu projeto em ZIP:** | |
| - Windows: Clique direito na pasta → "Enviar para" → "Pasta compactada" | |
| - Mac: Clique direito na pasta → "Comprimir" | |
| - Linux: `zip -r meu-projeto.zip meu-projeto/` | |
| 2. **Upload:** | |
| - Arraste o arquivo .zip aqui | |
| - OU clique e selecione o arquivo | |
| 3. **Automático:** | |
| - A análise inicia automaticamente! | |
| - Aguarde alguns segundos | |
| **🎯 Download do GitHub:** | |
| - Vá no repositório | |
| - Clique em "Code" → "Download ZIP" | |
| - Faça upload aqui! | |
| """) | |
| max_depth = gr.Slider( | |
| minimum=1, | |
| maximum=8, | |
| value=5, | |
| step=1, | |
| label="🔍 Profundidade de Análise", | |
| info="Níveis de subpastas a escanear" | |
| ) | |
| analyze_btn = gr.Button( | |
| "🚀 Re-Analisar", | |
| variant="secondary", | |
| size="lg" | |
| ) | |
| gr.Markdown(""" | |
| --- | |
| ### ℹ️ O que é analisado? | |
| **✅ INCLUÍDO:** | |
| - 🐍 Código fonte (.py, .js, .html, .css, .php) | |
| - ⚙️ Arquivos de config (package.json, requirements.txt) | |
| - 📖 Documentação (README.md, LICENSE) | |
| - 🐳 Docker (Dockerfile, docker-compose.yml) | |
| **🚫 IGNORADO AUTOMATICAMENTE:** | |
| - 📦 node_modules, vendor | |
| - 🗂️ .git, .vscode, .idea | |
| - 🐍 venv, __pycache__ | |
| - 🏗️ dist, build, cache | |
| - 📊 logs, tmp | |
| - 🗑️ Arquivos temporários | |
| """) | |
| with gr.Column(scale=2): | |
| gr.Markdown("### 📊 Resultado da Análise") | |
| report_output = gr.Markdown( | |
| value="""**📁 Status**: Aguardando upload... | |
| **💡 Dica Rápida:** | |
| 1. Comprima sua pasta de projeto em ZIP | |
| 2. Arraste o arquivo aqui | |
| 3. Pronto! Análise automática. | |
| **🎯 Para projetos do GitHub:** | |
| - Code → Download ZIP → Upload aqui""", | |
| label="Relatório" | |
| ) | |
| structure_output = gr.Textbox( | |
| label="🌳 Estrutura em Árvore (Copie e Cole)", | |
| lines=22, | |
| max_lines=30, | |
| value="ℹ️ A estrutura do projeto aparecerá aqui após o upload...", | |
| show_copy_button=True, | |
| container=True | |
| ) | |
| download_output = gr.File( | |
| label="💾 Download do Relatório Completo (Markdown)", | |
| visible=False | |
| ) | |
| # Eventos | |
| def analyze_wrapper(zip_file, depth): | |
| if not zip_file: | |
| return ( | |
| "**📁 Status**: Aguardando upload...", | |
| "ℹ️ Faça upload de um arquivo ZIP para começar...", | |
| gr.File(visible=False) | |
| ) | |
| report, structure, file_path = analyzer.process_zip(zip_file, depth) | |
| if file_path: | |
| return report, structure, gr.File(value=file_path, visible=True) | |
| else: | |
| return report, structure, gr.File(visible=False) | |
| # Upload automático | |
| zip_input.change( | |
| fn=analyze_wrapper, | |
| inputs=[zip_input, max_depth], | |
| outputs=[report_output, structure_output, download_output] | |
| ) | |
| # Botão manual | |
| analyze_btn.click( | |
| fn=analyze_wrapper, | |
| inputs=[zip_input, max_depth], | |
| outputs=[report_output, structure_output, download_output] | |
| ) | |
| # Quando muda a profundidade | |
| max_depth.change( | |
| fn=analyze_wrapper, | |
| inputs=[zip_input, max_depth], | |
| outputs=[report_output, structure_output, download_output] | |
| ) | |
| gr.Markdown(""" | |
| --- | |
| ### 🎯 Exemplos de Uso | |
| **React App:** | |
| ```bash | |
| cd meu-react-app | |
| zip -r meu-react-app.zip . -x "node_modules/*" ".git/*" | |
| ``` | |
| **Python/Django:** | |
| ```bash | |
| cd meu-projeto-django | |
| zip -r meu-projeto.zip . -x "venv/*" ".git/*" "*.pyc" | |
| ``` | |
| **Node.js API:** | |
| ```bash | |
| cd minha-api | |
| zip -r minha-api.zip . -x "node_modules/*" ".git/*" | |
| ``` | |
| --- | |
| ### 📋 Formatos Suportados | |
| - ✅ Arquivos .zip padrão | |
| - ✅ ZIP do GitHub ("Download ZIP") | |
| - ✅ ZIP com ou sem pasta raiz | |
| - ✅ Projetos de qualquer tamanho | |
| --- | |
| **Made with ❤️ using Gradio 5.49.1+ | Open Source** | |
| """) | |
| return demo | |
| if __name__ == "__main__": | |
| demo = create_interface() | |
| demo.launch() |