diff --git a/pixelle_video/services/frame_html.py b/pixelle_video/services/frame_html.py index b480c92..2eed3b5 100644 --- a/pixelle_video/services/frame_html.py +++ b/pixelle_video/services/frame_html.py @@ -25,6 +25,7 @@ Linux Environment Requirements: Playwright browser install: playwright install --with-deps chromium """ +import asyncio import os import re import tempfile @@ -55,6 +56,7 @@ class HTMLFrameGenerator: _browser = None _playwright = None + _browser_loop = None def __init__(self, template_path: str): """ @@ -307,7 +309,22 @@ class HTMLFrameGenerator: @classmethod async def _ensure_browser(cls): """Lazily initialize a shared Playwright browser instance""" - if cls._browser is None or not cls._browser.is_connected(): + current_loop = asyncio.get_running_loop() + browser_usable = ( + cls._browser is not None + and cls._browser_loop is current_loop + and cls._browser.is_connected() + ) + + if not browser_usable: + if cls._browser is not None and cls._browser_loop is not current_loop: + logger.warning( + "Detected cross-loop Playwright browser reuse attempt; " + "recreating browser for current event loop" + ) + + cls._browser = None + cls._playwright = None from playwright.async_api import async_playwright cls._playwright = await async_playwright().start() cls._browser = await cls._playwright.chromium.launch( @@ -318,6 +335,7 @@ class HTMLFrameGenerator: '--disable-extensions', ] ) + cls._browser_loop = current_loop logger.debug("Initialized Playwright Chromium browser") return cls._browser @@ -327,6 +345,7 @@ class HTMLFrameGenerator: if cls._browser: await cls._browser.close() cls._browser = None + cls._browser_loop = None if cls._playwright: await cls._playwright.stop() cls._playwright = None @@ -410,5 +429,7 @@ class HTMLFrameGenerator: return output_path except Exception as e: - logger.error(f"Failed to render HTML template: {e}") - raise RuntimeError(f"HTML rendering failed: {e}") + logger.exception("Failed to render HTML template") + raise RuntimeError( + f"HTML rendering failed: {type(e).__name__}: {e}" + ) from e diff --git a/web/utils/async_helpers.py b/web/utils/async_helpers.py index b755c99..f1b2d8b 100644 --- a/web/utils/async_helpers.py +++ b/web/utils/async_helpers.py @@ -15,6 +15,7 @@ Async helper functions for web UI """ import asyncio +import sys import tomllib from pathlib import Path @@ -23,6 +24,23 @@ from loguru import logger def run_async(coro): """Run async coroutine in sync context""" + if sys.platform == "win32": + # Streamlit/Tornado may switch the global asyncio policy to + # WindowsSelectorEventLoopPolicy, which breaks subprocess-based + # libraries such as Playwright on Windows. Use an explicit + # Proactor loop here so this sync bridge does not depend on the + # ambient global policy. + loop = asyncio.ProactorEventLoop() + try: + return loop.run_until_complete(coro) + finally: + try: + from pixelle_video.services.frame_html import HTMLFrameGenerator + + loop.run_until_complete(HTMLFrameGenerator.close_browser()) + except Exception as e: + logger.debug(f"Failed to cleanup HTML frame browser before loop close: {e}") + loop.close() return asyncio.run(coro)