• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

popstas / talks-reducer / 22026525963

14 Feb 2026 08:13PM UTC coverage: 67.505% (-0.4%) from 67.877%
22026525963

push

github

popstas
feat: show absolute duration and file size in result output

Display output duration (e.g. 12:34) and file size (e.g. 45.2MB)
alongside the existing ratio percentages in both CLI and GUI result
summaries. A format_file_size helper is extracted in summaries.py to
avoid duplicating the unit-conversion logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

8 of 36 new or added lines in 3 files covered. (22.22%)

367 existing lines in 3 files now uncovered.

5958 of 8826 relevant lines covered (67.51%)

0.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.38
/talks_reducer/cli.py
1
"""Command line interface for the talks reducer package."""
2

3
from __future__ import annotations
1✔
4

5
import argparse
1✔
6
import os
1✔
7
import shutil
1✔
8
import subprocess
1✔
9
import sys
1✔
10
import time
1✔
11
from importlib import import_module
1✔
12
from pathlib import Path
1✔
13
from typing import Callable, Dict, List, Optional, Sequence, Tuple
1✔
14

15
from . import audio
1✔
16
from .ffmpeg import FFmpegNotFoundError
1✔
17
from .models import ProcessingOptions, default_temp_folder
1✔
18
from .pipeline import speed_up_video
1✔
19
from .progress import TqdmProgressReporter
1✔
20
from .version_utils import resolve_version
1✔
21

22

23
def _build_parser() -> argparse.ArgumentParser:
1✔
24
    """Create the argument parser used by the command line interface."""
25

26
    parser = argparse.ArgumentParser(
1✔
27
        description="Modifies a video file to play at different speeds when there is sound vs. silence.",
28
    )
29

30
    # Add version argument
31
    pkg_version = resolve_version()
1✔
32

33
    parser.set_defaults(optimize=True)
1✔
34

35
    parser.add_argument(
1✔
36
        "--version",
37
        action="version",
38
        version=f"talks-reducer {pkg_version}",
39
    )
40

41
    parser.add_argument(
1✔
42
        "input_file",
43
        type=str,
44
        nargs="+",
45
        help="The video file(s) you want modified. Can be one or more directories and / or single files.",
46
    )
47
    parser.add_argument(
1✔
48
        "-o",
49
        "--output_file",
50
        type=str,
51
        dest="output_file",
52
        help="The output file. Only usable if a single file is given. If not included, it'll append _ALTERED to the name.",
53
    )
54
    parser.add_argument(
1✔
55
        "--temp_folder",
56
        type=str,
57
        default=str(default_temp_folder()),
58
        help="The file path of the temporary working folder.",
59
    )
60
    parser.add_argument(
1✔
61
        "-t",
62
        "--silent_threshold",
63
        type=float,
64
        dest="silent_threshold",
65
        help="The volume amount that frames' audio needs to surpass to be considered sounded. Defaults to 0.01.",
66
    )
67
    parser.add_argument(
1✔
68
        "-S",
69
        "--sounded_speed",
70
        type=float,
71
        dest="sounded_speed",
72
        help="The speed that sounded (spoken) frames should be played at. Defaults to 1.",
73
    )
74
    parser.add_argument(
1✔
75
        "-s",
76
        "--silent_speed",
77
        type=float,
78
        dest="silent_speed",
79
        help="The speed that silent frames should be played at. Defaults to 4.",
80
    )
81
    parser.add_argument(
1✔
82
        "-fm",
83
        "--frame_margin",
84
        type=float,
85
        dest="frame_spreadage",
86
        help="Some silent frames adjacent to sounded frames are included to provide context. Defaults to 2.",
87
    )
88
    parser.add_argument(
1✔
89
        "-sr",
90
        "--sample_rate",
91
        type=float,
92
        dest="sample_rate",
93
        help="Sample rate of the input and output videos. Usually extracted automatically by FFmpeg.",
94
    )
95
    parser.add_argument(
1✔
96
        "--keyframe-interval",
97
        type=float,
98
        dest="keyframe_interval_seconds",
99
        help="Override the keyframe spacing in seconds when using --small. Defaults to 30.",
100
    )
101
    parser.add_argument(
1✔
102
        "--video-codec",
103
        choices=["h264", "hevc", "av1"],
104
        default="hevc",
105
        help=(
106
            "Select the video encoder used for the final render (default: hevc — "
107
            "h.265 for roughly 25% smaller files). Pick h264 (about 10% faster) "
108
            "when speed matters or av1 (no advantages) for experimental runs."
109
        ),
110
    )
111
    parser.add_argument(
1✔
112
        "--add-codec-suffix",
113
        dest="add_codec_suffix",
114
        action="store_true",
115
        help="Append the selected video codec to the default output filename.",
116
    )
117
    parser.add_argument(
1✔
118
        "--prefer-global-ffmpeg",
119
        action="store_true",
120
        help="Use an FFmpeg binary from PATH before falling back to the bundled static build.",
121
    )
122
    parser.add_argument(
1✔
123
        "--small",
124
        action="store_true",
125
        help="Apply small file optimizations: resize video to 720p (or 480p with --480), audio to 128k bitrate, best compression (uses CUDA if available).",
126
    )
127
    parser.add_argument(
1✔
128
        "--480",
129
        dest="small_480",
130
        action="store_true",
131
        help="Use with --small to scale video to 480p instead of 720p.",
132
    )
133
    parser.add_argument(
1✔
134
        "--no-optimize",
135
        dest="optimize",
136
        action="store_false",
137
        help="Disable the tuned encoding presets and use the fastest CUDA-oriented settings instead.",
138
    )
139
    parser.add_argument(
1✔
140
        "--url",
141
        dest="server_url",
142
        default=None,
143
        help="Process videos via a Talks Reducer server at the provided base URL (for example, http://localhost:9005).",
144
    )
145
    parser.add_argument(
1✔
146
        "--host",
147
        dest="host",
148
        default=None,
149
        help="Shortcut for --url when targeting a Talks Reducer server on port 9005 (for example, localhost).",
150
    )
151
    parser.add_argument(
1✔
152
        "--server-stream",
153
        action="store_true",
154
        help="Stream remote progress updates when using --url.",
155
    )
156
    return parser
1✔
157

158

159
def gather_input_files(paths: List[str]) -> List[str]:
1✔
160
    """Expand provided paths into a flat list of files that contain video streams."""
161

162
    files: List[str] = []
1✔
163
    for input_path in paths:
1✔
164
        if os.path.isfile(input_path) and audio.is_valid_video_file(input_path):
1✔
165
            files.append(os.path.abspath(input_path))
1✔
166
        elif os.path.isdir(input_path):
1✔
167
            for file in os.listdir(input_path):
1✔
168
                candidate = os.path.join(input_path, file)
1✔
169
                if audio.is_valid_video_file(candidate):
1✔
170
                    files.append(candidate)
1✔
171
    return files
1✔
172

173

174
def _print_total_time(start_time: float) -> None:
1✔
175
    """Print the elapsed processing time since *start_time*."""
176

177
    end_time = time.time()
1✔
178
    total_time = end_time - start_time
1✔
179
    hours, remainder = divmod(total_time, 3600)
1✔
180
    minutes, seconds = divmod(remainder, 60)
1✔
181
    print(f"\nTime: {int(hours)}h {int(minutes)}m {seconds:.2f}s")
1✔
182

183

184
class CliApplication:
1✔
185
    """Coordinator for CLI processing with dependency injection support."""
186

187
    def __init__(
1✔
188
        self,
189
        *,
190
        gather_files: Callable[[List[str]], List[str]],
191
        send_video: Optional[Callable[..., Tuple[Path, str, str]]],
192
        speed_up: Callable[[ProcessingOptions, object], object],
193
        reporter_factory: Callable[[], object],
194
        remote_error_message: Optional[str] = None,
195
    ) -> None:
196
        self._gather_files = gather_files
1✔
197
        self._send_video = send_video
1✔
198
        self._speed_up = speed_up
1✔
199
        self._reporter_factory = reporter_factory
1✔
200
        self._remote_error_message = remote_error_message
1✔
201

202
    def run(self, parsed_args: argparse.Namespace) -> Tuple[int, List[str]]:
1✔
203
        """Execute the CLI pipeline for *parsed_args*."""
204

205
        start_time = time.time()
1✔
206
        files = self._gather_files(parsed_args.input_file)
1✔
207

208
        # Check if any files were found
209
        if not files:
1✔
210
            error_messages: List[str] = []
×
211
            for input_path in parsed_args.input_file:
×
212
                if os.path.isfile(input_path):
×
213
                    # File exists but was rejected - check if it's a valid video file
214
                    if not audio.is_valid_video_file(input_path):
×
215
                        error_messages.append(
×
216
                            f"Error: '{input_path}' is not a valid video file."
217
                        )
218
                    else:
219
                        error_messages.append(
×
220
                            f"Error: '{input_path}' could not be processed."
221
                        )
222
                elif os.path.isdir(input_path):
×
223
                    error_messages.append(
×
224
                        f"Error: No valid video files found in '{input_path}'."
225
                    )
226
                else:
227
                    error_messages.append(
×
228
                        f"Error: '{input_path}' does not exist or is not accessible."
229
                    )
230
            return 1, error_messages
×
231

232
        args: Dict[str, object] = {
1✔
233
            key: value for key, value in vars(parsed_args).items() if value is not None
234
        }
235
        del args["input_file"]
1✔
236

237
        if "host" in args:
1✔
238
            del args["host"]
×
239

240
        if len(files) > 1 and "output_file" in args:
1✔
241
            del args["output_file"]
×
242

243
        if getattr(parsed_args, "small_480", False) and not getattr(
1✔
244
            parsed_args, "small", False
245
        ):
246
            print(
×
247
                "Warning: --480 has no effect unless --small is also provided.",
248
                file=sys.stderr,
249
            )
250

251
        error_messages = []
1✔
252
        reporter_logs: List[str] = []
1✔
253

254
        if getattr(parsed_args, "server_url", None):
1✔
255
            remote_success, remote_errors, fallback_logs = self._process_via_server(
1✔
256
                files, parsed_args, start_time
257
            )
258
            error_messages.extend(remote_errors)
1✔
259
            reporter_logs.extend(fallback_logs)
1✔
260
            if remote_success:
1✔
261
                return 0, error_messages
1✔
262

263
        reporter = self._reporter_factory()
1✔
264
        for message in reporter_logs:
1✔
265
            reporter.log(message)
1✔
266

267
        for index, file in enumerate(files):
1✔
268
            print(
1✔
269
                f"Processing file {index + 1}/{len(files)} '{os.path.basename(file)}'"
270
            )
271
            local_options = dict(args)
1✔
272

273
            option_kwargs: Dict[str, object] = {"input_file": Path(file)}
1✔
274

275
            if "output_file" in local_options:
1✔
276
                option_kwargs["output_file"] = Path(local_options["output_file"])
1✔
277
            if "temp_folder" in local_options:
1✔
278
                option_kwargs["temp_folder"] = Path(local_options["temp_folder"])
1✔
279
            if "silent_threshold" in local_options:
1✔
280
                option_kwargs["silent_threshold"] = float(
1✔
281
                    local_options["silent_threshold"]
282
                )
283
            if "silent_speed" in local_options:
1✔
284
                option_kwargs["silent_speed"] = float(local_options["silent_speed"])
1✔
285
            if "sounded_speed" in local_options:
1✔
286
                option_kwargs["sounded_speed"] = float(local_options["sounded_speed"])
1✔
287
            if "frame_spreadage" in local_options:
1✔
288
                option_kwargs["frame_spreadage"] = int(local_options["frame_spreadage"])
1✔
289
            if "sample_rate" in local_options:
1✔
290
                option_kwargs["sample_rate"] = int(local_options["sample_rate"])
1✔
291
            if "keyframe_interval_seconds" in local_options:
1✔
292
                option_kwargs["keyframe_interval_seconds"] = float(
1✔
293
                    local_options["keyframe_interval_seconds"]
294
                )
295
            if "video_codec" in local_options:
1✔
296
                option_kwargs["video_codec"] = str(local_options["video_codec"])
1✔
297
            if local_options.get("add_codec_suffix"):
1✔
298
                option_kwargs["add_codec_suffix"] = True
1✔
299
            if "optimize" in local_options:
1✔
300
                option_kwargs["optimize"] = bool(local_options["optimize"])
1✔
301
            if "small" in local_options:
1✔
302
                option_kwargs["small"] = bool(local_options["small"])
1✔
303
            if local_options.get("small_480"):
1✔
304
                option_kwargs["small_target_height"] = 480
×
305
            if "prefer_global_ffmpeg" in local_options:
1✔
306
                option_kwargs["prefer_global_ffmpeg"] = bool(
1✔
307
                    local_options["prefer_global_ffmpeg"]
308
                )
309
            options = ProcessingOptions(**option_kwargs)
1✔
310

311
            try:
1✔
312
                result = self._speed_up(options, reporter=reporter)
1✔
313
            except FFmpegNotFoundError as exc:
×
314
                message = str(exc)
×
315
                return 1, [*error_messages, message]
×
316

317
            reporter.log(f"Completed: {result.output_file}")
1✔
318
            summary_parts: List[str] = []
1✔
319
            time_ratio = getattr(result, "time_ratio", None)
1✔
320
            size_ratio = getattr(result, "size_ratio", None)
1✔
321
            if time_ratio is not None:
1✔
322
                time_str = f"time: {time_ratio * 100:.0f}%"
1✔
323
                output_duration = getattr(result, "output_duration", None)
1✔
324
                if output_duration:
1✔
NEW
325
                    mins, secs = divmod(int(round(output_duration)), 60)
×
NEW
326
                    time_str += f" ({mins}:{secs:02d})"
×
327
                summary_parts.append(time_str)
1✔
328
            if size_ratio is not None:
1✔
329
                size_str = f"size: {size_ratio * 100:.0f}%"
1✔
330
                if result.output_file.exists():
1✔
NEW
331
                    size_bytes = result.output_file.stat().st_size
×
NEW
332
                    value = float(size_bytes)
×
NEW
333
                    for unit in ("B", "KB", "MB", "GB"):
×
NEW
334
                        if abs(value) < 1024:
×
NEW
335
                            size_label = (
×
336
                                f"{value:.0f}{unit}"
337
                                if unit == "B"
338
                                else f"{value:.1f}{unit}"
339
                            )
NEW
340
                            break
×
NEW
341
                        value /= 1024
×
342
                    else:
NEW
343
                        size_label = f"{value:.1f}TB"
×
NEW
344
                    size_str += f" ({size_label})"
×
345
                summary_parts.append(size_str)
1✔
346
            if summary_parts:
1✔
347
                reporter.log("Result: " + ", ".join(summary_parts))
1✔
348

349
        _print_total_time(start_time)
1✔
350
        return 0, error_messages
1✔
351

352
    def _process_via_server(
1✔
353
        self,
354
        files: Sequence[str],
355
        parsed_args: argparse.Namespace,
356
        start_time: float,
357
    ) -> Tuple[bool, List[str], List[str]]:
358
        """Upload *files* to the configured server and download the results."""
359

360
        if not self._send_video:
1✔
361
            message = self._remote_error_message or "Server processing is unavailable."
1✔
362
            fallback_notice = "Falling back to local processing pipeline."
1✔
363
            return False, [message, fallback_notice], [message, fallback_notice]
1✔
364

365
        server_url = parsed_args.server_url
1✔
366
        if not server_url:
1✔
367
            message = "Server URL was not provided."
×
368
            fallback_notice = "Falling back to local processing pipeline."
×
369
            return False, [message, fallback_notice], [message, fallback_notice]
×
370

371
        output_override: Optional[Path] = None
1✔
372
        if parsed_args.output_file and len(files) == 1:
1✔
373
            output_override = Path(parsed_args.output_file).expanduser()
×
374
        elif parsed_args.output_file and len(files) > 1:
1✔
375
            print(
1✔
376
                "Warning: --output is ignored when processing multiple files via the server.",
377
                file=sys.stderr,
378
            )
379

380
        remote_option_values: Dict[str, object] = {}
1✔
381
        if parsed_args.silent_threshold is not None:
1✔
382
            remote_option_values["silent_threshold"] = float(
1✔
383
                parsed_args.silent_threshold
384
            )
385
        if parsed_args.silent_speed is not None:
1✔
386
            remote_option_values["silent_speed"] = float(parsed_args.silent_speed)
1✔
387
        if parsed_args.sounded_speed is not None:
1✔
388
            remote_option_values["sounded_speed"] = float(parsed_args.sounded_speed)
1✔
389
        if getattr(parsed_args, "video_codec", None):
1✔
390
            remote_option_values["video_codec"] = str(parsed_args.video_codec)
1✔
391
        if getattr(parsed_args, "add_codec_suffix", False):
1✔
392
            remote_option_values["add_codec_suffix"] = True
×
393
        if getattr(parsed_args, "prefer_global_ffmpeg", False):
1✔
394
            remote_option_values["prefer_global_ffmpeg"] = True
1✔
395
        if getattr(parsed_args, "optimize", True) is False:
1✔
396
            remote_option_values["optimize"] = False
×
397

398
        unsupported_options: List[str] = []
1✔
399
        for name in (
1✔
400
            "frame_spreadage",
401
            "sample_rate",
402
            "temp_folder",
403
            "keyframe_interval_seconds",
404
        ):
405
            if getattr(parsed_args, name) is not None:
1✔
406
                unsupported_options.append(f"--{name.replace('_', '-')}")
1✔
407

408
        if unsupported_options:
1✔
409
            print(
1✔
410
                "Warning: the following options are ignored when using --url: "
411
                + ", ".join(sorted(unsupported_options)),
412
                file=sys.stderr,
413
            )
414

415
        small_480_mode = bool(getattr(parsed_args, "small_480", False)) and bool(
1✔
416
            getattr(parsed_args, "small", False)
417
        )
418
        if small_480_mode:
1✔
419
            remote_option_values["small_480"] = True
×
420

421
        for index, file in enumerate(files, start=1):
1✔
422
            basename = os.path.basename(file)
1✔
423
            print(
1✔
424
                f"Processing file {index}/{len(files)} '{basename}' via server {server_url}"
425
            )
426
            printed_log_header = False
1✔
427
            progress_state: dict[str, tuple[Optional[int], Optional[int], str]] = {}
1✔
428
            stream_updates = bool(getattr(parsed_args, "server_stream", False))
1✔
429

430
            def _stream_server_log(line: str) -> None:
1✔
431
                nonlocal printed_log_header
432
                if not printed_log_header:
1✔
433
                    print("\nServer log:", flush=True)
1✔
434
                    printed_log_header = True
1✔
435
                print(line, flush=True)
1✔
436

437
            def _stream_progress(
1✔
438
                desc: str, current: Optional[int], total: Optional[int], unit: str
439
            ) -> None:
440
                key = desc or "Processing"
1✔
441
                state = (current, total, unit)
1✔
442
                if progress_state.get(key) == state:
1✔
443
                    return
1✔
444
                progress_state[key] = state
1✔
445

446
                parts: List[str] = []
1✔
447
                if current is not None and total and total > 0:
1✔
448
                    percent = (current / total) * 100
1✔
449
                    parts.append(f"{current}/{total}")
1✔
450
                    parts.append(f"{percent:.1f}%")
1✔
451
                elif current is not None:
1✔
452
                    parts.append(str(current))
×
453
                if unit:
1✔
454
                    parts.append(unit)
1✔
455
                message = " ".join(parts).strip()
1✔
456
                print(f"{key}: {message or 'update'}", flush=True)
1✔
457

458
            try:
1✔
459
                destination, summary, log_text = self._send_video(
1✔
460
                    input_path=Path(file),
461
                    output_path=output_override,
462
                    server_url=server_url,
463
                    small=bool(parsed_args.small),
464
                    small_480=small_480_mode,
465
                    **remote_option_values,
466
                    log_callback=_stream_server_log,
467
                    stream_updates=stream_updates,
468
                    progress_callback=_stream_progress if stream_updates else None,
469
                )
470
            except Exception as exc:  # pragma: no cover - network failure safeguard
471
                message = f"Failed to process {basename} via server: {exc}"
472
                fallback_notice = "Falling back to local processing pipeline."
473
                return False, [message, fallback_notice], [message, fallback_notice]
474

475
            print(summary)
1✔
476
            print(f"Saved processed video to {destination}")
1✔
477
            if log_text.strip() and not printed_log_header:
1✔
478
                print("\nServer log:\n" + log_text)
1✔
479

480
        _print_total_time(start_time)
1✔
481
        return True, [], []
1✔
482

483

484
def _launch_gui(argv: Sequence[str]) -> bool:
1✔
485
    """Attempt to launch the GUI with the provided arguments."""
486

487
    try:
×
488
        gui_module = import_module(".gui", __package__)
×
489
    except ImportError:
×
490
        return False
×
491

492
    gui_main = getattr(gui_module, "main", None)
×
493
    if gui_main is None:
×
494
        return False
×
495

496
    return bool(gui_main(list(argv)))
×
497

498

499
def _launch_server(argv: Sequence[str]) -> bool:
1✔
500
    """Attempt to launch the Gradio server with the provided arguments."""
501

502
    try:
×
503
        server_module = import_module(".server", __package__)
×
504
    except ImportError:
×
505
        return False
×
506

507
    server_main = getattr(server_module, "main", None)
×
508
    if server_main is None:
×
509
        return False
×
510

511
    server_main(list(argv))
×
512
    return True
×
513

514

515
def _find_server_tray_binary() -> Optional[Path]:
1✔
516
    """Return the best available path to the server tray executable."""
517

518
    binary_name = "talks-reducer-server-tray"
1✔
519
    candidates: List[Path] = []
1✔
520

521
    which_path = shutil.which(binary_name)
1✔
522
    if which_path:
1✔
523
        candidates.append(Path(which_path))
1✔
524

525
    try:
1✔
526
        launcher_dir = Path(sys.argv[0]).resolve().parent
1✔
527
    except Exception:
×
528
        launcher_dir = None
×
529

530
    potential_names = [binary_name]
1✔
531
    if sys.platform == "win32":
1✔
532
        potential_names = [f"{binary_name}.exe", binary_name]
×
533

534
    if launcher_dir is not None:
1✔
535
        for name in potential_names:
1✔
536
            candidates.append(launcher_dir / name)
1✔
537

538
    for candidate in candidates:
1✔
539
        if candidate and candidate.exists() and os.access(candidate, os.X_OK):
1✔
540
            return candidate
1✔
541

542
    return None
×
543

544

545
def _should_hide_subprocess_console() -> bool:
1✔
546
    """Return ``True` ` when a detached Windows launch should hide the console."""
547

548
    if sys.platform != "win32":
1✔
549
        return False
1✔
550

551
    try:
1✔
552
        import ctypes
1✔
553
    except Exception:  # pragma: no cover - optional runtime dependency
554
        return False
555

556
    try:
1✔
557
        get_console_window = ctypes.windll.kernel32.GetConsoleWindow  # type: ignore[attr-defined]
1✔
558
    except Exception:  # pragma: no cover - platform specific guard
559
        return False
560

561
    try:
1✔
562
        handle = get_console_window()
1✔
563
    except Exception:  # pragma: no cover - defensive fallback
564
        return False
565

566
    return handle == 0
1✔
567

568

569
def _launch_server_tray_binary(argv: Sequence[str]) -> bool:
1✔
570
    """Launch the packaged server tray executable when available."""
571

572
    command = _find_server_tray_binary()
1✔
573
    if command is None:
1✔
574
        return False
1✔
575

576
    tray_args = [str(command), *list(argv)]
1✔
577

578
    run_kwargs: Dict[str, object] = {"check": False}
1✔
579

580
    if sys.platform == "win32":
1✔
581
        no_window_flag = getattr(subprocess, "CREATE_NO_WINDOW", 0)
1✔
582
        if no_window_flag and _should_hide_subprocess_console():
1✔
583
            run_kwargs["creationflags"] = no_window_flag
1✔
584

585
    try:
1✔
586
        result = subprocess.run(tray_args, **run_kwargs)
1✔
587
    except OSError:
×
588
        return False
×
589

590
    return result.returncode == 0
1✔
591

592

593
def _launch_server_tray(argv: Sequence[str]) -> bool:
1✔
594
    """Attempt to launch the server tray helper with the provided arguments."""
595

596
    if _launch_server_tray_binary(argv):
1✔
597
        return True
×
598

599
    try:
1✔
600
        tray_module = import_module(".server_tray", __package__)
1✔
601
    except ImportError:
×
602
        return False
×
603

604
    tray_main = getattr(tray_module, "main", None)
1✔
605
    if tray_main is None:
1✔
606
        return False
×
607

608
    tray_main(list(argv))
1✔
609
    return True
1✔
610

611

612
def main(argv: Optional[Sequence[str]] = None) -> None:
1✔
613
    """Entry point for the command line interface.
614

615
    Launch the GUI when run without arguments, otherwise defer to the CLI.
616
    """
617

618
    if argv is None:
1✔
619
        argv_list = sys.argv[1:]
×
620
    else:
621
        argv_list = list(argv)
1✔
622

623
    if "--server" in argv_list:
1✔
624
        index = argv_list.index("--server")
1✔
625
        tray_args = argv_list[index + 1 :]
1✔
626
        if not _launch_server_tray(tray_args):
1✔
627
            print("Server tray mode is unavailable.", file=sys.stderr)
1✔
628
            sys.exit(1)
1✔
629
        return
1✔
630

631
    if argv_list and argv_list[0] in {"server", "serve"}:
1✔
632
        if not _launch_server(argv_list[1:]):
1✔
633
            print("Gradio server mode is unavailable.", file=sys.stderr)
1✔
634
            sys.exit(1)
1✔
635
        return
1✔
636

637
    if not argv_list:
1✔
638
        if _launch_gui(argv_list):
1✔
639
            return
1✔
640

641
        parser = _build_parser()
×
642
        parser.print_help()
×
643
        return
×
644

645
    parser = _build_parser()
1✔
646
    parsed_args = parser.parse_args(argv_list)
1✔
647

648
    host_value = getattr(parsed_args, "host", None)
1✔
649
    if host_value:
1✔
650
        parsed_args.server_url = f"http://{host_value}:9005"
×
651

652
    send_video = None
1✔
653
    remote_error_message: Optional[str] = None
1✔
654
    try:  # pragma: no cover - optional dependency guard
655
        from . import service_client
656
    except ImportError as exc:
×
657
        remote_error_message = (
×
658
            "Server mode requires the gradio_client dependency. " f"({exc})"
659
        )
660
    else:
661
        send_video = service_client.send_video
1✔
662

663
    application = CliApplication(
1✔
664
        gather_files=gather_input_files,
665
        send_video=send_video,
666
        speed_up=speed_up_video,
667
        reporter_factory=TqdmProgressReporter,
668
        remote_error_message=remote_error_message,
669
    )
670

671
    exit_code, error_messages = application.run(parsed_args)
1✔
672
    for message in error_messages:
1✔
673
        print(message, file=sys.stderr)
×
674
    if exit_code:
1✔
675
        sys.exit(exit_code)
×
676

677

678
if __name__ == "__main__":
1✔
679
    main()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc