🎨 Redesign from AnyCoder

#1
Files changed (1) hide show
  1. app.py +257 -186
app.py CHANGED
@@ -1,14 +1,6 @@
1
  """
2
- Docling Document Processor - Aplicação Principal.
3
-
4
- Este é o ponto de entrada da aplicação Gradio que permite
5
- o upload e processamento de documentos usando Docling.
6
-
7
- Recursos:
8
- - Upload múltiplo (1-5 arquivos)
9
- - Formatos: PDF, DOC, DOCX
10
- - Saída: JSON, Markdown ou ambos (ZIP)
11
- - Aceleração GPU via ZeroGPU
12
  """
13
 
14
  import os
@@ -52,34 +44,21 @@ logger = setup_logger("docling_space")
52
  # RATE LIMITING (in-memory)
53
  # =============================================================================
54
 
55
- # Armazena requisições por IP: {ip: [timestamps]}
56
  _rate_limit_store: dict[str, list[datetime]] = defaultdict(list)
57
 
58
 
59
  def check_rate_limit(request: gr.Request) -> bool:
60
- """
61
- Verifica se o IP excedeu o limite de requisições.
62
-
63
- Args:
64
- request: Objeto de request do Gradio.
65
-
66
- Returns:
67
- True se está dentro do limite, False se excedeu.
68
- """
69
  if request is None:
70
  return True
71
 
72
- # Obtém IP do cliente de múltiplas fontes
73
  ip = None
74
-
75
- # Tenta headers primeiro (X-Forwarded-For para proxies/containers)
76
  if hasattr(request, "headers"):
77
  headers = request.headers or {}
78
  ip = headers.get("x-forwarded-for", "").split(",")[0].strip()
79
  if not ip:
80
  ip = headers.get("x-real-ip", "").strip()
81
 
82
- # Fallback para client info
83
  if not ip:
84
  client_info = getattr(request, "client", None)
85
  if client_info:
@@ -90,30 +69,25 @@ def check_rate_limit(request: gr.Request) -> bool:
90
  else:
91
  ip = str(client_info)
92
 
93
- # Se ainda não tem IP, usa session_hash como identificador alternativo
94
  if not ip or ip == "unknown":
95
  session_hash = getattr(request, "session_hash", None)
96
  if session_hash:
97
  ip = f"session_{session_hash[:16]}"
98
  else:
99
- # Último fallback: permite a requisição mas não rastreia
100
  return True
101
 
102
  now = datetime.now()
103
  window_start = now - timedelta(hours=config.RATE_LIMIT_WINDOW_HOURS)
104
 
105
- # Limpa requisições antigas
106
  _rate_limit_store[ip] = [
107
  ts for ts in _rate_limit_store[ip]
108
  if ts > window_start
109
  ]
110
 
111
- # Verifica limite
112
  if len(_rate_limit_store[ip]) >= config.RATE_LIMIT_REQUESTS:
113
  logger.warning(f"Rate limit excedido para IP: {ip}")
114
  return False
115
 
116
- # Registra nova requisição
117
  _rate_limit_store[ip].append(now)
118
  return True
119
 
@@ -127,25 +101,12 @@ def _process_documents_internal(
127
  output_format: str,
128
  progress: Optional[gr.Progress] = None
129
  ) -> tuple[str | list[str], str]:
130
- """
131
- Função interna de processamento (sem decorator GPU).
132
-
133
- Args:
134
- files: Lista de arquivos enviados.
135
- output_format: Formato de saída ("JSON", "Markdown", "Ambos").
136
- progress: Objeto de progresso do Gradio.
137
-
138
- Returns:
139
- Tupla (caminho(s) do arquivo de saída, mensagem de status).
140
- """
141
  start_time = time.time()
142
-
143
- # Limpa arquivos temporários antigos
144
  cleanup_old_files()
145
 
146
- # Valida arquivos
147
  if progress:
148
- progress(0.1, desc="Validando arquivos...")
149
 
150
  try:
151
  validated_files = validate_files(files)
@@ -153,9 +114,8 @@ def _process_documents_internal(
153
  logger.warning(f"Erro de validação: {e.message}")
154
  raise gr.Error(e.message)
155
 
156
- # Prepara processador
157
  if progress:
158
- progress(0.2, desc="Inicializando Docling...")
159
 
160
  processor = DoclingProcessor(
161
  enable_ocr=True,
@@ -163,27 +123,21 @@ def _process_documents_internal(
163
  use_gpu=HAS_SPACES
164
  )
165
 
166
- # Cria diretório de saída
167
  output_dir = create_temp_directory(prefix="output_")
168
  output_files = []
169
  processed_count = 0
170
  total_files = len(validated_files)
171
 
172
- # Processa cada arquivo
173
  for i, (file_path, sanitized_name) in enumerate(validated_files):
174
  progress_pct = 0.2 + (0.6 * (i / total_files))
175
 
176
  if progress:
177
- progress(progress_pct, desc=f"Processando {sanitized_name}...")
178
 
179
  try:
180
- # Processa documento
181
  processed_data = processor.process_document(file_path)
182
-
183
- # Gera nome base sem extensão
184
  base_name = Path(sanitized_name).stem
185
 
186
- # Formata saída
187
  if output_format == "JSON":
188
  json_content = format_to_json(processed_data, sanitized_name)
189
  json_path = save_output_file(
@@ -227,20 +181,17 @@ def _process_documents_internal(
227
  logger.error(f"Erro ao processar {sanitized_name}: {e}")
228
  logger.debug(traceback.format_exc())
229
 
230
- # Continua com próximos arquivos
231
  if total_files == 1:
232
  raise gr.Error(
233
  f"❌ Erro ao processar {sanitized_name}: {str(e)}"
234
  )
235
 
236
- # Prepara saída final
237
  if progress:
238
- progress(0.9, desc="Preparando download...")
239
 
240
  if not output_files:
241
  raise gr.Error("❌ Nenhum arquivo foi processado com sucesso.")
242
 
243
- # Se há múltiplos arquivos ou formato "Ambos", cria ZIP
244
  if len(output_files) > 1 or output_format == "Ambos":
245
  zip_path = create_zip_output(
246
  output_files,
@@ -250,18 +201,16 @@ def _process_documents_internal(
250
  else:
251
  final_output = str(output_files[0][0])
252
 
253
- # Calcula tempo total
254
  elapsed_time = time.time() - start_time
255
 
256
  if progress:
257
- progress(1.0, desc="Concluído!")
258
 
259
- # Mensagem de status
260
  status_msg = (
261
- f"✅ Processamento concluído!\n\n"
262
- f"📄 **Arquivos processados:** {processed_count}/{total_files}\n"
263
- f"📦 **Formato:** {output_format}\n"
264
- f"⏱️ **Tempo:** {elapsed_time:.1f} segundos"
265
  )
266
 
267
  logger.info(
@@ -292,30 +241,15 @@ def process_documents(
292
  request: gr.Request,
293
  progress: gr.Progress = gr.Progress()
294
  ) -> tuple[str | list[str], str]:
295
- """
296
- Função principal de processamento.
297
-
298
- Usa GPU se disponível, senão fallback para CPU.
299
-
300
- Args:
301
- files: Lista de arquivos enviados.
302
- output_format: Formato de saída.
303
- request: Request do Gradio (para rate limiting).
304
- progress: Objeto de progresso.
305
-
306
- Returns:
307
- Tupla (caminho do arquivo de saída, mensagem de status).
308
- """
309
- # Verifica rate limit
310
  if not check_rate_limit(request):
311
  raise gr.Error(
312
- f"⚠️ Limite de requisições excedido. "
313
- f"Máximo: {config.RATE_LIMIT_REQUESTS} por hora. "
314
- f"Tente novamente mais tarde."
315
  )
316
 
317
  try:
318
- # Tenta usar GPU
319
  if HAS_SPACES and process_documents_gpu is not None:
320
  logger.info("Usando processamento GPU (ZeroGPU)")
321
  return process_documents_gpu(files, output_format, progress)
@@ -324,154 +258,140 @@ def process_documents(
324
  return _process_documents_internal(files, output_format, progress)
325
 
326
  except gr.Error:
327
- # Re-raise erros do Gradio
328
  raise
329
  except TimeoutError:
330
  logger.error("Timeout no processamento")
331
  raise gr.Error(
332
- "⏱️ Tempo limite excedido. Tente com arquivos menores ou menos arquivos."
333
  )
334
  except MemoryError:
335
  logger.error("Memória insuficiente")
336
  raise gr.Error(
337
- "💾 Memória insuficiente. Tente com arquivos menores."
338
  )
339
  except Exception as e:
340
  logger.error(f"Erro inesperado: {e}")
341
  logger.debug(traceback.format_exc())
342
- raise gr.Error(f"❌ Erro inesperado: {str(e)}")
343
 
344
 
345
  # =============================================================================
346
- # INTERFACE GRADIO
347
  # =============================================================================
348
 
349
- # CSS customizado
350
- CUSTOM_CSS = """
351
- .main-container {
352
- max-width: 900px;
353
- margin: 0 auto;
354
- }
355
-
356
- .upload-box {
357
- border: 2px dashed #4a90a4;
358
- border-radius: 12px;
359
- padding: 20px;
360
- background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
361
- }
362
-
363
- .status-box {
364
- background: #f0f7f4;
365
- border-radius: 8px;
366
- padding: 15px;
367
- margin-top: 10px;
368
- }
369
-
370
- .info-text {
371
- font-size: 0.9em;
372
- color: #666;
373
- }
374
- """
375
-
376
- # Texto de descrição
377
- DESCRIPTION = """
378
- # 📄 Docling Document Processor
379
-
380
- Converta documentos PDF, DOC e DOCX em formatos estruturados usando IA.
381
-
382
- ## Recursos
383
- - 🔍 **Extração inteligente** de texto, tabelas e metadados
384
- - **Detecção automática** de idioma
385
- - 🚀 **Aceleração GPU** para processamento rápido
386
- - 📊 **Preserva estrutura** hierárquica do documento
387
- """
388
-
389
- INSTRUCTIONS = """
390
- ### Como usar
391
-
392
- 1. **Upload**: Arraste ou selecione seus arquivos (máx. 5 arquivos, 50MB cada)
393
- 2. **Formato**: Escolha o formato de saída desejado
394
- 3. **Processar**: Clique no botão e aguarde
395
- 4. **Download**: Baixe o resultado quando concluído
396
-
397
- ### Formatos suportados
398
- - **Entrada**: PDF, DOC, DOCX
399
- - **Saída**: JSON, Markdown ou ambos (ZIP)
400
- """
401
-
402
-
403
  def create_interface() -> gr.Blocks:
404
- """Cria e retorna a interface Gradio."""
405
 
406
  with gr.Blocks(
407
- title="Docling Document Processor",
408
- theme=gr.themes.Soft(
409
- primary_hue="teal",
410
- secondary_hue="blue",
411
- ),
412
- css=CUSTOM_CSS,
413
  ) as demo:
414
 
415
- # Header
416
- gr.Markdown(DESCRIPTION)
 
 
 
 
 
 
 
 
 
 
 
 
 
417
 
 
418
  with gr.Row():
419
- # Coluna principal
420
- with gr.Column(scale=2):
421
- # Upload de arquivos
422
  file_input = gr.File(
423
  file_count="multiple",
424
  file_types=[".pdf", ".doc", ".docx"],
425
- label="📁 Upload de Documentos",
426
- elem_classes=["upload-box"],
 
427
  )
428
-
429
- # Seletor de formato
430
  format_selector = gr.Radio(
431
  choices=config.OUTPUT_FORMATS,
432
  value="Markdown",
433
- label="📤 Formato de Saída",
434
- info="Escolha como deseja receber o documento processado",
 
435
  )
436
-
437
- # Botão de processar
438
  process_btn = gr.Button(
439
- "🚀 Processar Documentos",
440
  variant="primary",
441
  size="lg",
 
442
  )
443
-
444
- # Coluna de informações
445
- with gr.Column(scale=1):
446
- gr.Markdown(INSTRUCTIONS)
447
-
448
- # Área de resultados
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  with gr.Row():
450
- with gr.Column():
451
- # Status
452
  status_output = gr.Markdown(
453
  label="Status",
454
- elem_classes=["status-box"],
455
  )
456
-
457
- # Arquivo de saída
458
  file_output = gr.File(
459
- label="📥 Download do Resultado",
460
  interactive=False,
 
461
  )
462
 
463
- # Informações de limites
 
464
  gr.Markdown(
465
  f"""
466
- ---
467
- **Limites:** {config.MAX_FILES_PER_SESSION} arquivos por vez |
468
- {config.MAX_FILE_SIZE_MB}MB por arquivo |
469
- {config.RATE_LIMIT_REQUESTS} requisições/hora
 
 
 
470
  """,
471
- elem_classes=["info-text"],
472
  )
473
 
474
- # Evento de processamento
475
  process_btn.click(
476
  fn=process_documents,
477
  inputs=[file_input, format_selector],
@@ -479,7 +399,7 @@ def create_interface() -> gr.Blocks:
479
  show_progress="full",
480
  )
481
 
482
- # Limpa status quando novos arquivos são selecionados
483
  file_input.change(
484
  fn=lambda: ("", None),
485
  outputs=[status_output, file_output],
@@ -515,13 +435,160 @@ if __name__ == "__main__":
515
  server_port=7860,
516
  max_file_size=f"{config.MAX_FILE_SIZE_MB}mb",
517
  show_error=True,
518
- share=is_containerized, # Habilita link compartilhável em containers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519
  )
520
  except Exception as e:
521
  logger.error(f"Erro ao iniciar aplicação: {e}")
522
  logger.info("Tentando iniciar com configuração alternativa...")
523
 
524
- # Fallback: tenta com share=True
525
  try:
526
  demo.queue().launch(
527
  server_name="0.0.0.0",
@@ -529,7 +596,11 @@ if __name__ == "__main__":
529
  max_file_size=f"{config.MAX_FILE_SIZE_MB}mb",
530
  show_error=True,
531
  share=True,
 
 
 
 
532
  )
533
  except Exception as fallback_error:
534
  logger.critical(f"Falha crítica ao iniciar: {fallback_error}")
535
- raise
 
1
  """
2
+ Docling Document Processor - Modern Redesigned UI
3
+ A clean, mobile-first interface for document processing with AI.
 
 
 
 
 
 
 
 
4
  """
5
 
6
  import os
 
44
  # RATE LIMITING (in-memory)
45
  # =============================================================================
46
 
 
47
  _rate_limit_store: dict[str, list[datetime]] = defaultdict(list)
48
 
49
 
50
  def check_rate_limit(request: gr.Request) -> bool:
51
+ """Verifica se o IP excedeu o limite de requisições."""
 
 
 
 
 
 
 
 
52
  if request is None:
53
  return True
54
 
 
55
  ip = None
 
 
56
  if hasattr(request, "headers"):
57
  headers = request.headers or {}
58
  ip = headers.get("x-forwarded-for", "").split(",")[0].strip()
59
  if not ip:
60
  ip = headers.get("x-real-ip", "").strip()
61
 
 
62
  if not ip:
63
  client_info = getattr(request, "client", None)
64
  if client_info:
 
69
  else:
70
  ip = str(client_info)
71
 
 
72
  if not ip or ip == "unknown":
73
  session_hash = getattr(request, "session_hash", None)
74
  if session_hash:
75
  ip = f"session_{session_hash[:16]}"
76
  else:
 
77
  return True
78
 
79
  now = datetime.now()
80
  window_start = now - timedelta(hours=config.RATE_LIMIT_WINDOW_HOURS)
81
 
 
82
  _rate_limit_store[ip] = [
83
  ts for ts in _rate_limit_store[ip]
84
  if ts > window_start
85
  ]
86
 
 
87
  if len(_rate_limit_store[ip]) >= config.RATE_LIMIT_REQUESTS:
88
  logger.warning(f"Rate limit excedido para IP: {ip}")
89
  return False
90
 
 
91
  _rate_limit_store[ip].append(now)
92
  return True
93
 
 
101
  output_format: str,
102
  progress: Optional[gr.Progress] = None
103
  ) -> tuple[str | list[str], str]:
104
+ """Função interna de processamento (sem decorator GPU)."""
 
 
 
 
 
 
 
 
 
 
105
  start_time = time.time()
 
 
106
  cleanup_old_files()
107
 
 
108
  if progress:
109
+ progress(0.1, desc="🔍 Validating files...")
110
 
111
  try:
112
  validated_files = validate_files(files)
 
114
  logger.warning(f"Erro de validação: {e.message}")
115
  raise gr.Error(e.message)
116
 
 
117
  if progress:
118
+ progress(0.2, desc=" Initializing Docling...")
119
 
120
  processor = DoclingProcessor(
121
  enable_ocr=True,
 
123
  use_gpu=HAS_SPACES
124
  )
125
 
 
126
  output_dir = create_temp_directory(prefix="output_")
127
  output_files = []
128
  processed_count = 0
129
  total_files = len(validated_files)
130
 
 
131
  for i, (file_path, sanitized_name) in enumerate(validated_files):
132
  progress_pct = 0.2 + (0.6 * (i / total_files))
133
 
134
  if progress:
135
+ progress(progress_pct, desc=f"📄 Processing {sanitized_name}...")
136
 
137
  try:
 
138
  processed_data = processor.process_document(file_path)
 
 
139
  base_name = Path(sanitized_name).stem
140
 
 
141
  if output_format == "JSON":
142
  json_content = format_to_json(processed_data, sanitized_name)
143
  json_path = save_output_file(
 
181
  logger.error(f"Erro ao processar {sanitized_name}: {e}")
182
  logger.debug(traceback.format_exc())
183
 
 
184
  if total_files == 1:
185
  raise gr.Error(
186
  f"❌ Erro ao processar {sanitized_name}: {str(e)}"
187
  )
188
 
 
189
  if progress:
190
+ progress(0.9, desc="📦 Preparing download...")
191
 
192
  if not output_files:
193
  raise gr.Error("❌ Nenhum arquivo foi processado com sucesso.")
194
 
 
195
  if len(output_files) > 1 or output_format == "Ambos":
196
  zip_path = create_zip_output(
197
  output_files,
 
201
  else:
202
  final_output = str(output_files[0][0])
203
 
 
204
  elapsed_time = time.time() - start_time
205
 
206
  if progress:
207
+ progress(1.0, desc="✅ Complete!")
208
 
 
209
  status_msg = (
210
+ f"### Processing Complete!\n\n"
211
+ f"**Files processed:** {processed_count}/{total_files} \n"
212
+ f"**Format:** {output_format} \n"
213
+ f"**Time:** {elapsed_time:.1f}s"
214
  )
215
 
216
  logger.info(
 
241
  request: gr.Request,
242
  progress: gr.Progress = gr.Progress()
243
  ) -> tuple[str | list[str], str]:
244
+ """Função principal de processamento."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  if not check_rate_limit(request):
246
  raise gr.Error(
247
+ f"⚠️ Rate limit exceeded. "
248
+ f"Maximum: {config.RATE_LIMIT_REQUESTS} requests per hour. "
249
+ f"Please try again later."
250
  )
251
 
252
  try:
 
253
  if HAS_SPACES and process_documents_gpu is not None:
254
  logger.info("Usando processamento GPU (ZeroGPU)")
255
  return process_documents_gpu(files, output_format, progress)
 
258
  return _process_documents_internal(files, output_format, progress)
259
 
260
  except gr.Error:
 
261
  raise
262
  except TimeoutError:
263
  logger.error("Timeout no processamento")
264
  raise gr.Error(
265
+ "⏱️ Time limit exceeded. Try with smaller or fewer files."
266
  )
267
  except MemoryError:
268
  logger.error("Memória insuficiente")
269
  raise gr.Error(
270
+ "💾 Insufficient memory. Try with smaller files."
271
  )
272
  except Exception as e:
273
  logger.error(f"Erro inesperado: {e}")
274
  logger.debug(traceback.format_exc())
275
+ raise gr.Error(f"❌ Unexpected error: {str(e)}")
276
 
277
 
278
  # =============================================================================
279
+ # INTERFACE GRADIO - MODERN REDESIGN
280
  # =============================================================================
281
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  def create_interface() -> gr.Blocks:
283
+ """Creates a modern, mobile-first Gradio interface."""
284
 
285
  with gr.Blocks(
286
+ title="📄 Docling Processor",
287
+ fill_height=True,
 
 
 
 
288
  ) as demo:
289
 
290
+ # Header Section
291
+ with gr.Row():
292
+ with gr.Column(scale=1):
293
+ gr.Markdown(
294
+ """
295
+ # 📄 Docling Document Processor
296
+
297
+ Transform PDF, DOC, and DOCX files into structured formats using AI.
298
+
299
+ Built with [anycoder](https://huggingface.co/spaces/akhaliq/anycoder)
300
+ """,
301
+ elem_classes=["header-text"]
302
+ )
303
+
304
+ gr.Markdown("---")
305
 
306
+ # Main Content Area
307
  with gr.Row():
308
+ with gr.Column(scale=1):
309
+
310
+ # Upload Section
311
  file_input = gr.File(
312
  file_count="multiple",
313
  file_types=[".pdf", ".doc", ".docx"],
314
+ label="📁 Upload Documents",
315
+ height=200,
316
+ elem_classes=["upload-area"]
317
  )
318
+
319
+ # Format Selector
320
  format_selector = gr.Radio(
321
  choices=config.OUTPUT_FORMATS,
322
  value="Markdown",
323
+ label="📤 Output Format",
324
+ info="Choose your preferred output format",
325
+ elem_classes=["format-selector"]
326
  )
327
+
328
+ # Process Button
329
  process_btn = gr.Button(
330
+ "🚀 Process Documents",
331
  variant="primary",
332
  size="lg",
333
+ elem_classes=["process-button"]
334
  )
335
+
336
+ # Info Box
337
+ with gr.Accordion("ℹ️ How to Use", open=False):
338
+ gr.Markdown(
339
+ """
340
+ ### Quick Start
341
+
342
+ 1. **Upload** your documents (max 5 files, 50MB each)
343
+ 2. **Select** output format (JSON, Markdown, or both)
344
+ 3. **Click** Process Documents
345
+ 4. **Download** your results
346
+
347
+ ### Features
348
+
349
+ - 🔍 Smart text, table & metadata extraction
350
+ - 🌐 Automatic language detection
351
+ - 🚀 GPU acceleration for fast processing
352
+ - 📊 Preserves document structure
353
+
354
+ ### Supported Formats
355
+
356
+ **Input:** PDF, DOC, DOCX
357
+ **Output:** JSON, Markdown, or ZIP (both)
358
+ """
359
+ )
360
+
361
+ # Results Section
362
+ gr.Markdown("---")
363
+
364
  with gr.Row():
365
+ with gr.Column(scale=1):
366
+ # Status Output
367
  status_output = gr.Markdown(
368
  label="Status",
369
+ elem_classes=["status-output"]
370
  )
371
+
372
+ # File Download
373
  file_output = gr.File(
374
+ label="📥 Download Results",
375
  interactive=False,
376
+ elem_classes=["download-area"]
377
  )
378
 
379
+ # Footer
380
+ gr.Markdown("---")
381
  gr.Markdown(
382
  f"""
383
+ <div style="text-align: center; color: #666; font-size: 0.9em;">
384
+ <p><strong>Limits:</strong> {config.MAX_FILES_PER_SESSION} files per upload |
385
+ {config.MAX_FILE_SIZE_MB}MB per file |
386
+ {config.RATE_LIMIT_REQUESTS} requests/hour</p>
387
+ <p>Powered by <a href="https://github.com/docling-project/docling">Docling</a> •
388
+ Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder">anycoder</a></p>
389
+ </div>
390
  """,
391
+ elem_classes=["footer-text"]
392
  )
393
 
394
+ # Event Handlers
395
  process_btn.click(
396
  fn=process_documents,
397
  inputs=[file_input, format_selector],
 
399
  show_progress="full",
400
  )
401
 
402
+ # Clear status when new files are selected
403
  file_input.change(
404
  fn=lambda: ("", None),
405
  outputs=[status_output, file_output],
 
435
  server_port=7860,
436
  max_file_size=f"{config.MAX_FILE_SIZE_MB}mb",
437
  show_error=True,
438
+ share=is_containerized,
439
+ theme=gr.themes.Soft(
440
+ primary_hue="blue",
441
+ secondary_hue="indigo",
442
+ neutral_hue="slate",
443
+ font=gr.themes.GoogleFont("Inter"),
444
+ text_size="lg",
445
+ spacing_size="lg",
446
+ radius_size="md"
447
+ ).set(
448
+ button_primary_background_fill="*primary_600",
449
+ button_primary_background_fill_hover="*primary_700",
450
+ button_primary_text_color="white",
451
+ block_title_text_weight="600",
452
+ block_label_text_weight="500",
453
+ ),
454
+ css="""
455
+ /* Mobile-First Responsive Design */
456
+ .gradio-container {
457
+ max-width: 1200px !important;
458
+ margin: 0 auto !important;
459
+ padding: 1rem !important;
460
+ }
461
+
462
+ /* Header Styling */
463
+ .header-text h1 {
464
+ font-size: 2rem !important;
465
+ font-weight: 700 !important;
466
+ margin-bottom: 0.5rem !important;
467
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
468
+ -webkit-background-clip: text;
469
+ -webkit-text-fill-color: transparent;
470
+ }
471
+
472
+ .header-text p {
473
+ font-size: 1.1rem !important;
474
+ color: #64748b !important;
475
+ line-height: 1.6 !important;
476
+ }
477
+
478
+ /* Upload Area */
479
+ .upload-area {
480
+ border: 2px dashed #cbd5e1 !important;
481
+ border-radius: 12px !important;
482
+ transition: all 0.3s ease !important;
483
+ }
484
+
485
+ .upload-area:hover {
486
+ border-color: #667eea !important;
487
+ background: #f8fafc !important;
488
+ }
489
+
490
+ /* Format Selector */
491
+ .format-selector label {
492
+ font-weight: 500 !important;
493
+ margin-bottom: 0.5rem !important;
494
+ }
495
+
496
+ /* Process Button */
497
+ .process-button {
498
+ margin-top: 1rem !important;
499
+ font-size: 1.1rem !important;
500
+ padding: 0.75rem 2rem !important;
501
+ border-radius: 8px !important;
502
+ font-weight: 600 !important;
503
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1) !important;
504
+ transition: all 0.3s ease !important;
505
+ }
506
+
507
+ .process-button:hover {
508
+ transform: translateY(-2px) !important;
509
+ box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.2) !important;
510
+ }
511
+
512
+ /* Status Output */
513
+ .status-output {
514
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%) !important;
515
+ border-left: 4px solid #0ea5e9 !important;
516
+ padding: 1rem !important;
517
+ border-radius: 8px !important;
518
+ margin-top: 1rem !important;
519
+ }
520
+
521
+ /* Download Area */
522
+ .download-area {
523
+ margin-top: 1rem !important;
524
+ border-radius: 8px !important;
525
+ }
526
+
527
+ /* Footer */
528
+ .footer-text {
529
+ opacity: 0.8 !important;
530
+ }
531
+
532
+ .footer-text a {
533
+ color: #667eea !important;
534
+ text-decoration: none !important;
535
+ font-weight: 500 !important;
536
+ }
537
+
538
+ .footer-text a:hover {
539
+ text-decoration: underline !important;
540
+ }
541
+
542
+ /* Accordion Styling */
543
+ .accordion {
544
+ margin-top: 1rem !important;
545
+ }
546
+
547
+ /* Mobile Responsiveness */
548
+ @media (max-width: 768px) {
549
+ .gradio-container {
550
+ padding: 0.5rem !important;
551
+ }
552
+
553
+ .header-text h1 {
554
+ font-size: 1.5rem !important;
555
+ }
556
+
557
+ .header-text p {
558
+ font-size: 1rem !important;
559
+ }
560
+
561
+ .process-button {
562
+ width: 100% !important;
563
+ font-size: 1rem !important;
564
+ }
565
+ }
566
+
567
+ /* Dark Mode Support */
568
+ @media (prefers-color-scheme: dark) {
569
+ .upload-area {
570
+ border-color: #475569 !important;
571
+ }
572
+
573
+ .upload-area:hover {
574
+ background: #1e293b !important;
575
+ }
576
+
577
+ .status-output {
578
+ background: linear-gradient(135deg, #1e293b 0%, #334155 100%) !important;
579
+ }
580
+ }
581
+ """,
582
+ footer_links=[
583
+ {"label": "Built with anycoder", "url": "https://huggingface.co/spaces/akhaliq/anycoder"},
584
+ "gradio",
585
+ "api"
586
+ ]
587
  )
588
  except Exception as e:
589
  logger.error(f"Erro ao iniciar aplicação: {e}")
590
  logger.info("Tentando iniciar com configuração alternativa...")
591
 
 
592
  try:
593
  demo.queue().launch(
594
  server_name="0.0.0.0",
 
596
  max_file_size=f"{config.MAX_FILE_SIZE_MB}mb",
597
  show_error=True,
598
  share=True,
599
+ theme=gr.themes.Soft(primary_hue="blue"),
600
+ footer_links=[
601
+ {"label": "Built with anycoder", "url": "https://huggingface.co/spaces/akhaliq/anycoder"}
602
+ ]
603
  )
604
  except Exception as fallback_error:
605
  logger.critical(f"Falha crítica ao iniciar: {fallback_error}")
606
+ raise