mirror of
https://github.com/RVC-Project/Retrieval-based-Voice-Conversion-WebUI.git
synced 2026-08-29 10:09:32 +02:00
588 lines
22 KiB
Python
588 lines
22 KiB
Python
import argparse
|
|
import json
|
|
import sys
|
|
import warnings
|
|
|
|
from .ensemble import ENSEMBLE_ALGORITHMS, save_ensemble_audio
|
|
from .logger import get_separation_logger
|
|
from .model_download import download_all, download_model
|
|
from .model_registry import create_separator, list_models, resolve_model
|
|
from .progress import _CliInferenceProgress
|
|
from .workflow import load_workflow_file, run_workflow_file, validate_workflow, write_workflow_template
|
|
|
|
warnings.filterwarnings("ignore", category=UserWarning)
|
|
|
|
|
|
def _parse_key_value(values):
|
|
"""Parse key value.
|
|
|
|
Args:
|
|
values (Any): Values value.
|
|
|
|
Returns:
|
|
Any: Parsed value."""
|
|
result = {}
|
|
for value in values or []:
|
|
if "=" not in value:
|
|
raise argparse.ArgumentTypeError(f"Expected key=value, got {value!r}")
|
|
key, raw = value.split("=", 1)
|
|
lowered = raw.lower()
|
|
if lowered in {"true", "false"}:
|
|
result[key] = lowered == "true"
|
|
else:
|
|
try:
|
|
result[key] = int(raw)
|
|
except ValueError:
|
|
try:
|
|
result[key] = float(raw)
|
|
except ValueError:
|
|
result[key] = raw
|
|
return result
|
|
|
|
|
|
def cmd_list(args):
|
|
"""Implement the cmd list helper.
|
|
|
|
Args:
|
|
args (argparse.Namespace): Parsed command-line arguments.
|
|
|
|
Returns:
|
|
Any: Computed result."""
|
|
rows = list_models(category=args.category, supported=None if args.all else True)
|
|
if args.json:
|
|
print(json.dumps([item.__dict__ for item in rows], ensure_ascii=False, indent=2))
|
|
return 0
|
|
for item in rows:
|
|
status = "ok" if item.supported else item.unsupported_reason
|
|
category = item.category_path or item.primary_category
|
|
print(f"{item.name}\t{item.model_type or item.architecture}\t{category}\t{item.target_stem}\t{status}")
|
|
return 0
|
|
|
|
|
|
def cmd_info(args):
|
|
"""Implement the cmd info helper.
|
|
|
|
Args:
|
|
args (argparse.Namespace): Parsed command-line arguments.
|
|
|
|
Returns:
|
|
Any: Computed result."""
|
|
resolved = resolve_model(args.model, model_dir=args.model_dir, require_supported=False, require_exists=False)
|
|
entry = resolved["entry"]
|
|
data = {
|
|
"name": entry.name,
|
|
"model_type": entry.model_type,
|
|
"architecture": entry.architecture,
|
|
"supported": entry.supported,
|
|
"unsupported_reason": entry.unsupported_reason,
|
|
"category": entry.category_path or entry.primary_category,
|
|
"category_cn": " / ".join(filter(None, [entry.primary_category_cn, entry.secondary_category_cn])),
|
|
"target_stem": entry.target_stem,
|
|
"model_path": resolved["model_path"],
|
|
"config_path": resolved["config_path"],
|
|
"size_bytes": entry.size_bytes,
|
|
}
|
|
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
return 0
|
|
|
|
|
|
def cmd_download(args):
|
|
"""Implement the cmd download helper.
|
|
|
|
Args:
|
|
args (argparse.Namespace): Parsed command-line arguments.
|
|
|
|
Returns:
|
|
Any: Computed result."""
|
|
if args.model == "all":
|
|
results = download_all(
|
|
model_dir=args.model_dir,
|
|
source=args.source,
|
|
endpoint=args.endpoint,
|
|
supported_only=args.supported_only,
|
|
force=args.force,
|
|
)
|
|
failed = [item for item in results if item.get("error")]
|
|
print(f"Downloaded/skipped {len(results) - len(failed)} model(s), failed {len(failed)}.")
|
|
for item in failed:
|
|
print(f"ERROR {item['entry'].name}: {item['error']}", file=sys.stderr)
|
|
return 1 if failed else 0
|
|
|
|
result = download_model(
|
|
args.model,
|
|
model_dir=args.model_dir,
|
|
source=args.source,
|
|
endpoint=args.endpoint,
|
|
force=args.force,
|
|
)
|
|
_print_download_result(result)
|
|
return 0
|
|
|
|
|
|
def _print_download_result(result):
|
|
"""Print download result.
|
|
|
|
Args:
|
|
result (Any): Result value.
|
|
|
|
Returns:
|
|
None: This callable completes for its side effects."""
|
|
for path in result["skipped"]:
|
|
print(f"exists {path}")
|
|
for path in result["downloaded"]:
|
|
print(f"downloaded {path}")
|
|
|
|
|
|
def _ensure_model_files(args):
|
|
"""Ensure model files.
|
|
|
|
Args:
|
|
args (argparse.Namespace): Parsed command-line arguments.
|
|
|
|
Returns:
|
|
None: This callable completes for its side effects."""
|
|
try:
|
|
resolve_model(args.model, model_dir=args.model_dir, require_supported=True, require_exists=True)
|
|
except FileNotFoundError:
|
|
result = download_model(args.model, model_dir=args.model_dir, source=args.source, endpoint=args.endpoint)
|
|
_print_download_result(result)
|
|
else:
|
|
if args.download:
|
|
result = download_model(args.model, model_dir=args.model_dir, source=args.source, endpoint=args.endpoint)
|
|
_print_download_result(result)
|
|
|
|
|
|
def cmd_infer(args):
|
|
"""Implement the cmd infer helper.
|
|
|
|
Args:
|
|
args (argparse.Namespace): Parsed command-line arguments.
|
|
|
|
Returns:
|
|
Any: Computed result."""
|
|
_ensure_model_files(args)
|
|
logger = get_separation_logger()
|
|
inference_progress = _CliInferenceProgress()
|
|
with create_separator(
|
|
args.model,
|
|
model_dir=args.model_dir,
|
|
device=args.device,
|
|
device_ids=args.device_ids or [0],
|
|
output_format=args.output_format,
|
|
audio_params={
|
|
"wav_bit_depth": args.wav_bit_depth,
|
|
"flac_bit_depth": args.flac_bit_depth,
|
|
"mp3_bit_rate": args.mp3_bit_rate,
|
|
"m4a_bit_rate": args.m4a_bit_rate,
|
|
"m4a_aac_at_quality": args.m4a_aac_at_quality,
|
|
},
|
|
use_tta=args.tta,
|
|
store_dirs=args.output,
|
|
save_as_folder=args.save_as_folder,
|
|
logger=logger,
|
|
debug=args.debug,
|
|
progress_callback=inference_progress,
|
|
inference_params=_parse_key_value(args.param),
|
|
) as separator:
|
|
try:
|
|
files = separator.process_folder(args.input)
|
|
finally:
|
|
inference_progress.close()
|
|
logger.info(f"Processed {len(files)} file(s).")
|
|
return 0
|
|
|
|
|
|
def cmd_ensemble(args):
|
|
"""Run the ensemble CLI command.
|
|
|
|
Args:
|
|
args (argparse.Namespace): Parsed command-line arguments.
|
|
|
|
Returns:
|
|
Any: Computed result."""
|
|
logger = get_separation_logger()
|
|
output_path = save_ensemble_audio(
|
|
args.files,
|
|
args.output,
|
|
algorithm=args.algorithm,
|
|
weights=args.weights,
|
|
output_format=args.output_format,
|
|
audio_params={
|
|
"wav_bit_depth": args.wav_bit_depth,
|
|
"flac_bit_depth": args.flac_bit_depth,
|
|
"mp3_bit_rate": args.mp3_bit_rate,
|
|
"m4a_bit_rate": args.m4a_bit_rate,
|
|
"m4a_codec": args.m4a_codec,
|
|
"m4a_aac_at_quality": args.m4a_aac_at_quality,
|
|
},
|
|
logger=logger,
|
|
)
|
|
logger.info(f"Saved ensemble audio to {output_path}")
|
|
return 0
|
|
|
|
|
|
def cmd_workflow_init(args):
|
|
"""Write a starter workflow file."""
|
|
path = write_workflow_template(args.output, overwrite=args.force)
|
|
print(f"Wrote workflow template to {path}")
|
|
return 0
|
|
|
|
|
|
def cmd_workflow_validate(args):
|
|
"""Validate a workflow file without running inference."""
|
|
workflow = load_workflow_file(args.config)
|
|
model_resolver = resolve_model if args.check_models or args.require_files else None
|
|
validate_workflow(
|
|
workflow,
|
|
model_dir=args.model_dir,
|
|
require_model_files=args.require_files,
|
|
model_resolver=model_resolver,
|
|
)
|
|
print(f"Workflow is valid: {len(workflow.steps)} step(s).")
|
|
return 0
|
|
|
|
|
|
def cmd_workflow_run(args):
|
|
"""Run an audio workflow from a YAML/JSON file."""
|
|
logger = get_separation_logger()
|
|
files = run_workflow_file(
|
|
args.config,
|
|
args.input,
|
|
args.output,
|
|
model_dir=args.model_dir,
|
|
device=args.device,
|
|
output_format=args.output_format,
|
|
download=args.download,
|
|
source=args.source,
|
|
endpoint=args.endpoint,
|
|
output_layout=args.output_layout,
|
|
audio_params={
|
|
"wav_bit_depth": args.wav_bit_depth,
|
|
"flac_bit_depth": args.flac_bit_depth,
|
|
"mp3_bit_rate": args.mp3_bit_rate,
|
|
"m4a_bit_rate": args.m4a_bit_rate,
|
|
"m4a_codec": args.m4a_codec,
|
|
"m4a_aac_at_quality": args.m4a_aac_at_quality,
|
|
},
|
|
logger=logger,
|
|
debug=args.debug,
|
|
)
|
|
logger.info(f"Processed {len(files)} file(s).")
|
|
return 0
|
|
|
|
|
|
def cmd_serve(args):
|
|
"""Implement the cmd serve helper.
|
|
|
|
Args:
|
|
args (argparse.Namespace): Parsed command-line arguments.
|
|
|
|
Returns:
|
|
Any: Computed result."""
|
|
from .server import ServerConfig, run_server
|
|
|
|
config = ServerConfig(
|
|
model=args.model,
|
|
model_dir=args.model_dir,
|
|
source=args.source,
|
|
endpoint=args.endpoint,
|
|
device=args.device,
|
|
device_ids=args.device_ids or [0],
|
|
api_key=args.api_key,
|
|
host=args.host,
|
|
port=args.port,
|
|
debug=args.debug,
|
|
inference_params=_parse_key_value(args.param),
|
|
max_audio_seconds=args.max_audio_seconds,
|
|
max_request_bytes=args.max_request_bytes,
|
|
max_queue_size=args.max_queue_size,
|
|
request_timeout_seconds=args.request_timeout_seconds,
|
|
webui=args.webui,
|
|
)
|
|
run_server(config)
|
|
return 0
|
|
|
|
|
|
def build_parser():
|
|
"""Build the pymss command-line parser.
|
|
|
|
Args:
|
|
None: This callable does not accept user-provided arguments.
|
|
|
|
Returns:
|
|
argparse.ArgumentParser: Configured CLI parser."""
|
|
parser = argparse.ArgumentParser(
|
|
prog="pymss",
|
|
description="Command-line interface for the pymss music source separation package.",
|
|
formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=60),
|
|
)
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
|
|
# ==========================
|
|
# List models
|
|
# ==========================
|
|
list_parser = subparsers.add_parser(
|
|
"list",
|
|
help="List known models.",
|
|
formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=60),
|
|
)
|
|
list_parser.add_argument("--category", help="Filter by primary or secondary category.")
|
|
list_parser.add_argument("--all", action="store_true", help="Include models that are not supported for inference yet.")
|
|
list_parser.add_argument("--json", action="store_true")
|
|
list_parser.set_defaults(func=cmd_list)
|
|
|
|
# ==========================
|
|
# Show model info
|
|
# ==========================
|
|
info_parser = subparsers.add_parser(
|
|
"info",
|
|
help="Show model metadata.",
|
|
formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=60),
|
|
)
|
|
info_parser.add_argument("model")
|
|
info_parser.add_argument(
|
|
"--model-dir",
|
|
help="Local model cache directory. Defaults to PYMSS_MODEL_DIR, repository all_models if present, or ~/.cache/pymss/models.",
|
|
)
|
|
info_parser.set_defaults(func=cmd_info)
|
|
|
|
# ==========================
|
|
# Download models
|
|
# ==========================
|
|
download_parser = subparsers.add_parser(
|
|
"download",
|
|
help="Download a model by name, or use 'all'.",
|
|
formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=60),
|
|
)
|
|
download_parser.add_argument("model")
|
|
download_parser.add_argument(
|
|
"--model-dir",
|
|
help="Local model cache directory. Defaults to PYMSS_MODEL_DIR, repository all_models if present, or ~/.cache/pymss/models.",
|
|
)
|
|
download_parser.add_argument("--source", default="modelscope", choices=["modelscope", "huggingface", "hf-mirror"])
|
|
download_parser.add_argument("--endpoint", help="Custom resolve endpoint. It must serve files by relative path.")
|
|
download_parser.add_argument("--force", action="store_true")
|
|
download_parser.add_argument("--supported-only", action="store_true", help="Only used with model='all'.")
|
|
download_parser.set_defaults(func=cmd_download)
|
|
|
|
# ==========================
|
|
# Inference
|
|
# ==========================
|
|
infer_parser = subparsers.add_parser(
|
|
"infer",
|
|
help="Run inference by model name.",
|
|
formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=60),
|
|
)
|
|
infer_parser.add_argument("model")
|
|
infer_parser.add_argument(
|
|
"--model-dir",
|
|
help="Local model cache directory. Defaults to PYMSS_MODEL_DIR, repository all_models if present, or ~/.cache/pymss/models.",
|
|
)
|
|
infer_parser.add_argument(
|
|
"--download",
|
|
action="store_true",
|
|
help="Check/download the model before inference. Missing model files are downloaded automatically.",
|
|
)
|
|
infer_parser.add_argument("--source", default="modelscope", choices=["modelscope", "huggingface", "hf-mirror"])
|
|
infer_parser.add_argument("--endpoint", help="Custom resolve endpoint. It must serve files by relative path.")
|
|
infer_parser.add_argument("-i", "--input", required=True, help="Input audio file or folder.")
|
|
infer_parser.add_argument("-o", "--output", default="results", help="Output folder.")
|
|
infer_parser.add_argument(
|
|
"--save-as-folder",
|
|
action="store_true",
|
|
help="Save each input audio file's separated stems in a subfolder named after the audio file.",
|
|
)
|
|
infer_parser.add_argument("--device", default="auto", choices=["auto", "cpu", "cuda", "mps", "mlx"])
|
|
infer_parser.add_argument(
|
|
"--device-id", action="append", type=int, dest="device_ids", help="CUDA device id. Can be repeated."
|
|
)
|
|
infer_parser.add_argument("--format", default="wav", choices=["wav", "flac", "mp3", "m4a"], dest="output_format")
|
|
infer_parser.add_argument("--wav-bit-depth", default="FLOAT", choices=["FLOAT", "PCM_16", "PCM_24"])
|
|
infer_parser.add_argument("--flac-bit-depth", default="PCM_16", choices=["PCM_16", "PCM_24"])
|
|
infer_parser.add_argument("--mp3-bit-rate", default="320k")
|
|
infer_parser.add_argument("--m4a-bit-rate", default="512k")
|
|
infer_parser.add_argument("--m4a-codec", default="aac")
|
|
infer_parser.add_argument("--m4a-aac-at-quality", default=2, type=int)
|
|
infer_parser.add_argument("--tta", action="store_true", help="Enable test time augmentation.")
|
|
infer_parser.add_argument("--debug", action="store_true")
|
|
infer_parser.add_argument(
|
|
"--param", action="append", default=[], help="Inference override as key=value, for example --param batch_size=2."
|
|
)
|
|
infer_parser.set_defaults(func=cmd_infer)
|
|
|
|
# ==========================
|
|
# Ensemble
|
|
# ==========================
|
|
ensemble_parser = subparsers.add_parser(
|
|
"ensemble",
|
|
help="Combine multiple audio files with an ensemble algorithm.",
|
|
formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=60),
|
|
)
|
|
ensemble_parser.add_argument("files", nargs="+", help="Input audio files. At least two files are required.")
|
|
ensemble_parser.add_argument(
|
|
"-a",
|
|
"--algorithm",
|
|
default="avg_wave",
|
|
choices=ENSEMBLE_ALGORITHMS,
|
|
help="Ensemble algorithm.",
|
|
)
|
|
ensemble_parser.add_argument(
|
|
"-w",
|
|
"--weights",
|
|
nargs="+",
|
|
type=float,
|
|
help="Input weights, for example --weights 1 0.8 1.2. Defaults to all 1.",
|
|
)
|
|
ensemble_parser.add_argument("-o", "--output", required=True, help="Output audio file.")
|
|
ensemble_parser.add_argument("--format", choices=["wav", "flac", "mp3", "m4a"], dest="output_format")
|
|
ensemble_parser.add_argument("--wav-bit-depth", default="FLOAT", choices=["FLOAT", "PCM_16", "PCM_24"])
|
|
ensemble_parser.add_argument("--flac-bit-depth", default="PCM_16", choices=["PCM_16", "PCM_24"])
|
|
ensemble_parser.add_argument("--mp3-bit-rate", default="320k")
|
|
ensemble_parser.add_argument("--m4a-bit-rate", default="512k")
|
|
ensemble_parser.add_argument("--m4a-codec", default="aac")
|
|
ensemble_parser.add_argument("--m4a-aac-at-quality", default=2, type=int)
|
|
ensemble_parser.set_defaults(func=cmd_ensemble)
|
|
|
|
# ==========================
|
|
# Workflow
|
|
# ==========================
|
|
workflow_parser = subparsers.add_parser(
|
|
"workflow",
|
|
help="Create, validate, or run an automatic multi-model workflow.",
|
|
formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=60),
|
|
)
|
|
workflow_subparsers = workflow_parser.add_subparsers(dest="workflow_command", required=True)
|
|
|
|
workflow_init_parser = workflow_subparsers.add_parser(
|
|
"init",
|
|
help="Write a starter workflow YAML file.",
|
|
formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=60),
|
|
)
|
|
workflow_init_parser.add_argument("-o", "--output", default="workflow.yaml", help="Workflow file to create.")
|
|
workflow_init_parser.add_argument("--force", action="store_true", help="Overwrite the output file if it exists.")
|
|
workflow_init_parser.set_defaults(func=cmd_workflow_init)
|
|
|
|
workflow_validate_parser = workflow_subparsers.add_parser(
|
|
"validate",
|
|
help="Validate a workflow YAML/JSON file.",
|
|
formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=60),
|
|
)
|
|
workflow_validate_parser.add_argument("-c", "--config", required=True, help="Workflow YAML/JSON file.")
|
|
workflow_validate_parser.add_argument(
|
|
"--model-dir",
|
|
help="Local model cache directory used when --require-files is set.",
|
|
)
|
|
workflow_validate_parser.add_argument(
|
|
"--check-models",
|
|
action="store_true",
|
|
help="Also check that every referenced model exists in the catalog.",
|
|
)
|
|
workflow_validate_parser.add_argument(
|
|
"--require-files",
|
|
action="store_true",
|
|
help="Also require every referenced catalog model file to exist locally.",
|
|
)
|
|
workflow_validate_parser.set_defaults(func=cmd_workflow_validate)
|
|
|
|
workflow_run_parser = workflow_subparsers.add_parser(
|
|
"run",
|
|
help="Run inference through a workflow YAML/JSON file.",
|
|
formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=60),
|
|
)
|
|
workflow_run_parser.add_argument("-c", "--config", required=True, help="Workflow YAML/JSON file.")
|
|
workflow_run_parser.add_argument("-i", "--input", required=True, help="Input audio file or folder.")
|
|
workflow_run_parser.add_argument("-o", "--output", default="results", help="Output folder.")
|
|
workflow_run_parser.add_argument(
|
|
"--output-layout",
|
|
default="folders",
|
|
choices=["folders", "flat"],
|
|
help=(
|
|
"Workflow output layout. 'folders' keeps each input under <output>/<audio>/; "
|
|
"'flat' writes outputs directly under the workflow task folder and save subfolders."
|
|
),
|
|
)
|
|
workflow_run_parser.add_argument(
|
|
"--model-dir",
|
|
help="Local model cache directory. Workflow step model_dir values take precedence.",
|
|
)
|
|
workflow_run_parser.add_argument(
|
|
"--download",
|
|
action="store_true",
|
|
help="Download missing model files before each workflow step is loaded.",
|
|
)
|
|
workflow_run_parser.add_argument("--source", default="modelscope", choices=["modelscope", "huggingface", "hf-mirror"])
|
|
workflow_run_parser.add_argument("--endpoint", help="Custom resolve endpoint. It must serve files by relative path.")
|
|
workflow_run_parser.add_argument("--device", choices=["auto", "cpu", "cuda", "mps", "mlx"])
|
|
workflow_run_parser.add_argument("--format", choices=["wav", "flac", "mp3", "m4a"], dest="output_format")
|
|
workflow_run_parser.add_argument("--wav-bit-depth", default="FLOAT", choices=["FLOAT", "PCM_16", "PCM_24"])
|
|
workflow_run_parser.add_argument("--flac-bit-depth", default="PCM_16", choices=["PCM_16", "PCM_24"])
|
|
workflow_run_parser.add_argument("--mp3-bit-rate", default="320k")
|
|
workflow_run_parser.add_argument("--m4a-bit-rate", default="512k")
|
|
workflow_run_parser.add_argument("--m4a-codec", default="aac")
|
|
workflow_run_parser.add_argument("--m4a-aac-at-quality", default=2, type=int)
|
|
workflow_run_parser.add_argument("--debug", action="store_true")
|
|
workflow_run_parser.set_defaults(func=cmd_workflow_run)
|
|
|
|
# ==========================
|
|
# Server
|
|
# ==========================
|
|
serve_parser = subparsers.add_parser(
|
|
"serve",
|
|
help="Start an OpenAI-style HTTP inference server.",
|
|
formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=60),
|
|
)
|
|
serve_parser.add_argument("model", nargs="?")
|
|
serve_parser.add_argument(
|
|
"--model-dir",
|
|
help="Local model cache directory. Defaults to PYMSS_MODEL_DIR, repository all_models if present, or ~/.cache/pymss/models.",
|
|
)
|
|
serve_parser.add_argument("--source", default="modelscope", choices=["modelscope", "huggingface", "hf-mirror"])
|
|
serve_parser.add_argument("--endpoint", help="Custom resolve endpoint. It must serve files by relative path.")
|
|
serve_parser.add_argument("--device", default="auto", choices=["auto", "cpu", "cuda", "mps", "mlx"])
|
|
serve_parser.add_argument(
|
|
"--device-id",
|
|
action="append",
|
|
type=int,
|
|
dest="device_ids",
|
|
help="CUDA device id. Can be repeated.",
|
|
)
|
|
serve_parser.add_argument("--host", default="127.0.0.1")
|
|
serve_parser.add_argument("--port", default=8000, type=int)
|
|
serve_parser.add_argument("--api-key", help="Optional bearer token required for /v1/* endpoints.")
|
|
serve_parser.add_argument("--debug", action="store_true")
|
|
serve_parser.add_argument(
|
|
"--param",
|
|
action="append",
|
|
default=[],
|
|
help="Inference override as key=value, for example --param batch_size=2.",
|
|
)
|
|
serve_parser.add_argument("--max-audio-seconds", default=600.0, type=float)
|
|
serve_parser.add_argument("--max-request-bytes", default=536870912, type=int)
|
|
serve_parser.add_argument("--max-queue-size", default=8, type=int)
|
|
serve_parser.add_argument("--request-timeout-seconds", default=0.0, type=float)
|
|
serve_parser.add_argument("--webui", action="store_true", help="Serve the optional browser WebUI at /ui/.")
|
|
serve_parser.set_defaults(func=cmd_serve)
|
|
|
|
return parser
|
|
|
|
|
|
def main(argv=None):
|
|
"""Run the pymss command-line interface.
|
|
|
|
Args:
|
|
argv (Sequence[str] | None, optional): Command-line arguments. Uses sys.argv when None. Defaults to None.
|
|
|
|
Returns:
|
|
int: Process exit code."""
|
|
parser = build_parser()
|
|
args = parser.parse_args(argv)
|
|
try:
|
|
return args.func(args)
|
|
except Exception as exc:
|
|
print(f"pymss: error: {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|