pourimoto commited on
Commit
5bdeae4
·
verified ·
1 Parent(s): 4d6dd1d

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -580
app.py DELETED
@@ -1,580 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- app.py — Project File Aggregator for LLM Input (with Gradio UI)
4
-
5
- اضافه شده: پشتیبانی از فایل‌های ZIP به عنوان ورودی
6
- """
7
-
8
- import hashlib
9
- import tempfile
10
- import zipfile
11
- from pathlib import Path
12
- from datetime import datetime
13
- from concurrent.futures import ThreadPoolExecutor
14
- import os
15
- import shutil
16
-
17
- import gradio as gr
18
-
19
- # ---------------------------------------------------------------------------
20
- # Constants
21
- # ---------------------------------------------------------------------------
22
-
23
- IGNORE_DIRS_DEFAULT = (
24
- ".git,.idea,.vscode,__pycache__,.pytest_cache,node_modules,venv,.venv,"
25
- "env,build,dist,.next,.cache,site-packages,.mypy_cache,target,.gradle,bin,obj"
26
- )
27
-
28
- IGNORE_EXTENSIONS = {
29
- ".exe", ".dll", ".so", ".dylib", ".png", ".jpg", ".jpeg", ".gif", ".ico",
30
- ".svg", ".webp", ".zip", ".rar", ".7z", ".tar", ".gz", ".pdf", ".mp4",
31
- ".mp3", ".wav", ".mov", ".woff", ".woff2", ".ttf", ".eot", ".pyc",
32
- ".class", ".jar", ".db", ".sqlite3", ".lock", ".bin", ".dat",
33
- }
34
-
35
- LANGUAGE_MAP = {
36
- ".py": "Python", ".js": "JavaScript", ".jsx": "JavaScript (JSX)",
37
- ".ts": "TypeScript", ".tsx": "TypeScript (TSX)", ".html": "HTML",
38
- ".css": "CSS", ".scss": "SCSS", ".json": "JSON", ".yaml": "YAML",
39
- ".yml": "YAML", ".toml": "TOML", ".xml": "XML", ".md": "Markdown",
40
- ".sql": "SQL", ".java": "Java", ".go": "Go", ".rb": "Ruby",
41
- ".php": "PHP", ".c": "C", ".cpp": "C++", ".cs": "C#", ".swift": "Swift",
42
- ".kt": "Kotlin", ".sh": "Shell", ".txt": "Text", ".env": "Env",
43
- ".ini": "INI", ".cfg": "Config", ".rst": "reStructuredText",
44
- }
45
-
46
- EXTENSION_CATEGORIES = {
47
- "Python": {".py"},
48
- "JS / TypeScript": {".js", ".jsx", ".ts", ".tsx"},
49
- "Web (HTML/CSS)": {".html", ".css", ".scss"},
50
- "Config / Data (JSON, YAML, ...)": {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".xml", ".env"},
51
- "Documentation (Markdown/Text)": {".md", ".txt", ".rst"},
52
- "Other Languages (Java, Go, C++, ...)": {".java", ".go", ".rb", ".php", ".c", ".cpp", ".cs", ".swift", ".kt", ".sql", ".sh"},
53
- }
54
-
55
- CHARS_PER_TOKEN = 4.0
56
-
57
-
58
- # ---------------------------------------------------------------------------
59
- # Filtering logic
60
- # ---------------------------------------------------------------------------
61
-
62
- def should_ignore_dir(dir_name: str, extra_ignore_dirs: set) -> bool:
63
- return dir_name in extra_ignore_dirs or dir_name.startswith(".")
64
-
65
-
66
- # ---------------------------------------------------------------------------
67
- # ZIP handling functions (جدید)
68
- # ---------------------------------------------------------------------------
69
-
70
- def is_zip_file(file_path: str) -> bool:
71
- """بررسی می‌کند که آیا فایل یک ZIP معتبر است"""
72
- try:
73
- return zipfile.is_zipfile(file_path)
74
- except:
75
- return False
76
-
77
-
78
- def process_zip_input(zip_path: str, output_dir: str = None) -> Path:
79
- """
80
- فایل ZIP را استخراج کرده و مسیر پوشه استخراج شده را برمی‌گرداند
81
- اگر output_dir مشخص نشده باشد، از tempfile استفاده می‌کند
82
- """
83
- zip_path = Path(zip_path)
84
-
85
- # اگر دایرکتوری خروجی مشخص نشده، از دایرکتوری موقت استفاده کن
86
- if output_dir is None:
87
- temp_dir = tempfile.mkdtemp(prefix="context_llm_zip_")
88
- output_dir = Path(temp_dir)
89
- else:
90
- output_dir = Path(output_dir)
91
- output_dir.mkdir(parents=True, exist_ok=True)
92
-
93
- # استخراج فایل‌های زیپ
94
- try:
95
- with zipfile.ZipFile(zip_path, 'r') as zip_ref:
96
- # بررسی وجود فایل‌های مخرب (مسیرهای نسبی)
97
- for member in zip_ref.namelist():
98
- # جلوگیری از path traversal attacks
99
- member_path = (output_dir / member).resolve()
100
- if not str(member_path).startswith(str(output_dir.resolve())):
101
- raise Exception(f"فایل مخرب در ZIP: {member}")
102
-
103
- zip_ref.extractall(output_dir)
104
- return output_dir
105
- except Exception as e:
106
- # در صورت خطا، دایرکتوری موقت را پاک کن
107
- if output_dir is not None and str(output_dir).startswith(tempfile.gettempdir()):
108
- shutil.rmtree(output_dir, ignore_errors=True)
109
- raise Exception(f"خطا در استخراج ZIP: {str(e)}")
110
-
111
-
112
- def cleanup_temp_dir(temp_dir: Path):
113
- """پاک کردن دایرکتوری موقت (اگر از tempfile ایجاد شده باشد)"""
114
- if temp_dir is not None and str(temp_dir).startswith(tempfile.gettempdir()):
115
- try:
116
- shutil.rmtree(temp_dir, ignore_errors=True)
117
- except:
118
- pass
119
-
120
-
121
- # ---------------------------------------------------------------------------
122
- # Core scanning logic (تغییر داده شده برای پشتیبانی از ورودی‌های مختلف)
123
- # ---------------------------------------------------------------------------
124
-
125
- def scan_directory(
126
- root_path: Path,
127
- ignore_dirs: set = None,
128
- max_file_size_mb: int = 5,
129
- include_hidden: bool = False,
130
- use_gitignore: bool = False,
131
- is_temp_dir: bool = False # جدید: مشخص می‌کند که دایرکتوری موقت است
132
- ) -> tuple[list, list, int, list]:
133
- """
134
- اسکن دایرکتوری و جمع‌آوری فایل‌ها
135
- برمی‌گرداند: (file_infos, root_name, total_files, category_counts)
136
- """
137
- if ignore_dirs is None:
138
- ignore_dirs = set(IGNORE_DIRS_DEFAULT.split(","))
139
-
140
- root_path = Path(root_path)
141
- if not root_path.exists():
142
- raise FileNotFoundError(f"مسیر وجود ندارد: {root_path}")
143
-
144
- # برای پشتیبانی از فایل‌های زیپ، اگر ورودی فایل است، آن را استخراج کن
145
- if root_path.is_file() and is_zip_file(str(root_path)):
146
- # این یک فایل ZIP است
147
- temp_dir = process_zip_input(str(root_path))
148
- is_temp_dir = True
149
- root_path = temp_dir
150
- root_name = root_path.name
151
- elif root_path.is_file():
152
- # اگر فایل معمولی است، فقط همان را پردازش کن
153
- root_name = root_path.name
154
- # ایجاد یک دایرکتوری موقت برای پردازش فایل تنها
155
- temp_dir = tempfile.mkdtemp(prefix="context_llm_single_")
156
- shutil.copy2(root_path, Path(temp_dir) / root_name)
157
- root_path = Path(temp_dir) / root_name
158
- is_temp_dir = True
159
- else:
160
- root_name = root_path.name
161
- is_temp_dir = False
162
-
163
- files = []
164
- total_size = 0
165
- total_files = 0
166
- category_counts = {cat: 0 for cat in EXTENSION_CATEGORIES}
167
-
168
- for item in root_path.rglob("*"):
169
- if item.is_dir():
170
- # Check if directory should be ignored
171
- if should_ignore_dir(item.name, ignore_dirs) or item.name.startswith("."):
172
- continue
173
- continue
174
-
175
- # Check extension
176
- ext = item.suffix.lower()
177
- if ext in IGNORE_EXTENSIONS:
178
- continue
179
-
180
- # Skip if hidden file (unless requested)
181
- if not include_hidden and item.name.startswith("."):
182
- continue
183
-
184
- # Skip if file too large
185
- if item.stat().st_size > max_file_size_mb * 1024 * 1024:
186
- continue
187
-
188
- # Determine category
189
- category = "Other"
190
- for cat, exts in EXTENSION_CATEGORIES.items():
191
- if ext in exts:
192
- category = cat
193
- break
194
-
195
- category_counts[category] = category_counts.get(category, 0) + 1
196
-
197
- # Read file content
198
- try:
199
- content = item.read_text(encoding="utf-8", errors="ignore")
200
- except Exception:
201
- # Skip files that can't be read as text
202
- continue
203
-
204
- # Relative path from root
205
- rel_path = str(item.relative_to(root_path))
206
-
207
- files.append({
208
- "path": rel_path,
209
- "content": content,
210
- "ext": ext,
211
- "language": LANGUAGE_MAP.get(ext, "Unknown"),
212
- "category": category,
213
- "size": item.stat().st_size,
214
- })
215
- total_files += 1
216
-
217
- return files, root_name, total_files, category_counts, is_temp_dir
218
-
219
-
220
- # ---------------------------------------------------------------------------
221
- # Chunking and tokenization logic (بدون تغییر)
222
- # ---------------------------------------------------------------------------
223
-
224
- def chunk_file_content(file_info: dict, chunk_size_tokens: int = 4000, overlap_tokens: int = 200) -> list:
225
- """Split file content into chunks based on token estimation."""
226
- content = file_info["content"]
227
- # Estimate token count
228
- estimated_tokens = len(content) / CHARS_PER_TOKEN
229
- if estimated_tokens <= chunk_size_tokens:
230
- return [content]
231
-
232
- # Rough chunking based on character count (not exact token count)
233
- chunk_chars = int(chunk_size_tokens * CHARS_PER_TOKEN)
234
- overlap_chars = int(overlap_tokens * CHARS_PER_TOKEN)
235
-
236
- chunks = []
237
- start = 0
238
- while start < len(content):
239
- end = min(start + chunk_chars, len(content))
240
- # Try to cut at newline or space
241
- if end < len(content):
242
- # Look for newline
243
- newline_pos = content.rfind("\n", start, end)
244
- if newline_pos != -1 and newline_pos > start:
245
- end = newline_pos + 1
246
- else:
247
- # Look for space
248
- space_pos = content.rfind(" ", start, end)
249
- if space_pos != -1 and space_pos > start:
250
- end = space_pos + 1
251
-
252
- chunks.append(content[start:end])
253
- start = end - overlap_chars
254
- if start < 0:
255
- start = 0
256
-
257
- return chunks
258
-
259
-
260
- def estimate_tokens(text: str) -> int:
261
- """Estimate token count based on character count."""
262
- return int(len(text) / CHARS_PER_TOKEN)
263
-
264
-
265
- # ---------------------------------------------------------------------------
266
- # Main aggregation function (تغییر داده شده)
267
- # ---------------------------------------------------------------------------
268
-
269
- def build_context(
270
- input_path: str,
271
- ignore_dirs: str = None,
272
- max_file_size_mb: int = 5,
273
- chunk_size_tokens: int = 4000,
274
- overlap_tokens: int = 200,
275
- max_total_tokens: int = 100000,
276
- include_hidden: bool = False,
277
- use_gitignore: bool = False,
278
- include_hash: bool = True,
279
- include_tree: bool = True,
280
- ) -> tuple[str, str, dict]:
281
- """
282
- Build aggregated context for LLM input.
283
- برمی‌گرداند: (context_text, context_hash, stats)
284
- """
285
- # Parse ignore dirs
286
- if ignore_dirs:
287
- ignore_set = set(ignore_dirs.split(","))
288
- else:
289
- ignore_set = set(IGNORE_DIRS_DEFAULT.split(","))
290
-
291
- # Scan directory
292
- root_path = Path(input_path)
293
- is_temp = False
294
-
295
- try:
296
- files, root_name, total_files, category_counts, is_temp = scan_directory(
297
- root_path=root_path,
298
- ignore_dirs=ignore_set,
299
- max_file_size_mb=max_file_size_mb,
300
- include_hidden=include_hidden,
301
- use_gitignore=use_gitignore,
302
- )
303
- except Exception as e:
304
- return f"❌ خطا: {str(e)}", "", {}
305
-
306
- if not files:
307
- return "⚠️ هیچ فایل متنی قابل خواندنی در مسیر مشخص شده یافت نشد.", "", {}
308
-
309
- # Sort files by category then path
310
- files.sort(key=lambda x: (x["category"], x["path"]))
311
-
312
- # Build context
313
- context_parts = []
314
- context_parts.append(f"# Project Context: {root_name}")
315
- context_parts.append(f"# Generated: {datetime.now().isoformat()}")
316
- context_parts.append(f"# Total files: {len(files)}")
317
- context_parts.append("")
318
-
319
- # Add tree structure
320
- if include_tree:
321
- tree = build_tree_structure(files)
322
- context_parts.append("## Directory Structure")
323
- context_parts.append("```")
324
- context_parts.append(tree)
325
- context_parts.append("```")
326
- context_parts.append("")
327
-
328
- total_est_tokens = 0
329
- file_chunks = []
330
-
331
- for file_info in files:
332
- chunks = chunk_file_content(
333
- file_info,
334
- chunk_size_tokens=chunk_size_tokens,
335
- overlap_tokens=overlap_tokens,
336
- )
337
-
338
- for i, chunk in enumerate(chunks):
339
- # Estimate tokens in this chunk
340
- chunk_tokens = estimate_tokens(chunk)
341
-
342
- # Check if adding this chunk would exceed max tokens
343
- if total_est_tokens + chunk_tokens > max_total_tokens:
344
- # Add truncation message and stop
345
- remaining = max_total_tokens - total_est_tokens
346
- if remaining > 0:
347
- # Truncate chunk to fit remaining tokens
348
- char_limit = int(remaining * CHARS_PER_TOKEN)
349
- truncated = chunk[:char_limit]
350
- header = f"### {file_info['path']} [{file_info['language']}] (chunk {i+1}/{len(chunks)}) — TRUNCATED\n\n"
351
- context_parts.append(header + truncated)
352
- context_parts.append("\n\n⚠️ **Context truncated due to token limit**")
353
- context_parts.append(f"Limit: {max_total_tokens:,} tokens")
354
- break
355
-
356
- # Add file header
357
- chunk_header = f"### {file_info['path']} [{file_info['language']}] (chunk {i+1}/{len(chunks)})\n\n"
358
- context_parts.append(chunk_header + chunk)
359
- total_est_tokens += chunk_tokens + estimate_tokens(chunk_header)
360
-
361
- else:
362
- # If no break, continue to next file
363
- continue
364
- # If break was hit, exit outer loop too
365
- break
366
-
367
- context_text = "\n\n".join(context_parts)
368
-
369
- # Generate hash
370
- if include_hash:
371
- context_hash = hashlib.md5(context_text.encode()).hexdigest()
372
- else:
373
- context_hash = ""
374
-
375
- # Stats
376
- stats = {
377
- "files": len(files),
378
- "total_tokens": total_est_tokens,
379
- "categories": category_counts,
380
- "chunk_size": chunk_size_tokens,
381
- "overlap": overlap_tokens,
382
- "max_tokens": max_total_tokens,
383
- "truncated": total_est_tokens >= max_total_tokens,
384
- }
385
-
386
- # Cleanup temp directory if needed
387
- if is_temp and root_path:
388
- cleanup_temp_dir(root_path.parent if root_path.is_file() else root_path)
389
-
390
- return context_text, context_hash, stats
391
-
392
-
393
- def build_tree_structure(files: list) -> str:
394
- """Build a simple tree structure from file paths."""
395
- tree = {}
396
- for file_info in files:
397
- parts = Path(file_info["path"]).parts
398
- current = tree
399
- for part in parts[:-1]:
400
- current = current.setdefault(part, {})
401
- current[parts[-1]] = None
402
-
403
- def render_tree(node, prefix=""):
404
- lines = []
405
- items = sorted(node.items())
406
- for i, (key, value) in enumerate(items):
407
- is_last = (i == len(items) - 1)
408
- if value is None:
409
- lines.append(f"{prefix}{'└── ' if is_last else '├── '}{key}")
410
- else:
411
- lines.append(f"{prefix}{'└── ' if is_last else '├── '}{key}/")
412
- lines.extend(render_tree(
413
- value,
414
- prefix + (" " if is_last else "│ ")
415
- ))
416
- return lines
417
-
418
- return "\n".join(render_tree(tree)) if tree else "(empty)"
419
-
420
-
421
- # ---------------------------------------------------------------------------
422
- # Gradio UI (تغییر داده شده)
423
- # ---------------------------------------------------------------------------
424
-
425
- def create_ui():
426
- with gr.Blocks(title="Context LLM Builder") as demo:
427
- gr.Markdown("""
428
- # 📚 Context LLM Builder
429
-
430
- این ابزار فایل‌های یک پروژه را اسکن کرده و آن‌ها را به‌صورت یک متن یکپارچه
431
- برای ارسال به مدل‌های زبانی بزرگ (LLM) آماده می‌کند.
432
-
433
- **✅ پشتیبانی از ورودی‌ها:**
434
- - **پوشه** (کل پروژه)
435
- - **فایل ZIP** (استخراج خودکار در حافظه)
436
- - **فایل تکی** (مانند یک فایل پایتون)
437
- """)
438
-
439
- with gr.Row():
440
- with gr.Column(scale=1):
441
- input_path = gr.Textbox(
442
- label="مسیر پوشه / فایل ZIP / فایل تکی",
443
- placeholder="/path/to/your/project یا /path/to/file.zip",
444
- value=".",
445
- )
446
-
447
- with gr.Accordion("⚙️ تنظیمات پیشرفته", open=False):
448
- ignore_dirs = gr.Textbox(
449
- label="پوشه‌های نادیده گرفته شده (با کاما جدا کنید)",
450
- value=IGNORE_DIRS_DEFAULT,
451
- )
452
- max_file_size_mb = gr.Slider(
453
- label="حداکثر حجم فایل (MB)",
454
- minimum=1,
455
- maximum=50,
456
- value=5,
457
- step=1,
458
- )
459
- chunk_size_tokens = gr.Slider(
460
- label="حجم هر تکه (توکن)",
461
- minimum=500,
462
- maximum=16000,
463
- value=4000,
464
- step=100,
465
- )
466
- overlap_tokens = gr.Slider(
467
- label="همپوشانی بین تکه‌ها (توکن)",
468
- minimum=0,
469
- maximum=1000,
470
- value=200,
471
- step=50,
472
- )
473
- max_total_tokens = gr.Slider(
474
- label="حداکثر توکن کل خروجی",
475
- minimum=10000,
476
- maximum=200000,
477
- value=100000,
478
- step=1000,
479
- )
480
- include_hidden = gr.Checkbox(
481
- label="شامل فایل‌های پنهان (.bashrc و ...)",
482
- value=False,
483
- )
484
- include_hash = gr.Checkbox(
485
- label="شامل هش (MD5) متن خروجی",
486
- value=True,
487
- )
488
- include_tree = gr.Checkbox(
489
- label="شامل ساختار دایرکتوری",
490
- value=True,
491
- )
492
-
493
- build_btn = gr.Button("🚀 ساخت کانتکست", variant="primary", size="lg")
494
-
495
- with gr.Column(scale=2):
496
- status = gr.Markdown("⬅️ تنظیمات را وارد کرده و دکمه را بزنید")
497
- with gr.Row():
498
- with gr.Column(scale=3):
499
- output_text = gr.Textbox(
500
- label="📄 متن کانتکست",
501
- lines=30,
502
- max_lines=50,
503
- interactive=False,
504
- )
505
- with gr.Column(scale=1):
506
- hash_text = gr.Textbox(
507
- label="🔑 هش (MD5)",
508
- lines=1,
509
- interactive=False,
510
- max_lines=1,
511
- )
512
- stats_json = gr.JSON(
513
- label="📊 آمار",
514
- value={},
515
- )
516
-
517
- with gr.Row():
518
- copy_btn = gr.Button("📋 کپی متن", variant="secondary", size="sm")
519
- download_btn = gr.DownloadButton("💾 دانلود فایل", variant="secondary", size="sm")
520
-
521
- # Events
522
- build_btn.click(
523
- fn=build_context,
524
- inputs=[
525
- input_path,
526
- ignore_dirs,
527
- max_file_size_mb,
528
- chunk_size_tokens,
529
- overlap_tokens,
530
- max_total_tokens,
531
- include_hidden,
532
- include_hash,
533
- include_tree,
534
- ],
535
- outputs=[output_text, hash_text, stats_json],
536
- )
537
-
538
- # Copy button - اصلاح شده
539
- copy_btn.click(
540
- fn=lambda x: x,
541
- inputs=[output_text],
542
- outputs=[],
543
- js="(text) => { navigator.clipboard.writeText(text); return text; }",
544
- )
545
-
546
- # Download button - اصلاح شده با روش ساده‌تر
547
- def download_file(text):
548
- if not text:
549
- return None
550
- temp = tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False)
551
- temp.write(text)
552
- temp.close()
553
- return temp.name
554
-
555
- download_btn.click(
556
- fn=download_file,
557
- inputs=[output_text],
558
- outputs=[gr.File(label="📥 دانلود فایل")],
559
- )
560
-
561
- return demo
562
-
563
- # ---------------------------------------------------------------------------
564
- # Main
565
- # ---------------------------------------------------------------------------
566
-
567
- if __name__ == "__main__":
568
- try:
569
- demo = create_ui()
570
- demo.launch(
571
- server_name="0.0.0.0",
572
- server_port=7860,
573
- share=False,
574
- theme=gr.themes.Soft()
575
- )
576
- except Exception as e:
577
- print(f"❌ خطا در اجرا: {e}")
578
- # در Hugging Face، برنامه نباید متوقف شود
579
- import time
580
- time.sleep(3600) # 1 ساعت صبر کن تا لاگ‌ها قابل مشاهده باشند