lovarch-cli 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. lovarch_cli/__init__.py +16 -0
  2. lovarch_cli/__main__.py +10 -0
  3. lovarch_cli/ai/__init__.py +21 -0
  4. lovarch_cli/ai/gateway.py +240 -0
  5. lovarch_cli/api.py +111 -0
  6. lovarch_cli/auth/__init__.py +32 -0
  7. lovarch_cli/auth/keyring_store.py +214 -0
  8. lovarch_cli/auth/local_server.py +165 -0
  9. lovarch_cli/auth/pkce.py +57 -0
  10. lovarch_cli/auth/session.py +189 -0
  11. lovarch_cli/cli.py +262 -0
  12. lovarch_cli/clients/__init__.py +33 -0
  13. lovarch_cli/clients/factory.py +54 -0
  14. lovarch_cli/clients/local_client.py +432 -0
  15. lovarch_cli/clients/lovarch_storage.py +174 -0
  16. lovarch_cli/clients/lovarch_supabase.py +295 -0
  17. lovarch_cli/clients/persistence.py +166 -0
  18. lovarch_cli/clients/storage.py +66 -0
  19. lovarch_cli/commands/__init__.py +10 -0
  20. lovarch_cli/commands/account.py +172 -0
  21. lovarch_cli/commands/audit.py +394 -0
  22. lovarch_cli/commands/config_cmd.py +80 -0
  23. lovarch_cli/commands/consolidate.py +217 -0
  24. lovarch_cli/commands/context_cmd.py +73 -0
  25. lovarch_cli/commands/dev.py +287 -0
  26. lovarch_cli/commands/do_cmd.py +120 -0
  27. lovarch_cli/commands/init.py +218 -0
  28. lovarch_cli/commands/jobs_cmd.py +95 -0
  29. lovarch_cli/commands/login.py +202 -0
  30. lovarch_cli/commands/mcp_cmd.py +26 -0
  31. lovarch_cli/commands/run.py +375 -0
  32. lovarch_cli/commands/signup.py +185 -0
  33. lovarch_cli/commands/status.py +243 -0
  34. lovarch_cli/commands/upgrade.py +108 -0
  35. lovarch_cli/commands/verifica_cmd.py +174 -0
  36. lovarch_cli/config.py +101 -0
  37. lovarch_cli/config_store.py +111 -0
  38. lovarch_cli/credits/__init__.py +35 -0
  39. lovarch_cli/credits/base.py +84 -0
  40. lovarch_cli/credits/factory.py +36 -0
  41. lovarch_cli/credits/local.py +34 -0
  42. lovarch_cli/credits/lovarch.py +56 -0
  43. lovarch_cli/i18n/__init__.py +27 -0
  44. lovarch_cli/i18n/loader.py +121 -0
  45. lovarch_cli/i18n/translations/en.json +168 -0
  46. lovarch_cli/i18n/translations/es.json +168 -0
  47. lovarch_cli/i18n/translations/it.json +168 -0
  48. lovarch_cli/i18n/translations/pt.json +168 -0
  49. lovarch_cli/mcp/__init__.py +9 -0
  50. lovarch_cli/mcp/server.py +199 -0
  51. lovarch_cli/mcp/tools.py +372 -0
  52. lovarch_cli/sample_downloader.py +255 -0
  53. lovarch_cli/squad/README.md +206 -0
  54. lovarch_cli/squad/agents/auditor-input.md +353 -0
  55. lovarch_cli/squad/agents/bim-engineer.md +404 -0
  56. lovarch_cli/squad/agents/briefing-architect.md +249 -0
  57. lovarch_cli/squad/agents/cad-engineer.md +278 -0
  58. lovarch_cli/squad/agents/capitolato-writer.md +256 -0
  59. lovarch_cli/squad/agents/computo-engineer.md +258 -0
  60. lovarch_cli/squad/agents/concept-designer.md +399 -0
  61. lovarch_cli/squad/agents/contratto-architect.md +243 -0
  62. lovarch_cli/squad/agents/deliverable-builder.md +253 -0
  63. lovarch_cli/squad/agents/energy-prelim.md +388 -0
  64. lovarch_cli/squad/agents/pratiche-it.md +251 -0
  65. lovarch_cli/squad/agents/progetto-chief.md +768 -0
  66. lovarch_cli/squad/agents/quality-dati.md +409 -0
  67. lovarch_cli/squad/agents/quality-misure.md +418 -0
  68. lovarch_cli/squad/agents/quality-normativa.md +417 -0
  69. lovarch_cli/squad/agents/quality-output.md +436 -0
  70. lovarch_cli/squad/agents/regolatorio-it.md +278 -0
  71. lovarch_cli/squad/checklists/handoff-quality-gate.md +232 -0
  72. lovarch_cli/squad/checklists/quality-dati-checklist.md +134 -0
  73. lovarch_cli/squad/checklists/quality-misure-checklist.md +139 -0
  74. lovarch_cli/squad/checklists/quality-normativa-checklist.md +121 -0
  75. lovarch_cli/squad/checklists/quality-output-checklist.md +116 -0
  76. lovarch_cli/squad/config.yaml +408 -0
  77. lovarch_cli/squad/data/CHANGELOG.md +272 -0
  78. lovarch_cli/squad/data/agents-prd.md +428 -0
  79. lovarch_cli/squad/data/architettura-progetto-rules.md +328 -0
  80. lovarch_cli/squad/data/handoff-card-template.md +231 -0
  81. lovarch_cli/squad/data/mocks/catasto-visura.json +72 -0
  82. lovarch_cli/squad/data/mocks/firma-envelope.json +43 -0
  83. lovarch_cli/squad/data/prezzario-lombardia-sample.json +312 -0
  84. lovarch_cli/squad/scripts/api_clients.py +206 -0
  85. lovarch_cli/squad/scripts/architect_profile.py +276 -0
  86. lovarch_cli/squad/scripts/deliverable_generators.py +844 -0
  87. lovarch_cli/squad/scripts/generate_attico_brera_dwg.py +369 -0
  88. lovarch_cli/squad/scripts/generate_chianti_dxf.py +368 -0
  89. lovarch_cli/squad/scripts/generate_chianti_images.py +223 -0
  90. lovarch_cli/squad/scripts/generate_real_sample_images.py +189 -0
  91. lovarch_cli/squad/scripts/generate_sample_assets.py +382 -0
  92. lovarch_cli/squad/scripts/lovarch_client.py +1046 -0
  93. lovarch_cli/squad/scripts/pipeline_runner.py +2095 -0
  94. lovarch_cli/squad/scripts/render_dxf_to_png.py +57 -0
  95. lovarch_cli/squad/scripts/run_palestra_demo.sh +277 -0
  96. lovarch_cli/squad/scripts/simulate_squad_execution.py +515 -0
  97. lovarch_cli/squad/scripts/validate-squad.py +383 -0
  98. lovarch_cli/squad/tasks/audit-input.md +146 -0
  99. lovarch_cli/squad/tasks/compute-metric.md +105 -0
  100. lovarch_cli/squad/tasks/consolidate-dossier.md +187 -0
  101. lovarch_cli/squad/tasks/generate-cad-plan.md +120 -0
  102. lovarch_cli/squad/tasks/generate-ifc-model.md +108 -0
  103. lovarch_cli/squad/tasks/write-capitolato.md +100 -0
  104. lovarch_cli/squad/templates/asseverazione-tecnica.md +126 -0
  105. lovarch_cli/squad/templates/capitolato-uni-11337.md +235 -0
  106. lovarch_cli/squad/templates/cila-comune-milano.md +177 -0
  107. lovarch_cli/squad/templates/contratto-cnappc.md +220 -0
  108. lovarch_cli/squad/workflows/dal-brief-al-cantiere.yaml +218 -0
  109. lovarch_cli/squad_loader.py +114 -0
  110. lovarch_cli/verify/__init__.py +15 -0
  111. lovarch_cli/verify/contratto.py +110 -0
  112. lovarch_cli/verify/dossier.py +97 -0
  113. lovarch_cli/verify/misure.py +83 -0
  114. lovarch_cli/verify/normativa.py +178 -0
  115. lovarch_cli/version.py +13 -0
  116. lovarch_cli/workflows/__init__.py +9 -0
  117. lovarch_cli/workflows/platform.py +212 -0
  118. lovarch_cli-0.2.1.dist-info/METADATA +232 -0
  119. lovarch_cli-0.2.1.dist-info/RECORD +122 -0
  120. lovarch_cli-0.2.1.dist-info/WHEEL +4 -0
  121. lovarch_cli-0.2.1.dist-info/entry_points.txt +3 -0
  122. lovarch_cli-0.2.1.dist-info/licenses/LICENSE +38 -0
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env python3
2
+ """Generate photorealistic sample images for Attico Brera demo.
3
+
4
+ Replaces PIL placeholders with real AI-generated photos:
5
+ - 6 stato attuale (1980s mediocre renovation, dated aesthetic)
6
+ - 6 reference style photos (wabi-sabi · neoclassico contemporaneo · italian residential)
7
+
8
+ Uses OpenAI gpt-image-1 via direct API.
9
+ """
10
+
11
+ import os
12
+ import sys
13
+ import base64
14
+ from pathlib import Path
15
+ from openai import OpenAI
16
+
17
+ ROOT = Path(__file__).resolve().parents[1]
18
+ FOTO_DIR = ROOT / "data" / "sample-input" / "foto"
19
+ PIN_DIR = ROOT / "data" / "sample-input" / "pinterest-references"
20
+
21
+ client = OpenAI()
22
+
23
+ STATO_ATTUALE_PROMPTS = {
24
+ "01-facciata.jpg": (
25
+ "Photorealistic exterior photo of a 1910 historic Milanese palazzo facade in Brera district, "
26
+ "Via Fiori Chiari Milan. Italian neo-Renaissance style, ochre plaster walls with white stone "
27
+ "framing around windows, original wrought iron balcony railings, weathered wooden shutters in "
28
+ "warm green-grey, ground floor with arched portico. Real architectural photography, golden hour "
29
+ "afternoon light, narrow cobblestone street perspective, slight wear and patina showing 100+ "
30
+ "years of history. Camera: 35mm full-frame, f/8, eye-level shot. Documentary realism, "
31
+ "no people, no cars."
32
+ ),
33
+ "02-ingresso.jpg": (
34
+ "Photorealistic interior photo of a long narrow Italian apartment entrance corridor from a "
35
+ "1980s mediocre renovation. Cold cream-white walls, dated polished marble travertine floor "
36
+ "with grey veining, single hanging pendant lamp from the 80s with frosted glass, brown wooden "
37
+ "doors with metal handles, narrow corridor about 1.2m wide and 5m long with three closed "
38
+ "doors visible. Some small wear marks on walls, electrical outlets visible, dated white plastic "
39
+ "switches. Slightly cluttered, real-estate photography style, neutral indoor lighting, "
40
+ "documentary not aspirational. Camera: 24mm wide, f/5.6."
41
+ ),
42
+ "03-soggiorno.jpg": (
43
+ "Photorealistic interior of a Milanese living room in a 1910 building with 1980s renovation. "
44
+ "Original Venetian seminato terrazzo flooring with warm earth tones (terracotta, cream, ochre), "
45
+ "preserved decorated coffered ceiling with subtle stucco details typical of early 1900s "
46
+ "Italian palazzo. Dated 1980s elements: a beige tiled wall behind a dark wood unit with TV "
47
+ "from the 90s, brown velvet sofa, ugly aluminum-frame split AC unit mounted high on the wall. "
48
+ "Tall French window leading to a terrace with old aluminum frame. Warm afternoon natural light. "
49
+ "Real estate photography, 24mm lens, slight clutter, lived-in but dated, NOT staged. "
50
+ "Documentary realism."
51
+ ),
52
+ "04-cucina.jpg": (
53
+ "Photorealistic interior of a narrow Italian kitchen from a 1980s renovation, only about 2.5m "
54
+ "wide. Cheap brown laminate cabinets with brass handles, dated beige ceramic tile backsplash "
55
+ "with floral motifs, white melamine countertop with rounded edges, old gas stove with 4 burners, "
56
+ "small white domestic refrigerator from 90s on the right, single small window with frosted "
57
+ "glass at the end. Beige terracotta floor tiles. Yellowish fluorescent ceiling light. "
58
+ "Cramped, functional, dated, real-estate photo, not aspirational. 24mm, neutral light."
59
+ ),
60
+ "05-camera-padronale.jpg": (
61
+ "Photorealistic Italian master bedroom about 12 sqm, 1910 building 1980s renovation. "
62
+ "Preserved original decorated stucco ceiling rosette in cream and pale grey, but room itself "
63
+ "is small and dated. Beige walls, aged parquet wood floor with darker tones near edges, "
64
+ "a wardrobe with mirrored doors from the 80s, plain double bed with simple cream linens, "
65
+ "outdated wall sconces, single window with white aluminum frame. Modest split AC visible. "
66
+ "Awkward proportions making it feel cramped. Real-estate photo, soft window daylight, "
67
+ "no staging, dated, documentary."
68
+ ),
69
+ "06-bagno.jpg": (
70
+ "Photorealistic interior of a small windowless Italian bathroom (bagno cieco) from 1980s "
71
+ "renovation. Mottled beige and pink ceramic tiles on walls floor to ceiling, ugly geometric "
72
+ "patterns, white porcelain sink with chrome faucet from the 80s, plain white toilet and bidet, "
73
+ "small old shower stall in the corner with frosted plastic doors and limescale stains, "
74
+ "dated white framed mirror with fluorescent light strip above, ventilation grate on ceiling. "
75
+ "Cramped 5 sqm, beige tile floor matching walls. Yellowish artificial light, no natural light, "
76
+ "claustrophobic feel. Real-estate photo, dated, urgent renovation needed, documentary realism."
77
+ ),
78
+ }
79
+
80
+ PINTEREST_REFS_PROMPTS = {
81
+ "pin-01-de-cotiis.jpg": (
82
+ "Photorealistic interior of an exclusive Milanese palazzo apartment, modernist contemporary "
83
+ "intervention inside a 1900s historic shell. Polished cement floor with subtle veining, "
84
+ "original ornate stucco ceiling preserved and slightly aged, warm white walls with patina "
85
+ "treatment, large abstract artwork, brutalist concrete coffee table with smooth bronze sculpture, "
86
+ "low-profile sofa in raw natural linen pale cream, antique brass floor lamp. Atmosphere is "
87
+ "monastic and refined, dialog between old and new, material honesty. Soft north light from "
88
+ "tall window. Editorial interior photography, 35mm, f/4, wide composition. No clutter."
89
+ ),
90
+ "pin-02-studiopepe.jpg": (
91
+ "Photorealistic Italian residential living room with sophisticated warm earth tones palette: "
92
+ "terracotta, ochre, dusty rose, sage green. Hand-painted decorative wall in geometric pattern "
93
+ "with matte finish, herringbone parquet oak floor, curved velvet sofa in dusty rose, round "
94
+ "marble coffee table in beige travertine, sculptural ceramic vases, brass arc lamp, vintage "
95
+ "Italian armchair in caramel leather. Layered textiles, warm afternoon light through tall "
96
+ "window with linen curtains. Style: contemporary Italian eclectic, female-driven aesthetic, "
97
+ "bold but refined. Editorial photography 50mm f/2.8."
98
+ ),
99
+ "pin-03-marcante-testa.jpg": (
100
+ "Photorealistic Italian apartment in a Turin historic building, contemporary renovation that "
101
+ "celebrates restored decorated ceilings with bold pastel colors. Pale pink walls, restored "
102
+ "ornate ceiling rosette in cream and gold, polished oak parquet, mid-century modern furniture: "
103
+ "saffron yellow Cassina-style armchair, blue velvet ottoman, glass and brass coffee table, "
104
+ "abstract artwork, plenty of natural light. Bright cheerful but sophisticated. Italian eclectic "
105
+ "warm contemporary. Editorial interior photography, 35mm, daylight."
106
+ ),
107
+ "pin-04-vilon.jpg": (
108
+ "Photorealistic Roman boutique hotel suite interior with refined neoclassic-contemporary mix. "
109
+ "Original Venetian seminato terrazzo floor in cream-pink-grey tones, restored decorated "
110
+ "ceiling with classical motifs, warm cream walls, contemporary canopy bed in pale linen, "
111
+ "gold-framed mirror, fluted brass wall sconces, plump linen armchair, brass nightstand with "
112
+ "marble top, tall French windows with sheer curtains, vintage Persian rug in muted tones. "
113
+ "Cozy and luxurious, masculine-feminine balance. Editorial photo, golden hour, 35mm f/4."
114
+ ),
115
+ "pin-05-cassina.jpg": (
116
+ "Photorealistic upscale Italian living room featuring iconic mid-century Italian design. "
117
+ "Cream Maralunga-style modular sofa with curved soft cushions, Cab black leather chairs around "
118
+ "an oval glass dining table, Castiglioni Arco-style floor lamp with marble base and brass arc, "
119
+ "warm parquet oak floor, off-white walls, large abstract painting, sculptural side table. "
120
+ "Restrained, elegant, Italian classical contemporary. Soft natural light, editorial photography "
121
+ "50mm f/4, refined neutral palette."
122
+ ),
123
+ "pin-06-devon-devon.jpg": (
124
+ "Photorealistic high-end Italian kitchen with light oak natural wood cabinetry, classic shaker "
125
+ "doors with polished brass handles, large central island with thick honed granite countertop "
126
+ "in pale beige speckled with grey, brass faucet, linear pendant lights with frosted glass globes, "
127
+ "honed travertine backsplash, warm white walls, light oak parquet floor, comfortable wooden "
128
+ "stools at the island, fresh herbs and ceramic bowls on counter. Devon-Devon-Cassina-Boffi style "
129
+ "blend. Bright window light, editorial photography, 35mm f/5.6."
130
+ ),
131
+ }
132
+
133
+
134
+ OPENAI_IMAGE_MODEL = os.environ.get("OPENAI_IMAGE_MODEL", "gpt-image-2") # latest released 2026-04-21
135
+
136
+
137
+ def gen_image(prompt: str, out_path: Path, size: str = "1024x1024") -> bool:
138
+ """Generate one image via OpenAI gpt-image-2 (mirror archchat-generate-image)."""
139
+ print(f" → generating {out_path.name} via {OPENAI_IMAGE_MODEL} ({len(prompt)} chars prompt)...", flush=True)
140
+ try:
141
+ resp = client.images.generate(
142
+ model=OPENAI_IMAGE_MODEL,
143
+ prompt=prompt,
144
+ size=size,
145
+ quality="high", # mirror edge function archchat-generate-image
146
+ n=1,
147
+ )
148
+ b64 = resp.data[0].b64_json
149
+ img_bytes = base64.b64decode(b64)
150
+ out_path.write_bytes(img_bytes)
151
+ kb = len(img_bytes) // 1024
152
+ print(f" ✓ saved {out_path.name} · {kb} KB", flush=True)
153
+ return True
154
+ except Exception as e:
155
+ print(f" ✗ FAILED {out_path.name}: {e}", flush=True)
156
+ return False
157
+
158
+
159
+ def main():
160
+ target = sys.argv[1] if len(sys.argv) > 1 else "all"
161
+
162
+ success = 0
163
+ total = 0
164
+
165
+ if target in ("foto", "all"):
166
+ print("\n=== Stato attuale (1980s mediocre renovation) ===")
167
+ FOTO_DIR.mkdir(parents=True, exist_ok=True)
168
+ for name, prompt in STATO_ATTUALE_PROMPTS.items():
169
+ total += 1
170
+ if gen_image(prompt, FOTO_DIR / name):
171
+ success += 1
172
+
173
+ if target in ("pinterest", "all"):
174
+ print("\n=== Pinterest references (style refs) ===")
175
+ PIN_DIR.mkdir(parents=True, exist_ok=True)
176
+ for name, prompt in PINTEREST_REFS_PROMPTS.items():
177
+ total += 1
178
+ if gen_image(prompt, PIN_DIR / name):
179
+ success += 1
180
+
181
+ print(f"\n{'='*60}")
182
+ print(f"Done · {success}/{total} images generated")
183
+ print(f" foto/ → {FOTO_DIR}")
184
+ print(f" pinterest/ → {PIN_DIR}")
185
+ return 0 if success == total else 1
186
+
187
+
188
+ if __name__ == "__main__":
189
+ sys.exit(main())
@@ -0,0 +1,382 @@
1
+ """
2
+ Generate sample placeholder assets for Attico Brera demo input.
3
+
4
+ Outputs (all in data/sample-input/):
5
+ - foto/ · 6 JPG placeholders (facciata, ingresso, soggiorno, cucina, camera, bagno)
6
+ - visura-catastale.pdf · 2-page PDF mock with cadastral data
7
+ - pinterest-references/ · 6 JPG style references
8
+
9
+ These are PLACEHOLDERS for testing. Real demo would use actual photos +
10
+ real catastral PDF from Agenzia Entrate.
11
+
12
+ Usage:
13
+ python3 generate_sample_assets.py
14
+ """
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ try:
19
+ from PIL import Image, ImageDraw, ImageFont
20
+ except ImportError:
21
+ sys.stderr.write("ERROR: pillow not installed. Run: pip3 install --user pillow\n")
22
+ sys.exit(1)
23
+
24
+ try:
25
+ from reportlab.lib.pagesizes import A4
26
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
27
+ from reportlab.lib.units import cm, mm
28
+ from reportlab.lib.colors import HexColor, black, grey
29
+ from reportlab.pdfgen import canvas
30
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
31
+ from reportlab.lib import colors
32
+ except ImportError:
33
+ sys.stderr.write("ERROR: reportlab not installed. Run: pip3 install --user reportlab\n")
34
+ sys.exit(1)
35
+
36
+
37
+ SAMPLE_DIR = Path(__file__).parent.parent / "data" / "sample-input"
38
+ FOTO_DIR = SAMPLE_DIR / "foto"
39
+ PINTEREST_DIR = SAMPLE_DIR / "pinterest-references"
40
+
41
+
42
+ # ============================================================================
43
+ # 1. FOTO placeholders · 6 JPG (1280x960 · ~150KB each)
44
+ # ============================================================================
45
+
46
+ PHOTOS = [
47
+ {
48
+ "filename": "01-facciata.jpg",
49
+ "title": "Facciata",
50
+ "subtitle": "Via Fiori Chiari 17, Milano · Brera",
51
+ "color": "#7B6F5C", # warm beige
52
+ "description": "Edificio 1910 · facciata vincolata\nZona A1 PGT · NAF Brera",
53
+ },
54
+ {
55
+ "filename": "02-ingresso.jpg",
56
+ "title": "Ingresso",
57
+ "subtitle": "Stato attuale",
58
+ "color": "#9B8A7A",
59
+ "description": "~6 m² · pavimento marmette anni '80\nCorridoio lungo a sostituire",
60
+ },
61
+ {
62
+ "filename": "03-soggiorno.jpg",
63
+ "title": "Soggiorno",
64
+ "subtitle": "Stato attuale",
65
+ "color": "#A99580",
66
+ "description": "~30 m² · seminato veneziano originale\nSoffitti decorati da preservare",
67
+ },
68
+ {
69
+ "filename": "04-cucina.jpg",
70
+ "title": "Cucina",
71
+ "subtitle": "Stato attuale",
72
+ "color": "#C8B69E",
73
+ "description": "~14 m² · separata dal soggiorno\nDa unificare in open-space",
74
+ },
75
+ {
76
+ "filename": "05-camera-padronale.jpg",
77
+ "title": "Camera padronale",
78
+ "subtitle": "Stato attuale",
79
+ "color": "#8B7B6E",
80
+ "description": "~18 m² · soffitti alti 290 cm\nDa rifare con cabina armadio",
81
+ },
82
+ {
83
+ "filename": "06-bagno.jpg",
84
+ "title": "Bagno principale",
85
+ "subtitle": "Stato attuale",
86
+ "color": "#A89887",
87
+ "description": "~6 m² · cieco · piastrelle anni '80\nDa rifare con doccia walk-in",
88
+ },
89
+ ]
90
+
91
+
92
+ def generate_photo_placeholder(filepath: Path, photo: dict) -> None:
93
+ """Create a 1280x960 JPG with title + description · architectural placeholder style."""
94
+ width, height = 1280, 960
95
+ bg_color = photo["color"]
96
+ img = Image.new("RGB", (width, height), bg_color)
97
+ draw = ImageDraw.Draw(img)
98
+
99
+ # Add gradient overlay (simulating ambient lighting)
100
+ for y in range(height):
101
+ alpha = int(80 * (y / height))
102
+ overlay = Image.new("RGBA", (width, 1), (0, 0, 0, alpha))
103
+ img.paste(overlay, (0, y), overlay if overlay.mode == "RGBA" else None)
104
+
105
+ # Try load custom font, fall back to default
106
+ try:
107
+ title_font = ImageFont.truetype("/System/Library/Fonts/Supplemental/Times New Roman.ttf", 80)
108
+ sub_font = ImageFont.truetype("/System/Library/Fonts/Supplemental/Helvetica.ttc", 38)
109
+ body_font = ImageFont.truetype("/System/Library/Fonts/Supplemental/Helvetica.ttc", 32)
110
+ meta_font = ImageFont.truetype("/System/Library/Fonts/Supplemental/Helvetica.ttc", 20)
111
+ except (OSError, IOError):
112
+ title_font = ImageFont.load_default()
113
+ sub_font = ImageFont.load_default()
114
+ body_font = ImageFont.load_default()
115
+ meta_font = ImageFont.load_default()
116
+
117
+ # Title (centered top)
118
+ draw.text((width // 2, 280), photo["title"], font=title_font, fill="white", anchor="mm")
119
+ draw.text((width // 2, 360), photo["subtitle"], font=sub_font, fill="#E8DDD0", anchor="mm")
120
+
121
+ # Description (centered middle)
122
+ y = 500
123
+ for line in photo["description"].split("\n"):
124
+ draw.text((width // 2, y), line, font=body_font, fill="white", anchor="mm")
125
+ y += 50
126
+
127
+ # Meta footer
128
+ draw.text(
129
+ (width // 2, height - 40),
130
+ "[ PLACEHOLDER · sample-input squad architettura-progetto ]",
131
+ font=meta_font,
132
+ fill="#D0C5B5",
133
+ anchor="mm",
134
+ )
135
+
136
+ # Save as JPEG quality 85 (~120-180KB)
137
+ img.save(filepath, "JPEG", quality=85, optimize=True)
138
+
139
+
140
+ # ============================================================================
141
+ # 2. PINTEREST REFERENCES · 6 style placeholders
142
+ # ============================================================================
143
+
144
+ PINTEREST = [
145
+ {"filename": "pin-01-de-cotiis.jpg", "title": "Vincenzo De Cotiis", "subtitle": "Studio Milano", "color": "#A89880"},
146
+ {"filename": "pin-02-studiopepe.jpg", "title": "Studiopepe", "subtitle": "Tonalità terra", "color": "#B8A38C"},
147
+ {"filename": "pin-03-marcante-testa.jpg", "title": "Marcante & Testa", "subtitle": "Decori restaurati", "color": "#967D62"},
148
+ {"filename": "pin-04-vilon.jpg", "title": "Hotel Vilòn Roma", "subtitle": "Seminato + contemporaneo", "color": "#C5A98A"},
149
+ {"filename": "pin-05-cassina.jpg", "title": "Cassina Living", "subtitle": "Sofa Maralunga", "color": "#8E7B65"},
150
+ {"filename": "pin-06-devon-devon.jpg", "title": "Devon&Devon", "subtitle": "Cucina rovere chiaro", "color": "#D4BC9C"},
151
+ ]
152
+
153
+
154
+ # ============================================================================
155
+ # 3. VISURA CATASTALE · 2-page PDF mock
156
+ # ============================================================================
157
+
158
+ def generate_visura_pdf(filepath: Path) -> None:
159
+ """Generate 2-page mock visura catastale (Agenzia Entrate format)."""
160
+ doc = SimpleDocTemplate(
161
+ str(filepath),
162
+ pagesize=A4,
163
+ topMargin=2 * cm,
164
+ bottomMargin=2 * cm,
165
+ leftMargin=2 * cm,
166
+ rightMargin=2 * cm,
167
+ )
168
+
169
+ styles = getSampleStyleSheet()
170
+ title_style = ParagraphStyle(
171
+ "Title",
172
+ parent=styles["Heading1"],
173
+ fontSize=14,
174
+ spaceAfter=12,
175
+ textColor=HexColor("#003566"),
176
+ )
177
+ h2 = ParagraphStyle(
178
+ "H2",
179
+ parent=styles["Heading2"],
180
+ fontSize=11,
181
+ spaceAfter=6,
182
+ textColor=HexColor("#1a1a1a"),
183
+ )
184
+ body = ParagraphStyle("Body", parent=styles["Normal"], fontSize=9, leading=12)
185
+ small = ParagraphStyle("Small", parent=styles["Normal"], fontSize=7, leading=9, textColor=grey)
186
+
187
+ story = []
188
+
189
+ # Header
190
+ story.append(Paragraph("AGENZIA DELLE ENTRATE", title_style))
191
+ story.append(Paragraph("Direzione Provinciale di Milano · Ufficio Provinciale Territorio", body))
192
+ story.append(Spacer(1, 6))
193
+ story.append(Paragraph("<b>VISURA CATASTALE</b>", h2))
194
+ story.append(Paragraph("Visura per immobile · Catasto Fabbricati", body))
195
+ story.append(Spacer(1, 12))
196
+
197
+ # Identificazione
198
+ story.append(Paragraph("<b>IDENTIFICATIVO IMMOBILE</b>", h2))
199
+ data = [
200
+ ["Comune", "MILANO (cod. F205)"],
201
+ ["Sezione", "—"],
202
+ ["Foglio", "356"],
203
+ ["Particella", "127"],
204
+ ["Subalterno", "12"],
205
+ ["Categoria", "A/2 (Abitazione di tipo civile)"],
206
+ ["Classe", "5"],
207
+ ["Consistenza", "5,5 vani"],
208
+ ["Superficie catastale", "120 m² (totale lordo)"],
209
+ ["Rendita catastale", "€ 1.456,32"],
210
+ ["Zona censuaria", "01 (centro storico)"],
211
+ ]
212
+ t = Table(data, colWidths=[5 * cm, 12 * cm])
213
+ t.setStyle(
214
+ TableStyle(
215
+ [
216
+ ("FONT", (0, 0), (-1, -1), "Helvetica", 9),
217
+ ("FONT", (0, 0), (0, -1), "Helvetica-Bold", 9),
218
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
219
+ ("TOPPADDING", (0, 0), (-1, -1), 4),
220
+ ("LINEBELOW", (0, 0), (-1, -1), 0.25, colors.lightgrey),
221
+ ]
222
+ )
223
+ )
224
+ story.append(t)
225
+ story.append(Spacer(1, 12))
226
+
227
+ # Ubicazione
228
+ story.append(Paragraph("<b>UBICAZIONE</b>", h2))
229
+ data = [
230
+ ["Indirizzo", "Via Fiori Chiari 17"],
231
+ ["CAP", "20121"],
232
+ ["Comune", "Milano (MI)"],
233
+ ["Piano", "3"],
234
+ ["Scala", "A"],
235
+ ["Interno", "5"],
236
+ ["Civico", "17"],
237
+ ]
238
+ t = Table(data, colWidths=[5 * cm, 12 * cm])
239
+ t.setStyle(
240
+ TableStyle(
241
+ [
242
+ ("FONT", (0, 0), (-1, -1), "Helvetica", 9),
243
+ ("FONT", (0, 0), (0, -1), "Helvetica-Bold", 9),
244
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
245
+ ("TOPPADDING", (0, 0), (-1, -1), 4),
246
+ ("LINEBELOW", (0, 0), (-1, -1), 0.25, colors.lightgrey),
247
+ ]
248
+ )
249
+ )
250
+ story.append(t)
251
+ story.append(Spacer(1, 12))
252
+
253
+ # Intestatari
254
+ story.append(Paragraph("<b>INTESTATARI</b>", h2))
255
+ data = [
256
+ ["Cognome / Nome", "Codice Fiscale", "Quota", "Diritto"],
257
+ ["ROSSINI MARCO", "RSSMRC83A15F205X", "1/2", "Proprietà"],
258
+ ["BIANCHI GIULIA", "BNCGLI88D52F205Y", "1/2", "Proprietà"],
259
+ ]
260
+ t = Table(data, colWidths=[6 * cm, 5 * cm, 2.5 * cm, 3.5 * cm])
261
+ t.setStyle(
262
+ TableStyle(
263
+ [
264
+ ("FONT", (0, 0), (-1, -1), "Helvetica", 9),
265
+ ("FONT", (0, 0), (-1, 0), "Helvetica-Bold", 9),
266
+ ("BACKGROUND", (0, 0), (-1, 0), HexColor("#E8E8E8")),
267
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
268
+ ("TOPPADDING", (0, 0), (-1, -1), 5),
269
+ ("GRID", (0, 0), (-1, -1), 0.25, colors.grey),
270
+ ]
271
+ )
272
+ )
273
+ story.append(t)
274
+ story.append(Spacer(1, 12))
275
+
276
+ # Vincoli
277
+ story.append(Paragraph("<b>VINCOLI E DENUNCE</b>", h2))
278
+ data = [
279
+ ["Tipo", "Riferimento", "Descrizione"],
280
+ ["Vincolo paesaggistico", "D.Lgs 42/2004 art. 142", "Zona A1 PGT Milano · NAF Brera"],
281
+ ["Vincolo monumentale indiretto", "art. 45 D.Lgs 42/2004", "Edificio in zona di rispetto"],
282
+ ]
283
+ t = Table(data, colWidths=[5 * cm, 5 * cm, 7 * cm])
284
+ t.setStyle(
285
+ TableStyle(
286
+ [
287
+ ("FONT", (0, 0), (-1, -1), "Helvetica", 8),
288
+ ("FONT", (0, 0), (-1, 0), "Helvetica-Bold", 8),
289
+ ("BACKGROUND", (0, 0), (-1, 0), HexColor("#E8E8E8")),
290
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
291
+ ("TOPPADDING", (0, 0), (-1, -1), 5),
292
+ ("GRID", (0, 0), (-1, -1), 0.25, colors.grey),
293
+ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
294
+ ]
295
+ )
296
+ )
297
+ story.append(t)
298
+ story.append(Spacer(1, 12))
299
+
300
+ # Storico
301
+ story.append(Paragraph("<b>STORICO VARIAZIONI CATASTALI</b>", h2))
302
+ data = [
303
+ ["Data", "Tipo", "Riferimento"],
304
+ ["1910-XX-XX", "Costruzione originaria", "Licenza edilizia n. 1234/1910"],
305
+ ["1985-XX-XX", "Ristrutturazione", "Concessione edilizia n. 5678/1985"],
306
+ ["2018-06-15", "Aggiornamento dati", "Variazione catastale (no oneri)"],
307
+ ]
308
+ t = Table(data, colWidths=[3 * cm, 5 * cm, 9 * cm])
309
+ t.setStyle(
310
+ TableStyle(
311
+ [
312
+ ("FONT", (0, 0), (-1, -1), "Helvetica", 8),
313
+ ("FONT", (0, 0), (-1, 0), "Helvetica-Bold", 8),
314
+ ("BACKGROUND", (0, 0), (-1, 0), HexColor("#E8E8E8")),
315
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
316
+ ("TOPPADDING", (0, 0), (-1, -1), 5),
317
+ ("GRID", (0, 0), (-1, -1), 0.25, colors.grey),
318
+ ]
319
+ )
320
+ )
321
+ story.append(t)
322
+ story.append(Spacer(1, 24))
323
+
324
+ # Footer · disclaimer
325
+ story.append(
326
+ Paragraph(
327
+ "<b>NOTA</b> · La presente visura ha valore informativo e non costituisce certificazione giuridica.<br/>"
328
+ "Per atti notarili o azioni legali è necessario richiedere visura ipo-catastale per soggetto.<br/>"
329
+ "Questo documento è un MOCK · sample-input squad architettura-progetto · "
330
+ "in produzione il dato proviene da Catasto via OpenAPI.com · NON valido per uso reale.",
331
+ small,
332
+ )
333
+ )
334
+
335
+ doc.build(story)
336
+
337
+
338
+ # ============================================================================
339
+ # RUN
340
+ # ============================================================================
341
+
342
+ def main():
343
+ SAMPLE_DIR.mkdir(parents=True, exist_ok=True)
344
+ FOTO_DIR.mkdir(parents=True, exist_ok=True)
345
+ PINTEREST_DIR.mkdir(parents=True, exist_ok=True)
346
+
347
+ print("=== Generating sample assets ===\n")
348
+
349
+ # Foto
350
+ print("[1/3] Foto placeholders...")
351
+ for photo in PHOTOS:
352
+ path = FOTO_DIR / photo["filename"]
353
+ generate_photo_placeholder(path, photo)
354
+ size_kb = path.stat().st_size / 1024
355
+ print(f" ✓ {photo['filename']} · {size_kb:.0f} KB")
356
+
357
+ # Pinterest
358
+ print("\n[2/3] Pinterest references...")
359
+ for ref in PINTEREST:
360
+ path = PINTEREST_DIR / ref["filename"]
361
+ generate_photo_placeholder(path, {**ref, "description": ref["subtitle"]})
362
+ size_kb = path.stat().st_size / 1024
363
+ print(f" ✓ {ref['filename']} · {size_kb:.0f} KB")
364
+
365
+ # Visura
366
+ print("\n[3/3] Visura catastale PDF...")
367
+ visura_path = SAMPLE_DIR / "visura-catastale.pdf"
368
+ generate_visura_pdf(visura_path)
369
+ size_kb = visura_path.stat().st_size / 1024
370
+ print(f" ✓ visura-catastale.pdf · {size_kb:.0f} KB")
371
+
372
+ # Summary
373
+ print("\n=== Done ===")
374
+ print(f" · sample-input/foto/ · {len(PHOTOS)} JPG (architectural placeholders)")
375
+ print(f" · sample-input/pinterest-references/ · {len(PINTEREST)} JPG")
376
+ print(f" · sample-input/visura-catastale.pdf · 2 pages")
377
+ print(f" · sample-input/briefing-cliente.md (already exists)")
378
+ print(f" · sample-input/stato-attuale.dxf (already generated)")
379
+
380
+
381
+ if __name__ == "__main__":
382
+ main()