Files
LocalAI/backend/python/longcat-video/backend.py
LocalAI [bot] b00422e45f feat(backends): add LongCat video and avatar generation (#10792)
* feat(backends): add LongCat video and avatar generation

Assisted-by: Codex:GPT-5 [apply_patch] [exec_command] [web]

* refactor(config): declare model I/O modalities

Make model configs declare input and output modalities so capability discovery no longer branches on backend or checkpoint names. Complete the LongCat gallery and user documentation, make the SDPA patch apply to the pinned upstream revision, and stabilize the Agent Jobs race exposed by the required hook.

Assisted-by: Codex:GPT-5 [web]

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-07-12 23:58:46 +02:00

905 lines
31 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
import argparse
import datetime
import gc
import math
import os
import signal
import subprocess
import sys
import tempfile
import traceback
from concurrent import futures
import grpc
import backend_pb2
import backend_pb2_grpc
from longcat_utils import (
BASE_MODEL_ID,
MODEL_KIND_AVATAR,
MODEL_KIND_BASE,
attention_overrides,
avatar_segments_for_duration,
avatar_segments_for_frames,
classify_model,
normalize_model_source,
normalize_num_frames,
parse_options,
require_bool,
require_float,
require_int,
validate_dimensions,
)
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "common"))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "sources", "LongCat-Video"))
from grpc_auth import get_auth_interceptors
MAX_WORKERS = int(os.environ.get("PYTHON_GRPC_MAX_WORKERS", "1"))
DEFAULT_NEGATIVE_PROMPT = (
"Close-up, bright tones, overexposed, static, blurred details, subtitles, "
"paintings, low quality, JPEG compression residue, ugly, incomplete, extra "
"fingers, poorly drawn hands, poorly drawn faces, deformed, disfigured, "
"misshapen limbs, fused fingers, still picture, messy background, three legs, "
"many people in the background, walking backwards"
)
LOAD_OPTIONS = {
"attention_backend",
"base_model",
"max_segments",
"resolution",
"use_distill",
"use_int8",
}
REQUEST_PARAMS = {
"audio_guidance_scale",
"mask_frame_range",
"num_segments",
"offload_kv_cache",
"ref_img_index",
"resolution",
}
BASE_CHECKPOINT_PATTERNS = [
"config.json",
"model_index.json",
"dit/**",
"lora/cfg_step_lora.safetensors",
"scheduler/**",
"text_encoder/**",
"tokenizer/**",
"vae/**",
]
AVATAR_BASE_PATTERNS = [
"config.json",
"model_index.json",
"text_encoder/**",
"tokenizer/**",
"vae/**",
]
AVATAR_COMMON_PATTERNS = [
"config.json",
"lora/dmd_lora.safetensors",
"model_index.json",
"scheduler/**",
"whisper-large-v3/config.json",
"whisper-large-v3/model.safetensors",
"whisper-large-v3/preprocessor_config.json",
]
class BackendServicer(backend_pb2_grpc.BackendServicer):
def __init__(self):
self.model_kind = None
self.pipeline = None
self.options = {}
self.device_index = 0
self.cp_split_hw = None
self._dist_store_dir = None
def Health(self, request, context):
return backend_pb2.Reply(message=b"OK")
def LoadModel(self, request, context):
model = request.Model
if request.ModelFile and os.path.isdir(request.ModelFile):
model = request.ModelFile
model_kind = classify_model(model)
if model_kind is None:
return self._fail(
context,
grpc.StatusCode.INVALID_ARGUMENT,
"longcat-video only accepts LongCat-Video or LongCat-Video-Avatar-1.5 checkpoints",
)
try:
options = parse_options(request.Options)
unknown = sorted(set(options) - LOAD_OPTIONS)
if unknown:
raise ValueError(f"unknown model option(s): {', '.join(unknown)}")
self._import_torch()
if not self.torch.cuda.is_available():
return self._fail(
context,
grpc.StatusCode.FAILED_PRECONDITION,
"longcat-video requires an NVIDIA CUDA GPU",
)
if request.TensorParallelSize > 1:
return self._fail(
context,
grpc.StatusCode.UNIMPLEMENTED,
"longcat-video currently supports one GPU per backend process",
)
self._import_runtime()
attention_name = str(options.get("attention_backend", "sdpa")).lower()
attention_overrides(attention_name)
resolution = str(options.get("resolution", "480p")).lower()
if resolution not in {"480p", "720p"}:
raise ValueError("resolution must be 480p or 720p")
use_distill_default = model_kind == MODEL_KIND_AVATAR
use_distill = require_bool(
options.get("use_distill", use_distill_default),
"use_distill",
)
use_int8 = require_bool(options.get("use_int8", False), "use_int8")
if model_kind == MODEL_KIND_BASE and use_int8:
raise ValueError(
"use_int8 is supported only by LongCat-Video-Avatar-1.5"
)
self.options = {
**options,
"attention_backend": attention_name,
"resolution": resolution,
"use_distill": use_distill,
"use_int8": use_int8,
"max_segments": require_int(
options.get("max_segments", 8),
"max_segments",
minimum=1,
maximum=64,
),
}
self._release_model()
self._ensure_distributed()
if model_kind == MODEL_KIND_BASE:
self._load_base_model(model)
else:
self._load_avatar_model(model)
self.model_kind = model_kind
print(
f"Loaded {normalize_model_source(model)} as {model_kind} "
f"with attention_backend={attention_name}",
file=sys.stderr,
)
return backend_pb2.Result(message="Model loaded successfully", success=True)
except ValueError as err:
self._release_model()
return self._fail(context, grpc.StatusCode.INVALID_ARGUMENT, str(err))
except Exception as err:
self._release_model()
print(f"Error loading LongCat model: {err}", file=sys.stderr)
traceback.print_exc()
return self._fail(
context,
grpc.StatusCode.INTERNAL,
f"failed to load LongCat model: {err}",
)
def Free(self, request, context):
self._release_model()
return backend_pb2.Result(message="Model released", success=True)
def GenerateVideo(self, request, context):
if self.pipeline is None or self.model_kind is None:
return self._fail(
context,
grpc.StatusCode.FAILED_PRECONDITION,
"model is not loaded",
)
if not request.prompt.strip():
return self._fail(
context,
grpc.StatusCode.INVALID_ARGUMENT,
"prompt is required",
)
if not request.dst:
return self._fail(
context,
grpc.StatusCode.INVALID_ARGUMENT,
"output destination is required",
)
if request.end_image:
return self._fail(
context,
grpc.StatusCode.INVALID_ARGUMENT,
"longcat-video does not support end_image conditioning",
)
request_state = {"finished": False}
def interrupt_if_cancelled():
if not request_state["finished"] and self.pipeline is not None:
self.pipeline._interrupt = True
try:
params = dict(request.params)
unknown = sorted(set(params) - REQUEST_PARAMS)
if unknown:
raise ValueError(f"unknown request param(s): {', '.join(unknown)}")
os.makedirs(os.path.dirname(request.dst) or ".", mode=0o750, exist_ok=True)
if hasattr(context, "add_callback"):
context.add_callback(interrupt_if_cancelled)
if request.start_image and not os.path.isfile(request.start_image):
raise ValueError("start_image is not a readable staged file")
if request.num_frames < 0:
raise ValueError("num_frames must not be negative")
if self.model_kind == MODEL_KIND_BASE:
if request.audio:
raise ValueError(
"audio input requires a LongCat-Video-Avatar-1.5 model"
)
self._generate_base(request, params)
else:
self._generate_avatar(request, params, context)
return backend_pb2.Result(
message="Video generated successfully", success=True
)
except ValueError as err:
return self._fail(context, grpc.StatusCode.INVALID_ARGUMENT, str(err))
except Exception as err:
print(f"Error generating LongCat video: {err}", file=sys.stderr)
traceback.print_exc()
return self._fail(
context,
grpc.StatusCode.INTERNAL,
f"LongCat video generation failed: {err}",
)
finally:
request_state["finished"] = True
if self.pipeline is not None:
self.pipeline._interrupt = False
def _import_torch(self):
if hasattr(self, "torch"):
return
import torch
self.torch = torch
def _import_runtime(self):
if hasattr(self, "LongCatVideoPipeline"):
return
import imageio.v2 as imageio
import imageio_ffmpeg
import librosa
import numpy as np
import torch.distributed as dist
from diffusers.utils import load_image
from huggingface_hub import snapshot_download
from PIL import Image
from transformers import AutoTokenizer, UMT5EncoderModel
from longcat_video.audio_process import (
get_audio_encoder,
get_audio_feature_extractor,
)
from longcat_video.context_parallel import context_parallel_util
from longcat_video.modules.autoencoder_kl_wan import AutoencoderKLWan
from longcat_video.modules.avatar.longcat_video_dit_avatar import (
LongCatVideoAvatarTransformer3DModel,
)
from longcat_video.modules.longcat_video_dit import (
LongCatVideoTransformer3DModel,
)
from longcat_video.modules.quantization import load_quantized_dit
from longcat_video.modules.scheduling_flow_match_euler_discrete import (
FlowMatchEulerDiscreteScheduler,
)
from longcat_video.pipeline_longcat_video import LongCatVideoPipeline
from longcat_video.pipeline_longcat_video_avatar import (
LongCatVideoAvatarPipeline,
)
self.imageio = imageio
self.imageio_ffmpeg = imageio_ffmpeg
self.librosa = librosa
self.np = np
self.dist = dist
self.load_image = load_image
self.snapshot_download = snapshot_download
self.Image = Image
self.AutoTokenizer = AutoTokenizer
self.UMT5EncoderModel = UMT5EncoderModel
self.get_audio_encoder = get_audio_encoder
self.get_audio_feature_extractor = get_audio_feature_extractor
self.context_parallel_util = context_parallel_util
self.AutoencoderKLWan = AutoencoderKLWan
self.LongCatVideoAvatarTransformer3DModel = LongCatVideoAvatarTransformer3DModel
self.LongCatVideoTransformer3DModel = LongCatVideoTransformer3DModel
self.load_quantized_dit = load_quantized_dit
self.FlowMatchEulerDiscreteScheduler = FlowMatchEulerDiscreteScheduler
self.LongCatVideoPipeline = LongCatVideoPipeline
self.LongCatVideoAvatarPipeline = LongCatVideoAvatarPipeline
def _ensure_distributed(self):
self.torch.cuda.set_device(self.device_index)
if not self.dist.is_initialized():
self._dist_store_dir = tempfile.mkdtemp(prefix="localai-longcat-dist-")
init_file = os.path.join(self._dist_store_dir, "store")
self.dist.init_process_group(
backend="nccl",
init_method=f"file://{init_file}",
rank=0,
world_size=1,
timeout=datetime.timedelta(hours=24),
)
self.context_parallel_util.init_context_parallel(
context_parallel_size=1,
global_rank=0,
world_size=1,
)
self.cp_split_hw = self.context_parallel_util.get_optimal_split(1)
def _resolve_checkpoint(self, model, patterns):
source = normalize_model_source(model)
if os.path.isdir(source):
return source
print(f"Downloading required files for {source}", file=sys.stderr)
return self.snapshot_download(repo_id=source, allow_patterns=patterns)
def _load_base_model(self, model):
checkpoint = self._resolve_checkpoint(model, BASE_CHECKPOINT_PATTERNS)
dtype = self.torch.bfloat16
overrides = attention_overrides(self.options["attention_backend"])
tokenizer = self.AutoTokenizer.from_pretrained(
checkpoint,
subfolder="tokenizer",
)
text_encoder = self.UMT5EncoderModel.from_pretrained(
checkpoint,
subfolder="text_encoder",
torch_dtype=dtype,
low_cpu_mem_usage=True,
)
vae = self.AutoencoderKLWan.from_pretrained(
checkpoint,
subfolder="vae",
torch_dtype=dtype,
low_cpu_mem_usage=True,
)
scheduler = self.FlowMatchEulerDiscreteScheduler.from_pretrained(
checkpoint,
subfolder="scheduler",
)
dit = self.LongCatVideoTransformer3DModel.from_pretrained(
checkpoint,
subfolder="dit",
cp_split_hw=self.cp_split_hw,
torch_dtype=dtype,
low_cpu_mem_usage=True,
**overrides,
)
if self.options["use_distill"]:
dit.load_lora(
os.path.join(checkpoint, "lora", "cfg_step_lora.safetensors"),
"cfg_step_lora",
)
dit.enable_loras(["cfg_step_lora"])
self.pipeline = self.LongCatVideoPipeline(
tokenizer=tokenizer,
text_encoder=text_encoder,
vae=vae,
scheduler=scheduler,
dit=dit,
)
self.pipeline.to(self.device_index)
def _load_avatar_model(self, model):
avatar_patterns = list(AVATAR_COMMON_PATTERNS)
model_subfolder = (
"base_model_int8" if self.options["use_int8"] else "base_model"
)
avatar_patterns.append(f"{model_subfolder}/**")
checkpoint = self._resolve_checkpoint(model, avatar_patterns)
base_model = self.options.get("base_model")
if not base_model and os.path.isdir(normalize_model_source(model)):
sibling = os.path.join(
os.path.dirname(normalize_model_source(model)), "LongCat-Video"
)
if os.path.isdir(sibling):
base_model = sibling
base_model = base_model or BASE_MODEL_ID
if classify_model(str(base_model)) != MODEL_KIND_BASE:
raise ValueError("base_model must point to a LongCat-Video checkpoint")
base_checkpoint = self._resolve_checkpoint(base_model, AVATAR_BASE_PATTERNS)
dtype = self.torch.bfloat16
overrides = attention_overrides(self.options["attention_backend"])
tokenizer = self.AutoTokenizer.from_pretrained(
base_checkpoint,
subfolder="tokenizer",
)
text_encoder = self.UMT5EncoderModel.from_pretrained(
base_checkpoint,
subfolder="text_encoder",
torch_dtype=dtype,
low_cpu_mem_usage=True,
)
vae = self.AutoencoderKLWan.from_pretrained(
base_checkpoint,
subfolder="vae",
torch_dtype=dtype,
low_cpu_mem_usage=True,
)
scheduler = self.FlowMatchEulerDiscreteScheduler.from_pretrained(
checkpoint,
subfolder="scheduler",
)
if self.options["use_int8"]:
previous_dtype = self.torch.get_default_dtype()
self.torch.set_default_dtype(dtype)
try:
dit = self.load_quantized_dit(
checkpoint,
subfolder="base_model_int8",
cp_split_hw=self.cp_split_hw,
**overrides,
)
finally:
self.torch.set_default_dtype(previous_dtype)
else:
dit = self.LongCatVideoAvatarTransformer3DModel.from_pretrained(
checkpoint,
subfolder="base_model",
cp_split_hw=self.cp_split_hw,
torch_dtype=dtype,
low_cpu_mem_usage=True,
**overrides,
)
if self.options["use_distill"]:
dit.load_lora(
os.path.join(checkpoint, "lora", "dmd_lora.safetensors"),
"dmd",
multiplier=1.0,
lora_network_dim=128,
lora_network_alpha=64,
)
dit.enable_loras(["dmd"])
audio_checkpoint = os.path.join(checkpoint, "whisper-large-v3")
audio_encoder = self.get_audio_encoder(
audio_checkpoint,
MODEL_KIND_AVATAR + "-v1.5",
).to(self.device_index)
audio_feature_extractor = self.get_audio_feature_extractor(
audio_checkpoint,
MODEL_KIND_AVATAR + "-v1.5",
)
self.pipeline = self.LongCatVideoAvatarPipeline(
tokenizer=tokenizer,
text_encoder=text_encoder,
vae=vae,
scheduler=scheduler,
dit=dit,
audio_encoder=audio_encoder,
audio_feature_extractor=audio_feature_extractor,
model_type="avatar-v1.5",
)
self.pipeline.to(self.device_index)
def _generate_base(self, request, params):
use_distill = self.options["use_distill"]
frames = normalize_num_frames(request.num_frames)
steps = (
16
if use_distill
else require_int(
request.step or 50,
"step",
minimum=1,
maximum=200,
)
)
guidance_scale = (
1.0
if use_distill
else require_float(
request.cfg_scale or 4.0,
"cfg_scale",
minimum=0.0,
maximum=30.0,
)
)
fps = require_int(request.fps or 15, "fps", minimum=1, maximum=60)
seed = request.seed if request.seed > 0 else 42
negative_prompt = request.negative_prompt or DEFAULT_NEGATIVE_PROMPT
generator = self.torch.Generator(device=self.device_index).manual_seed(seed)
if request.start_image:
resolution = self._resolution(params)
image = self.load_image(request.start_image)
output = self.pipeline.generate_i2v(
image=image,
prompt=request.prompt,
negative_prompt=negative_prompt,
resolution=resolution,
num_frames=frames,
num_inference_steps=steps,
use_distill=use_distill,
guidance_scale=guidance_scale,
generator=generator,
)[0]
else:
width, height = validate_dimensions(request.width, request.height)
output = self.pipeline.generate_t2v(
prompt=request.prompt,
negative_prompt=negative_prompt,
height=height,
width=width,
num_frames=frames,
num_inference_steps=steps,
use_distill=use_distill,
guidance_scale=guidance_scale,
generator=generator,
)[0]
self._save_video(output, request.dst, fps)
def _generate_avatar(self, request, params, context):
if not request.audio:
raise ValueError("audio is required for LongCat-Video-Avatar-1.5")
if not os.path.isfile(request.audio):
raise ValueError("audio input is not a readable staged file")
use_distill = self.options["use_distill"]
steps = (
8
if use_distill
else require_int(
request.step or 50,
"step",
minimum=1,
maximum=200,
)
)
text_guidance = (
1.0
if use_distill
else require_float(
request.cfg_scale or 4.0,
"cfg_scale",
minimum=0.0,
maximum=30.0,
)
)
audio_guidance = (
1.0
if use_distill
else require_float(
params.get("audio_guidance_scale", 4.0),
"audio_guidance_scale",
minimum=0.0,
maximum=20.0,
)
)
seed = request.seed if request.seed > 0 else 42
generator = self.torch.Generator(device=self.device_index).manual_seed(seed)
negative_prompt = request.negative_prompt or DEFAULT_NEGATIVE_PROMPT
resolution = self._resolution(params)
speech, sample_rate = self.librosa.load(request.audio, sr=16000, mono=True)
if speech.size == 0:
raise ValueError("audio contains no samples")
audio_duration = len(speech) / sample_rate
segments = self._avatar_segments(request, params, audio_duration)
segment_frames = 93
conditioning_frames = 13
avatar_fps = 25
generated_duration = (
segment_frames + (segments - 1) * (segment_frames - conditioning_frames)
) / avatar_fps
pad_samples = max(
0, math.ceil((generated_duration - audio_duration) * sample_rate)
)
if pad_samples:
speech = self.np.pad(speech, (0, pad_samples))
full_audio_embedding = self.pipeline.get_audio_embedding(
speech,
fps=avatar_fps,
device=self.device_index,
sample_rate=sample_rate,
model_type="avatar-v1.5",
)
if not self.torch.isfinite(full_audio_embedding).all():
raise ValueError("audio encoder returned non-finite values")
indices = self.torch.arange(5) - 2
def audio_window(start_index):
centers = self.torch.arange(
start_index,
start_index + segment_frames,
).unsqueeze(1) + indices.unsqueeze(0)
centers = self.torch.clamp(
centers,
min=0,
max=full_audio_embedding.shape[0] - 1,
)
return full_audio_embedding[centers][None, ...].to(self.device_index)
audio_start = 0
common = {
"prompt": request.prompt,
"negative_prompt": negative_prompt,
"num_frames": segment_frames,
"num_inference_steps": steps,
"text_guidance_scale": text_guidance,
"audio_guidance_scale": audio_guidance,
"output_type": "both",
"generator": generator,
"audio_emb": audio_window(audio_start),
"use_distill": use_distill,
}
if request.start_image:
output, latent = self.pipeline.generate_ai2v(
image=self.load_image(request.start_image),
resolution=resolution,
**common,
)
else:
width, height = validate_dimensions(request.width, request.height)
output, latent = self.pipeline.generate_at2v(
height=height,
width=width,
**common,
)
video = self._frames_to_pil(output[0])
width, height = video[0].size
current_video = video
reference_latent = latent[:, :, :1].clone()
all_frames = list(video)
for segment in range(1, segments):
if hasattr(context, "is_active") and not context.is_active():
raise RuntimeError("request was cancelled")
print(
f"Generating avatar segment {segment + 1}/{segments}", file=sys.stderr
)
audio_start += segment_frames - conditioning_frames
output, latent = self.pipeline.generate_avc(
video=current_video,
video_latent=latent,
prompt=request.prompt,
negative_prompt=negative_prompt,
height=height,
width=width,
num_frames=segment_frames,
num_cond_frames=conditioning_frames,
num_inference_steps=steps,
text_guidance_scale=text_guidance,
audio_guidance_scale=audio_guidance,
generator=generator,
output_type="both",
use_kv_cache=True,
offload_kv_cache=require_bool(
params.get("offload_kv_cache", False),
"offload_kv_cache",
),
enhance_hf=not use_distill,
audio_emb=audio_window(audio_start),
ref_latent=reference_latent,
ref_img_index=require_int(
params.get("ref_img_index", 10),
"ref_img_index",
minimum=-30,
maximum=30,
),
mask_frame_range=require_int(
params.get("mask_frame_range", 3),
"mask_frame_range",
minimum=0,
maximum=32,
),
use_distill=use_distill,
)
current_video = self._frames_to_pil(output[0])
all_frames.extend(current_video[conditioning_frames:])
self._save_avatar_video(all_frames, request.audio, request.dst, avatar_fps)
def _avatar_segments(self, request, params, audio_duration):
if "num_segments" in params:
segments = require_int(
params["num_segments"],
"num_segments",
minimum=1,
)
elif request.num_frames > 0:
segments = avatar_segments_for_frames(request.num_frames)
else:
segments = avatar_segments_for_duration(audio_duration)
max_segments = self.options["max_segments"]
if segments > max_segments:
raise ValueError(
f"request needs {segments} avatar segments, but max_segments is {max_segments}; "
"trim the audio or raise the model's max_segments option"
)
return segments
def _resolution(self, params):
resolution = str(params.get("resolution", self.options["resolution"])).lower()
if resolution not in {"480p", "720p"}:
raise ValueError("resolution must be 480p or 720p")
return resolution
def _frames_to_pil(self, frames):
images = []
for frame in frames:
array = self.np.asarray(frame)
if self.np.issubdtype(array.dtype, self.np.floating):
array = self.np.clip(array, 0.0, 1.0) * 255
images.append(self.Image.fromarray(array.astype(self.np.uint8)))
return images
def _save_video(self, frames, path, fps):
writer = self.imageio.get_writer(
path,
format="FFMPEG",
mode="I",
fps=fps,
codec="libx264",
macro_block_size=1,
ffmpeg_params=[
"-crf",
"18",
"-pix_fmt",
"yuv420p",
"-movflags",
"+faststart",
"-f",
"mp4",
],
)
try:
for frame in frames:
array = self.np.asarray(frame)
if self.np.issubdtype(array.dtype, self.np.floating):
array = self.np.clip(array, 0.0, 1.0) * 255
writer.append_data(array.astype(self.np.uint8))
finally:
writer.close()
def _save_avatar_video(self, frames, audio_path, dst, fps):
output_dir = os.path.dirname(dst) or "."
handle, silent_path = tempfile.mkstemp(
prefix="longcat-silent-",
suffix=".mp4",
dir=output_dir,
)
os.close(handle)
try:
self._save_video(frames, silent_path, fps)
command = [
self.imageio_ffmpeg.get_ffmpeg_exe(),
"-y",
"-i",
silent_path,
"-i",
audio_path,
"-map",
"0:v:0",
"-map",
"1:a:0",
"-c:v",
"copy",
"-c:a",
"aac",
"-b:a",
"192k",
"-shortest",
"-movflags",
"+faststart",
"-f",
"mp4",
dst,
]
subprocess.run(
command,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
)
except subprocess.CalledProcessError as err:
details = (err.stderr or "ffmpeg failed")[-2000:]
raise RuntimeError(f"failed to mux avatar audio: {details}") from err
finally:
try:
os.remove(silent_path)
except FileNotFoundError:
pass
def _release_model(self):
self.pipeline = None
self.model_kind = None
gc.collect()
if hasattr(self, "torch") and self.torch.cuda.is_available():
self.torch.cuda.empty_cache()
self.torch.cuda.ipc_collect()
@staticmethod
def _fail(context, code, message):
if context is not None:
context.set_code(code)
context.set_details(message)
return backend_pb2.Result(message=message, success=False)
def serve(address):
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=MAX_WORKERS),
options=[
("grpc.max_message_length", 64 * 1024 * 1024),
("grpc.max_send_message_length", 64 * 1024 * 1024),
("grpc.max_receive_message_length", 64 * 1024 * 1024),
],
interceptors=get_auth_interceptors(),
)
backend_pb2_grpc.add_BackendServicer_to_server(BackendServicer(), server)
server.add_insecure_port(address)
server.start()
print(f"LongCat Video backend listening on {address}", file=sys.stderr)
def stop_server(signum, frame):
del signum, frame
server.stop(0)
signal.signal(signal.SIGINT, stop_server)
signal.signal(signal.SIGTERM, stop_server)
server.wait_for_termination()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run the LongCat Video gRPC backend")
parser.add_argument(
"--addr",
default="localhost:50051",
help="address on which to serve the backend",
)
arguments = parser.parse_args()
serve(arguments.addr)