Update app.py
Browse files
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))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|