import streamlit as st
import os
import tempfile
import subprocess
import logging
from api.gemini import generate_video
from api.fallback_gemini import fix_manim_code
from services.manim_service import create_manim_video
from services.tts_service import generate_audio
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
def main():
st.title("Manimator")
st.write(
"Generate videos from text ideas or PDF files, You can also just paste arxiv links ;p"
)
input_type = st.radio("Choose input type:", ("Text Idea", "Upload PDF"))
idea = None
uploaded_file = None
original_context = ""
if input_type == "Text Idea":
idea = st.text_area("Enter your idea:")
if idea:
original_context = idea
else:
uploaded_file = st.file_uploader("Choose a PDF file", type="pdf")
if uploaded_file:
original_context = f"Summary/concept from PDF: {uploaded_file.name}"
if st.button("Generate Video"):
files_to_cleanup = set()
video_data = None
script = None
final_video = None
max_retries = 2 # retries for fallback
try:
# Step 1: Generate initial script and code from Gemini
if input_type == "Text Idea" and idea:
with st.spinner("Generating initial script and code from idea..."):
video_data, script = generate_video(idea=idea)
elif input_type == "Upload PDF" and uploaded_file is not None:
with st.spinner("Generating initial script and code from PDF..."):
with tempfile.NamedTemporaryFile(
delete=False, suffix=".pdf"
) as temp_pdf:
temp_pdf.write(uploaded_file.getvalue())
pdf_path = temp_pdf.name
files_to_cleanup.add(pdf_path)
video_data, script = generate_video(pdf_path=pdf_path)
else:
st.error("Please provide an idea or upload a PDF.")
return
if not video_data or not script:
st.error("Failed to generate initial script/code from Gemini.")
return
# Step 2: Generate audio and subtitles from the script
with st.spinner("Generating audio and subtitles..."):
logging.info("Generating audio and subtitles for the script.")
try:
# Unpack both audio and subtitle file paths
audio_file, subtitle_file = generate_audio(script)
if audio_file:
files_to_cleanup.add(audio_file)
if subtitle_file:
files_to_cleanup.add(subtitle_file)
except ValueError as e:
st.warning(
f"Could not generate audio: {e}. Proceeding without audio/subtitles."
)
audio_file, subtitle_file = None, None
current_manim_code = video_data["manim_code"]
current_script = script
current_audio_file = audio_file
current_subtitle_file = subtitle_file
# Step 3: Attempt to render the video, with fallback retries
for attempt in range(max_retries + 1):
try:
with st.spinner(f"Attempt {attempt + 1}: Creating Manim video..."):
logging.info(f"Attempt {attempt + 1} to create Manim video.")
final_video = create_manim_video(
video_data,
current_manim_code,
audio_file=current_audio_file,
subtitle_file=current_subtitle_file,
)
logging.info("Manim video creation successful.")
break # Exit the loop on success
except (subprocess.CalledProcessError, FileNotFoundError) as e:
error_output = e.stderr if hasattr(e, "stderr") else str(e)
logging.error(f"Manim execution failed on attempt {attempt + 1}.")
st.warning(
f"Attempt {attempt + 1} failed. Manim error:\n```\n{error_output}\n```"
)
if attempt < max_retries:
st.info("Attempting to fix the code using fallback...")
logging.info("Calling fallback Gemini to fix code.")
fixed_video_data, fixed_script = fix_manim_code(
faulty_code=current_manim_code,
error_message=error_output,
original_context=original_context,
)
if fixed_video_data and fixed_script is not None:
st.success(
"Fallback successful! Retrying video generation with fixed code."
)
logging.info("Fallback successful. Received fixed code.")
current_manim_code = fixed_video_data["manim_code"]
# If narration changed, regenerate audio and subtitles
if fixed_script != current_script and fixed_script:
st.info(
"Narration script was updated. Regenerating audio and subtitles..."
)
current_script = fixed_script
try:
new_audio, new_subtitle = generate_audio(
current_script
)
if new_audio:
files_to_cleanup.add(new_audio)
if new_subtitle:
files_to_cleanup.add(new_subtitle)
current_audio_file = new_audio
current_subtitle_file = new_subtitle
except ValueError as audio_e:
st.warning(
f"Could not generate new audio: {audio_e}."
)
current_audio_file, current_subtitle_file = (
None,
None,
)
else:
logging.info("Fallback kept the original narration.")
else:
st.error("Fallback failed to fix the code. Stopping.")
final_video = None
break
else:
st.error(
f"Manim failed after {max_retries + 1} attempts. Could not generate video."
)
final_video = None
except Exception as e:
st.error(
f"An unexpected error occurred during video creation: {str(e)}"
)
logging.exception(
"Unexpected error during create_manim_video call."
)
final_video = None
break
# Step 4: Display the final result
if final_video and os.path.exists(final_video):
st.success("Video generated successfully!")
st.video(final_video)
st.write("Generated Narration:")
st.text_area(
"Narration",
current_script if current_script else "No narration was generated.",
height=150,
)
elif not final_video and attempt >= max_retries:
# This message is shown if all retries failed
st.error("Could not generate the video after multiple attempts.")
elif not final_video:
# A general failure message
st.error("Video generation was unsuccessful.")
else:
st.error("Error: Generated video file not found after processing.")
logging.error(f"Final video file '{final_video}' not found.")
except Exception as e:
st.error(f"An unexpected and critical error occurred: {str(e)}")
logging.exception("Unhandled exception in main generation block.")
finally:
# Step 5: Clean up all generated temporary files
logging.info(f"Cleaning up {len(files_to_cleanup)} temporary files.")
for f_path in files_to_cleanup:
if f_path and os.path.exists(f_path):
try:
os.remove(f_path)
logging.info(f"Removed temporary file: {f_path}")
except OSError as e:
logging.error(f"Error removing temporary file {f_path}: {e}")
st.markdown("
", unsafe_allow_html=True)
st.markdown("---")
st.markdown(
"""
### Want to help improve this app?
- Give good Manim Examples and make PRs in guide.md, find it in repo [GitHub](https://github.com/mostlykiguess/Manimator)
- Report issues on [GitHub Issues](https://github.com/mostlykiguess/Manimator/issues)
- Email problematic prompts to me
"""
)
if __name__ == "__main__":
main()