|
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 |
|
pdf_path = None |
|
original_context = "" |
|
audio_file = None |
|
current_audio_file = None |
|
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"): |
|
temp_pdf_file = None |
|
video_data = None |
|
script = None |
|
audio_file = None |
|
final_video = None |
|
max_retries = 1 |
|
|
|
try: |
|
if input_type == "Text Idea" and idea: |
|
with st.spinner("Generating initial script and code from idea..."): |
|
logging.info(f"Generating video from idea: {idea[:50]}...") |
|
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..."): |
|
logging.info(f"Generating video from PDF: {uploaded_file.name}") |
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_pdf: |
|
temp_pdf.write(uploaded_file.getvalue()) |
|
pdf_path = temp_pdf.name |
|
temp_pdf_file = 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..."): |
|
logging.info("Generating audio for the script.") |
|
try: |
|
audio_file = generate_audio(script) |
|
except ValueError as e: |
|
st.warning(f"Could not generate audio: {e}. Proceeding without audio.") |
|
audio_file = None |
|
|
|
current_manim_code = video_data["manim_code"] |
|
current_script = script |
|
current_audio_file = audio_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( |
|
{"manim_code": current_manim_code, "output_file": "output.mp4"}, |
|
current_manim_code, |
|
audio_file=current_audio_file |
|
) |
|
logging.info("Manim video creation successful.") |
|
break |
|
except subprocess.CalledProcessError as e: |
|
logging.error(f"Manim execution failed on attempt {attempt + 1}.") |
|
st.warning(f"Attempt {attempt + 1} failed. Manim error:\n```\n{e.stderr.decode() if e.stderr else 'No stderr captured.'}\n```") |
|
if attempt < max_retries: |
|
st.info("Attempting to fix the code using fallback...") |
|
logging.info("Calling fallback Gemini to fix code.") |
|
error_message = e.stderr.decode() if e.stderr else "Manim execution failed without specific error output." |
|
|
|
fixed_video_data, fixed_script = fix_manim_code( |
|
faulty_code=current_manim_code, |
|
error_message=error_message, |
|
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 by the fallback. Regenerating audio...") |
|
logging.info("Regenerating audio for updated script.") |
|
current_script = fixed_script |
|
try: |
|
current_audio_file = generate_audio(current_script) |
|
except ValueError as e: |
|
st.warning(f"Could not generate audio for fixed script: {e}. Proceeding without audio.") |
|
current_audio_file = None |
|
elif not fixed_script: |
|
st.warning("Fallback provided code but no narration. Using original audio (if any).") |
|
logging.warning("Fallback provided empty narration.") |
|
current_script = "" |
|
current_audio_file = None |
|
else: |
|
logging.info("Fallback kept the original narration.") |
|
else: |
|
st.error("Fallback failed to fix the code. Stopping.") |
|
logging.error("Fallback failed to return valid code/script.") |
|
final_video = None |
|
break |
|
else: |
|
st.error(f"Manim failed after {max_retries + 1} attempts. Could not generate video.") |
|
logging.error(f"Manim failed after {max_retries + 1} attempts.") |
|
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 is not None else "Narration could not be generated.", height=150) |
|
elif not final_video: |
|
pass |
|
else: |
|
st.error("Error: Generated video file not found after processing.") |
|
logging.error(f"Final video file '{final_video}' not found.") |
|
|
|
except FileNotFoundError as e: |
|
st.error(f"Error: A required file was not found. {str(e)}") |
|
logging.exception("FileNotFoundError during generation process.") |
|
except ValueError as e: |
|
st.error(f"Input Error: {str(e)}") |
|
logging.exception("ValueError during generation process.") |
|
except Exception as e: |
|
st.error(f"An unexpected error occurred: {str(e)}") |
|
logging.exception("Unhandled exception in main generation block.") |
|
finally: |
|
if temp_pdf_file and os.path.exists(temp_pdf_file): |
|
try: |
|
os.remove(temp_pdf_file) |
|
logging.info(f"Removed temporary file: {temp_pdf_file}") |
|
except OSError as e: |
|
logging.error(f"Error removing temporary file {temp_pdf_file}: {e}") |
|
if audio_file and os.path.exists(audio_file) and audio_file != current_audio_file: |
|
try: |
|
os.remove(audio_file) |
|
logging.info(f"Removed temporary audio file: {audio_file}") |
|
except OSError as e: |
|
logging.error(f"Error removing temporary audio file {audio_file}: {e}") |
|
if current_audio_file and os.path.exists(current_audio_file): |
|
try: |
|
os.remove(current_audio_file) |
|
logging.info(f"Removed potentially updated temporary audio file: {current_audio_file}") |
|
except OSError as e: |
|
logging.error(f"Error removing potentially updated temporary audio file {current_audio_file}: {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() |
|
|