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)