Spaces:
Runtime error
Runtime error
| from fastapi import APIRouter, Query, HTTPException | |
| from fastapi.responses import StreamingResponse | |
| from PIL import Image, ImageDraw, ImageEnhance, ImageFont | |
| from io import BytesIO | |
| import requests | |
| from typing import Optional, List, Dict | |
| import logging | |
| from urllib.parse import quote | |
| from datetime import datetime | |
| # Configurar logging | |
| logging.basicConfig(level=logging.INFO) | |
| log = logging.getLogger("memoriam-api") | |
| router = APIRouter() | |
| def download_image_from_url(url: str) -> Image.Image: | |
| headers = { | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36" | |
| } | |
| response = requests.get(url, headers=headers) | |
| if response.status_code != 200: | |
| raise HTTPException(status_code=400, detail=f"Imagem não pôde ser baixada. Código {response.status_code}") | |
| try: | |
| return Image.open(BytesIO(response.content)).convert("RGB") | |
| except Exception as e: | |
| raise HTTPException(status_code=400, detail=f"Erro ao abrir imagem: {str(e)}") | |
| def resize_and_crop_to_fill(img: Image.Image, target_width: int, target_height: int) -> Image.Image: | |
| img_ratio = img.width / img.height | |
| target_ratio = target_width / target_height | |
| if img_ratio > target_ratio: | |
| scale_height = target_height | |
| scale_width = int(scale_height * img_ratio) | |
| else: | |
| scale_width = target_width | |
| scale_height = int(scale_width / img_ratio) | |
| img_resized = img.resize((scale_width, scale_height), Image.LANCZOS) | |
| left = (scale_width - target_width) // 2 | |
| top = (scale_height - target_height) // 2 | |
| right = left + target_width | |
| bottom = top + target_height | |
| return img_resized.crop((left, top, right, bottom)) | |
| def create_bottom_black_gradient(width: int, height: int) -> Image.Image: | |
| """Cria um gradiente preto suave que vai do topo transparente até a metade da imagem preto""" | |
| gradient = Image.new("RGBA", (width, height), color=(0, 0, 0, 0)) | |
| draw = ImageDraw.Draw(gradient) | |
| for y in range(height): | |
| # Gradiente mais suave que começa transparente e vai até metade da imagem | |
| ratio = y / height | |
| if ratio <= 0.6: | |
| # Primeira parte: totalmente transparente | |
| alpha = 0 | |
| elif ratio <= 0.75: | |
| # Transição muito suave (60% a 75% da altura) | |
| alpha = int(80 * (ratio - 0.6) / 0.15) | |
| else: | |
| # Final suave (75% a 100% da altura) | |
| alpha = int(80 + 50 * (ratio - 0.75) / 0.25) | |
| # Usar preto puro (0, 0, 0) com alpha mais baixo | |
| draw.line([(0, y), (width, y)], fill=(0, 0, 0, alpha)) | |
| return gradient | |
| def create_top_black_gradient(width: int, height: int) -> Image.Image: | |
| """Cria um gradiente preto suave que vai do fundo transparente até a metade da imagem preto""" | |
| gradient = Image.new("RGBA", (width, height), color=(0, 0, 0, 0)) | |
| draw = ImageDraw.Draw(gradient) | |
| for y in range(height): | |
| # Gradiente mais suave que começa preto e vai até metade da imagem | |
| ratio = y / height | |
| if ratio <= 0.25: | |
| # Primeira parte suave (0% a 25% da altura) | |
| alpha = int(80 + 50 * (0.25 - ratio) / 0.25) | |
| elif ratio <= 0.4: | |
| # Transição muito suave (25% a 40% da altura) | |
| alpha = int(80 * (0.4 - ratio) / 0.15) | |
| else: | |
| # Segunda parte: totalmente transparente | |
| alpha = 0 | |
| # Usar preto puro (0, 0, 0) com alpha mais baixo | |
| draw.line([(0, y), (width, y)], fill=(0, 0, 0, alpha)) | |
| return gradient | |
| def draw_text_left_aligned(draw: ImageDraw.Draw, text: str, x: int, y: int, font_path: str, font_size: int): | |
| """Desenha texto alinhado à esquerda com especificações exatas""" | |
| try: | |
| font = ImageFont.truetype(font_path, font_size) | |
| except Exception: | |
| font = ImageFont.load_default() | |
| # Espaçamento entre letras 0% e cor branca | |
| draw.text((x, y), text, font=font, fill=(255, 255, 255), spacing=0) | |
| def search_wikipedia(name: str) -> List[Dict]: | |
| """ | |
| Busca nomes na Wikipedia e retorna lista com foto, nome completo e wikibase_item | |
| """ | |
| try: | |
| # Primeira busca para obter dados básicos e foto | |
| search_url = "https://en.wikipedia.org/w/rest.php/v1/search/title" | |
| search_params = { | |
| "q": name, | |
| "limit": 5 # Limite de resultados | |
| } | |
| headers = { | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36" | |
| } | |
| response = requests.get(search_url, params=search_params, headers=headers) | |
| response.raise_for_status() | |
| search_data = response.json() | |
| results = [] | |
| for page in search_data.get("pages", []): | |
| title = page.get("title", "") | |
| description = page.get("description", "") | |
| thumbnail = page.get("thumbnail", {}) | |
| # Obter wikibase_item usando a API de props | |
| wikibase_item = None | |
| try: | |
| props_url = "https://en.wikipedia.org/w/api.php" | |
| props_params = { | |
| "action": "query", | |
| "prop": "pageprops", | |
| "titles": title, | |
| "format": "json" | |
| } | |
| props_response = requests.get(props_url, params=props_params, headers=headers) | |
| props_response.raise_for_status() | |
| props_data = props_response.json() | |
| pages = props_data.get("query", {}).get("pages", {}) | |
| for page_id, page_data in pages.items(): | |
| pageprops = page_data.get("pageprops", {}) | |
| wikibase_item = pageprops.get("wikibase_item") | |
| break | |
| except Exception as e: | |
| log.warning(f"Erro ao obter wikibase_item para {title}: {e}") | |
| # Construir URL completa da imagem | |
| image_url = None | |
| if thumbnail and thumbnail.get("url"): | |
| thumb_url = thumbnail["url"] | |
| # A URL vem como //upload.wikimedia... então precisa adicionar https: | |
| if thumb_url.startswith("//"): | |
| image_url = f"https:{thumb_url}" | |
| # Converter para versão de tamanho maior (remover o /60px- e usar tamanho original) | |
| image_url = image_url.replace("/60px-", "/400px-") | |
| else: | |
| image_url = thumb_url | |
| result = { | |
| "name": title, | |
| "description": description, | |
| "image_url": image_url, | |
| "wikibase_item": wikibase_item | |
| } | |
| results.append(result) | |
| return results | |
| except Exception as e: | |
| log.error(f"Erro na busca Wikipedia: {e}") | |
| raise HTTPException(status_code=500, detail=f"Erro ao buscar na Wikipedia: {str(e)}") | |
| def get_wikidata_dates(wikibase_item: str) -> Dict[str, Optional[str]]: | |
| """ | |
| Consulta o Wikidata para obter datas de nascimento (P569) e falecimento (P570) | |
| """ | |
| try: | |
| if not wikibase_item or not wikibase_item.startswith('Q'): | |
| return {"birth": None, "death": None} | |
| url = f"https://www.wikidata.org/wiki/Special:EntityData/{wikibase_item}.json" | |
| headers = { | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36" | |
| } | |
| response = requests.get(url, headers=headers) | |
| response.raise_for_status() | |
| data = response.json() | |
| # Navegar até as claims | |
| entities = data.get("entities", {}) | |
| entity_data = entities.get(wikibase_item, {}) | |
| claims = entity_data.get("claims", {}) | |
| birth_date = None | |
| death_date = None | |
| # Extrair data de nascimento (P569) | |
| if "P569" in claims: | |
| birth_claims = claims["P569"] | |
| for claim in birth_claims: | |
| try: | |
| mainsnak = claim.get("mainsnak", {}) | |
| datavalue = mainsnak.get("datavalue", {}) | |
| value = datavalue.get("value", {}) | |
| time = value.get("time") | |
| if time: | |
| # Formato: +1943-08-17T00:00:00Z | |
| year = time.split("-")[0].replace("+", "") | |
| birth_date = year | |
| break | |
| except Exception as e: | |
| log.warning(f"Erro ao processar data de nascimento: {e}") | |
| continue | |
| # Extrair data de falecimento (P570) | |
| if "P570" in claims: | |
| death_claims = claims["P570"] | |
| for claim in death_claims: | |
| try: | |
| mainsnak = claim.get("mainsnak", {}) | |
| datavalue = mainsnak.get("datavalue", {}) | |
| value = datavalue.get("value", {}) | |
| time = value.get("time") | |
| if time: | |
| # Formato: +2023-01-15T00:00:00Z | |
| year = time.split("-")[0].replace("+", "") | |
| death_date = year | |
| break | |
| except Exception as e: | |
| log.warning(f"Erro ao processar data de falecimento: {e}") | |
| continue | |
| return {"birth": birth_date, "death": death_date} | |
| except Exception as e: | |
| log.error(f"Erro ao consultar Wikidata para {wikibase_item}: {e}") | |
| return {"birth": None, "death": None} | |
| def create_canvas(image_url: Optional[str], name: Optional[str], birth: Optional[str], death: Optional[str], text_position: str = "bottom") -> BytesIO: | |
| # Dimensões fixas para Instagram | |
| width = 1080 | |
| height = 1350 | |
| canvas = Image.new("RGBA", (width, height), (0, 0, 0, 0)) # Fundo transparente | |
| # Carregar e processar imagem de fundo se fornecida | |
| if image_url: | |
| try: | |
| img = download_image_from_url(image_url) | |
| img_bw = ImageEnhance.Color(img).enhance(0.0).convert("RGBA") | |
| filled_img = resize_and_crop_to_fill(img_bw, width, height) | |
| canvas.paste(filled_img, (0, 0)) | |
| except Exception as e: | |
| log.warning(f"Erro ao carregar imagem: {e}") | |
| # Aplicar gradiente baseado na posição do texto | |
| if text_position.lower() == "top": | |
| gradient_overlay = create_top_black_gradient(width, height) | |
| else: # bottom | |
| gradient_overlay = create_bottom_black_gradient(width, height) | |
| canvas = Image.alpha_composite(canvas, gradient_overlay) | |
| # Adicionar logo no canto inferior direito com opacidade | |
| try: | |
| logo = Image.open("recurve.png").convert("RGBA") | |
| logo_resized = logo.resize((120, 22)) | |
| # Aplicar opacidade à logo | |
| logo_with_opacity = Image.new("RGBA", logo_resized.size) | |
| logo_with_opacity.paste(logo_resized, (0, 0)) | |
| # Reduzir opacidade | |
| logo_alpha = logo_with_opacity.split()[-1].point(lambda x: int(x * 0.42)) # 42% de opacidade | |
| logo_with_opacity.putalpha(logo_alpha) | |
| logo_padding = 40 | |
| logo_x = width - 120 - logo_padding | |
| logo_y = height - 22 - logo_padding | |
| canvas.paste(logo_with_opacity, (logo_x, logo_y), logo_with_opacity) | |
| except Exception as e: | |
| log.warning(f"Erro ao carregar a logo: {e}") | |
| draw = ImageDraw.Draw(canvas) | |
| # Configurar posições baseadas no text_position | |
| text_x = 80 # Alinhamento à esquerda com margem | |
| if text_position.lower() == "top": | |
| dates_y = 100 | |
| name_y = dates_y + 36 + 6 # Ano + espaçamento de 6px + nome | |
| else: # bottom | |
| dates_y = height - 250 | |
| name_y = dates_y + 36 + 6 # Ano + espaçamento de 6px + nome | |
| # Desenhar datas primeiro (se fornecidas) | |
| if birth or death: | |
| font_path_regular = "fonts/AGaramondPro-Regular.ttf" | |
| # Construir texto das datas | |
| dates_text = "" | |
| if birth and death: | |
| dates_text = f"{birth} - {death}" | |
| elif birth: | |
| dates_text = f"{birth}" | |
| elif death: | |
| dates_text = f"- {death}" | |
| if dates_text: | |
| draw_text_left_aligned(draw, dates_text, text_x, dates_y, font_path_regular, 36) | |
| # Desenhar nome abaixo das datas | |
| if name: | |
| font_path = "fonts/AGaramondPro-BoldItalic.ttf" | |
| draw_text_left_aligned(draw, name, text_x, name_y, font_path, 87) | |
| buffer = BytesIO() | |
| canvas.save(buffer, format="PNG") | |
| buffer.seek(0) | |
| return buffer | |
| def search_wikipedia_names( | |
| name: str = Query(..., description="Nome para buscar na Wikipedia") | |
| ): | |
| """ | |
| Busca nomes na Wikipedia e retorna lista com foto, nome completo e wikibase_item. | |
| Retorna até 5 resultados ordenados por relevância. | |
| """ | |
| if not name or len(name.strip()) < 2: | |
| raise HTTPException(status_code=400, detail="Nome deve ter pelo menos 2 caracteres") | |
| try: | |
| results = search_wikipedia(name.strip()) | |
| return { | |
| "query": name, | |
| "results": results, | |
| "total": len(results) | |
| } | |
| except Exception as e: | |
| log.error(f"Erro na busca: {e}") | |
| raise HTTPException(status_code=500, detail=f"Erro ao buscar: {str(e)}") | |
| def get_wikidata_person_info( | |
| wikibase_item: str = Query(..., description="ID do Wikidata (ex: Q10304982)"), | |
| name: Optional[str] = Query(None, description="Nome da pessoa (opcional)"), | |
| image_url: Optional[str] = Query(None, description="URL da imagem (opcional, padrão: placeholder)") | |
| ): | |
| """ | |
| Consulta o Wikidata para obter datas de nascimento e falecimento, | |
| e retorna URL formatada para o endpoint de memoriam. | |
| Se death_year for null/vazio, usa o ano atual por padrão. | |
| """ | |
| if not wikibase_item or not wikibase_item.startswith('Q'): | |
| raise HTTPException(status_code=400, detail="wikibase_item deve ser um ID válido do Wikidata (ex: Q10304982)") | |
| try: | |
| # Obter datas do Wikidata | |
| dates = get_wikidata_dates(wikibase_item) | |
| birth_year = dates.get("birth") | |
| death_year = dates.get("death") | |
| # Se death_year estiver vazio/null, usar o ano atual | |
| if not death_year: | |
| death_year = str(datetime.now().year) | |
| # Usar placeholder como padrão se image_url não fornecida | |
| if not image_url: | |
| image_url = "https://placehold.co/1080x1350.png" | |
| # Construir URL do memoriam | |
| base_url = "https://habulaj-newapi-clone3.hf.space/cover/memoriam" | |
| params = [] | |
| if name: | |
| params.append(f"name={quote(name)}") | |
| if birth_year: | |
| params.append(f"birth={birth_year}") | |
| if death_year: | |
| params.append(f"death={death_year}") | |
| if image_url: | |
| params.append(f"image_url={quote(image_url)}") | |
| # Sempre adicionar text_position=bottom como padrão | |
| params.append("text_position=bottom") | |
| # Montar URL final | |
| memoriam_url = base_url + "?" + "&".join(params) | |
| return { | |
| "wikibase_item": wikibase_item, | |
| "name": name, | |
| "image_url": image_url, | |
| "birth_year": birth_year, | |
| "death_year": death_year, | |
| "memoriam_url": memoriam_url, | |
| "dates_found": { | |
| "birth": birth_year is not None, | |
| "death": dates.get("death") is not None # Original death from Wikidata | |
| }, | |
| "death_year_source": "wikidata" if dates.get("death") else "current_year" | |
| } | |
| except Exception as e: | |
| log.error(f"Erro ao processar informações: {e}") | |
| raise HTTPException(status_code=500, detail=f"Erro ao processar: {str(e)}") | |
| def get_memoriam_image( | |
| image_url: Optional[str] = Query(None, description="URL da imagem de fundo"), | |
| name: Optional[str] = Query(None, description="Nome (será exibido em maiúsculas)"), | |
| birth: Optional[str] = Query(None, description="Ano de nascimento (ex: 1943)"), | |
| death: Optional[str] = Query(None, description="Ano de falecimento (ex: 2023)"), | |
| text_position: str = Query("bottom", description="Posição do texto: 'top' ou 'bottom'") | |
| ): | |
| """ | |
| Gera imagem de memoriam no formato 1080x1350 (Instagram). | |
| Todos os parâmetros são opcionais, mas recomenda-se fornecer pelo menos o nome. | |
| O gradiente será aplicado baseado na posição do texto (top ou bottom). | |
| """ | |
| try: | |
| buffer = create_canvas(image_url, name, birth, death, text_position) | |
| return StreamingResponse(buffer, media_type="image/png") | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Erro ao gerar imagem: {str(e)}") |