Update video2.py
Browse files
video2.py
CHANGED
|
@@ -256,47 +256,68 @@ def audio_func(id, lines, lang):
|
|
| 256 |
return asyncio.run(generate_tts(id, lines, lang))
|
| 257 |
#-----------------------------
|
| 258 |
#---------------------------------
|
| 259 |
-
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
if not duration or not audio_path:
|
| 262 |
print("Failed to generate audio.")
|
| 263 |
return None
|
| 264 |
-
|
| 265 |
-
#TEXT = listf[0].strip()
|
| 266 |
-
TEXT=lines[id]
|
| 267 |
print("-----------------------------------------------------------------------------")
|
| 268 |
print(TEXT)
|
| 269 |
SKIP_SPACES = False
|
| 270 |
|
| 271 |
-
FPS = 30
|
| 272 |
-
ANIMATION_FRAMES_PER_CHAR =
|
| 273 |
-
WIDTH, HEIGHT = 1280, 720
|
| 274 |
MARGIN_X, MARGIN_Y = 40, 60
|
| 275 |
-
LINE_SPACING = 8
|
| 276 |
FONT = cv2.FONT_HERSHEY_SIMPLEX
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
|
|
|
|
|
|
|
|
|
| 281 |
silent_video_name = f"silent_video{id}.mp4"
|
| 282 |
silent_video_path = os.path.join(CLIPS_DIR, silent_video_name)
|
| 283 |
-
FFMPEG_PRESET = "ultrafast"
|
| 284 |
-
CRF =
|
| 285 |
# Pen settings
|
| 286 |
-
PEN_COLOR = (0, 0, 255)
|
| 287 |
-
PEN_TIP_RADIUS = 5
|
| 288 |
-
PEN_LENGTH = 20
|
| 289 |
-
PEN_THICKNESS = 2
|
| 290 |
-
PEN_BASE_ANGLE = 45
|
| 291 |
-
PEN_MOVEMENT_AMPLITUDE = 10
|
| 292 |
# ===================================
|
| 293 |
|
| 294 |
-
# Helper: wrap text by pixel width using cv2.getTextSize
|
| 295 |
-
def wrap_text_cv(text, font,
|
| 296 |
wrapped_lines = []
|
|
|
|
| 297 |
for para in text.splitlines():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
if para == "":
|
| 299 |
-
wrapped_lines.append("")
|
|
|
|
| 300 |
continue
|
| 301 |
words = para.split(" ")
|
| 302 |
cur = ""
|
|
@@ -308,6 +329,7 @@ def video_func(id, lines,lang):
|
|
| 308 |
else:
|
| 309 |
if cur != "":
|
| 310 |
wrapped_lines.append(cur)
|
|
|
|
| 311 |
(single_w, _), _ = cv2.getTextSize(w, font, font_scale, thickness)
|
| 312 |
if single_w > max_width:
|
| 313 |
chunk = ""
|
|
@@ -318,6 +340,7 @@ def video_func(id, lines,lang):
|
|
| 318 |
chunk = cand2
|
| 319 |
else:
|
| 320 |
wrapped_lines.append(chunk)
|
|
|
|
| 321 |
chunk = ch
|
| 322 |
if chunk:
|
| 323 |
cur = chunk
|
|
@@ -327,10 +350,12 @@ def video_func(id, lines,lang):
|
|
| 327 |
cur = w
|
| 328 |
if cur != "":
|
| 329 |
wrapped_lines.append(cur)
|
| 330 |
-
|
| 331 |
-
|
|
|
|
|
|
|
| 332 |
text_area_width = WIDTH - 2 * MARGIN_X
|
| 333 |
-
wrapped_lines = wrap_text_cv(TEXT, FONT,
|
| 334 |
full_text = "\n".join(wrapped_lines)
|
| 335 |
if not full_text:
|
| 336 |
full_text = ""
|
|
@@ -345,20 +370,23 @@ def video_func(id, lines,lang):
|
|
| 345 |
if total_glyphs == 0:
|
| 346 |
print("No text to animate.")
|
| 347 |
return None
|
| 348 |
-
#
|
| 349 |
min_frames = total_glyphs * ANIMATION_FRAMES_PER_CHAR
|
| 350 |
print(f"Rendering {min_frames} minimal frames for full text animation.")
|
| 351 |
-
# Pre-calc line heights and y_positions
|
| 352 |
line_heights = []
|
| 353 |
-
for line in wrapped_lines:
|
| 354 |
-
if line == "":
|
| 355 |
-
(w, h), baseline = cv2.getTextSize("Ay", FONT, FONT_SCALE, THICKNESS)
|
| 356 |
-
else:
|
| 357 |
-
(w, h), baseline = cv2.getTextSize(line, FONT, FONT_SCALE, THICKNESS)
|
| 358 |
-
line_heights.append(h + baseline + LINE_SPACING)
|
| 359 |
y_positions = []
|
| 360 |
y = MARGIN_Y
|
| 361 |
-
for
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
y_positions.append(y)
|
| 363 |
y += lh
|
| 364 |
# Prepare ffmpeg
|
|
@@ -372,18 +400,22 @@ def video_func(id, lines,lang):
|
|
| 372 |
print("FFMPEG CMD:", ffmpeg_cmd)
|
| 373 |
|
| 374 |
proc = subprocess.Popen(shlex.split(ffmpeg_cmd), stdin=subprocess.PIPE, bufsize=10**8)
|
| 375 |
-
# Render function,
|
| 376 |
def render_frame(visible_text, pen_x, pen_y, anim_offset):
|
| 377 |
img = np.full((HEIGHT, WIDTH, 3), BG_COLOR, dtype=np.uint8)
|
| 378 |
lines = visible_text.split("\n")
|
| 379 |
for idx, line in enumerate(lines):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
x = MARGIN_X
|
| 381 |
y = y_positions[idx]
|
| 382 |
-
(w, h), baseline = cv2.getTextSize(line, FONT,
|
| 383 |
y_draw = y + h
|
| 384 |
if line != "":
|
| 385 |
-
cv2.putText(img, line, (x, y_draw), FONT,
|
| 386 |
-
if pen_x > 0:
|
| 387 |
offset_y = int(PEN_MOVEMENT_AMPLITUDE * math.sin(anim_offset * math.pi))
|
| 388 |
pen_tip_y = pen_y + offset_y
|
| 389 |
angle_rad = math.radians(PEN_BASE_ANGLE)
|
|
@@ -404,7 +436,10 @@ def video_func(id, lines,lang):
|
|
| 404 |
lines = visible_sub.split("\n")
|
| 405 |
last_line = lines[-1]
|
| 406 |
line_idx = len(lines) - 1
|
| 407 |
-
|
|
|
|
|
|
|
|
|
|
| 408 |
pen_x = MARGIN_X + w + 5
|
| 409 |
pen_y = y_positions[line_idx] + h // 2
|
| 410 |
last_pen_x = pen_x
|
|
@@ -414,7 +449,6 @@ def video_func(id, lines,lang):
|
|
| 414 |
proc.stdin.write(frame_img.tobytes())
|
| 415 |
frames_sent += 1
|
| 416 |
prev_visible_sub = visible_sub
|
| 417 |
-
# No repeat or remaining frames added during rendering - full minimal animation only
|
| 418 |
proc.stdin.close()
|
| 419 |
proc.wait()
|
| 420 |
elapsed = time.time() - t0
|
|
@@ -422,7 +456,7 @@ def video_func(id, lines,lang):
|
|
| 422 |
if not os.path.exists(silent_video_path):
|
| 423 |
print("Silent video generation failed.")
|
| 424 |
return None
|
| 425 |
-
#
|
| 426 |
final_video_name = f"clip{id}.mp4"
|
| 427 |
final_video_path = os.path.join(CLIPS_DIR, final_video_name)
|
| 428 |
video_clip = VideoFileClip(silent_video_path)
|
|
@@ -433,15 +467,9 @@ def video_func(id, lines,lang):
|
|
| 433 |
print(f"Adjusting video speed by factor: {speed_factor:.3f}")
|
| 434 |
video_clip = video_clip.fx(speedx, speed_factor)
|
| 435 |
final_clip = video_clip.set_audio(AudioFileClip(audio_path))
|
| 436 |
-
# Write final video
|
| 437 |
-
final_clip.write_videofile(final_video_path, codec='libx264', audio_codec='aac', preset='ultrafast', verbose=False, logger=None)
|
| 438 |
-
# Print the final video file name
|
| 439 |
print(f"Final video saved at: {final_video_path}")
|
| 440 |
-
#
|
| 441 |
-
# if os.path.exists(final_video_path):
|
| 442 |
-
# display(Video(final_video_path, embed=True, width=WIDTH, height=HEIGHT))
|
| 443 |
-
# Clean up silent video if not needed
|
| 444 |
os.remove(silent_video_path)
|
| 445 |
-
return final_video_path
|
| 446 |
-
|
| 447 |
-
#video
|
|
|
|
| 256 |
return asyncio.run(generate_tts(id, lines, lang))
|
| 257 |
#-----------------------------
|
| 258 |
#---------------------------------
|
| 259 |
+
import os
|
| 260 |
+
import subprocess
|
| 261 |
+
import shlex
|
| 262 |
+
import time
|
| 263 |
+
import math
|
| 264 |
+
import numpy as np
|
| 265 |
+
import cv2
|
| 266 |
+
from moviepy.editor import VideoFileClip, AudioFileClip
|
| 267 |
+
from moviepy.video.fx.speedx import speedx
|
| 268 |
+
|
| 269 |
+
def video_func(id, lines, lang):
|
| 270 |
+
duration, audio_path = audio_func(id, lines, lang)
|
| 271 |
if not duration or not audio_path:
|
| 272 |
print("Failed to generate audio.")
|
| 273 |
return None
|
| 274 |
+
TEXT = lines[id]
|
|
|
|
|
|
|
| 275 |
print("-----------------------------------------------------------------------------")
|
| 276 |
print(TEXT)
|
| 277 |
SKIP_SPACES = False
|
| 278 |
|
| 279 |
+
FPS = 30 # Keep for smoothness, but can reduce to 24 if needed for speed
|
| 280 |
+
ANIMATION_FRAMES_PER_CHAR = 2 # Reduced from 3 for faster rendering (less frames per char)
|
| 281 |
+
WIDTH, HEIGHT = 1280, 720
|
| 282 |
MARGIN_X, MARGIN_Y = 40, 60
|
| 283 |
+
LINE_SPACING = 8
|
| 284 |
FONT = cv2.FONT_HERSHEY_SIMPLEX
|
| 285 |
+
DEFAULT_FONT_SCALE = 1.5
|
| 286 |
+
HEADER_FONT_SCALE = 2.0 # Increased size for headers
|
| 287 |
+
DEFAULT_THICKNESS = 2
|
| 288 |
+
HEADER_THICKNESS = 3 # Bolder for headers
|
| 289 |
+
DEFAULT_TEXT_COLOR = (0, 0, 0) # BGR Black
|
| 290 |
+
HEADER_TEXT_COLOR = (255, 0, 0) # BGR Blue
|
| 291 |
+
BG_COLOR = (255, 255, 255) # BGR White
|
| 292 |
silent_video_name = f"silent_video{id}.mp4"
|
| 293 |
silent_video_path = os.path.join(CLIPS_DIR, silent_video_name)
|
| 294 |
+
FFMPEG_PRESET = "ultrafast"
|
| 295 |
+
CRF = 28 # Increased CRF for faster encoding (lower quality, but quicker)
|
| 296 |
# Pen settings
|
| 297 |
+
PEN_COLOR = (0, 0, 255) # Red pen (BGR)
|
| 298 |
+
PEN_TIP_RADIUS = 5
|
| 299 |
+
PEN_LENGTH = 20
|
| 300 |
+
PEN_THICKNESS = 2
|
| 301 |
+
PEN_BASE_ANGLE = 45
|
| 302 |
+
PEN_MOVEMENT_AMPLITUDE = 10
|
| 303 |
# ===================================
|
| 304 |
|
| 305 |
+
# Helper: wrap text by pixel width using cv2.getTextSize, now with per-line styles
|
| 306 |
+
def wrap_text_cv(text, font, default_font_scale, default_thickness, max_width):
|
| 307 |
wrapped_lines = []
|
| 308 |
+
styles = [] # List of (is_header) for each wrapped line
|
| 309 |
for para in text.splitlines():
|
| 310 |
+
is_header = para.strip().startswith("###")
|
| 311 |
+
if is_header:
|
| 312 |
+
para = para.strip()[3:].strip() # Remove "### " or "###"
|
| 313 |
+
font_scale = HEADER_FONT_SCALE
|
| 314 |
+
thickness = HEADER_THICKNESS
|
| 315 |
+
else:
|
| 316 |
+
font_scale = default_font_scale
|
| 317 |
+
thickness = default_thickness
|
| 318 |
if para == "":
|
| 319 |
+
wrapped_lines.append("")
|
| 320 |
+
styles.append(False) # Not header
|
| 321 |
continue
|
| 322 |
words = para.split(" ")
|
| 323 |
cur = ""
|
|
|
|
| 329 |
else:
|
| 330 |
if cur != "":
|
| 331 |
wrapped_lines.append(cur)
|
| 332 |
+
styles.append(is_header)
|
| 333 |
(single_w, _), _ = cv2.getTextSize(w, font, font_scale, thickness)
|
| 334 |
if single_w > max_width:
|
| 335 |
chunk = ""
|
|
|
|
| 340 |
chunk = cand2
|
| 341 |
else:
|
| 342 |
wrapped_lines.append(chunk)
|
| 343 |
+
styles.append(is_header)
|
| 344 |
chunk = ch
|
| 345 |
if chunk:
|
| 346 |
cur = chunk
|
|
|
|
| 350 |
cur = w
|
| 351 |
if cur != "":
|
| 352 |
wrapped_lines.append(cur)
|
| 353 |
+
styles.append(is_header)
|
| 354 |
+
return wrapped_lines, styles
|
| 355 |
+
|
| 356 |
+
# Pre-wrap text with styles
|
| 357 |
text_area_width = WIDTH - 2 * MARGIN_X
|
| 358 |
+
wrapped_lines, line_styles = wrap_text_cv(TEXT, FONT, DEFAULT_FONT_SCALE, DEFAULT_THICKNESS, text_area_width)
|
| 359 |
full_text = "\n".join(wrapped_lines)
|
| 360 |
if not full_text:
|
| 361 |
full_text = ""
|
|
|
|
| 370 |
if total_glyphs == 0:
|
| 371 |
print("No text to animate.")
|
| 372 |
return None
|
| 373 |
+
# Minimal frames
|
| 374 |
min_frames = total_glyphs * ANIMATION_FRAMES_PER_CHAR
|
| 375 |
print(f"Rendering {min_frames} minimal frames for full text animation.")
|
| 376 |
+
# Pre-calc line heights and y_positions with per-line styles
|
| 377 |
line_heights = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
y_positions = []
|
| 379 |
y = MARGIN_Y
|
| 380 |
+
for i, line in enumerate(wrapped_lines):
|
| 381 |
+
is_header = line_styles[i]
|
| 382 |
+
font_scale = HEADER_FONT_SCALE if is_header else DEFAULT_FONT_SCALE
|
| 383 |
+
thickness = HEADER_THICKNESS if is_header else DEFAULT_THICKNESS
|
| 384 |
+
if line == "":
|
| 385 |
+
(w, h), baseline = cv2.getTextSize("Ay", FONT, font_scale, thickness)
|
| 386 |
+
else:
|
| 387 |
+
(w, h), baseline = cv2.getTextSize(line, FONT, font_scale, thickness)
|
| 388 |
+
lh = h + baseline + LINE_SPACING
|
| 389 |
+
line_heights.append(lh)
|
| 390 |
y_positions.append(y)
|
| 391 |
y += lh
|
| 392 |
# Prepare ffmpeg
|
|
|
|
| 400 |
print("FFMPEG CMD:", ffmpeg_cmd)
|
| 401 |
|
| 402 |
proc = subprocess.Popen(shlex.split(ffmpeg_cmd), stdin=subprocess.PIPE, bufsize=10**8)
|
| 403 |
+
# Render function, now with per-line colors and styles
|
| 404 |
def render_frame(visible_text, pen_x, pen_y, anim_offset):
|
| 405 |
img = np.full((HEIGHT, WIDTH, 3), BG_COLOR, dtype=np.uint8)
|
| 406 |
lines = visible_text.split("\n")
|
| 407 |
for idx, line in enumerate(lines):
|
| 408 |
+
is_header = line_styles[idx]
|
| 409 |
+
font_scale = HEADER_FONT_SCALE if is_header else DEFAULT_FONT_SCALE
|
| 410 |
+
thickness = HEADER_THICKNESS if is_header else DEFAULT_THICKNESS
|
| 411 |
+
color = HEADER_TEXT_COLOR if is_header else DEFAULT_TEXT_COLOR
|
| 412 |
x = MARGIN_X
|
| 413 |
y = y_positions[idx]
|
| 414 |
+
(w, h), baseline = cv2.getTextSize(line, FONT, font_scale, thickness)
|
| 415 |
y_draw = y + h
|
| 416 |
if line != "":
|
| 417 |
+
cv2.putText(img, line, (x, y_draw), FONT, font_scale, color, thickness, lineType=cv2.LINE_AA)
|
| 418 |
+
if pen_x > 0:
|
| 419 |
offset_y = int(PEN_MOVEMENT_AMPLITUDE * math.sin(anim_offset * math.pi))
|
| 420 |
pen_tip_y = pen_y + offset_y
|
| 421 |
angle_rad = math.radians(PEN_BASE_ANGLE)
|
|
|
|
| 436 |
lines = visible_sub.split("\n")
|
| 437 |
last_line = lines[-1]
|
| 438 |
line_idx = len(lines) - 1
|
| 439 |
+
is_header = line_styles[line_idx]
|
| 440 |
+
font_scale = HEADER_FONT_SCALE if is_header else DEFAULT_FONT_SCALE
|
| 441 |
+
thickness = HEADER_THICKNESS if is_header else DEFAULT_THICKNESS
|
| 442 |
+
(w, h), baseline = cv2.getTextSize(last_line, FONT, font_scale, thickness)
|
| 443 |
pen_x = MARGIN_X + w + 5
|
| 444 |
pen_y = y_positions[line_idx] + h // 2
|
| 445 |
last_pen_x = pen_x
|
|
|
|
| 449 |
proc.stdin.write(frame_img.tobytes())
|
| 450 |
frames_sent += 1
|
| 451 |
prev_visible_sub = visible_sub
|
|
|
|
| 452 |
proc.stdin.close()
|
| 453 |
proc.wait()
|
| 454 |
elapsed = time.time() - t0
|
|
|
|
| 456 |
if not os.path.exists(silent_video_path):
|
| 457 |
print("Silent video generation failed.")
|
| 458 |
return None
|
| 459 |
+
# Combine with audio using MoviePy
|
| 460 |
final_video_name = f"clip{id}.mp4"
|
| 461 |
final_video_path = os.path.join(CLIPS_DIR, final_video_name)
|
| 462 |
video_clip = VideoFileClip(silent_video_path)
|
|
|
|
| 467 |
print(f"Adjusting video speed by factor: {speed_factor:.3f}")
|
| 468 |
video_clip = video_clip.fx(speedx, speed_factor)
|
| 469 |
final_clip = video_clip.set_audio(AudioFileClip(audio_path))
|
| 470 |
+
# Write final video with faster settings
|
| 471 |
+
final_clip.write_videofile(final_video_path, codec='libx264', audio_codec='aac', preset='ultrafast', verbose=False, logger=None, threads=4) # Added threads for multi-threading
|
|
|
|
| 472 |
print(f"Final video saved at: {final_video_path}")
|
| 473 |
+
# Clean up
|
|
|
|
|
|
|
|
|
|
| 474 |
os.remove(silent_video_path)
|
| 475 |
+
return final_video_path
|
|
|
|
|
|