Skip to content

Conversation

@Ankit-Kumar20
Copy link
Owner

@Ankit-Kumar20 Ankit-Kumar20 commented Sep 2, 2025

Summary by CodeRabbit

  • New Features

    • Direct Manim video generation from a topic with automatic ~28s pacing.
    • Optional educational-style voice narration for videos.
  • Improvements

    • Upgraded AI model for chat and article generation to enhance quality and consistency.
    • More resilient Manim code generation with self-healing and validation for smoother renders.
    • Clearer execution logs and more reliable video file handling.
    • Reduced CLI noise during rendering.
  • Behavior Changes

    • Manim rendering no longer auto-opens a preview window; videos are generated for later viewing.

@vercel
Copy link

vercel bot commented Sep 2, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
murf-manim-hack Ready Ready Preview Comment Sep 2, 2025 5:05am

@coderabbitai
Copy link

coderabbitai bot commented Sep 2, 2025

Walkthrough

Configuration sets reactStrictMode to false. API routes switch model to Anthropic Claude 3.5 Sonnet. Manim generation route refactors to produce structured Manim code, adjusts execution, logging, and optional voice narration. Sandbox updates Manim CLI flags. Structured generator adds JSON-driven flow, validation/repair, pacing heuristics, and storage usage.

Changes

Cohort / File(s) Summary
Next.js Config
next.config.ts
Replaced placeholder with explicit reactStrictMode: false in exported config.
API Model Switches
src/app/api/chatbot/route.ts, src/app/api/generate-article/route.ts
Switched generateText model from openai/gpt-4o to anthropic/claude-3.5-sonnet; logic and control flow unchanged.
Manim API Refactor
src/app/api/generate-manim/route.ts
Replaced two-step breakdown/script flow with direct generateStructuredManimCode; removed prior validation import; adjusted execution to use formatted code; expanded runtime logging and error context; updated voice narration to “educational” style; return now includes formatted Manim code.
Sandbox Execution
src/utils/sandbox.ts
Updated Manim CLI flags from -pqm -v WARNING to -qm; adjusted arg ordering; emphasized video file discovery logs.
Structured Manim Generator
src/utils/structuredManimGenerator.ts
Switched to Claude 3.5 Sonnet; adopted JSON-structured generation with fallback; enhanced validation/repair of code; added pacing to ~28s with waits; educational content checks; normalization and caching retained; public signatures unchanged.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant API as API: /api/generate-manim
  participant Gen as generateStructuredManimCode
  participant Sand as executeCodeAndListFiles
  participant Store as blobStorage
  Note over API,Gen: New flow (direct structured code generation)
  User->>API: POST topic (+ includeVoice?)
  API->>Gen: Generate structured Manim JSON/code
  Gen->>Gen: Validate & repair code + pacing
  Gen-->>API: formatted Manim code
  API->>Sand: Execute code and list video files
  alt Execution success
    Sand-->>API: Files + logs + status
    API->>Store: (optional) Upload video
    API-->>User: {manimCode, videoUrl, logs}
    opt includeVoice
      API->>Gen: Generate educational narration script
      API->>Store: Generate/upload audio
      API-->>User: + voiceUrl
    end
  else Execution failure
    Sand-->>API: error + logs
    API->>API: Log full failure context
    API-->>User: Error (includes serialized logs)
  end
Loading
sequenceDiagram
  autonumber
  participant Old as Old Flow
  participant New as New Flow
  Note over Old: Breakdown -> First lesson script -> Validate -> Execute
  Note over New: Structured JSON -> Repair/Pace -> Execute
  Old->>Old: generateLessonBreakdown
  Old->>Old: generate first lesson script
  Old->>Old: validateAndFixManimCode
  Old->>Old: execute Manim
  New->>New: generateStructuredManimCode (JSON)
  New->>New: validateAndFixManimCode + pacing
  New->>New: executeCodeAndListFiles (enhanced logs)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I thump my paws at Claude’s new light,
Scripts hop from JSON, neat and tight.
Manim spins with calmer logs,
No preview flags, just wiser cogs.
Twenty-eight seconds, timed just so—
A learned burrow’s ready show.
Carrot emojis: 🥕🥕 Bravo!

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch Single_Branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/utils/structuredManimGenerator.ts (1)

366-408: Dangerous indentation rewriter will break Python block structure

This trims all lines and re-indents uniformly, destroying nested blocks (for/if/with) and causing syntax errors. Remove this step or use a proper formatter (black) in-sandbox; do not heuristically re-indent.

-  // Ensure proper indentation
-  const lines = fixedCode.split("\n");
-  const properlyIndentedLines = [];
-  let inClass = false;
-  let inMethod = false;
-  for (const line of lines) {
-    const trimmedLine = line.trim();
-    if (!trimmedLine) { properlyIndentedLines.push(""); continue; }
-    if (trimmedLine.startsWith("class ") && trimmedLine.includes("(Scene):")) {
-      inClass = true; inMethod = false; properlyIndentedLines.push(trimmedLine); continue;
-    }
-    if (inClass && trimmedLine.startsWith("def ")) {
-      inMethod = true; properlyIndentedLines.push("    " + trimmedLine); continue;
-    }
-    if (inMethod) { properlyIndentedLines.push("        " + trimmedLine); }
-    else if (inClass) { properlyIndentedLines.push("    " + trimmedLine); }
-    else { properlyIndentedLines.push(trimmedLine); }
-  }
-  fixedCode = properlyIndentedLines.join("\n");
+  // Avoid re-indenting; preserve original indentation to keep Python blocks valid
+  // If formatting is needed, run a formatter (e.g., black) in the sandbox before rendering.
🧹 Nitpick comments (17)
src/app/api/chatbot/route.ts (1)

99-101: Model switch to Claude 3.5 Sonnet: verify access and harden error paths

Good switch, but ensure the OpenRouter org/model is enabled on your key and map 401/403/429 to actionable client errors. Optionally guard missing API key.

Example guard + error mapping:

   // Get AI response using OpenRouter
-  const { text: responseText } = await generateText({
+  if (!process.env.OPENROUTER_API_KEY) {
+    return NextResponse.json({ error: 'Server misconfigured: missing OPENROUTER_API_KEY' }, { status: 500 });
+  }
+  let responseText: string;
+  try {
+    const { text } = await generateText({
       model: openrouter("anthropic/claude-3.5-sonnet"),
       messages: promptMessages
-    });
+    });
+    responseText = text;
+  } catch (e: any) {
+    const msg = e?.message || 'Upstream model error';
+    const isAuth = /401|403/.test(msg);
+    const isRate = /429/.test(msg);
+    const status = isRate ? 429 : isAuth ? 502 : 500;
+    return NextResponse.json({ error: msg }, { status });
+  }
next.config.ts (1)

4-4: Turning off reactStrictMode hides effects-only bugs in dev

Unless you have a concrete incompatibility, keep it enabled (dev-only cost). Consider env-gating instead of a hard disable.

-  reactStrictMode: false,
+  reactStrictMode: process.env.NEXT_STRICT_MODE !== 'false',
src/app/api/generate-article/route.ts (1)

179-191: Model switch OK; consider schema-driven JSON to avoid brittle parsing

Claude tends to comply, but using generateObject + zod (as in structuredManimGenerator) would remove the regex/cleaning path.

Minimal sketch:

-const { text: articleJson } = await generateText({
-  model: openrouter("anthropic/claude-3.5-sonnet"),
-  messages: [...],
-  temperature: 0.3
-});
-// ... manual JSON extraction
+const { object } = await generateObject({
+  model: openrouter("anthropic/claude-3.5-sonnet"),
+  schema: /* zod schema mirroring ParsedArticle */,
+  prompt: systemPrompt + '\nGenerate the JSON now.'
+});
+const parsedArticle = object as ParsedArticle;

Also, if you keep deduping, collisions can occur when includeAudio=true with different voice/rate/pitch—consider adding those to requestKey.

src/utils/sandbox.ts (2)

196-198: CLI change LGTM; add explicit media/output for deterministic discovery

Current -qm is fine. To make file scraping simpler and resilient, set media dir and output name.

-  ? `cd /code && manim -qm ${filename} ${sceneClassName}`
-  : `cd /code && manim -qm ${filename}`;
+  ? `cd /code && manim -qm --media_dir /code/media -o output ${filename} ${sceneClassName}`
+  : `cd /code && manim -qm --media_dir /code/media -o output ${filename}`;

315-315: Nit: logging prefix OK

The added "----------" prefix is harmless and improves greppability.

src/utils/structuredManimGenerator.ts (4)

61-88: Adopting generateObject with a Zod schema is the right move

Good shift to schema-constrained JSON and Claude 3.5 Sonnet. Consider setting temperature to 0.2 and a conservative maxTokens to reduce truncation risk.

-const result = await generateObject({
+const result = await generateObject({
   model: openrouter("anthropic/claude-3.5-sonnet"),
   schema: manimCodeSchema,
+  temperature: 0.2,
+  maxTokens: 4096,
   prompt: `...`
});

189-222: Sensible fallback path

The simplified prompt fallback is a solid recovery strategy. Consider logging result.validation errors (if exposed by the SDK) to see why the first pass failed.


433-466: Wait injection after self.play: guard against pathological patterns

Approach is fine, but dynamic regex over arbitrary code can be slow and fragile for multiline play calls. Bound replacements and skip overly long matches.

-  const playLines = fixedCode.match(/self\.play\(.*?\)/g);
+  const playLines = fixedCode.match(/self\.play\([^()\n]*\)/g); // single-line plays only
   if (playLines && playLines.length > 0) {
     ...
-      const playLineEscaped = playLine.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+      const playLineEscaped = playLine.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+      if (playLineEscaped.length > 300) continue; // safety bound
       const hasWaitAfterPlay = new RegExp(
         `${playLineEscaped}[\\s\\S]*?self\\.wait\\(`,
         "m"
       ).test(fixedCode);

311-323: Text replacement heuristic: keep existing kwargs

Your Text("...") replace will drop existing kwargs (e.g., color). Use a callback that preserves trailing args.

-.replace(/Text\("([^"]+)"\)/g, (match, text) => {
+.replace(/Text\("([^"]+)"(,[^)]+)?\)/g, (match, text, rest = "") => {
   if (text.length > 20) {
-    return `Text("${text}", font_size=32)`;
+    return `Text("${text}", font_size=32${rest})`;
   }
-  return `Text("${text}", font_size=36)`;
+  return `Text("${text}", font_size=36${rest})`;
 })
src/app/api/generate-manim/route.ts (8)

10-13: Nit: clarify the log message.

“generated and validated” happens upstream inside generateStructuredManimCode; rephrase to avoid future confusion if internals change.

-  console.log("🤖 Manim code generated and validated.");
+  console.log("🤖 Manim code generated via structured pipeline (validated upstream).");

14-18: Gate and truncate verbose code logging.

Dumping full Manim code to prod logs can be noisy and expensive. Gate by env and truncate.

-  console.log("Generated Manim code:");
-  console.log("=".repeat(50));
-  console.log(manimCode);
-  console.log("=".repeat(50));
+  if (process.env.NODE_ENV !== "production") {
+    console.log("Generated Manim code (truncated):");
+    console.log("=".repeat(50));
+    const MAX_LOG_CHARS = 4000;
+    const display =
+      manimCode.length > MAX_LOG_CHARS
+        ? manimCode.slice(0, MAX_LOG_CHARS) +
+          `\n...[${manimCode.length - MAX_LOG_CHARS} more chars]`
+        : manimCode;
+    console.log(display);
+    console.log("=".repeat(50));
+  }

26-34: Scope execution-detail logging to non-production.

Consider gating or truncating executionLogs to avoid large payloads in prod logs.

-  console.log("Execution result details:", {
+  if (process.env.NODE_ENV !== "production") {
+    console.log("Execution result details:", {
       success: executionResult.success,
       error: executionResult.error,
       executionError: executionResult.execution?.error,
       hasVideoFiles: executionResult.videoFiles?.length > 0,
       videoFilesCount: executionResult.videoFiles?.length || 0,
       executionLogs: executionResult.execution?.logs || "No execution logs available"
-  });
+    });
+  }

51-55: Prefer best-quality video instead of first item.

Pick by size (proxy for quality) to avoid returning a lower-res file when multiple outputs exist.

-console.log("📹 Video file generated:", executionResult.videoFiles[0].path);
-// 4. The video is already uploaded by executeCodeAndListFiles, so we just get the URL.
-const videoUrl = executionResult.videoFiles[0].path;
+const bestVideo = [...executionResult.videoFiles].sort((a, b) => (b.size ?? 0) - (a.size ?? 0))[0];
+console.log("📹 Video file selected:", bestVideo?.path);
+// 4. The video is already uploaded by executeCodeAndListFiles, so we just get the URL.
+const videoUrl = bestVideo?.path as string;

57-57: Strengthen typing for voiceData.

Avoid implicit any from let voiceData = null.

-let voiceData = null;
+let voiceData: Awaited<ReturnType<typeof generateVoiceNarration>> | null = null;

59-66: Allow voice options via request for flexibility and cost control.

Expose voice, style, and speakingRate in the POST body with safe defaults; keep current behavior if not provided.

// Example usage after parsing request body:
const voiceOptions = body.voiceOptions as { voice?: string; style?: string; speakingRate?: number } | undefined;
// ...
voiceData = await generateVoiceNarration(topic, script, {
  voice: voiceOptions?.voice,
  style: voiceOptions?.style ?? "educational",
  speakingRate: voiceOptions?.speakingRate,
});

80-84: Validate request body with a schema and cap topic length.

This route can trigger long-running, costly work; add strict validation (type, length, booleans).

// Example with zod (add `import { z } from "zod"`):
const BodySchema = z.object({
  topic: z.string().min(1).max(200),
  includeVoice: z.boolean().optional().default(true),
  voiceOptions: z
    .object({
      voice: z.string().optional(),
      style: z.string().optional(),
      speakingRate: z.number().min(0.5).max(2).optional(),
    })
    .optional(),
});

const parsed = BodySchema.safeParse(await request.json());
if (!parsed.success) {
  return NextResponse.json({ error: "Invalid request", issues: parsed.error.flatten() }, { status: 400 });
}
const { topic, includeVoice, voiceOptions } = parsed.data;

78-98: Operational advice: move heavy work off the request thread.

Rendering can take minutes. Consider 202 + job enqueue (queue + worker) and polling/webhooks; add auth/rate limiting to prevent abuse; include correlation IDs in logs for traceability.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c96b954 and fb87706.

📒 Files selected for processing (6)
  • next.config.ts (1 hunks)
  • src/app/api/chatbot/route.ts (1 hunks)
  • src/app/api/generate-article/route.ts (1 hunks)
  • src/app/api/generate-manim/route.ts (2 hunks)
  • src/utils/sandbox.ts (2 hunks)
  • src/utils/structuredManimGenerator.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/app/api/generate-manim/route.ts (3)
src/utils/structuredManimGenerator.ts (1)
  • generateStructuredManimCode (44-237)
src/utils/sandbox.ts (1)
  • executeCodeAndListFiles (175-477)
src/utils/voiceNarration.ts (1)
  • generateVoiceNarration (189-268)
🪛 ast-grep (0.38.6)
src/utils/structuredManimGenerator.ts

[warning] 518-518: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(escapedOriginal, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (4)
src/app/api/generate-manim/route.ts (4)

4-4: Switch to structured generator import looks good.

Centralizing generation via generateStructuredManimCode simplifies the route and keeps concerns in one place.


20-21: LGTM: explicit formatting step before execution.

Clear separation improves debuggability.


24-24: LGTM: unified sandbox execution.

Using executeCodeAndListFiles keeps the route slim and consistent with the new sandbox behavior.


71-71: Confirm intent to return full Manim code to clients.

If not needed by the UI, consider omitting or only returning in non-prod to reduce payload and potential leakage.

-  manimCode: formattedManimCode,
+  // manimCode: formattedManimCode, // Uncomment if UI needs to display/edit code

Comment on lines +36 to 49
if (!executionResult.success || executionResult.videoFiles.length === 0) {
console.error("Manim execution failed:", executionResult.error || executionResult.execution?.error);
console.error("Execution logs:", executionResult.execution?.logs || "No logs available");

// Log the Manim code that failed for debugging
console.error("Failed Manim code:");
console.error(formattedManimCode);

const errorMessage = executionResult.error || executionResult.execution?.error || "No video produced.";
const logs = executionResult.execution?.logs || ["No logs available"];

throw new Error(
`Manim execution failed. Error: ${
executionResult.error || "No video produced."
}`
`Manim execution failed. Error: ${errorMessage}. Logs: ${JSON.stringify(logs)}`
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not include execution logs in client-facing error messages.

Returning serialized logs to the client leaks internal details and can bloat responses. Log server-side (truncated), return a minimal message.

   const errorMessage = executionResult.error || executionResult.execution?.error || "No video produced.";
   const logs = executionResult.execution?.logs || ["No logs available"];
 
-  throw new Error(
-    `Manim execution failed. Error: ${errorMessage}. Logs: ${JSON.stringify(logs)}`
-  );
+  const serializedLogs = (Array.isArray(logs) ? logs.join("\n") : String(logs)).slice(0, 4000);
+  console.error("Execution logs (truncated):", serializedLogs);
+  throw new Error(`Manim execution failed. Error: ${String(errorMessage).slice(0, 500)}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!executionResult.success || executionResult.videoFiles.length === 0) {
console.error("Manim execution failed:", executionResult.error || executionResult.execution?.error);
console.error("Execution logs:", executionResult.execution?.logs || "No logs available");
// Log the Manim code that failed for debugging
console.error("Failed Manim code:");
console.error(formattedManimCode);
const errorMessage = executionResult.error || executionResult.execution?.error || "No video produced.";
const logs = executionResult.execution?.logs || ["No logs available"];
throw new Error(
`Manim execution failed. Error: ${
executionResult.error || "No video produced."
}`
`Manim execution failed. Error: ${errorMessage}. Logs: ${JSON.stringify(logs)}`
);
if (!executionResult.success || executionResult.videoFiles.length === 0) {
console.error("Manim execution failed:", executionResult.error || executionResult.execution?.error);
console.error("Execution logs:", executionResult.execution?.logs || "No logs available");
// Log the Manim code that failed for debugging
console.error("Failed Manim code:");
console.error(formattedManimCode);
const errorMessage = executionResult.error || executionResult.execution?.error || "No video produced.";
const logs = executionResult.execution?.logs || ["No logs available"];
const serializedLogs = (Array.isArray(logs) ? logs.join("\n") : String(logs)).slice(0, 4000);
console.error("Execution logs (truncated):", serializedLogs);
throw new Error(`Manim execution failed. Error: ${String(errorMessage).slice(0, 500)}`);
}

Comment on lines +470 to 541
const waitCalls = fixedCode.match(/self\.wait\(([^)]+)\)/g);
if (waitCalls) {
const totalWaitTime = waitCalls.reduce((sum, call) => {
const match = call.match(/self\.wait\(([^)]+)\)/);
return sum + (match ? parseFloat(match[1]) : 0);
}, 0);

// Count animation calls to estimate total video duration
const playCalls = fixedCode.match(/self\.play\(/g) || [];
const animationCount = playCalls.length;

// Estimate animation duration (each play call typically takes 1-2 seconds)
const estimatedAnimationTime = animationCount * 1.5;

// Total estimated video duration
const estimatedTotalDuration = totalWaitTime + estimatedAnimationTime;

console.log(`⏱️ Video Duration Analysis: ${animationCount} animations (${estimatedAnimationTime.toFixed(1)}s) + ${totalWaitTime.toFixed(1)}s waits = ${estimatedTotalDuration.toFixed(1)}s total`);

// Adjust timing to reach 28-second target
if (estimatedTotalDuration < 26) {
const timeNeeded = 28 - estimatedTotalDuration;
console.log(`📈 Extending video duration by ${timeNeeded.toFixed(1)} seconds to reach 28-second target`);

// Distribute additional time across existing wait calls
let timeDistributed = 0;
const waitReplacements = waitCalls.map((call, index) => {
const match = call.match(/self\.wait\(([^)]+)\)/);
if (match) {
const currentWait = parseFloat(match[1]);
const additionalTime = index === waitCalls.length - 1 ?
timeNeeded - timeDistributed : // Add remaining time to last wait
Math.min(timeNeeded * 0.3, 2.0); // Distribute across earlier waits

timeDistributed += additionalTime;
const newWait = Math.min(currentWait + additionalTime, 4.0); // Cap at 4 seconds

return {
original: call,
replacement: `self.wait(${newWait.toFixed(1)})`
};
}
return null;
}).filter(Boolean);

// Apply the replacements
waitReplacements.forEach((item) => {
if (item) {
const escapedOriginal = item.original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
fixedCode = fixedCode.replace(new RegExp(escapedOriginal, 'g'), item.replacement);
}
});

// If still short, add final wait
if (timeDistributed < timeNeeded) {
const finalWaitTime = timeNeeded - timeDistributed;
fixedCode = fixedCode.replace(
/self\.wait\([^)]+\)$/m,
`self.wait(${finalWaitTime.toFixed(1)})\n # Final extension to reach 28-second target\n self.wait(${Math.max(0, finalWaitTime - 2).toFixed(1)})`
);
}

} else if (estimatedTotalDuration > 32) {
console.log(`⚡ Video too long (${estimatedTotalDuration.toFixed(1)}s), optimizing for 28-second target`);

// Reduce excessive wait times
fixedCode = fixedCode.replace(/self\.wait\((4\.?\d*)\)/g, (match, time) => {
const reducedTime = Math.max(2.0, parseFloat(time) * 0.7);
return `self.wait(${reducedTime.toFixed(1)})`;
});
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Duration normalization: avoid global regex replace on user-influenced substrings

The new RegExp(escapedOriginal, 'g') approach (later) can be vulnerable to ReDoS on long inputs. Prefer tokenized adjustments or index-based edits; at minimum cap input sizes.

-      waitReplacements.forEach((item) => {
-        if (item) {
-          const escapedOriginal = item.original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
-          fixedCode = fixedCode.replace(new RegExp(escapedOriginal, 'g'), item.replacement);
-        }
-      });
+      for (const item of waitReplacements) {
+        if (!item) continue;
+        const idx = fixedCode.indexOf(item.original);
+        if (idx === -1) continue;
+        fixedCode = fixedCode.slice(0, idx) + item.replacement + fixedCode.slice(idx + item.original.length);
+      }

🏁 Script executed:

#!/bin/bash
set -eux

# Locate the structuredManimGenerator file
FILE=$(fd structuredManimGenerator.ts | head -n1)
echo "Inspecting: $FILE"

# Find all occurrences of fixedCode to understand its origin and scope
rg -n 'fixedCode' "$FILE"

# Show the section where fixedCode is declared and manipulated (around the waitReplacements logic)
sed -n '430,600p' "$FILE"

Length of output: 6724


Prefer index-based replacements over global regex for per-call accuracy
Global new RegExp(escapedOriginal, 'g') will replace all identical wait calls at once—breaking per-call timing adjustments and risking performance issues. Switch to sequential, index-based edits to preserve each replacement’s intended value.

-      waitReplacements.forEach((item) => {
-        if (item) {
-          const escapedOriginal = item.original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
-          fixedCode = fixedCode.replace(new RegExp(escapedOriginal, 'g'), item.replacement);
-        }
-      });
+      for (const item of waitReplacements) {
+        if (!item) continue;
+        const idx = fixedCode.indexOf(item.original);
+        if (idx === -1) continue;
+        fixedCode = fixedCode.slice(0, idx)
+                   + item.replacement
+                   + fixedCode.slice(idx + item.original.length);
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const waitCalls = fixedCode.match(/self\.wait\(([^)]+)\)/g);
if (waitCalls) {
const totalWaitTime = waitCalls.reduce((sum, call) => {
const match = call.match(/self\.wait\(([^)]+)\)/);
return sum + (match ? parseFloat(match[1]) : 0);
}, 0);
// Count animation calls to estimate total video duration
const playCalls = fixedCode.match(/self\.play\(/g) || [];
const animationCount = playCalls.length;
// Estimate animation duration (each play call typically takes 1-2 seconds)
const estimatedAnimationTime = animationCount * 1.5;
// Total estimated video duration
const estimatedTotalDuration = totalWaitTime + estimatedAnimationTime;
console.log(`⏱️ Video Duration Analysis: ${animationCount} animations (${estimatedAnimationTime.toFixed(1)}s) + ${totalWaitTime.toFixed(1)}s waits = ${estimatedTotalDuration.toFixed(1)}s total`);
// Adjust timing to reach 28-second target
if (estimatedTotalDuration < 26) {
const timeNeeded = 28 - estimatedTotalDuration;
console.log(`📈 Extending video duration by ${timeNeeded.toFixed(1)} seconds to reach 28-second target`);
// Distribute additional time across existing wait calls
let timeDistributed = 0;
const waitReplacements = waitCalls.map((call, index) => {
const match = call.match(/self\.wait\(([^)]+)\)/);
if (match) {
const currentWait = parseFloat(match[1]);
const additionalTime = index === waitCalls.length - 1 ?
timeNeeded - timeDistributed : // Add remaining time to last wait
Math.min(timeNeeded * 0.3, 2.0); // Distribute across earlier waits
timeDistributed += additionalTime;
const newWait = Math.min(currentWait + additionalTime, 4.0); // Cap at 4 seconds
return {
original: call,
replacement: `self.wait(${newWait.toFixed(1)})`
};
}
return null;
}).filter(Boolean);
// Apply the replacements
waitReplacements.forEach((item) => {
if (item) {
const escapedOriginal = item.original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
fixedCode = fixedCode.replace(new RegExp(escapedOriginal, 'g'), item.replacement);
}
});
// If still short, add final wait
if (timeDistributed < timeNeeded) {
const finalWaitTime = timeNeeded - timeDistributed;
fixedCode = fixedCode.replace(
/self\.wait\([^)]+\)$/m,
`self.wait(${finalWaitTime.toFixed(1)})\n # Final extension to reach 28-second target\n self.wait(${Math.max(0, finalWaitTime - 2).toFixed(1)})`
);
}
} else if (estimatedTotalDuration > 32) {
console.log(`⚡ Video too long (${estimatedTotalDuration.toFixed(1)}s), optimizing for 28-second target`);
// Reduce excessive wait times
fixedCode = fixedCode.replace(/self\.wait\((4\.?\d*)\)/g, (match, time) => {
const reducedTime = Math.max(2.0, parseFloat(time) * 0.7);
return `self.wait(${reducedTime.toFixed(1)})`;
});
}
}
// Apply the replacements
for (const item of waitReplacements) {
if (!item) continue;
const idx = fixedCode.indexOf(item.original);
if (idx === -1) continue;
fixedCode = fixedCode.slice(0, idx)
item.replacement
fixedCode.slice(idx + item.original.length);
}
🧰 Tools
🪛 ast-grep (0.38.6)

[warning] 518-518: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(escapedOriginal, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant