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

improve airline image quality profiles

SHA
08e6974a05f3704253ae1e0bfe54edadebb0678f
Commit date
Author
nnneeeooo-nnneeeooo
Committer
nnneeeooo-nnneeeooo

Changed files (14)

  • config/airline_visual_profiles.json added · +96 / −0
  • data/articles/2026-07-28T17.json modified · +7 / −7
  • data/articles/2026-08-06T12.json modified · +6 / −6
  • data/articles/2026-08-20T07.json modified · +4 / −4
  • data/articles/2026-08-20T11.json modified · +4 / −4
  • data/articles/2026-08-26T12.json modified · +7 / −7
  • data/articles/2026-09-10T07.json modified · +6 / −6
  • data/images.json modified · +61 / −34
  • pipeline/image_fallbacks.py modified · +0 / −18
  • pipeline/image_selection.py modified · +5 / −0
  • pipeline/images.py modified · +110 / −8
  • tests/test_image_fallbacks.py modified · +4 / −8
  • tests/test_image_selection.py modified · +90 / −0
  • tests/test_images.py modified · +3 / −2

Full diff

@@ -0,0 +1,96 @@\n+{\n+  "version": 1,\n+  "description": "Verified fleet-role preferences for generic airline news images. Exact event, registration and article-named aircraft evidence always outrank these profiles.",\n+  "profiles": [\n+    {\n+      "airline": "China Airlines",\n+      "flagship": ["Airbus A350-900", "Boeing 777-300ER"],\n+      "regional": ["Airbus A321neo", "Airbus A330-300"],\n+      "cargo": ["Boeing 777F", "Boeing 747-400F"],\n+      "future": ["Airbus A350-1000", "Boeing 787-9", "Boeing 787-10", "Boeing 777-9", "Boeing 777-8F"],\n+      "excluded_generic": ["Boeing 737-800"],\n+      "verified_utc": "2026-09-10T00:00:00Z",\n+      "source": "https://www.china-airlines.com/tw/en/about-china-airlines/about-us/operations"\n+    },\n+    {\n+      "airline": "EVA Air",\n+      "flagship": ["Boeing 787-10", "Boeing 787-9", "Boeing 777-300ER"],\n+      "regional": ["Boeing 787-10", "Airbus A330-300", "Airbus A321-200"],\n+      "cargo": ["Boeing 777F"],\n+      "future": ["Airbus A350-1000", "Airbus A321neo"],\n+      "excluded_generic": [],\n+      "verified_utc": "2026-09-10T00:00:00Z",\n+      "source": "https://www.evaair.com/en-tw/about-eva-air/about-us/market-and-sales-overview/eva-air-fleet/"\n+    },\n+    {\n+      "airline": "STARLUX Airlines",\n+      "flagship": ["Airbus A350-1000", "Airbus A350-900"],\n+      "regional": ["Airbus A330neo", "Airbus A321neo"],\n+      "cargo": [],\n+      "future": [],\n+      "excluded_generic": [],\n+      "verified_utc": "2026-09-10T00:00:00Z",\n+      "source": "https://www.starlux-airlines.com/en-US/experience/walk-into-starlux/our-fleet"\n+    },\n+    {\n+      "airline": "Delta Air Lines",\n+      "flagship": ["Airbus A350-900", "Airbus A330neo"],\n+      "regional": ["Airbus A321neo", "Airbus A220-300"],\n+      "cargo": [],\n+      "future": ["Airbus A350-1000", "Boeing 787-10"],\n+      "excluded_generic": [],\n+      "verified_utc": "2026-09-10T00:00:00Z",\n+      "source": "https://www.delta.com/us/en/aircraft/overview"\n+    },\n+    {\n+      "airline": "American Airlines",\n+      "flagship": ["Boeing 787-9", "Boeing 777-300ER"],\n+      "regional": ["Airbus A321neo", "Boeing 737 MAX 8"],\n+      "cargo": [],\n+      "future": [],\n+      "excluded_generic": [],\n+      "verified_utc": "2026-09-10T00:00:00Z",\n+      "source": "https://www.aa.com/web/i18n/travel-info/experience/planes/planes.html"\n+    },\n+    {\n+      "airline": "Emirates",\n+      "flagship": ["Airbus A380-800", "Airbus A350-900", "Boeing 777-300ER"],\n+      "regional": ["Airbus A350-900", "Boeing 777-300ER"],\n+      "cargo": [],\n+      "future": [],\n+      "excluded_generic": [],\n+      "verified_utc": "2026-09-10T00:00:00Z",\n+      "source": "https://www.emirates.com/us/english/experience/our-fleet/"\n+    },\n+    {\n+      "airline": "British Airways",\n+      "flagship": ["Airbus A350-1000", "Boeing 787-10", "Boeing 787-9"],\n+      "regional": ["Airbus A321neo", "Airbus A320neo"],\n+      "cargo": [],\n+      "future": [],\n+      "excluded_generic": [],\n+      "verified_utc": "2026-09-10T00:00:00Z",\n+      "source": "https://www.britishairways.com/content/information/about-ba/fleet-facts"\n+    },\n+    {\n+      "airline": "All Nippon Airways",\n+      "flagship": ["Boeing 787-9", "Boeing 787-10"],\n+      "regional": ["Airbus A321neo", "Airbus A320neo"],\n+      "cargo": ["Boeing 777F", "Boeing 767-300F"],\n+      "future": [],\n+      "excluded_generic": [],\n+      "verified_utc": "2026-09-10T00:00:00Z",\n+      "source": "https://www.ana.co.jp/group/en/company/ana/scale/"\n+    },\n+    {\n+      "airline": "Qatar Airways",\n+      "flagship": ["Airbus A350-1000", "Airbus A350-900", "Boeing 787-9"],\n+      "regional": ["Boeing 787-9", "Airbus A320-200"],\n+      "cargo": [],\n+      "future": [],\n+      "excluded_generic": [],\n+      "verified_utc": "2026-09-10T00:00:00Z",\n+      "source": "https://www.qatarairways.com/en-gb/fleet.html"\n+    }\n+  ]\n+}\n@@ -143,15 +143,15 @@\n         ]\n       },\n       "image": {\n-        "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n-        "link": "https://commons.wikimedia.org/w/index.php?curid=187945993",\n-        "credit": "4300streetcar",\n-        "license": "CC BY 4.0",\n+        "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/c/c6/China_Airlines_Airbus_A350-900_B-18918.jpg/1280px-China_Airlines_Airbus_A350-900_B-18918.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n+        "link": "https://commons.wikimedia.org/wiki/File:China_Airlines_Airbus_A350-900_B-18918.jpg",\n+        "credit": "Melv_L - MACASR",\n+        "license": "CC BY-SA 2.0",\n         "provider": "Wikimedia Commons",\n         "kind": "file_photo",\n-        "matched": "China Airlines aircraft",\n-        "subject": "China Airlines Boeing 737-800",\n-        "photoYear": 2026\n+        "matched": "China Airlines Airbus A350-900",\n+        "subject": "China Airlines Airbus A350-900",\n+        "photoYear": 2019\n       }\n     }\n   ]\n@@ -6,14 +6,14 @@\n       "cat": "ops",\n       "primarySource": "中央社 CNA 國籍航空",\n       "image": {\n-        "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n-        "link": "https://commons.wikimedia.org/w/index.php?curid=187945993",\n-        "credit": "4300streetcar",\n-        "license": "CC BY 4.0",\n+        "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/8/87/Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_%28Carbon_Fibre_livery%29_%2855500680951%29.jpg/1280px-Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_%28Carbon_Fibre_livery%29_%2855500680951%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n+        "link": "https://commons.wikimedia.org/wiki/File:Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_(Carbon_Fibre_livery)_(55500680951).jpg",\n+        "credit": "Alan Wilson",\n+        "license": "CC BY-SA 4.0",\n         "provider": "Wikimedia Commons",\n         "kind": "file_photo",\n-        "matched": "China Airlines aircraft",\n-        "subject": "China Airlines Boeing 737-800",\n+        "matched": "China Airlines Airbus A350-900",\n+        "subject": "China Airlines Airbus A350-900",\n         "photoYear": 2026\n       },\n       "zh": {\n@@ -106,14 +106,14 @@\n         ]\n       },\n       "image": {\n-        "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n-        "link": "https://commons.wikimedia.org/wiki/File:China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg",\n+        "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/a/a4/China_Airlines_Cargo_Boeing_777F_B-18773_departing_Taoyuan_February_2026.jpg/1280px-China_Airlines_Cargo_Boeing_777F_B-18773_departing_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n+        "link": "https://commons.wikimedia.org/wiki/File:China_Airlines_Cargo_Boeing_777F_B-18773_departing_Taoyuan_February_2026.jpg",\n         "credit": "4300streetcar",\n         "license": "CC BY 4.0",\n         "provider": "Wikimedia Commons",\n         "kind": "file_photo",\n-        "matched": "topic:china-airlines",\n-        "subject": "China Airlines Boeing 737-800",\n+        "matched": "China Airlines Boeing 777F",\n+        "subject": "China Airlines Cargo Boeing 777F",\n         "photoYear": 2026\n       }\n     },\n@@ -89,14 +89,14 @@\n         ]\n       },\n       "image": {\n-        "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n-        "link": "https://commons.wikimedia.org/wiki/File:China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg",\n+        "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/China_Airlines_Boeing_777-300ER_B-18051_departing_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_777-300ER_B-18051_departing_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n+        "link": "https://commons.wikimedia.org/w/index.php?curid=187535326",\n         "credit": "4300streetcar",\n         "license": "CC BY 4.0",\n         "provider": "Wikimedia Commons",\n         "kind": "file_photo",\n-        "matched": "topic:china-airlines",\n-        "subject": "China Airlines Boeing 737-800",\n+        "matched": "China Airlines Boeing 777-300ER",\n+        "subject": "China Airlines Boeing 777-300ER",\n         "photoYear": 2026\n       }\n     },\n@@ -6,15 +6,15 @@\n       "cat": "ops",\n       "primarySource": "中央社 CNA 國籍航空",\n       "image": {\n-        "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n-        "link": "https://commons.wikimedia.org/wiki/File:China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg",\n-        "credit": "4300streetcar",\n-        "license": "CC BY 4.0",\n+        "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/c/c6/China_Airlines_Airbus_A350-900_B-18918.jpg/1280px-China_Airlines_Airbus_A350-900_B-18918.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n+        "link": "https://commons.wikimedia.org/wiki/File:China_Airlines_Airbus_A350-900_B-18918.jpg",\n+        "credit": "Melv_L - MACASR",\n+        "license": "CC BY-SA 2.0",\n         "provider": "Wikimedia Commons",\n         "kind": "file_photo",\n-        "matched": "topic:china-airlines",\n-        "subject": "China Airlines Boeing 737-800",\n-        "photoYear": 2026\n+        "matched": "China Airlines Airbus A350-900",\n+        "subject": "China Airlines Airbus A350-900",\n+        "photoYear": 2019\n       },\n       "zh": {\n         "title": "華航預計2027年3月起調整一線人員名牌呈現方式",\n@@ -6,14 +6,14 @@\n       "cat": "biz",\n       "primarySource": "中央社 CNA 國籍航空",\n       "image": {\n-        "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n-        "link": "https://commons.wikimedia.org/wiki/File:China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg",\n-        "credit": "4300streetcar",\n-        "license": "CC BY 4.0",\n+        "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/8/87/Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_%28Carbon_Fibre_livery%29_%2855500680951%29.jpg/1280px-Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_%28Carbon_Fibre_livery%29_%2855500680951%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n+        "link": "https://commons.wikimedia.org/wiki/File:Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_(Carbon_Fibre_livery)_(55500680951).jpg",\n+        "credit": "Alan Wilson",\n+        "license": "CC BY-SA 4.0",\n         "provider": "Wikimedia Commons",\n         "kind": "file_photo",\n-        "matched": "topic:china-airlines",\n-        "subject": "China Airlines Boeing 737-800",\n+        "matched": "China Airlines Airbus A350-900",\n+        "subject": "China Airlines Airbus A350-900",\n         "photoYear": 2026\n       },\n       "sourceImageCandidates": [],\n@@ -513,17 +513,17 @@\n     "a-20260806-1215-typhoon-white-dolphin-prompts-china-airl": {\n       "status": "matched",\n       "image": {\n-        "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n-        "link": "https://commons.wikimedia.org/w/index.php?curid=187945993",\n-        "credit": "4300streetcar",\n-        "license": "CC BY 4.0",\n+        "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/8/87/Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_%28Carbon_Fibre_livery%29_%2855500680951%29.jpg/1280px-Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_%28Carbon_Fibre_livery%29_%2855500680951%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n+        "link": "https://commons.wikimedia.org/wiki/File:Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_(Carbon_Fibre_livery)_(55500680951).jpg",\n+        "credit": "Alan Wilson",\n+        "license": "CC BY-SA 4.0",\n         "provider": "Wikimedia Commons",\n         "kind": "file_photo",\n-        "matched": "China Airlines aircraft",\n-        "subject": "China Airlines Boeing 737-800",\n+        "matched": "China Airlines Airbus A350-900",\n+        "subject": "China Airlines Airbus A350-900",\n         "photoYear": 2026\n       },\n-      "checked_utc": "2026-08-06T13:56:31.252671+00:00"\n+      "checked_utc": "2026-09-10T14:04:56+00:00"\n     },\n     "a-20260806-1215-china-airlines-airlifts-relief-partition": {\n       "status": "matched",\n@@ -739,17 +739,17 @@\n     "a-20260728-1735-kumamoto-earthquake-taiwan-flight-disruptions": {\n       "status": "matched",\n       "image": {\n-        "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n-        "link": "https://commons.wikimedia.org/w/index.php?curid=187945993",\n-        "credit": "4300streetcar",\n-        "license": "CC BY 4.0",\n+        "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/c/c6/China_Airlines_Airbus_A350-900_B-18918.jpg/1280px-China_Airlines_Airbus_A350-900_B-18918.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n+        "link": "https://commons.wikimedia.org/wiki/File:China_Airlines_Airbus_A350-900_B-18918.jpg",\n+        "credit": "Melv_L - MACASR",\n+        "license": "CC BY-SA 2.0",\n         "provider": "Wikimedia Commons",\n         "kind": "file_photo",\n-        "matched": "China Airlines aircraft",\n-        "subject": "China Airlines Boeing 737-800",\n-        "photoYear": 2026\n+        "matched": "China Airlines Airbus A350-900",\n+        "subject": "China Airlines Airbus A350-900",\n+        "photoYear": 2019\n       },\n-      "checked_utc": "2026-08-09T20:58:03.582807+00:00"\n+      "checked_utc": "2026-09-10T14:04:56+00:00"\n     },\n     "a-20260808-0704-japan-airlines-and-malaysia-airlines-lau": {\n       "status": "matched",\n@@ -2308,16 +2308,34 @@\n       "checked_utc": "2026-08-24T10:31:26+00:00"\n     },\n     "a-20260820-1119-china-airlines-to-launch-new-business-cl": {\n-      "status": "none",\n-      "attempts": 3,\n-      "checked_utc": "2026-08-25T01:11:41.505073+00:00",\n-      "next_retry_utc": "2026-08-25T07:11:41.505073+00:00"\n+      "status": "matched",\n+      "image": {\n+        "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/China_Airlines_Boeing_777-300ER_B-18051_departing_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_777-300ER_B-18051_departing_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n+        "link": "https://commons.wikimedia.org/w/index.php?curid=187535326",\n+        "credit": "4300streetcar",\n+        "license": "CC BY 4.0",\n+        "provider": "Wikimedia Commons",\n+        "kind": "file_photo",\n+        "matched": "China Airlines Boeing 777-300ER",\n+        "subject": "China Airlines Boeing 777-300ER",\n+        "photoYear": 2026\n+      },\n+      "checked_utc": "2026-09-10T14:04:56+00:00"\n     },\n     "a-20260820-0733-china-airlines-to-raise-cargo-fuel-surch": {\n-      "status": "none",\n-      "attempts": 3,\n-      "checked_utc": "2026-08-25T01:11:41.505073+00:00",\n-      "next_retry_utc": "2026-08-25T07:11:41.505073+00:00"\n+      "status": "matched",\n+      "image": {\n+        "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/a/a4/China_Airlines_Cargo_Boeing_777F_B-18773_departing_Taoyuan_February_2026.jpg/1280px-China_Airlines_Cargo_Boeing_777F_B-18773_departing_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n+        "link": "https://commons.wikimedia.org/wiki/File:China_Airlines_Cargo_Boeing_777F_B-18773_departing_Taoyuan_February_2026.jpg",\n+        "credit": "4300streetcar",\n+        "license": "CC BY 4.0",\n+        "provider": "Wikimedia Commons",\n+        "kind": "file_photo",\n+        "matched": "China Airlines Boeing 777F",\n+        "subject": "China Airlines Cargo Boeing 777F",\n+        "photoYear": 2026\n+      },\n+      "checked_utc": "2026-09-10T14:04:56+00:00"\n     },\n     "a-20260817-0444-china-airlines-wins-best-companies-to-wo": {\n       "status": "none",\n@@ -2814,10 +2832,19 @@\n       "checked_utc": "2026-08-26T10:29:25.961860+00:00"\n     },\n     "a-20260826-1232-china-airlines-to-revise-frontline-staff": {\n-      "status": "none",\n-      "attempts": 1,\n-      "checked_utc": "2026-08-26T13:00:34.211320+00:00",\n-      "next_retry_utc": "2026-08-26T19:00:34.211320+00:00"\n+      "status": "matched",\n+      "image": {\n+        "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/c/c6/China_Airlines_Airbus_A350-900_B-18918.jpg/1280px-China_Airlines_Airbus_A350-900_B-18918.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n+        "link": "https://commons.wikimedia.org/wiki/File:China_Airlines_Airbus_A350-900_B-18918.jpg",\n+        "credit": "Melv_L - MACASR",\n+        "license": "CC BY-SA 2.0",\n+        "provider": "Wikimedia Commons",\n+        "kind": "file_photo",\n+        "matched": "China Airlines Airbus A350-900",\n+        "subject": "China Airlines Airbus A350-900",\n+        "photoYear": 2019\n+      },\n+      "checked_utc": "2026-09-10T14:04:56+00:00"\n     },\n     "a-20260826-1232-us-army-mh-60-black-hawk-comes-down-near": {\n       "status": "matched",\n@@ -4577,17 +4604,17 @@\n     "a-20260910-0755-china-airlines-to-raise-international-bo": {\n       "status": "matched",\n       "image": {\n-        "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n-        "link": "https://commons.wikimedia.org/wiki/File:China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg",\n-        "credit": "4300streetcar",\n-        "license": "CC BY 4.0",\n+        "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/8/87/Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_%28Carbon_Fibre_livery%29_%2855500680951%29.jpg/1280px-Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_%28Carbon_Fibre_livery%29_%2855500680951%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n+        "link": "https://commons.wikimedia.org/wiki/File:Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_(Carbon_Fibre_livery)_(55500680951).jpg",\n+        "credit": "Alan Wilson",\n+        "license": "CC BY-SA 4.0",\n         "provider": "Wikimedia Commons",\n         "kind": "file_photo",\n-        "matched": "topic:china-airlines",\n-        "subject": "China Airlines Boeing 737-800",\n+        "matched": "China Airlines Airbus A350-900",\n+        "subject": "China Airlines Airbus A350-900",\n         "photoYear": 2026\n       },\n-      "checked_utc": "2026-09-10T08:16:48.824123+00:00"\n+      "checked_utc": "2026-09-10T14:04:56+00:00"\n     },\n     "a-20260910-0755-netherlands-signs-letter-of-intent-to-ac": {\n       "status": "none",\n@@ -79,24 +79,6 @@\n             "photoYear": 2012,\n         },\n     },\n-    {\n-        "name": "china-airlines",\n-        "patterns": (\n-            re.compile(r"^(?:華航|中華航空)"),\n-            re.compile(r"^China Airlines\b", re.I),\n-        ),\n-        "image": {\n-            "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail",\n-            "link": "https://commons.wikimedia.org/wiki/File:China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg",\n-            "credit": "4300streetcar",\n-            "license": "CC BY 4.0",\n-            "provider": "Wikimedia Commons",\n-            "kind": "file_photo",\n-            "matched": "topic:china-airlines",\n-            "subject": "China Airlines Boeing 737-800",\n-            "photoYear": 2026,\n-        },\n-    },\n     {\n         "name": "f16",\n         "patterns": (\n@@ -222,6 +222,11 @@ def rejection_reason(article, raw, captions=None):\n     if airline:\n         if not any(phrase(alias, ev) for alias in images._airline_aliases(airline)):\n             return "airline-mismatch"\n+        if stock and not model:\n+            quality_reason = images.profiled_airline_stock_reason(\n+                article, airline, ev)\n+            if quality_reason:\n+                return quality_reason\n         if not model and im.get("provider") == "Wikimedia Commons":\n             try:\n                 if not images._article_year(article)-10 <= int(im.get("photoYear")) <= images._article_year(article):\n@@ -84,6 +84,7 @@\n MAX_ARTICLE_AGE_DAYS = 7  # scan only the user-facing seven-day news window\n GENERIC_AIRLINE_MAX_PHOTO_AGE_YEARS = 10\n GENERIC_ORG_MAX_PHOTO_AGE_YEARS = 10\n+MAX_PROFILE_MODEL_QUERIES = 2\n \n \n def _retry_after_hours(attempts: int) -> int:\n@@ -183,6 +184,19 @@ def _retry_after_hours(attempts: int) -> int:\n                 .get("airports") or [])\n     if isinstance(a, dict)\n ]\n+_visual_profiles = {\n+    str(row.get("airline") or "").casefold(): row\n+    for row in (load_json(ROOT / "config" / "airline_visual_profiles.json", {})\n+                .get("profiles") or [])\n+    if isinstance(row, dict) and row.get("airline")\n+}\n+\n+_CARGO_VISUAL_RE = re.compile(\n+    r"貨機|貨運|航空貨運|全貨機|客改貨|\bcargo\b|\bfreight(?:er)?\b", re.I)\n+_REGIONAL_VISUAL_RE = re.compile(\n+    r"國內線|區域航線|短程航線|離島航線|\bdomestic\b|\bregional\b|\bshort[- ]haul\b",\n+    re.I,\n+)\n \n \n def _article_text(article: dict) -> str:\n@@ -204,6 +218,58 @@ def _article_text(article: dict) -> str:\n     return " ".join(parts)\n \n \n+def airline_visual_profile(airline: str | None) -> dict | None:\n+    """Return a verified profile; absence must never be filled by guessing."""\n+    return _visual_profiles.get(str(airline or "").casefold())\n+\n+\n+def airline_visual_role(article: dict) -> str:\n+    """Classify only broad roles that are safe for a generic airline photo."""\n+    lead = f"{article_headline_text(article)} {article_lead_text(article)}"\n+    if _CARGO_VISUAL_RE.search(lead):\n+        return "cargo"\n+    if _REGIONAL_VISUAL_RE.search(lead):\n+        return "regional"\n+    return "flagship"\n+\n+\n+def preferred_airline_models(article: dict, airline: str | None) -> list[str]:\n+    """Verified representative models for an airline-only story.\n+\n+    Exact article-named models bypass this function.  A cargo story with no\n+    verified cargo profile deliberately returns no passenger substitute.\n+    """\n+    profile = airline_visual_profile(airline)\n+    if not profile:\n+        return []\n+    role = airline_visual_role(article)\n+    values = profile.get(role) or (\n+        [] if role == "cargo" else profile.get("flagship"))\n+    return [str(value).strip() for value in values or [] if str(value).strip()]\n+\n+\n+def profiled_airline_stock_reason(article: dict, airline: str,\n+                                  evidence_text: str) -> str | None:\n+    """Reject a generic stock photo outside the airline's verified role."""\n+    if find_aircraft_type(article):\n+        return None\n+    profile = airline_visual_profile(airline)\n+    if not profile:\n+        return None\n+    from image_selection import model_matches\n+    role = airline_visual_role(article)\n+    allowed = preferred_airline_models(article, airline)\n+    if role == "cargo" and not allowed:\n+        return "airline-cargo-profile-unavailable"\n+    if any(model_matches(str(model), evidence_text)\n+           for model in profile.get("excluded_generic") or []):\n+        return "nonrepresentative-airline-stock"\n+    if allowed and not any(model_matches(model, evidence_text)\n+                           for model in allowed):\n+        return "airline-role-mismatch"\n+    return None\n+\n+\n def find_registration(article: dict) -> str | None:\n     lead = article_lead_text(article)\n     entities = article.get("entities") or {}\n@@ -552,7 +618,8 @@ def lookup_commons(query: str, require_tokens: list[str],\n                    subject: str | None = None,\n                    require_all: bool = False,\n                    min_year: int | None = None,\n-                   prefer_recent: bool = False,\n+                   prefer_recent: bool = False, prefer_landscape: bool = False,\n+                   required_model: str | None = None,\n                    reject_title_re=None, article=None, usage=None) -> dict | None:\n     """First freely-licensed Commons bitmap matching the query.\n \n@@ -568,7 +635,7 @@ def lookup_commons(query: str, require_tokens: list[str],\n                             "gsrlimit": 20,\n                             "gsrsearch": f"filetype:bitmap {query}",\n                             "prop": "imageinfo",\n-                            "iiprop": "url|extmetadata",\n+                            "iiprop": "url|size|extmetadata",\n                             "iiurlwidth": 1280,\n                         })\n     if resp.status_code != 200:\n@@ -587,6 +654,10 @@ def lookup_commons(query: str, require_tokens: list[str],\n         if tokens and (not all(token_hits) if require_all\n                        else not any(token_hits)):\n             continue\n+        if required_model:\n+            from image_selection import model_matches\n+            if not model_matches(required_model, title):\n+                continue\n         for info in page.get("imageinfo") or []:\n             meta = info.get("extmetadata") or {}\n             license_name = _strip_html(\n@@ -620,11 +691,20 @@ def lookup_commons(query: str, require_tokens: list[str],\n             count = (usage or {}).get(image_key(candidate), 0)\n             if count >= MAX_STOCK_REUSE:\n                 continue\n-            if not prefer_recent and not usage:\n+            if not prefer_recent and not prefer_landscape and not usage:\n                 return candidate\n-            candidates.append((-count, photo_year or 0 if prefer_recent else 0,\n+            try:\n+                width = int(info.get("width") or 0)\n+                height = int(info.get("height") or 0)\n+                ratio = width / height if height else 0\n+            except (TypeError, ValueError, ZeroDivisionError):\n+                width, ratio = 0, 0\n+            landscape = int(1.25 <= ratio <= 2.5) if prefer_landscape else 0\n+            resolution = min(width, 4000) if prefer_landscape else 0\n+            candidates.append((-count, landscape, resolution,\n+                               photo_year or 0 if prefer_recent else 0,\n                                -int(page.get("index", 99)), candidate))\n-    return max(candidates, key=lambda row: row[:3])[3] if candidates else None\n+    return max(candidates, key=lambda row: row[:-1])[-1] if candidates else None\n \n \n def resolve_image(article: dict, *, usage=None, diagnostics=None) -> dict | None:\n@@ -676,11 +756,33 @@ def commons(query, tokens, **kwargs):\n         # within A350-900); an exact phrase prefilter would discard them first.\n         photo = commons(query, [airline] if airline else [],\n                         subject=query.removesuffix(" aircraft"), require_all=True,\n+                        prefer_landscape=True,\n                         reject_title_re=BAD_AIRCRAFT_TITLE_RE)\n     elif airline:\n-        photo = commons(f"{airline} aircraft exterior", [airline], subject=airline,\n-                        min_year=_article_year(article)-GENERIC_AIRLINE_MAX_PHOTO_AGE_YEARS,\n-                        prefer_recent=True, reject_title_re=_BAD_AIRLINE_IMAGE_RE)\n+        profile = airline_visual_profile(airline)\n+        role = airline_visual_role(article)\n+        preferred = preferred_airline_models(article, airline)\n+        for representative in preferred[:MAX_PROFILE_MODEL_QUERIES]:\n+            photo = commons(\n+                f"{airline} {representative}", [airline],\n+                subject=f"{airline} {representative}", require_all=True,\n+                required_model=representative,\n+                min_year=_article_year(article)-GENERIC_AIRLINE_MAX_PHOTO_AGE_YEARS,\n+                prefer_recent=True, prefer_landscape=True,\n+                reject_title_re=_BAD_AIRLINE_IMAGE_RE)\n+            if photo:\n+                break\n+        # Unprofiled cargo carriers still receive a cargo-specific query.  A\n+        # profiled passenger airline with no verified cargo fleet fails closed.\n+        if not preferred and (role != "cargo" or not profile):\n+            generic_query = (f"{airline} cargo aircraft"\n+                             if role == "cargo"\n+                             else f"{airline} aircraft exterior")\n+            photo = commons(\n+                generic_query, [airline], subject=airline,\n+                min_year=_article_year(article)-GENERIC_AIRLINE_MAX_PHOTO_AGE_YEARS,\n+                prefer_recent=True, prefer_landscape=True,\n+                reject_title_re=_BAD_AIRLINE_IMAGE_RE)\n     else:\n         org = find_org(article)\n         if org:\n@@ -57,15 +57,11 @@ def main() -> int:\n     assert english is not None\n     assert english["matched"] == "topic:drone"\n \n-    # A China Airlines headline gets an aircraft file photo, not a cabin or\n-    # lounge image; a body-only comparison mention must not trigger it.\n-    china = image_fallbacks.topic_image(\n+    # Airlines use the shared verified fleet-profile search.  Topic fallbacks\n+    # must not give one carrier a permanently hard-coded aircraft photo.\n+    assert image_fallbacks.topic_image(\n         article("華航籌集公益款項捐贈團體", "中華航空公布會員哩程捐贈成果")\n-    )\n-    assert china is not None\n-    assert china["matched"] == "topic:china-airlines"\n-    assert china["subject"] == "China Airlines Boeing 737-800"\n-    assert "Boeing_737-800" in china["url"]\n+    ) is None\n     assert image_fallbacks.topic_image(\n         article("星宇航空公布新航線", "華航也在同篇背景資料中被提及")\n     ) is None\n@@ -26,6 +26,18 @@ def photo(filename, **extra):\n \n \n class ImageSelectionTests(unittest.TestCase):\n+    def test_visual_profiles_are_auditable_and_not_a_single_airline_exception(self):\n+        profiles = list(images._visual_profiles.values())\n+        self.assertGreaterEqual(len(profiles), 9)\n+        for profile in profiles:\n+            with self.subTest(airline=profile.get("airline")):\n+                self.assertTrue(str(profile.get("source") or "").startswith("https://"))\n+                self.assertRegex(str(profile.get("verified_utc") or ""), r"^20\d\d-")\n+                self.assertIsInstance(profile.get("flagship"), list)\n+                self.assertFalse(\n+                    set(profile.get("flagship") or [])\n+                    & set(profile.get("excluded_generic") or []))\n+\n     def test_medical_logistics_cannot_use_consumer_quadcopter(self):\n         im = photo("Quadcopter_Drone_in_flight", matched="topic:drone")\n         for title in ("JEDSY medical logistics drone partnership",\n@@ -107,6 +119,62 @@ def test_topic_rule_does_not_override_model(self):\n         a = article("China Airlines A350-1000 delivery")\n         self.assertIsNone(image_fallbacks.topic_image(a, allow_airport_lookup=False))\n \n+    def test_airline_profiles_apply_the_same_role_rules_across_carriers(self):\n+        cases = (\n+            ("China Airlines reports annual earnings", "Airbus A350-900"),\n+            ("EVA Air reports annual earnings", "Boeing 787-10"),\n+            ("STARLUX Airlines reports annual earnings", "Airbus A350-1000"),\n+            ("Delta Air Lines reports annual earnings", "Airbus A350-900"),\n+        )\n+        for title, expected in cases:\n+            a = article(title, entities={"airlines": [title.split(" reports")[0]]})\n+            airline = images.find_airline(a)\n+            self.assertEqual(images.preferred_airline_models(a, airline)[0], expected)\n+\n+    def test_airline_profiles_separate_cargo_from_passenger_images(self):\n+        a = article("China Airlines cargo revenue rises",\n+                    entities={"airlines": ["China Airlines"]})\n+        self.assertEqual(images.airline_visual_role(a), "cargo")\n+        self.assertEqual(images.preferred_airline_models(a, "China Airlines")[:2],\n+                         ["Boeing 777F", "Boeing 747-400F"])\n+        self.assertEqual(\n+            images.profiled_airline_stock_reason(\n+                a, "China Airlines", "China Airlines Airbus A350-900"),\n+            "airline-role-mismatch")\n+\n+    def test_nonrepresentative_generic_stock_is_rejected_but_exact_story_is_not(self):\n+        generic = article("China Airlines reports annual earnings",\n+                          entities={"airlines": ["China Airlines"]})\n+        old = photo("China_Airlines_Boeing_737-800")\n+        self.assertEqual(policy.rejection_reason(generic, old),\n+                         "nonrepresentative-airline-stock")\n+        exact = article("China Airlines Boeing 737-800 maintenance update",\n+                        entities={"airlines": ["China Airlines"],\n+                                  "aircraft_models": ["Boeing 737-800"]})\n+        self.assertIsNone(policy.rejection_reason(exact, old))\n+\n+    def test_unprofiled_airline_is_not_guessed(self):\n+        a = article("Air Astana reports annual earnings",\n+                    entities={"airlines": ["Air Astana"]})\n+        self.assertEqual(images.preferred_airline_models(a, "Air Astana"), [])\n+\n+    def test_published_china_airlines_737_photos_require_a_737_story(self):\n+        root = Path(__file__).resolve().parents[1] / "data" / "articles"\n+        violations = []\n+        for path in root.glob("*.json"):\n+            batch = json.loads(path.read_text(encoding="utf-8"))\n+            for row in batch.get("articles") or []:\n+                image = row.get("image") or {}\n+                if not isinstance(image, dict):\n+                    continue\n+                identity = f"{image.get('url', '')} {image.get('subject', '')}"\n+                if "China_Airlines_Boeing_737" not in identity and \\n+                        "China Airlines Boeing 737" not in identity:\n+                    continue\n+                if "737" not in images.article_headline_text(row):\n+                    violations.append(row.get("id"))\n+        self.assertEqual(violations, [])\n+\n     def test_captioned_rss_wrong_carrier_is_rejected(self):\n         a = article("Alaska Airlines sued", entities={"airlines": ["Alaska Airlines"]}, sources=[{"url": "https://publisher.test/story"}])\n         url = "https://cdn.test/photo.jpg"\n@@ -197,6 +265,28 @@ def json(self): return payload\n                                           article=a, require_all=True, usage={policy.image_key(first): 1})\n         self.assertEqual(found["url"], second["url"])\n \n+    def test_landscape_aircraft_photo_beats_portrait_result(self):\n+        a = article("Delta Air Lines Airbus A350")\n+        payload = {"query": {"pages": {\n+            "1": {"index": 0, "title": "File:Delta Air Lines Airbus A350-941 portrait.jpg",\n+                  "imageinfo": [{"thumburl": "https://upload.wikimedia.org/portrait.jpg",\n+                                 "width": 1200, "height": 1800,\n+                                 "extmetadata": {"LicenseShortName": {"value": "CC BY 4.0"}}}]},\n+            "2": {"index": 1, "title": "File:Delta Air Lines Airbus A350-941 landscape.jpg",\n+                  "imageinfo": [{"thumburl": "https://upload.wikimedia.org/landscape.jpg",\n+                                 "width": 2400, "height": 1400,\n+                                 "extmetadata": {"LicenseShortName": {"value": "CC BY 4.0"}}}]},\n+        }}}\n+        class Response:\n+            status_code = 200\n+            def json(self): return payload\n+        with patch.object(images.requests, "get", return_value=Response()):\n+            found = images.lookup_commons(\n+                "Delta Air Lines Airbus A350", ["Delta Air Lines"],\n+                article=a, require_all=True, required_model="Airbus A350-900",\n+                prefer_landscape=True)\n+        self.assertEqual(found["url"], "https://upload.wikimedia.org/landscape.jpg")\n+\n     def test_failed_provider_continues_to_next(self):\n         a = article("Delta Air Lines Airbus A350")\n         im = photo("Delta_Air_Lines_Airbus_A350")\n@@ -344,10 +344,11 @@ def reset():\n     "a-generic-china", "China Airlines provides relief flight assistance")\n generic_china_story["entities"]["airlines"] = ["China Airlines"]\n old_generic_airline_image = {\n-    "url": "https://upload.wikimedia.org/China_Airlines_Boeing_737-800.jpg",\n+    "url": "https://upload.wikimedia.org/China_Airlines_Airbus_A350-900.jpg",\n     "link": "https://commons.wikimedia.org/w/old",\n     "provider": "Wikimedia Commons", "kind": "file_photo",\n-    "matched": "China Airlines aircraft", "subject": "China Airlines",\n+    "matched": "China Airlines Airbus A350-900",\n+    "subject": "China Airlines Airbus A350-900",\n }\n fresh_generic_airline_image = dict(old_generic_airline_image,\n                                    photoYear=now_utc().year)

You found a secret theme!

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