auto-sync: 2026-04-30 01:30:01
This commit is contained in:
@@ -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.0–1.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
|
||||
|
||||
Reference in New Issue
Block a user