Spaces:
Paused
Paused
Upload 2 files
Browse files- Dockerfile +16 -37
- app.py +31 -36
Dockerfile
CHANGED
|
@@ -1,19 +1,19 @@
|
|
| 1 |
-
|
| 2 |
-
FROM nvidia/cuda:12.4.1-devel-ubuntu22.04 AS builder
|
| 3 |
|
| 4 |
ENV DEBIAN_FRONTEND=noninteractive
|
| 5 |
|
|
|
|
| 6 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
build-essential nasm yasm bzip2 git wget \
|
| 8 |
-
|
| 9 |
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
|
| 11 |
-
#
|
| 12 |
RUN git clone --depth 1 https://git.videolan.org/git/ffmpeg/nv-codec-headers.git /tmp/nv-codec-headers \
|
| 13 |
&& make -C /tmp/nv-codec-headers install \
|
| 14 |
&& rm -rf /tmp/nv-codec-headers
|
| 15 |
|
| 16 |
-
# Compila
|
| 17 |
RUN wget -q -O /tmp/ffmpeg.tar.bz2 https://ffmpeg.org/releases/ffmpeg-7.1.1.tar.bz2 \
|
| 18 |
&& tar xf /tmp/ffmpeg.tar.bz2 -C /tmp \
|
| 19 |
&& rm /tmp/ffmpeg.tar.bz2 \
|
|
@@ -24,22 +24,11 @@ RUN wget -q -O /tmp/ffmpeg.tar.bz2 https://ffmpeg.org/releases/ffmpeg-7.1.1.tar.
|
|
| 24 |
--enable-nonfree \
|
| 25 |
--disable-everything \
|
| 26 |
--enable-encoder=libx264,libx265,libfdk_aac,aac,h264_nvenc,hevc_nvenc \
|
| 27 |
-
--enable-decoder=h264,hevc,aac,mp3,
|
| 28 |
--enable-hwaccel=h264_nvdec,hevc_nvdec \
|
| 29 |
-
--enable-
|
| 30 |
-
--
|
| 31 |
-
--
|
| 32 |
-
--enable-protocol=file,pipe \
|
| 33 |
-
--enable-libx264 \
|
| 34 |
-
--enable-libx265 \
|
| 35 |
-
--enable-libfdk-aac \
|
| 36 |
-
--enable-nvenc \
|
| 37 |
-
--enable-nvdec \
|
| 38 |
-
--enable-cuda \
|
| 39 |
-
--enable-zlib \
|
| 40 |
-
--enable-cuvid \
|
| 41 |
-
--extra-cflags="-O2 -pipe -I/usr/local/cuda/include" \
|
| 42 |
-
--extra-ldflags="-s -L/usr/local/cuda/lib64" \
|
| 43 |
--disable-doc \
|
| 44 |
--disable-htmlpages \
|
| 45 |
--disable-manpages \
|
|
@@ -49,22 +38,22 @@ RUN wget -q -O /tmp/ffmpeg.tar.bz2 https://ffmpeg.org/releases/ffmpeg-7.1.1.tar.
|
|
| 49 |
&& make install \
|
| 50 |
&& rm -rf /tmp/ffmpeg-7.1.1
|
| 51 |
|
| 52 |
-
#
|
| 53 |
FROM nvidia/cuda:12.4.1-runtime-ubuntu22.04
|
| 54 |
|
| 55 |
ENV DEBIAN_FRONTEND=noninteractive
|
| 56 |
|
| 57 |
-
#
|
| 58 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 59 |
python3 python3-pip ca-certificates \
|
| 60 |
-
|
| 61 |
&& rm -rf /var/lib/apt/lists/*
|
| 62 |
|
| 63 |
-
# Copia
|
| 64 |
-
COPY --from=builder /usr/local/bin/ffmpeg
|
| 65 |
COPY --from=builder /usr/local/bin/ffprobe /usr/local/bin/ffprobe
|
| 66 |
|
| 67 |
-
#
|
| 68 |
RUN set -e; \
|
| 69 |
echo "--- versΓ£o ---"; ffmpeg -version | head -1; \
|
| 70 |
echo "--- encoders ---"; \
|
|
@@ -73,7 +62,7 @@ RUN set -e; \
|
|
| 73 |
echo " β
encoder $c"; \
|
| 74 |
done; \
|
| 75 |
echo "--- decoders ---"; \
|
| 76 |
-
for c in h264 hevc aac mp3 png mjpeg; do \
|
| 77 |
ffmpeg -decoders 2>/dev/null | grep -q " $c " || (echo "β decoder ausente: $c" && exit 1); \
|
| 78 |
echo " β
decoder $c"; \
|
| 79 |
done; \
|
|
@@ -83,13 +72,3 @@ RUN set -e; \
|
|
| 83 |
echo " β
filtro $f"; \
|
| 84 |
done; \
|
| 85 |
echo "β
ffmpeg OK β todos os componentes presentes"
|
| 86 |
-
|
| 87 |
-
WORKDIR /app
|
| 88 |
-
COPY app.py .
|
| 89 |
-
|
| 90 |
-
RUN pip3 install --no-cache-dir gradio
|
| 91 |
-
|
| 92 |
-
EXPOSE 7860
|
| 93 |
-
ENV PYTHONUNBUFFERED=1
|
| 94 |
-
|
| 95 |
-
CMD ["python3", "app.py"]
|
|
|
|
| 1 |
+
FROM nvidia/cuda:12.4.1-ubuntu22.04
|
|
|
|
| 2 |
|
| 3 |
ENV DEBIAN_FRONTEND=noninteractive
|
| 4 |
|
| 5 |
+
# Instala dependΓͺncias necessΓ‘rias
|
| 6 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
build-essential nasm yasm bzip2 git wget \
|
| 8 |
+
libx264-163 libx265-199 libnuma1 zlib1g \
|
| 9 |
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
|
| 11 |
+
# Instala headers do NVENC (necessΓ‘rio para h264_nvenc/hevc_nvenc)
|
| 12 |
RUN git clone --depth 1 https://git.videolan.org/git/ffmpeg/nv-codec-headers.git /tmp/nv-codec-headers \
|
| 13 |
&& make -C /tmp/nv-codec-headers install \
|
| 14 |
&& rm -rf /tmp/nv-codec-headers
|
| 15 |
|
| 16 |
+
# Compila FFmpeg com NVENC
|
| 17 |
RUN wget -q -O /tmp/ffmpeg.tar.bz2 https://ffmpeg.org/releases/ffmpeg-7.1.1.tar.bz2 \
|
| 18 |
&& tar xf /tmp/ffmpeg.tar.bz2 -C /tmp \
|
| 19 |
&& rm /tmp/ffmpeg.tar.bz2 \
|
|
|
|
| 24 |
--enable-nonfree \
|
| 25 |
--disable-everything \
|
| 26 |
--enable-encoder=libx264,libx265,libfdk_aac,aac,h264_nvenc,hevc_nvenc \
|
| 27 |
+
--enable-decoder=h264,hevc,aac,mp3,mp4,png,mjpeg \
|
| 28 |
--enable-hwaccel=h264_nvdec,hevc_nvdec \
|
| 29 |
+
--enable-ncv=1 --enable-ndc=1 \
|
| 30 |
+
--extra-cflags="-I/usr/local/cuda/include" \
|
| 31 |
+
--extra-ldflags="-L/usr/local/cuda/lib64" \
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
--disable-doc \
|
| 33 |
--disable-htmlpages \
|
| 34 |
--disable-manpages \
|
|
|
|
| 38 |
&& make install \
|
| 39 |
&& rm -rf /tmp/ffmpeg-7.1.1
|
| 40 |
|
| 41 |
+
# Stage 2: imagem final
|
| 42 |
FROM nvidia/cuda:12.4.1-runtime-ubuntu22.04
|
| 43 |
|
| 44 |
ENV DEBIAN_FRONTEND=noninteractive
|
| 45 |
|
| 46 |
+
# Instala apenas as librerias de runtime necessΓ‘rias
|
| 47 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 48 |
python3 python3-pip ca-certificates \
|
| 49 |
+
libx264-163 libx265-199 libnuma1 zlib1g \
|
| 50 |
&& rm -rf /var/lib/apt/lists/*
|
| 51 |
|
| 52 |
+
# Copia o binΓ‘rio do stage 1
|
| 53 |
+
COPY --from=builder /usr/local/bin/ffmpeg /usr/local/bin/ffmpeg
|
| 54 |
COPY --from=builder /usr/local/bin/ffprobe /usr/local/bin/ffprobe
|
| 55 |
|
| 56 |
+
# Valida que todos os componentes estΓ£o presentes
|
| 57 |
RUN set -e; \
|
| 58 |
echo "--- versΓ£o ---"; ffmpeg -version | head -1; \
|
| 59 |
echo "--- encoders ---"; \
|
|
|
|
| 62 |
echo " β
encoder $c"; \
|
| 63 |
done; \
|
| 64 |
echo "--- decoders ---"; \
|
| 65 |
+
for c in h264 hevc aac mp3 mp4 png mjpeg; do \
|
| 66 |
ffmpeg -decoders 2>/dev/null | grep -q " $c " || (echo "β decoder ausente: $c" && exit 1); \
|
| 67 |
echo " β
decoder $c"; \
|
| 68 |
done; \
|
|
|
|
| 72 |
echo " β
filtro $f"; \
|
| 73 |
done; \
|
| 74 |
echo "β
ffmpeg OK β todos os componentes presentes"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -18,16 +18,15 @@ def gpu_disponivel():
|
|
| 18 |
|
| 19 |
|
| 20 |
def contar_frames(path):
|
| 21 |
-
"""Conta frames via
|
| 22 |
r = subprocess.run([
|
| 23 |
"ffprobe", "-v", "error",
|
| 24 |
"-select_streams", "v:0",
|
| 25 |
-
"-
|
| 26 |
-
"-show_entries", "stream=nb_read_packets",
|
| 27 |
"-of", "json", path
|
| 28 |
], capture_output=True, text=True)
|
| 29 |
try:
|
| 30 |
-
val = json.loads(r.stdout).get("streams", [{}])[0].get("
|
| 31 |
return int(val) if val else None
|
| 32 |
except (json.JSONDecodeError, ValueError, TypeError):
|
| 33 |
return None
|
|
@@ -39,7 +38,7 @@ def contar_frames(path):
|
|
| 39 |
|
| 40 |
def preparar_logo(logo_path, opacidade_pct):
|
| 41 |
"""
|
| 42 |
-
|
| 43 |
O redimensionamento Γ© feito pelo prΓ³prio FFmpeg com scale filter β
|
| 44 |
exatamente como o LegendadorBrasileiroWhisperX faz. Assim evitamos
|
| 45 |
re-saves desnecessΓ‘rios da PNG e preservamos as bordas originais.
|
|
@@ -49,8 +48,8 @@ def preparar_logo(logo_path, opacidade_pct):
|
|
| 49 |
if opacidade_pct < 100:
|
| 50 |
r, g, b, a = img.split()
|
| 51 |
fator = opacidade_pct / 100.0
|
| 52 |
-
a
|
| 53 |
-
img
|
| 54 |
|
| 55 |
temp_path = "/tmp/logo_overlay.png"
|
| 56 |
img.save(temp_path, "PNG")
|
|
@@ -62,12 +61,12 @@ def calcular_posicao_logo(posicao, margem=20, offset_x=0, offset_y=0):
|
|
| 62 |
ox, oy = offset_x, offset_y
|
| 63 |
m = margem
|
| 64 |
return {
|
| 65 |
-
"Centro": f"x=
|
| 66 |
-
"Canto superior esquerdo": f"x={m+ox}:y={m+oy}",
|
| 67 |
-
"Canto superior direito": f"x=W-w-
|
| 68 |
-
"Canto inferior esquerdo": f"x={m+ox}:y=H-h-
|
| 69 |
-
"Canto inferior direito": f"x=W-w-
|
| 70 |
-
}.get(posicao, f"x=
|
| 71 |
|
| 72 |
|
| 73 |
# ββ Preview ao vivo βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -77,8 +76,7 @@ MARGEM_PREVIEW = 12
|
|
| 77 |
|
| 78 |
|
| 79 |
def gerar_preview_logo(logo_file, logo_posicao, logo_margem,
|
| 80 |
-
logo_offset_x, logo_offset_y,
|
| 81 |
-
logo_tamanho, logo_opacidade):
|
| 82 |
"""Gera um preview PNG mostrando a logo posicionada sobre um fundo simulado."""
|
| 83 |
canvas = Image.new("RGBA", (PREVIEW_W, PREVIEW_H), (30, 30, 30, 255))
|
| 84 |
draw = ImageDraw.Draw(canvas)
|
|
@@ -108,8 +106,8 @@ def gerar_preview_logo(logo_file, logo_posicao, logo_margem,
|
|
| 108 |
if logo_opacidade < 100:
|
| 109 |
r, g, b, a = logo.split()
|
| 110 |
fator = logo_opacidade / 100.0
|
| 111 |
-
a
|
| 112 |
-
logo
|
| 113 |
|
| 114 |
escala = PREVIEW_W / 1920
|
| 115 |
mp = int(logo_margem * escala)
|
|
@@ -144,7 +142,6 @@ def gerar_preview_logo(logo_file, logo_posicao, logo_margem,
|
|
| 144 |
# Γudio β perfis disponΓveis
|
| 145 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 146 |
|
| 147 |
-
# label β (encoder, profile_arg_list)
|
| 148 |
AUDIO_CODECS = {
|
| 149 |
"libfdk_aac HE-AACv2 (mais eficiente, sΓ³ estΓ©reo)":
|
| 150 |
("libfdk_aac", ["-profile:a", "aac_he_v2"]),
|
|
@@ -207,7 +204,7 @@ def reencode_video(
|
|
| 207 |
], capture_output=True, text=True)
|
| 208 |
probe_data = json.loads(probe.stdout)
|
| 209 |
stream = probe_data.get("streams", [{}])[0]
|
| 210 |
-
vid_w = int(stream.get("width",
|
| 211 |
vid_h = int(stream.get("height", 1080))
|
| 212 |
is_vertical = vid_h > vid_w
|
| 213 |
|
|
@@ -318,13 +315,13 @@ def reencode_video(
|
|
| 318 |
"-rc", "vbr", "-cq", str(crf_valor), "-b:v", "0",
|
| 319 |
"-profile:v", "high", "-pix_fmt", "yuv420p"]
|
| 320 |
else:
|
| 321 |
-
cmd += ["-c:v", "libx264", "-
|
| 322 |
-
"-preset", "slow", "-crf", str(crf_valor), "-pix_fmt", "yuv420p"]
|
| 323 |
else: # x265
|
| 324 |
if use_gpu:
|
| 325 |
cmd += ["-c:v", "hevc_nvenc", "-preset", "p7", "-tune", "uhq",
|
| 326 |
"-rc", "vbr", "-cq", str(crf_valor), "-b:v", "0",
|
| 327 |
-
"-profile:v", "main", "-pix_fmt", "yuv420p"
|
|
|
|
| 328 |
else:
|
| 329 |
cmd += ["-c:v", "libx265", "-preset", "slow", "-crf", str(crf_valor),
|
| 330 |
"-pix_fmt", "yuv420p",
|
|
@@ -335,10 +332,10 @@ def reencode_video(
|
|
| 335 |
if has_audio and normalizar_audio:
|
| 336 |
yield None, "β³ Analisando volume do Γ‘udio (1Βͺ passada de loudnorm)..."
|
| 337 |
result_ln = subprocess.run([
|
| 338 |
-
"ffmpeg", "-hide_banner", "-loglevel", "info", "-
|
| 339 |
"-i", input_path,
|
| 340 |
"-af", "loudnorm=print_format=json",
|
| 341 |
-
"-f", "null", "-"
|
| 342 |
], capture_output=True, text=True)
|
| 343 |
|
| 344 |
raw = result_ln.stderr
|
|
@@ -352,7 +349,6 @@ def reencode_video(
|
|
| 352 |
f":measured_I={stats['input_i']}"
|
| 353 |
f":measured_tp={stats['input_tp']}"
|
| 354 |
f":measured_LRA={stats['input_lra']}"
|
| 355 |
-
f":measured_thresh={stats['input_thresh']}"
|
| 356 |
f":offset={stats['target_offset']}"
|
| 357 |
)
|
| 358 |
except (json.JSONDecodeError, KeyError):
|
|
@@ -410,7 +406,7 @@ def reencode_video(
|
|
| 410 |
yield None, f"β Erro no FFmpeg:\n{err[-1500:]}"
|
| 411 |
return
|
| 412 |
|
| 413 |
-
# ββ RelatΓ³rio final βββββββββββββββββββββββββββββββββββββββββ
|
| 414 |
orig_mb = os.path.getsize(input_path) / (1024 * 1024)
|
| 415 |
final_mb = os.path.getsize(output_path) / (1024 * 1024)
|
| 416 |
reducao = round((1 - final_mb / orig_mb) * 100, 1)
|
|
@@ -487,12 +483,12 @@ with gr.Blocks(title="Super Re-Encoder", theme=gr.themes.Soft(),
|
|
| 487 |
"""
|
| 488 |
)
|
| 489 |
|
| 490 |
-
#
|
| 491 |
with gr.Group():
|
| 492 |
gr.Markdown("### 1 Β· Envie o vΓdeo")
|
| 493 |
video = gr.Video(label="Arquivo de vΓdeo", sources=["upload"], height=280)
|
| 494 |
|
| 495 |
-
#
|
| 496 |
with gr.Accordion("2 Β· ConfiguraΓ§Γ΅es de vΓdeo", open=True):
|
| 497 |
with gr.Row():
|
| 498 |
with gr.Column():
|
|
@@ -527,7 +523,7 @@ with gr.Blocks(title="Super Re-Encoder", theme=gr.themes.Soft(),
|
|
| 527 |
label="CRF / CQ (menor = mais qualidade, arquivo maior)"
|
| 528 |
)
|
| 529 |
|
| 530 |
-
#
|
| 531 |
with gr.Accordion("3 Β· ConfiguraΓ§Γ΅es de Γ‘udio", open=True):
|
| 532 |
with gr.Row():
|
| 533 |
audio_codec_label = gr.Dropdown(
|
|
@@ -562,7 +558,7 @@ with gr.Blocks(title="Super Re-Encoder", theme=gr.themes.Soft(),
|
|
| 562 |
elem_id="audio-hint"
|
| 563 |
)
|
| 564 |
|
| 565 |
-
#
|
| 566 |
with gr.Accordion("4 Β· Filtros extras", open=False):
|
| 567 |
remover_duplicados = gr.Checkbox(
|
| 568 |
value=True,
|
|
@@ -570,7 +566,7 @@ with gr.Blocks(title="Super Re-Encoder", theme=gr.themes.Soft(),
|
|
| 570 |
"para usar o pipeline GPU completo"
|
| 571 |
)
|
| 572 |
|
| 573 |
-
#
|
| 574 |
with gr.Accordion("5 Β· Logo / marca d'Γ‘gua (opcional)", open=False):
|
| 575 |
with gr.Row():
|
| 576 |
with gr.Column(scale=1):
|
|
@@ -620,8 +616,7 @@ with gr.Blocks(title="Super Re-Encoder", theme=gr.themes.Soft(),
|
|
| 620 |
|
| 621 |
# Preview ao vivo reativo a qualquer controle de logo
|
| 622 |
_preview_inputs = [logo_file, logo_posicao, logo_margem,
|
| 623 |
-
logo_offset_x, logo_offset_y,
|
| 624 |
-
logo_tamanho, logo_opacidade]
|
| 625 |
for ctrl in _preview_inputs:
|
| 626 |
ctrl.change(
|
| 627 |
fn=gerar_preview_logo,
|
|
@@ -629,7 +624,7 @@ with gr.Blocks(title="Super Re-Encoder", theme=gr.themes.Soft(),
|
|
| 629 |
outputs=[logo_preview],
|
| 630 |
)
|
| 631 |
|
| 632 |
-
#
|
| 633 |
gr.Markdown("### 6 Β· Rodar")
|
| 634 |
btn = gr.Button("π RE-ENCODE AGORA", variant="primary", size="lg")
|
| 635 |
|
|
@@ -641,7 +636,7 @@ with gr.Blocks(title="Super Re-Encoder", theme=gr.themes.Soft(),
|
|
| 641 |
reencode_video,
|
| 642 |
inputs=[
|
| 643 |
video, modo, resolucao, fps, crf,
|
| 644 |
-
audio_codec_label, audio_bitrate, audio_sample_rate, audio_canais,
|
| 645 |
normalizar_audio,
|
| 646 |
remover_duplicados,
|
| 647 |
logo_file, logo_posicao, logo_margem,
|
|
@@ -651,4 +646,4 @@ with gr.Blocks(title="Super Re-Encoder", theme=gr.themes.Soft(),
|
|
| 651 |
)
|
| 652 |
|
| 653 |
demo.queue(max_size=5)
|
| 654 |
-
demo.launch(server_name="0.0.0.0", server_port=7860)
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
def contar_frames(path):
|
| 21 |
+
"""Conta frames via ffprobe β mais confiΓ‘vel que nb_frames."""
|
| 22 |
r = subprocess.run([
|
| 23 |
"ffprobe", "-v", "error",
|
| 24 |
"-select_streams", "v:0",
|
| 25 |
+
"-show_entries", "stream=width,height",
|
|
|
|
| 26 |
"-of", "json", path
|
| 27 |
], capture_output=True, text=True)
|
| 28 |
try:
|
| 29 |
+
val = json.loads(r.stdout).get("streams", [{}])[0].get("width", 1920)
|
| 30 |
return int(val) if val else None
|
| 31 |
except (json.JSONDecodeError, ValueError, TypeError):
|
| 32 |
return None
|
|
|
|
| 38 |
|
| 39 |
def preparar_logo(logo_path, opacidade_pct):
|
| 40 |
"""
|
| 41 |
+
Aplica a opacidade (escala o canal alpha) via Pillow.
|
| 42 |
O redimensionamento Γ© feito pelo prΓ³prio FFmpeg com scale filter β
|
| 43 |
exatamente como o LegendadorBrasileiroWhisperX faz. Assim evitamos
|
| 44 |
re-saves desnecessΓ‘rios da PNG e preservamos as bordas originais.
|
|
|
|
| 48 |
if opacidade_pct < 100:
|
| 49 |
r, g, b, a = img.split()
|
| 50 |
fator = opacidade_pct / 100.0
|
| 51 |
+
a = a.point(lambda x: int(x * fator))
|
| 52 |
+
img = Image.merge("RGBA", (r, g, b, a))
|
| 53 |
|
| 54 |
temp_path = "/tmp/logo_overlay.png"
|
| 55 |
img.save(temp_path, "PNG")
|
|
|
|
| 61 |
ox, oy = offset_x, offset_y
|
| 62 |
m = margem
|
| 63 |
return {
|
| 64 |
+
"Centro": f"x={W - w}/2 + {ox}:y={H - h}/2 + {oy}",
|
| 65 |
+
"Canto superior esquerdo": f"x={m + ox}:y={m + oy}",
|
| 66 |
+
"Canto superior direito": f"x={W - w - m} + {ox}:y={m + oy}",
|
| 67 |
+
"Canto inferior esquerdo": f"x={m + ox}:y={H - h - m} + {oy}",
|
| 68 |
+
"Canto inferior direito": f"x={W - w - m} + {ox}:y={H - h - m} + {oy}",
|
| 69 |
+
}.get(posicao, f"x={W - w}/2 + {ox}:y={H - h}/2 + {oy}")
|
| 70 |
|
| 71 |
|
| 72 |
# ββ Preview ao vivo βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 76 |
|
| 77 |
|
| 78 |
def gerar_preview_logo(logo_file, logo_posicao, logo_margem,
|
| 79 |
+
logo_offset_x, logo_offset_y, logo_tamanho, logo_opacidade):
|
|
|
|
| 80 |
"""Gera um preview PNG mostrando a logo posicionada sobre um fundo simulado."""
|
| 81 |
canvas = Image.new("RGBA", (PREVIEW_W, PREVIEW_H), (30, 30, 30, 255))
|
| 82 |
draw = ImageDraw.Draw(canvas)
|
|
|
|
| 106 |
if logo_opacidade < 100:
|
| 107 |
r, g, b, a = logo.split()
|
| 108 |
fator = logo_opacidade / 100.0
|
| 109 |
+
a = a.point(lambda px: int(px * fator))
|
| 110 |
+
logo = Image.merge("RGBA", (r, g, b, a))
|
| 111 |
|
| 112 |
escala = PREVIEW_W / 1920
|
| 113 |
mp = int(logo_margem * escala)
|
|
|
|
| 142 |
# Γudio β perfis disponΓveis
|
| 143 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 144 |
|
|
|
|
| 145 |
AUDIO_CODECS = {
|
| 146 |
"libfdk_aac HE-AACv2 (mais eficiente, sΓ³ estΓ©reo)":
|
| 147 |
("libfdk_aac", ["-profile:a", "aac_he_v2"]),
|
|
|
|
| 204 |
], capture_output=True, text=True)
|
| 205 |
probe_data = json.loads(probe.stdout)
|
| 206 |
stream = probe_data.get("streams", [{}])[0]
|
| 207 |
+
vid_w = int(stream.get("width", 1920))
|
| 208 |
vid_h = int(stream.get("height", 1080))
|
| 209 |
is_vertical = vid_h > vid_w
|
| 210 |
|
|
|
|
| 315 |
"-rc", "vbr", "-cq", str(crf_valor), "-b:v", "0",
|
| 316 |
"-profile:v", "high", "-pix_fmt", "yuv420p"]
|
| 317 |
else:
|
| 318 |
+
cmd += ["-c:v", "libx264", "-preset", "slow", "-crf", str(crf_valor), "-pix_fmt", "yuv420p"]
|
|
|
|
| 319 |
else: # x265
|
| 320 |
if use_gpu:
|
| 321 |
cmd += ["-c:v", "hevc_nvenc", "-preset", "p7", "-tune", "uhq",
|
| 322 |
"-rc", "vbr", "-cq", str(crf_valor), "-b:v", "0",
|
| 323 |
+
"-profile:v", "main", "-pix_fmt", "yuv420p",
|
| 324 |
+
"-x265-params", "sao=0:rd=6:psy-rd=1.0:psy-rdoq=2.0:rskip=1"]
|
| 325 |
else:
|
| 326 |
cmd += ["-c:v", "libx265", "-preset", "slow", "-crf", str(crf_valor),
|
| 327 |
"-pix_fmt", "yuv420p",
|
|
|
|
| 332 |
if has_audio and normalizar_audio:
|
| 333 |
yield None, "β³ Analisando volume do Γ‘udio (1Βͺ passada de loudnorm)..."
|
| 334 |
result_ln = subprocess.run([
|
| 335 |
+
"ffmpeg", "-hide_banner", "-loglevel", "info", "-v", "error",
|
| 336 |
"-i", input_path,
|
| 337 |
"-af", "loudnorm=print_format=json",
|
| 338 |
+
"-f", "null", "-",
|
| 339 |
], capture_output=True, text=True)
|
| 340 |
|
| 341 |
raw = result_ln.stderr
|
|
|
|
| 349 |
f":measured_I={stats['input_i']}"
|
| 350 |
f":measured_tp={stats['input_tp']}"
|
| 351 |
f":measured_LRA={stats['input_lra']}"
|
|
|
|
| 352 |
f":offset={stats['target_offset']}"
|
| 353 |
)
|
| 354 |
except (json.JSONDecodeError, KeyError):
|
|
|
|
| 406 |
yield None, f"β Erro no FFmpeg:\n{err[-1500:]}"
|
| 407 |
return
|
| 408 |
|
| 409 |
+
# ββ RelatΓ³rio final βββββββββββββββββββββββββββββββββββββββββββ
|
| 410 |
orig_mb = os.path.getsize(input_path) / (1024 * 1024)
|
| 411 |
final_mb = os.path.getsize(output_path) / (1024 * 1024)
|
| 412 |
reducao = round((1 - final_mb / orig_mb) * 100, 1)
|
|
|
|
| 483 |
"""
|
| 484 |
)
|
| 485 |
|
| 486 |
+
# 1) UPLOAD
|
| 487 |
with gr.Group():
|
| 488 |
gr.Markdown("### 1 Β· Envie o vΓdeo")
|
| 489 |
video = gr.Video(label="Arquivo de vΓdeo", sources=["upload"], height=280)
|
| 490 |
|
| 491 |
+
# 2) VΓDEO
|
| 492 |
with gr.Accordion("2 Β· ConfiguraΓ§Γ΅es de vΓdeo", open=True):
|
| 493 |
with gr.Row():
|
| 494 |
with gr.Column():
|
|
|
|
| 523 |
label="CRF / CQ (menor = mais qualidade, arquivo maior)"
|
| 524 |
)
|
| 525 |
|
| 526 |
+
# 3) ΓUDIO
|
| 527 |
with gr.Accordion("3 Β· ConfiguraΓ§Γ΅es de Γ‘udio", open=True):
|
| 528 |
with gr.Row():
|
| 529 |
audio_codec_label = gr.Dropdown(
|
|
|
|
| 558 |
elem_id="audio-hint"
|
| 559 |
)
|
| 560 |
|
| 561 |
+
# 4) Filtros extras
|
| 562 |
with gr.Accordion("4 Β· Filtros extras", open=False):
|
| 563 |
remover_duplicados = gr.Checkbox(
|
| 564 |
value=True,
|
|
|
|
| 566 |
"para usar o pipeline GPU completo"
|
| 567 |
)
|
| 568 |
|
| 569 |
+
# 5) Logo / marca d'Γ‘gua
|
| 570 |
with gr.Accordion("5 Β· Logo / marca d'Γ‘gua (opcional)", open=False):
|
| 571 |
with gr.Row():
|
| 572 |
with gr.Column(scale=1):
|
|
|
|
| 616 |
|
| 617 |
# Preview ao vivo reativo a qualquer controle de logo
|
| 618 |
_preview_inputs = [logo_file, logo_posicao, logo_margem,
|
| 619 |
+
logo_offset_x, logo_offset_y, logo_tamanho, logo_opacidade]
|
|
|
|
| 620 |
for ctrl in _preview_inputs:
|
| 621 |
ctrl.change(
|
| 622 |
fn=gerar_preview_logo,
|
|
|
|
| 624 |
outputs=[logo_preview],
|
| 625 |
)
|
| 626 |
|
| 627 |
+
# 6) AΓΓO + SAΓDA
|
| 628 |
gr.Markdown("### 6 Β· Rodar")
|
| 629 |
btn = gr.Button("π RE-ENCODE AGORA", variant="primary", size="lg")
|
| 630 |
|
|
|
|
| 636 |
reencode_video,
|
| 637 |
inputs=[
|
| 638 |
video, modo, resolucao, fps, crf,
|
| 639 |
+
audio_codec_label, audio_bitrate, audio_sample_rate, int(audio_canais),
|
| 640 |
normalizar_audio,
|
| 641 |
remover_duplicados,
|
| 642 |
logo_file, logo_posicao, logo_margem,
|
|
|
|
| 646 |
)
|
| 647 |
|
| 648 |
demo.queue(max_size=5)
|
| 649 |
+
demo.launch(server_name="0.0.0.0", server_port=7860)
|