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"""

📦 Декомпиляция завершена

{total}
Всего файлов
{success_count}
Успешно
{errors}
Ошибок

Все подпапки сохранены. Исходные .pyc удалены, оставлен только чистый код.

""".strip() yield { code_view: gr.update(visible=False, value=""), zip_info: gr.update(visible=True, value=report), download_btn: gr.update(visible=True, value=out_zip_path), progress_bar: gr.update(visible=False, value=1.0), status_text: gr.update(value=f'{status_icon} Готово'), console_out: gr.update(value=f"".join(log_lines) + f"\n> Архив собран: decompiled_project.zip\n> Готово!"), file_input: gr.update(interactive=True) } else: yield { code_view: gr.update(visible=True, value="# ⚠ Неподдерживаемый формат\n\nЗагрузите файл с расширением `.pyc` или `.zip`"), 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='✗ Неверный формат'), console_out: gr.update(value="> Ошибка: Неподдерживаемый формат файла"), file_input: gr.update(interactive=True) } # ==================== UI ==================== with gr.Blocks( title="PyLingual | Black Edition", css=CUSTOM_CSS, theme=gr.themes.Base( primary_hue="neutral", secondary_hue="neutral", neutral_hue="neutral", font=["Inter", "system-ui", "sans-serif"], font_mono=["JetBrains Mono", "monospace"], ).set( body_background_fill="#0a0a0a", body_background_fill_dark="#0a0a0a", body_text_color="#f0f0f0", body_text_color_dark="#f0f0f0", background_fill_primary="#111111", background_fill_primary_dark="#111111", background_fill_secondary="#1a1a1a", background_fill_secondary_dark="#1a1a1a", border_color_primary="#333333", border_color_primary_dark="#333333", button_primary_background_fill="#ffffff", button_primary_background_fill_dark="#ffffff", button_primary_text_color="#0a0a0a", button_primary_text_color_dark="#0a0a0a", button_primary_border_color="#ffffff", button_primary_border_color_dark="#ffffff", button_secondary_background_fill="transparent", button_secondary_background_fill_dark="transparent", button_secondary_text_color="#a0a0a0", button_secondary_text_color_dark="#a0a0a0", button_secondary_border_color="#333333", button_secondary_border_color_dark="#333333", input_background_fill="#111111", input_background_fill_dark="#111111", input_border_color="#333333", input_border_color_dark="#333333", input_placeholder_color="#666666", input_placeholder_color_dark="#666666", slider_color="#ffffff", slider_color_dark="#ffffff", block_label_text_color="#a0a0a0", block_label_text_color_dark="#a0a0a0", block_title_text_color="#f0f0f0", block_title_text_color_dark="#f0f0f0", block_background_fill="#111111", block_background_fill_dark="#111111", block_border_color="#333333", block_border_color_dark="#333333", ) ) as demo: # Header gr.Markdown("""

◈ PYLINGUAL

Локальный декомпилятор Python байткода

""") with gr.Row(): # Левая колонка — загрузка и статус with gr.Column(scale=1): gr.Markdown("### 📤 Загрузка") file_input = gr.File( label="Перетащите .pyc или .zip сюда", file_types=[".pyc", ".zip"], type="filepath", file_count="single", elem_classes="card" ) # Статус и прогресс status_text = gr.HTML( value="", visible=False, label="Статус" ) progress_bar = gr.Slider( minimum=0, maximum=1, value=0, step=0.01, label="Прогресс", visible=False, interactive=False ) # Консоль вывода console_out = gr.Textbox( label="Консоль", visible=False, lines=8, max_lines=12, interactive=False, elem_classes="console-output" ) # Кнопка скачивания — ФИКС: используем File с правильными параметрами download_btn = gr.File( label="⬇ Скачать результат", visible=False, interactive=False, elem_classes="download-button" ) gr.Markdown("""

ДВИЖОК: syssec-utd/pylingual
ПОДДЕРЖКА: .pyc, .zip (рекурсивно)
ВЕРСИЯ: 2.0 Black Edition

""") # Правая колонка — результат with gr.Column(scale=2): gr.Markdown("### 📝 Результат") code_view = gr.Code( label="Декомпилированный код", language="python", interactive=False, visible=False, lines=25, elem_classes="card" ) zip_info = gr.HTML( visible=False, label="Информация" ) # Event handler — используем генератор для прогресса! file_input.upload( fn=process_file, inputs=file_input, outputs=[code_view, zip_info, download_btn, progress_bar, status_text, console_out, file_input], show_progress="hidden" # Скрываем дефолтный прогресс, используем свой ) if __name__ == "__main__": demo.queue().launch( server_name="0.0.0.0", server_port=7860, show_error=True, favicon_path=None )