|
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 |
|
try: |
|
|
|
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 |
|
|
|
|
|
with st.spinner("Generating audio and subtitles..."): |
|
logging.info("Generating audio and subtitles for the script.") |
|
try: |
|
|
|
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 |
|
|
|
|
|
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 |
|
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 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 |
|
|
|
|
|
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: |
|
|
|
st.error("Could not generate the video after multiple attempts.") |
|
elif not final_video: |
|
|
|
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: |
|
|
|
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("<br><br>", 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() |
|
|