Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@ jobs:
run: |
python -m pip install --disable-pip-version-check \
pytest==8.3.5 \
openai \
"gradio==4.44.1" \
"huggingface_hub<1.0" \
"starlette<1.0"
python -m pip check

- name: Test runtime, installation, and CLI help
run: python -m pytest -q tests/test_gradio_runtime_compat.py tests/test_installation_docs.py tests/test_cli_stage_help.py

- name: Test MiniMax presets and offline SDK requests
env:
GRADIO_ANALYTICS_ENABLED: "False"
run: python -m pytest -q tests/test_minimax_api.py tests/test_minimax_launch_integration.py tests/test_minimax_http.py
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ Follow the guide below to explore LLM based clipping:

<img src="docs/images/LLM_guide.png" width=360/>

#### Using MiniMax for transcript-based clipping (optional)

The **LLM Model Name** dropdown lists `minimax/MiniMax-M3.1-Flash` first among the MiniMax presets, followed by `minimax/MiniMax-M3`. Select a model and enter your API key in **APIKEY**, or set `MINIMAX_API_KEY`. Requests use `https://api.minimax.io/v1` by default; set `MINIMAX_API_BASE=https://api.minimaxi.com/v1` for the mainland China endpoint. You can still enter an explicit legacy or custom `minimax/` model ID in the dropdown; FunClip passes it through without replacing it. See the [MiniMax model guide](https://platform.minimax.io/docs/guides/models-intro.md) for availability.

#### Using OrcaRouter as your LLM gateway (optional)

Besides the transcript-based LLMs above, FunClip can route LLM-assisted clipping through [OrcaRouter](https://www.orcarouter.ai), an OpenAI-compatible smart-routing gateway. Select any `orcarouter/` model in the **LLM Model Name** dropdown (`orcarouter/auto` routes each request to the best model for the task), paste an OrcaRouter API key in the **APIKEY** box, and click 'LLM Inference' — FunClip sends the transcript and prompts to `https://api.orcarouter.ai/v1/chat/completions`, and the returned segments work with the existing 'AI Clip' button unchanged.
Expand Down
3 changes: 1 addition & 2 deletions funclip/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,8 @@ def AI_clip_subti(LLM_res, dest_text, video_spk_input, start_ost, end_ost, video
"litellm/anthropic/claude-sonnet-4-6",
"atlascloud/qwen/qwen3.5-flash",
"atlascloud/deepseek-ai/deepseek-v4-pro",
"minimax/MiniMax-M3.1-Flash",
"minimax/MiniMax-M3",
"minimax/MiniMax-M2.7",
"minimax/MiniMax-M2.7-highspeed",
"orcarouter/auto",
"orcarouter/fusion",
"orcarouter/fusion-flash",
Expand Down
128 changes: 128 additions & 0 deletions tests/test_minimax_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""Exercise the launcher dispatcher through the real SDK's HTTP boundary."""

import ast
import json
from pathlib import Path

import httpx
import pytest
from openai import OpenAI

from funclip.llm import openai_api


LAUNCH_PATH = Path(__file__).resolve().parents[1] / "funclip" / "launch.py"


@pytest.fixture
def launcher():
tree = ast.parse(LAUNCH_PATH.read_text(encoding="utf-8"))
dispatcher = next(node for node in ast.walk(tree)
if isinstance(node, ast.FunctionDef)
and node.name == "llm_inference")
dropdown = next(node.value for node in ast.walk(tree)
if isinstance(node, ast.Assign)
and any(isinstance(target, ast.Name) and target.id == "llm_model"
for target in node.targets))
options = {keyword.arg: ast.literal_eval(keyword.value)
for keyword in dropdown.keywords}
# As in the CLI parser tests, execute the actual callback without loading ASR.
namespace = {"openai_call": openai_api.openai_call}
exec(compile(ast.Module(body=[dispatcher], type_ignores=[]),
str(LAUNCH_PATH), "exec"), namespace)
return namespace["llm_inference"], options


@pytest.fixture
def requests(monkeypatch):
captured = []
clients = []

def respond(request):
captured.append(request)
return httpx.Response(200, json={
"id": "offline-completion", "object": "chat.completion", "created": 0,
"model": json.loads(request.content)["model"],
"choices": [{"index": 0, "finish_reason": "stop",
"message": {"role": "assistant", "content": "clip plan"}}],
})

def client_factory(**kwargs):
client = OpenAI(**kwargs, max_retries=0,
http_client=httpx.Client(transport=httpx.MockTransport(respond)))
clients.append(client)
return client

monkeypatch.setattr(openai_api, "OpenAI", client_factory)
monkeypatch.delenv("MINIMAX_API_BASE", raising=False)
monkeypatch.setenv("MINIMAX_API_KEY", "offline-env-key")
yield captured
for client in clients:
client.close()


@pytest.mark.parametrize("base", [None, " ", "https://api.minimaxi.com/v1",
"https://custom.example/v1"])
@pytest.mark.parametrize("model", [None, "MiniMax-M3", "MiniMax-M2.7",
"MiniMax-M2.7-highspeed", "custom/saved-model"])
@pytest.mark.parametrize("apikey", ["", "offline-explicit-key"])
def test_launcher_serializes_selected_and_custom_models(launcher, requests, monkeypatch,
base, model, apikey):
dispatch, options = launcher
if base is not None:
monkeypatch.setenv("MINIMAX_API_BASE", base)
if model is None:
selected = next(choice for choice in options["choices"]
if choice.startswith("minimax/"))
assert selected == "minimax/MiniMax-M3.1-Flash"
model = "MiniMax-M3.1-Flash"
else:
selected = "minimax/" + model
assert dispatch("find highlights", "analyze subtitles", "00:00 sample",
selected, apikey) == "clip plan"
assert len(requests) == 1
request = requests[0]
expected_base = (base or "").strip() or "https://api.minimax.io/v1"
assert str(request.url) == expected_base + "/chat/completions"
assert request.headers["authorization"] == "Bearer " + (apikey or "offline-env-key")
# Exact payload also guards against unsupported thinking/effort/tier fields.
assert json.loads(request.content) == {
"model": model,
"messages": [{"role": "system", "content": "find highlights"},
{"role": "user", "content": "analyze subtitles\n00:00 sample"}],
}


def test_dropdown_accepts_current_and_explicit_legacy_values(launcher):
import gradio
from starlette.testclient import TestClient

_, options = launcher
with gradio.Blocks() as demo:
dropdown = gradio.Dropdown(**options)
assert dropdown.value == options["value"]
for model in ("MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7",
"MiniMax-M2.7-highspeed", "custom/saved-model"):
selected = "minimax/" + model
assert dropdown.preprocess(selected) == selected
response = TestClient(gradio.routes.App.create_app(demo)).get("/")
assert response.status_code == 200
assert "minimax/MiniMax-M3.1-Flash" in response.text


def test_existing_default_route_is_preserved(launcher, requests):
dispatch, options = launcher
assert dispatch("system", "user", "subtitle", options["value"],
"offline-explicit-key") == "clip plan"
assert str(requests[0].url) == "https://api.deepseek.com/chat/completions"
assert json.loads(requests[0].content)["model"] == options["value"]


def test_explicit_content_parts_are_preserved(requests):
content = [{"type": "text", "text": "describe the frame"},
{"type": "image_url", "image_url": {"url": "https://example.com/frame.png"}}]
assert openai_api.openai_call("offline-explicit-key", "minimax/MiniMax-M3",
content) == "clip plan"
assert json.loads(requests[0].content) == {
"model": "MiniMax-M3", "messages": [{"role": "user", "content": content}],
}
20 changes: 13 additions & 7 deletions tests/test_minimax_launch_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,21 @@ def test_openai_compatible_route_preserves_prompt_roles(self):
self.assertEqual(ast.unparse(g4f_call.args[2]), "system_content")

def test_dropdown_only_lists_documented_minimax_models(self):
string_literals = {
dropdown = next(
node.value
for node in ast.walk(self.tree)
if isinstance(node, ast.Constant) and isinstance(node.value, str)
}

self.assertIn("minimax/MiniMax-M3", string_literals)
self.assertIn("minimax/MiniMax-M2.7", string_literals)
self.assertIn("minimax/MiniMax-M2.7-highspeed", string_literals)
if isinstance(node, ast.Assign)
and any(isinstance(target, ast.Name) and target.id == "llm_model"
for target in node.targets)
)
options = {keyword.arg: ast.literal_eval(keyword.value)
for keyword in dropdown.keywords}
self.assertEqual(
[model for model in options["choices"] if model.startswith("minimax/")],
["minimax/MiniMax-M3.1-Flash", "minimax/MiniMax-M3"],
)
self.assertTrue(options["allow_custom_value"])
self.assertEqual(options["value"], "deepseek-chat")


if __name__ == "__main__":
Expand Down
Loading