freQuensy23 commited on
Commit
f485648
·
0 Parent(s):

Clean repo with background music feature (no binary files)

Browse files
.gitignore ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Video files
2
+ *.mp4
3
+ *.avi
4
+ *.mov
5
+ *.mkv
6
+ *.wmv
7
+ *.flv
8
+
9
+ # Output directories
10
+ output/
11
+ test_output/
12
+
13
+ # Python
14
+ __pycache__/
15
+ *.pyc
16
+ *.pyo
17
+ *.pyd
18
+ .Python
19
+ env/
20
+ venv/
21
+ .env
22
+
23
+ # IDE
24
+ .vscode/
25
+ .idea/
26
+ *.swp
27
+ *.swo
28
+
29
+ # OS
30
+ .DS_Store
31
+ Thumbs.db
32
+
33
+ # Temporary files
34
+ *.tmp
35
+ *.temp
context.py ADDED
File without changes
demo.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # gradio_manim_gemini_app.py – **v3**
2
+ """Gradio demo
3
+ ============
4
+ — third revision —
5
+ • **Правильная структура history** — теперь `Chatbot` получает список *пар*
6
+ `(user_text, bot_text)`. Чанки бота апдей‑тят второй элемент последней пары,
7
+ поэтому «дубли» и «робот‑юзер» исчезают.
8
+ • **Ошибки рендера** публикуются *как пользовательское сообщение* и немедленно
9
+ отправляются в Gemini; модель отвечает, мы снова пытаемся сгенерировать код —
10
+ полностью автоматический цикл, как в вашем CLI‑скрипте.
11
+ • Управление состоянием сведено к чётким этапам: `await_task`, `coding_loop`,
12
+ `review_loop`, `finished`.
13
+
14
+ Запуск:
15
+ ```bash
16
+ pip install --upgrade gradio google-genai manim_video_generator
17
+ export GEMINI_API_KEY="YOUR_KEY"
18
+ python gradio_manim_gemini_app.py
19
+ ```
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import asyncio
24
+ import os
25
+ import re
26
+ import traceback
27
+ from pathlib import Path
28
+ from typing import List, Tuple
29
+
30
+ import gradio as gr
31
+ from google import genai
32
+ from google.genai.chats import Chat
33
+ from google.genai.types import GenerateContentConfig, ThinkingConfig, UploadFileConfig
34
+
35
+ from manim_video_generator.video_executor import VideoExecutor # type: ignore
36
+ from prompts import SYSTEM_PROMPT_SCENARIO_GENERATOR, SYSTEM_PROMPT_CODEGEN, REVIEW_PROMPT
37
+
38
+ # ──────────────────────────────── Config ─────────────────────────────────────
39
+
40
+ API_KEY = os.getenv("GEMINI_API_KEY")
41
+ if not API_KEY:
42
+ raise EnvironmentError("GEMINI_API_KEY env variable not set.")
43
+
44
+ client = genai.Client(api_key=API_KEY)
45
+ MODEL = "gemini-2.5-flash-preview-05-20"
46
+ video_executor = VideoExecutor()
47
+
48
+ # ─────────────────────── Helpers to work with Chatbot ─────────────────────────
49
+
50
+ def add_user_msg(history: List[Tuple[str, str]], text: str):
51
+ """Append new (user, «») pair."""
52
+ history.append((text, ""))
53
+
54
+
55
+ def append_bot_chunk(history: List[Tuple[str, str]], chunk: str):
56
+ """Add chunk to bot part of the last pair."""
57
+ user, bot = history[-1]
58
+ history[-1] = (user, bot + chunk)
59
+
60
+
61
+ class StreamPart:
62
+ def __init__(self, text: str):
63
+ self.text = text
64
+
65
+ class ThinkingStreamPart(StreamPart): pass
66
+ class TextStreamPart(StreamPart): pass
67
+
68
+
69
+ def stream_parts(chat: Chat, prompt):
70
+ cfg = GenerateContentConfig(thinking_config=ThinkingConfig(include_thoughts=True))
71
+ for chunk in chat.send_message_stream(prompt, config=cfg):
72
+ if chunk.candidates:
73
+ cand = chunk.candidates[0]
74
+ if cand.content and cand.content.parts:
75
+ for part in cand.content.parts:
76
+ if part.text:
77
+ if part.thought:
78
+ yield ThinkingStreamPart(part.text)
79
+ else:
80
+ yield TextStreamPart(part.text)
81
+
82
+
83
+ def extract_python(md: str) -> str:
84
+ m = re.search(r"```python(.*?)```", md, re.S)
85
+ if not m:
86
+ raise ValueError("No ```python``` block found in model output.")
87
+ return m.group(1).strip()
88
+
89
+ # ────────────────────────── Session state ────────────────────────────────────
90
+
91
+ class Session(dict):
92
+ phase: str # await_task | coding_loop | review_loop | finished
93
+ chat: Chat | None
94
+ last_video: Path | None
95
+
96
+ def __init__(self):
97
+ super().__init__(phase="await_task", chat=None, last_video=None)
98
+ self.phase = "await_task"
99
+ self.chat = None
100
+ self.last_video = None
101
+
102
+ # ──────────────────────── Main chat handler ──────────────────────────────────
103
+
104
+ async def chat_handler(user_msg: str, history: List[Tuple[str, str]], state: Session):
105
+ history = history or []
106
+
107
+ # 0. Always reflect user input
108
+ add_user_msg(history, user_msg)
109
+ yield history, state, state.last_video
110
+
111
+ # bootstrap chat on very first user request
112
+ if state.phase == "await_task":
113
+ state.chat = client.chats.create(model=MODEL)
114
+ # ── Scenario generation ────────────────────────────────────────────────
115
+ scenario_prompt = f"{SYSTEM_PROMPT_SCENARIO_GENERATOR}\n\n{user_msg}"
116
+ for txt in stream_parts(state.chat, scenario_prompt):
117
+ append_bot_chunk(history, txt.text)
118
+ yield history, state, state.last_video
119
+ await asyncio.sleep(0)
120
+
121
+ append_bot_chunk(history, "\n\n*(type **continue** to proceed to code generation)*")
122
+ state.phase = "coding_loop"
123
+ yield history, state, state.last_video
124
+ return
125
+
126
+ # later phases require chat obj
127
+ if not state.chat:
128
+ append_bot_chunk(history, "⚠️ Internal error: lost chat session.")
129
+ yield history, state, state.last_video
130
+ return
131
+
132
+ # ── Coding loop ─────────────────────────────────────────────────────────────
133
+ if state.phase == "coding_loop":
134
+ if user_msg.strip().lower() not in {"c", "continue", "с"}:
135
+ append_bot_chunk(history, "⚠️ Type **continue** to move on.")
136
+ yield history, state, state.last_video
137
+ return
138
+ prompt = (
139
+ "Thanks. It is good scenario. Now generate code for it.\n\n" + SYSTEM_PROMPT_CODEGEN
140
+ )
141
+ while True: # keep cycling until render succeeds
142
+ # 1. Ask for code
143
+ for chunk in stream_parts(state.chat, prompt):
144
+ append_bot_chunk(history, chunk.text)
145
+ yield history, state, state.last_video
146
+ await asyncio.sleep(0)
147
+
148
+ full_answer = history[-1][1]
149
+ try:
150
+ py_code = extract_python(full_answer)
151
+ except ValueError as e:
152
+ # send formatting error to model, loop again
153
+ err_msg = f"Error: {e}. Please wrap the code in ```python``` fence."
154
+ prompt = err_msg
155
+ add_user_msg(history, err_msg)
156
+ yield history, state, state.last_video
157
+ continue # restart loop
158
+
159
+ # 2. Render
160
+ try:
161
+ video_path = video_executor.execute_manim_code(py_code)
162
+ state.last_video = video_path
163
+ except Exception as e:
164
+ tb = traceback.format_exc(limit=10)
165
+ err_msg = f"Error, your code is not valid: {e}. Traceback: {tb}. Please fix this error and regenerate the code again."
166
+ prompt = err_msg
167
+ add_user_msg(history, err_msg) # error == user message
168
+ yield history, state, state.last_video
169
+ continue # Gemini will answer with a fix
170
+
171
+ append_bot_chunk(history, "\n🎞️ Rendering done, uploading for review…")
172
+ yield history, state, state.last_video
173
+
174
+ # 3. Upload
175
+ try:
176
+ file_ref = client.files.upload(
177
+ file=video_path, config=UploadFileConfig(display_name=video_path.name)
178
+ )
179
+ while file_ref.state and file_ref.state.name == "PROCESSING":
180
+ await asyncio.sleep(3)
181
+ if file_ref.name:
182
+ file_ref = client.files.get(name=file_ref.name)
183
+ if file_ref.state and file_ref.state.name == "FAILED":
184
+ raise RuntimeError("Gemini failed to process upload")
185
+ except Exception as up_err:
186
+ err_msg = f"Upload error: {up_err}"
187
+ add_user_msg(history, err_msg)
188
+ yield history, state, state.last_video
189
+ continue # ask Gemini to fix
190
+
191
+ # 4. Review
192
+ review_prompt = [file_ref, REVIEW_PROMPT]
193
+ add_user_msg(history, "# system → review video")
194
+ for chunk in stream_parts(state.chat, review_prompt):
195
+ append_bot_chunk(history, chunk.text)
196
+ yield history, state, state.last_video
197
+ await asyncio.sleep(0)
198
+
199
+ if "no issues found" in history[-1][1].lower():
200
+ append_bot_chunk(history, "\n✅ Video accepted! 🎉")
201
+ state.phase = "finished"
202
+ yield history, state, state.last_video
203
+ return
204
+ else:
205
+ append_bot_chunk(history, "\n🔄 Issues found. Trying again…")
206
+ # let the loop run again (Gemini will generate corrected code)
207
+ continue
208
+
209
+ # ── Finished phase ──────────────────────────────────────────────────────────
210
+ if state.phase == "finished":
211
+ append_bot_chunk(history, "Session complete. Refresh page to start over.")
212
+ yield history, state, state.last_video
213
+
214
+ # ─────────────────────────────── UI ──────────────────────────────────────────
215
+
216
+ def build_app():
217
+ with gr.Blocks(title="Gemini‑Manim Video Creator") as demo:
218
+ gr.Markdown("# 🎬 Gemini‑Manim Video Creator\nCreate an explanatory animation from a single prompt.")
219
+
220
+ history = gr.Chatbot(height=850)
221
+ session = gr.State(Session())
222
+
223
+ with gr.Row():
224
+ txt = gr.Textbox(placeholder="Describe the concept…", scale=4)
225
+ btn = gr.Button("Send", variant="primary")
226
+
227
+ vid = gr.Video(label="Rendered video", interactive=False)
228
+
229
+ def get_vid(state: Session):
230
+ return state.last_video if state.last_video else None
231
+
232
+ btn.click(chat_handler, [txt, history, session], [history, session, vid]) \
233
+ .then(lambda: "", None, txt)
234
+
235
+ return demo
236
+
237
+
238
+ if __name__ == "__main__":
239
+ build_app().launch()
main.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import time
3
+ from google import genai
4
+ import os
5
+ from dotenv import load_dotenv
6
+ from google.genai.chats import Chat
7
+ from manim_video_generator.video_executor import VideoExecutor
8
+ from prompts import SYSTEM_PROMPT_SCENARIO_GENERATOR, SYSTEM_PROMPT_CODEGEN, REVIEW_PROMPT
9
+ from google.genai.types import (
10
+ GenerateContentResponse,
11
+ ThinkingConfig,
12
+ GenerateContentConfig,
13
+ UploadFileConfig,
14
+ )
15
+ from pathlib import Path
16
+ import traceback
17
+
18
+ load_dotenv()
19
+
20
+
21
+
22
+ async def main():
23
+ client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
24
+ video_executor = VideoExecutor()
25
+
26
+ chat: Chat = client.chats.create(model="gemini-2.5-flash-preview-05-20")
27
+
28
+ user_task = input("Enter your task: ")
29
+ assert (
30
+ len(user_task) > 0 and len(user_task) < 10000
31
+ ), "Task must be between 1 and 10000 characters"
32
+
33
+ user_input = SYSTEM_PROMPT_SCENARIO_GENERATOR + "\n\n" + user_task
34
+ # Generate scenario
35
+ for iter in range(1000):
36
+ answer = ""
37
+ chunk: GenerateContentResponse
38
+ for chunk in chat.send_message_stream(
39
+ user_input,
40
+ config=GenerateContentConfig(
41
+ thinking_config=ThinkingConfig(
42
+ include_thoughts=True,
43
+ ),
44
+ ),
45
+ ):
46
+ print()
47
+ if chunk.candidates:
48
+ candidate = chunk.candidates[0]
49
+ if candidate.content and candidate.content.parts:
50
+ for part in candidate.content.parts:
51
+ if part.thought:
52
+ print('💭: ', part.text, end="", flush=True)
53
+ elif part.text:
54
+ print(part.text, end="", flush=True)
55
+ answer += part.text
56
+ user_input = input("Answer answer to scenario manager or continue (c)?")
57
+ if user_input.lower() in ("c", "continue", 'с'):
58
+ print("Scenario created")
59
+ scenario = answer
60
+ break
61
+
62
+ # Generate code
63
+ user_input = "Thanks. It is good scenario. Now generate code for it.\n\n" + SYSTEM_PROMPT_CODEGEN
64
+ print('Generating code...')
65
+ for iter in range(1000):
66
+ answer = ""
67
+ chunk: GenerateContentResponse
68
+ for chunk in chat.send_message_stream(
69
+ user_input,
70
+ config=GenerateContentConfig(
71
+ thinking_config=ThinkingConfig(
72
+ include_thoughts=True,
73
+ ),
74
+ ),
75
+ ):
76
+ print()
77
+ if chunk.candidates:
78
+ candidate = chunk.candidates[0]
79
+ if candidate.content and candidate.content.parts:
80
+ for part in candidate.content.parts:
81
+ if part.thought:
82
+ print('💭: ', part.text, end="", flush=True)
83
+ elif part.text:
84
+ print(part.text, end="", flush=True)
85
+ answer += part.text
86
+ try:
87
+ code = answer.split("```python")[1].split("```")[0]
88
+ except Exception as e:
89
+ print(f"Error: {e}")
90
+ user_input = f"Error, your answer is not valid formated manim code."
91
+ continue
92
+
93
+
94
+ try:
95
+ video_path: Path = video_executor.execute_manim_code(code)
96
+ print(f"Video generated at {video_path}")
97
+ except Exception as e:
98
+ print(f"Error: {e}")
99
+ traceback_str = traceback.format_exc()
100
+ user_input = f"Error, your code is not valid: {e}. Please fix it. Traceback: {traceback_str}"
101
+ continue
102
+
103
+ myfile = client.files.upload(file=video_path.absolute(),
104
+ config=UploadFileConfig(
105
+ display_name=video_path.name
106
+ ))
107
+ assert myfile.name, "File name is not set"
108
+ assert myfile.state, "File state is not set"
109
+
110
+ print('Uploading video file to google genai...')
111
+ while myfile.state.name == "PROCESSING":
112
+ print('.', end='', flush=True)
113
+ time.sleep(10)
114
+ myfile = client.files.get(name=myfile.name)
115
+ print(f"File uploaded at {myfile.name}")
116
+
117
+ if myfile.state.name == "FAILED":
118
+ raise ValueError(myfile.state.name)
119
+
120
+ print(f"File uploaded at {myfile.name}")
121
+
122
+
123
+
124
+ for chunk in chat.send_message_stream(
125
+ [myfile, REVIEW_PROMPT],
126
+ config=GenerateContentConfig(
127
+ thinking_config=ThinkingConfig(
128
+ include_thoughts=True,
129
+ ),
130
+ ),
131
+ ):
132
+ if chunk.candidates:
133
+ candidate = chunk.candidates[0]
134
+ if candidate.content and candidate.content.parts:
135
+ for part in candidate.content.parts:
136
+ if part.text:
137
+ if part.thought:
138
+ print('💭: ', part.text, end="", flush=True)
139
+ else:
140
+ print(part.text, end="", flush=True)
141
+ answer += part.text
142
+ if "no issues found" in answer.lower():
143
+ print("No issues found")
144
+ break
145
+ else:
146
+ print("Issues found")
147
+ user_prompt = input("Prompt for fixing issues (or n to exit): ")
148
+ if user_prompt.lower() == 'n':
149
+ break
150
+ else:
151
+ user_input = f"Fix this problems please."
152
+ if user_prompt.strip():
153
+ user_input += f" TIP: {user_prompt}"
154
+
155
+
156
+ if __name__ == "__main__":
157
+ asyncio.run(main())
manim_video_generator/gemini_client.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import google.generativeai as genai
3
+ from loguru import logger
4
+ from dotenv import load_dotenv
5
+ from typing import Optional, TYPE_CHECKING
6
+ import re
7
+
8
+ if TYPE_CHECKING:
9
+ from .context_manager import ConversationContext
10
+
11
+ load_dotenv()
12
+
13
+
14
+ class GeminiClient:
15
+ def __init__(self, api_key: str = None, context_manager: Optional['ConversationContext'] = None):
16
+ self.api_key = api_key or os.getenv("GEMINI_API_KEY")
17
+ if not self.api_key:
18
+ raise ValueError("GEMINI_API_KEY not found in environment variables")
19
+
20
+ genai.configure(api_key=self.api_key)
21
+ self.model = genai.GenerativeModel("gemini-2.0-flash-thinking-exp")
22
+ self.context_manager = context_manager
23
+ logger.info("Gemini client initialized")
24
+
25
+ def generate_manim_code(self, user_request: str) -> str:
26
+ """Generate Manim code based on the user's request"""
27
+
28
+ # Системный промпт с инструкциями
29
+ system_prompt = """You are a Manim code generator. Your ONLY task is to generate executable Python code using the Manim library.
30
+
31
+ CRITICAL RULES:
32
+ - You MUST respond with ONLY Python code, nothing else
33
+ - NO explanations, NO text, NO comments outside the code
34
+ - The code MUST be ready to execute immediately
35
+ - The response should start with "from manim import *" and contain a complete Scene class
36
+
37
+ CODE REQUIREMENTS:
38
+ 1. Always import: from manim import *
39
+ 2. Create a class that inherits from Scene (default name: VideoScene)
40
+ 3. Implement the construct(self) method
41
+ 4. Use self.play() for animations
42
+ 5. End with self.wait(1) or self.wait(2)
43
+ 6. Use Text() instead of MathTex() for all text (MathTex requires LaTeX setup)
44
+ 7. For mathematical formulas, use Text() with Unicode symbols: ², ³, ∫, ∑, π, etc.
45
+ 8. Use simple geometric shapes: Square(), Circle(), Rectangle(), Line(), etc.
46
+ 9. Common animations: Write(), Create(), Transform(), FadeIn(), FadeOut(), DrawBorderThenFill()
47
+ 10. Position objects with .move_to(), .shift(), .to_edge(), .next_to()
48
+
49
+ EXAMPLE OUTPUT FORMAT (this is exactly how your response should look):
50
+ ```python
51
+ from manim import *
52
+
53
+ class VideoScene(Scene):
54
+ def construct(self):
55
+ title = Text("Example Title")
56
+ self.play(Write(title))
57
+ self.wait(2)
58
+ ```
59
+
60
+ IMPORTANT:
61
+ - Your entire response must be valid Python code
62
+ - Do not include any text before or after the code
63
+ - If the request is in any language other than English, still generate code with English variable names and comments
64
+ - Focus on creating visually appealing animations that demonstrate the requested concept"""
65
+
66
+ # Получаем контекст предыдущих сообщений
67
+ messages = []
68
+ if self.context_manager:
69
+ messages = self.context_manager.get_context_for_gemini()
70
+
71
+ # Добавляем системный промпт и текущий запрос если истории нет
72
+ if not messages:
73
+ messages = [
74
+ {"role": "user", "parts": [{"text": f"{system_prompt}\n\nCreate a video for the request: {user_request}"}]}
75
+ ]
76
+
77
+ # Debug логирование полного контекста
78
+ logger.debug(f"Sending {len(messages)} messages to Gemini:")
79
+ for i, message in enumerate(messages):
80
+ logger.debug(f"Message {i+1} ({message['role']}): {message['parts'][0]['text'][:200]}{'...' if len(message['parts'][0]['text']) > 200 else ''}")
81
+
82
+ logger.info(f"Sending request to Gemini with {len(messages)} context messages")
83
+ response = self.model.generate_content(messages)
84
+
85
+ # Extract code from the response
86
+ code = response.text.strip()
87
+
88
+ # Улучшенное извлечение кода
89
+ if code.startswith("```python"):
90
+ # Стандартный случай: код начинается с ```python
91
+ code = code[9:]
92
+ if code.endswith("```"):
93
+ code = code[:-3]
94
+ elif code.startswith("```"):
95
+ # Код начинается с ```
96
+ code = code[3:]
97
+ if code.endswith("```"):
98
+ code = code[:-3]
99
+ else:
100
+ # Ищем первый блок кода внутри текста
101
+ python_match = re.search(r'```python\s*\n(.*?)\n```', code, re.DOTALL)
102
+ if python_match:
103
+ code = python_match.group(1)
104
+ else:
105
+ # Ищем любой блок ```
106
+ code_match = re.search(r'```\s*\n(.*?)\n```', code, re.DOTALL)
107
+ if code_match:
108
+ code = code_match.group(1)
109
+ # Если нет блоков кода, оставляем как есть (весь ответ)
110
+
111
+ code = code.strip()
112
+ logger.info("Manim code generated successfully")
113
+ return code
114
+
115
+ def fix_manim_code(self, current_code: str, error_trace: str, user_hint: str | None = None) -> str:
116
+ """Fix Manim code using the error trace and optional user hint"""
117
+
118
+ # Получаем контекст
119
+ messages = []
120
+ if self.context_manager:
121
+ messages = self.context_manager.get_context_for_gemini()
122
+
123
+ # Если контекста нет, создаем базовое сообщение
124
+ if not messages:
125
+ hint_block = f"\nUser hint: {user_hint}" if user_hint else ""
126
+ prompt = f"""
127
+ You are an assistant that helps fix errors in Manim code.
128
+
129
+ Current code:
130
+ ```python
131
+ {current_code}
132
+ ```
133
+
134
+ Execution error:
135
+ {error_trace}
136
+ {hint_block}
137
+
138
+ Provide the corrected code. Return ONLY the Python code without explanations.
139
+ """
140
+ messages = [{"role": "user", "parts": [{"text": prompt}]}]
141
+
142
+ # Debug логирование полного контекста
143
+ logger.debug(f"Sending {len(messages)} messages to Gemini for code fix:")
144
+ for i, message in enumerate(messages):
145
+ logger.debug(f"Message {i+1} ({message['role']}): {message['parts'][0]['text'][:200]}{'...' if len(message['parts'][0]['text']) > 200 else ''}")
146
+
147
+ logger.info("Sending code fix request to Gemini")
148
+ response = self.model.generate_content(messages)
149
+
150
+ fixed = response.text.strip()
151
+
152
+ # Улучшенное извлечение кода
153
+ if fixed.startswith("```python"):
154
+ # Стандартный случай: код начинается с ```python
155
+ fixed = fixed[9:]
156
+ if fixed.endswith("```"):
157
+ fixed = fixed[:-3]
158
+ elif fixed.startswith("```"):
159
+ # Код начинается с ```
160
+ fixed = fixed[3:]
161
+ if fixed.endswith("```"):
162
+ fixed = fixed[:-3]
163
+ else:
164
+ # Ищем первый блок кода внутри текста
165
+ python_match = re.search(r'```python\s*\n(.*?)\n```', fixed, re.DOTALL)
166
+ if python_match:
167
+ fixed = python_match.group(1)
168
+ else:
169
+ # Ищем любой блок ```
170
+ code_match = re.search(r'```\s*\n(.*?)\n```', fixed, re.DOTALL)
171
+ if code_match:
172
+ fixed = code_match.group(1)
173
+ # Если нет блоков кода, оставляем как есть (весь ответ)
174
+
175
+ fixed = fixed.strip()
176
+ logger.info("Received fixed code from Gemini")
177
+ return fixed
manim_video_generator/video_executor.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ import subprocess
4
+ import shutil
5
+ from pathlib import Path
6
+ from loguru import logger
7
+ from moviepy.editor import VideoFileClip, AudioFileClip, CompositeAudioClip, concatenate_audioclips
8
+
9
+
10
+ class VideoExecutor:
11
+ def __init__(self, output_dir: str = "output"):
12
+ self.output_dir = Path(output_dir)
13
+ self.output_dir.mkdir(exist_ok=True)
14
+ self.music_file = Path("data/music.mp3")
15
+ logger.info(f"VideoExecutor initialized, output directory: {self.output_dir}")
16
+
17
+ if not self.music_file.exists():
18
+ logger.warning(f"Background music file not found: {self.music_file}")
19
+
20
+ def execute_manim_code(self, code: str, scene_name: str = "VideoScene") -> Path:
21
+ """Execute Manim code in an isolated environment and return the video path"""
22
+
23
+ with tempfile.TemporaryDirectory() as temp_dir:
24
+ temp_path = Path(temp_dir)
25
+
26
+ # Create a temporary file with the code
27
+ code_file = temp_path / "scene.py"
28
+ with open(code_file, "w", encoding="utf-8") as f:
29
+ f.write(code)
30
+
31
+ logger.info(f"Code written to temporary file: {code_file}")
32
+
33
+ # Run Manim
34
+ output_file = self._run_manim(code_file, scene_name, temp_path)
35
+
36
+ # Add background music
37
+ output_file = self._add_background_music(output_file, temp_path)
38
+
39
+ # Copy the result to the output folder
40
+ final_output = self._copy_to_output(output_file)
41
+
42
+ return final_output
43
+
44
+ def _run_manim(self, code_file: Path, scene_name: str, temp_dir: Path) -> Path:
45
+ """Run Manim to render the video"""
46
+
47
+ cmd = [
48
+ "manim", "render",
49
+ str(code_file),
50
+ scene_name,
51
+ "--format", "mp4",
52
+ "-q", "m", # medium quality: 'l'=low, 'm'=medium, 'h'=high, 'p'=4k, 'k'=8k
53
+ "--output_file", "video.mp4"
54
+ ]
55
+
56
+ logger.info(f"Executing command: {' '.join(cmd)}")
57
+
58
+ # Execute the command in the temporary directory
59
+ result = subprocess.run(
60
+ cmd,
61
+ cwd=temp_dir,
62
+ capture_output=True,
63
+ text=True,
64
+ timeout=300 # 5 minutes max
65
+ )
66
+
67
+ if result.returncode != 0:
68
+ logger.error(f"Manim execution error: {result.stderr}")
69
+ raise RuntimeError(f"Manim exited with error: {result.stderr}")
70
+
71
+ logger.info("Manim executed successfully")
72
+
73
+ # Look for the created video
74
+ media_dir = temp_dir / "media"
75
+ if not media_dir.exists():
76
+ raise FileNotFoundError("Media folder not found after running Manim")
77
+
78
+ # Search for mp4 file recursively
79
+ video_files = list(media_dir.rglob("*.mp4"))
80
+ if not video_files:
81
+ raise FileNotFoundError("Video file not found after rendering")
82
+
83
+ # Take the most recent file
84
+ video_file = max(video_files, key=lambda f: f.stat().st_mtime)
85
+ logger.info(f"Video file found: {video_file}")
86
+
87
+ return video_file
88
+
89
+ def _add_background_music(self, video_file: Path, temp_dir: Path) -> Path:
90
+ """Add background music to the video"""
91
+ logger.info("Adding background music to video")
92
+
93
+ video_with_music = temp_dir / "video_with_music.mp4"
94
+
95
+ # Load video and music
96
+ video = VideoFileClip(str(video_file))
97
+ music = AudioFileClip(str(self.music_file))
98
+
99
+ # Adjust music duration to match video
100
+ video_duration = video.duration
101
+ if music.duration > video_duration:
102
+ # Trim music if longer than video
103
+ music = music.subclip(0, video_duration)
104
+ else:
105
+ # Loop music if shorter than video
106
+ loops_needed = int(video_duration // music.duration) + 1
107
+ music = concatenate_audioclips([music] * loops_needed).subclip(0, video_duration)
108
+
109
+ # Set music volume lower to not overpower original audio (if any)
110
+ music = music.volumex(0.3) # 30% volume
111
+
112
+ # Combine original audio with music
113
+ if video.audio is not None:
114
+ final_audio = CompositeAudioClip([video.audio, music])
115
+ else:
116
+ final_audio = music
117
+
118
+ # Create final video with music
119
+ final_video = video.set_audio(final_audio)
120
+ final_video.write_videofile(str(video_with_music), codec='libx264', audio_codec='aac')
121
+
122
+ # Clean up
123
+ video.close()
124
+ music.close()
125
+ final_video.close()
126
+
127
+ logger.info(f"Background music added: {video_with_music}")
128
+ return video_with_music
129
+
130
+ def _copy_to_output(self, video_file: Path) -> Path:
131
+ """Copy video to the output folder with a unique name"""
132
+
133
+ import time
134
+ timestamp = int(time.time())
135
+ output_file = self.output_dir / f"video_{timestamp}.mp4"
136
+
137
+ shutil.copy2(video_file, output_file)
138
+ logger.info(f"Video copied to: {output_file}")
139
+
140
+ return output_file
prompts.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ SYSTEM_PROMPT_SCENARIO_GENERATOR = """
2
+ You are a scenario generator for short manim video (5-30 seconds). Your first task is to generate a scenario for a video.
3
+ User will provide you a video idea and you will need to generate a scenario for the video. You have a technical restrictions:
4
+ - The video should be 5-30 seconds long.
5
+ - The video will be generated using Manim library
6
+
7
+ Answer is a normal text, not a code. If you have any questions, ask user for clarification.
8
+ """
9
+
10
+ SYSTEM_PROMPT_CODEGEN = """
11
+ Now you are a Manim code generator. Your ONLY task is to generate executable Python code using the Manim library.
12
+ User will provide you a video idea that he has discussed with scenario manager and you will need to generate a Manim code that will execute the user request.
13
+ Format code inside ```python and ``` tags. Answer only code, no other text. Generate your code in one file. P.S. Do not use latex for text.
14
+ """
15
+
16
+ REVIEW_PROMPT = """
17
+ You are a short video reviewer. User with video producer and coder has generated a video. Your task is to review the video and make sure it is correct.
18
+ First, check the consistency of the video. The text should not overlap, the image should not go off frame, and objects should not hang in the air but be in their proper places.
19
+ If you see any issues, please point them out in text format, othervise answer "No issues found".
20
+ """
21
+
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ google-genai
3
+ manim
4
+ moviepy==1.0.3
5
+ loguru