Spaces:
Paused
Paused
Upload 4 files
Browse files
app.py
CHANGED
|
@@ -4,7 +4,7 @@ import os
|
|
| 4 |
import re
|
| 5 |
import json
|
| 6 |
import shutil
|
| 7 |
-
from PIL import Image, ImageDraw
|
| 8 |
|
| 9 |
|
| 10 |
# ══════════════════════════════════════════════════════════════════
|
|
@@ -37,43 +37,57 @@ def contar_frames(path):
|
|
| 37 |
# Logo — preparação, posição e preview ao vivo
|
| 38 |
# ══════════════════════════════════════════════════════════════════
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
def preparar_logo(logo_path, vid_w, tamanho_pct, transparencia_pct):
|
| 41 |
"""
|
| 42 |
Abre o logo, redimensiona para tamanho_pct% da largura do vídeo,
|
| 43 |
-
aplica transparencia_pct%
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
"""
|
| 48 |
img = Image.open(logo_path)
|
| 49 |
if img.mode != "RGBA":
|
| 50 |
img = img.convert("RGBA")
|
| 51 |
|
| 52 |
-
# Redimensiona
|
| 53 |
alvo_w = max(1, int(vid_w * tamanho_pct / 100))
|
| 54 |
ratio = alvo_w / img.width
|
| 55 |
alvo_h = max(1, int(img.height * ratio))
|
| 56 |
img = img.resize((alvo_w, alvo_h), Image.LANCZOS)
|
| 57 |
|
| 58 |
-
# Separa canais
|
| 59 |
r_ch, g_ch, b_ch, a_ch = img.split()
|
| 60 |
|
| 61 |
-
# Aplica transparência global (
|
| 62 |
if transparencia_pct > 0:
|
| 63 |
fator = 1.0 - (transparencia_pct / 100.0)
|
| 64 |
a_ch = a_ch.point(lambda x: int(x * fator))
|
| 65 |
|
| 66 |
-
# Zera RGB
|
| 67 |
-
|
| 68 |
-
# overlay converte para yuv420p com chroma subsampling.
|
| 69 |
-
mask_transp = a_ch.point(lambda v: 0 if v == 0 else 255)
|
| 70 |
empty = Image.new("L", img.size, 0)
|
| 71 |
-
r_ch = Image.composite(r_ch, empty,
|
| 72 |
-
g_ch = Image.composite(g_ch, empty,
|
| 73 |
-
b_ch = Image.composite(b_ch, empty,
|
| 74 |
|
| 75 |
img = Image.merge("RGBA", (r_ch, g_ch, b_ch, a_ch))
|
| 76 |
|
|
|
|
|
|
|
|
|
|
| 77 |
temp_path = "/tmp/logo_overlay.png"
|
| 78 |
img.save(temp_path, "PNG", optimize=False)
|
| 79 |
return temp_path, alvo_w, alvo_h
|
|
@@ -128,16 +142,20 @@ def gerar_preview_logo(logo_file, logo_posicao, logo_margem,
|
|
| 128 |
alvo_h = max(1, int(logo.height * ratio))
|
| 129 |
logo = logo.resize((alvo_w, alvo_h), Image.LANCZOS)
|
| 130 |
|
| 131 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
r_ch, g_ch, b_ch, a_ch = logo.split()
|
| 133 |
if logo_transparencia > 0:
|
| 134 |
fator = 1.0 - (logo_transparencia / 100.0)
|
| 135 |
a_ch = a_ch.point(lambda px: int(px * fator))
|
| 136 |
-
|
| 137 |
empty = Image.new("L", logo.size, 0)
|
| 138 |
-
r_ch = Image.composite(r_ch, empty,
|
| 139 |
-
g_ch = Image.composite(g_ch, empty,
|
| 140 |
-
b_ch = Image.composite(b_ch, empty,
|
| 141 |
logo = Image.merge("RGBA", (r_ch, g_ch, b_ch, a_ch))
|
| 142 |
|
| 143 |
escala = PREVIEW_W / 1920
|
|
@@ -315,22 +333,29 @@ def reencode_video(
|
|
| 315 |
overlay_pos = calcular_posicao_logo(
|
| 316 |
logo_posicao, logo_margem, logo_offset_x, logo_offset_y
|
| 317 |
)
|
| 318 |
-
# Correção do "fundo verde
|
| 319 |
-
#
|
| 320 |
-
#
|
| 321 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
if vf_parts:
|
| 323 |
fc = (
|
| 324 |
-
f"[0:v]{','.join(vf_parts)}[base];"
|
| 325 |
-
f"[1:v]format=
|
| 326 |
-
f"[base][logo]overlay={overlay_pos}:format=auto:
|
| 327 |
-
f"
|
|
|
|
| 328 |
)
|
| 329 |
else:
|
| 330 |
fc = (
|
| 331 |
-
f"[
|
| 332 |
-
f"[
|
| 333 |
-
f"
|
|
|
|
|
|
|
| 334 |
)
|
| 335 |
cmd += ["-filter_complex", fc, "-map", "[vout]"]
|
| 336 |
if has_audio:
|
|
|
|
| 4 |
import re
|
| 5 |
import json
|
| 6 |
import shutil
|
| 7 |
+
from PIL import Image, ImageDraw, ImageChops
|
| 8 |
|
| 9 |
|
| 10 |
# ══════════════════════════════════════════════════════════════════
|
|
|
|
| 37 |
# Logo — preparação, posição e preview ao vivo
|
| 38 |
# ══════════════════════════════════════════════════════════════════
|
| 39 |
|
| 40 |
+
def _premultiplicar_alpha(img_rgba):
|
| 41 |
+
"""
|
| 42 |
+
Pré-multiplica os canais RGB pelo canal alpha.
|
| 43 |
+
Após isso, pixels semi-transparentes têm RGB já escalado por alpha,
|
| 44 |
+
o que ELIMINA o halo verde/magenta quando o overlay do FFmpeg
|
| 45 |
+
compõe em yuv420p — não existe mais cor "solta" nas bordas.
|
| 46 |
+
"""
|
| 47 |
+
r_ch, g_ch, b_ch, a_ch = img_rgba.split()
|
| 48 |
+
r_pm = ImageChops.multiply(r_ch, a_ch) # ImageChops.multiply(A,B) = A*B/255
|
| 49 |
+
g_pm = ImageChops.multiply(g_ch, a_ch)
|
| 50 |
+
b_pm = ImageChops.multiply(b_ch, a_ch)
|
| 51 |
+
return Image.merge("RGBA", (r_pm, g_pm, b_pm, a_ch))
|
| 52 |
+
|
| 53 |
+
|
| 54 |
def preparar_logo(logo_path, vid_w, tamanho_pct, transparencia_pct):
|
| 55 |
"""
|
| 56 |
Abre o logo, redimensiona para tamanho_pct% da largura do vídeo,
|
| 57 |
+
aplica transparencia_pct% (0 = opaco, 100 = invisível),
|
| 58 |
+
ZERA RGB onde alpha == 0 e PRÉ-MULTIPLICA alpha em todo o resto.
|
| 59 |
+
O resultado é um PNG sem nenhum pixel colorido "solto" — o overlay
|
| 60 |
+
do FFmpeg pode usar alpha=premultiplied e não gera halo verde.
|
| 61 |
"""
|
| 62 |
img = Image.open(logo_path)
|
| 63 |
if img.mode != "RGBA":
|
| 64 |
img = img.convert("RGBA")
|
| 65 |
|
| 66 |
+
# Redimensiona para a largura alvo no vídeo
|
| 67 |
alvo_w = max(1, int(vid_w * tamanho_pct / 100))
|
| 68 |
ratio = alvo_w / img.width
|
| 69 |
alvo_h = max(1, int(img.height * ratio))
|
| 70 |
img = img.resize((alvo_w, alvo_h), Image.LANCZOS)
|
| 71 |
|
|
|
|
| 72 |
r_ch, g_ch, b_ch, a_ch = img.split()
|
| 73 |
|
| 74 |
+
# 1) Aplica transparência global (reduz o canal alpha proporcionalmente)
|
| 75 |
if transparencia_pct > 0:
|
| 76 |
fator = 1.0 - (transparencia_pct / 100.0)
|
| 77 |
a_ch = a_ch.point(lambda x: int(x * fator))
|
| 78 |
|
| 79 |
+
# 2) Zera RGB onde alpha == 0 (mata matte verde/preto que venha no PNG)
|
| 80 |
+
mask_keep = a_ch.point(lambda v: 0 if v == 0 else 255)
|
|
|
|
|
|
|
| 81 |
empty = Image.new("L", img.size, 0)
|
| 82 |
+
r_ch = Image.composite(r_ch, empty, mask_keep)
|
| 83 |
+
g_ch = Image.composite(g_ch, empty, mask_keep)
|
| 84 |
+
b_ch = Image.composite(b_ch, empty, mask_keep)
|
| 85 |
|
| 86 |
img = Image.merge("RGBA", (r_ch, g_ch, b_ch, a_ch))
|
| 87 |
|
| 88 |
+
# 3) Pré-multiplica alpha — fim do halo verde
|
| 89 |
+
img = _premultiplicar_alpha(img)
|
| 90 |
+
|
| 91 |
temp_path = "/tmp/logo_overlay.png"
|
| 92 |
img.save(temp_path, "PNG", optimize=False)
|
| 93 |
return temp_path, alvo_w, alvo_h
|
|
|
|
| 142 |
alvo_h = max(1, int(logo.height * ratio))
|
| 143 |
logo = logo.resize((alvo_w, alvo_h), Image.LANCZOS)
|
| 144 |
|
| 145 |
+
# Mesma lógica de render final:
|
| 146 |
+
# 1) aplica transparência no alpha
|
| 147 |
+
# 2) zera RGB onde alpha==0 (mata matte verde)
|
| 148 |
+
# 3) NÃO pré-multiplica aqui, pois o alpha_composite do Pillow
|
| 149 |
+
# espera straight alpha e compõe em RGB — já sem halo verde.
|
| 150 |
r_ch, g_ch, b_ch, a_ch = logo.split()
|
| 151 |
if logo_transparencia > 0:
|
| 152 |
fator = 1.0 - (logo_transparencia / 100.0)
|
| 153 |
a_ch = a_ch.point(lambda px: int(px * fator))
|
| 154 |
+
mask_keep = a_ch.point(lambda v: 0 if v == 0 else 255)
|
| 155 |
empty = Image.new("L", logo.size, 0)
|
| 156 |
+
r_ch = Image.composite(r_ch, empty, mask_keep)
|
| 157 |
+
g_ch = Image.composite(g_ch, empty, mask_keep)
|
| 158 |
+
b_ch = Image.composite(b_ch, empty, mask_keep)
|
| 159 |
logo = Image.merge("RGBA", (r_ch, g_ch, b_ch, a_ch))
|
| 160 |
|
| 161 |
escala = PREVIEW_W / 1920
|
|
|
|
| 333 |
overlay_pos = calcular_posicao_logo(
|
| 334 |
logo_posicao, logo_margem, logo_offset_x, logo_offset_y
|
| 335 |
)
|
| 336 |
+
# Correção do "fundo verde" ao redor da logo:
|
| 337 |
+
# 1) O PNG já saiu do Pillow com alpha pré-multiplicado
|
| 338 |
+
# (RGB zerado onde alpha==0, RGB = cor*alpha nos demais).
|
| 339 |
+
# 2) Base do vídeo é convertida para RGBA antes do overlay,
|
| 340 |
+
# então a composição acontece em RGB puro (sem YUV 4:2:0
|
| 341 |
+
# introduzir halo verde/magenta nas bordas).
|
| 342 |
+
# 3) overlay usa alpha=premultiplied (combina com o PNG).
|
| 343 |
+
# 4) format=yuv420p só no fim da cadeia, para o encoder.
|
| 344 |
if vf_parts:
|
| 345 |
fc = (
|
| 346 |
+
f"[0:v]{','.join(vf_parts)},format=rgba[base];"
|
| 347 |
+
f"[1:v]format=rgba[logo];"
|
| 348 |
+
f"[base][logo]overlay={overlay_pos}:format=auto:"
|
| 349 |
+
f"alpha=premultiplied[composed];"
|
| 350 |
+
f"[composed]format=yuv420p[vout]"
|
| 351 |
)
|
| 352 |
else:
|
| 353 |
fc = (
|
| 354 |
+
f"[0:v]format=rgba[base];"
|
| 355 |
+
f"[1:v]format=rgba[logo];"
|
| 356 |
+
f"[base][logo]overlay={overlay_pos}:format=auto:"
|
| 357 |
+
f"alpha=premultiplied[composed];"
|
| 358 |
+
f"[composed]format=yuv420p[vout]"
|
| 359 |
)
|
| 360 |
cmd += ["-filter_complex", fc, "-map", "[vout]"]
|
| 361 |
if has_audio:
|