Add intro text improvements and BGM download management

- Intro text: Add 0.7s freeze frame effect before video plays
- Intro text: Auto-scale font size to prevent overflow
- Intro text: Split long text into 2 lines automatically
- BGM Page: Add free BGM download section with 6 categories
- BGM Page: Support individual and bulk download from Freesound.org

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kihong.kim
2026-01-04 19:01:58 +09:00
parent 3f9fc90e61
commit 49ca7d8913
2 changed files with 212 additions and 63 deletions

View File

@@ -74,8 +74,11 @@ async def process_video(
# Build video filter chain
video_filters = []
# Note: We no longer use tpad to add frozen frames, as it extends the video duration.
# Instead, intro text is simply overlaid on the existing video content.
# 1. Add freeze frame at the beginning if intro text is provided
# tpad adds frozen frames at start using clone mode (copies first frame)
if intro_text and intro_duration > 0:
# Clone the first frame for intro_duration seconds
video_filters.append(f"tpad=start_duration={intro_duration}:start_mode=clone")
# 2. Add subtitle overlay if provided
if subtitle_path and os.path.exists(subtitle_path):
@@ -89,6 +92,7 @@ async def process_video(
"/System/Library/Fonts/Supplemental/AppleGothic.ttf", # macOS Korean
"/System/Library/Fonts/AppleSDGothicNeo.ttc", # macOS Korean
"/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf", # Linux Korean
"/usr/share/fonts/truetype/korean/Pretendard-Bold.otf", # Docker Korean
"/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc", # Linux CJK
]
@@ -98,79 +102,82 @@ async def process_video(
font_file = font.replace(":", "\\:")
break
# Adjust font size and split text if too long
# Shorts video is 1080 width, so ~10-12 chars fit comfortably at 100px
text_len = len(intro_text)
adjusted_font_size = intro_font_size
# Calculate font size based on text length to prevent overflow
# Shorts video is typically 720px width
# Korean characters are nearly square (width ≈ height), so char_width_ratio ≈ 1.0
video_width = 720 # Default Shorts width
box_padding = 40 # boxborderw=20 on each side
max_width_ratio = 0.75 # Leave 25% margin for safety
char_width_ratio = 1.0 # Korean characters are nearly square
available_width = (video_width * max_width_ratio) - box_padding
# Split into 2 lines if text is long (more than 10 chars)
lines = []
if text_len > 10:
# Find best split point near middle
mid = text_len // 2
split_pos = mid
for i in range(mid, max(0, mid - 5), -1):
if intro_text[i] in ' ,、,':
split_pos = i + 1
# Split text into 2 lines if too long (more than 10 chars or font would be too small)
text_len = len(intro_text)
single_line_font = int(available_width / (text_len * char_width_ratio))
# Use 2 lines if single line font would be less than 50px
if single_line_font < 50 and text_len > 6:
# Find best split point (prefer space near middle)
mid = len(intro_text) // 2
split_pos = None
# Search for space within 5 chars of middle
for offset in range(6):
if mid + offset < len(intro_text) and intro_text[mid + offset] == ' ':
split_pos = mid + offset
break
for i in range(mid, min(text_len, mid + 5)):
if intro_text[i] in ' ,、,':
split_pos = i + 1
if mid - offset >= 0 and intro_text[mid - offset] == ' ':
split_pos = mid - offset
break
# If no space found, split at middle
if split_pos is None:
split_pos = mid
line1 = intro_text[:split_pos].strip()
line2 = intro_text[split_pos:].strip()
if line2:
lines = [line1, line2]
else:
lines = [intro_text]
display_text = f"{line1}\\n{line2}"
# Calculate font size based on longer line
max_line_len = max(len(line1), len(line2))
calculated_max_font = int(available_width / (max_line_len * char_width_ratio))
print(f"[Intro] Split into 2 lines: '{line1}' / '{line2}' (max {max_line_len} chars)")
else:
lines = [intro_text]
display_text = intro_text
calculated_max_font = single_line_font
print(f"[Intro] Single line: '{intro_text}' ({text_len} chars)")
# Adjust font size based on longest line length
max_line_len = max(len(line) for line in lines)
if max_line_len > 12:
adjusted_font_size = int(intro_font_size * 10 / max_line_len)
adjusted_font_size = max(50, min(adjusted_font_size, intro_font_size)) # Clamp between 50-100
adjusted_font_size = min(intro_font_size, calculated_max_font)
adjusted_font_size = max(36, adjusted_font_size) # Minimum 36px font size
print(f"[Intro] Requested font: {intro_font_size}px, Adjusted: {adjusted_font_size}px")
# Add fade effect timing
fade_out_start = max(0.1, intro_duration - 0.3)
alpha_expr = f"if(gt(t,{fade_out_start}),(({intro_duration}-t)/0.3),1)"
# Fade out effect timing (fade starts 0.2s before end)
fade_out_start = max(0.1, intro_duration - 0.2)
alpha_expr = f"if(gt(t,{fade_out_start}),(({intro_duration}-t)/0.2),1)"
# Create drawtext filter(s) for each line
line_height = adjusted_font_size + 20
total_height = line_height * len(lines)
escaped_text = display_text.replace("'", "\\'").replace(":", "\\:")
for i, line in enumerate(lines):
escaped_text = line.replace("'", "\\'").replace(":", "\\:").replace("\\", "\\\\")
# Draw text centered on screen during freeze frame
drawtext_parts = [
f"text='{escaped_text}'",
f"fontsize={adjusted_font_size}",
"fontcolor=white",
"x=(w-text_w)/2", # Center horizontally
"y=(h-text_h)/2", # Center vertically
f"enable='lt(t,{intro_duration})'",
"borderw=4",
"bordercolor=black",
"box=1",
"boxcolor=black@0.7",
"boxborderw=20",
f"alpha='{alpha_expr}'",
"line_spacing=10", # Add spacing between lines
]
# Calculate y position for this line (centered overall)
if len(lines) == 1:
y_expr = "(h-text_h)/2"
else:
# Center the block of lines, then position each line
y_offset = int((i - (len(lines) - 1) / 2) * line_height)
y_expr = f"(h-text_h)/2+{y_offset}"
if font_file:
drawtext_parts.insert(1, f"fontfile='{font_file}'")
drawtext_parts = [
f"text='{escaped_text}'",
f"fontsize={adjusted_font_size}",
"fontcolor=white",
"x=(w-text_w)/2", # Center horizontally
f"y={y_expr}",
f"enable='lt(t,{intro_duration})'",
"borderw=3",
"bordercolor=black",
"box=1",
"boxcolor=black@0.6",
"boxborderw=15",
f"alpha='{alpha_expr}'",
]
if font_file:
drawtext_parts.insert(1, f"fontfile='{font_file}'")
video_filters.append(f"drawtext={':'.join(drawtext_parts)}")
video_filters.append(f"drawtext={':'.join(drawtext_parts)}")
# Combine video filters
video_filter_str = ",".join(video_filters) if video_filters else None