File size: 18,087 Bytes
73c0719
 
 
 
 
 
 
 
 
153e27b
73c0719
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153e27b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73c0719
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153e27b
73c0719
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153e27b
 
 
73c0719
b218964
73c0719
 
153e27b
 
73c0719
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153e27b
 
73c0719
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
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
import re

# 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 clean_name_parentheses(name: str) -> str:
    """
    Remove parênteses e seu conteúdo do nome e remove espaços extras
    Exemplo: "Charlie Kirk (Activist)" -> "Charlie Kirk"
    """
    if not name:
        return name
    
    # Remove parênteses e todo o conteúdo dentro deles
    cleaned_name = re.sub(r'\s*\([^)]*\)', '', name)
    
    # Remove espaços extras no início, fim e múltiplos espaços no meio
    cleaned_name = re.sub(r'\s+', ' ', cleaned_name.strip())
    
    return cleaned_name

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

@router.get("/search/wikipedia")
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)}")

@router.get("/wikidata/dates")
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.
    Remove automaticamente parênteses do nome para manter a URL limpa.
    """
    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"
        
        # Limpar nome removendo parênteses
        clean_name = clean_name_parentheses(name) if name else None
        
        # Construir URL do memoriam
        base_url = "https://habulaj-recurve-api-img.hf.space/cover/memoriam"
        params = []
        
        if clean_name:
            params.append(f"name={quote(clean_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,  # Nome original fornecido
            "clean_name": clean_name,  # Nome limpo sem parênteses
            "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)}")

@router.get("/cover/memoriam")
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)}")