coidada commited on
Commit
a4715f0
·
verified ·
1 Parent(s): f40b211

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +580 -68
app.py CHANGED
@@ -4,6 +4,268 @@ import subprocess
4
  import zipfile
5
  import tempfile
6
  import shutil
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  def decompile_pyc(pyc_path, workspace_dir):
9
  """Вызов локального движка PyLingual"""
@@ -26,40 +288,89 @@ def decompile_pyc(pyc_path, workspace_dir):
26
  if not code:
27
  code = result.stdout.strip()
28
  if not code:
29
- code = f"#[Ошибка PyLingual]\n# STDERR: {result.stderr}\n# STDOUT: {result.stdout}"
30
 
31
  return code, result.stderr
32
 
33
- def process_file(file_obj):
 
 
 
 
 
 
34
  if not file_obj:
35
- return (
36
- gr.update(visible=False),
37
- gr.update(visible=False),
38
- gr.update(visible=False, value=None)
39
- )
 
 
 
 
 
40
 
41
  file_path = file_obj.name
42
  ext = os.path.splitext(file_path)[1].lower()
43
-
44
  temp_workspace = tempfile.mkdtemp()
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  if ext == '.pyc':
 
 
 
 
 
 
 
47
  code, err = decompile_pyc(file_path, temp_workspace)
48
 
 
 
 
 
 
 
49
  base_name = os.path.basename(file_path)
50
  out_name = base_name[:-1] if base_name.endswith('c') else base_name + ".py"
51
  download_path = os.path.join(temp_workspace, out_name)
52
 
53
  with open(download_path, "w", encoding="utf-8") as f:
54
  f.write(code)
55
-
56
- return (
57
- gr.update(visible=True, value=code),
58
- gr.update(visible=False),
59
- gr.update(visible=True, value=download_path)
 
 
 
60
  )
 
 
 
 
 
 
 
 
 
 
61
 
62
  elif ext == '.zip':
 
63
  extract_dir = os.path.join(temp_workspace, "extracted")
64
  out_zip_dir = os.path.join(temp_workspace, "output_zip")
65
  os.makedirs(extract_dir)
@@ -67,87 +378,288 @@ def process_file(file_obj):
67
 
68
  out_zip_path = os.path.join(out_zip_dir, "decompiled_project.zip")
69
 
 
 
 
 
 
 
70
  with zipfile.ZipFile(file_path, 'r') as zip_ref:
71
  zip_ref.extractall(extract_dir)
72
-
73
- pyc_count = 0
74
- errors = 0
75
 
 
 
76
  for root, dirs, files in os.walk(extract_dir):
77
  for file in files:
78
  if file.lower().endswith('.pyc'):
79
- pyc_count += 1
80
- full_path = os.path.join(root, file)
81
-
82
- code, err = decompile_pyc(full_path, temp_workspace)
83
- if "[Ошибка PyLingual]" in code:
84
- errors += 1
85
-
86
- out_py = full_path[:-1] if full_path.endswith('c') else full_path + ".py"
87
- with open(out_py, "w", encoding="utf-8") as f:
88
- f.write(code)
89
-
90
- try:
91
- os.remove(full_path)
92
- except:
93
- pass
94
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  shutil.make_archive(out_zip_path[:-4], 'zip', extract_dir)
96
 
97
- msg = (
98
- "## 📦 Архив успешно декомпилирован!\n\n"
99
- f"- **Найдено `.pyc` файлов:** {pyc_count}\n"
100
- f"- **Ошибок декомпиляции:** {errors}\n\n"
101
- "*Все подпапки сохранены на своих местах. Исходные `.pyc` удалены, оставлен только чистый код.*"
102
- )
103
-
104
- return (
105
- gr.update(visible=False),
106
- gr.update(visible=True, value=msg),
107
- gr.update(visible=True, value=out_zip_path)
108
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
  else:
111
- return (
112
- gr.update(visible=True, value="# Неподдерживаемый формат. Закинь .pyc или .zip"),
113
- gr.update(visible=False),
114
- gr.update(visible=False, value=None)
115
- )
 
 
 
 
116
 
117
- with gr.Blocks(title="PyLingual Engine UI", theme=gr.themes.Soft()) as demo:
118
- gr.Markdown("# 🐍 PyLingual Local Decompiler")
119
- gr.Markdown("*Движок: [syssec-utd/pylingual](https://github.com/syssec-utd/pylingual)*")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
  with gr.Row():
 
122
  with gr.Column(scale=1):
 
 
123
  file_input = gr.File(
124
- label="Закинь сюда .pyc или .zip (с подпапками)",
125
  file_types=[".pyc", ".zip"],
126
  type="filepath",
127
- file_count="single"
 
128
  )
129
 
130
- with gr.Column(scale=1):
131
- code_view = gr.Code(
132
- label="Декомпилированный код",
133
- language="python",
134
- interactive=False,
135
- visible=False
 
 
 
 
 
 
 
 
 
136
  )
137
 
138
- zip_info = gr.Markdown(visible=False)
 
 
 
 
 
 
 
 
139
 
 
140
  download_btn = gr.File(
141
- label="📥 Скачать результат",
142
- interactive=False,
143
- visible=False
 
144
  )
145
-
146
- file_input.change(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  fn=process_file,
148
  inputs=file_input,
149
- outputs=[code_view, zip_info, download_btn]
 
150
  )
151
 
152
  if __name__ == "__main__":
153
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
4
  import zipfile
5
  import tempfile
6
  import shutil
7
+ import time
8
+
9
+ # ==================== КАСТОМНЫЙ CSS (Чёрно-белая тема) ====================
10
+ CUSTOM_CSS = """
11
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@400;600;800&display=swap');
12
+
13
+ :root {
14
+ --bg-primary: #0a0a0a;
15
+ --bg-secondary: #111111;
16
+ --bg-tertiary: #1a1a1a;
17
+ --bg-hover: #222222;
18
+ --border-color: #333333;
19
+ --text-primary: #f0f0f0;
20
+ --text-secondary: #a0a0a0;
21
+ --accent: #ffffff;
22
+ --accent-dim: #666666;
23
+ --error: #ff4444;
24
+ --success: #00ff88;
25
+ --warning: #ffaa00;
26
+ }
27
+
28
+ /* Глобальные стили */
29
+ .gradio-container {
30
+ font-family: 'Inter', sans-serif !important;
31
+ background-color: var(--bg-primary) !important;
32
+ color: var(--text-primary) !important;
33
+ }
34
+
35
+ /* Заголовки */
36
+ h1, h2, h3 {
37
+ font-family: 'JetBrains Mono', monospace !important;
38
+ color: var(--text-primary) !important;
39
+ letter-spacing: -0.02em;
40
+ }
41
+
42
+ /* Основные контейнеры */
43
+ .contain {
44
+ background-color: var(--bg-primary) !important;
45
+ max-width: 1200px !important;
46
+ }
47
+
48
+ /* Табы и кнопки */
49
+ button.primary {
50
+ background-color: var(--text-primary) !important;
51
+ color: var(--bg-primary) !important;
52
+ border: 2px solid var(--text-primary) !important;
53
+ border-radius: 4px !important;
54
+ font-weight: 700 !important;
55
+ text-transform: uppercase !important;
56
+ letter-spacing: 0.05em !important;
57
+ transition: all 0.2s ease !important;
58
+ }
59
+
60
+ button.primary:hover {
61
+ background-color: transparent !important;
62
+ color: var(--text-primary) !important;
63
+ box-shadow: 0 0 20px rgba(255,255,255,0.1) !important;
64
+ }
65
+
66
+ /* Вторичные кнопки */
67
+ button.secondary {
68
+ background-color: transparent !important;
69
+ color: var(--text-secondary) !important;
70
+ border: 1px solid var(--border-color) !important;
71
+ border-radius: 4px !important;
72
+ }
73
+
74
+ button.secondary:hover {
75
+ border-color: var(--text-primary) !important;
76
+ color: var(--text-primary) !important;
77
+ }
78
+
79
+ /* File input - Drop zone */
80
+ input[type="file"] {
81
+ background-color: var(--bg-secondary) !important;
82
+ border: 2px dashed var(--border-color) !important;
83
+ border-radius: 8px !important;
84
+ color: var(--text-secondary) !important;
85
+ transition: all 0.3s ease !important;
86
+ }
87
+
88
+ input[type="file"]:hover {
89
+ border-color: var(--text-primary) !important;
90
+ background-color: var(--bg-tertiary) !important;
91
+ }
92
+
93
+ /* Code block */
94
+ code, pre {
95
+ font-family: 'JetBrains Mono', monospace !important;
96
+ background-color: var(--bg-secondary) !important;
97
+ border: 1px solid var(--border-color) !important;
98
+ border-radius: 6px !important;
99
+ }
100
+
101
+ /* Scrollbar */
102
+ ::-webkit-scrollbar {
103
+ width: 8px;
104
+ height: 8px;
105
+ }
106
+
107
+ ::-webkit-scrollbar-track {
108
+ background: var(--bg-primary);
109
+ }
110
+
111
+ ::-webkit-scrollbar-thumb {
112
+ background: var(--border-color);
113
+ border-radius: 4px;
114
+ }
115
+
116
+ ::-webkit-scrollbar-thumb:hover {
117
+ background: var(--accent-dim);
118
+ }
119
+
120
+ /* Progress bar */
121
+ .progress-bar-wrap {
122
+ background-color: var(--bg-secondary) !important;
123
+ border: 1px solid var(--border-color) !important;
124
+ border-radius: 4px !important;
125
+ overflow: hidden !important;
126
+ }
127
+
128
+ .progress-bar {
129
+ background: linear-gradient(90deg, #333, #fff, #333) !important;
130
+ background-size: 200% 100% !important;
131
+ animation: shimmer 2s infinite !important;
132
+ height: 100% !important;
133
+ }
134
+
135
+ @keyframes shimmer {
136
+ 0% { background-position: 200% 0; }
137
+ 100% { background-position: -200% 0; }
138
+ }
139
+
140
+ /* Status badges */
141
+ .status-badge {
142
+ display: inline-block;
143
+ padding: 4px 12px;
144
+ border-radius: 4px;
145
+ font-family: 'JetBrains Mono', monospace;
146
+ font-size: 0.85em;
147
+ font-weight: 700;
148
+ text-transform: uppercase;
149
+ letter-spacing: 0.05em;
150
+ }
151
+
152
+ .status-success {
153
+ background-color: rgba(0, 255, 136, 0.1);
154
+ color: var(--success);
155
+ border: 1px solid rgba(0, 255, 136, 0.3);
156
+ }
157
+
158
+ .status-error {
159
+ background-color: rgba(255, 68, 68, 0.1);
160
+ color: var(--error);
161
+ border: 1px solid rgba(255, 68, 68, 0.3);
162
+ }
163
+
164
+ .status-processing {
165
+ background-color: rgba(255, 170, 0, 0.1);
166
+ color: var(--warning);
167
+ border: 1px solid rgba(255, 170, 0, 0.3);
168
+ }
169
+
170
+ /* Cards */
171
+ .card {
172
+ background-color: var(--bg-secondary);
173
+ border: 1px solid var(--border-color);
174
+ border-radius: 8px;
175
+ padding: 24px;
176
+ margin-bottom: 16px;
177
+ }
178
+
179
+ .card-hover:hover {
180
+ border-color: var(--accent-dim);
181
+ transition: border-color 0.3s ease;
182
+ }
183
+
184
+ /* Divider */
185
+ .divider {
186
+ height: 1px;
187
+ background: linear-gradient(90deg, transparent, var(--border-color), transparent);
188
+ margin: 24px 0;
189
+ }
190
+
191
+ /* Stats grid */
192
+ .stats-grid {
193
+ display: grid;
194
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
195
+ gap: 16px;
196
+ margin-top: 16px;
197
+ }
198
+
199
+ .stat-box {
200
+ background-color: var(--bg-tertiary);
201
+ border: 1px solid var(--border-color);
202
+ border-radius: 6px;
203
+ padding: 16px;
204
+ text-align: center;
205
+ }
206
+
207
+ .stat-value {
208
+ font-family: 'JetBrains Mono', monospace;
209
+ font-size: 1.5em;
210
+ font-weight: 700;
211
+ color: var(--text-primary);
212
+ }
213
+
214
+ .stat-label {
215
+ font-size: 0.75em;
216
+ color: var(--text-secondary);
217
+ text-transform: uppercase;
218
+ letter-spacing: 0.1em;
219
+ margin-top: 4px;
220
+ }
221
+
222
+ /* Console output */
223
+ .console-output {
224
+ background-color: #000;
225
+ border: 1px solid #333;
226
+ border-radius: 6px;
227
+ padding: 16px;
228
+ font-family: 'JetBrains Mono', monospace;
229
+ font-size: 0.85em;
230
+ color: #aaa;
231
+ max-height: 200px;
232
+ overflow-y: auto;
233
+ white-space: pre-wrap;
234
+ word-break: break-all;
235
+ }
236
+
237
+ /* Hide default Gradio branding */
238
+ footer { display: none !important; }
239
+
240
+ /* File component fix */
241
+ .file-preview {
242
+ background-color: var(--bg-secondary) !important;
243
+ border: 1px solid var(--border-color) !important;
244
+ }
245
+
246
+ /* Download button fix */
247
+ .download-button {
248
+ width: 100% !important;
249
+ }
250
+
251
+ /* Loading spinner */
252
+ @keyframes spin {
253
+ to { transform: rotate(360deg); }
254
+ }
255
+
256
+ .spinner {
257
+ display: inline-block;
258
+ width: 16px;
259
+ height: 16px;
260
+ border: 2px solid var(--border-color);
261
+ border-top-color: var(--text-primary);
262
+ border-radius: 50%;
263
+ animation: spin 0.8s linear infinite;
264
+ margin-right: 8px;
265
+ }
266
+ """
267
+
268
+ # ==================== ЛОГИКА ДЕКОМПИЛЯЦИИ ====================
269
 
270
  def decompile_pyc(pyc_path, workspace_dir):
271
  """Вызов локального движка PyLingual"""
 
288
  if not code:
289
  code = result.stdout.strip()
290
  if not code:
291
+ code = f"# [Ошибка PyLingual]\n# STDERR: {result.stderr}\n# STDOUT: {result.stdout}"
292
 
293
  return code, result.stderr
294
 
295
+
296
+ def process_file(file_obj, progress=gr.Progress()):
297
+ """
298
+ Генератор для потоковой передачи прогресса.
299
+ Используем yield для обновления UI на каждом этапе.
300
+ """
301
+ # Начальное состояние — всё скрыто
302
  if not file_obj:
303
+ yield {
304
+ code_view: gr.update(visible=False, value=""),
305
+ zip_info: gr.update(visible=False, value=""),
306
+ download_btn: gr.update(visible=False, value=None),
307
+ progress_bar: gr.update(visible=False, value=0),
308
+ status_text: gr.update(visible=False, value=""),
309
+ console_out: gr.update(visible=False, value=""),
310
+ file_input: gr.update()
311
+ }
312
+ return
313
 
314
  file_path = file_obj.name
315
  ext = os.path.splitext(file_path)[1].lower()
 
316
  temp_workspace = tempfile.mkdtemp()
317
+
318
+ # Показываем прогресс-бар и статус
319
+ yield {
320
+ code_view: gr.update(visible=False, value=""),
321
+ zip_info: gr.update(visible=False, value=""),
322
+ download_btn: gr.update(visible=False, value=None),
323
+ progress_bar: gr.update(visible=True, value=0.05),
324
+ status_text: gr.update(visible=True, value='<span class="status-badge status-processing"><span class="spinner"></span>Инициализация...</span>'),
325
+ console_out: gr.update(visible=True, value=f"> Анализ файла: {os.path.basename(file_path)}\n> Расширение: {ext}\n"),
326
+ file_input: gr.update(interactive=False)
327
+ }
328
+ time.sleep(0.3)
329
 
330
  if ext == '.pyc':
331
+ # === ОБРАБОТКА ОДНОГО .pyc ===
332
+ yield {
333
+ progress_bar: gr.update(value=0.2),
334
+ status_text: gr.update(value='<span class="status-badge status-processing"><span class="spinner"></span>Декомпиляция...</span>'),
335
+ console_out: gr.update(value=f"> Запуск PyLingual...\n> Рабочая директория: {temp_workspace}\n"),
336
+ }
337
+
338
  code, err = decompile_pyc(file_path, temp_workspace)
339
 
340
+ yield {
341
+ progress_bar: gr.update(value=0.7),
342
+ status_text: gr.update(value='<span class="status-badge status-processing"><span class="spinner"></span>Сохранение...</span>'),
343
+ console_out: gr.update(value=f"> Декомпиляция завершена\n> Ошибок: {'Нет' if not err else 'Есть'}\n"),
344
+ }
345
+
346
  base_name = os.path.basename(file_path)
347
  out_name = base_name[:-1] if base_name.endswith('c') else base_name + ".py"
348
  download_path = os.path.join(temp_workspace, out_name)
349
 
350
  with open(download_path, "w", encoding="utf-8") as f:
351
  f.write(code)
352
+
353
+ time.sleep(0.2)
354
+
355
+ is_error = "[Ошибка PyLingual]" in code
356
+ status_html = (
357
+ '<span class="status-badge status-error">⚠ Ошибка декомпиляции</span>'
358
+ if is_error else
359
+ '<span class="status-badge status-success">✓ Успешно</span>'
360
  )
361
+
362
+ yield {
363
+ code_view: gr.update(visible=True, value=code),
364
+ zip_info: gr.update(visible=False, value=""),
365
+ download_btn: gr.update(visible=True, value=download_path),
366
+ progress_bar: gr.update(visible=False, value=1.0),
367
+ status_text: gr.update(value=status_html),
368
+ console_out: gr.update(value=f"> Результат сохранён: {out_name}\n> Готово к скачиванию"),
369
+ file_input: gr.update(interactive=True)
370
+ }
371
 
372
  elif ext == '.zip':
373
+ # === ОБРАБОТКА ZIP-АРХИВА ===
374
  extract_dir = os.path.join(temp_workspace, "extracted")
375
  out_zip_dir = os.path.join(temp_workspace, "output_zip")
376
  os.makedirs(extract_dir)
 
378
 
379
  out_zip_path = os.path.join(out_zip_dir, "decompiled_project.zip")
380
 
381
+ yield {
382
+ progress_bar: gr.update(value=0.1),
383
+ status_text: gr.update(value='<span class="status-badge status-processing"><span class="spinner"></span>Распаковка архива...</span>'),
384
+ console_out: gr.update(value=f"> Распаковка {os.path.basename(file_path)}...\n"),
385
+ }
386
+
387
  with zipfile.ZipFile(file_path, 'r') as zip_ref:
388
  zip_ref.extractall(extract_dir)
 
 
 
389
 
390
+ # Собираем все .pyc файлы
391
+ pyc_files = []
392
  for root, dirs, files in os.walk(extract_dir):
393
  for file in files:
394
  if file.lower().endswith('.pyc'):
395
+ pyc_files.append(os.path.join(root, file))
396
+
397
+ total = len(pyc_files)
398
+
399
+ if total == 0:
400
+ yield {
401
+ code_view: gr.update(visible=True, value="# В архиве не найдено .pyc файлов"),
402
+ zip_info: gr.update(visible=False, value=""),
403
+ download_btn: gr.update(visible=False, value=None),
404
+ progress_bar: gr.update(visible=False, value=0),
405
+ status_text: gr.update(value='<span class="status-badge status-error">✗ Нет .pyc файлов</span>'),
406
+ console_out: gr.update(value="> Ошибка: В архиве отсутствуют .pyc файлы"),
407
+ file_input: gr.update(interactive=True)
408
+ }
409
+ return
410
+
411
+ yield {
412
+ progress_bar: gr.update(value=0.15),
413
+ status_text: gr.update(value=f'<span class="status-badge status-processing"><span class="spinner"></span>Найдено {total} файлов...</span>'),
414
+ console_out: gr.update(value=f"> Найдено .pyc файлов: {total}\n> Начинаю декомпиляцию...\n"),
415
+ }
416
+
417
+ errors = 0
418
+ log_lines = [f"> Найдено .pyc файлов: {total}\n"]
419
+
420
+ for i, full_path in enumerate(pyc_files, 1):
421
+ rel_path = os.path.relpath(full_path, extract_dir)
422
+ progress_val = 0.15 + (0.7 * (i / total))
423
+
424
+ yield {
425
+ progress_bar: gr.update(value=progress_val),
426
+ status_text: gr.update(value=f'<span class="status-badge status-processing"><span class="spinner"></span>Обработка {i}/{total}...</span>'),
427
+ console_out: gr.update(value=f"".join(log_lines) + f"> [{i}/{total}] {rel_path}"),
428
+ }
429
+
430
+ code, err = decompile_pyc(full_path, temp_workspace)
431
+ if "[Ошибка PyLingual]" in code:
432
+ errors += 1
433
+ log_lines.append(f"> [{i}/{total}] ⚠ {rel_path} — ОШИБКА\n")
434
+ else:
435
+ log_lines.append(f"> [{i}/{total}] ✓ {rel_path}\n")
436
+
437
+ # Сохраняем .py рядом с .pyc
438
+ out_py = full_path[:-1] if full_path.endswith('c') else full_path + ".py"
439
+ with open(out_py, "w", encoding="utf-8") as f:
440
+ f.write(code)
441
+
442
+ # Удаляем оригинальный .pyc
443
+ try:
444
+ os.remove(full_path)
445
+ except:
446
+ pass
447
+
448
+ yield {
449
+ progress_bar: gr.update(value=0.9),
450
+ status_text: gr.update(value='<span class="status-badge status-processing"><span class="spinner"></span>Сборка архива...</span>'),
451
+ console_out: gr.update(value=f"".join(log_lines) + f"\n> Сборка выходного архива..."),
452
+ }
453
+
454
  shutil.make_archive(out_zip_path[:-4], 'zip', extract_dir)
455
 
456
+ time.sleep(0.3)
457
+
458
+ # Формируем красивый отчёт
459
+ success_count = total - errors
460
+ status_color = "status-success" if errors == 0 else "status-error" if errors == total else "status-processing"
461
+ status_icon = "✓" if errors == 0 else "⚠"
462
+
463
+ report = f"""
464
+ <div class="card">
465
+ <h3 style="margin-top:0">📦 Декомпиляция завершена</h3>
466
+ <div class="divider"></div>
467
+ <div class="stats-grid">
468
+ <div class="stat-box">
469
+ <div class="stat-value">{total}</div>
470
+ <div class="stat-label">Всего файлов</div>
471
+ </div>
472
+ <div class="stat-box">
473
+ <div class="stat-value" style="color: var(--success)">{success_count}</div>
474
+ <div class="stat-label">Успешно</div>
475
+ </div>
476
+ <div class="stat-box">
477
+ <div class="stat-value" style="color: {'var(--success)' if errors == 0 else 'var(--error)'}">{errors}</div>
478
+ <div class="stat-label">Ошибок</div>
479
+ </div>
480
+ </div>
481
+ <p style="color: var(--text-secondary); margin-top: 16px; font-size: 0.9em;">
482
+ Все подпапки сохранены. Исходные <code>.pyc</code> удалены, оставлен только чистый код.
483
+ </p>
484
+ </div>
485
+ """.strip()
486
+
487
+ yield {
488
+ code_view: gr.update(visible=False, value=""),
489
+ zip_info: gr.update(visible=True, value=report),
490
+ download_btn: gr.update(visible=True, value=out_zip_path),
491
+ progress_bar: gr.update(visible=False, value=1.0),
492
+ status_text: gr.update(value=f'<span class="status-badge {status_color}">{status_icon} Готово</span>'),
493
+ console_out: gr.update(value=f"".join(log_lines) + f"\n> Архив собран: decompiled_project.zip\n> Готово!"),
494
+ file_input: gr.update(interactive=True)
495
+ }
496
 
497
  else:
498
+ yield {
499
+ code_view: gr.update(visible=True, value="# ⚠ Неподдерживаемый формат\n\nЗагрузите файл с расширением `.pyc` или `.zip`"),
500
+ zip_info: gr.update(visible=False, value=""),
501
+ download_btn: gr.update(visible=False, value=None),
502
+ progress_bar: gr.update(visible=False, value=0),
503
+ status_text: gr.update(value='<span class="status-badge status-error">✗ Неверный формат</span>'),
504
+ console_out: gr.update(value="> Ошибка: Неподдерживаемый формат файла"),
505
+ file_input: gr.update(interactive=True)
506
+ }
507
 
508
+
509
+ # ==================== UI ====================
510
+
511
+ with gr.Blocks(
512
+ title="PyLingual | Black Edition",
513
+ css=CUSTOM_CSS,
514
+ theme=gr.themes.Base(
515
+ primary_hue="neutral",
516
+ secondary_hue="neutral",
517
+ neutral_hue="neutral",
518
+ font=["Inter", "system-ui", "sans-serif"],
519
+ font_mono=["JetBrains Mono", "monospace"],
520
+ ).set(
521
+ body_background_fill="#0a0a0a",
522
+ body_background_fill_dark="#0a0a0a",
523
+ body_text_color="#f0f0f0",
524
+ body_text_color_dark="#f0f0f0",
525
+ background_fill_primary="#111111",
526
+ background_fill_primary_dark="#111111",
527
+ background_fill_secondary="#1a1a1a",
528
+ background_fill_secondary_dark="#1a1a1a",
529
+ border_color_primary="#333333",
530
+ border_color_primary_dark="#333333",
531
+ button_primary_background_fill="#ffffff",
532
+ button_primary_background_fill_dark="#ffffff",
533
+ button_primary_text_color="#0a0a0a",
534
+ button_primary_text_color_dark="#0a0a0a",
535
+ button_primary_border_color="#ffffff",
536
+ button_primary_border_color_dark="#ffffff",
537
+ button_secondary_background_fill="transparent",
538
+ button_secondary_background_fill_dark="transparent",
539
+ button_secondary_text_color="#a0a0a0",
540
+ button_secondary_text_color_dark="#a0a0a0",
541
+ button_secondary_border_color="#333333",
542
+ button_secondary_border_color_dark="#333333",
543
+ input_background_fill="#111111",
544
+ input_background_fill_dark="#111111",
545
+ input_border_color="#333333",
546
+ input_border_color_dark="#333333",
547
+ input_placeholder_color="#666666",
548
+ input_placeholder_color_dark="#666666",
549
+ slider_color="#ffffff",
550
+ slider_color_dark="#ffffff",
551
+ block_label_text_color="#a0a0a0",
552
+ block_label_text_color_dark="#a0a0a0",
553
+ block_title_text_color="#f0f0f0",
554
+ block_title_text_color_dark="#f0f0f0",
555
+ block_background_fill="#111111",
556
+ block_background_fill_dark="#111111",
557
+ block_border_color="#333333",
558
+ block_border_color_dark="#333333",
559
+ )
560
+ ) as demo:
561
+
562
+ # Header
563
+ gr.Markdown("""
564
+ <div style="text-align: center; padding: 20px 0 10px 0;">
565
+ <h1 style="font-size: 2.5em; margin-bottom: 8px; font-weight: 800; letter-spacing: -0.03em;">
566
+ ◈ PYLINGUAL
567
+ </h1>
568
+ <p style="color: #666; font-family: 'JetBrains Mono', monospace; font-size: 0.9em; letter-spacing: 0.1em; text-transform: uppercase;">
569
+ Локальный декомпилятор Python байткода
570
+ </p>
571
+ <div class="divider" style="margin-top: 20px;"></div>
572
+ </div>
573
+ """)
574
 
575
  with gr.Row():
576
+ # Левая колонка — загрузка и статус
577
  with gr.Column(scale=1):
578
+ gr.Markdown("### 📤 Загрузка")
579
+
580
  file_input = gr.File(
581
+ label="Перетащите .pyc или .zip сюда",
582
  file_types=[".pyc", ".zip"],
583
  type="filepath",
584
+ file_count="single",
585
+ elem_classes="card"
586
  )
587
 
588
+ # Статус и прогресс
589
+ status_text = gr.HTML(
590
+ value="",
591
+ visible=False,
592
+ label="Статус"
593
+ )
594
+
595
+ progress_bar = gr.Slider(
596
+ minimum=0,
597
+ maximum=1,
598
+ value=0,
599
+ step=0.01,
600
+ label="Прогресс",
601
+ visible=False,
602
+ interactive=False
603
  )
604
 
605
+ # Консоль вывода
606
+ console_out = gr.Textbox(
607
+ label="Консоль",
608
+ visible=False,
609
+ lines=8,
610
+ max_lines=12,
611
+ interactive=False,
612
+ elem_classes="console-output"
613
+ )
614
 
615
+ # Кнопка скачивания — ФИКС: используем File с правильными параметрами
616
  download_btn = gr.File(
617
+ label="⬇ Скачать результат",
618
+ visible=False,
619
+ interactive=False,
620
+ elem_classes="download-button"
621
  )
622
+
623
+ gr.Markdown("""
624
+ <div style="margin-top: 24px; padding: 16px; background: #111; border: 1px solid #333; border-radius: 8px;">
625
+ <p style="color: #666; font-size: 0.8em; margin: 0; font-family: 'JetBrains Mono', monospace;">
626
+ <strong style="color: #888;">ДВИЖОК:</strong> syssec-utd/pylingual<br>
627
+ <strong style="color: #888;">ПОДДЕРЖКА:</strong> .pyc, .zip (рекурсивно)<br>
628
+ <strong style="color: #888;">ВЕРСИЯ:</strong> 2.0 Black Edition
629
+ </p>
630
+ </div>
631
+ """)
632
+
633
+ # Правая колонка — результат
634
+ with gr.Column(scale=2):
635
+ gr.Markdown("### 📝 Результат")
636
+
637
+ code_view = gr.Code(
638
+ label="Декомпилированный код",
639
+ language="python",
640
+ interactive=False,
641
+ visible=False,
642
+ lines=25,
643
+ elem_classes="card"
644
+ )
645
+
646
+ zip_info = gr.HTML(
647
+ visible=False,
648
+ label="Информация"
649
+ )
650
+
651
+ # Event handler — используем генератор для прогресса!
652
+ file_input.upload(
653
  fn=process_file,
654
  inputs=file_input,
655
+ outputs=[code_view, zip_info, download_btn, progress_bar, status_text, console_out, file_input],
656
+ show_progress="hidden" # Скрываем дефолтный прогресс, используем свой
657
  )
658
 
659
  if __name__ == "__main__":
660
+ demo.queue().launch(
661
+ server_name="0.0.0.0",
662
+ server_port=7860,
663
+ show_error=True,
664
+ favicon_path=None
665
+ )