auto-sync: 2026-04-30 01:30:01

This commit is contained in:
Stream
2026-04-30 01:30:01 +03:00
parent 75ae562ab4
commit 91b2502ee1
3 changed files with 252 additions and 18 deletions

View File

@@ -37,27 +37,124 @@ def _load_font(path: str, size: int) -> ImageFont.FreeTypeFont:
def _draw_text_centered(image: Image.Image, text: str,
font_active, font_inactive,
active: bool, fade_alpha: int,
y_ratio: float = 0.82):
"""Рисует текст по центру кадра, с тенью и цветом по статусу."""
draw = ImageDraw.Draw(image)
font = font_active if active else font_inactive
color = ACTIVE_COLOR if active else INACTIVE_COLOR
y_ratio: float = 0.82,
alpha: int = None):
"""Рисует текст по центру кадра, с полупрозрачной тенью и цветом.
# Тень
bbox = draw.textbbox((0, 0), text, font=font)
Работает корректно как с RGB, так и с RGBA-изображениями.
Если `alpha` передан — используется как общий множитель прозрачности.
"""
# Определяем цвет с учётом alpha
if active:
base_color = (255, 220, 0) # жёлтый
else:
base_color = (255, 255, 255) # белый
# Глобальный alpha (по умолчанию = fade_alpha)
g_alpha = alpha if alpha is not None else fade_alpha
# Цвет текста (RGBA)
txt_color = (base_color[0], base_color[1], base_color[2], g_alpha)
# Цвет тени — чёрный с альфа ~180, масштабируется по g_alpha
sh_alpha = int(180 * g_alpha / 255)
sh_color = (0, 0, 0, sh_alpha)
font = font_active if active else font_inactive
bbox = font.getbbox(text)
tw = bbox[2] - bbox[0]
th = bbox[3] - bbox[1]
x = (WIDTH - tw) // 2
y = int(HEIGHT * y_ratio) - th // 2
# Тень (чёрная, с офсетом)
draw.text((x + 3, y + 3), text, font=font, fill=SHADOW_COLOR)
# Основной текст с alpha
# Отрисовка на overlay с правильным alpha-композитингом
overlay = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0))
d = ImageDraw.Draw(overlay)
d.text((x, y), text, font=font, fill=(*color, fade_alpha))
image.paste(overlay.convert("RGB"), (0, 0), overlay)
# Тень
d.text((x + 3, y + 3), text, font=font, fill=sh_color)
# Основной текст
d.text((x, y), text, font=font, fill=txt_color)
# Композитим поверх переданного image
if image.mode == "RGBA":
image.alpha_composite(overlay)
else:
image.paste(overlay.convert("RGB"), (0, 0), overlay)
def draw_text_with_alpha(image: Image.Image, text: str,
font_active, font_inactive,
alpha: int = 255, active: bool = True,
y_ratio: float = 0.82):
"""Удобная обёртка: нарисовать одну строку с заданной прозрачностью."""
_draw_text_centered(image, text, font_active, font_inactive,
active=active, fade_alpha=alpha,
y_ratio=y_ratio, alpha=alpha)
def draw_progress_bar(image: Image.Image, current_time: float,
prev_end: float, next_start: float):
"""Рисует прогресс-бар для инструментальных пауз > 20 сек.
- Горизонтальная полоса по центру экрана (50% высоты)
- Ширина 60% кадра, высота 8px, скруглённые углы (r=4)
- Под полосой — таймер оставшегося времени
"""
draw = ImageDraw.Draw(image)
bar_w = int(WIDTH * 0.60)
bar_h = 8
bar_x = (WIDTH - bar_w) // 2
bar_y = HEIGHT // 2
# Прогресс (0.01.0)
total_gap = next_start - prev_end
progress = max(0.0, min(1.0, (current_time - prev_end) / total_gap))
fill_w = int(bar_w * progress)
radius = 4
def _rounded_rect(draw_, x, y, w, h, r, fill):
"""Рисует прямоугольник со скруглёнными углами на RGBA overlay."""
ov = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0))
d = ImageDraw.Draw(ov)
d.rounded_rectangle([x, y, x + w, y + h], radius=r, fill=fill)
if image.mode == "RGBA":
image.alpha_composite(ov)
else:
image.paste(ov.convert("RGB"), (0, 0), ov)
# Фон полосы
_rounded_rect(draw, bar_x, bar_y, bar_w, bar_h, radius, (80, 80, 80, 200))
# Заполнение
if fill_w > 0:
fill_r = min(radius, fill_w // 2, radius)
_rounded_rect(draw, bar_x, bar_y, fill_w, bar_h, fill_r, (255, 255, 255, 220))
# Таймер: оставшееся время до следующей строки
remaining = max(0, next_start - current_time)
mins = int(remaining) // 60
secs = int(remaining) % 60
timer_text = f"{mins}:{secs:02d}"
# Рисуем таймер под полосой (шрифт 32px)
try:
timer_font = ImageFont.truetype(FONT_INACTIVE, 32)
except Exception:
timer_font = ImageFont.load_default()
overlay = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0))
d = ImageDraw.Draw(overlay)
bbox = d.textbbox((0, 0), timer_text, font=timer_font)
tw = bbox[2] - bbox[0]
tx = (WIDTH - tw) // 2
ty = bar_y + bar_h + 8
d.text((tx + 2, ty + 2), timer_text, font=timer_font, fill=(0, 0, 0, 140))
d.text((tx, ty), timer_text, font=timer_font, fill=(255, 255, 255, 220))
if image.mode == "RGBA":
image.alpha_composite(overlay)
else:
image.paste(overlay.convert("RGB"), (0, 0), overlay)
# ---------- Рендер через FFmpeg ----------
@@ -182,13 +279,36 @@ def render_with_bg(segments: list[dict], audio_path: str, bg_video: str,
t = i / fps
frame = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0))
# Определяем активный сегмент
active_seg = None
for seg in segments:
seg_idx = -1
for idx, seg in enumerate(segments):
if seg["start"] <= t <= seg["end"]:
active_seg = seg
seg_idx = idx
break
# Предыдущий сегмент (для прогресс-бара и fallback-текста)
prev_seg = None
prev_seg_end = 0.0
for seg in segments:
if seg["end"] <= t:
prev_seg = seg
prev_seg_end = seg["end"]
else:
break
# Следующий сегмент
next_seg = None
for seg in segments:
if seg["start"] > t:
next_seg = seg
break
gap = (next_seg["start"] - t) if next_seg else 999
if active_seg:
# Активный сегмент — рисуем с fade in/out
frames_from_start = int((t - active_seg["start"]) * fps)
fade_alpha = min(255, int(255 * frames_from_start / max(FADE_FRAMES, 1)))
frames_to_end = int((active_seg["end"] - t) * fps)
@@ -198,6 +318,24 @@ def render_with_bg(segments: list[dict], audio_path: str, bg_video: str,
font_active, font_inactive,
True, max(fade_alpha, 128))
elif gap > 20 and next_seg:
# Длинная пауза (>20s) — показываем прогресс-бар
draw_progress_bar(frame, t, prev_seg_end, next_seg["start"])
elif gap <= 5 and next_seg:
# За 5 сек до следующей строки — плавное появление (fade in)
fade = 1.0 - (gap / 5.0) # 0.0 → 1.0
draw_text_with_alpha(frame, next_seg["text"],
font_active, font_inactive,
alpha=int(fade * 255), active=True)
else:
# Показываем предыдущий сегмент с пониженной заметностью
if prev_seg:
_draw_text_centered(frame, prev_seg["text"],
font_active, font_inactive,
False, 128)
frame.save(os.path.join(tmpdir, f"ov_{i:07d}.png"), "PNG")
# Комбинирование: ffmpeg complex filter