patocolher commited on
Commit
b3ba734
Β·
verified Β·
1 Parent(s): f3da6ec

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +39 -84
app.py CHANGED
@@ -4,7 +4,7 @@ import os
4
  import re
5
  import json
6
  import shutil
7
- from PIL import Image, ImageDraw, ImageChops
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 _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
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, logo_transparencia):
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
- # 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
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, logo_transparencia,
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, logo_transparencia
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 anti-halo verde) ────────────────
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 "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]"]
@@ -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}% | transp {logo_transparencia}%"
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
- logo_transparencia = gr.Slider(
661
- minimum=0, maximum=100, value=0, step=5,
662
- label="TransparΓͺncia (0 % = opaco Β· 100 % = invisΓ­vel)"
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, logo_transparencia]
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, logo_transparencia,
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
  )