2026-10-20 TUE UTC 12:42 PM TPE 8:42 PM Auto-updates hourly 中文 EN
LIVE WIRE
← Back to the site changelog Site Changelog

Original commit record

News photos: full frame, full color, labelled subject Per the owner's direction, article photos must make their relation to the story obvious at a glance: - No more cropping: hero and article images render the FULL photo (height auto + object-fit contain with a max height and a neutral letterbox background). The FAA headquarters sign - the part that identifies the photo - is no longer cut off. - No more grayscale on real news photos; the grayscale treatment stays only on the empty placeholder art. - Every matched photo now carries a zh subject label rendered ahead of the credit line (e.g. 澎湖馬公機場 / 美國聯邦航空總署(FAA)總部 / 註冊號 B-XXXXX), stored on the image dict by images.py at match time. - Re-resolved both published articles with subjects attached. tests/test_images.py: 26 checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

SHA
a7e8d762dff5ad4d64996183307e17dd5734b019
Commit date
Author
avwire
Committer
avwire

Changed files (9)

  • data/articles/2026-07-26T17.json modified · +2 / −1
  • data/articles/2026-07-26T18.json modified · +2 / −1
  • data/images.json modified · +6 / −4
  • pipeline/build.py modified · +2 / −1
  • pipeline/images.py modified · +44 / −36
  • static/site.css modified · +6 / −1
  • templates/article.html modified · +2 / −1
  • templates/home.html modified · +2 / −2
  • tests/test_images.py modified · +5 / −3

Full diff

@@ -12,7 +12,8 @@\n         "license": "CC BY-SA 4.0",\n         "provider": "Wikimedia Commons",\n         "kind": "file_photo",\n-        "matched": "Magong Airport Penghu"\n+        "matched": "Magong Airport Penghu",\n+        "subject": "澎湖馬公機場"\n       },\n       "zh": {\n         "title": "民航局公布中秋連假離島加班 1340架次7/27開訂",\n@@ -12,7 +12,8 @@\n         "license": "CC BY-SA 4.0",\n         "provider": "Wikimedia Commons",\n         "kind": "file_photo",\n-        "matched": "Federal Aviation Administration headquarters"\n+        "matched": "Federal Aviation Administration headquarters",\n+        "subject": "美國聯邦航空總署(FAA)總部"\n       },\n       "zh": {\n         "title": "FAA宣布世足賽反無人機行動成果 逾700架無人機被查扣",\n@@ -9,9 +9,10 @@\n         "license": "CC BY-SA 4.0",\n         "provider": "Wikimedia Commons",\n         "kind": "file_photo",\n-        "matched": "Federal Aviation Administration headquarters"\n+        "matched": "Federal Aviation Administration headquarters",\n+        "subject": "美國聯邦航空總署(FAA)總部"\n       },\n-      "checked_utc": "2026-07-26T22:05:12.562330+00:00"\n+      "checked_utc": "2026-07-26T22:18:30.459690+00:00"\n     },\n     "a-20260726-1748-caa-taiwan-announces-1-340-extra-flights": {\n       "status": "matched",\n@@ -22,9 +23,10 @@\n         "license": "CC BY-SA 4.0",\n         "provider": "Wikimedia Commons",\n         "kind": "file_photo",\n-        "matched": "Magong Airport Penghu"\n+        "matched": "Magong Airport Penghu",\n+        "subject": "澎湖馬公機場"\n       },\n-      "checked_utc": "2026-07-26T22:05:12.562330+00:00"\n+      "checked_utc": "2026-07-26T22:18:30.459690+00:00"\n     }\n   }\n }\n@@ -631,6 +631,7 @@ def normalize_image(raw_img):\n             "credit": str(raw_img.get("credit") or "") or None,\n             "license": str(raw_img.get("license") or "") or None,\n             "provider": str(raw_img.get("provider") or "") or None,\n+            "subject": str(raw_img.get("subject") or "") or None,\n             "kind": (raw_img.get("kind")\n                      if raw_img.get("kind") in ("airframe_photo",\n                                                 "file_photo")\n@@ -641,7 +642,7 @@ def normalize_image(raw_img):\n         if not _safe_web_url(url):\n             return None\n         return {"url": url, "link": None, "credit": None, "license": None,\n-                "provider": None, "kind": "file_photo"}\n+                "provider": None, "subject": None, "kind": "file_photo"}\n     return None\n \n \n@@ -71,38 +71,40 @@\n # routes say 澎湖/金門/馬祖, never the airport's registry name, so the\n # config-name match below rarely fires on its own.\n _AIRPORT_ALIASES = (\n-    ("桃園機場", "Taoyuan International Airport", ("Taoyuan",)),\n-    ("桃園國際機場", "Taoyuan International Airport", ("Taoyuan",)),\n-    ("松山機場", "Taipei Songshan Airport", ("Songshan",)),\n-    ("小港機場", "Kaohsiung International Airport", ("Kaohsiung",)),\n-    ("高雄國際機場", "Kaohsiung International Airport", ("Kaohsiung",)),\n-    ("澎湖", "Magong Airport Penghu", ("Magong",)),\n-    ("馬公", "Magong Airport Penghu", ("Magong",)),\n-    ("金門", "Kinmen Airport", ("Kinmen",)),\n-    ("馬祖", "Matsu Nangan Airport", ("Nangan", "Matsu")),\n-    ("南竿", "Matsu Nangan Airport", ("Nangan", "Matsu")),\n-    ("北竿", "Matsu Beigan Airport", ("Beigan", "Matsu")),\n-    ("花蓮機場", "Hualien Airport", ("Hualien",)),\n-    ("臺東機場", "Taitung Airport", ("Taitung",)),\n-    ("台東機場", "Taitung Airport", ("Taitung",)),\n+    ("桃園機場", "Taoyuan International Airport", ("Taoyuan",), "桃園國際機場"),\n+    ("桃園國際機場", "Taoyuan International Airport", ("Taoyuan",), "桃園國際機場"),\n+    ("松山機場", "Taipei Songshan Airport", ("Songshan",), "臺北松山機場"),\n+    ("小港機場", "Kaohsiung International Airport", ("Kaohsiung",), "高雄國際機場"),\n+    ("高雄國際機場", "Kaohsiung International Airport", ("Kaohsiung",), "高雄國際機場"),\n+    ("澎湖", "Magong Airport Penghu", ("Magong",), "澎湖馬公機場"),\n+    ("馬公", "Magong Airport Penghu", ("Magong",), "澎湖馬公機場"),\n+    ("金門", "Kinmen Airport", ("Kinmen",), "金門機場"),\n+    ("馬祖", "Matsu Nangan Airport", ("Nangan", "Matsu"), "馬祖南竿機場"),\n+    ("南竿", "Matsu Nangan Airport", ("Nangan", "Matsu"), "馬祖南竿機場"),\n+    ("北竿", "Matsu Beigan Airport", ("Beigan", "Matsu"), "馬祖北竿機場"),\n+    ("花蓮機場", "Hualien Airport", ("Hualien",), "花蓮機場"),\n+    ("臺東機場", "Taitung Airport", ("Taitung",), "臺東機場"),\n+    ("台東機場", "Taitung Airport", ("Taitung",), "臺東機場"),\n )\n \n # Official-agency fallback: a story about a regulator with no aircraft/\n # airport entity gets an honest agency file photo. ASCII keys match on\n # word boundaries; CJK keys as substrings.\n _ORG_QUERIES = (\n     ("faa", "Federal Aviation Administration headquarters",\n-     ("Federal Aviation",)),\n+     ("Federal Aviation",), "美國聯邦航空總署(FAA)總部"),\n     ("ntsb", "National Transportation Safety Board",\n-     ("NTSB", "National Transportation Safety")),\n+     ("NTSB", "National Transportation Safety"), "美國國家運輸安全委員會(NTSB)"),\n     ("icao", "International Civil Aviation Organization headquarters",\n-     ("ICAO", "International Civil Aviation")),\n+     ("ICAO", "International Civil Aviation"), "國際民航組織(ICAO)總部"),\n     ("iata", "International Air Transport Association",\n-     ("IATA",)),\n+     ("IATA",), "國際航空運輸協會(IATA)"),\n     ("easa", "European Union Aviation Safety Agency",\n-     ("EASA",)),\n-    ("eurocontrol", "Eurocontrol headquarters", ("Eurocontrol",)),\n-    ("民用航空局", "交通部民用航空局", ("民用航空局", "Civil Aeronautics")),\n+     ("EASA",), "歐盟航空安全局(EASA)"),\n+    ("eurocontrol", "Eurocontrol headquarters", ("Eurocontrol",),\n+     "歐洲空管組織(Eurocontrol)總部"),\n+    ("民用航空局", "交通部民用航空局", ("民用航空局", "Civil Aeronautics"),\n+     "交通部民用航空局"),\n )\n \n _airlines = [\n@@ -183,32 +185,32 @@ def find_aircraft_type(article: dict) -> str | None:\n \n \n def find_airport(article: dict):\n-    """(commons_query, required_title_tokens) for a mentioned airport."""\n+    """(commons_query, required_title_tokens, subject_zh) for an airport."""\n     text = _article_text(article)\n     low = text.casefold()\n-    for marker, query, tokens in _AIRPORT_ALIASES:\n+    for marker, query, tokens, subject in _AIRPORT_ALIASES:\n         if marker in text:\n-            return query, list(tokens)\n+            return query, list(tokens), subject\n     for ap in _tw_airports:\n         for key in ("name_zh", "name_en"):\n             val = str(ap.get(key) or "")\n             if val and (val in text or val.casefold() in low):\n                 name = str(ap.get("name_en") or val)\n-                return name, [name.split()[0]]\n+                return name, [name.split()[0]], str(ap.get("name_zh") or name)\n     return None\n \n \n def find_org(article: dict):\n-    """(commons_query, required_title_tokens) for an official agency."""\n+    """(commons_query, required_title_tokens, subject_zh) for an agency."""\n     text = _article_text(article)\n     low = text.casefold()\n-    for marker, query, tokens in _ORG_QUERIES:\n+    for marker, query, tokens, subject in _ORG_QUERIES:\n         if re.search(r"[一-鿿]", marker):\n             hit = marker in text\n         else:\n             hit = re.search(rf"\b{re.escape(marker)}\b", low) is not None\n         if hit:\n-            return query, list(tokens)\n+            return query, list(tokens), subject\n     return None\n \n \n@@ -239,11 +241,13 @@ def lookup_planespotters(reg: str) -> dict | None:\n             "provider": "Planespotters.net",\n             "kind": "airframe_photo",\n             "matched": reg,\n+            "subject": f"註冊號 {reg}",\n         }\n     return None\n \n \n-def lookup_commons(query: str, require_tokens: list[str]) -> dict | None:\n+def lookup_commons(query: str, require_tokens: list[str],\n+                   subject: str | None = None) -> dict | None:\n     """First freely-licensed Commons bitmap matching the query.\n \n     require_tokens: at least one must appear in the file title, so a\n@@ -289,6 +293,7 @@ def lookup_commons(query: str, require_tokens: list[str]) -> dict | None:\n                 "provider": "Wikimedia Commons",\n                 "kind": "file_photo",\n                 "matched": query,\n+                "subject": subject or None,\n             }\n     return None\n \n@@ -306,19 +311,22 @@ def resolve_image(article: dict) -> dict | None:\n     if airline and actype:\n         # family token ("A350") so Commons titles with any sub-variant match\n         type_token = actype.split()[-1].split("-")[0]\n-        return lookup_commons(f'{airline} {actype}', [type_token, airline])\n+        return lookup_commons(f'{airline} {actype}', [type_token, airline],\n+                              subject=f"{airline} {actype}")\n     if actype:\n         type_token = actype.split()[-1].split("-")[0]\n-        return lookup_commons(f'{actype} aircraft', [type_token])\n+        return lookup_commons(f'{actype} aircraft', [type_token],\n+                              subject=actype)\n     if airline:\n-        return lookup_commons(f'{airline} aircraft', [airline])\n+        return lookup_commons(f'{airline} aircraft', [airline],\n+                              subject=airline)\n     if airport:\n-        query, tokens = airport\n-        return lookup_commons(query, tokens)\n+        query, tokens, subject = airport\n+        return lookup_commons(query, tokens, subject=subject)\n     org = find_org(article)\n     if org:\n-        query, tokens = org\n-        return lookup_commons(query, tokens)\n+        query, tokens, subject = org\n+        return lookup_commons(query, tokens, subject=subject)\n     return None  # nothing visual verified in this article -> no image\n \n \n@@ -100,7 +100,12 @@ a.flash-row:hover { color: inherit; }\n   border: 1px solid var(--color-divider);\n   display: flex; align-items: flex-end; padding: 10px;\n }\n-.hero-img { height: 280px; width: 100%; object-fit: cover; border: 1px solid var(--color-divider); }\n+/* real news photos: full frame, full color — never crop away the subject */\n+.hero-img {\n+  width: 100%; height: auto; max-height: 420px; object-fit: contain;\n+  background: color-mix(in srgb, var(--color-text) 6%, var(--color-surface));\n+  border: 1px solid var(--color-divider);\n+}\n \n .feed-row {\n   display: grid; grid-template-columns: 48px 84px minmax(0, 1fr) auto;\n@@ -13,8 +13,9 @@ <h1 style="font-size:38px;line-height:1.18;margin:14px 0 12px;">{{ a.title }}</h\n     </div>\n     {% if a.image %}\n     <figure style="margin:0 0 22px;">\n-      <img class="grayscale" src="{{ a.image.url }}" alt="{{ a.title }}" loading="lazy" style="width:100%;max-height:420px;object-fit:cover;border:1px solid var(--color-divider);">\n+      <img src="{{ a.image.url }}" alt="{{ a.image.subject or a.title }}" loading="lazy" style="width:100%;height:auto;max-height:560px;object-fit:contain;background:color-mix(in srgb,var(--color-text) 6%,var(--color-surface));border:1px solid var(--color-divider);">\n       <figcaption class="text-muted" style="font-size:11.5px;margin-top:6px;display:flex;gap:10px;flex-wrap:wrap;">\n+        {% if a.image.subject %}<span style="font-weight:800;color:var(--color-text);">{{ a.image.subject }}</span>{% endif %}\n         <span>{{ t.photoKind[a.image.kind] }}</span>\n         {% if a.image.credit %}<span>© {{ a.image.credit }}</span>{% endif %}\n         {% if a.image.license %}<span>{{ a.image.license }}</span>{% endif %}\n@@ -38,8 +38,8 @@ <h6 style="margin:28px 0 12px;">{{ t.sourceStatus }}</h6>\n       {% if hero %}\n       <a href="{{ hero.url }}"{% if hero.external %} target="_blank" rel="noopener"{% endif %} style="display:block;color:inherit;text-decoration:none;cursor:pointer;">\n         {% if hero.image %}\n-        <img class="grayscale hero-img" src="{{ hero.image.url }}" alt="">\n-        <div class="text-muted" style="font-size:10.5px;margin-top:4px;">{{ t.photoKind[hero.image.kind] }}{% if hero.image.credit %} · © {{ hero.image.credit }}{% endif %}{% if hero.image.license %} · {{ hero.image.license }}{% endif %}{% if hero.image.provider %} · {{ hero.image.provider }}{% endif %}</div>\n+        <img class="hero-img" src="{{ hero.image.url }}" alt="{{ hero.image.subject or '' }}">\n+        <div class="text-muted" style="font-size:10.5px;margin-top:4px;">{% if hero.image.subject %}<b style="color:var(--color-text);">{{ hero.image.subject }}</b> · {% endif %}{{ t.photoKind[hero.image.kind] }}{% if hero.image.credit %} · © {{ hero.image.credit }}{% endif %}{% if hero.image.license %} · {{ hero.image.license }}{% endif %}{% if hero.image.provider %} · {{ hero.image.provider }}{% endif %}</div>\n         {% else %}\n         <div class="grayscale hero-ph">\n           <span class="text-muted" style="font-family:monospace;font-size:11px;">{{ t.heroImg }}</span>\n@@ -154,13 +154,13 @@ def reset():\n        and images.resolve_image(make_article(\n            "a", "Global travel demand keeps rising")) is None\n        and fake.calls == []))\n-check("island wording maps to the airport photo query",\n+check("island wording maps to the airport photo query with zh subject",\n       images.find_airport(make_article("a", "澎湖離島航線加班"))\n-      == ("Magong Airport Penghu", ["Magong"]))\n+      == ("Magong Airport Penghu", ["Magong"], "澎湖馬公機場"))\n check("agency stories fall back to an official-agency photo query",\n       images.find_org(make_article("a", "FAA announces new drone rule"))\n       == ("Federal Aviation Administration headquarters",\n-          ["Federal Aviation"]))\n+          ["Federal Aviation"], "美國聯邦航空總署(FAA)總部"))\n check("substring 'faa' inside a word never triggers the org match",\n       images.find_org(make_article("a", "shortfaall of capacity")) is None)\n \n@@ -218,6 +218,8 @@ def reset():\n check("agency fallback attaches the building photo, never the logo",\n       art["image"]["url"] == "https://upload.wikimedia.org/faa-hq.jpg"\n       and "logo" not in art["image"]["url"])\n+check("photo carries its zh subject caption",\n+      art["image"]["subject"] == "美國聯邦航空總署(FAA)總部")\n \n # ── cache behaviour ──────────────────────────────────────────────────────────\n 

You found a secret theme!

8-bit mode is unlocked. Refresh to add it to your theme selector.