sreepathi-ravikumar commited on
Commit
7df00a9
·
verified ·
1 Parent(s): 80b8199

Update video2.py

Browse files
Files changed (1) hide show
  1. video2.py +197 -24
video2.py CHANGED
@@ -317,37 +317,210 @@ def audio_func(id, lines, lang):
317
 
318
  #-----------------------------
319
  #---------------------------------
320
- import os
321
- import subprocess
322
- import shlex
323
- import time
324
- import math
325
- import numpy as np
326
- import cv2
327
- from moviepy.editor import VideoFileClip, AudioFileClip
328
- from moviepy.video.fx.speedx import speedx
329
-
330
- # video.py
331
  def video_func(id, lines, lang):
332
  duration, audio_path = audio_func(id, lines, lang)
333
  if not duration or not audio_path:
334
  print("Failed to generate audio.")
335
  return None
336
-
337
  TEXT = lines[id]
338
  print("-----------------------------------------------------------------------------")
339
  print(TEXT)
340
-
341
- # CREATE CLIPS DIRECTORY IF IT DOESN'T EXIST
342
- os.makedirs(CLIPS_DIR, exist_ok=True)
343
-
344
- # Call Rust function
345
- final_video_path = rust_highlight.generate_video_clip(id, TEXT, audio_path, duration, CLIPS_DIR)
346
-
347
- if final_video_path:
348
- print(f"Final video saved at: {final_video_path}")
349
- return final_video_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  else:
351
- print("Video generation failed.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  return None
353
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
  #-----------------------------
319
  #---------------------------------
 
 
 
 
 
 
 
 
 
 
 
320
  def video_func(id, lines, lang):
321
  duration, audio_path = audio_func(id, lines, lang)
322
  if not duration or not audio_path:
323
  print("Failed to generate audio.")
324
  return None
 
325
  TEXT = lines[id]
326
  print("-----------------------------------------------------------------------------")
327
  print(TEXT)
328
+ SKIP_SPACES = False
329
+
330
+ FPS = 30 # Keep for smoothness, but can reduce to 24 if needed for speed
331
+ ANIMATION_FRAMES_PER_CHAR = 2 # Reduced from 3 for faster rendering (less frames per char)
332
+ WIDTH, HEIGHT = 1280, 720
333
+ MARGIN_X, MARGIN_Y = 40, 60
334
+ LINE_SPACING = 8
335
+ FONT = cv2.FONT_HERSHEY_SIMPLEX
336
+ DEFAULT_FONT_SCALE = 1.5
337
+ HEADER_FONT_SCALE = 2.0 # Increased size for headers
338
+ DEFAULT_THICKNESS = 2
339
+ HEADER_THICKNESS = 3 # Bolder for headers
340
+ DEFAULT_TEXT_COLOR = (0, 0, 0) # BGR Black
341
+ HEADER_TEXT_COLOR = (255, 0, 0) # BGR Blue
342
+ BG_COLOR = (255, 255, 255) # BGR White
343
+ silent_video_name = f"silent_video{id}.mp4"
344
+ silent_video_path = os.path.join(CLIPS_DIR, silent_video_name)
345
+ FFMPEG_PRESET = "ultrafast"
346
+ CRF = 28 # Increased CRF for faster encoding (lower quality, but quicker)
347
+ # Pen settings
348
+ PEN_COLOR = (0, 0, 255) # Red pen (BGR)
349
+ PEN_TIP_RADIUS = 5
350
+ PEN_LENGTH = 20
351
+ PEN_THICKNESS = 2
352
+ PEN_BASE_ANGLE = 45
353
+ PEN_MOVEMENT_AMPLITUDE = 10
354
+ # ===================================
355
+
356
+ # Helper: wrap text by pixel width using cv2.getTextSize, now with per-line styles
357
+ def wrap_text_cv(text, font, default_font_scale, default_thickness, max_width):
358
+ wrapped_lines = []
359
+ styles = [] # List of (is_header) for each wrapped line
360
+ for para in text.splitlines():
361
+ is_header = para.strip().startswith("###")
362
+ if is_header:
363
+ para = para.strip()[3:].strip() # Remove "### " or "###"
364
+ font_scale = HEADER_FONT_SCALE
365
+ thickness = HEADER_THICKNESS
366
+ else:
367
+ font_scale = default_font_scale
368
+ thickness = default_thickness
369
+ if para == "":
370
+ wrapped_lines.append("")
371
+ styles.append(False) # Not header
372
+ continue
373
+ words = para.split(" ")
374
+ cur = ""
375
+ for w in words:
376
+ candidate = w if cur == "" else cur + " " + w
377
+ (w_w, w_h), _ = cv2.getTextSize(candidate, font, font_scale, thickness)
378
+ if w_w <= max_width:
379
+ cur = candidate
380
+ else:
381
+ if cur != "":
382
+ wrapped_lines.append(cur)
383
+ styles.append(is_header)
384
+ (single_w, _), _ = cv2.getTextSize(w, font, font_scale, thickness)
385
+ if single_w > max_width:
386
+ chunk = ""
387
+ for ch in w:
388
+ cand2 = chunk + ch
389
+ (c_w, _), _ = cv2.getTextSize(cand2, font, font_scale, thickness)
390
+ if c_w <= max_width:
391
+ chunk = cand2
392
+ else:
393
+ wrapped_lines.append(chunk)
394
+ styles.append(is_header)
395
+ chunk = ch
396
+ if chunk:
397
+ cur = chunk
398
+ else:
399
+ cur = ""
400
+ else:
401
+ cur = w
402
+ if cur != "":
403
+ wrapped_lines.append(cur)
404
+ styles.append(is_header)
405
+ return wrapped_lines, styles
406
+
407
+ # Pre-wrap text with styles
408
+ text_area_width = WIDTH - 2 * MARGIN_X
409
+ wrapped_lines, line_styles = wrap_text_cv(TEXT, FONT, DEFAULT_FONT_SCALE, DEFAULT_THICKNESS, text_area_width)
410
+ full_text = "\n".join(wrapped_lines)
411
+ if not full_text:
412
+ full_text = ""
413
+ # Visible indices
414
+ if SKIP_SPACES:
415
+ visible_indices = [i for i, ch in enumerate(full_text) if (ch != ' ' and ch != '\n' and ch != '\t')]
416
  else:
417
+ visible_indices = list(range(len(full_text)))
418
+
419
+ total_glyphs = len(visible_indices)
420
+ print(f"Wrapped lines: {len(wrapped_lines)} lines, total glyphs (counted): {total_glyphs}")
421
+ if total_glyphs == 0:
422
+ print("No text to animate.")
423
+ return None
424
+ # Minimal frames
425
+ min_frames = total_glyphs * ANIMATION_FRAMES_PER_CHAR
426
+ print(f"Rendering {min_frames} minimal frames for full text animation.")
427
+ # Pre-calc line heights and y_positions with per-line styles
428
+ line_heights = []
429
+ y_positions = []
430
+ y = MARGIN_Y
431
+ for i, line in enumerate(wrapped_lines):
432
+ is_header = line_styles[i]
433
+ font_scale = HEADER_FONT_SCALE if is_header else DEFAULT_FONT_SCALE
434
+ thickness = HEADER_THICKNESS if is_header else DEFAULT_THICKNESS
435
+ if line == "":
436
+ (w, h), baseline = cv2.getTextSize("Ay", FONT, font_scale, thickness)
437
+ else:
438
+ (w, h), baseline = cv2.getTextSize(line, FONT, font_scale, thickness)
439
+ lh = h + baseline + LINE_SPACING
440
+ line_heights.append(lh)
441
+ y_positions.append(y)
442
+ y += lh
443
+ # Prepare ffmpeg
444
+ ffmpeg_cmd = (
445
+ f'ffmpeg -y '
446
+ f'-f rawvideo -pix_fmt bgr24 -s {WIDTH}x{HEIGHT} -r {FPS} -i - '
447
+ f'-an '
448
+ f'-c:v libx264 -preset {FFMPEG_PRESET} -crf {CRF} -pix_fmt yuv420p '
449
+ f'{silent_video_path}'
450
+ )
451
+ print("FFMPEG CMD:", ffmpeg_cmd)
452
+
453
+ proc = subprocess.Popen(shlex.split(ffmpeg_cmd), stdin=subprocess.PIPE, bufsize=10**8)
454
+ # Render function, now with per-line colors and styles
455
+ def render_frame(visible_text, pen_x, pen_y, anim_offset):
456
+ img = np.full((HEIGHT, WIDTH, 3), BG_COLOR, dtype=np.uint8)
457
+ lines = visible_text.split("\n")
458
+ for idx, line in enumerate(lines):
459
+ is_header = line_styles[idx]
460
+ font_scale = HEADER_FONT_SCALE if is_header else DEFAULT_FONT_SCALE
461
+ thickness = HEADER_THICKNESS if is_header else DEFAULT_THICKNESS
462
+ color = HEADER_TEXT_COLOR if is_header else DEFAULT_TEXT_COLOR
463
+ x = MARGIN_X
464
+ y = y_positions[idx]
465
+ (w, h), baseline = cv2.getTextSize(line, FONT, font_scale, thickness)
466
+ y_draw = y + h
467
+ if line != "":
468
+ cv2.putText(img, line, (x, y_draw), FONT, font_scale, color, thickness, lineType=cv2.LINE_AA)
469
+ if pen_x > 0:
470
+ offset_y = int(PEN_MOVEMENT_AMPLITUDE * math.sin(anim_offset * math.pi))
471
+ pen_tip_y = pen_y + offset_y
472
+ angle_rad = math.radians(PEN_BASE_ANGLE)
473
+ pen_end_x = pen_x + int(PEN_LENGTH * math.cos(angle_rad))
474
+ pen_end_y = pen_tip_y - int(PEN_LENGTH * math.sin(angle_rad))
475
+ cv2.line(img, (pen_x, pen_tip_y), (pen_end_x, pen_end_y), PEN_COLOR, PEN_THICKNESS)
476
+ cv2.circle(img, (pen_x, pen_tip_y), PEN_TIP_RADIUS, PEN_COLOR, -1)
477
+ return img
478
+
479
+ t0 = time.time()
480
+ frames_sent = 0
481
+ prev_visible_sub = ""
482
+ last_pen_x = 0
483
+ last_pen_y = 0
484
+ for rank, idx_in_full in enumerate(visible_indices):
485
+ visible_sub = full_text[:idx_in_full + 1]
486
+ if visible_sub != prev_visible_sub:
487
+ lines = visible_sub.split("\n")
488
+ last_line = lines[-1]
489
+ line_idx = len(lines) - 1
490
+ is_header = line_styles[line_idx]
491
+ font_scale = HEADER_FONT_SCALE if is_header else DEFAULT_FONT_SCALE
492
+ thickness = HEADER_THICKNESS if is_header else DEFAULT_THICKNESS
493
+ (w, h), baseline = cv2.getTextSize(last_line, FONT, font_scale, thickness)
494
+ pen_x = MARGIN_X + w + 5
495
+ pen_y = y_positions[line_idx] + h // 2
496
+ last_pen_x = pen_x
497
+ last_pen_y = pen_y
498
+ for anim_step in range(ANIMATION_FRAMES_PER_CHAR):
499
+ frame_img = render_frame(visible_sub, pen_x, pen_y, anim_step / ANIMATION_FRAMES_PER_CHAR)
500
+ proc.stdin.write(frame_img.tobytes())
501
+ frames_sent += 1
502
+ prev_visible_sub = visible_sub
503
+ proc.stdin.close()
504
+ proc.wait()
505
+ elapsed = time.time() - t0
506
+ print(f"Frames sent: {frames_sent}, elapsed time: {elapsed:.3f} seconds")
507
+ if not os.path.exists(silent_video_path):
508
+ print("Silent video generation failed.")
509
  return None
510
+ # Combine with audio using MoviePy
511
+ final_video_name = f"clip{id}.mp4"
512
+ final_video_path = os.path.join(CLIPS_DIR, final_video_name)
513
+ video_clip = VideoFileClip(silent_video_path)
514
+ rendered_duration = video_clip.duration
515
+ print(f"Rendered video duration: {rendered_duration:.3f}s, Audio duration: {duration:.3f}s")
516
+ if rendered_duration > 0 and duration > 0:
517
+ speed_factor = rendered_duration / duration
518
+ print(f"Adjusting video speed by factor: {speed_factor:.3f}")
519
+ video_clip = video_clip.fx(speedx, speed_factor)
520
+ final_clip = video_clip.set_audio(AudioFileClip(audio_path))
521
+ # Write final video with faster settings
522
+ 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
523
+ print(f"Final video saved at: {final_video_path}")
524
+ # Clean up
525
+ os.remove(silent_video_path)
526
+ return final_video_path