Update app.py
Browse files
app.py
CHANGED
|
@@ -488,7 +488,7 @@ def audio_func(id: int, lines, lang: str) -> Tuple[Optional[float], Optional[str
|
|
| 488 |
traceback.print_exc()
|
| 489 |
return None, None
|
| 490 |
def create_manim_script(problem_data, script_path, audio_path, audio_length):
|
| 491 |
-
"""Generate Manim script with
|
| 492 |
|
| 493 |
settings = problem_data.get("video_settings", {
|
| 494 |
"background_color": "#0f0f23",
|
|
@@ -505,6 +505,15 @@ def create_manim_script(problem_data, script_path, audio_path, audio_length):
|
|
| 505 |
if not slides:
|
| 506 |
raise ValueError("No slides provided in input data")
|
| 507 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
# Calculate separate durations for different slide types
|
| 509 |
equation_duration = 0.0
|
| 510 |
text_title_duration = 0.0
|
|
@@ -513,15 +522,14 @@ def create_manim_script(problem_data, script_path, audio_path, audio_length):
|
|
| 513 |
slide_duration = float(slide.get("duration", 1.0))
|
| 514 |
if slide.get("type") == "equation":
|
| 515 |
equation_duration += slide_duration
|
| 516 |
-
else:
|
| 517 |
text_title_duration += slide_duration
|
| 518 |
|
| 519 |
-
#
|
| 520 |
-
|
| 521 |
|
| 522 |
-
if equation_duration > 0 and
|
| 523 |
-
equation_scale =
|
| 524 |
-
# Prevent extreme scaling (between 0.5x and 2.5x)
|
| 525 |
equation_scale = max(0.5, min(2.5, equation_scale))
|
| 526 |
else:
|
| 527 |
equation_scale = 1.0
|
|
@@ -539,7 +547,6 @@ def create_manim_script(problem_data, script_path, audio_path, audio_length):
|
|
| 539 |
title_size = settings.get("title_size", 48)
|
| 540 |
|
| 541 |
manim_code = f"""from manim import *
|
| 542 |
-
|
| 543 |
class GeneratedMathScene(Scene):
|
| 544 |
def construct(self):
|
| 545 |
# Scene settings
|
|
@@ -552,139 +559,153 @@ class GeneratedMathScene(Scene):
|
|
| 552 |
equation_size = {equation_size}
|
| 553 |
title_size = {title_size}
|
| 554 |
wrap_width = {wrap_width}
|
| 555 |
-
equation_scale = {equation_scale}
|
| 556 |
-
|
|
|
|
| 557 |
def make_inline_segments(content, color, font, text_size, equation_size):
|
| 558 |
if not content:
|
| 559 |
return VGroup()
|
| 560 |
-
|
| 561 |
segments = content.split("#")
|
| 562 |
all_lines = []
|
| 563 |
current_line = []
|
| 564 |
-
|
|
|
|
| 565 |
for segment in segments:
|
| 566 |
segment = segment.strip()
|
| 567 |
if not segment:
|
| 568 |
continue
|
| 569 |
-
|
|
|
|
| 570 |
if segment.startswith("%"):
|
| 571 |
latex_content = segment[1:]
|
| 572 |
mob = MathTex(latex_content, color=color, font_size=equation_size)
|
| 573 |
else:
|
| 574 |
mob = Text(segment, color=color, font=font, font_size=text_size)
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
|
|
|
|
|
|
| 580 |
line_group = VGroup(*current_line).arrange(RIGHT, buff=0.05)
|
| 581 |
all_lines.append(line_group)
|
| 582 |
current_line = [mob]
|
|
|
|
| 583 |
else:
|
|
|
|
| 584 |
current_line.append(mob)
|
| 585 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
if current_line:
|
| 587 |
line_group = VGroup(*current_line).arrange(RIGHT, buff=0.05)
|
| 588 |
all_lines.append(line_group)
|
| 589 |
-
|
| 590 |
if not all_lines:
|
| 591 |
return VGroup()
|
| 592 |
-
|
| 593 |
final_group = VGroup(*all_lines).arrange(DOWN, aligned_edge=LEFT, buff=0.2)
|
| 594 |
return final_group
|
| 595 |
-
|
| 596 |
def make_wrapped_paragraph(content, color, font, font_size, line_spacing=0.2):
|
| 597 |
lines = []
|
| 598 |
words = content.split()
|
| 599 |
current = ""
|
|
|
|
| 600 |
for w in words:
|
| 601 |
test = w if not current else current + " " + w
|
| 602 |
test_obj = Text(test, color=color, font=font, font_size=font_size)
|
| 603 |
-
|
|
|
|
| 604 |
current = test
|
| 605 |
else:
|
| 606 |
if current:
|
| 607 |
line_obj = Text(current, color=color, font=font, font_size=font_size)
|
| 608 |
lines.append(line_obj)
|
| 609 |
current = w
|
|
|
|
| 610 |
if current:
|
| 611 |
lines.append(Text(current, color=color, font=font, font_size=font_size))
|
|
|
|
| 612 |
if not lines:
|
| 613 |
return VGroup()
|
|
|
|
| 614 |
first_line = lines[0]
|
| 615 |
for ln in lines:
|
| 616 |
ln.align_to(first_line, LEFT)
|
|
|
|
| 617 |
para = VGroup(*lines).arrange(DOWN, aligned_edge=LEFT, buff=line_spacing)
|
| 618 |
return para
|
| 619 |
-
|
| 620 |
content_group = VGroup()
|
| 621 |
current_y = 3.0
|
| 622 |
line_spacing = 0.8
|
| 623 |
slides = {slides_repr}
|
| 624 |
-
|
| 625 |
for idx, slide in enumerate(slides):
|
| 626 |
obj = None
|
| 627 |
content = slide.get("content", "")
|
| 628 |
animation = slide.get("animation", "write_left")
|
| 629 |
base_duration = slide.get("duration", 1.0)
|
| 630 |
slide_type = slide.get("type", "text")
|
| 631 |
-
|
| 632 |
-
# Apply scale ONLY to equations
|
| 633 |
if slide_type == "equation":
|
| 634 |
duration = base_duration * equation_scale
|
| 635 |
else:
|
| 636 |
-
duration = base_duration
|
| 637 |
-
|
| 638 |
if slide_type == "title":
|
| 639 |
obj = make_inline_segments(content, highlight_color, default_font, title_size, equation_size)
|
| 640 |
-
|
| 641 |
if len(obj) == 0:
|
| 642 |
obj = Text(content, color=highlight_color, font=default_font, font_size=title_size)
|
| 643 |
-
|
|
|
|
| 644 |
if obj.width > wrap_width:
|
| 645 |
-
obj.scale_to_fit_width(wrap_width)
|
|
|
|
| 646 |
obj.move_to(ORIGIN)
|
| 647 |
self.play(FadeIn(obj), run_time=duration * 0.8)
|
| 648 |
self.wait(duration * 0.3)
|
| 649 |
self.play(FadeOut(obj), run_time=duration * 0.3)
|
| 650 |
continue
|
| 651 |
-
|
| 652 |
elif slide_type == "text":
|
| 653 |
obj = make_inline_segments(content, default_color, default_font, text_size, equation_size)
|
| 654 |
-
|
| 655 |
if len(obj) == 0:
|
| 656 |
obj = make_wrapped_paragraph(content, default_color, default_font, text_size, line_spacing=0.25)
|
| 657 |
-
|
|
|
|
| 658 |
if obj.width > wrap_width:
|
| 659 |
-
obj.scale_to_fit_width(wrap_width)
|
| 660 |
-
|
| 661 |
elif slide_type == "equation":
|
| 662 |
eq_content = content
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
mid = len(parts) // 2
|
| 667 |
-
line1 = " ".join(parts[:mid])
|
| 668 |
-
line2 = " ".join(parts[mid:])
|
| 669 |
-
wrapped_eq = f"{{{{line1}}}} \\\\ {{{{line2}}}}"
|
| 670 |
-
obj = MathTex(wrapped_eq, color=default_color, font_size=equation_size)
|
| 671 |
-
else:
|
| 672 |
-
obj = MathTex(eq_content, color=default_color, font_size=equation_size)
|
| 673 |
if obj.width > wrap_width:
|
| 674 |
-
obj.scale_to_fit_width(wrap_width)
|
| 675 |
-
|
| 676 |
if obj:
|
| 677 |
obj.to_edge(LEFT, buff=0.3)
|
| 678 |
obj.shift(UP * (current_y - obj.height / 2))
|
|
|
|
| 679 |
obj_bottom = obj.get_bottom()[1]
|
| 680 |
-
|
| 681 |
if obj_bottom < -3.5:
|
| 682 |
scroll_amount = abs(obj_bottom - (-3.5)) + 0.3
|
| 683 |
self.play(content_group.animate.shift(UP * scroll_amount), run_time=0.5)
|
| 684 |
current_y += scroll_amount
|
| 685 |
obj.shift(UP * scroll_amount)
|
| 686 |
obj.to_edge(LEFT, buff=0.3)
|
| 687 |
-
|
| 688 |
if animation == "write_left":
|
| 689 |
self.play(Write(obj), run_time=duration)
|
| 690 |
elif animation == "fade_in":
|
|
@@ -694,11 +715,11 @@ class GeneratedMathScene(Scene):
|
|
| 694 |
self.play(obj.animate.set_color(highlight_color), run_time=duration * 0.4)
|
| 695 |
else:
|
| 696 |
self.play(Write(obj), run_time=duration)
|
| 697 |
-
|
| 698 |
content_group.add(obj)
|
| 699 |
current_y -= (getattr(obj, "height", 0) + line_spacing)
|
| 700 |
self.wait(0.3)
|
| 701 |
-
|
| 702 |
if len(content_group) > 0:
|
| 703 |
final_box = SurroundingRectangle(content_group[-1], color=highlight_color, buff=0.2)
|
| 704 |
self.play(Create(final_box), run_time=0.8)
|
|
@@ -709,9 +730,12 @@ class GeneratedMathScene(Scene):
|
|
| 709 |
with open(script_path, 'w', encoding='utf-8') as f:
|
| 710 |
f.write(manim_code)
|
| 711 |
print(f"Generated script at {script_path}")
|
|
|
|
|
|
|
| 712 |
print(f"Equation scale factor: {equation_scale:.2f}x")
|
| 713 |
print(f"Text/Title duration: {text_title_duration:.2f}s (unchanged)")
|
| 714 |
print(f"Equation duration: {equation_duration:.2f}s -> {equation_duration * equation_scale:.2f}s")
|
|
|
|
| 715 |
except Exception as e:
|
| 716 |
print(f"Error writing script: {e}")
|
| 717 |
raise
|
|
|
|
| 488 |
traceback.print_exc()
|
| 489 |
return None, None
|
| 490 |
def create_manim_script(problem_data, script_path, audio_path, audio_length):
|
| 491 |
+
"""Generate Manim script with proper wrapping and audio-video sync."""
|
| 492 |
|
| 493 |
settings = problem_data.get("video_settings", {
|
| 494 |
"background_color": "#0f0f23",
|
|
|
|
| 505 |
if not slides:
|
| 506 |
raise ValueError("No slides provided in input data")
|
| 507 |
|
| 508 |
+
# FIX #2: Calculate timing overhead for accurate audio-video sync
|
| 509 |
+
num_slides = len(slides)
|
| 510 |
+
num_titles = sum(1 for s in slides if s.get("type") == "title")
|
| 511 |
+
|
| 512 |
+
overhead_time = (num_slides - num_titles) * 0.3 # wait after each content slide
|
| 513 |
+
overhead_time += num_titles * 0.4 # title animation overhead
|
| 514 |
+
overhead_time += 2.3 # final highlight + wait
|
| 515 |
+
overhead_time += (num_slides / 3) * 0.5 # estimated scroll overhead
|
| 516 |
+
|
| 517 |
# Calculate separate durations for different slide types
|
| 518 |
equation_duration = 0.0
|
| 519 |
text_title_duration = 0.0
|
|
|
|
| 522 |
slide_duration = float(slide.get("duration", 1.0))
|
| 523 |
if slide.get("type") == "equation":
|
| 524 |
equation_duration += slide_duration
|
| 525 |
+
else:
|
| 526 |
text_title_duration += slide_duration
|
| 527 |
|
| 528 |
+
# FIX #2: Subtract overhead from available time before calculating scale
|
| 529 |
+
available_time = audio_length - text_title_duration - overhead_time
|
| 530 |
|
| 531 |
+
if equation_duration > 0 and available_time > 0:
|
| 532 |
+
equation_scale = available_time / equation_duration
|
|
|
|
| 533 |
equation_scale = max(0.5, min(2.5, equation_scale))
|
| 534 |
else:
|
| 535 |
equation_scale = 1.0
|
|
|
|
| 547 |
title_size = settings.get("title_size", 48)
|
| 548 |
|
| 549 |
manim_code = f"""from manim import *
|
|
|
|
| 550 |
class GeneratedMathScene(Scene):
|
| 551 |
def construct(self):
|
| 552 |
# Scene settings
|
|
|
|
| 559 |
equation_size = {equation_size}
|
| 560 |
title_size = {title_size}
|
| 561 |
wrap_width = {wrap_width}
|
| 562 |
+
equation_scale = {equation_scale}
|
| 563 |
+
|
| 564 |
+
# FIX #1: Improved wrapping function - check width BEFORE arranging
|
| 565 |
def make_inline_segments(content, color, font, text_size, equation_size):
|
| 566 |
if not content:
|
| 567 |
return VGroup()
|
| 568 |
+
|
| 569 |
segments = content.split("#")
|
| 570 |
all_lines = []
|
| 571 |
current_line = []
|
| 572 |
+
current_width = 0.0
|
| 573 |
+
|
| 574 |
for segment in segments:
|
| 575 |
segment = segment.strip()
|
| 576 |
if not segment:
|
| 577 |
continue
|
| 578 |
+
|
| 579 |
+
# Create mobject
|
| 580 |
if segment.startswith("%"):
|
| 581 |
latex_content = segment[1:]
|
| 582 |
mob = MathTex(latex_content, color=color, font_size=equation_size)
|
| 583 |
else:
|
| 584 |
mob = Text(segment, color=color, font=font, font_size=text_size)
|
| 585 |
+
|
| 586 |
+
# FIX #1: Check width BEFORE adding to line
|
| 587 |
+
mob_width = mob.width
|
| 588 |
+
potential_width = current_width + mob_width + (0.05 * len(current_line))
|
| 589 |
+
|
| 590 |
+
if potential_width > wrap_width and len(current_line) > 0:
|
| 591 |
+
# Line is full, save it and start new line
|
| 592 |
line_group = VGroup(*current_line).arrange(RIGHT, buff=0.05)
|
| 593 |
all_lines.append(line_group)
|
| 594 |
current_line = [mob]
|
| 595 |
+
current_width = mob_width
|
| 596 |
else:
|
| 597 |
+
# Add to current line
|
| 598 |
current_line.append(mob)
|
| 599 |
+
current_width = potential_width
|
| 600 |
+
|
| 601 |
+
# Safety: If single item exceeds width, scale it down
|
| 602 |
+
if len(current_line) == 1 and mob.width > wrap_width:
|
| 603 |
+
mob.scale_to_fit_width(wrap_width * 0.95)
|
| 604 |
+
current_width = mob.width
|
| 605 |
+
|
| 606 |
+
# Add final line
|
| 607 |
if current_line:
|
| 608 |
line_group = VGroup(*current_line).arrange(RIGHT, buff=0.05)
|
| 609 |
all_lines.append(line_group)
|
| 610 |
+
|
| 611 |
if not all_lines:
|
| 612 |
return VGroup()
|
| 613 |
+
|
| 614 |
final_group = VGroup(*all_lines).arrange(DOWN, aligned_edge=LEFT, buff=0.2)
|
| 615 |
return final_group
|
| 616 |
+
|
| 617 |
def make_wrapped_paragraph(content, color, font, font_size, line_spacing=0.2):
|
| 618 |
lines = []
|
| 619 |
words = content.split()
|
| 620 |
current = ""
|
| 621 |
+
|
| 622 |
for w in words:
|
| 623 |
test = w if not current else current + " " + w
|
| 624 |
test_obj = Text(test, color=color, font=font, font_size=font_size)
|
| 625 |
+
|
| 626 |
+
if test_obj.width <= wrap_width * 0.95:
|
| 627 |
current = test
|
| 628 |
else:
|
| 629 |
if current:
|
| 630 |
line_obj = Text(current, color=color, font=font, font_size=font_size)
|
| 631 |
lines.append(line_obj)
|
| 632 |
current = w
|
| 633 |
+
|
| 634 |
if current:
|
| 635 |
lines.append(Text(current, color=color, font=font, font_size=font_size))
|
| 636 |
+
|
| 637 |
if not lines:
|
| 638 |
return VGroup()
|
| 639 |
+
|
| 640 |
first_line = lines[0]
|
| 641 |
for ln in lines:
|
| 642 |
ln.align_to(first_line, LEFT)
|
| 643 |
+
|
| 644 |
para = VGroup(*lines).arrange(DOWN, aligned_edge=LEFT, buff=line_spacing)
|
| 645 |
return para
|
| 646 |
+
|
| 647 |
content_group = VGroup()
|
| 648 |
current_y = 3.0
|
| 649 |
line_spacing = 0.8
|
| 650 |
slides = {slides_repr}
|
| 651 |
+
|
| 652 |
for idx, slide in enumerate(slides):
|
| 653 |
obj = None
|
| 654 |
content = slide.get("content", "")
|
| 655 |
animation = slide.get("animation", "write_left")
|
| 656 |
base_duration = slide.get("duration", 1.0)
|
| 657 |
slide_type = slide.get("type", "text")
|
| 658 |
+
|
| 659 |
+
# Apply scale ONLY to equations
|
| 660 |
if slide_type == "equation":
|
| 661 |
duration = base_duration * equation_scale
|
| 662 |
else:
|
| 663 |
+
duration = base_duration
|
| 664 |
+
|
| 665 |
if slide_type == "title":
|
| 666 |
obj = make_inline_segments(content, highlight_color, default_font, title_size, equation_size)
|
|
|
|
| 667 |
if len(obj) == 0:
|
| 668 |
obj = Text(content, color=highlight_color, font=default_font, font_size=title_size)
|
| 669 |
+
|
| 670 |
+
# FIX #1: Ensure title fits within screen
|
| 671 |
if obj.width > wrap_width:
|
| 672 |
+
obj.scale_to_fit_width(wrap_width * 0.95)
|
| 673 |
+
|
| 674 |
obj.move_to(ORIGIN)
|
| 675 |
self.play(FadeIn(obj), run_time=duration * 0.8)
|
| 676 |
self.wait(duration * 0.3)
|
| 677 |
self.play(FadeOut(obj), run_time=duration * 0.3)
|
| 678 |
continue
|
| 679 |
+
|
| 680 |
elif slide_type == "text":
|
| 681 |
obj = make_inline_segments(content, default_color, default_font, text_size, equation_size)
|
|
|
|
| 682 |
if len(obj) == 0:
|
| 683 |
obj = make_wrapped_paragraph(content, default_color, default_font, text_size, line_spacing=0.25)
|
| 684 |
+
|
| 685 |
+
# FIX #1: Safety check for text overflow
|
| 686 |
if obj.width > wrap_width:
|
| 687 |
+
obj.scale_to_fit_width(wrap_width * 0.95)
|
| 688 |
+
|
| 689 |
elif slide_type == "equation":
|
| 690 |
eq_content = content
|
| 691 |
+
obj = MathTex(eq_content, color=default_color, font_size=equation_size)
|
| 692 |
+
|
| 693 |
+
# FIX #1: Scale equation instead of splitting by spaces
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
if obj.width > wrap_width:
|
| 695 |
+
obj.scale_to_fit_width(wrap_width * 0.95)
|
| 696 |
+
|
| 697 |
if obj:
|
| 698 |
obj.to_edge(LEFT, buff=0.3)
|
| 699 |
obj.shift(UP * (current_y - obj.height / 2))
|
| 700 |
+
|
| 701 |
obj_bottom = obj.get_bottom()[1]
|
|
|
|
| 702 |
if obj_bottom < -3.5:
|
| 703 |
scroll_amount = abs(obj_bottom - (-3.5)) + 0.3
|
| 704 |
self.play(content_group.animate.shift(UP * scroll_amount), run_time=0.5)
|
| 705 |
current_y += scroll_amount
|
| 706 |
obj.shift(UP * scroll_amount)
|
| 707 |
obj.to_edge(LEFT, buff=0.3)
|
| 708 |
+
|
| 709 |
if animation == "write_left":
|
| 710 |
self.play(Write(obj), run_time=duration)
|
| 711 |
elif animation == "fade_in":
|
|
|
|
| 715 |
self.play(obj.animate.set_color(highlight_color), run_time=duration * 0.4)
|
| 716 |
else:
|
| 717 |
self.play(Write(obj), run_time=duration)
|
| 718 |
+
|
| 719 |
content_group.add(obj)
|
| 720 |
current_y -= (getattr(obj, "height", 0) + line_spacing)
|
| 721 |
self.wait(0.3)
|
| 722 |
+
|
| 723 |
if len(content_group) > 0:
|
| 724 |
final_box = SurroundingRectangle(content_group[-1], color=highlight_color, buff=0.2)
|
| 725 |
self.play(Create(final_box), run_time=0.8)
|
|
|
|
| 730 |
with open(script_path, 'w', encoding='utf-8') as f:
|
| 731 |
f.write(manim_code)
|
| 732 |
print(f"Generated script at {script_path}")
|
| 733 |
+
print(f"Audio length: {audio_length:.2f}s")
|
| 734 |
+
print(f"Overhead time: {overhead_time:.2f}s")
|
| 735 |
print(f"Equation scale factor: {equation_scale:.2f}x")
|
| 736 |
print(f"Text/Title duration: {text_title_duration:.2f}s (unchanged)")
|
| 737 |
print(f"Equation duration: {equation_duration:.2f}s -> {equation_duration * equation_scale:.2f}s")
|
| 738 |
+
print(f"Expected total: {text_title_duration + (equation_duration * equation_scale) + overhead_time:.2f}s")
|
| 739 |
except Exception as e:
|
| 740 |
print(f"Error writing script: {e}")
|
| 741 |
raise
|