Spaces:
Paused
Paused
Upload app.py
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,59 +37,30 @@ def contar_frames(path):
|
|
| 37 |
# Logo β preparaΓ§Γ£o, posiΓ§Γ£o e preview ao vivo
|
| 38 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 39 |
|
| 40 |
-
def
|
| 41 |
"""
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
"""
|
| 47 |
-
|
| 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 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 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"
|
| 93 |
return temp_path, alvo_w, alvo_h
|
| 94 |
|
| 95 |
|
|
@@ -114,7 +85,7 @@ MARGEM_PREVIEW = 12
|
|
| 114 |
|
| 115 |
def gerar_preview_logo(logo_file, logo_posicao, logo_margem,
|
| 116 |
logo_offset_x, logo_offset_y,
|
| 117 |
-
logo_tamanho,
|
| 118 |
"""Gera um preview PNG mostrando a logo posicionada sobre um fundo simulado."""
|
| 119 |
canvas = Image.new("RGBA", (PREVIEW_W, PREVIEW_H), (30, 30, 30, 255))
|
| 120 |
draw = ImageDraw.Draw(canvas)
|
|
@@ -133,30 +104,19 @@ def gerar_preview_logo(logo_file, logo_posicao, logo_margem,
|
|
| 133 |
return out
|
| 134 |
|
| 135 |
try:
|
| 136 |
-
logo = Image.open(logo_file)
|
| 137 |
-
if logo.mode != "RGBA":
|
| 138 |
-
logo = logo.convert("RGBA")
|
| 139 |
|
| 140 |
alvo_w = max(1, int(PREVIEW_W * logo_tamanho / 100))
|
| 141 |
ratio = alvo_w / logo.width
|
| 142 |
alvo_h = max(1, int(logo.height * ratio))
|
| 143 |
logo = logo.resize((alvo_w, alvo_h), Image.LANCZOS)
|
| 144 |
|
| 145 |
-
#
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 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
|
| 162 |
mp = int(logo_margem * escala)
|
|
@@ -235,7 +195,7 @@ def reencode_video(
|
|
| 235 |
remover_duplicados,
|
| 236 |
# logo
|
| 237 |
logo_file, logo_posicao, logo_margem,
|
| 238 |
-
logo_offset_x, logo_offset_y, logo_tamanho,
|
| 239 |
):
|
| 240 |
if video_file is None:
|
| 241 |
yield None, "β Nenhum vΓdeo enviado!"
|
|
@@ -294,7 +254,7 @@ def reencode_video(
|
|
| 294 |
if tem_logo:
|
| 295 |
try:
|
| 296 |
logo_tmp, _, _ = preparar_logo(
|
| 297 |
-
logo_file, vid_w, logo_tamanho,
|
| 298 |
)
|
| 299 |
except Exception as e:
|
| 300 |
yield None, f"β Erro ao processar logo: {e}"
|
|
@@ -328,33 +288,28 @@ def reencode_video(
|
|
| 328 |
if tem_logo:
|
| 329 |
cmd += ["-i", logo_tmp]
|
| 330 |
|
| 331 |
-
# ββ filter_complex (overlay
|
| 332 |
if tem_logo:
|
| 333 |
overlay_pos = calcular_posicao_logo(
|
| 334 |
logo_posicao, logo_margem, logo_offset_x, logo_offset_y
|
| 335 |
)
|
| 336 |
-
# CorreΓ§Γ£o do
|
| 337 |
-
#
|
| 338 |
-
#
|
| 339 |
-
#
|
| 340 |
-
#
|
| 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}
|
| 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}
|
| 357 |
-
f"alpha=premultiplied[composed];"
|
| 358 |
f"[composed]format=yuv420p[vout]"
|
| 359 |
)
|
| 360 |
cmd += ["-filter_complex", fc, "-map", "[vout]"]
|
|
@@ -494,7 +449,7 @@ def reencode_video(
|
|
| 494 |
if tem_logo:
|
| 495 |
extras.append(
|
| 496 |
f"logo: {logo_posicao.lower()} | "
|
| 497 |
-
f"tam {logo_tamanho}% |
|
| 498 |
)
|
| 499 |
if gpu_fallback:
|
| 500 |
extras.append("β οΈ GPU indisponΓvel β caiu para CPU")
|
|
@@ -657,9 +612,9 @@ with gr.Blocks(title="Super Re-Encoder", theme=gr.themes.Soft(),
|
|
| 657 |
minimum=5, maximum=50, value=15, step=1,
|
| 658 |
label="Tamanho (% da largura do vΓdeo)"
|
| 659 |
)
|
| 660 |
-
|
| 661 |
-
minimum=0, maximum=100, value=
|
| 662 |
-
label="
|
| 663 |
)
|
| 664 |
logo_margem = gr.Slider(
|
| 665 |
minimum=0, maximum=300, value=20, step=5,
|
|
@@ -678,7 +633,7 @@ with gr.Blocks(title="Super Re-Encoder", theme=gr.themes.Soft(),
|
|
| 678 |
# Preview ao vivo reativo a qualquer controle de logo
|
| 679 |
_preview_inputs = [logo_file, logo_posicao, logo_margem,
|
| 680 |
logo_offset_x, logo_offset_y,
|
| 681 |
-
logo_tamanho,
|
| 682 |
for ctrl in _preview_inputs:
|
| 683 |
ctrl.change(
|
| 684 |
fn=gerar_preview_logo,
|
|
@@ -702,7 +657,7 @@ with gr.Blocks(title="Super Re-Encoder", theme=gr.themes.Soft(),
|
|
| 702 |
normalizar_audio,
|
| 703 |
remover_duplicados,
|
| 704 |
logo_file, logo_posicao, logo_margem,
|
| 705 |
-
logo_offset_x, logo_offset_y, logo_tamanho,
|
| 706 |
],
|
| 707 |
outputs=[out_video, status],
|
| 708 |
)
|
|
|
|
| 4 |
import re
|
| 5 |
import json
|
| 6 |
import shutil
|
| 7 |
+
from PIL import Image, ImageDraw
|
| 8 |
|
| 9 |
|
| 10 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 37 |
# Logo β preparaΓ§Γ£o, posiΓ§Γ£o e preview ao vivo
|
| 38 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 39 |
|
| 40 |
+
def preparar_logo(logo_path, vid_w, tamanho_pct, opacidade_pct):
|
| 41 |
"""
|
| 42 |
+
Abre o logo, redimensiona para tamanho_pct% da largura do vΓdeo
|
| 43 |
+
e escala o canal alpha por opacidade_pct (100 = totalmente opaco,
|
| 44 |
+
0 = invisΓvel). Mesmo tratamento simples que o radar-virumania usa.
|
| 45 |
+
A eliminaΓ§Γ£o do halo verde Γ© feita na composiΓ§Γ£o FFmpeg (em RGBA).
|
| 46 |
"""
|
| 47 |
+
img = Image.open(logo_path).convert("RGBA")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
+
# Redimensiona proporcionalmente Γ largura alvo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
alvo_w = max(1, int(vid_w * tamanho_pct / 100))
|
| 51 |
ratio = alvo_w / img.width
|
| 52 |
alvo_h = max(1, int(img.height * ratio))
|
| 53 |
img = img.resize((alvo_w, alvo_h), Image.LANCZOS)
|
| 54 |
|
| 55 |
+
# Escala o canal alpha pela opacidade escolhida
|
| 56 |
+
if opacidade_pct < 100:
|
| 57 |
+
r, g, b, a = img.split()
|
| 58 |
+
fator = opacidade_pct / 100.0
|
| 59 |
+
a = a.point(lambda x: int(x * fator))
|
| 60 |
+
img = Image.merge("RGBA", (r, g, b, a))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
temp_path = "/tmp/logo_overlay.png"
|
| 63 |
+
img.save(temp_path, "PNG")
|
| 64 |
return temp_path, alvo_w, alvo_h
|
| 65 |
|
| 66 |
|
|
|
|
| 85 |
|
| 86 |
def gerar_preview_logo(logo_file, logo_posicao, logo_margem,
|
| 87 |
logo_offset_x, logo_offset_y,
|
| 88 |
+
logo_tamanho, logo_opacidade):
|
| 89 |
"""Gera um preview PNG mostrando a logo posicionada sobre um fundo simulado."""
|
| 90 |
canvas = Image.new("RGBA", (PREVIEW_W, PREVIEW_H), (30, 30, 30, 255))
|
| 91 |
draw = ImageDraw.Draw(canvas)
|
|
|
|
| 104 |
return out
|
| 105 |
|
| 106 |
try:
|
| 107 |
+
logo = Image.open(logo_file).convert("RGBA")
|
|
|
|
|
|
|
| 108 |
|
| 109 |
alvo_w = max(1, int(PREVIEW_W * logo_tamanho / 100))
|
| 110 |
ratio = alvo_w / logo.width
|
| 111 |
alvo_h = max(1, int(logo.height * ratio))
|
| 112 |
logo = logo.resize((alvo_w, alvo_h), Image.LANCZOS)
|
| 113 |
|
| 114 |
+
# Aplica opacidade (idΓͺntico ao radar-virumania)
|
| 115 |
+
if logo_opacidade < 100:
|
| 116 |
+
r, g, b, a = logo.split()
|
| 117 |
+
fator = logo_opacidade / 100.0
|
| 118 |
+
a = a.point(lambda px: int(px * fator))
|
| 119 |
+
logo = Image.merge("RGBA", (r, g, b, a))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
escala = PREVIEW_W / 1920
|
| 122 |
mp = int(logo_margem * escala)
|
|
|
|
| 195 |
remover_duplicados,
|
| 196 |
# logo
|
| 197 |
logo_file, logo_posicao, logo_margem,
|
| 198 |
+
logo_offset_x, logo_offset_y, logo_tamanho, logo_opacidade,
|
| 199 |
):
|
| 200 |
if video_file is None:
|
| 201 |
yield None, "β Nenhum vΓdeo enviado!"
|
|
|
|
| 254 |
if tem_logo:
|
| 255 |
try:
|
| 256 |
logo_tmp, _, _ = preparar_logo(
|
| 257 |
+
logo_file, vid_w, logo_tamanho, logo_opacidade
|
| 258 |
)
|
| 259 |
except Exception as e:
|
| 260 |
yield None, f"β Erro ao processar logo: {e}"
|
|
|
|
| 288 |
if tem_logo:
|
| 289 |
cmd += ["-i", logo_tmp]
|
| 290 |
|
| 291 |
+
# ββ filter_complex (overlay em RGBA β yuv420p no fim) βββββββ
|
| 292 |
if tem_logo:
|
| 293 |
overlay_pos = calcular_posicao_logo(
|
| 294 |
logo_posicao, logo_margem, logo_offset_x, logo_offset_y
|
| 295 |
)
|
| 296 |
+
# CorreΓ§Γ£o do halo verde:
|
| 297 |
+
# BASE e LOGO sΓ£o convertidos para RGBA antes do overlay, entΓ£o
|
| 298 |
+
# a composiΓ§Γ£o acontece em RGB puro (sem YUV 4:2:0 introduzir
|
| 299 |
+
# sangramento de chroma nas bordas). SΓ³ no fim convertemos para
|
| 300 |
+
# yuv420p para o encoder. Sem premultiply β straight alpha puro.
|
|
|
|
|
|
|
|
|
|
| 301 |
if vf_parts:
|
| 302 |
fc = (
|
| 303 |
f"[0:v]{','.join(vf_parts)},format=rgba[base];"
|
| 304 |
f"[1:v]format=rgba[logo];"
|
| 305 |
+
f"[base][logo]overlay={overlay_pos}[composed];"
|
|
|
|
| 306 |
f"[composed]format=yuv420p[vout]"
|
| 307 |
)
|
| 308 |
else:
|
| 309 |
fc = (
|
| 310 |
f"[0:v]format=rgba[base];"
|
| 311 |
f"[1:v]format=rgba[logo];"
|
| 312 |
+
f"[base][logo]overlay={overlay_pos}[composed];"
|
|
|
|
| 313 |
f"[composed]format=yuv420p[vout]"
|
| 314 |
)
|
| 315 |
cmd += ["-filter_complex", fc, "-map", "[vout]"]
|
|
|
|
| 449 |
if tem_logo:
|
| 450 |
extras.append(
|
| 451 |
f"logo: {logo_posicao.lower()} | "
|
| 452 |
+
f"tam {logo_tamanho}% | opacidade {logo_opacidade}%"
|
| 453 |
)
|
| 454 |
if gpu_fallback:
|
| 455 |
extras.append("β οΈ GPU indisponΓvel β caiu para CPU")
|
|
|
|
| 612 |
minimum=5, maximum=50, value=15, step=1,
|
| 613 |
label="Tamanho (% da largura do vΓdeo)"
|
| 614 |
)
|
| 615 |
+
logo_opacidade = gr.Slider(
|
| 616 |
+
minimum=0, maximum=100, value=30, step=5,
|
| 617 |
+
label="Opacidade (100 % = opaco Β· 0 % = invisΓvel)"
|
| 618 |
)
|
| 619 |
logo_margem = gr.Slider(
|
| 620 |
minimum=0, maximum=300, value=20, step=5,
|
|
|
|
| 633 |
# Preview ao vivo reativo a qualquer controle de logo
|
| 634 |
_preview_inputs = [logo_file, logo_posicao, logo_margem,
|
| 635 |
logo_offset_x, logo_offset_y,
|
| 636 |
+
logo_tamanho, logo_opacidade]
|
| 637 |
for ctrl in _preview_inputs:
|
| 638 |
ctrl.change(
|
| 639 |
fn=gerar_preview_logo,
|
|
|
|
| 657 |
normalizar_audio,
|
| 658 |
remover_duplicados,
|
| 659 |
logo_file, logo_posicao, logo_margem,
|
| 660 |
+
logo_offset_x, logo_offset_y, logo_tamanho, logo_opacidade,
|
| 661 |
],
|
| 662 |
outputs=[out_video, status],
|
| 663 |
)
|