BoxOfColors Claude Opus 4.7 (1M context) commited on
Commit
ed7d6c7
·
1 Parent(s): cd42852

refactor: round 2 — add CropRegion edge/box properties, externalise CSS

Browse files

pipeline/crop.py
- Add CropRegion.frame_x2 / frame_y2 (exclusive right/bottom edges) and
pil_box (4-tuple for PIL.Image.crop()). The pattern
``(cr.frame_x, cr.frame_y, cr.frame_x + cr.frame_w, cr.frame_y + cr.frame_h)``
was reconstructed in 5+ places; ``cr.pil_box`` is now the single source.
Numpy slices like ``arr[cr.frame_y:cr.frame_y2, cr.frame_x:cr.frame_x2]``
read more cleanly than the previous ``+ cr.frame_h`` arithmetic.

Sites updated to use the new properties:
- pipeline/composite.py: composite_with_alpha numpy slicing (also drops
a redundant trailing .astype(np.uint8) on the already-uint8 result)
- pipeline/lama.py: _load_crop
- pipeline/vace.py: _load_chunk_crops
- pipeline/crop.py: build_inpaint_mask source-mask cropping
- app.py: _create_crop_preview rectangle + mask overlay

static/style.css (new file)
- The 100-line CSS string previously embedded in app.py now lives in its
own file so it gets proper editor syntax highlighting and shows up as
a separate diff in PR reviews. Loaded at module import via
``Path(__file__).parent / "static/style.css"``.

Net: app.py 792 → 688 (-104 lines), other files net -5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (6) hide show
  1. app.py +4 -108
  2. pipeline/composite.py +6 -11
  3. pipeline/crop.py +18 -5
  4. pipeline/lama.py +1 -9
  5. pipeline/vace.py +1 -6
  6. static/style.css +100 -0
app.py CHANGED
@@ -76,110 +76,9 @@ ALL_MODES = (MODE_FAST, MODE_QUALITY)
76
  prewarm_vace_cache()
77
 
78
  # ---------------------------------------------------------------------------
79
- # CSS — dark premium theme
80
  # ---------------------------------------------------------------------------
81
- CSS = """
82
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
83
-
84
- * { box-sizing: border-box; }
85
-
86
- body, .gradio-container {
87
- font-family: 'Inter', sans-serif !important;
88
- background: #0d1117 !important;
89
- color: #e6edf3 !important;
90
- }
91
-
92
- .gradio-container {
93
- max-width: 1200px !important;
94
- margin: 0 auto !important;
95
- padding: 24px !important;
96
- }
97
-
98
- /* Header */
99
- .header-block {
100
- text-align: center;
101
- padding: 32px 0 24px;
102
- }
103
- .header-block h1 {
104
- font-size: 2rem;
105
- font-weight: 700;
106
- background: linear-gradient(135deg, #2dd4bf, #06b6d4);
107
- -webkit-background-clip: text;
108
- -webkit-text-fill-color: transparent;
109
- margin-bottom: 8px;
110
- }
111
- .header-block p {
112
- color: #8b949e;
113
- font-size: 0.95rem;
114
- }
115
-
116
- /* Cards */
117
- .card {
118
- background: #161b22;
119
- border: 1px solid #30363d;
120
- border-radius: 12px;
121
- padding: 20px;
122
- margin-bottom: 16px;
123
- }
124
- .card-title {
125
- font-size: 0.8rem;
126
- font-weight: 600;
127
- text-transform: uppercase;
128
- letter-spacing: 0.08em;
129
- color: #8b949e;
130
- margin-bottom: 12px;
131
- }
132
-
133
- /* Buttons */
134
- .btn-primary {
135
- background: linear-gradient(135deg, #0f766e, #0e7490) !important;
136
- border: none !important;
137
- color: white !important;
138
- font-weight: 600 !important;
139
- border-radius: 8px !important;
140
- padding: 10px 24px !important;
141
- transition: opacity 0.2s !important;
142
- }
143
- .btn-primary:hover { opacity: 0.85 !important; }
144
-
145
- .btn-secondary {
146
- background: #21262d !important;
147
- border: 1px solid #30363d !important;
148
- color: #e6edf3 !important;
149
- font-weight: 500 !important;
150
- border-radius: 8px !important;
151
- transition: border-color 0.2s !important;
152
- }
153
- .btn-secondary:hover { border-color: #2dd4bf !important; }
154
-
155
- /* Status */
156
- .status-box textarea {
157
- background: #0d1117 !important;
158
- border: 1px solid #30363d !important;
159
- color: #8b949e !important;
160
- font-family: 'JetBrains Mono', monospace !important;
161
- font-size: 0.82rem !important;
162
- border-radius: 8px !important;
163
- }
164
-
165
- /* Mode radio */
166
- .mode-radio label { font-weight: 500 !important; }
167
-
168
- /* Steps badge */
169
- .step-badge {
170
- display: inline-flex;
171
- align-items: center;
172
- justify-content: center;
173
- width: 24px;
174
- height: 24px;
175
- border-radius: 50%;
176
- background: #0f766e;
177
- color: white;
178
- font-size: 0.75rem;
179
- font-weight: 700;
180
- margin-right: 8px;
181
- }
182
- """
183
 
184
  # ---------------------------------------------------------------------------
185
  # Helpers
@@ -261,7 +160,7 @@ def _create_crop_preview(
261
 
262
  # Semi-transparent teal fill for crop region
263
  draw.rectangle(
264
- [cr.frame_x, cr.frame_y, cr.frame_x + cr.frame_w, cr.frame_y + cr.frame_h],
265
  fill=(13, 148, 136, 40),
266
  outline=(45, 212, 191, 200),
267
  width=2,
@@ -270,10 +169,7 @@ def _create_crop_preview(
270
  # Inpaint mask overlay (red, semi-transparent)
271
  if inpaint_mask is not None:
272
  mask_full = np.zeros((img.height, img.width), dtype=np.uint8)
273
- mask_full[
274
- cr.frame_y : cr.frame_y + cr.frame_h,
275
- cr.frame_x : cr.frame_x + cr.frame_w,
276
- ] = inpaint_mask
277
  rgba = np.zeros((img.height, img.width, 4), dtype=np.uint8)
278
  rgba[mask_full > 0] = (239, 68, 68, 140)
279
  overlay = Image.alpha_composite(overlay, Image.fromarray(rgba, mode="RGBA"))
 
76
  prewarm_vace_cache()
77
 
78
  # ---------------------------------------------------------------------------
79
+ # CSS — dark theme; lives in static/style.css for syntax highlighting & diffability
80
  # ---------------------------------------------------------------------------
81
+ CSS = (Path(__file__).resolve().parent / "static" / "style.css").read_text()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
  # ---------------------------------------------------------------------------
84
  # Helpers
 
160
 
161
  # Semi-transparent teal fill for crop region
162
  draw.rectangle(
163
+ list(cr.pil_box),
164
  fill=(13, 148, 136, 40),
165
  outline=(45, 212, 191, 200),
166
  width=2,
 
169
  # Inpaint mask overlay (red, semi-transparent)
170
  if inpaint_mask is not None:
171
  mask_full = np.zeros((img.height, img.width), dtype=np.uint8)
172
+ mask_full[cr.frame_y : cr.frame_y2, cr.frame_x : cr.frame_x2] = inpaint_mask
 
 
 
173
  rgba = np.zeros((img.height, img.width, 4), dtype=np.uint8)
174
  rgba[mask_full > 0] = (239, 68, 68, 140)
175
  overlay = Image.alpha_composite(overlay, Image.fromarray(rgba, mode="RGBA"))
pipeline/composite.py CHANGED
@@ -108,19 +108,14 @@ def composite_with_alpha(
108
 
109
  inpainted_crop = _resize_crop_if_needed(inpainted_crop, cr)
110
 
111
- original_crop = result[
112
- cr.frame_y : cr.frame_y + cr.frame_h,
113
- cr.frame_x : cr.frame_x + cr.frame_w,
114
- ].astype(np.float32)
115
-
116
  alpha_3 = alpha[:, :, np.newaxis] # broadcast over RGB channels
117
- blended = alpha_3 * inpainted_crop.astype(np.float32) + (1.0 - alpha_3) * original_crop
118
- result[
119
- cr.frame_y : cr.frame_y + cr.frame_h,
120
- cr.frame_x : cr.frame_x + cr.frame_w,
121
- ] = np.clip(blended, 0, 255).astype(np.uint8)
122
 
123
- return result.astype(np.uint8)
124
 
125
 
126
  # ---------------------------------------------------------------------------
 
108
 
109
  inpainted_crop = _resize_crop_if_needed(inpainted_crop, cr)
110
 
111
+ region = result[cr.frame_y : cr.frame_y2, cr.frame_x : cr.frame_x2]
 
 
 
 
112
  alpha_3 = alpha[:, :, np.newaxis] # broadcast over RGB channels
113
+ blended = alpha_3 * inpainted_crop.astype(np.float32) + (1.0 - alpha_3) * region.astype(np.float32)
114
+ result[cr.frame_y : cr.frame_y2, cr.frame_x : cr.frame_x2] = (
115
+ np.clip(blended, 0, 255).astype(np.uint8)
116
+ )
 
117
 
118
+ return result
119
 
120
 
121
  # ---------------------------------------------------------------------------
pipeline/crop.py CHANGED
@@ -163,6 +163,22 @@ class CropRegion:
163
  target_h: int
164
  mask_bbox: BBox # in crop-local pixel space
165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  def __repr__(self) -> str:
167
  return (
168
  f"CropRegion(frame=({self.frame_x},{self.frame_y},"
@@ -430,11 +446,8 @@ def build_inpaint_mask(
430
  if source_mask.ndim == 3:
431
  source_mask = source_mask.max(axis=2)
432
  # Crop to the crop region
433
- y1 = crop_region.frame_y
434
- y2 = crop_region.frame_y + crop_region.frame_h
435
- x1 = crop_region.frame_x
436
- x2 = crop_region.frame_x + crop_region.frame_w
437
- cropped = source_mask[y1:y2, x1:x2]
438
  # Binarise: any non-zero value (including raw Gradio layer values
439
  # like 200 that are not exactly 255) becomes 255.
440
  binarised = (cropped > 0).astype(np.uint8) * 255
 
163
  target_h: int
164
  mask_bbox: BBox # in crop-local pixel space
165
 
166
+ # Right/bottom edge as exclusive coordinates, mirroring numpy slicing
167
+ # semantics. Use these to keep ``frame[y:cr.frame_y2, x:cr.frame_x2]``
168
+ # readable instead of ``frame[y:y+cr.frame_h, x:x+cr.frame_w]``.
169
+ @property
170
+ def frame_x2(self) -> int:
171
+ return self.frame_x + self.frame_w
172
+
173
+ @property
174
+ def frame_y2(self) -> int:
175
+ return self.frame_y + self.frame_h
176
+
177
+ @property
178
+ def pil_box(self) -> Tuple[int, int, int, int]:
179
+ """(left, upper, right, lower) — 4-tuple expected by PIL.Image.crop()."""
180
+ return (self.frame_x, self.frame_y, self.frame_x2, self.frame_y2)
181
+
182
  def __repr__(self) -> str:
183
  return (
184
  f"CropRegion(frame=({self.frame_x},{self.frame_y},"
 
446
  if source_mask.ndim == 3:
447
  source_mask = source_mask.max(axis=2)
448
  # Crop to the crop region
449
+ cr = crop_region
450
+ cropped = source_mask[cr.frame_y : cr.frame_y2, cr.frame_x : cr.frame_x2]
 
 
 
451
  # Binarise: any non-zero value (including raw Gradio layer values
452
  # like 200 that are not exactly 255) becomes 255.
453
  binarised = (cropped > 0).astype(np.uint8) * 255
pipeline/lama.py CHANGED
@@ -165,15 +165,7 @@ def inpaint_frames_lama(
165
  def _load_crop(frame_path: Path, crop_region: CropRegion) -> np.ndarray:
166
  """Load a frame and return only the crop region (RGB uint8)."""
167
  img = Image.open(frame_path).convert("RGB")
168
- cr = crop_region
169
- # PIL box is (left, upper, right, lower) — exclusive right/lower
170
- box = (
171
- cr.frame_x,
172
- cr.frame_y,
173
- cr.frame_x + cr.frame_w,
174
- cr.frame_y + cr.frame_h,
175
- )
176
- return np.array(img.crop(box))
177
 
178
 
179
  def _mask_to_pil(mask: np.ndarray) -> Image.Image:
 
165
  def _load_crop(frame_path: Path, crop_region: CropRegion) -> np.ndarray:
166
  """Load a frame and return only the crop region (RGB uint8)."""
167
  img = Image.open(frame_path).convert("RGB")
168
+ return np.array(img.crop(crop_region.pil_box))
 
 
 
 
 
 
 
 
169
 
170
 
171
  def _mask_to_pil(mask: np.ndarray) -> Image.Image:
pipeline/vace.py CHANGED
@@ -270,12 +270,7 @@ def _load_chunk_crops(
270
  end: int,
271
  ) -> List[Image.Image]:
272
  """Load crops for [start, end) at VACE target resolution."""
273
- box = (
274
- crop_region.frame_x,
275
- crop_region.frame_y,
276
- crop_region.frame_x + crop_region.frame_w,
277
- crop_region.frame_y + crop_region.frame_h,
278
- )
279
  crops: List[Image.Image] = []
280
  for fp in frame_paths[start:end]:
281
  img = Image.open(fp).convert("RGB")
 
270
  end: int,
271
  ) -> List[Image.Image]:
272
  """Load crops for [start, end) at VACE target resolution."""
273
+ box = crop_region.pil_box
 
 
 
 
 
274
  crops: List[Image.Image] = []
275
  for fp in frame_paths[start:end]:
276
  img = Image.open(fp).convert("RGB")
static/style.css ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
2
+
3
+ * { box-sizing: border-box; }
4
+
5
+ body, .gradio-container {
6
+ font-family: 'Inter', sans-serif !important;
7
+ background: #0d1117 !important;
8
+ color: #e6edf3 !important;
9
+ }
10
+
11
+ .gradio-container {
12
+ max-width: 1200px !important;
13
+ margin: 0 auto !important;
14
+ padding: 24px !important;
15
+ }
16
+
17
+ /* Header */
18
+ .header-block {
19
+ text-align: center;
20
+ padding: 32px 0 24px;
21
+ }
22
+ .header-block h1 {
23
+ font-size: 2rem;
24
+ font-weight: 700;
25
+ background: linear-gradient(135deg, #2dd4bf, #06b6d4);
26
+ -webkit-background-clip: text;
27
+ -webkit-text-fill-color: transparent;
28
+ margin-bottom: 8px;
29
+ }
30
+ .header-block p {
31
+ color: #8b949e;
32
+ font-size: 0.95rem;
33
+ }
34
+
35
+ /* Cards */
36
+ .card {
37
+ background: #161b22;
38
+ border: 1px solid #30363d;
39
+ border-radius: 12px;
40
+ padding: 20px;
41
+ margin-bottom: 16px;
42
+ }
43
+ .card-title {
44
+ font-size: 0.8rem;
45
+ font-weight: 600;
46
+ text-transform: uppercase;
47
+ letter-spacing: 0.08em;
48
+ color: #8b949e;
49
+ margin-bottom: 12px;
50
+ }
51
+
52
+ /* Buttons */
53
+ .btn-primary {
54
+ background: linear-gradient(135deg, #0f766e, #0e7490) !important;
55
+ border: none !important;
56
+ color: white !important;
57
+ font-weight: 600 !important;
58
+ border-radius: 8px !important;
59
+ padding: 10px 24px !important;
60
+ transition: opacity 0.2s !important;
61
+ }
62
+ .btn-primary:hover { opacity: 0.85 !important; }
63
+
64
+ .btn-secondary {
65
+ background: #21262d !important;
66
+ border: 1px solid #30363d !important;
67
+ color: #e6edf3 !important;
68
+ font-weight: 500 !important;
69
+ border-radius: 8px !important;
70
+ transition: border-color 0.2s !important;
71
+ }
72
+ .btn-secondary:hover { border-color: #2dd4bf !important; }
73
+
74
+ /* Status */
75
+ .status-box textarea {
76
+ background: #0d1117 !important;
77
+ border: 1px solid #30363d !important;
78
+ color: #8b949e !important;
79
+ font-family: 'JetBrains Mono', monospace !important;
80
+ font-size: 0.82rem !important;
81
+ border-radius: 8px !important;
82
+ }
83
+
84
+ /* Mode radio */
85
+ .mode-radio label { font-weight: 500 !important; }
86
+
87
+ /* Steps badge */
88
+ .step-badge {
89
+ display: inline-flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ width: 24px;
93
+ height: 24px;
94
+ border-radius: 50%;
95
+ background: #0f766e;
96
+ color: white;
97
+ font-size: 0.75rem;
98
+ font-weight: 700;
99
+ margin-right: 8px;
100
+ }