| @@ -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 lignes | 133 | # 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 | continue | 138 | 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'echec | 151 | # 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 tail | 152 | 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) |