hlassiege/darylpublic Fork
⑂ 99a15e2
Commits
⬇ Clone ▾
git clone https://git.rickub.com/hlassiege/daryl.git
git clone ssh://git@rickub.com/hlassiege/daryl.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

fix(render): fail when a composition never registers its timeline

HyperFrames waits 45 s for a sub-composition's window.__timelines entry, then renders anyway and exits 0: a composition whose script threw comes out as a frozen image. The render now fails, with the script error the CLI logged.
hlassiege committed 2026-10-24T21:08:15+02:00 Browse files
99a15e2 parent: 4114666
modified backend/studio/modules/animations/render.py +17 -0
@@ -26,6 +26,10 @@ RENDER_OPTIONS = ["--workers", "12"]
2626 GPU_OPTIONS = ["--gpu"]
2727
2828 _PERCENT = re.compile(r"(\d{1,3})%\s+(.*)")
29+# les avertissements du CLI quand une sous-composition n'a pas enregistré window.__timelines[id]
30+_UNREGISTERED = re.compile(r"Sub-composition timelines not registered|still waiting on the failed script")
31+# et l'erreur de son script (une par worker : dédoublonnée)
32+_SCRIPT_ERROR = re.compile(r"Composition script failed (.*)|\[Browser:PAGEERROR\] (.*)")
2933 STAGES = {"Capturing": "capture des images", "Encoding": "encodage", "Assembling": "assemblage",
3034 "Extracting": "lecture de la vidéo", "Compiling": "préparation", "Render complete": "terminé"}
3135
@@ -122,6 +126,9 @@ def render(directory: Path, output: Path, on_progress: Callable[[float, str], No
122126 cwd=directory, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
123127 text=True, encoding="utf-8", errors="replace")
124128 tail: list[str] = []
129+ # une composition dont le script plante n'enregistre pas sa timeline : le CLI rend quand même, image figée
130+ page_errors: list[str] = []
131+ unregistered = False
125132 try:
126133 # le CLI redessine sa barre avec \r : le mode texte en fait des lignes
127134 for line in proc.stdout:
@@ -130,6 +137,11 @@ def render(directory: Path, output: Path, on_progress: Callable[[float, str], No
130137 if not line:
131138 continue
132139 tail = (tail + [line])[-40:]
140+ failed = _SCRIPT_ERROR.search(line)
141+ if failed:
142+ page_errors.append((failed.group(1) or failed.group(2)).strip())
143+ if _UNREGISTERED.search(line):
144+ unregistered = True
133145 found = _PERCENT.search(line)
134146 if found:
135147 label = found.group(2)
@@ -139,6 +151,11 @@ def render(directory: Path, output: Path, on_progress: Callable[[float, str], No
139151 # les dernieres lignes hors traces techniques : c'est la que le CLI explique l'echec
140152 errors = [t for t in tail if not t.startswith("[INFO]")] or tail
141153 raise RuntimeError("le rendu HyperFrames a échoué : " + " | ".join(errors[-5:])[-800:])
154+ if unregistered:
155+ output.unlink(missing_ok=True)
156+ why = " | ".join(dict.fromkeys(page_errors))[-800:] or "aucune erreur de script remontée"
157+ raise RuntimeError(f"une composition n'a pas enregistré sa timeline (son script a planté ?), "
158+ f"le rendu serait figé : {why}")
142159 finally:
143160 if proc.poll() is None:
144161 _kill_tree(proc)
@@ -26,6 +26,10 @@ RENDER_OPTIONS = ["--workers", "12"]
26 GPU_OPTIONS = ["--gpu"]26 GPU_OPTIONS = ["--gpu"]
27 27
28 _PERCENT = re.compile(r"(\d{1,3})%\s+(.*)")28 _PERCENT = re.compile(r"(\d{1,3})%\s+(.*)")
29+# les avertissements du CLI quand une sous-composition n'a pas enregistré window.__timelines[id]
30+_UNREGISTERED = re.compile(r"Sub-composition timelines not registered|still waiting on the failed script")
31+# et l'erreur de son script (une par worker : dédoublonnée)
32+_SCRIPT_ERROR = re.compile(r"Composition script failed (.*)|\[Browser:PAGEERROR\] (.*)")
29 STAGES = {"Capturing": "capture des images", "Encoding": "encodage", "Assembling": "assemblage",33 STAGES = {"Capturing": "capture des images", "Encoding": "encodage", "Assembling": "assemblage",
30 "Extracting": "lecture de la vidéo", "Compiling": "préparation", "Render complete": "terminé"}34 "Extracting": "lecture de la vidéo", "Compiling": "préparation", "Render complete": "terminé"}
31 35
@@ -122,6 +126,9 @@ def render(directory: Path, output: Path, on_progress: Callable[[float, str], No
122 cwd=directory, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,126 cwd=directory, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
123 text=True, encoding="utf-8", errors="replace")127 text=True, encoding="utf-8", errors="replace")
124 tail: list[str] = []128 tail: list[str] = []
129+ # une composition dont le script plante n'enregistre pas sa timeline : le CLI rend quand même, image figée
130+ page_errors: list[str] = []
131+ unregistered = False
125 try:132 try:
126 # le CLI redessine sa barre avec \r : le mode texte en fait des lignes133 # le CLI redessine sa barre avec \r : le mode texte en fait des lignes
127 for line in proc.stdout:134 for line in proc.stdout:
@@ -130,6 +137,11 @@ def render(directory: Path, output: Path, on_progress: Callable[[float, str], No
130 if not line:137 if not line:
131 continue138 continue
132 tail = (tail + [line])[-40:]139 tail = (tail + [line])[-40:]
140+ failed = _SCRIPT_ERROR.search(line)
141+ if failed:
142+ page_errors.append((failed.group(1) or failed.group(2)).strip())
143+ if _UNREGISTERED.search(line):
144+ unregistered = True
133 found = _PERCENT.search(line)145 found = _PERCENT.search(line)
134 if found:146 if found:
135 label = found.group(2)147 label = found.group(2)
@@ -139,6 +151,11 @@ def render(directory: Path, output: Path, on_progress: Callable[[float, str], No
139 # les dernieres lignes hors traces techniques : c'est la que le CLI explique l'echec151 # les dernieres lignes hors traces techniques : c'est la que le CLI explique l'echec
140 errors = [t for t in tail if not t.startswith("[INFO]")] or tail152 errors = [t for t in tail if not t.startswith("[INFO]")] or tail
141 raise RuntimeError("le rendu HyperFrames a échoué : " + " | ".join(errors[-5:])[-800:])153 raise RuntimeError("le rendu HyperFrames a échoué : " + " | ".join(errors[-5:])[-800:])
154+ if unregistered:
155+ output.unlink(missing_ok=True)
156+ why = " | ".join(dict.fromkeys(page_errors))[-800:] or "aucune erreur de script remontée"
157+ raise RuntimeError(f"une composition n'a pas enregistré sa timeline (son script a planté ?), "
158+ f"le rendu serait figé : {why}")
142 finally:159 finally:
143 if proc.poll() is None:160 if proc.poll() is None:
144 _kill_tree(proc)161 _kill_tree(proc)