diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..480e545 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,37 @@ +name: Tests + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: tests-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + gradio-runtime: + runs-on: ubuntu-latest + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + + - name: Install Gradio runtime test dependencies + run: | + python -m pip install --disable-pip-version-check \ + pytest==8.3.5 \ + "gradio==4.44.1" \ + "huggingface_hub<1.0" \ + "starlette<1.0" + python -m pip check + + - name: Test Gradio runtime compatibility + run: python -m pytest -q tests/test_gradio_runtime_compat.py diff --git a/funclip/launch.py b/funclip/launch.py index 36ce753..4ffdf00 100644 --- a/funclip/launch.py +++ b/funclip/launch.py @@ -19,6 +19,7 @@ from llm.twelvelabs_api import call_twelvelabs_pegasus from utils.trans_utils import extract_timestamps from introduction import top_md_1, top_md_3, top_md_4 +from launch_config import build_launch_kwargs def create_asr_model(model_name, lang, auto_model_cls=AutoModel): @@ -66,10 +67,6 @@ def create_asr_model(model_name, lang, auto_model_cls=AutoModel): audio_clipper = VideoClipper(funasr_model) audio_clipper.lang = args.lang - server_name='127.0.0.1' - if args.listen: - server_name = '0.0.0.0' - def save_text_to_file(content, extension, output_dir=None): if not content: return None @@ -373,8 +370,6 @@ def AI_clip_subti(LLM_res, dest_text, video_spk_input, start_ost, end_ost, video ], outputs=[video_output, audio_output, clip_message, srt_clipped]) - # start gradio service in local or share - if args.listen: - funclip_service.launch(share=args.share, server_port=args.port, server_name=server_name, inbrowser=False) - else: - funclip_service.launch(share=args.share, server_port=args.port, server_name=server_name) + funclip_service.launch( + **build_launch_kwargs(share=args.share, port=args.port, listen=args.listen) + ) diff --git a/funclip/launch_config.py b/funclip/launch_config.py new file mode 100644 index 0000000..bc2ef4e --- /dev/null +++ b/funclip/launch_config.py @@ -0,0 +1,16 @@ +"""Launch policy for the FunClip Gradio service.""" + + +def build_launch_kwargs(*, share, port, listen): + kwargs = { + "share": share, + "server_port": port, + "server_name": "127.0.0.1", + } + if listen: + kwargs.update( + server_name="0.0.0.0", + inbrowser=False, + _frontend=False, + ) + return kwargs diff --git a/requirements.txt b/requirements.txt index 27b4d7f..d479796 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ huggingface_hub>=0.19.3,<1.0 moviepy==1.0.3 numpy==1.26.4 gradio>=4.31.3,<5.0 +starlette<1.0 modelscope torch>=1.13 torchaudio diff --git a/tests/test_gradio_runtime_compat.py b/tests/test_gradio_runtime_compat.py new file mode 100644 index 0000000..977122f --- /dev/null +++ b/tests/test_gradio_runtime_compat.py @@ -0,0 +1,84 @@ +"""Regression tests for the supported Gradio/Starlette launch contract.""" + +import ast +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +LAUNCH_PATH = ROOT / "funclip" / "launch.py" + + +def test_gradio4_excludes_breaking_starlette_releases(): + requirements = { + line.strip() + for line in (ROOT / "requirements.txt").read_text(encoding="utf-8").splitlines() + if line.strip() and not line.lstrip().startswith("#") + } + + assert "gradio>=4.31.3,<5.0" in requirements + assert "starlette<1.0" in requirements + + +def test_supported_gradio_stack_renders_index(): + import gradio + from starlette.testclient import TestClient + + with gradio.Blocks() as demo: + gradio.Markdown("FunClip runtime smoke test") + + app = gradio.routes.App.create_app(demo) + response = TestClient(app).get("/") + + assert response.status_code == 200 + assert "gradio_config" in response.text + + +def test_local_launch_keeps_gradio_frontend_probe_enabled(): + from funclip.launch_config import build_launch_kwargs + + assert build_launch_kwargs(share=False, port=7860, listen=False) == { + "share": False, + "server_port": 7860, + "server_name": "127.0.0.1", + } + + +def test_explicit_listen_skips_only_the_local_frontend_probe(): + from funclip.launch_config import build_launch_kwargs + + assert build_launch_kwargs(share=False, port=12235, listen=True) == { + "share": False, + "server_port": 12235, + "server_name": "0.0.0.0", + "inbrowser": False, + "_frontend": False, + } + + +def test_explicit_share_choice_is_preserved(): + from funclip.launch_config import build_launch_kwargs + + kwargs = build_launch_kwargs(share=True, port=7860, listen=True) + + assert kwargs["share"] is True + + +def test_launcher_does_not_patch_dependencies_or_retry_with_public_share(): + tree = ast.parse(LAUNCH_PATH.read_text(encoding="utf-8")) + launch_calls = [ + node + for node in ast.walk(tree) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr == "launch" + ] + patched_attributes = [ + target.attr + for node in ast.walk(tree) + if isinstance(node, (ast.Assign, ast.AnnAssign)) + for target in (node.targets if isinstance(node, ast.Assign) else [node.target]) + if isinstance(target, ast.Attribute) + and target.attr in {"TemplateResponse", "cache", "cache_size"} + ] + + assert len(launch_calls) == 1 + assert patched_attributes == []