Skip to content
Draft
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
87 changes: 81 additions & 6 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,11 +322,15 @@ async def websocket_endpoint(websocket: WebSocket, client_id: str = None):
"nano-banana/edit-official-stable",
]
RUNNINGHUB_DEFAULT_VIDEO_MODELS = [
"xai/grok-imagine/image-to-video-channel-low-price-v1.5",
"google/veo3.1-fast/text-to-video-channel-low-price",
"sora-2/text-to-video-official-stable",
"seedance-2.0-global/text-to-video",
"seedance-2.0-global/image-to-video",
]
RUNNINGHUB_PINNED_VIDEO_MODELS = [
"xai/grok-imagine/image-to-video-channel-low-price-v1.5",
]
RUNNINGHUB_DEFAULT_APPS = [
{
"id": "2058517022748798977",
Expand Down Expand Up @@ -805,7 +809,7 @@ def merge_default_api_providers(providers):
current["protocol"] = "runninghub"
current["image_models"] = model_list_from_values(current.get("image_models") or [])
current["chat_models"] = model_list_from_values(current.get("chat_models") or [])
current["video_models"] = model_list_from_values(current.get("video_models") or [])
current["video_models"] = model_list_from_values([*(current.get("video_models") or []), *RUNNINGHUB_PINNED_VIDEO_MODELS])
current["rh_apps"] = merge_runninghub_system_entries(rh_default.get("rh_apps") or [], current.get("rh_apps") or [], "app")
current["rh_workflows"] = merge_runninghub_system_entries(rh_default.get("rh_workflows") or [], current.get("rh_workflows") or [], "workflow")
volc_default = next((d for d in default_api_providers() if d["id"] == "volcengine"), None)
Expand Down Expand Up @@ -863,6 +867,9 @@ def merge_default_api_providers(providers):
*[item for item in (current.get("video_models") or []) if str(item or "").strip() not in JIMENG_LEGACY_VIDEO_MODELS],
*JIMENG_DEFAULT_VIDEO_MODELS,
])
for current in merged:
if str(current.get("protocol") or "").lower() == "runninghub":
current["video_models"] = model_list_from_values([*(current.get("video_models") or []), *RUNNINGHUB_PINNED_VIDEO_MODELS])
return merged

def normalize_model_list(values):
Expand Down Expand Up @@ -7764,6 +7771,54 @@ def runninghub_registry_fallback():
{"name_en": "nano-banana/edit-official-stable", "endpoint": "rhart-image-v1-official/edit", "output_type": "image"},
]
video = [
{
"name_en": "xai/grok-imagine/image-to-video-channel-low-price-v1.5",
"endpoint": "rhart-video-g/image-to-video",
"output_type": "video",
"params": [
{
"type": "STRING",
"fieldKey": "prompt",
"required": True,
"defaultValue": "Fixed scene, cute puppy running and playing with a ball on the grass.",
"multipleInputs": False,
},
{
"type": "LIST",
"fieldKey": "aspectRatio",
"required": True,
"defaultValue": "16:9",
"options": [{"value": value} for value in ("2:3", "3:2", "1:1", "16:9", "9:16")],
"multipleInputs": False,
},
{
"type": "IMAGE",
"fieldKey": "imageUrls",
"required": True,
"defaultValue": "",
"multipleInputs": True,
"maxInpuNum": 7,
},
{
"type": "LIST",
"fieldKey": "resolution",
"required": False,
"defaultValue": "480p",
"options": [{"value": value} for value in ("720p", "480p")],
"multipleInputs": False,
},
{
"type": "INT",
"fieldKey": "duration",
"required": False,
"defaultValue": 6,
"multipleInputs": False,
"min": 6,
"max": 30,
"step": 1,
},
],
},
{"name_en": "google/veo3.1-fast/text-to-video-channel-low-price", "endpoint": "rhart-video-v3.1-fast/text-to-video", "output_type": "video"},
{"name_en": "sora-2/text-to-video-official-stable", "endpoint": "rhart-video-s-official/text-to-video", "output_type": "video"},
{"name_en": "seedance-2.0-global/text-to-video", "endpoint": "bytedance/seedance-2.0-global/text-to-video", "output_type": "video"},
Expand Down Expand Up @@ -8580,7 +8635,15 @@ async def generate_runninghub_video(payload, provider):
body["size"] = runninghub_schema_value(field, runninghub_size_for_aspect(aspect))
if runninghub_schema_field(params, "duration"):
field = runninghub_schema_field(params, "duration")
body["duration"] = runninghub_schema_value(field, str(max(1, min(60, int(payload.duration or 5)))))
try:
duration_value = int(payload.duration or field.get("defaultValue") or 5)
if field.get("min") not in (None, ""):
duration_value = max(duration_value, int(field.get("min")))
if field.get("max") not in (None, ""):
duration_value = min(duration_value, int(field.get("max")))
except Exception:
duration_value = field.get("defaultValue") or 5
body["duration"] = duration_value
if runninghub_schema_field(params, "resolution"):
field = runninghub_schema_field(params, "resolution")
body["resolution"] = runninghub_schema_value(field, str(payload.resolution or "720p").lower())
Expand Down Expand Up @@ -11311,23 +11374,34 @@ async def image_params(provider_id: str = "", model: str = ""):
VIDEO_URL_KEYS = (
"url", "video_url", "videoUrl", "mp4_url", "mp4Url",
"output", "output_url", "outputUrl", "download_url", "downloadUrl",
"file_url", "fileUrl", "result_url", "resultUrl",
"video", "src", "uri", "preview_url", "previewUrl", "path",
"last_frame_url", "lastFrameUrl", "remixed_from_video_id",
"resultShow", "result_show", "result_json", "resultJson",
)

def _collect_video_url(value, urls):
if not value:
return
if isinstance(value, str):
if value.startswith("http://") or value.startswith("https://") or value.startswith("/output/") or value.startswith("/assets/"):
urls.append(value)
text = value.strip()
if not text:
return
if text.startswith("[") or text.startswith("{"):
try:
_collect_video_url(json.loads(text), urls)
return
except Exception:
pass
if text.startswith("http://") or text.startswith("https://") or text.startswith("/output/") or text.startswith("/assets/"):
urls.append(text)
return
if isinstance(value, list):
for item in value:
_collect_video_url(item, urls)
return
if isinstance(value, dict):
for key in ("videos", "outputs", "data", "result", "content"):
for key in ("videos", "outputs", "output", "data", "result", "results", "content", "items", "files"):
if key in value:
_collect_video_url(value.get(key), urls)
for key in VIDEO_URL_KEYS:
Expand All @@ -11337,6 +11411,7 @@ def _collect_video_url(value, urls):
def video_output_urls(raw):
urls = []
if not isinstance(raw, dict):
_collect_video_url(raw, urls)
return urls
candidates = [raw]
data = raw.get("data")
Expand Down Expand Up @@ -11364,7 +11439,7 @@ def video_output_urls(raw):
for node in candidates:
if not isinstance(node, dict):
continue
for key in ("videos", "outputs", "content"):
for key in ("videos", "outputs", "output", "results", "result", "data", "content", "items", "files"):
value = node.get(key)
if value:
_collect_video_url(value, urls)
Expand Down
13 changes: 8 additions & 5 deletions static/online.html
Original file line number Diff line number Diff line change
Expand Up @@ -347,16 +347,19 @@ <h1 class="text-4xl font-extrabold tracking-tighter italic" data-i18n="online.ti
providerSelect.innerHTML = providers.map(p => `<option value="${escapeHtml(p.id)}" ${p.id === provider ? 'selected' : ''}>${escapeHtml(p.name || p.id)}</option>`).join('');
if(isRunningHubProvider(provider)){
const entries = runningHubEntries(provider);
const keys = entries.map(e => `${e.kind}:${e.id}`);
const apiModels = providerModels(provider);
const entryKeys = entries.map(e => `${e.kind}:${e.id}`);
const modelKeys = apiModels.map(m => String(m || '').trim()).filter(Boolean);
const keys = [...entryKeys, ...modelKeys];
if(!keys.includes(selectedModel)) selectedModel = keys[0] || '';
modelSelect.innerHTML = entries.length
? entries.map(e => {
const entryOptions = entries.map(e => {
const key = `${e.kind}:${e.id}`;
const tag = e.kind === 'workflow' ? tr('online.rhWorkflow') : tr('online.rhApp');
const label = e.title || e.id;
return `<option value="${escapeHtml(key)}" ${key === selectedModel ? 'selected' : ''}>${escapeHtml(`[${tag}] ${label}`)}</option>`;
}).join('')
: `<option value="">${escapeHtml(tr('online.rhNoEntries'))}</option>`;
});
const modelOptions = apiModels.map(m => `<option value="${escapeHtml(m)}" ${m === selectedModel ? 'selected' : ''}>${escapeHtml(`[OpenAPI] ${m}`)}</option>`);
modelSelect.innerHTML = [...entryOptions, ...modelOptions].join('') || `<option value="">${escapeHtml(tr('online.rhNoEntries'))}</option>`;
} else {
const modelsForProvider = providerModels(provider);
if(!modelsForProvider.includes(selectedModel)) selectedModel = modelsForProvider[0] || models.gpt;
Expand Down