fix(montage): render each motion from its own page, with the stage text color
HyperFrames drops the id=stage of an embedded composition, so style.css lost its text color and font: the render page now sets them. A shared index.html once gave a motion another one's render: each motion gets its own page. The page version is part of the motion signature, so existing renders are redone.
750e515 parent: 079a7ba modified
backend/studio/modules/projects/montage.py +4 -1 | @@ -64,6 +64,9 @@ TIMELINE_FILE = "timeline.json" | ||
| 64 | 64 | INSERTIONS_FILE = "insertions.json" |
| 65 | 65 | MOTION_DIR = "motion" |
| 66 | 66 | RENDERS_DIR = "renders" |
| 67 | +# la page qui rend un motion (montage_tasks._index) entre dans sa signature : à monter quand elle change, | |
| 68 | +# les motions déjà rendus se refont | |
| 69 | +MOTION_PAGE_VERSION = 2 | |
| 67 | 70 | # les médias posés dans les compositions, copiés à côté d'elles (le rendu ne lit que montage/motion/) |
| 68 | 71 | MEDIA_DIR = "media" |
| 69 | 72 | HISTORY_DIR = ".history" |
| @@ -831,7 +834,7 @@ def motion_signature(pid: str, item: dict) -> str | None: | ||
| 831 | 834 | comp = motion_dir(pid) / f"{item['id']}.html" |
| 832 | 835 | if not comp.is_file(): |
| 833 | 836 | return None |
| 834 | - body = comp.read_bytes() + json.dumps([item["dur"]]).encode() | |
| 837 | + body = comp.read_bytes() + json.dumps([item["dur"], MOTION_PAGE_VERSION]).encode() | |
| 835 | 838 | style = motion_dir(pid) / "style.css" |
| 836 | 839 | if style.is_file(): |
| 837 | 840 | body += style.read_bytes() |
| @@ -64,6 +64,9 @@ TIMELINE_FILE = "timeline.json" | |||
| 64 | INSERTIONS_FILE = "insertions.json" | 64 | INSERTIONS_FILE = "insertions.json" |
| 65 | MOTION_DIR = "motion" | 65 | MOTION_DIR = "motion" |
| 66 | RENDERS_DIR = "renders" | 66 | RENDERS_DIR = "renders" |
| 67 | +# la page qui rend un motion (montage_tasks._index) entre dans sa signature : à monter quand elle change, | ||
| 68 | +# les motions déjà rendus se refont | ||
| 69 | +MOTION_PAGE_VERSION = 2 | ||
| 67 | # les médias posés dans les compositions, copiés à côté d'elles (le rendu ne lit que montage/motion/) | 70 | # les médias posés dans les compositions, copiés à côté d'elles (le rendu ne lit que montage/motion/) |
| 68 | MEDIA_DIR = "media" | 71 | MEDIA_DIR = "media" |
| 69 | HISTORY_DIR = ".history" | 72 | HISTORY_DIR = ".history" |
| @@ -831,7 +834,7 @@ def motion_signature(pid: str, item: dict) -> str | None: | |||
| 831 | comp = motion_dir(pid) / f"{item['id']}.html" | 834 | comp = motion_dir(pid) / f"{item['id']}.html" |
| 832 | if not comp.is_file(): | 835 | if not comp.is_file(): |
| 833 | return None | 836 | return None |
| 834 | - body = comp.read_bytes() + json.dumps([item["dur"]]).encode() | 837 | + body = comp.read_bytes() + json.dumps([item["dur"], MOTION_PAGE_VERSION]).encode() |
| 835 | style = motion_dir(pid) / "style.css" | 838 | style = motion_dir(pid) / "style.css" |
| 836 | if style.is_file(): | 839 | if style.is_file(): |
| 837 | body += style.read_bytes() | 840 | body += style.read_bytes() |
modified
backend/studio/modules/projects/montage_tasks.py +18 -6 | @@ -441,8 +441,13 @@ def _apply(pid: str, task: str) -> None: | ||
| 441 | 441 | |
| 442 | 442 | # --- Rendu d'un motion design -------------------------------------------------------------- |
| 443 | 443 | |
| 444 | -def _index(directory: Path, comp: str, duration: float) -> None: | |
| 445 | - (directory / "index.html").write_text(f"""<!doctype html> | |
| 444 | +def _index(directory: Path, comp: str, duration: float) -> Path: | |
| 445 | + """ | |
| 446 | + La page qui rend un motion, propre à lui : un index.html commun à tous les motions a déjà donné à un motion | |
| 447 | + le rendu d'un autre. Une modification ici : monter montage.MOTION_PAGE_VERSION. | |
| 448 | + """ | |
| 449 | + page = directory / f"render-{comp}.html" | |
| 450 | + page.write_text(f"""<!doctype html> | |
| 446 | 451 | <html lang="{language()}"> |
| 447 | 452 | <head> |
| 448 | 453 | <meta charset="utf-8"> |
| @@ -450,7 +455,10 @@ def _index(directory: Path, comp: str, duration: float) -> None: | ||
| 450 | 455 | <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script> |
| 451 | 456 | <style> |
| 452 | 457 | html, body {{ margin: 0; padding: 0; background: transparent; overflow: hidden; }} |
| 453 | -.layer {{ position: absolute; inset: 0; font-family: "Space Grotesk", "Inter", system-ui, sans-serif; }} | |
| 458 | +/* HyperFrames retire l'id="stage" du motion en l'intégrant : la règle #stage de style.css (police, couleur | |
| 459 | + du texte) ne s'applique plus. Le calque la remplace, avec les variables de la charte. */ | |
| 460 | +.layer {{ position: absolute; inset: 0; font-family: var(--font, "Space Grotesk", "Inter", system-ui, sans-serif); | |
| 461 | + color: var(--text, #f4f1ea); -webkit-font-smoothing: antialiased; }} | |
| 454 | 462 | .fonts-charte-mono {{ font-family: "JetBrains Mono"; }} |
| 455 | 463 | </style> |
| 456 | 464 | </head> |
| @@ -467,6 +475,7 @@ html, body {{ margin: 0; padding: 0; background: transparent; overflow: hidden; | ||
| 467 | 475 | </body> |
| 468 | 476 | </html> |
| 469 | 477 | """, encoding="utf-8") |
| 478 | + return page | |
| 470 | 479 | |
| 471 | 480 | |
| 472 | 481 | def render_motion(ctx: TaskContext) -> None: |
| @@ -477,7 +486,7 @@ def render_motion(ctx: TaskContext) -> None: | ||
| 477 | 486 | missing = montage.bring_media(pid, f"{item_id}.html") |
| 478 | 487 | if missing: |
| 479 | 488 | raise RuntimeError("; ".join(missing) + " : indique son src dans data-from") |
| 480 | - _index(directory, item_id, p["duration"]) | |
| 489 | + page = _index(directory, item_id, p["duration"]) | |
| 481 | 490 | ctx.set_status("rendering") |
| 482 | 491 | |
| 483 | 492 | def on_progress(value: float, stage: str) -> None: |
| @@ -486,10 +495,13 @@ def render_motion(ctx: TaskContext) -> None: | ||
| 486 | 495 | ctx.save() |
| 487 | 496 | |
| 488 | 497 | output.parent.mkdir(parents=True, exist_ok=True) |
| 489 | - extra = ["--format", "mov"] | |
| 498 | + extra = ["--format", "mov", "--composition", page.name] | |
| 490 | 499 | if p.get("width", 1920) >= 3840: |
| 491 | 500 | extra += ["--resolution", "landscape-4k"] |
| 492 | - hyperframes.render(directory, output, on_progress, ctx.check_cancel, extra) | |
| 501 | + try: | |
| 502 | + hyperframes.render(directory, output, on_progress, ctx.check_cancel, extra) | |
| 503 | + finally: | |
| 504 | + page.unlink(missing_ok=True) | |
| 493 | 505 | # les rendus précédents de ce motion ne servent plus |
| 494 | 506 | for old in output.parent.glob(f"{item_id}-*.mov"): |
| 495 | 507 | if old != output: |
| @@ -441,8 +441,13 @@ def _apply(pid: str, task: str) -> None: | |||
| 441 | 441 | ||
| 442 | # --- Rendu d'un motion design -------------------------------------------------------------- | 442 | # --- Rendu d'un motion design -------------------------------------------------------------- |
| 443 | 443 | ||
| 444 | -def _index(directory: Path, comp: str, duration: float) -> None: | 444 | +def _index(directory: Path, comp: str, duration: float) -> Path: |
| 445 | - (directory / "index.html").write_text(f"""<!doctype html> | 445 | + """ |
| 446 | + La page qui rend un motion, propre à lui : un index.html commun à tous les motions a déjà donné à un motion | ||
| 447 | + le rendu d'un autre. Une modification ici : monter montage.MOTION_PAGE_VERSION. | ||
| 448 | + """ | ||
| 449 | + page = directory / f"render-{comp}.html" | ||
| 450 | + page.write_text(f"""<!doctype html> | ||
| 446 | <html lang="{language()}"> | 451 | <html lang="{language()}"> |
| 447 | <head> | 452 | <head> |
| 448 | <meta charset="utf-8"> | 453 | <meta charset="utf-8"> |
| @@ -450,7 +455,10 @@ def _index(directory: Path, comp: str, duration: float) -> None: | |||
| 450 | <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script> | 455 | <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script> |
| 451 | <style> | 456 | <style> |
| 452 | html, body {{ margin: 0; padding: 0; background: transparent; overflow: hidden; }} | 457 | html, body {{ margin: 0; padding: 0; background: transparent; overflow: hidden; }} |
| 453 | -.layer {{ position: absolute; inset: 0; font-family: "Space Grotesk", "Inter", system-ui, sans-serif; }} | 458 | +/* HyperFrames retire l'id="stage" du motion en l'intégrant : la règle #stage de style.css (police, couleur |
| 459 | + du texte) ne s'applique plus. Le calque la remplace, avec les variables de la charte. */ | ||
| 460 | +.layer {{ position: absolute; inset: 0; font-family: var(--font, "Space Grotesk", "Inter", system-ui, sans-serif); | ||
| 461 | + color: var(--text, #f4f1ea); -webkit-font-smoothing: antialiased; }} | ||
| 454 | .fonts-charte-mono {{ font-family: "JetBrains Mono"; }} | 462 | .fonts-charte-mono {{ font-family: "JetBrains Mono"; }} |
| 455 | </style> | 463 | </style> |
| 456 | </head> | 464 | </head> |
| @@ -467,6 +475,7 @@ html, body {{ margin: 0; padding: 0; background: transparent; overflow: hidden; | |||
| 467 | </body> | 475 | </body> |
| 468 | </html> | 476 | </html> |
| 469 | """, encoding="utf-8") | 477 | """, encoding="utf-8") |
| 478 | + return page | ||
| 470 | 479 | ||
| 471 | 480 | ||
| 472 | def render_motion(ctx: TaskContext) -> None: | 481 | def render_motion(ctx: TaskContext) -> None: |
| @@ -477,7 +486,7 @@ def render_motion(ctx: TaskContext) -> None: | |||
| 477 | missing = montage.bring_media(pid, f"{item_id}.html") | 486 | missing = montage.bring_media(pid, f"{item_id}.html") |
| 478 | if missing: | 487 | if missing: |
| 479 | raise RuntimeError("; ".join(missing) + " : indique son src dans data-from") | 488 | raise RuntimeError("; ".join(missing) + " : indique son src dans data-from") |
| 480 | - _index(directory, item_id, p["duration"]) | 489 | + page = _index(directory, item_id, p["duration"]) |
| 481 | ctx.set_status("rendering") | 490 | ctx.set_status("rendering") |
| 482 | 491 | ||
| 483 | def on_progress(value: float, stage: str) -> None: | 492 | def on_progress(value: float, stage: str) -> None: |
| @@ -486,10 +495,13 @@ def render_motion(ctx: TaskContext) -> None: | |||
| 486 | ctx.save() | 495 | ctx.save() |
| 487 | 496 | ||
| 488 | output.parent.mkdir(parents=True, exist_ok=True) | 497 | output.parent.mkdir(parents=True, exist_ok=True) |
| 489 | - extra = ["--format", "mov"] | 498 | + extra = ["--format", "mov", "--composition", page.name] |
| 490 | if p.get("width", 1920) >= 3840: | 499 | if p.get("width", 1920) >= 3840: |
| 491 | extra += ["--resolution", "landscape-4k"] | 500 | extra += ["--resolution", "landscape-4k"] |
| 492 | - hyperframes.render(directory, output, on_progress, ctx.check_cancel, extra) | 501 | + try: |
| 502 | + hyperframes.render(directory, output, on_progress, ctx.check_cancel, extra) | ||
| 503 | + finally: | ||
| 504 | + page.unlink(missing_ok=True) | ||
| 493 | # les rendus précédents de ce motion ne servent plus | 505 | # les rendus précédents de ce motion ne servent plus |
| 494 | for old in output.parent.glob(f"{item_id}-*.mov"): | 506 | for old in output.parent.glob(f"{item_id}-*.mov"): |
| 495 | if old != output: | 507 | if old != output: |