Buckets:
| """Data contracts for the training package: manifest entries + configs. | |
| Mirrors the discipline of :mod:`fpgm.types` and :mod:`fpgm.datagen.types`: every | |
| stage (manifest discovery, bundle decode, augmentation, dataset assembly) | |
| consumes/produces a dataclass defined here, exceptions are named and rooted in | |
| :class:`fpgm.types.FpgmError`, and shape/contract violations fail loudly at | |
| construction rather than three stages downstream. | |
| No heavy imports here (no torch/cv2/DiffSynth) -- this module must import | |
| cleanly on a machine with no GPU and no conda env active, since the manifest | |
| filter (which only reads ``sample.json``) is exercised by CI-style unit tests. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| from pathlib import Path | |
| from fpgm.types import FpgmError | |
| # --------------------------------------------------------------------------- # | |
| # Exceptions | |
| # --------------------------------------------------------------------------- # | |
| class TrainingError(FpgmError): | |
| """Base class for every error raised by :mod:`fpgm.training`.""" | |
| class BundleReadError(TrainingError): | |
| """A window bundle's files are missing, unreadable, or violate the shape | |
| contract (wrong resolution/frame count/dtype). | |
| Distinct from a gate *filter* decision (which is a policy choice recorded | |
| in ``dropped_by_reason``): this is raised for bundles that claim to have | |
| passed export but are actually broken on disk -- a bug, not a quality flag. | |
| """ | |
| class ManifestEmptyError(TrainingError): | |
| """The gate filter kept zero windows. | |
| Raised rather than silently constructing an empty dataset (which would | |
| make ``accelerate launch`` hang or crash confusingly many steps later) -- | |
| see the module docstring's "a silently shrunken training set is a bug we | |
| would not notice" concern in the owning plan. | |
| """ | |
| # --------------------------------------------------------------------------- # | |
| # Manifest | |
| # --------------------------------------------------------------------------- # | |
| #: Every gate key ``sample.json["gates_passed"]`` is currently known to carry | |
| #: (see ``fpgm.datagen.export_vace``'s ``sample.json`` writer). Kept as an | |
| #: explicit tuple rather than "whatever keys happen to be in the dict" so a | |
| #: gate silently renamed/dropped upstream fails the manifest scan loudly | |
| #: instead of just quietly no longer being checked. | |
| KNOWN_GATE_KEYS: tuple[str, ...] = ( | |
| "s2_robot_alignment", | |
| "s7_drawer_prismatic_gated", | |
| "s7_support_plane_fit", | |
| "resolution_contract_hw16", | |
| "window_frame_count_4n1", | |
| ) | |
| class WindowSample: | |
| """One ``vace/window_*`` export directory, resolved to absolute paths. | |
| Every path is validated to exist by :func:`fpgm.training.manifest.discover_windows` | |
| at scan time (a manifest entry pointing at a file that vanished between | |
| S8 export and training time must fail at manifest-build time, not mid-epoch). | |
| """ | |
| episode_uuid: str | |
| camera_serial: str | |
| window_dir: Path | |
| target_video: Path | |
| control_depth: Path | |
| control_seg: Path | |
| control_normal: Path | |
| fg_mask: Path | |
| ref_plate: Path | |
| caption_path: Path | |
| sample_json: Path | |
| resolution: tuple[int, int] # (width, height), from sample.json | |
| frame_range: tuple[int, int] # (video_frame_start, video_frame_end) | |
| gates_passed: dict[str, bool] | |
| overlaps_pose_gap: bool | |
| def name(self) -> str: | |
| return f"{self.episode_uuid}/{self.camera_serial}/{self.window_dir.name}" | |
| class GateFilterConfig: | |
| """Policy for which ``vace/window_*`` directories are trainable. | |
| Defaults are the strict, "don't teach the model a wrong association" | |
| reading of the owning plan: a window is kept only if every known gate in | |
| ``sample.json["gates_passed"]`` is ``True`` *and* it does not overlap a | |
| pose gap (``overlaps_pose_gap`` -- 2 of the 3 windows that exist as of | |
| this build have it, per the S6 brick-GAP defect recorded in | |
| ``fpgm.datagen.export_vace``'s own module docstring). | |
| ``allow_pose_gap_overlap`` and ``ignore_gate_failures`` exist for exactly | |
| one legitimate use in this codebase: the Job 4 smoke train, which needs | |
| to exercise the training loop's mechanics on all 3 currently-exported | |
| windows regardless of data quality (see ``scripts/train_wan_vace_lora.sh | |
| --smoke``). Every caller that flips them must say so out loud in an | |
| invocation log -- :func:`fpgm.training.manifest.discover_windows` does, | |
| unconditionally, so a permissive run can never look like a strict one in | |
| the logs. | |
| """ | |
| allow_pose_gap_overlap: bool = False | |
| #: Gate keys whose failure is tolerated. Empty by default: every gate in | |
| #: KNOWN_GATE_KEYS must pass. Not "ignore_gate_failures: bool" -- a named | |
| #: allowlist makes a partial relaxation ("skip a broken gate we know is | |
| #: too strict") visible in the config, instead of an unreviewable blanket | |
| #: bypass. | |
| ignored_gate_keys: tuple[str, ...] = () | |
| #: Unknown gate keys are only a problem if this is True -- during rapid | |
| #: iteration on the S8 exporter (owned by a sibling agent) a new gate key | |
| #: appearing is expected; a *missing* one of KNOWN_GATE_KEYS is not. | |
| require_known_gate_keys_only: bool = False | |
| class ManifestFilterReport: | |
| """What :func:`fpgm.training.manifest.discover_windows` dropped, and why. | |
| Logged in full at manifest-build time (never just a count) -- see the | |
| owning plan: "log how many samples were dropped and why -- a silently | |
| shrunken training set is a bug we would not notice." | |
| """ | |
| total_found: int | |
| kept: tuple[WindowSample, ...] | |
| dropped: tuple[tuple[str, str], ...] = field(default_factory=tuple) # (window name, reason) | |
| def n_kept(self) -> int: | |
| return len(self.kept) | |
| def n_dropped(self) -> int: | |
| return len(self.dropped) | |
| def reason_counts(self) -> dict[str, int]: | |
| counts: dict[str, int] = {} | |
| for _, reason in self.dropped: | |
| counts[reason] = counts.get(reason, 0) + 1 | |
| return counts | |
| # --------------------------------------------------------------------------- # | |
| # Bundle assembly (control-video compositing, reference letterboxing) | |
| # --------------------------------------------------------------------------- # | |
| class BundleAssemblyConfig: | |
| """How a :class:`WindowSample` becomes the tensor format | |
| ``diffsynth.pipelines.wan_video.WanVideoPipeline``'s VACE branch consumes. | |
| **Why a composited RGB control video, not three separate control | |
| inputs.** Read from the trainer's own code | |
| (``diffsynth/pipelines/wan_video.py``'s ``WanVideoUnit_VACE.process``, | |
| ``diffsynth/models/wan_video_vace.py``'s ``VaceWanModel``): the VACE | |
| branch's patch embedding (``vace_patch_embedding``, ``in_channels=96``) | |
| is fed exactly one ``vace_context`` tensor built from exactly one | |
| ``vace_video`` + one ``vace_video_mask`` + one ``vace_reference_image`` | |
| -- there is no multi-control-video collate path to plug three separate | |
| channels into. So ``control_depth``/``control_seg``/``control_normal`` | |
| must be composited into a single RGB video before it reaches the | |
| trainer, as the owning plan's fallback suggestion says: depth in R, | |
| normalised seg-id in G, normal-Z in B. | |
| **Why no ``vace_video_mask`` by default.** ``fpgm.datagen.export_vace``'s | |
| own module docstring records a measured, not assumed, finding: "this | |
| exporter never emits a training ``src_mask``" and "The real background | |
| must come from the reference image ..., never from a mask carve-out of | |
| ``src_video``" -- backed by the ablation in | |
| ``outputs/wan_ablation/comparison.md`` (variant D, no mask + ref image, | |
| LPIPS 0.428 vs variant C, mask + no ref image, LPIPS 0.714). Following | |
| that precedent here keeps train-time input construction consistent with | |
| the export design it is built on. ``include_fg_mask`` exists to make | |
| this a policy switch, not a silent omission, and to support a future | |
| ablation that re-tests it against *this* project's now-complete | |
| depth/seg/normal control (the cited numbers are from an earlier control | |
| version with no brick/drawer wired in -- see that file's own caveat). | |
| """ | |
| #: Composite channel order: R=depth, G=seg id (normalised), B=normal-Z. | |
| #: ``max_seg_id`` sets the normalisation divisor -- fpgm.config's | |
| #: SegmentationConfig.max_num_objects default (16) is reused here so the | |
| #: composite has headroom for every id this pipeline can ever emit, | |
| #: without recomputing a per-episode max (which would make the same | |
| #: physical id map to a different R-channel value in different windows). | |
| max_seg_id: int = 16 | |
| #: If True, also emit ``vace_video_mask`` from ``fg_mask.mp4``. Off by | |
| #: default -- see class docstring. | |
| include_fg_mask: bool = False | |
| #: Reference-image letterbox background (white, matching the established | |
| #: Wan/VACE inference-time convention -- see fpgm.datagen.export_vace's | |
| #: "Reference image: letterbox on a white canvas" contract clause). | |
| letterbox_fill: int = 255 | |
| class AugmentConfig: | |
| """Train<->inference-gap augmentations. Every knob individually toggleable. | |
| These exist because training conditioning is exporter-perfect (poses | |
| solved by PnP against the *same* video the model must reproduce, masks | |
| from the same render) while inference conditioning comes from a MuJoCo | |
| simulation whose pose estimate, mask, and per-channel availability will | |
| all be slightly different. Skipping these would let the LoRA overfit to | |
| exporter-perfect conditioning it will never see again at inference. | |
| """ | |
| enable_pose_noise: bool = True | |
| #: Foreground-region rigid jitter applied to the *composited control* | |
| #: only (never to ``target.mp4``, which is real ground truth and must | |
| #: stay untouched). See fpgm.training.augment.pose_noise's docstring for | |
| #: why this is a 2D affine approximation of a 3D pose perturbation, not a | |
| #: mesh re-render. | |
| pose_noise_translate_mm_std: float = 2.0 | |
| pose_noise_rotate_deg_std: float = 1.0 | |
| #: Nominal px-per-mm at the image plane used to convert the physical | |
| #: pose-noise spec above into a pixel-space affine jitter, since the | |
| #: Dataset only has the exported raster bundle, not per-sample camera | |
| #: intrinsics (those live in ``master/meta.json``, outside the ``vace/`` | |
| #: tier this package reads -- see module docstring on why we don't reach | |
| #: into ``master/``). Derived from a 60 deg horizontal FOV pinhole at | |
| #: this export's 832 px width and a ~0.6 m nominal DROID desk working | |
| #: distance: focal_px = 832 / (2*tan(30 deg)) ~= 720 px; | |
| #: px_per_mm = focal_px / 600 mm ~= 1.2. Documented as an approximation: | |
| #: the augmentation's job is to blur exact-pixel-alignment reliance, not | |
| #: to simulate a physically exact reprojection. | |
| pose_noise_px_per_mm: float = 1.2 | |
| enable_mask_dilate_erode: bool = True | |
| #: Max structuring-element half-width (px) for the random dilate/erode of | |
| #: the foreground region (fg_mask.mp4), applied identically to every | |
| #: frame in a window (one random draw per __getitem__, not per frame -- | |
| #: a per-frame-random kernel would flicker in a way no real mask-quality | |
| #: variation looks like). | |
| mask_dilate_erode_max_px: int = 5 | |
| enable_channel_dropout: bool = True | |
| #: Each of the 3 composited channels (depth/seg/normal) is independently | |
| #: zeroed with this probability, for the whole window (not per frame). | |
| #: 0.10-0.20 per the owning plan; 0.15 is the midpoint. | |
| channel_dropout_prob: float = 0.15 | |
| #: Base seed for the per-sample RNG (mixed with the dataset index so | |
| #: different windows/epochs draw independent augmentations while a given | |
| #: (seed, index) pair stays reproducible for debugging). | |
| seed: int = 0 | |
Xet Storage Details
- Size:
- 12 kB
- Xet hash:
- 32148cc664871a82010356dc5f67c29c47f0a89fd8fb946d987454218dc1c11c
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.