Update video2.py
Browse files
video2.py
CHANGED
|
@@ -396,228 +396,38 @@ def audio_func(id, lines, lang):
|
|
| 396 |
return generate_tts_gtts(id, lines, lang)
|
| 397 |
|
| 398 |
|
| 399 |
-
# Example usage
|
| 400 |
-
if __name__ == "__main__":
|
| 401 |
-
# Example 1: Simple English text
|
| 402 |
-
lines = ["Hello, this is a test of the Google Text-to-Speech system."]
|
| 403 |
-
duration, path = audio_func(0, lines, "English")
|
| 404 |
-
print(f"Generated: {path} ({duration}s)")
|
| 405 |
-
|
| 406 |
-
# Example 2: Bilingual text with custom format
|
| 407 |
-
bilingual_text = "Hello, welcome to our service. வணக்கம், எங்கள் சேவைக்கு வரவேற்கிறோம். &&&Tamil"
|
| 408 |
-
duration, path = audio_func(1, bilingual_text, bilingual_text)
|
| 409 |
-
print(f"Generated: {path} ({duration}s)")
|
| 410 |
-
|
| 411 |
-
# Example 3: Tamil text
|
| 412 |
-
tamil_lines = ["வணக்கம், இது தமிழில் ஒரு சோதனை செய்தி."]
|
| 413 |
-
duration, path = audio_func(2, tamil_lines, "Tamil")
|
| 414 |
-
print(f"Generated: {path} ({duration}s)")
|
| 415 |
#-----------------------------
|
| 416 |
#---------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
def video_func(id, lines, lang):
|
| 418 |
duration, audio_path = audio_func(id, lines, lang)
|
| 419 |
if not duration or not audio_path:
|
| 420 |
print("Failed to generate audio.")
|
| 421 |
return None
|
|
|
|
| 422 |
TEXT = lines[id]
|
| 423 |
print("-----------------------------------------------------------------------------")
|
| 424 |
print(TEXT)
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
DEFAULT_THICKNESS = 2
|
| 436 |
-
HEADER_THICKNESS = 3 # Bolder for headers
|
| 437 |
-
DEFAULT_TEXT_COLOR = (0, 0, 0) # BGR Black
|
| 438 |
-
HEADER_TEXT_COLOR = (255, 0, 0) # BGR Blue
|
| 439 |
-
BG_COLOR = (255, 255, 255) # BGR White
|
| 440 |
-
silent_video_name = f"silent_video{id}.mp4"
|
| 441 |
-
silent_video_path = os.path.join(CLIPS_DIR, silent_video_name)
|
| 442 |
-
FFMPEG_PRESET = "ultrafast"
|
| 443 |
-
CRF = 28 # Increased CRF for faster encoding (lower quality, but quicker)
|
| 444 |
-
# Pen settings
|
| 445 |
-
PEN_COLOR = (0, 0, 255) # Red pen (BGR)
|
| 446 |
-
PEN_TIP_RADIUS = 5
|
| 447 |
-
PEN_LENGTH = 20
|
| 448 |
-
PEN_THICKNESS = 2
|
| 449 |
-
PEN_BASE_ANGLE = 45
|
| 450 |
-
PEN_MOVEMENT_AMPLITUDE = 10
|
| 451 |
-
# ===================================
|
| 452 |
-
|
| 453 |
-
# Helper: wrap text by pixel width using cv2.getTextSize, now with per-line styles
|
| 454 |
-
def wrap_text_cv(text, font, default_font_scale, default_thickness, max_width):
|
| 455 |
-
wrapped_lines = []
|
| 456 |
-
styles = [] # List of (is_header) for each wrapped line
|
| 457 |
-
for para in text.splitlines():
|
| 458 |
-
is_header = para.strip().startswith("###")
|
| 459 |
-
if is_header:
|
| 460 |
-
para = para.strip()[3:].strip() # Remove "### " or "###"
|
| 461 |
-
font_scale = HEADER_FONT_SCALE
|
| 462 |
-
thickness = HEADER_THICKNESS
|
| 463 |
-
else:
|
| 464 |
-
font_scale = default_font_scale
|
| 465 |
-
thickness = default_thickness
|
| 466 |
-
if para == "":
|
| 467 |
-
wrapped_lines.append("")
|
| 468 |
-
styles.append(False) # Not header
|
| 469 |
-
continue
|
| 470 |
-
words = para.split(" ")
|
| 471 |
-
cur = ""
|
| 472 |
-
for w in words:
|
| 473 |
-
candidate = w if cur == "" else cur + " " + w
|
| 474 |
-
(w_w, w_h), _ = cv2.getTextSize(candidate, font, font_scale, thickness)
|
| 475 |
-
if w_w <= max_width:
|
| 476 |
-
cur = candidate
|
| 477 |
-
else:
|
| 478 |
-
if cur != "":
|
| 479 |
-
wrapped_lines.append(cur)
|
| 480 |
-
styles.append(is_header)
|
| 481 |
-
(single_w, _), _ = cv2.getTextSize(w, font, font_scale, thickness)
|
| 482 |
-
if single_w > max_width:
|
| 483 |
-
chunk = ""
|
| 484 |
-
for ch in w:
|
| 485 |
-
cand2 = chunk + ch
|
| 486 |
-
(c_w, _), _ = cv2.getTextSize(cand2, font, font_scale, thickness)
|
| 487 |
-
if c_w <= max_width:
|
| 488 |
-
chunk = cand2
|
| 489 |
-
else:
|
| 490 |
-
wrapped_lines.append(chunk)
|
| 491 |
-
styles.append(is_header)
|
| 492 |
-
chunk = ch
|
| 493 |
-
if chunk:
|
| 494 |
-
cur = chunk
|
| 495 |
-
else:
|
| 496 |
-
cur = ""
|
| 497 |
-
else:
|
| 498 |
-
cur = w
|
| 499 |
-
if cur != "":
|
| 500 |
-
wrapped_lines.append(cur)
|
| 501 |
-
styles.append(is_header)
|
| 502 |
-
return wrapped_lines, styles
|
| 503 |
-
|
| 504 |
-
# Pre-wrap text with styles
|
| 505 |
-
text_area_width = WIDTH - 2 * MARGIN_X
|
| 506 |
-
wrapped_lines, line_styles = wrap_text_cv(TEXT, FONT, DEFAULT_FONT_SCALE, DEFAULT_THICKNESS, text_area_width)
|
| 507 |
-
full_text = "\n".join(wrapped_lines)
|
| 508 |
-
if not full_text:
|
| 509 |
-
full_text = ""
|
| 510 |
-
# Visible indices
|
| 511 |
-
if SKIP_SPACES:
|
| 512 |
-
visible_indices = [i for i, ch in enumerate(full_text) if (ch != ' ' and ch != '\n' and ch != '\t')]
|
| 513 |
else:
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
total_glyphs = len(visible_indices)
|
| 517 |
-
print(f"Wrapped lines: {len(wrapped_lines)} lines, total glyphs (counted): {total_glyphs}")
|
| 518 |
-
if total_glyphs == 0:
|
| 519 |
-
print("No text to animate.")
|
| 520 |
-
return None
|
| 521 |
-
# Minimal frames
|
| 522 |
-
min_frames = total_glyphs * ANIMATION_FRAMES_PER_CHAR
|
| 523 |
-
print(f"Rendering {min_frames} minimal frames for full text animation.")
|
| 524 |
-
# Pre-calc line heights and y_positions with per-line styles
|
| 525 |
-
line_heights = []
|
| 526 |
-
y_positions = []
|
| 527 |
-
y = MARGIN_Y
|
| 528 |
-
for i, line in enumerate(wrapped_lines):
|
| 529 |
-
is_header = line_styles[i]
|
| 530 |
-
font_scale = HEADER_FONT_SCALE if is_header else DEFAULT_FONT_SCALE
|
| 531 |
-
thickness = HEADER_THICKNESS if is_header else DEFAULT_THICKNESS
|
| 532 |
-
if line == "":
|
| 533 |
-
(w, h), baseline = cv2.getTextSize("Ay", FONT, font_scale, thickness)
|
| 534 |
-
else:
|
| 535 |
-
(w, h), baseline = cv2.getTextSize(line, FONT, font_scale, thickness)
|
| 536 |
-
lh = h + baseline + LINE_SPACING
|
| 537 |
-
line_heights.append(lh)
|
| 538 |
-
y_positions.append(y)
|
| 539 |
-
y += lh
|
| 540 |
-
# Prepare ffmpeg
|
| 541 |
-
ffmpeg_cmd = (
|
| 542 |
-
f'ffmpeg -y '
|
| 543 |
-
f'-f rawvideo -pix_fmt bgr24 -s {WIDTH}x{HEIGHT} -r {FPS} -i - '
|
| 544 |
-
f'-an '
|
| 545 |
-
f'-c:v libx264 -preset {FFMPEG_PRESET} -crf {CRF} -pix_fmt yuv420p '
|
| 546 |
-
f'{silent_video_path}'
|
| 547 |
-
)
|
| 548 |
-
print("FFMPEG CMD:", ffmpeg_cmd)
|
| 549 |
-
|
| 550 |
-
proc = subprocess.Popen(shlex.split(ffmpeg_cmd), stdin=subprocess.PIPE, bufsize=10**8)
|
| 551 |
-
# Render function, now with per-line colors and styles
|
| 552 |
-
def render_frame(visible_text, pen_x, pen_y, anim_offset):
|
| 553 |
-
img = np.full((HEIGHT, WIDTH, 3), BG_COLOR, dtype=np.uint8)
|
| 554 |
-
lines = visible_text.split("\n")
|
| 555 |
-
for idx, line in enumerate(lines):
|
| 556 |
-
is_header = line_styles[idx]
|
| 557 |
-
font_scale = HEADER_FONT_SCALE if is_header else DEFAULT_FONT_SCALE
|
| 558 |
-
thickness = HEADER_THICKNESS if is_header else DEFAULT_THICKNESS
|
| 559 |
-
color = HEADER_TEXT_COLOR if is_header else DEFAULT_TEXT_COLOR
|
| 560 |
-
x = MARGIN_X
|
| 561 |
-
y = y_positions[idx]
|
| 562 |
-
(w, h), baseline = cv2.getTextSize(line, FONT, font_scale, thickness)
|
| 563 |
-
y_draw = y + h
|
| 564 |
-
if line != "":
|
| 565 |
-
cv2.putText(img, line, (x, y_draw), FONT, font_scale, color, thickness, lineType=cv2.LINE_AA)
|
| 566 |
-
if pen_x > 0:
|
| 567 |
-
offset_y = int(PEN_MOVEMENT_AMPLITUDE * math.sin(anim_offset * math.pi))
|
| 568 |
-
pen_tip_y = pen_y + offset_y
|
| 569 |
-
angle_rad = math.radians(PEN_BASE_ANGLE)
|
| 570 |
-
pen_end_x = pen_x + int(PEN_LENGTH * math.cos(angle_rad))
|
| 571 |
-
pen_end_y = pen_tip_y - int(PEN_LENGTH * math.sin(angle_rad))
|
| 572 |
-
cv2.line(img, (pen_x, pen_tip_y), (pen_end_x, pen_end_y), PEN_COLOR, PEN_THICKNESS)
|
| 573 |
-
cv2.circle(img, (pen_x, pen_tip_y), PEN_TIP_RADIUS, PEN_COLOR, -1)
|
| 574 |
-
return img
|
| 575 |
-
|
| 576 |
-
t0 = time.time()
|
| 577 |
-
frames_sent = 0
|
| 578 |
-
prev_visible_sub = ""
|
| 579 |
-
last_pen_x = 0
|
| 580 |
-
last_pen_y = 0
|
| 581 |
-
for rank, idx_in_full in enumerate(visible_indices):
|
| 582 |
-
visible_sub = full_text[:idx_in_full + 1]
|
| 583 |
-
if visible_sub != prev_visible_sub:
|
| 584 |
-
lines = visible_sub.split("\n")
|
| 585 |
-
last_line = lines[-1]
|
| 586 |
-
line_idx = len(lines) - 1
|
| 587 |
-
is_header = line_styles[line_idx]
|
| 588 |
-
font_scale = HEADER_FONT_SCALE if is_header else DEFAULT_FONT_SCALE
|
| 589 |
-
thickness = HEADER_THICKNESS if is_header else DEFAULT_THICKNESS
|
| 590 |
-
(w, h), baseline = cv2.getTextSize(last_line, FONT, font_scale, thickness)
|
| 591 |
-
pen_x = MARGIN_X + w + 5
|
| 592 |
-
pen_y = y_positions[line_idx] + h // 2
|
| 593 |
-
last_pen_x = pen_x
|
| 594 |
-
last_pen_y = pen_y
|
| 595 |
-
for anim_step in range(ANIMATION_FRAMES_PER_CHAR):
|
| 596 |
-
frame_img = render_frame(visible_sub, pen_x, pen_y, anim_step / ANIMATION_FRAMES_PER_CHAR)
|
| 597 |
-
proc.stdin.write(frame_img.tobytes())
|
| 598 |
-
frames_sent += 1
|
| 599 |
-
prev_visible_sub = visible_sub
|
| 600 |
-
proc.stdin.close()
|
| 601 |
-
proc.wait()
|
| 602 |
-
elapsed = time.time() - t0
|
| 603 |
-
print(f"Frames sent: {frames_sent}, elapsed time: {elapsed:.3f} seconds")
|
| 604 |
-
if not os.path.exists(silent_video_path):
|
| 605 |
-
print("Silent video generation failed.")
|
| 606 |
-
return None
|
| 607 |
-
# Combine with audio using MoviePy
|
| 608 |
-
final_video_name = f"clip{id}.mp4"
|
| 609 |
-
final_video_path = os.path.join(CLIPS_DIR, final_video_name)
|
| 610 |
-
video_clip = VideoFileClip(silent_video_path)
|
| 611 |
-
rendered_duration = video_clip.duration
|
| 612 |
-
print(f"Rendered video duration: {rendered_duration:.3f}s, Audio duration: {duration:.3f}s")
|
| 613 |
-
if rendered_duration > 0 and duration > 0:
|
| 614 |
-
speed_factor = rendered_duration / duration
|
| 615 |
-
print(f"Adjusting video speed by factor: {speed_factor:.3f}")
|
| 616 |
-
video_clip = video_clip.fx(speedx, speed_factor)
|
| 617 |
-
final_clip = video_clip.set_audio(AudioFileClip(audio_path))
|
| 618 |
-
# Write final video with faster settings
|
| 619 |
-
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
|
| 620 |
-
print(f"Final video saved at: {final_video_path}")
|
| 621 |
-
# Clean up
|
| 622 |
-
os.remove(silent_video_path)
|
| 623 |
-
return final_video_path
|
|
|
|
| 396 |
return generate_tts_gtts(id, lines, lang)
|
| 397 |
|
| 398 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
#-----------------------------
|
| 400 |
#---------------------------------
|
| 401 |
+
import os
|
| 402 |
+
import subprocess
|
| 403 |
+
import shlex
|
| 404 |
+
import time
|
| 405 |
+
import math
|
| 406 |
+
import numpy as np
|
| 407 |
+
import cv2
|
| 408 |
+
from moviepy.editor import VideoFileClip, AudioFileClip
|
| 409 |
+
from moviepy.video.fx.speedx import speedx
|
| 410 |
+
|
| 411 |
+
# video.py
|
| 412 |
def video_func(id, lines, lang):
|
| 413 |
duration, audio_path = audio_func(id, lines, lang)
|
| 414 |
if not duration or not audio_path:
|
| 415 |
print("Failed to generate audio.")
|
| 416 |
return None
|
| 417 |
+
|
| 418 |
TEXT = lines[id]
|
| 419 |
print("-----------------------------------------------------------------------------")
|
| 420 |
print(TEXT)
|
| 421 |
+
|
| 422 |
+
# CREATE CLIPS DIRECTORY IF IT DOESN'T EXIST
|
| 423 |
+
os.makedirs(CLIPS_DIR, exist_ok=True)
|
| 424 |
+
|
| 425 |
+
# Call Rust function
|
| 426 |
+
final_video_path = rust_highlight.generate_video_clip(id, TEXT, audio_path, duration, CLIPS_DIR)
|
| 427 |
+
|
| 428 |
+
if final_video_path:
|
| 429 |
+
print(f"Final video saved at: {final_video_path}")
|
| 430 |
+
return final_video_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
else:
|
| 432 |
+
print("Video generation failed.")
|
| 433 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|