habulaj commited on
Commit
d578cba
·
verified ·
1 Parent(s): d69d8a0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1 -1161
app.py CHANGED
@@ -437,269 +437,6 @@ def cut_video(input_path: str, output_path: str, start: str, end: str):
437
  return False
438
 
439
 
440
- class NewsExtractionRequest(BaseModel):
441
- video_url: str
442
- context: Optional[str] = None
443
- model: Optional[str] = "flash"
444
-
445
-
446
- @app.post("/news-extraction")
447
- async def news_extraction_endpoint(request: NewsExtractionRequest):
448
- """
449
- Analisa um vídeo e extrai títulos de notícias com timestamps.
450
- """
451
- if not chatbots:
452
- raise HTTPException(status_code=500, detail="Chatbot não inicializado")
453
-
454
- temp_file = None
455
-
456
- try:
457
- # 1. Validar e Baixar Vídeo
458
- if not request.video_url:
459
- raise HTTPException(status_code=400, detail="URL do vídeo é obrigatória")
460
-
461
- print(f"📥 [NewsExtraction] Baixando vídeo: {request.video_url}")
462
-
463
- # Baixar direto para um arquivo temporário
464
- response = download_file_with_retry(request.video_url, timeout=600) # Timeout maior para vídeos
465
-
466
- # Salvar em arquivo temporário com extensão correta
467
- content_type = response.headers.get('content-type', '').lower()
468
- ext = '.mp4'
469
- if 'webm' in content_type: ext = '.webm'
470
- elif 'mkv' in content_type: ext = '.mkv'
471
-
472
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
473
- for chunk in response.iter_content(chunk_size=1024*1024): # 1MB chunks
474
- if chunk:
475
- temp_file.write(chunk)
476
- temp_file.close()
477
-
478
- print(f"✅ Vídeo salvo temporariamente em: {temp_file.name} ({os.path.getsize(temp_file.name) / 1024 / 1024:.2f} MB)")
479
-
480
- # 2. Preparar Prompt
481
- contexto_add = f"\n\nCONTEXTO EM TEXTO: {request.context}" if request.context else ""
482
-
483
- prompt = f"""Analise essa notícia e o vídeo:
484
- {contexto_add}
485
-
486
- Com base nela, gere titulos de notícias que podem ser extraidos do vídeo/notícia. Títulos de notícias.
487
-
488
- Exemplos: [
489
- {{
490
- "title": "Presidente Trump repreende repórter da CNN por não sorrir",
491
- "start": "04:10",
492
- "end": "05:30",
493
- "description": "Neste trecho do vídeo, a câmera mostra uma coletiva de imprensa na Casa Branca. O presidente Trump interrompe a pergunta de um repórter da CNN e faz um comentário direto sobre sua expressão facial, dizendo que ele deveria sorrir mais. O clima fica visivelmente tenso, com murmúrios entre outros jornalistas presentes. A cena alterna entre closes no rosto do repórter, que aparenta surpresa e constrangimento, e planos médios do presidente falando de forma assertiva, usando gestos amplos com as mãos. O momento é amplamente repercutido nas redes sociais por levantar debates sobre postura presidencial e relação com a imprensa."
494
- }},
495
- {{
496
- "title": "Duas semanas de fortes nevascas causam grandes transtornos no Japão",
497
- "start": "00:45",
498
- "end": "02:00",
499
- "description": "O vídeo apresenta imagens aéreas de cidades japonesas cobertas por neve espessa, com ruas quase irreconhecíveis. Veículos abandonados nas estradas, trens parados e pessoas caminhando com dificuldade são mostrados em sequência. Repórteres locais explicam que o país enfrenta duas semanas consecutivas de nevascas intensas, algo incomum em algumas regiões. Há entrevistas com moradores relatando falta de energia, escassez de suprimentos e dificuldades para chegar ao trabalho. O som ambiente mistura vento forte com o ruído de máquinas retirando neve."
500
- }},
501
- {{
502
- "title": "Novos arquivos sobre Jeffrey Epstein lançam luz sobre vínculos com homens influentes",
503
- "start": "07:20",
504
- "end": "08:50",
505
- "description": "Neste segmento, o vídeo assume um tom investigativo. Documentos aparecem na tela com trechos destacados, enquanto a narração explica que novos arquivos judiciais foram divulgados. Fotografias antigas, registros de voos e agendas são exibidos para contextualizar as conexões de Jeffrey Epstein com figuras poderosas do mundo político e financeiro. Especialistas comentam em off sobre a importância dessas revelações e como elas podem impactar investigações em andamento. A edição é mais lenta e séria, reforçando o peso das informações apresentadas."
506
- }},
507
- {{
508
- "title": "Kennedy Center fechará em julho para reforma de dois anos, diz Trump",
509
- "start": "02:30",
510
- "end": "03:15",
511
- "description": "O vídeo corta para imagens externas do Kennedy Center, em Washington, com pessoas entrando e saindo do local. Em seguida, aparece um trecho de discurso do presidente Trump anunciando o fechamento do centro cultural para uma ampla reforma de dois anos. Gráficos animados ilustram o cronograma das obras e os investimentos previstos. A narração explica a importância histórica do espaço para as artes nos Estados Unidos e mostra reações de artistas e produtores culturais, alguns demonstrando preocupação com o impacto no setor."
512
- }},
513
- {{
514
- "title": "Como tecnologias de campo de batalha foram usadas em Minneapolis",
515
- "start": "05:50",
516
- "end": "07:00",
517
- "description": "Neste momento do vídeo, são mostradas cenas urbanas de Minneapolis durante operações policiais. Equipamentos como drones, veículos blindados e sistemas avançados de vigilância aparecem em ação. A narração explica que tecnologias originalmente desenvolvidas para uso militar foram adaptadas para o controle urbano e policiamento. Especialistas em segurança comentam os riscos e benefícios desse tipo de aplicação, enquanto imagens de arquivo mostram testes e demonstrações desses equipamentos. O tom é crítico e reflexivo, levantando questões éticas."
518
- }},
519
- {{
520
- "title": "Trump diz ter alcançado acordo preliminar com a OTAN sobre a Groenlândia",
521
- "start": "01:10",
522
- "end": "02:05",
523
- "description": "O vídeo exibe uma reunião diplomática, com bandeiras dos Estados Unidos e da OTAN ao fundo. Trump aparece falando a jornalistas sobre um suposto acordo preliminar envolvendo a Groenlândia. Mapas animados são usados para mostrar a localização estratégica da região no Ártico. Analistas políticos explicam o contexto geopolítico e os interesses envolvidos, enquanto imagens de arquivo mostram paisagens geladas da Groenlândia, reforçando a importância estratégica do território."
524
- }},
525
- {{
526
- "title": "Trump impõe tarifas a oito países europeus para forçar venda da Groenlândia",
527
- "start": "08:55",
528
- "end": "09:40",
529
- "description": "Neste trecho final, gráficos econômicos aparecem na tela para ilustrar o impacto das tarifas anunciadas por Trump contra oito países europeus. A narração explica que a medida teria como objetivo pressionar negociações relacionadas à Groenlândia. Economistas comentam as possíveis consequências para o comércio internacional, enquanto imagens de portos, fábricas e reuniões da União Europeia são exibidas. O clima do vídeo é de alerta, destacando possíveis tensões diplomáticas."
530
- }},
531
- {{
532
- "title": "Trump tem muitas opções para lidar com o Irã, afirma Casa Branca",
533
- "start": "03:40",
534
- "end": "04:05",
535
- "description": "O vídeo mostra a porta-voz da Casa Branca em um briefing diário, respondendo perguntas sobre o Irã. Ela afirma que o presidente tem diversas opções sobre a mesa, sem entrar em detalhes. Imagens de arquivo de instalações militares, reuniões de segurança nacional e mapas do Oriente Médio ajudam a contextualizar a fala. A trilha sonora é discreta e tensa, reforçando a sensação de incerteza e expectativa em torno das próximas decisões do governo americano."
536
- }}
537
- ]
538
-
539
- Mande apenas os títulos, em forma JSON. Sem texto adicional. Deve analisar todo o contexto do vídeo e verificar quando pode cortar (start e end) e virar uma notícia sólida pro Instagram. Não corte trechos muitos curtos, seja preciso. No description, explique o que acontece nesse trecho, pra que o editor verifique se pode virar notícia. O título deve ser sólido, evitar emojis, dois pontos, etc. O estilo dos títulos devem ser parecidos com o JSON. Seja extremamente preciso no timing (start e end). Considere que o jornal é imparcial na apresentação dos fatos, mas possui uma inclinação editorial claramente mais conservadora, republicana e de direita na entonação. Adapte os títulos e descrições de acordo com esse posicionamento, mantendo rigor factual e linguagem jornalística."""
540
-
541
- # 3. Enviar para o Gemini
542
- model_name = request.model or "flash"
543
- chatbot = chatbots.get(model_name, chatbots.get('flash', chatbots['default']))
544
-
545
- print(f"🧠 [NewsExtraction] Enviando para Gemini ({model_name})...")
546
-
547
- # O chatbot.ask suporta 'video' como caminho do arquivo
548
- response_gemini = await chatbot.ask(prompt, video=temp_file.name)
549
-
550
- if response_gemini.get("error"):
551
- raise HTTPException(status_code=500, detail=f"Erro no Gemini: {response_gemini.get('content')}")
552
-
553
- content = response_gemini.get("content", "")
554
- print(f"✅ Resposta recebida ({len(content)} chars)")
555
-
556
- # 4. Processar Resposta
557
- titles_data = extract_json_from_text(content)
558
-
559
- if not titles_data:
560
- print(f"⚠️ Falha ao extrair JSON. Conteúdo bruto: {content[:200]}...")
561
- return JSONResponse(content={"raw_content": content, "error": "Failed to parse JSON"}, status_code=200)
562
-
563
- return titles_data
564
-
565
- except HTTPException:
566
- raise
567
- except Exception as e:
568
- import traceback
569
- traceback.print_exc()
570
- raise HTTPException(status_code=500, detail=f"Erro interno: {str(e)}")
571
- finally:
572
- # Limpar arquivo temporário
573
- if temp_file and os.path.exists(temp_file.name):
574
- try:
575
- os.unlink(temp_file.name)
576
- print(f"🧹 Arquivo temporário removido: {temp_file.name}")
577
- except:
578
- pass
579
-
580
-
581
- class GenerateTitlesRequest(BaseModel):
582
- video_url: str
583
- context: Optional[str] = None
584
- start: Optional[str] = None
585
- end: Optional[str] = None
586
- model: Optional[str] = "flash"
587
-
588
-
589
- @app.post("/generate-titles")
590
- async def generate_titles_endpoint(request: GenerateTitlesRequest):
591
- """
592
- Gera títulos de notícias a partir de um vídeo (ou trecho dele).
593
- """
594
- if not chatbots:
595
- raise HTTPException(status_code=500, detail="Chatbot não inicializado")
596
-
597
- temp_file = None
598
- cut_file = None
599
-
600
- try:
601
- # 1. Validar e Baixar Vídeo
602
- if not request.video_url:
603
- raise HTTPException(status_code=400, detail="URL do vídeo é obrigatória")
604
-
605
- print(f"📥 [GenerateTitles] Baixando vídeo: {request.video_url}")
606
-
607
- # Baixar direto para um arquivo temporário
608
- response = download_file_with_retry(request.video_url, timeout=600)
609
-
610
- content_type = response.headers.get('content-type', '').lower()
611
- ext = '.mp4'
612
- if 'webm' in content_type: ext = '.webm'
613
- elif 'mkv' in content_type: ext = '.mkv'
614
-
615
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
616
- for chunk in response.iter_content(chunk_size=1024*1024):
617
- if chunk:
618
- temp_file.write(chunk)
619
- temp_file.close()
620
-
621
- video_path_to_analyze = temp_file.name
622
-
623
- # 2. Cortar Vídeo se necessário
624
- if request.start and request.end:
625
- print(f"✂️ [GenerateTitles] Cortando vídeo de {request.start} até {request.end}...")
626
- cut_file = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
627
- cut_file.close()
628
-
629
- success = cut_video(temp_file.name, cut_file.name, request.start, request.end)
630
- if success:
631
- video_path_to_analyze = cut_file.name
632
- print(f"✅ Vídeo cortado: {video_path_to_analyze}")
633
- else:
634
- print("⚠️ Falha ao cortar vídeo, usando original.")
635
-
636
- # 3. Preparar Prompt
637
- contexto_add = f"\n{request.context}" if request.context else ""
638
-
639
- prompt = f"""Analise essa notícia e o vídeo:
640
-
641
- {contexto_add}
642
-
643
- Com base nela, gere titulos de notícias que podem ser extraidos do vídeo/notícia. Títulos de notícias.
644
-
645
- Exemplos:
646
-
647
- [
648
- {{"title":"Trump repreende repórter da CNN por não sorrir"}},
649
- {{"title":"Duas semanas de fortes nevascas causam caos no Japão"}},
650
- {{"title":"Nova divulgação de arquivos sobre Jeffrey Epstein revela ligações com homens influentes"}},
651
- {{"title":"Trump anuncia fechamento do Kennedy Center em julho para reforma de dois anos"}},
652
- {{"title":"Como tecnologia de guerra foi usada em Minneapolis"}},
653
- {{"title":"Trump diz ter alcançado base de acordo com a Otan sobre a Groenlândia"}},
654
- {{"title":"Trump anuncia tarifas contra oito países europeus para forçar venda da Groenlândia"}},
655
- {{"title":"Trump tem muitas opções para lidar com o Irã, diz porta-voz"}}
656
- ]
657
-
658
- Mande apenas os títulos, em forma JSON. Sem texto adicional. Deve analisar todo o contexto do vídeo e verificar qual o melhor título. O título deve ser sólido, evitar emojis, dois pontos, etc. O estilo dos títulos devem ser parecidos com o JSON.
659
-
660
- Deve ter parametro start e end, e cortar o vídeo enviado nesse tempo. Pra analisar corretamente apenas a parte que o usuário quer. Considere que o jornal é imparcial na apresentação dos fatos, mas possui uma inclinação editorial claramente mais conservadora, republicana e de direita na entonação. Adapte os títulos de acordo com esse posicionamento, mantendo rigor factual e linguagem jornalística."""
661
-
662
- # 4. Enviar para o Gemini
663
- model_name = request.model or "flash"
664
- chatbot = chatbots.get(model_name, chatbots.get('flash', chatbots['default']))
665
-
666
- print(f"🧠 [GenerateTitles] Enviando para Gemini ({model_name})...")
667
-
668
- response_gemini = await chatbot.ask(prompt, video=video_path_to_analyze)
669
-
670
- if response_gemini.get("error"):
671
- raise HTTPException(status_code=500, detail=f"Erro no Gemini: {response_gemini.get('content')}")
672
-
673
- content = response_gemini.get("content", "")
674
- print(f"✅ Resposta recebida ({len(content)} chars)")
675
-
676
- # 5. Processar Resposta
677
- titles_data = extract_json_from_text(content)
678
-
679
- if not titles_data:
680
- print(f"⚠️ Falha ao extrair JSON. Conteúdo bruto: {content[:200]}...")
681
- return JSONResponse(content={"raw_content": content, "error": "Failed to parse JSON"}, status_code=200)
682
-
683
- return titles_data
684
-
685
- except HTTPException:
686
- raise
687
- except Exception as e:
688
- import traceback
689
- traceback.print_exc()
690
- raise HTTPException(status_code=500, detail=f"Erro interno: {str(e)}")
691
- finally:
692
- # Limpar arquivos temporários
693
- if temp_file and os.path.exists(temp_file.name):
694
- try:
695
- os.unlink(temp_file.name)
696
- except: pass
697
- if cut_file and os.path.exists(cut_file.name):
698
- try:
699
- os.unlink(cut_file.name)
700
- except: pass
701
-
702
-
703
  class GenerateElementsRequest(BaseModel):
704
  video_url: str
705
  context: Optional[str] = None
@@ -918,380 +655,6 @@ Se o contexto enviado pelo usuário não for verdadeiro ou estiver impreciso, ig
918
 
919
 
920
 
921
- def flip_image_both_axes(image_path: str) -> str:
922
- """
923
- Inverte uma imagem horizontalmente e verticalmente.
924
- Usa compressão JPEG com qualidade 90 e subsampling 4:2:0 para alterar
925
- o fingerprint da imagem e evitar detecção de figuras públicas.
926
- Retorna o caminho para um novo arquivo temporário com a imagem invertida.
927
- """
928
- with Image.open(image_path) as img:
929
- # Inverter horizontalmente e verticalmente
930
- flipped = img.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.FLIP_TOP_BOTTOM)
931
-
932
- # Determinar extensão e formato
933
- suffix = Path(image_path).suffix.lower()
934
- if suffix in ['.jpg', '.jpeg']:
935
- out_suffix = '.jpg'
936
- else:
937
- out_suffix = suffix
938
-
939
- # Salvar em arquivo temporário
940
- temp_flipped = tempfile.NamedTemporaryFile(delete=False, suffix=out_suffix)
941
- temp_flipped.close()
942
-
943
- # Salvar com parâmetros específicos para alterar fingerprint
944
- if out_suffix in ['.jpg', '.jpeg']:
945
- # Usar qualidade 90 e subsampling 4:2:0 (como o Figma faz)
946
- # Isso altera os artefatos de compressão JPEG
947
- flipped.save(
948
- temp_flipped.name,
949
- format='JPEG',
950
- quality=90,
951
- subsampling='4:2:0', # Chroma subsampling diferente
952
- optimize=True
953
- )
954
- else:
955
- flipped.save(temp_flipped.name)
956
-
957
- return temp_flipped.name
958
-
959
-
960
- def flip_base64_image_both_axes(img_base64: str, content_type: str) -> str:
961
- """
962
- Inverte uma imagem base64 horizontalmente e verticalmente.
963
- Usa compressão JPEG com qualidade 90 para alterar o fingerprint.
964
- Retorna o base64 da imagem invertida.
965
- """
966
- # Decodificar base64 para bytes
967
- img_bytes = base64.b64decode(img_base64)
968
-
969
- # Abrir imagem dos bytes
970
- with Image.open(io.BytesIO(img_bytes)) as img:
971
- # Inverter horizontalmente e verticalmente
972
- flipped = img.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.FLIP_TOP_BOTTOM)
973
-
974
- # Salvar em buffer
975
- buffer = io.BytesIO()
976
-
977
- # Determinar formato baseado no content_type
978
- if 'jpeg' in content_type or 'jpg' in content_type:
979
- # Usar qualidade 90 e subsampling 4:2:0 para alterar fingerprint
980
- flipped.save(
981
- buffer,
982
- format='JPEG',
983
- quality=90,
984
- subsampling='4:2:0',
985
- optimize=True
986
- )
987
- elif 'webp' in content_type:
988
- flipped.save(buffer, format='WEBP', quality=90)
989
- else:
990
- flipped.save(buffer, format='PNG')
991
-
992
- buffer.seek(0)
993
-
994
- # Converter de volta para base64
995
- return base64.b64encode(buffer.read()).decode('utf-8')
996
-
997
-
998
- async def _try_upscale(chatbot, image_path: Union[str, list[str]], prompt: str):
999
- """
1000
- Tenta fazer upscale de uma imagem (ou série de imagens).
1001
- Retorna (result, upscaled_url) ou (None, None) em caso de erro.
1002
- """
1003
- result = await chatbot.ask(prompt, image=image_path)
1004
-
1005
- if result.get("error"):
1006
- return None, None
1007
-
1008
- upscaled_url = None
1009
- if result.get("images"):
1010
- # Preferir imagens geradas
1011
- for img in result["images"]:
1012
- if "[Generated Image" in img.get("title", ""):
1013
- upscaled_url = img["url"]
1014
- break
1015
-
1016
- # Se não achou 'Generated Image', pega a última
1017
- if not upscaled_url and len(result["images"]) > 0:
1018
- upscaled_url = result["images"][-1]["url"]
1019
-
1020
- return result, upscaled_url
1021
-
1022
-
1023
- async def _download_upscaled_image(upscaled_url: str, cookies_dict: dict) -> tuple:
1024
- """
1025
- Baixa a imagem upscaled e retorna (img_base64, content_type, download_url).
1026
- Tenta múltiplas estratégias de download para lidar com 403 Forbidden.
1027
- """
1028
- from fastapi.concurrency import run_in_threadpool
1029
- import requests
1030
-
1031
- headers_with_cookies = {
1032
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
1033
- 'Referer': 'https://gemini.google.com/',
1034
- 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
1035
- 'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7',
1036
- 'Sec-Fetch-Dest': 'image',
1037
- 'Sec-Fetch-Mode': 'no-cors',
1038
- 'Sec-Fetch-Site': 'same-site',
1039
- }
1040
-
1041
- # Preparar URL base (sem parâmetros de resolução)
1042
- base_url = upscaled_url
1043
- if "?" in base_url:
1044
- base_url = base_url.split("?")[0]
1045
- if "=s" in base_url:
1046
- base_url = base_url.rsplit("=s", 1)[0]
1047
-
1048
- # Lista de estratégias de download para tentar
1049
- download_strategies = [
1050
- # Estratégia 1: URL original com =s0-d-I, com cookies, sem seguir redirect
1051
- {"url": base_url + "=s0-d-I", "cookies": cookies_dict, "allow_redirects": False, "label": "Original URL + full res + cookies (no redirect)"},
1052
- # Estratégia 2: URL original com =s0-d-I, com cookies, seguindo redirect
1053
- {"url": base_url + "=s0-d-I", "cookies": cookies_dict, "allow_redirects": True, "label": "Original URL + full res + cookies (with redirect)"},
1054
- # Estratégia 3: URL original direta (como veio do Gemini), com cookies
1055
- {"url": upscaled_url, "cookies": cookies_dict, "allow_redirects": True, "label": "Original URL as-is + cookies"},
1056
- # Estratégia 4: URL original com =s0-d-I, SEM cookies
1057
- {"url": base_url + "=s0-d-I", "cookies": None, "allow_redirects": True, "label": "Original URL + full res (no cookies)"},
1058
- # Estratégia 5: URL original direta, SEM cookies
1059
- {"url": upscaled_url, "cookies": None, "allow_redirects": True, "label": "Original URL as-is (no cookies)"},
1060
- ]
1061
-
1062
- img_response = None
1063
- download_url = base_url + "=s0-d-I"
1064
-
1065
- for i, strategy in enumerate(download_strategies):
1066
- try:
1067
- print(f"📥 Tentativa {i+1}/{len(download_strategies)}: {strategy['label']}")
1068
- print(f" URL: {strategy['url'][:100]}...")
1069
-
1070
- resp = await run_in_threadpool(
1071
- requests.get,
1072
- strategy['url'],
1073
- timeout=60,
1074
- allow_redirects=strategy['allow_redirects'],
1075
- cookies=strategy['cookies'],
1076
- headers=headers_with_cookies
1077
- )
1078
-
1079
- # Se recebeu redirect (3xx) e não seguiu, tentar seguir manualmente
1080
- if resp.status_code in (301, 302, 303, 307, 308) and not strategy['allow_redirects']:
1081
- redirect_url = resp.headers.get('Location', '')
1082
- if redirect_url:
1083
- print(f" ↳ Redirect para: {redirect_url[:100]}...")
1084
- resp = await run_in_threadpool(
1085
- requests.get,
1086
- redirect_url,
1087
- timeout=60,
1088
- allow_redirects=True,
1089
- cookies=strategy['cookies'],
1090
- headers=headers_with_cookies
1091
- )
1092
-
1093
- if resp.status_code == 200 and len(resp.content) > 1000:
1094
- content_type_header = resp.headers.get('content-type', '')
1095
- if 'image' in content_type_header or len(resp.content) > 10000:
1096
- print(f" ✅ Download OK! ({len(resp.content)} bytes, {content_type_header})")
1097
- img_response = resp
1098
- download_url = strategy['url']
1099
- break
1100
- else:
1101
- print(f" ⚠️ Resposta não parece ser imagem: {content_type_header}")
1102
- else:
1103
- print(f" ❌ Status: {resp.status_code}, Size: {len(resp.content)} bytes")
1104
-
1105
- except Exception as e:
1106
- print(f" ❌ Erro: {e}")
1107
- continue
1108
-
1109
- if img_response is None:
1110
- raise Exception(f"Não foi possível baixar a imagem após {len(download_strategies)} tentativas. URL original: {upscaled_url[:100]}")
1111
-
1112
- img_content = img_response.content
1113
- try:
1114
- from watermark_remover import remove_watermark
1115
- print(f"🧹 Removendo marca d'água da imagem...")
1116
- img_content, _ = remove_watermark(img_content)
1117
- print(f"✨ Marca d'água removida com sucesso!")
1118
- except Exception as e:
1119
- print(f"⚠️ Erro ao remover marca d'água: {e}")
1120
- import traceback
1121
- traceback.print_exc()
1122
-
1123
- # Converter para base64
1124
- import base64
1125
- img_base64 = base64.b64encode(img_content).decode('utf-8')
1126
- content_type = img_response.headers.get('content-type', 'image/png')
1127
-
1128
- # Fazer upload para a API externa (upload(2).py)
1129
- final_image_url = download_url
1130
- try:
1131
- print(f"☁️ Fazendo upload da imagem sem marca d'água para https://habulaj-recurve-api.hf.space/upload-image")
1132
-
1133
- upload_response = await run_in_threadpool(
1134
- requests.post,
1135
- "https://habulaj-recurve-api.hf.space/upload-image",
1136
- json={"image_base64": img_base64},
1137
- timeout=60
1138
- )
1139
- upload_response.raise_for_status()
1140
- upload_data = upload_response.json()
1141
-
1142
- if "url" in upload_data:
1143
- final_image_url = upload_data["url"]
1144
- print(f"✅ Upload concluído: {final_image_url}")
1145
- else:
1146
- print(f"⚠️ Resposta de upload inesperada: {upload_data}")
1147
-
1148
- except Exception as e:
1149
- print(f"⚠️ Erro ao fazer upload da imagem: {e}")
1150
- import traceback
1151
- traceback.print_exc()
1152
-
1153
- print(f"✅ Imagem processada e convertida para base64 ({len(img_base64)} chars)")
1154
-
1155
- return img_base64, content_type, final_image_url
1156
-
1157
-
1158
- @app.get("/upscale")
1159
- async def upscale_image(
1160
- file: str
1161
- ):
1162
- """
1163
- Endpoint para fazer upscale 4x de uma imagem usando o Nano Banana Pro.
1164
-
1165
- Se a primeira tentativa falhar (ex: erro de figuras públicas),
1166
- tenta novamente invertendo a imagem horizontalmente e verticalmente,
1167
- e depois inverte o resultado de volta.
1168
-
1169
- Parâmetros:
1170
- - file: URL da imagem
1171
-
1172
- Retorna:
1173
- - JSON com a imagem gerada em base64 (upscaled)
1174
- """
1175
- if upscale_chatbot is None:
1176
- raise HTTPException(status_code=500, detail="Upscale Chatbot não inicializado")
1177
-
1178
- if not file:
1179
- raise HTTPException(status_code=400, detail="Parâmetro 'file' é obrigatório")
1180
-
1181
- temp_file = None
1182
- temp_flipped_file = None
1183
-
1184
- try:
1185
- # Baixar arquivo da URL com retry
1186
- response = download_file_with_retry(file, max_retries=3, timeout=300)
1187
-
1188
- # Verificar se é uma imagem
1189
- content_type = response.headers.get('content-type', '').lower()
1190
- if 'image' not in content_type and not any(ext in file.lower() for ext in ['.jpg', '.jpeg', '.png', '.webp']):
1191
- raise HTTPException(status_code=400, detail="O arquivo fornecido não parece ser uma imagem.")
1192
-
1193
- file_extension = '.jpg' # default
1194
- if 'png' in content_type: file_extension = '.png'
1195
- elif 'webp' in content_type: file_extension = '.webp'
1196
-
1197
- # Salvar arquivo temporariamente
1198
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=file_extension)
1199
- for chunk in response.iter_content(chunk_size=8192):
1200
- if chunk:
1201
- temp_file.write(chunk)
1202
- temp_file.close()
1203
-
1204
- print(f"✅ Arquivo para upscale baixado: {temp_file.name}")
1205
-
1206
- prompt = "Upscale this image by 4x. Keep everything exactly as it is, including text, colors, texture, aspect ratio, zoom and proportions. Just increase the quality and sharpness. Do not add any new elements or modify the composition"
1207
-
1208
- # Carregar cookies para download
1209
- cookies_dict = {}
1210
- cookie_path = os.getenv("COOKIE_PATH", "cookies.json")
1211
- try:
1212
- secure_1psid, secure_1psidts, additional_cookies = load_cookies(cookie_path)
1213
- cookies_dict = additional_cookies.copy()
1214
- cookies_dict['__Secure-1PSID'] = secure_1psid
1215
- cookies_dict['__Secure-1PSIDTS'] = secure_1psidts
1216
- print(f"✅ {len(cookies_dict)} cookies carregados para download")
1217
- except Exception as cookie_err:
1218
- print(f"⚠️ Aviso: Não foi possível carregar cookies: {cookie_err}")
1219
-
1220
- # ========== PRIMEIRA TENTATIVA: Normal ==========
1221
- print(f"🧠 [Tentativa 1] Enviando imagem para upscale...")
1222
- result, upscaled_url = await _try_upscale(upscale_chatbot, temp_file.name, prompt)
1223
-
1224
- used_flip_workaround = False
1225
-
1226
- if result is None or upscaled_url is None:
1227
- # ========== SEGUNDA TENTATIVA: Inverter imagem ==========
1228
- print(f"⚠️ Primeira tentativa falhou. Tentando workaround de inversão...")
1229
-
1230
- # Inverter a imagem horizontalmente e verticalmente
1231
- print(f"🔄 Invertendo imagem (horizontal + vertical)...")
1232
- temp_flipped_file = flip_image_both_axes(temp_file.name)
1233
- print(f"✅ Imagem invertida salva em: {temp_flipped_file}")
1234
-
1235
- # Tentar novamente com a imagem invertida
1236
- print(f"🧠 [Tentativa 2] Enviando imagem INVERTIDA para upscale...")
1237
- result, upscaled_url = await _try_upscale(upscale_chatbot, temp_flipped_file, prompt)
1238
-
1239
- if result is None or upscaled_url is None:
1240
- raise HTTPException(
1241
- status_code=500,
1242
- detail="Nenhuma imagem de upscale foi retornada pelo modelo após múltiplas tentativas."
1243
- )
1244
-
1245
- used_flip_workaround = True
1246
- print(f"✅ Segunda tentativa (com inversão) funcionou!")
1247
-
1248
- # Baixar imagem upscaled
1249
- try:
1250
- img_base64, img_content_type, download_url = await _download_upscaled_image(upscaled_url, cookies_dict)
1251
- except Exception as download_error:
1252
- print(f"❌ Erro ao baixar imagem: {download_error}")
1253
- raise HTTPException(
1254
- status_code=500,
1255
- detail=f"Erro ao baixar imagem upscaled: {str(download_error)}"
1256
- )
1257
-
1258
- # Se usamos o workaround de inversão, precisamos inverter o resultado de volta
1259
- if used_flip_workaround:
1260
- print(f"🔄 Invertendo resultado de volta (restaurando orientação original)...")
1261
- img_base64 = flip_base64_image_both_axes(img_base64, img_content_type)
1262
- print(f"✅ Imagem restaurada para orientação original")
1263
-
1264
- return JSONResponse(
1265
- content={
1266
- "image_base64": img_base64,
1267
- "content_type": img_content_type,
1268
- "success": True,
1269
- "original_url": file,
1270
- "upscaled_url": download_url,
1271
- "used_flip_workaround": used_flip_workaround
1272
- }
1273
- )
1274
-
1275
- except HTTPException:
1276
- raise
1277
- except Exception as e:
1278
- import traceback
1279
- traceback.print_exc()
1280
- raise HTTPException(status_code=500, detail=f"Erro interno no upscale: {str(e)}")
1281
- finally:
1282
- # Limpar arquivos temporários
1283
- if temp_file and os.path.exists(temp_file.name):
1284
- try:
1285
- os.unlink(temp_file.name)
1286
- except:
1287
- pass
1288
- if temp_flipped_file and os.path.exists(temp_flipped_file):
1289
- try:
1290
- os.unlink(temp_flipped_file)
1291
- except:
1292
- pass
1293
-
1294
-
1295
  # ==========================================
1296
  # GROQ ENDPOINT
1297
  # ==========================================
@@ -1649,527 +1012,4 @@ INSTRUÇÕES/CONTEXTO DO USUÁRIO (OPCIONAL): {processed_context}
1649
  except Exception as e:
1650
  import traceback
1651
  traceback.print_exc()
1652
- raise HTTPException(status_code=500, detail=str(e))
1653
-
1654
- class CreateBonecoRequest(BaseModel):
1655
- character_name: str
1656
- instruction: Optional[str] = ""
1657
- images: Optional[list[str]] = []
1658
-
1659
- @app.post("/create-boneco")
1660
- async def create_boneco_endpoint(request: CreateBonecoRequest):
1661
- """
1662
- Endpoint para criar bonecos a partir de um arquivo base e referências extras.
1663
- """
1664
- if upscale_chatbot is None:
1665
- raise HTTPException(status_code=500, detail="Upscale Chatbot não inicializado")
1666
-
1667
- base_image_url = "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772053948027x705745004124176100/Frame%201(1).png"
1668
-
1669
- # Garantir que a imagem de referência seja SEMPRE a primeira
1670
- urls_to_download = [base_image_url]
1671
- if request.images:
1672
- urls_to_download.extend(request.images)
1673
-
1674
- temp_files = []
1675
- try:
1676
- # Baixar todas as imagens
1677
- for url in urls_to_download:
1678
- try:
1679
- resp = download_file_with_retry(url, max_retries=3)
1680
- ext = ".png" if "png" in resp.headers.get("content-type", "") else ".jpg"
1681
- tf = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
1682
- for chunk in resp.iter_content(chunk_size=8192):
1683
- if chunk:
1684
- tf.write(chunk)
1685
- tf.close()
1686
- temp_files.append(tf.name)
1687
- except Exception as e:
1688
- print(f"Erro ao baixar {url}: {e}")
1689
-
1690
- if not temp_files:
1691
- raise HTTPException(status_code=400, detail="Nenhuma imagem pôde ser baixada.")
1692
-
1693
- # Pré-processar imagens de referência (index 1+) para forçar aspect ratio 1:1
1694
- # A imagem base (index 0 = Frame 1.png) já é quadrada.
1695
- # As imagens de referência do personagem podem ser retrato/paisagem,
1696
- # o que faz o Gemini copiar o aspect ratio delas no resultado.
1697
- # Solução: paddar com fundo branco para 1:1 antes de enviar.
1698
- for i in range(1, len(temp_files)):
1699
- try:
1700
- img = Image.open(temp_files[i]).convert("RGB")
1701
- w, h = img.size
1702
- if w != h:
1703
- new_size = max(w, h)
1704
- padded = Image.new("RGB", (new_size, new_size), (255, 255, 255))
1705
- paste_x = (new_size - w) // 2
1706
- paste_y = (new_size - h) // 2
1707
- padded.paste(img, (paste_x, paste_y))
1708
- # Salvar de volta no mesmo arquivo temporário
1709
- padded.save(temp_files[i], "JPEG", quality=95)
1710
- print(f"📐 Imagem de referência {i} redimensionada de {w}x{h} para {new_size}x{new_size} (1:1)")
1711
- else:
1712
- print(f"📐 Imagem de referência {i} já é quadrada ({w}x{h})")
1713
- except Exception as e:
1714
- print(f"⚠️ Erro ao pré-processar imagem {i}: {e}")
1715
-
1716
- prompt = f'''Você vai adaptar o boneco base (a PRIMEIRA IMAGEM anexada) para ser este personagem: {request.character_name}.
1717
-
1718
- INSTRUÇÕES DO PERSONAGEM: {request.character_name} | {request.instruction}
1719
-
1720
- REGRAS DE ESTRUTURA E ANATOMIA:
1721
- 1. Adapte a anatomia e o tipo físico do boneco base para refletir o personagem real (ex: se for alguém gordo, molde o corpo do boneco para ficar gordo; se for magro, mais magro, etc).
1722
- 2. Copie exatamente o mesmo estilo, corte e volume de CABELO do personagem de referência.
1723
- 3. Vista-o com as roupas características e adicione quaisquer acessórios necessários (óculos, chapéu, etc).
1724
-
1725
- REGRA DE OURO - FORMATO INFERIOR:
1726
- O boneco deve permanecer exatamente SEM PERNAS, terminando na mesma proporção e limite inferior da imagem base. Em hipótese alguma gere pernas, sapatos ou estenda o corpo para baixo.
1727
-
1728
- ESTILO VISUAL (OBRIGATÓRIO):
1729
- - A imagem final DEVE SER ESTRITAMENTE QUADRADA (proporção 1:1), com fundo branco puramente liso. O boneco deve ficar centralizado, mantendo o mesmo zoom da PRIMEIRA imagem anexada.
1730
- - Cores sólidas e uniformes. Sem gradientes. Sem texturas. Sem glitch. Sem sombras adicionais. Estilo de desenho vetorial flat (2D puro).
1731
- - ROSTO: O boneco DEVE ter os 2 olhos simples (apenas dois pontinhos pretos). Sem olhos complexos. Os olhos pretos redondinhos devem ficar na exata mesma altura e posição do boneco original.
1732
- - Alta fidelidade ao estilo de desenho simples. Saída em 4K, ultra nítido.'''
1733
-
1734
- print(f"🧠 [CreateBoneco] Enviando prompt para Gemini (NANO_BANANA)...")
1735
- result, generated_url = await _try_upscale(upscale_chatbot, temp_files, prompt)
1736
-
1737
- if result is None or generated_url is None:
1738
- raise HTTPException(status_code=500, detail="Falha ao gerar o boneco no Gemini.")
1739
-
1740
- # Carregar cookies para baixar a imagem gerada
1741
- cookies_dict = {}
1742
- cookie_path = os.getenv("COOKIE_PATH", "cookies.json")
1743
- try:
1744
- secure_1psid, secure_1psidts, additional_cookies = load_cookies(cookie_path)
1745
- cookies_dict = additional_cookies.copy()
1746
- cookies_dict['__Secure-1PSID'] = secure_1psid
1747
- cookies_dict['__Secure-1PSIDTS'] = secure_1psidts
1748
- except Exception:
1749
- pass
1750
-
1751
- img_base64, img_content_type, actual_url = await _download_upscaled_image(generated_url, cookies_dict)
1752
-
1753
- return JSONResponse(
1754
- content={
1755
- "image_base64": img_base64,
1756
- "content_type": img_content_type,
1757
- "success": True,
1758
- "generated_url": actual_url
1759
- }
1760
- )
1761
-
1762
- except HTTPException:
1763
- raise
1764
- except Exception as e:
1765
- import traceback
1766
- traceback.print_exc()
1767
- raise HTTPException(status_code=500, detail=f"Erro interno: {str(e)}")
1768
- finally:
1769
- for tf in temp_files:
1770
- if os.path.exists(tf):
1771
- try: os.unlink(tf)
1772
- except: pass
1773
-
1774
- AVAILABLE_POSES = {
1775
- "frontal": {
1776
- "id": "frontal",
1777
- "name": "Frontal",
1778
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772056382491x553551500711791300/Frontal.png"
1779
- },
1780
- "lateral": {
1781
- "id": "lateral",
1782
- "name": "Lateral",
1783
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772056392317x525252134032321700/Lateral.png"
1784
- },
1785
- "levemente_inclinado": {
1786
- "id": "levemente_inclinado",
1787
- "name": "Levemente inclinado",
1788
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772056404224x608542850369763200/Levemente%20inclinado.png"
1789
- },
1790
- "visao_cima_olhando_cima": {
1791
- "id": "visao_cima_olhando_cima",
1792
- "name": "Visão de cima olhando pra cima",
1793
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772056421453x237894377175501760/Vis%C3%A3o%20de%20cima%20olhando%20pra%20cima.png"
1794
- },
1795
- "visao_cima_olhando_reto": {
1796
- "id": "visao_cima_olhando_reto",
1797
- "name": "Visão de cima olhando reto",
1798
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772056431828x290688843529110600/Vis%C3%A3o%20de%20cima%20olhando%20reto.png"
1799
- },
1800
- "de_costas": {
1801
- "id": "de_costas",
1802
- "name": "De costas",
1803
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772056374715x975886246017891600/De%20costas.png"
1804
- },
1805
- "sentado_cadeira_lateral": {
1806
- "id": "sentado_cadeira_lateral",
1807
- "name": "Sentado numa cadeira lateral",
1808
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071771582x811232277086660900/Sentado%20numa%20cadeira%20lateral.png"
1809
- },
1810
- "sentado_cadeira_frontal": {
1811
- "id": "sentado_cadeira_frontal",
1812
- "name": "Sentado numa cadeira frontal",
1813
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071763383x694992495797004800/Sentado%20numa%20cadeira%20frontal.png"
1814
- },
1815
- "segurando_papel_antigo": {
1816
- "id": "segurando_papel_antigo",
1817
- "name": "Segurando um papel antigo",
1818
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071754856x742358679324603500/Segurando%20um%20papel%20antigo.png"
1819
- },
1820
- "em_frente_pulpito": {
1821
- "id": "em_frente_pulpito",
1822
- "name": "Em frente a um púlpito de madeira",
1823
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071748588x666930999439381500/Em%20frente%20a%20um%20p%C3%BAlpito%20de%20madeira.png"
1824
- },
1825
- "duvida": {
1826
- "id": "duvida",
1827
- "name": "Duvida (mão no rosto de duvida)",
1828
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071741300x211037726196138900/Duvida%20(m%C3%A3o%20no%20rosto%20de%20duvida).png"
1829
- },
1830
- "deitado_pra_cima": {
1831
- "id": "deitado_pra_cima",
1832
- "name": "Deitado para cima",
1833
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071733182x313353197840595700/Deitado%20para%20cima.png"
1834
- },
1835
- "deitado_pra_baixo": {
1836
- "id": "deitado_pra_baixo",
1837
- "name": "Deitado para baixo (dormindo)",
1838
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071725229x683109943339230500/Deitado%20para%20baixo%20(dormindo).png"
1839
- },
1840
- "continencia_rosto_neutro": {
1841
- "id": "continencia_rosto_neutro",
1842
- "name": "Continencia com rosto neutro",
1843
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071717695x840859019351342600/Continencia%20com%20rosto%20neutro.png"
1844
- },
1845
- "continencia_rosto_bravo": {
1846
- "id": "continencia_rosto_bravo",
1847
- "name": "Continencia com rosto bravo",
1848
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071710301x217256255491910850/Continencia%20com%20rosto%20bravo.png"
1849
- },
1850
- "caindo_no_vacuo": {
1851
- "id": "caindo_no_vacuo",
1852
- "name": "Caindo no vácuo",
1853
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071702682x752555202742032100/Caindo%20no%20v%C3%A1cuo.png"
1854
- },
1855
- "bracos_levantados": {
1856
- "id": "bracos_levantados",
1857
- "name": "Braços levantados para cima",
1858
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071684343x887395473109532400/Bra%C3%A7os%20levantados%20para%20cima.png"
1859
- },
1860
- "bracos_apoiados_gluteos": {
1861
- "id": "bracos_apoiados_gluteos",
1862
- "name": "Braços apoiados nos gluteos medios",
1863
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071652574x814651799994602600/Bra%C3%A7os%20apoiados%20nos%20gluteos%20medios.png"
1864
- },
1865
- "apontando_quadro_chroma": {
1866
- "id": "apontando_quadro_chroma",
1867
- "name": "Apontando para um quadro de chroma key",
1868
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071639742x758356447340169000/Apontando%20para%20um%20quadro%20de%20chroma%20key.png"
1869
- },
1870
- "apontando_para_lado": {
1871
- "id": "apontando_para_lado",
1872
- "name": "Apontando para o lado",
1873
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071627966x990711926733976800/Apontando%20para%20o%20lado.png"
1874
- },
1875
- "andar_desanimado": {
1876
- "id": "andar_desanimado",
1877
- "name": "Andar desanimado",
1878
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071619155x305722515016405400/Andar%20desanimado.png"
1879
- },
1880
- "agachado_olhando_chao": {
1881
- "id": "agachado_olhando_chao",
1882
- "name": "Agachado olhando para o chão",
1883
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071610322x420177596610840100/Agachado%20olhando%20para%20o%20ch%C3%A3o.png"
1884
- },
1885
- "sentado_pernas_cruzadas": {
1886
- "id": "sentado_pernas_cruzadas",
1887
- "name": "Sentado para frente com as pernas cruzadas",
1888
- "image_url": "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772071791497x959445447942171300/Sentado%20para%20frente%20com%20as%20pernas%20cruzadas.png"
1889
- }
1890
- }
1891
-
1892
- @app.get("/poses")
1893
- async def get_poses():
1894
- """
1895
- Retorna a lista de poses disponíveis para serem usadas.
1896
- """
1897
- return JSONResponse(content=list(AVAILABLE_POSES.values()))
1898
-
1899
- class CreatePoseRequest(BaseModel):
1900
- base_character_image_url: str
1901
- pose_id: Optional[str] = None
1902
- custom_pose_description: Optional[str] = None
1903
-
1904
- @app.post("/create-pose")
1905
- async def create_pose_endpoint(request: CreatePoseRequest):
1906
- """
1907
- Endpoint para criar uma nova pose de um personagem criado.
1908
- Pode usar uma pose pré-definida (pose_id) ou uma descrição customizada (custom_pose_description).
1909
- """
1910
- if upscale_chatbot is None:
1911
- raise HTTPException(status_code=500, detail="Upscale Chatbot não inicializado")
1912
-
1913
- is_custom_pose = bool(request.custom_pose_description and request.custom_pose_description.strip())
1914
-
1915
- if not is_custom_pose and not request.pose_id:
1916
- raise HTTPException(status_code=400, detail="É necessário fornecer um 'pose_id' ou um 'custom_pose_description'.")
1917
-
1918
- if not is_custom_pose and request.pose_id not in AVAILABLE_POSES:
1919
- raise HTTPException(status_code=400, detail="ID da pose inválido.")
1920
-
1921
- # Definir URLs para baixar
1922
- urls_to_download = [request.base_character_image_url]
1923
- if not is_custom_pose:
1924
- pose_image_url = AVAILABLE_POSES[request.pose_id]["image_url"]
1925
- urls_to_download.append(pose_image_url)
1926
-
1927
- temp_files = []
1928
-
1929
- try:
1930
- # Baixar as imagens
1931
- for url in urls_to_download:
1932
- try:
1933
- resp = download_file_with_retry(url, max_retries=3)
1934
- ext = ".png" if "png" in resp.headers.get("content-type", "") else ".jpg"
1935
- tf = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
1936
- for chunk in resp.iter_content(chunk_size=8192):
1937
- if chunk:
1938
- tf.write(chunk)
1939
- tf.close()
1940
- temp_files.append(tf.name)
1941
- except Exception as e:
1942
- print(f"Erro ao baixar {url}: {e}")
1943
-
1944
- expected_files = 1 if is_custom_pose else 2
1945
- if len(temp_files) != expected_files:
1946
- raise HTTPException(status_code=400, detail="Não foi possível baixar as imagens necessárias.")
1947
-
1948
- # Pré-processar imagens para aspect ratio 1:1, garantindo fundo branco
1949
- for i in range(len(temp_files)):
1950
- try:
1951
- img = Image.open(temp_files[i]).convert("RGB")
1952
- w, h = img.size
1953
- if w != h:
1954
- new_size = max(w, h)
1955
- padded = Image.new("RGB", (new_size, new_size), (255, 255, 255))
1956
- paste_x = (new_size - w) // 2
1957
- paste_y = (new_size - h) // 2
1958
- padded.paste(img, (paste_x, paste_y))
1959
- padded.save(temp_files[i], "JPEG", quality=95)
1960
- print(f"📐 Imagem do create-pose {i} redimensionada de {w}x{h} para {new_size}x{new_size} (1:1)")
1961
- except Exception as e:
1962
- print(f"⚠️ Erro ao pré-processar imagem do create-pose {i}: {e}")
1963
-
1964
- # Lógica de prompts diferentes dependendo do modo (custom ou predefined)
1965
- if is_custom_pose:
1966
- pose_desc = request.custom_pose_description.strip()
1967
- prompt = f'''Preserve 100% da estrutura original do boneco. Mesmo zoom e posição (fundo branco). NÃO altere silhueta, proporções, contorno, posição ou traços existentes. NÃO adicione novos traços, linhas, volumes, sombras ou detalhes estruturais.
1968
-
1969
- Faça, apenas, o boneco fazer a pose: [{pose_desc}]. Retorne em alta qualidade.'''
1970
- print(f"🧠 [CreatePose] Enviando prompt para Gemini (NANO_BANANA) | Custom Pose: {pose_desc}...")
1971
- else:
1972
- pose_name = AVAILABLE_POSES[request.pose_id]["name"]
1973
- prompt = f'''Preserve 100% da estrutura original do boneco. Mesmo zoom e posição (fundo branco). NÃO altere silhueta, proporções, contorno, posição ou traços existentes. NÃO adicione novos traços, linhas, volumes, sombras ou detalhes estruturais. IGNORE QUALQUER DEFORMIDADE DO SEGUNDO ANEXO, é um exemplo de pose, não para seguir a risca. Deve arrumar qualquer imperfeição.
1974
-
1975
- Faça, apenas, o boneco fazer a pose da segunda foto anexada: [{pose_name}]. Retorne em alta qualidade, mesmo que a segunda imagem não seja. Deve ser apenas o boneco, apenas 1 pose/ângulo (igual da segunda foto), deve manter absolutamente todas as características do boneco, apenas botar o estilo nele no ângulo/pose da segunda foto.'''
1976
- print(f"🧠 [CreatePose] Enviando prompt para Gemini (NANO_BANANA) | Predefined Pose: {pose_name}...")
1977
-
1978
- result, generated_url = await _try_upscale(upscale_chatbot, temp_files, prompt)
1979
-
1980
- if result is None or generated_url is None:
1981
- raise HTTPException(status_code=500, detail="Falha ao gerar a pose no Gemini.")
1982
-
1983
- # Carregar cookies para baixar a imagem gerada
1984
- cookies_dict = {}
1985
- cookie_path = os.getenv("COOKIE_PATH", "cookies.json")
1986
- try:
1987
- secure_1psid, secure_1psidts, additional_cookies = load_cookies(cookie_path)
1988
- cookies_dict = additional_cookies.copy()
1989
- cookies_dict['__Secure-1PSID'] = secure_1psid
1990
- cookies_dict['__Secure-1PSIDTS'] = secure_1psidts
1991
- except Exception:
1992
- pass
1993
-
1994
- img_base64, img_content_type, actual_url = await _download_upscaled_image(generated_url, cookies_dict)
1995
-
1996
- return JSONResponse(
1997
- content={
1998
- "image_base64": img_base64,
1999
- "content_type": img_content_type,
2000
- "success": True,
2001
- "generated_url": actual_url
2002
- }
2003
- )
2004
-
2005
- except HTTPException:
2006
- raise
2007
- except Exception as e:
2008
- import traceback
2009
- traceback.print_exc()
2010
- raise HTTPException(status_code=500, detail=f"Erro interno: {str(e)}")
2011
- finally:
2012
- for tf in temp_files:
2013
- if os.path.exists(tf):
2014
- try: os.unlink(tf)
2015
- except: pass
2016
-
2017
- class CreateScenarioRequest(BaseModel):
2018
- explanation: str
2019
-
2020
- @app.post("/create-scenario")
2021
- async def create_scenario_endpoint(request: CreateScenarioRequest):
2022
- """
2023
- Endpoint para criar um cenário digital informal baseado em 3 imagens de referência.
2024
- """
2025
- if upscale_chatbot is None:
2026
- raise HTTPException(status_code=500, detail="Upscale Chatbot não inicializado")
2027
-
2028
- urls_to_download = [
2029
- "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772074850699x161592875261018720/unwatermarked_Gemini_Generated_Image_9v6srr9v6srr9v6s.png",
2030
- "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772074876638x406441129370988700/unwatermarked_Gemini_Generated_Image_vr3b15vr3b15vr3b.png",
2031
- "https://3120b81107781652e8544d604cd0343d.cdn.bubble.io/f1772074867731x493123391147277000/unwatermarked_Gemini_Generated_Image_muoccsmuoccsmuoc.png"
2032
- ]
2033
-
2034
- temp_files = []
2035
-
2036
- try:
2037
- # Baixar as 3 imagens de referência
2038
- for url in urls_to_download:
2039
- try:
2040
- resp = download_file_with_retry(url, max_retries=3)
2041
- ext = ".png" if "png" in resp.headers.get("content-type", "") else ".jpg"
2042
- tf = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
2043
- for chunk in resp.iter_content(chunk_size=8192):
2044
- if chunk:
2045
- tf.write(chunk)
2046
- tf.close()
2047
- temp_files.append(tf.name)
2048
- except Exception as e:
2049
- print(f"Erro ao baixar {url}: {e}")
2050
-
2051
- if len(temp_files) != 3:
2052
- raise HTTPException(status_code=400, detail="Não foi possível baixar as imagens de referência de cenário.")
2053
-
2054
- prompt = f'''Faça um cenário digital informal, inspirado nos desenhos anexados. O traçado e as sombras devem ser exatamente igual. Desenhos sem muita informação visual. O cenário do novo desenho, inspirado nos mesmos traços/estilo dos desenhos anexados, que você deve fazer, é: {request.explanation}.
2055
-
2056
- Deve ser 1920x1080 (16:9). Lembrando, não deve ter nenhum personagem no cenário, apenas o cenário bruto.'''
2057
-
2058
- print(f"🧠 [CreateScenario] Enviando prompt para Gemini (NANO_BANANA)...")
2059
- result, generated_url = await _try_upscale(upscale_chatbot, temp_files, prompt)
2060
-
2061
- if result is None or generated_url is None:
2062
- raise HTTPException(status_code=500, detail="Falha ao gerar o cenário no Gemini.")
2063
-
2064
- # Carregar cookies para baixar a imagem gerada
2065
- cookies_dict = {}
2066
- cookie_path = os.getenv("COOKIE_PATH", "cookies.json")
2067
- try:
2068
- secure_1psid, secure_1psidts, additional_cookies = load_cookies(cookie_path)
2069
- cookies_dict = additional_cookies.copy()
2070
- cookies_dict['__Secure-1PSID'] = secure_1psid
2071
- cookies_dict['__Secure-1PSIDTS'] = secure_1psidts
2072
- except Exception:
2073
- pass
2074
-
2075
- img_base64, img_content_type, actual_url = await _download_upscaled_image(generated_url, cookies_dict)
2076
-
2077
- return JSONResponse(
2078
- content={
2079
- "image_base64": img_base64,
2080
- "content_type": img_content_type,
2081
- "success": True,
2082
- "generated_url": actual_url
2083
- }
2084
- )
2085
-
2086
- except HTTPException:
2087
- raise
2088
- except Exception as e:
2089
- import traceback
2090
- traceback.print_exc()
2091
- raise HTTPException(status_code=500, detail=f"Erro interno: {str(e)}")
2092
- finally:
2093
- for tf in temp_files:
2094
- if os.path.exists(tf):
2095
- try: os.unlink(tf)
2096
- except: pass
2097
-
2098
- class RestorePhotoRequest(BaseModel):
2099
- image_url: str
2100
- make_16_9: bool = False
2101
-
2102
- @app.post("/restore-photo")
2103
- async def restore_photo_endpoint(request: RestorePhotoRequest):
2104
- """
2105
- Endpoint para restaurar uma foto antiga, colorir e transformar em 4K.
2106
- """
2107
- if upscale_chatbot is None:
2108
- raise HTTPException(status_code=500, detail="Upscale Chatbot não inicializado")
2109
-
2110
- urls_to_download = [request.image_url]
2111
- temp_files = []
2112
-
2113
- try:
2114
- # Baixar a foto original
2115
- for url in urls_to_download:
2116
- try:
2117
- resp = download_file_with_retry(url, max_retries=3)
2118
- ext = ".png" if "png" in resp.headers.get("content-type", "") else ".jpg"
2119
- tf = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
2120
- for chunk in resp.iter_content(chunk_size=8192):
2121
- if chunk:
2122
- tf.write(chunk)
2123
- tf.close()
2124
- temp_files.append(tf.name)
2125
- except Exception as e:
2126
- print(f"Erro ao baixar {url}: {e}")
2127
-
2128
- if len(temp_files) != 1:
2129
- raise HTTPException(status_code=400, detail="Não foi possível baixar a foto fornecida.")
2130
-
2131
- ratio_instruction = "Transforme em 16:9 também." if request.make_16_9 else "Deixe nas proporções originais da foto, NÃO altere a proporção."
2132
-
2133
- prompt = f'''Restaure essa foto antiga e colora ela, mantenha mesmos elementos, posições e tamanhos. Transforme em 4k, ultra nítido.
2134
-
2135
- {ratio_instruction}'''
2136
-
2137
- print(f"🧠 [RestorePhoto] Enviando prompt para Gemini (NANO_BANANA)...")
2138
- result, generated_url = await _try_upscale(upscale_chatbot, temp_files, prompt)
2139
-
2140
- if result is None or generated_url is None:
2141
- raise HTTPException(status_code=500, detail="Falha ao restaurar a foto no Gemini.")
2142
-
2143
- # Carregar cookies para baixar a imagem gerada
2144
- cookies_dict = {}
2145
- cookie_path = os.getenv("COOKIE_PATH", "cookies.json")
2146
- try:
2147
- secure_1psid, secure_1psidts, additional_cookies = load_cookies(cookie_path)
2148
- cookies_dict = additional_cookies.copy()
2149
- cookies_dict['__Secure-1PSID'] = secure_1psid
2150
- cookies_dict['__Secure-1PSIDTS'] = secure_1psidts
2151
- except Exception:
2152
- pass
2153
-
2154
- img_base64, img_content_type, actual_url = await _download_upscaled_image(generated_url, cookies_dict)
2155
-
2156
- return JSONResponse(
2157
- content={
2158
- "image_base64": img_base64,
2159
- "content_type": img_content_type,
2160
- "success": True,
2161
- "generated_url": actual_url
2162
- }
2163
- )
2164
-
2165
- except HTTPException:
2166
- raise
2167
- except Exception as e:
2168
- import traceback
2169
- traceback.print_exc()
2170
- raise HTTPException(status_code=500, detail=f"Erro interno: {str(e)}")
2171
- finally:
2172
- for tf in temp_files:
2173
- if os.path.exists(tf):
2174
- try: os.unlink(tf)
2175
- except: pass
 
437
  return False
438
 
439
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  class GenerateElementsRequest(BaseModel):
441
  video_url: str
442
  context: Optional[str] = None
 
655
 
656
 
657
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  # ==========================================
659
  # GROQ ENDPOINT
660
  # ==========================================
 
1012
  except Exception as e:
1013
  import traceback
1014
  traceback.print_exc()
1015
+ raise HTTPException(status_code=500, detail=str(e))