Delete app.py
Browse files
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 ساعت صبر کن تا لاگها قابل مشاهده باشند
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|