Download app.py from coidada/PyLingual: direct link, hf CLI and curl.
- Browser
- Download file 23.7 kB
-
https://huggingface.co/spaces/coidada/PyLingual/resolve/main/app.py
- Command line
-
hf download hf://spaces/coidada/PyLingual/app.py
-
curl -L -o app.py https://huggingface.co/spaces/coidada/PyLingual/resolve/main/app.py
23.7 kB
| 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='<span class="status-badge status-processing"><span class="spinner"></span>Инициализация...</span>'), | |
| 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='<span class="status-badge status-processing"><span class="spinner"></span>Декомпиляция...</span>'), | |
| 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='<span class="status-badge status-processing"><span class="spinner"></span>Сохранение...</span>'), | |
| 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 = ( | |
| '<span class="status-badge status-error">⚠ Ошибка декомпиляции</span>' | |
| if is_error else | |
| '<span class="status-badge status-success">✓ Успешно</span>' | |
| ) | |
| 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='<span class="status-badge status-processing"><span class="spinner"></span>Распаковка архива...</span>'), | |
| 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='<span class="status-badge status-error">✗ Нет .pyc файлов</span>'), | |
| 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'<span class="status-badge status-processing"><span class="spinner"></span>Найдено {total} файлов...</span>'), | |
| 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'<span class="status-badge status-processing"><span class="spinner"></span>Обработка {i}/{total}...</span>'), | |
| 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='<span class="status-badge status-processing"><span class="spinner"></span>Сборка архива...</span>'), | |
| 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""" | |
| <div class="card"> | |
| <h3 style="margin-top:0">📦 Декомпиляция завершена</h3> | |
| <div class="divider"></div> | |
| <div class="stats-grid"> | |
| <div class="stat-box"> | |
| <div class="stat-value">{total}</div> | |
| <div class="stat-label">Всего файлов</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-value" style="color: var(--success)">{success_count}</div> | |
| <div class="stat-label">Успешно</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-value" style="color: {'var(--success)' if errors == 0 else 'var(--error)'}">{errors}</div> | |
| <div class="stat-label">Ошибок</div> | |
| </div> | |
| </div> | |
| <p style="color: var(--text-secondary); margin-top: 16px; font-size: 0.9em;"> | |
| Все подпапки сохранены. Исходные <code>.pyc</code> удалены, оставлен только чистый код. | |
| </p> | |
| </div> | |
| """.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'<span class="status-badge {status_color}">{status_icon} Готово</span>'), | |
| 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='<span class="status-badge status-error">✗ Неверный формат</span>'), | |
| 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(""" | |
| <div style="text-align: center; padding: 20px 0 10px 0;"> | |
| <h1 style="font-size: 2.5em; margin-bottom: 8px; font-weight: 800; letter-spacing: -0.03em;"> | |
| ◈ PYLINGUAL | |
| </h1> | |
| <p style="color: #666; font-family: 'JetBrains Mono', monospace; font-size: 0.9em; letter-spacing: 0.1em; text-transform: uppercase;"> | |
| Локальный декомпилятор Python байткода | |
| </p> | |
| <div class="divider" style="margin-top: 20px;"></div> | |
| </div> | |
| """) | |
| 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(""" | |
| <div style="margin-top: 24px; padding: 16px; background: #111; border: 1px solid #333; border-radius: 8px;"> | |
| <p style="color: #666; font-size: 0.8em; margin: 0; font-family: 'JetBrains Mono', monospace;"> | |
| <strong style="color: #888;">ДВИЖОК:</strong> syssec-utd/pylingual<br> | |
| <strong style="color: #888;">ПОДДЕРЖКА:</strong> .pyc, .zip (рекурсивно)<br> | |
| <strong style="color: #888;">ВЕРСИЯ:</strong> 2.0 Black Edition | |
| </p> | |
| </div> | |
| """) | |
| # Правая колонка — результат | |
| 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 | |
| ) |