From f40c888018d388dea6035abfef815a76282e12b9 Mon Sep 17 00:00:00 2001 From: Gangzheng Tong Date: Wed, 24 Jun 2026 19:16:40 +0000 Subject: [PATCH] Add Omnidream interactive upsampler client --- .../interactive_drive/backends/world_model.py | 80 ++++- .../configs/example_world_model.yaml | 11 +- .../configs/example_world_model_perf.yaml | 7 + .../world_model/flashdreams_adapter.py | 4 - .../interactive_drive/world_model/manifest.py | 14 + .../interactive_drive/world_model/uplift.py | 296 ++++++++++++++++++ integrations/omnidreams/pyproject.toml | 2 + .../tests/interactive_drive/test_manifest.py | 33 ++ .../test_world_model_adapter.py | 4 + uv.lock | 2 + 10 files changed, 441 insertions(+), 12 deletions(-) create mode 100644 integrations/omnidreams/omnidreams/interactive_drive/world_model/uplift.py diff --git a/integrations/omnidreams/omnidreams/interactive_drive/backends/world_model.py b/integrations/omnidreams/omnidreams/interactive_drive/backends/world_model.py index d4fae0b58..459a4809c 100644 --- a/integrations/omnidreams/omnidreams/interactive_drive/backends/world_model.py +++ b/integrations/omnidreams/omnidreams/interactive_drive/backends/world_model.py @@ -29,6 +29,10 @@ FlashdreamsWorldModelSession, ) from omnidreams.interactive_drive.world_model.manifest import WorldModelManifest +from omnidreams.interactive_drive.world_model.uplift import ( + UpliftStreamClient, + UpliftStreamConfig, +) from PIL import Image _FIRST_STEADY_STATE_WARMUP_MESSAGE = "Optimizing world model..." @@ -55,6 +59,7 @@ def __init__( self._scene: SceneBundle | None = None self._next_chunk_count = 0 self._debug_first_chunk_condition_frames: tuple[np.ndarray, ...] | None = None + self._uplift: UpliftStreamClient | None = self._build_uplift_client() @property def can_prewarm(self) -> bool: @@ -107,6 +112,7 @@ def load_scene(self, scene: SceneBundle) -> None: self._session.prepare_for_scene( initial_rgb=scene.initial_rgb, prompt=scene.prompt ) + self._reset_uplift_stream() prepare_end = time.perf_counter() logger.info( "[world-model] load_scene " @@ -154,10 +160,12 @@ def render_first_chunk(self, trajectory: TrajectoryChunk) -> FrameChunk: scene.initial_rgb, condition_frames, scene.prompt ) model_end = time.perf_counter() + uplift_status = self._submit_uplift(model_frames) merged_frames = self._merge_frames( display_frames, model_frames, annotate_first_transition=True, + status_message=uplift_status, ) merge_end = time.perf_counter() logger.info( @@ -193,7 +201,12 @@ def render_next_chunk(self, trajectory: TrajectoryChunk) -> FrameChunk: condition_frames = [frame.rgb_host_uint8 for frame in raster_chunk.frames] model_frames = self._session.continue_generation(condition_frames) model_end = time.perf_counter() - merged_frames = self._merge_frames(raster_chunk.frames, model_frames) + uplift_status = self._submit_uplift(model_frames) + merged_frames = self._merge_frames( + raster_chunk.frames, + model_frames, + status_message=uplift_status, + ) merge_end = time.perf_counter() self._next_chunk_count += 1 total_ms = (merge_end - chunk_start) * 1000.0 @@ -228,15 +241,58 @@ def render_next_chunk(self, trajectory: TrajectoryChunk) -> FrameChunk: def reset(self) -> None: self._session.reset() self._next_chunk_count = 0 + self._reset_uplift_stream() def reset_scene_conditioning(self) -> None: self._session.reset(clear_precomputed_embeddings=True) self._next_chunk_count = 0 + self._reset_uplift_stream() def close(self) -> None: + if self._uplift is not None: + self._uplift.close() + self._uplift = None self._session.close() self._rasterizer.cleanup() + def _build_uplift_client(self) -> UpliftStreamClient | None: + if not self._manifest.upsampling_enabled: + return None + config = UpliftStreamConfig( + server=self._manifest.upsampling_server, + scale=self._manifest.upsampling_scale, + sparse_ratio=self._manifest.upsampling_sparse_ratio, + input_format=self._manifest.upsampling_input_format, # type: ignore[arg-type] + input_jpeg_quality=self._manifest.upsampling_input_jpeg_quality, + return_frames=self._manifest.upsampling_return_frames, + max_queue_chunks=self._manifest.upsampling_max_queue_chunks, + max_message_mb=self._manifest.upsampling_max_message_mb, + ) + logger.info( + "[world-model] uplift enabled server={} scale={}x queue_depth={} " + "input_format={} return_frames={}", + config.server, + config.scale, + config.max_queue_chunks, + config.input_format, + config.return_frames, + ) + client = UpliftStreamClient(config) + client.start() + return client + + def _reset_uplift_stream(self) -> None: + if not self._manifest.upsampling_enabled: + return + if self._uplift is not None: + self._uplift.close() + self._uplift = self._build_uplift_client() + + def _submit_uplift(self, model_frames: list[object]) -> str | None: + if self._uplift is None: + return None + return self._uplift.submit(model_frames) + def _require_scene(self) -> SceneBundle: if self._scene is None: raise RuntimeError( @@ -271,6 +327,7 @@ def _merge_frames( model_frames: Sequence[object], *, annotate_first_transition: bool = False, + status_message: str | None = None, ) -> tuple[PresentedFrame, ...]: if len(raster_frames) != len(model_frames): raise ValueError( @@ -283,6 +340,14 @@ def _merge_frames( for index, (raster_frame, model_rgb) in enumerate( zip(raster_frames, model_frames, strict=True) ): + frame_status = _combine_status_messages( + ( + _FIRST_STEADY_STATE_WARMUP_MESSAGE + if annotate_first_transition and index == last_index + else None + ), + status_message, + ) merged.append( PresentedFrame( timestamp_us=raster_frame.timestamp_us, @@ -292,11 +357,7 @@ def _merge_frames( depth_native=raster_frame.depth_native, model_rgb_host_uint8=model_rgb, bev_host_uint8=raster_frame.bev_host_uint8, - status_message=( - _FIRST_STEADY_STATE_WARMUP_MESSAGE - if annotate_first_transition and index == last_index - else None - ), + status_message=frame_status, ) ) return tuple(merged) @@ -314,3 +375,10 @@ def _log_prompt_handoff(stage: str, scene: SceneBundle) -> None: f"length={len(prompt)} " f"text={prompt_text!r}", ) + + +def _combine_status_messages(*messages: str | None) -> str | None: + parts = [message for message in messages if message] + if not parts: + return None + return " | ".join(parts) diff --git a/integrations/omnidreams/omnidreams/interactive_drive/configs/example_world_model.yaml b/integrations/omnidreams/omnidreams/interactive_drive/configs/example_world_model.yaml index c91e31eef..fda6d0d0b 100644 --- a/integrations/omnidreams/omnidreams/interactive_drive/configs/example_world_model.yaml +++ b/integrations/omnidreams/omnidreams/interactive_drive/configs/example_world_model.yaml @@ -26,7 +26,14 @@ local_attn_size: 6 skip_finalize_kv_cache: false sink_size: 0 denoising_steps: [1000, 500] -upsampling_enabled: false -upsampling_scale: 4 +upsampling_enabled: true +upsampling_server: 127.0.0.1:8090 +upsampling_scale: 2 +upsampling_sparse_ratio: 1.5 +upsampling_input_format: jpeg +upsampling_input_jpeg_quality: 90 +upsampling_return_frames: false +upsampling_max_queue_chunks: 16 +upsampling_max_message_mb: 512 device: cuda:0 seed_for_every_rollout: diff --git a/integrations/omnidreams/omnidreams/interactive_drive/configs/example_world_model_perf.yaml b/integrations/omnidreams/omnidreams/interactive_drive/configs/example_world_model_perf.yaml index 335ffa8b7..b64d1244a 100644 --- a/integrations/omnidreams/omnidreams/interactive_drive/configs/example_world_model_perf.yaml +++ b/integrations/omnidreams/omnidreams/interactive_drive/configs/example_world_model_perf.yaml @@ -28,7 +28,14 @@ skip_finalize_kv_cache: true sink_size: 0 denoising_steps: [1000, 100] upsampling_enabled: false +upsampling_server: 127.0.0.1:8090 upsampling_scale: 4 +upsampling_sparse_ratio: 0.0 +upsampling_input_format: jpeg +upsampling_input_jpeg_quality: 90 +upsampling_return_frames: false +upsampling_max_queue_chunks: 4 +upsampling_max_message_mb: 512 device: cuda:0 seed_for_every_rollout: diff --git a/integrations/omnidreams/omnidreams/interactive_drive/world_model/flashdreams_adapter.py b/integrations/omnidreams/omnidreams/interactive_drive/world_model/flashdreams_adapter.py index 1dfc9a123..25f7545ef 100644 --- a/integrations/omnidreams/omnidreams/interactive_drive/world_model/flashdreams_adapter.py +++ b/integrations/omnidreams/omnidreams/interactive_drive/world_model/flashdreams_adapter.py @@ -31,10 +31,6 @@ def _select_config_name(manifest: WorldModelManifest) -> str: Returns a key from ``omnidreams.config.OMNIDREAMS_CONFIGS`` (i.e. the same slug ``flashdreams-run`` accepts as its first positional arg). """ - if manifest.upsampling_enabled: - raise NotImplementedError( - "flashdreams interactive-drive path does not support upsampling." - ) if manifest.sink_size != 0: raise NotImplementedError( "flashdreams interactive-drive path currently supports sink_size=0 only." diff --git a/integrations/omnidreams/omnidreams/interactive_drive/world_model/manifest.py b/integrations/omnidreams/omnidreams/interactive_drive/world_model/manifest.py index abc60280d..a0f69357e 100644 --- a/integrations/omnidreams/omnidreams/interactive_drive/world_model/manifest.py +++ b/integrations/omnidreams/omnidreams/interactive_drive/world_model/manifest.py @@ -157,7 +157,14 @@ class WorldModelManifest: sink_size: int = 0 denoising_steps: list[int] = field(default_factory=lambda: [1000, 500]) upsampling_enabled: bool = False + upsampling_server: str = "127.0.0.1:8090" upsampling_scale: int = 4 + upsampling_sparse_ratio: float = 0.0 + upsampling_input_format: str = "jpeg" + upsampling_input_jpeg_quality: int = 90 + upsampling_return_frames: bool = False + upsampling_max_queue_chunks: int = 4 + upsampling_max_message_mb: int = 512 device: str = "cuda:0" seed_for_every_rollout: int | None = None native_dit_acceleration: str = "disabled" @@ -207,7 +214,14 @@ def load_world_model_manifest(path: str | Path) -> WorldModelManifest: sink_size=int(data.get("sink_size", 0)), denoising_steps=[int(x) for x in data.get("denoising_steps", [1000, 500])], upsampling_enabled=bool(data.get("upsampling_enabled", False)), + upsampling_server=str(data.get("upsampling_server", "127.0.0.1:8090")), upsampling_scale=int(data.get("upsampling_scale", 4)), + upsampling_sparse_ratio=float(data.get("upsampling_sparse_ratio", 0.0)), + upsampling_input_format=str(data.get("upsampling_input_format", "jpeg")), + upsampling_input_jpeg_quality=int(data.get("upsampling_input_jpeg_quality", 90)), + upsampling_return_frames=bool(data.get("upsampling_return_frames", False)), + upsampling_max_queue_chunks=int(data.get("upsampling_max_queue_chunks", 4)), + upsampling_max_message_mb=int(data.get("upsampling_max_message_mb", 512)), device=str(data.get("device", "cuda:0")), seed_for_every_rollout=( int(data["seed_for_every_rollout"]) diff --git a/integrations/omnidreams/omnidreams/interactive_drive/world_model/uplift.py b/integrations/omnidreams/omnidreams/interactive_drive/world_model/uplift.py new file mode 100644 index 000000000..1df243c04 --- /dev/null +++ b/integrations/omnidreams/omnidreams/interactive_drive/world_model/uplift.py @@ -0,0 +1,296 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + +from __future__ import annotations + +import io +import queue +import threading +import time +import uuid +from dataclasses import dataclass +from typing import Literal + +import grpc +import numpy as np +from loguru import logger + +FrameEncoding = Literal["jpeg", "raw"] + + +@dataclass(frozen=True) +class UpliftStreamConfig: + server: str = "127.0.0.1:8090" + scale: int = 4 + sparse_ratio: float = 0.0 + input_format: FrameEncoding = "jpeg" + input_jpeg_quality: int = 90 + return_frames: bool = False + max_queue_chunks: int = 4 + max_message_mb: int = 512 + connect_timeout_s: float = 5.0 + + +@dataclass(frozen=True) +class _UpliftChunk: + chunk_index: int + frames: tuple[object, ...] + + +class UpliftStreamClient: + """Best-effort async sender from interactive-drive chunks to FlashVSR uplift. + + The demo does not consume upsampled frames yet. By default requests are + ``display_only`` so the FlashVSR server's HTTP viewer owns presentation. + The response loop still runs so gRPC backpressure and server errors are + observed, and ``return_frames=True`` is kept as the future integration hook. + """ + + def __init__(self, config: UpliftStreamConfig) -> None: + if config.scale not in (2, 4): + raise ValueError(f"Uplift scale must be 2 or 4, got {config.scale}") + if config.input_format not in ("jpeg", "raw"): + raise ValueError( + f"Uplift input format must be 'jpeg' or 'raw', got {config.input_format!r}" + ) + if not 1 <= config.input_jpeg_quality <= 100: + raise ValueError("Uplift JPEG quality must be between 1 and 100") + self._config = config + self._queue: queue.Queue[_UpliftChunk | None] = queue.Queue( + maxsize=max(1, int(config.max_queue_chunks)) + ) + self._stop = threading.Event() + self._lock = threading.Lock() + self._thread: threading.Thread | None = None + self._next_chunk_index = 0 + self._queued = 0 + self._sent = 0 + self._received = 0 + self._dropped = 0 + self._last_error: str | None = None + self._last_elapsed_ms: float | None = None + self._last_response_chunk: int | None = None + self._connected = False + + def start(self) -> None: + if self._thread is not None: + return + self._thread = threading.Thread( + target=self._run, + name="interactive_drive-uplift-stream", + daemon=True, + ) + self._thread.start() + + def submit(self, frames: list[object]) -> str: + """Queue one generated world-model chunk without blocking rendering.""" + if self._thread is None: + self.start() + chunk = _UpliftChunk( + chunk_index=self._next_chunk_index, + frames=tuple(frames), + ) + self._next_chunk_index += 1 + try: + self._queue.put_nowait(chunk) + with self._lock: + self._queued += 1 + except queue.Full: + with self._lock: + self._dropped += 1 + logger.warning( + "[uplift] drop chunk due to backpressure chunk_index={} " + "server={} queue_depth={}", + chunk.chunk_index, + self._config.server, + self._queue.maxsize, + ) + return self.status_message() + + def status_message(self) -> str: + with self._lock: + state = "connected" if self._connected else "connecting" + parts = [ + f"Upsampling {state}", + f"server={self._config.server}", + f"scale={self._config.scale}x", + f"queue={self._queue.qsize()}/{self._queue.maxsize}", + f"sent={self._sent}", + f"ack={self._received}", + ] + if self._dropped: + parts.append(f"dropped={self._dropped}") + if self._last_elapsed_ms is not None: + parts.append(f"last={self._last_elapsed_ms:.0f}ms") + if self._last_error: + parts.append(f"error={self._last_error}") + return " | ".join(parts) + + def close(self) -> None: + self._stop.set() + while True: + try: + self._queue.get_nowait() + except queue.Empty: + break + try: + self._queue.put_nowait(None) + except queue.Full: + pass + thread = self._thread + if thread is not None: + thread.join(timeout=2.0) + self._thread = None + + def _run(self) -> None: + try: + from flashvsr.grpc.protos import flashvsr_pb2 as pb2 + from flashvsr.grpc.protos import flashvsr_pb2_grpc as pb2_grpc + except ModuleNotFoundError as exc: + self._set_error( + "flashvsr package is required when upsampling_enabled=true" + ) + logger.exception("[uplift] unable to import FlashVSR gRPC protos") + return + + max_bytes = self._config.max_message_mb * 1024 * 1024 + channel = grpc.insecure_channel( + self._config.server, + options=[ + ("grpc.max_send_message_length", max_bytes), + ("grpc.max_receive_message_length", max_bytes), + ], + ) + stub = pb2_grpc.FlashVSRStub(channel) + try: + status = stub.get_status( + pb2.StatusRequest(), timeout=self._config.connect_timeout_s + ) + with self._lock: + self._connected = bool(status.ready) + self._last_error = None if status.ready else "server not ready" + if not status.ready: + return + logger.info( + "[uplift] connected server={} device={} model={}", + self._config.server, + status.device, + status.model_name, + ) + + for response in stub.upscale_video(self._request_iter(pb2)): + if response.error: + self._set_error(response.error) + logger.error("[uplift] server error: {}", response.error) + continue + with self._lock: + self._received += 1 + self._last_elapsed_ms = float(response.elapsed_ms) + self._last_response_chunk = int(response.chunk_index) + if self._config.return_frames and response.frames_rgb: + # Future hook: this is where upsampled frames can be routed + # back into the demo. For now the FlashVSR HTTP viewer owns + # display, so we only drain responses to keep flow healthy. + pass + except grpc.RpcError as exc: + self._set_error(_grpc_error_details(exc)) + logger.exception("[uplift] gRPC stream failed") + except Exception as exc: + self._set_error(str(exc)) + logger.exception("[uplift] stream failed") + finally: + with self._lock: + self._connected = False + channel.close() + + def _request_iter(self, pb2: object): + session_id = str(uuid.uuid4()) + while not self._stop.is_set(): + try: + item = self._queue.get(timeout=0.1) + except queue.Empty: + continue + if item is None: + return + try: + request = self._build_request(pb2, item, session_id=session_id) + except Exception as exc: + self._set_error(str(exc)) + logger.exception( + "[uplift] failed to build request chunk_index={}", + item.chunk_index, + ) + continue + with self._lock: + self._sent += 1 + self._last_error = None + yield request + + def _build_request( + self, pb2: object, item: _UpliftChunk, *, session_id: str + ) -> object: + frame_data = _frames_to_numpy(item.frames) + num_frames, height, width, _channels = frame_data.shape + req = pb2.UpscaleChunkRequest( + session_id=session_id, + chunk_index=item.chunk_index, + num_frames=num_frames, + height=height, + width=width, + display_only=not self._config.return_frames, + ) + if self._config.input_format == "jpeg": + req.frame_encoding = pb2.FRAME_ENCODING_JPEG + req.frames_jpeg.extend( + _encode_jpeg_frames(frame_data, self._config.input_jpeg_quality) + ) + else: + req.frame_encoding = pb2.FRAME_ENCODING_RAW_RGB + req.frames_rgb = frame_data.tobytes() + + if item.chunk_index == 0: + req.input_height = height + req.input_width = width + req.scale = self._config.scale + req.sparse_ratio = self._config.sparse_ratio + return req + + def _set_error(self, message: str) -> None: + with self._lock: + self._last_error = message + + +def _frames_to_numpy(frames: tuple[object, ...]) -> np.ndarray: + if not frames: + raise ValueError("Cannot send an empty chunk to uplift") + arrays = [ + np.ascontiguousarray(np.asarray(frame, dtype=np.uint8)[..., :3]) + for frame in frames + ] + return np.ascontiguousarray(np.stack(arrays, axis=0)) + + +def _encode_jpeg_frames(frames: np.ndarray, quality: int) -> list[bytes]: + try: + from PIL import Image + except ImportError as exc: + raise RuntimeError("Uplift JPEG input requires Pillow") from exc + + encoded: list[bytes] = [] + for frame in frames: + buf = io.BytesIO() + Image.fromarray(np.ascontiguousarray(frame)).save( + buf, format="JPEG", quality=quality + ) + encoded.append(buf.getvalue()) + return encoded + + +def _grpc_error_details(exc: grpc.RpcError) -> str: + details = getattr(exc, "details", None) + if callable(details): + try: + return str(details()) + except Exception: + pass + return str(exc) diff --git a/integrations/omnidreams/pyproject.toml b/integrations/omnidreams/pyproject.toml index f9c40d37e..1230bfa69 100644 --- a/integrations/omnidreams/pyproject.toml +++ b/integrations/omnidreams/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ # ``omnidreams.grpc``, and the ``omnidreams.interactive_drive`` desktop # demo subpackage). "flashdreams[serving]", + "flashdreams-flashvsr", "mediapy>=1.1", "ludus-renderer", "imageio>=2.20", @@ -55,6 +56,7 @@ dependencies = [ [tool.uv.sources] flashdreams = { workspace = true } +flashdreams-flashvsr = { workspace = true } ludus-renderer = { workspace = true } [project.optional-dependencies] diff --git a/integrations/omnidreams/tests/interactive_drive/test_manifest.py b/integrations/omnidreams/tests/interactive_drive/test_manifest.py index b268ad31e..a0cf420f7 100644 --- a/integrations/omnidreams/tests/interactive_drive/test_manifest.py +++ b/integrations/omnidreams/tests/interactive_drive/test_manifest.py @@ -30,6 +30,39 @@ def test_loads_defaults(self) -> None: self.assertEqual(manifest.native_dit_acceleration, "disabled") self.assertEqual(manifest.native_vae_encoder, "disabled") self.assertIsNone(manifest.native_vae_fp8_state_path) + self.assertFalse(manifest.upsampling_enabled) + self.assertEqual(manifest.upsampling_server, "127.0.0.1:8090") + self.assertEqual(manifest.upsampling_scale, 4) + + def test_loads_upsampling_knobs(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + path = Path(tmpdir) / "manifest.yaml" + path.write_text( + textwrap.dedent( + """ + upsampling_enabled: true + upsampling_server: uplift.example:8090 + upsampling_scale: 2 + upsampling_sparse_ratio: 1.5 + upsampling_input_format: raw + upsampling_input_jpeg_quality: 85 + upsampling_return_frames: true + upsampling_max_queue_chunks: 2 + upsampling_max_message_mb: 256 + """ + ).strip(), + encoding="utf-8", + ) + manifest = load_world_model_manifest(path) + self.assertTrue(manifest.upsampling_enabled) + self.assertEqual(manifest.upsampling_server, "uplift.example:8090") + self.assertEqual(manifest.upsampling_scale, 2) + self.assertEqual(manifest.upsampling_sparse_ratio, 1.5) + self.assertEqual(manifest.upsampling_input_format, "raw") + self.assertEqual(manifest.upsampling_input_jpeg_quality, 85) + self.assertTrue(manifest.upsampling_return_frames) + self.assertEqual(manifest.upsampling_max_queue_chunks, 2) + self.assertEqual(manifest.upsampling_max_message_mb, 256) def test_loads_native_dit_knobs(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: diff --git a/integrations/omnidreams/tests/interactive_drive/test_world_model_adapter.py b/integrations/omnidreams/tests/interactive_drive/test_world_model_adapter.py index 3f618ff97..a4be6a728 100644 --- a/integrations/omnidreams/tests/interactive_drive/test_world_model_adapter.py +++ b/integrations/omnidreams/tests/interactive_drive/test_world_model_adapter.py @@ -70,6 +70,10 @@ def test_select_config_name_uses_omnidreams_recipe_slugs() -> None: _select_config_name(_manifest()) == "omnidreams-sv-2steps-chunk2-loc6-lightvae-lighttae" ) + assert ( + _select_config_name(replace(_manifest(), upsampling_enabled=True)) + == "omnidreams-sv-2steps-chunk2-loc6-lightvae-lighttae" + ) assert ( _select_config_name(replace(_manifest(), light_vae=False)) == "omnidreams-sv-2steps-chunk2-loc6-vae-vae" diff --git a/uv.lock b/uv.lock index 39e436ab2..0707c8858 100644 --- a/uv.lock +++ b/uv.lock @@ -1369,6 +1369,7 @@ source = { editable = "integrations/omnidreams" } dependencies = [ { name = "einops" }, { name = "flashdreams", extra = ["serving"] }, + { name = "flashdreams-flashvsr" }, { name = "grpcio" }, { name = "grpcio-tools" }, { name = "huggingface-hub" }, @@ -1407,6 +1408,7 @@ interactive-drive = [ requires-dist = [ { name = "einops", specifier = ">=0.8" }, { name = "flashdreams", extras = ["serving"], editable = "flashdreams" }, + { name = "flashdreams-flashvsr", editable = "integrations/flashvsr" }, { name = "flip-evaluator", marker = "extra == 'dev'", specifier = ">=1.7" }, { name = "grpcio", specifier = ">=1.50" }, { name = "grpcio-tools", specifier = ">=1.50" },