import gradio as gr import os import subprocess import zipfile import tempfile import shutil import time # ==================== КАСТОМНЫЙ CSS (Чёрно-белая тема) ==================== CUSTOM_CSS = """ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@400;600;800&display=swap'); :root { --bg-primary: #0a0a0a; --bg-secondary: #111111; --bg-tertiary: #1a1a1a; --bg-hover: #222222; --border-color: #333333; --text-primary: #f0f0f0; --text-secondary: #a0a0a0; --accent: #ffffff; --accent-dim: #666666; --error: #ff4444; --success: #00ff88; --warning: #ffaa00; } /* Глобальные стили */ .gradio-container { font-family: 'Inter', sans-serif !important; background-color: var(--bg-primary) !important; color: var(--text-primary) !important; } /* Заголовки */ h1, h2, h3 { font-family: 'JetBrains Mono', monospace !important; color: var(--text-primary) !important; letter-spacing: -0.02em; } /* Основные контейнеры */ .contain { background-color: var(--bg-primary) !important; max-width: 1200px !important; } /* Табы и кнопки */ button.primary { background-color: var(--text-primary) !important; color: var(--bg-primary) !important; border: 2px solid var(--text-primary) !important; border-radius: 4px !important; font-weight: 700 !important; text-transform: uppercase !important; letter-spacing: 0.05em !important; transition: all 0.2s ease !important; } button.primary:hover { background-color: transparent !important; color: var(--text-primary) !important; box-shadow: 0 0 20px rgba(255,255,255,0.1) !important; } /* Вторичные кнопки */ button.secondary { background-color: transparent !important; color: var(--text-secondary) !important; border: 1px solid var(--border-color) !important; border-radius: 4px !important; } button.secondary:hover { border-color: var(--text-primary) !important; color: var(--text-primary) !important; } /* File input - Drop zone */ input[type="file"] { background-color: var(--bg-secondary) !important; border: 2px dashed var(--border-color) !important; border-radius: 8px !important; color: var(--text-secondary) !important; transition: all 0.3s ease !important; } input[type="file"]:hover { border-color: var(--text-primary) !important; background-color: var(--bg-tertiary) !important; } /* Code block */ code, pre { font-family: 'JetBrains Mono', monospace !important; background-color: var(--bg-secondary) !important; border: 1px solid var(--border-color) !important; border-radius: 6px !important; } /* Scrollbar */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: var(--bg-primary); } ::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: var(--accent-dim); } /* Progress bar */ .progress-bar-wrap { background-color: var(--bg-secondary) !important; border: 1px solid var(--border-color) !important; border-radius: 4px !important; overflow: hidden !important; } .progress-bar { background: linear-gradient(90deg, #333, #fff, #333) !important; background-size: 200% 100% !important; animation: shimmer 2s infinite !important; height: 100% !important; } @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } /* Status badges */ .status-badge { display: inline-block; padding: 4px 12px; border-radius: 4px; font-family: 'JetBrains Mono', monospace; font-size: 0.85em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; } .status-success { background-color: rgba(0, 255, 136, 0.1); color: var(--success); border: 1px solid rgba(0, 255, 136, 0.3); } .status-error { background-color: rgba(255, 68, 68, 0.1); color: var(--error); border: 1px solid rgba(255, 68, 68, 0.3); } .status-processing { background-color: rgba(255, 170, 0, 0.1); color: var(--warning); border: 1px solid rgba(255, 170, 0, 0.3); } /* Cards */ .card { background-color: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 8px; padding: 24px; margin-bottom: 16px; } .card-hover:hover { border-color: var(--accent-dim); transition: border-color 0.3s ease; } /* Divider */ .divider { height: 1px; background: linear-gradient(90deg, transparent, var(--border-color), transparent); margin: 24px 0; } /* Stats grid */ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; margin-top: 16px; } .stat-box { background-color: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 6px; padding: 16px; text-align: center; } .stat-value { font-family: 'JetBrains Mono', monospace; font-size: 1.5em; font-weight: 700; color: var(--text-primary); } .stat-label { font-size: 0.75em; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.1em; margin-top: 4px; } /* Console output */ .console-output { background-color: #000; border: 1px solid #333; border-radius: 6px; padding: 16px; font-family: 'JetBrains Mono', monospace; font-size: 0.85em; color: #aaa; max-height: 200px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; } /* Hide default Gradio branding */ footer { display: none !important; } /* File component fix */ .file-preview { background-color: var(--bg-secondary) !important; border: 1px solid var(--border-color) !important; } /* Download button fix */ .download-button { width: 100% !important; } /* Loading spinner */ @keyframes spin { to { transform: rotate(360deg); } } .spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid var(--border-color); border-top-color: var(--text-primary); border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 8px; } """ # ==================== ЛОГИКА ДЕКОМПИЛЯЦИИ ==================== def decompile_pyc(pyc_path, workspace_dir): """Вызов локального движка PyLingual""" out_dir = os.path.join(workspace_dir, "out_" + os.urandom(4).hex()) os.makedirs(out_dir, exist_ok=True) cmd = ['pylingual', pyc_path, '-o', out_dir, '-k', '10'] result = subprocess.run(cmd, capture_output=True, text=True) code = "" for root, _, files in os.walk(out_dir): for f in files: if f.endswith('.py'): with open(os.path.join(root, f), 'r', encoding='utf-8') as pyf: code = pyf.read() break if code: break if not code: code = result.stdout.strip() if not code: code = f"# [Ошибка PyLingual]\n# STDERR: {result.stderr}\n# STDOUT: {result.stdout}" return code, result.stderr def process_file(file_obj, progress=gr.Progress()): """ Генератор для потоковой передачи прогресса. Используем yield для обновления UI на каждом этапе. """ # Начальное состояние — всё скрыто if not file_obj: yield { code_view: gr.update(visible=False, value=""), zip_info: gr.update(visible=False, value=""), download_btn: gr.update(visible=False, value=None), progress_bar: gr.update(visible=False, value=0), status_text: gr.update(visible=False, value=""), console_out: gr.update(visible=False, value=""), file_input: gr.update() } return file_path = file_obj.name ext = os.path.splitext(file_path)[1].lower() temp_workspace = tempfile.mkdtemp() # Показываем прогресс-бар и статус yield { code_view: gr.update(visible=False, value=""), zip_info: gr.update(visible=False, value=""), download_btn: gr.update(visible=False, value=None), progress_bar: gr.update(visible=True, value=0.05), status_text: gr.update(visible=True, value='Инициализация...'), console_out: gr.update(visible=True, value=f"> Анализ файла: {os.path.basename(file_path)}\n> Расширение: {ext}\n"), file_input: gr.update(interactive=False) } time.sleep(0.3) if ext == '.pyc': # === ОБРАБОТКА ОДНОГО .pyc === yield { progress_bar: gr.update(value=0.2), status_text: gr.update(value='Декомпиляция...'), console_out: gr.update(value=f"> Запуск PyLingual...\n> Рабочая директория: {temp_workspace}\n"), } code, err = decompile_pyc(file_path, temp_workspace) yield { progress_bar: gr.update(value=0.7), status_text: gr.update(value='Сохранение...'), console_out: gr.update(value=f"> Декомпиляция завершена\n> Ошибок: {'Нет' if not err else 'Есть'}\n"), } base_name = os.path.basename(file_path) out_name = base_name[:-1] if base_name.endswith('c') else base_name + ".py" download_path = os.path.join(temp_workspace, out_name) with open(download_path, "w", encoding="utf-8") as f: f.write(code) time.sleep(0.2) is_error = "[Ошибка PyLingual]" in code status_html = ( '⚠ Ошибка декомпиляции' if is_error else '✓ Успешно' ) yield { code_view: gr.update(visible=True, value=code), zip_info: gr.update(visible=False, value=""), download_btn: gr.update(visible=True, value=download_path), progress_bar: gr.update(visible=False, value=1.0), status_text: gr.update(value=status_html), console_out: gr.update(value=f"> Результат сохранён: {out_name}\n> Готово к скачиванию"), file_input: gr.update(interactive=True) } elif ext == '.zip': # === ОБРАБОТКА ZIP-АРХИВА === extract_dir = os.path.join(temp_workspace, "extracted") out_zip_dir = os.path.join(temp_workspace, "output_zip") os.makedirs(extract_dir) os.makedirs(out_zip_dir) out_zip_path = os.path.join(out_zip_dir, "decompiled_project.zip") yield { progress_bar: gr.update(value=0.1), status_text: gr.update(value='Распаковка архива...'), console_out: gr.update(value=f"> Распаковка {os.path.basename(file_path)}...\n"), } with zipfile.ZipFile(file_path, 'r') as zip_ref: zip_ref.extractall(extract_dir) # Собираем все .pyc файлы pyc_files = [] for root, dirs, files in os.walk(extract_dir): for file in files: if file.lower().endswith('.pyc'): pyc_files.append(os.path.join(root, file)) total = len(pyc_files) if total == 0: yield { code_view: gr.update(visible=True, value="# В архиве не найдено .pyc файлов"), zip_info: gr.update(visible=False, value=""), download_btn: gr.update(visible=False, value=None), progress_bar: gr.update(visible=False, value=0), status_text: gr.update(value='✗ Нет .pyc файлов'), console_out: gr.update(value="> Ошибка: В архиве отсутствуют .pyc файлы"), file_input: gr.update(interactive=True) } return yield { progress_bar: gr.update(value=0.15), status_text: gr.update(value=f'Найдено {total} файлов...'), console_out: gr.update(value=f"> Найдено .pyc файлов: {total}\n> Начинаю декомпиляцию...\n"), } errors = 0 log_lines = [f"> Найдено .pyc файлов: {total}\n"] for i, full_path in enumerate(pyc_files, 1): rel_path = os.path.relpath(full_path, extract_dir) progress_val = 0.15 + (0.7 * (i / total)) yield { progress_bar: gr.update(value=progress_val), status_text: gr.update(value=f'Обработка {i}/{total}...'), console_out: gr.update(value=f"".join(log_lines) + f"> [{i}/{total}] {rel_path}"), } code, err = decompile_pyc(full_path, temp_workspace) if "[Ошибка PyLingual]" in code: errors += 1 log_lines.append(f"> [{i}/{total}] ⚠ {rel_path} — ОШИБКА\n") else: log_lines.append(f"> [{i}/{total}] ✓ {rel_path}\n") # Сохраняем .py рядом с .pyc out_py = full_path[:-1] if full_path.endswith('c') else full_path + ".py" with open(out_py, "w", encoding="utf-8") as f: f.write(code) # Удаляем оригинальный .pyc try: os.remove(full_path) except: pass yield { progress_bar: gr.update(value=0.9), status_text: gr.update(value='Сборка архива...'), console_out: gr.update(value=f"".join(log_lines) + f"\n> Сборка выходного архива..."), } shutil.make_archive(out_zip_path[:-4], 'zip', extract_dir) time.sleep(0.3) # Формируем красивый отчёт success_count = total - errors status_color = "status-success" if errors == 0 else "status-error" if errors == total else "status-processing" status_icon = "✓" if errors == 0 else "⚠" report = f"""
Все подпапки сохранены. Исходные .pyc удалены, оставлен только чистый код.
Локальный декомпилятор Python байткода
ДВИЖОК: syssec-utd/pylingual
ПОДДЕРЖКА: .pyc, .zip (рекурсивно)
ВЕРСИЯ: 2.0 Black Edition