face-off-agent / app.py
speedn's picture
Update wrapper: always reextract zip to use latest project
e19ce8e verified
Raw
History Blame Contribute Delete
5.07 kB
"""
A robust wrapper for the Face/Off application that extracts a bundled zip file and
locates the real `app.py` file within it. This wrapper avoids executing the
embedded app's `if __name__ == '__main__'` block and supports multiple archive
layouts (with or without a top-level directory).
When deployed on Hugging Face Spaces, the repository root is `/home/user/app`.
This script will extract `faceoff-space.zip` into a dedicated directory and
search for `app.py` within the extracted contents. The first match is loaded
with `runpy.run_path` using a custom module name to prevent side effects.
"""
import os
import zipfile
import sys
import runpy
# Name of the bundled zip containing the full Face/Off project
ZIP_NAME = "faceoff-space.zip"
# Determine absolute paths based on current file location
base_dir = os.path.dirname(__file__)
zip_path = os.path.join(base_dir, ZIP_NAME)
# Directory where the archive will be unpacked. Using a distinct name ensures we
# don't pollute the repo root and can detect if we've already extracted it.
extract_target = os.path.join(base_dir, "faceoff-space-extracted")
# Always (re)unpack the zip on startup. If the extraction directory exists from
# a previous run, remove it first to ensure that stale files are not served.
if os.path.isfile(zip_path):
# Clean out any old extracted contents
if os.path.isdir(extract_target):
import shutil
shutil.rmtree(extract_target, ignore_errors=True)
os.makedirs(extract_target, exist_ok=True)
with zipfile.ZipFile(zip_path, "r") as archive:
archive.extractall(extract_target)
# Ensure extracted directories are in the module search path. We insert both
# the extraction root and any immediate subdirectories to ease module imports.
if os.path.isdir(extract_target) and extract_target not in sys.path:
sys.path.insert(0, extract_target)
# Also add one level deeper (e.g., faceoff-space) to sys.path if present
for entry in os.listdir(extract_target):
candidate = os.path.join(extract_target, entry)
if os.path.isdir(candidate) and candidate not in sys.path:
sys.path.insert(0, candidate)
def find_app_py(root: str) -> str:
"""Recursively search for an `app.py` file starting at `root` and return
its path. Raises FileNotFoundError if none is found.
"""
for current_dir, subdirs, files in os.walk(root):
if "app.py" in files:
return os.path.join(current_dir, "app.py")
raise FileNotFoundError(f"`app.py` not found under {root}")
# Locate the actual application file within the extracted archive
try:
app_path = find_app_py(extract_target)
except FileNotFoundError:
# Fall back to the repository root in case extraction failed but files exist directly
try:
app_path = find_app_py(base_dir)
except FileNotFoundError as exc:
raise RuntimeError(
"Unable to locate the Face/Off application (`app.py`). Ensure that "
f"{ZIP_NAME} contains the app files."
) from exc
# Dynamically load the application while preventing execution of its main block.
app_globals = runpy.run_path(app_path, run_name="faceoff_app")
# Gradio Spaces expect either a `demo` object (Blocks/Interface) or, for FastAPI
# Spaces, an `app` object. The Face/Off project defines both. Surface both
# variables from the loaded module if they exist so that the runtime can
# initialize properly regardless of the selected SDK.
demo = app_globals.get("demo")
app = app_globals.get("app")
# If neither object was found, raise an informative error. This makes it clear
# that the wrapped project did not expose the expected variables.
if demo is None and app is None:
raise RuntimeError(
"Neither `demo` nor `app` was found in the extracted Face/Off project. "
"Ensure that `demo` (Gradio Blocks) or `app` (FastAPI) is defined."
)
# If running in a Hugging Face Space, automatically launch the Gradio demo. Many
# Gradio Spaces rely on an implicit call to `demo.launch()` provided by the
# platform. However, because we dynamically import the application, the
# automatic initialization does not occur. To ensure the server starts,
# explicitly launch the demo when this file is executed as the main module. We
# use `queue()` to enable concurrency and disable analytics endpoints with
# `show_api=False`. When running locally, you can still execute this file
# directly (e.g. `python app.py`) and the server will start.
if __name__ == "__main__":
import os
if demo is not None:
# Determine the port Hugging Face spaces use (defaults to 7860 locally).
port = int(os.getenv("PORT", "7860"))
# Launch the Gradio app with queuing and without external share links.
demo.queue().launch(server_name="0.0.0.0", server_port=port, show_api=False, share=False)
elif app is not None:
# Fallback: run FastAPI using uvicorn if no Gradio demo is available.
import uvicorn
port = int(os.getenv("PORT", "7860"))
uvicorn.run(app, host="0.0.0.0", port=port)