Add comprehensive articles on Vascular Dementia and Wallerian Degeneration

- Created a detailed article for Vascular Dementia covering key facts, terminology, imaging findings, differential diagnoses, pathology, clinical issues, and diagnostic checklist.
- Developed an extensive article on Wallerian Degeneration including key facts, terminology, imaging features, differential diagnoses, pathology, clinical issues, and diagnostic checklist.
This commit is contained in:
Ross
2025-10-17 22:11:27 +01:00
parent efbfd301f0
commit 2f8df5f820
104 changed files with 6295 additions and 1469 deletions
@@ -163,11 +163,19 @@ async def run(args):
fname = f"{safe}_{h}_{ts}.json"
body_path = str(output / fname)
save_text(output / fname, pretty)
try:
print(f"{now_ts()}\tSAVED\t{body_path}")
except Exception:
pass
excerpt = pretty[:800]
except Exception:
fname = f"{safe}_{h}_{ts}.txt"
body_path = str(output / fname)
save_text(output / fname, txt)
try:
print(f"{now_ts()}\tSAVED\t{body_path}")
except Exception:
pass
excerpt = txt[:800]
else:
try:
@@ -186,12 +194,20 @@ async def run(args):
fname = f"{safe}_{h}_{ts}.{ext}"
body_path = str(subdir / fname)
save_binary(subdir / fname, data)
try:
print(f"{now_ts()}\tSAVED\t{body_path}")
except Exception:
pass
else:
subdir = output / 'assets'
subdir.mkdir(parents=True, exist_ok=True)
fname = f"{safe}_{h}_{ts}.{ext}"
body_path = str(subdir / fname)
save_binary(subdir / fname, data)
try:
print(f"{now_ts()}\tSAVED\t{body_path}")
except Exception:
pass
except Exception:
# final fallback: try binary
try:
@@ -203,12 +219,20 @@ async def run(args):
fname = f"{safe}_{h}_{ts}.{ext}"
body_path = str(subdir / fname)
save_binary(subdir / fname, data)
try:
print(f"{now_ts()}\tSAVED\t{body_path}")
except Exception:
pass
else:
subdir = output / 'assets'
subdir.mkdir(parents=True, exist_ok=True)
fname = f"{safe}_{h}_{ts}.{ext}"
body_path = str(subdir / fname)
save_binary(subdir / fname, data)
try:
print(f"{now_ts()}\tSAVED\t{body_path}")
except Exception:
pass
except Exception:
# skip if still failing
return
+257 -25
View File
@@ -19,9 +19,12 @@ import json
import os
import re
from datetime import datetime
import shutil
from typing import Iterable
from bs4 import BeautifulSoup, NavigableString, Tag
import html
import hashlib
def text_of(node) -> str:
@@ -279,31 +282,190 @@ def find_title_for_docid(docid: str, search_dir: str) -> str | None:
"""
if not docid:
return None
pattern = os.path.join(search_dir, f"*{docid}*.json")
for path in sorted(glob.glob(pattern)):
# Phase 1: scan all JSON files for breadcrumb/list-style captures and prefer
# their enhancedDocumentName/name when present. Breadcrumb captures often
# don't include the docid in their filename, so scan every JSON once.
all_paths = sorted(glob.glob(os.path.join(search_dir, "*.json")))
for path in all_paths:
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
except Exception:
continue
# direct title
if isinstance(data, list):
for item in data:
try:
if isinstance(item, dict) and item.get("documentId") == docid:
ed = item.get("enhancedDocumentName") or item.get("name")
if isinstance(ed, str) and ed.strip():
return html.unescape(ed.strip())
except Exception:
continue
# Phase 2: look only at files whose filename contains the docid for other
# title signals (top-level names, recursive title keys, searchResults).
pattern = os.path.join(search_dir, f"*{docid}*.json")
paths = sorted(glob.glob(pattern))
for path in paths:
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
except Exception:
continue
# top-level keys commonly used
if isinstance(data, dict):
for key in ("enhancedDocumentName", "documentName", "name"):
if key in data:
v = data.get(key)
if isinstance(v, str) and v.strip():
return html.unescape(v.strip())
# fallback: direct title anywhere in the JSON
title = recursive_search_for_key(data, "title")
if title:
# some files include empty titles; skip blanks
if isinstance(title, str) and title.strip():
return title.strip()
if title and isinstance(title, str) and title.strip():
return html.unescape(title.strip())
# sometimes summary objects contain results with titles
# try 'searchResults' -> 'results' list
sr = data.get("searchResults") if isinstance(data, dict) else None
if isinstance(sr, dict):
results = sr.get("results")
if isinstance(results, list):
for r in results:
if isinstance(r, dict) and r.get("id") == docid and r.get("title"):
return r.get("title")
return html.unescape(r.get("title"))
return None
def find_breadcrumbs_for_docid(docid: str, search_dir: str) -> list:
"""Return a list of breadcrumb strings associated with docid by scanning
captured JSONs. Preserves discovery order and HTML-unescapes values.
"""
if not docid:
return []
def collect_names_from_breadcrumbs(bc_list):
names = []
if isinstance(bc_list, list):
for item in bc_list:
if isinstance(item, dict) and item.get("name"):
names.append(html.unescape(item.get("name")))
return names
# First, prefer files whose filename contains the docid. These often
# include a structured `breadcrumbs` list (ordered parents) and may
# include a leaf node entry for the document.
candidates = []
doc_paths = sorted(glob.glob(os.path.join(search_dir, f"*{docid}*.json")))
for path in doc_paths:
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
except Exception:
continue
# look for a breadcrumbs list anywhere in this JSON
# and prefer the first full list we find
def recurse_find_breadcrumb_lists(obj):
out = []
if isinstance(obj, dict):
for k, v in obj.items():
if k == "breadcrumbs" and isinstance(v, list):
names = collect_names_from_breadcrumbs(v)
if names:
out.append(names)
out.extend(recurse_find_breadcrumb_lists(v))
elif isinstance(obj, list):
for item in obj:
out.extend(recurse_find_breadcrumb_lists(item))
return out
bc_lists = recurse_find_breadcrumb_lists(data)
if bc_lists:
# take the first breadcrumbs list as base path
base = bc_lists[0].copy()
# try to find a leaf name for this doc within the JSON
leaf = None
def recurse_find_leaf(obj):
nonlocal leaf
if leaf:
return
if isinstance(obj, dict):
# node that references the document directly
if obj.get("documentId") == docid and obj.get("name"):
leaf = html.unescape(obj.get("name"))
return
# some files use enhancedDocumentName/documentName
for k in ("enhancedDocumentName", "documentName", "name", "title"):
if k in obj and isinstance(obj[k], str) and obj.get("documentId") == docid:
leaf = html.unescape(obj.get(k))
return
for v in obj.values():
recurse_find_leaf(v)
elif isinstance(obj, list):
for item in obj:
recurse_find_leaf(item)
recurse_find_leaf(data)
if leaf and (not base or base[-1] != leaf):
base.append(leaf)
if base and base not in candidates:
candidates.append(base)
# If we found candidate breadcrumb paths from docid-matching files, return the first
if candidates:
return candidates[0]
# Otherwise, scan all JSONs for entries that explicitly list the docid and
# attempt to reconstruct a breadcrumb path by combining any nearby breadcrumbs
all_paths = sorted(glob.glob(os.path.join(search_dir, "*.json")))
for path in all_paths:
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
except Exception:
continue
# list-of-dicts captures where an item references the documentId
if isinstance(data, list):
for item in data:
if isinstance(item, dict) and item.get("documentId") == docid:
# if this file also contains a breadcrumbs list elsewhere, use it
bc_lists = []
if isinstance(data, dict):
bc_lists = []
# try to locate breadcrumbs within same file by re-loading
def find_bcs(obj):
if isinstance(obj, dict):
if obj.get("breadcrumbs") and isinstance(obj.get("breadcrumbs"), list):
return collect_names_from_breadcrumbs(obj.get("breadcrumbs"))
for v in obj.values():
res = find_bcs(v)
if res:
return res
elif isinstance(obj, list):
for it in obj:
res = find_bcs(it)
if res:
return res
return None
bcs = find_bcs(data)
path_names = bcs or []
# append the item's own enhancedDocumentName/name if present
leaf = item.get("enhancedDocumentName") or item.get("name")
if isinstance(leaf, str) and leaf.strip():
leaf = html.unescape(leaf.strip())
if not path_names or path_names[-1] != leaf:
path_names = path_names + [leaf]
if path_names:
return path_names
return []
def find_title_in_capture_index(docid: str, base_dir: str) -> str | None:
"""Look for a title for docid inside a capture_index.jsonl file in base_dir.
@@ -354,6 +516,9 @@ def process_file(path: str, out_dir: str, overwrite: bool = False) -> tuple[bool
except Exception:
data_peek = None
# deterministic doc id (if present in filename)
docid = None
article_name = None
if isinstance(data_peek, dict):
article_name = find_article_name_in_json(data_peek)
@@ -363,13 +528,16 @@ def process_file(path: str, out_dir: str, overwrite: bool = False) -> tuple[bool
# try to parse a uuid-like docid from filename
m = re.search(r"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})", base)
if m:
docid = m.group(1)
# first consult capture_index.jsonl if present
title_from_index = find_title_in_capture_index(docid, os.path.dirname(path) or ".")
if not title_from_index:
title_from_index = find_title_for_docid(docid, os.path.dirname(path) or ".")
if title_from_index:
article_name = title_from_index
docid = m.group(1)
# Prefer nearby breadcrumb/document captures (which include
# enhancedDocumentName/name) over titles that appear in
# capture_index.jsonl. This makes section-level names
# like "Brain Tumor in Newborn/Infant" the canonical title.
title_from_nearby = find_title_for_docid(docid, os.path.dirname(path) or ".")
if not title_from_nearby:
title_from_nearby = find_title_in_capture_index(docid, os.path.dirname(path) or ".")
if title_from_nearby:
article_name = title_from_nearby
# if still not found, try to extract from inline HTML
if not article_name and isinstance(data_peek, dict):
@@ -381,11 +549,15 @@ def process_file(path: str, out_dir: str, overwrite: bool = False) -> tuple[bool
if article_name:
slug = slugify(article_name)
out_base = f"{slug}_{name}" if slug else name
# prefer deterministic filename based on docid when available to avoid duplicates
if docid:
out_base = f"{slug}_{docid}" if slug else docid
else:
out_base = f"{slug}_{name}" if slug else name
# place titled articles into an articles/ subfolder
target_dir = os.path.join(out_dir, "articles")
if article_name.startswith("https"):
if isinstance(article_name, str) and article_name.startswith("https"):
target_dir = os.path.join(out_dir, "external")
else:
# per request: only save files that have titles
@@ -416,15 +588,79 @@ def process_file(path: str, out_dir: str, overwrite: bool = False) -> tuple[bool
normalized_first = re.sub(r"[#\s]+", "", first_line).strip().lower()
normalized_title = re.sub(r"[^a-z0-9]+", "", article_name.lower())
if not normalized_first or normalized_title not in normalized_first:
md = "# " + article_name.strip() + "\n\n" + md
# Build YAML frontmatter with title and breadcrumbs (and docid)
front_lines = ["---"]
# use JSON quoting to safely escape strings in YAML
front_lines.append(f"title: {json.dumps(article_name.strip())}")
if docid:
front_lines.append(f"docid: {json.dumps(docid)}")
# collect breadcrumbs and render as YAML list if present
crumbs = []
if docid:
crumbs = find_breadcrumbs_for_docid(docid, os.path.dirname(path) or ".")
if crumbs:
front_lines.append("breadcrumbs:")
for c in crumbs:
front_lines.append(f" - {json.dumps(c)}")
front_lines.append("---\n")
front = "\n".join(front_lines)
# Avoid duplicating an H1 that matches the title in the body
md_lines = md.lstrip().splitlines()
if md_lines:
first = md_lines[0].strip()
normalized_first = re.sub(r"[#\s]+", "", first).strip().lower()
normalized_title = re.sub(r"[^a-z0-9]+", "", article_name.lower())
if first.startswith("#") and normalized_title in normalized_first:
# drop the first line (existing H1)
md_lines = md_lines[1:]
md = front + "\n".join(md_lines).lstrip()
except Exception:
# be conservative: if anything goes wrong, keep the original md
pass
# Before writing, compute a normalized content hash to detect duplicates
def normalize_for_hash(s: str) -> str:
# remove ISO8601-like timestamps and common date patterns, collapse whitespace
s2 = re.sub(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:?\d{2})?", "", s)
s2 = re.sub(r"\b\d{1,2}/\d{1,2}/\d{2,4}\b", "", s2)
# remove lines that are just timestamps or contain 'Last updated'
s2 = "\n".join([ln for ln in s2.splitlines() if not re.match(r"^\s*(Last updated|LastVisited|LastVisitedAsDate|Updated).*$", ln, re.I)])
s2 = re.sub(r"\s+", " ", s2).strip()
return s2
def content_hash(s: str) -> str:
nh = normalize_for_hash(s)
return hashlib.sha256(nh.encode("utf-8")).hexdigest()
# index stored at top-level out_dir to dedupe across articles/external
index_path = os.path.join(out_dir, "_content_index.json")
index = {}
try:
if os.path.exists(index_path):
with open(index_path, "r", encoding="utf-8") as ix:
index = json.load(ix)
except Exception:
index = {}
ch = content_hash(md)
existing = index.get(ch)
if existing and os.path.exists(existing) and not overwrite:
# content already saved elsewhere — skip writing
return False, f"duplicate: {existing}"
try:
os.makedirs(target_dir, exist_ok=True)
with open(out_path, "w", encoding="utf-8") as f:
f.write(md)
# update index
try:
index[ch] = out_path
with open(index_path, "w", encoding="utf-8") as ix:
json.dump(index, ix, indent=2, ensure_ascii=False)
except Exception:
pass
# log saved file path with timestamp
try:
log_path = os.path.join(target_dir, "_saved_files.log")
@@ -455,12 +691,8 @@ def main(argv: Iterable[str] | None = None) -> int:
if args.clear and os.path.exists(output_dir):
# remove all .md files in output_dir
for f in glob.glob(os.path.join(output_dir, "*.md")):
try:
os.remove(f)
except Exception:
pass
shutil.rmtree(output_dir, ignore_errors=True)
files = sorted(glob.glob(os.path.join(input_dir, args.pattern)))
if not files:
print(f"No files found in {input_dir} matching {args.pattern}")