This commit is contained in:
MrARM
2026-07-10 22:39:27 -05:00
parent b2c2355a06
commit 717cc7d94f
11 changed files with 21334 additions and 67 deletions

View File

@@ -10,6 +10,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "viewer.html"
SOUND_INDEX_OUT = ROOT / "sound-index.json"
def read_text(path: Path) -> str:
@@ -23,6 +24,23 @@ def title_for(path: Path, text: str) -> str:
return path.stem.replace("-", " ").replace("_", " ").title()
def content_group_for(path: Path) -> str:
"""Separate narrative research from generated catalogs and galleries."""
name = path.name
if path.suffix != ".md":
return "source"
if name.startswith("sound-best-named-gallery-") or name in {
"sound-asr-catalog-labels.md",
"sound-asr-draft-labels.md",
"sound-asr-review-candidates.md",
"sound-pokedex-voice.md",
}:
return "voice"
if re.match(r"^(asset-.+-gallery|radium-section3-bitmap-gallery|sound-gallery)-", name):
return "media"
return "wiki"
def page_order() -> list[str]:
readme = read_text(ROOT / "README.md")
ordered = ["README.md"]
@@ -53,12 +71,48 @@ def build_pages() -> list[dict[str, str]]:
"path": name,
"title": title_for(path, text),
"kind": kind,
"group": content_group_for(path),
"text": text,
}
)
return pages
def parse_directive_attrs(raw: str) -> dict[str, str]:
return dict(re.findall(r'([A-Za-z0-9_-]+)="([^"]*)"', raw))
def build_sound_index() -> dict[str, object]:
"""Build one canonical entry for every best-named main/menu sound."""
sounds: list[dict[str, str]] = []
source_pages: list[str] = []
seen_sources: set[str] = set()
for path in sorted(ROOT.glob("sound-best-named-gallery-*.md")):
source_pages.append(path.name)
for match in re.finditer(r"^::audio\{(.*)\}\s*$", read_text(path), re.MULTILINE):
attrs = parse_directive_attrs(match.group(1))
src = attrs.get("src", "")
if not src or src in seen_sources:
continue
seen_sources.add(src)
sounds.append(
{
"id": f"sound-{len(sounds) + 1:04d}",
"src": src,
"filename": Path(src).name,
"title": attrs.get("title", Path(src).name),
"meta": attrs.get("meta", ""),
"source_page": path.name,
}
)
return {
"schema_version": 1,
"generated_from": source_pages,
"count": len(sounds),
"sounds": sounds,
}
HTML_TEMPLATE = r"""<!doctype html>
<html lang="en">
<head>
@@ -161,7 +215,7 @@ HTML_TEMPLATE = r"""<!doctype html>
.sidebar-stats {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
margin: 14px 0 16px;
}
@@ -367,6 +421,22 @@ HTML_TEMPLATE = r"""<!doctype html>
min-width: 0;
}
.search-controls {
display: grid;
grid-template-columns: minmax(0, 1fr) 116px;
gap: 7px;
}
.search-controls select {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
color: var(--ink);
padding: 0 8px;
min-height: 38px;
outline: none;
}
.search-results {
position: absolute;
z-index: 20;
@@ -384,17 +454,79 @@ HTML_TEMPLATE = r"""<!doctype html>
.search-results.open { display: block; }
.search-results a {
.search-results > a {
display: block;
padding: 10px 12px;
border-bottom: 1px solid var(--line);
color: var(--ink);
}
.search-results a:last-child { border-bottom: 0; }
.search-results a:hover { background: var(--panel-soft); text-decoration: none; }
.search-results > a:last-child { border-bottom: 0; }
.search-results > a:hover { background: var(--panel-soft); text-decoration: none; }
.search-results small { display: block; color: var(--muted); margin-top: 2px; }
.sound-search-summary {
position: sticky;
top: 0;
z-index: 1;
border-bottom: 1px solid var(--line);
background: #fff;
color: var(--muted);
padding: 9px 12px;
font-size: 12px;
}
.sound-search-summary strong { color: var(--ink); }
.sound-result {
border-bottom: 1px solid var(--line);
padding: 11px 12px 12px;
}
.sound-result:last-child { border-bottom: 0; }
.sound-result > strong {
display: block;
line-height: 1.35;
overflow-wrap: anywhere;
}
.sound-result audio {
display: block;
width: 100%;
height: 34px;
margin: 9px 0 7px;
}
.sound-result-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-width: 0;
}
.sound-result-actions code {
color: var(--quiet);
font-size: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sound-download {
flex: 0 0 auto;
border: 1px solid var(--line);
border-radius: 7px;
background: var(--panel-soft);
color: var(--accent);
padding: 5px 8px;
font-size: 11px;
font-weight: 700;
}
.sound-download:hover { background: var(--accent-soft); text-decoration: none; }
.article-layout {
display: grid;
grid-template-columns: minmax(0, 960px) 280px;
@@ -776,6 +908,7 @@ HTML_TEMPLATE = r"""<!doctype html>
.tabs { grid-column: 1 / -1; width: 100%; justify-content: center; }
.tab { flex: 1; }
.search-wrap { grid-column: 1 / -1; }
.search-controls { grid-template-columns: minmax(0, 1fr) 108px; }
.article-layout {
display: flex;
flex-direction: column;
@@ -804,14 +937,23 @@ HTML_TEMPLATE = r"""<!doctype html>
</span>
</a>
<div class="sidebar-stats" aria-label="Wiki counts">
<div class="stat"><strong id="page-count">0</strong><span>pages</span></div>
<div class="stat"><strong id="artifact-count">0</strong><span>artifacts</span></div>
<div class="stat"><strong id="page-count">0</strong><span>research</span></div>
<div class="stat"><strong id="voice-count">0</strong><span>voice</span></div>
<div class="stat"><strong id="media-count">0</strong><span>media</span></div>
</div>
<input class="nav-filter" id="nav-filter" type="search" autocomplete="off" placeholder="Filter pages">
<nav class="side-section" aria-label="Wiki pages">
<p class="side-heading">Pages <span id="visible-page-count"></span></p>
<nav class="side-section" aria-label="Research pages">
<p class="side-heading">Research <span id="visible-page-count"></span></p>
<ul class="page-list" id="page-list"></ul>
</nav>
<nav class="side-section" aria-label="Voice line catalogs">
<p class="side-heading">Voice lines <span id="visible-voice-count"></span></p>
<ul class="page-list" id="voice-list"></ul>
</nav>
<nav class="side-section" aria-label="Media galleries">
<p class="side-heading">Media galleries <span id="visible-media-count"></span></p>
<ul class="page-list" id="media-list"></ul>
</nav>
<nav class="side-section" aria-label="Generated artifacts">
<p class="side-heading">Generated files <span id="visible-artifact-count"></span></p>
<ul class="page-list" id="artifact-list"></ul>
@@ -830,7 +972,16 @@ HTML_TEMPLATE = r"""<!doctype html>
<button class="tab" id="source-tab" type="button">Source</button>
</div>
<div class="search-wrap">
<input id="search" type="search" autocomplete="off" placeholder="Search the wiki">
<div class="search-controls">
<input id="search" type="search" autocomplete="off" placeholder="Search research pages">
<select id="search-scope" aria-label="Search scope">
<option value="wiki">Research</option>
<option value="sounds">Sounds</option>
<option value="voice">Voice pages</option>
<option value="media">Media</option>
<option value="all">Everything</option>
</select>
</div>
<div class="search-results" id="search-results"></div>
</div>
</div>
@@ -860,6 +1011,8 @@ HTML_TEMPLATE = r"""<!doctype html>
let headingIndex = [];
const pageList = document.getElementById("page-list");
const voiceList = document.getElementById("voice-list");
const mediaList = document.getElementById("media-list");
const artifactList = document.getElementById("artifact-list");
const articleView = document.getElementById("article-view");
const tocView = document.getElementById("toc");
@@ -867,13 +1020,17 @@ HTML_TEMPLATE = r"""<!doctype html>
const currentTitle = document.getElementById("current-title");
const currentPathEl = document.getElementById("current-path");
const pageCount = document.getElementById("page-count");
const artifactCount = document.getElementById("artifact-count");
const voiceCount = document.getElementById("voice-count");
const mediaCount = document.getElementById("media-count");
const visiblePageCount = document.getElementById("visible-page-count");
const visibleVoiceCount = document.getElementById("visible-voice-count");
const visibleMediaCount = document.getElementById("visible-media-count");
const visibleArtifactCount = document.getElementById("visible-artifact-count");
const navFilter = document.getElementById("nav-filter");
const mobileNav = document.getElementById("mobile-nav");
const scrim = document.getElementById("scrim");
const searchInput = document.getElementById("search");
const searchScope = document.getElementById("search-scope");
const searchResults = document.getElementById("search-results");
const readTab = document.getElementById("read-tab");
const sourceTab = document.getElementById("source-tab");
@@ -898,6 +1055,8 @@ HTML_TEMPLATE = r"""<!doctype html>
function kindLabel(page) {
if (page.kind !== "markdown") return page.path.split(".").pop() || "file";
if (page.group === "voice") return "voice";
if (page.group === "media") return "gallery";
if (/gallery|asset|sound|radium/i.test(page.path)) return "asset";
if (/conagent|emulator|behavior|package|firmware/i.test(page.path)) return "re";
return "note";
@@ -1234,11 +1393,16 @@ HTML_TEMPLATE = r"""<!doctype html>
}
function renderNavigation() {
const pages = wiki.pages.filter((page) => page.kind === "markdown");
const artifacts = wiki.pages.filter((page) => page.kind !== "markdown");
const pages = wiki.pages.filter((page) => page.group === "wiki");
const voices = wiki.pages.filter((page) => page.group === "voice");
const media = wiki.pages.filter((page) => page.group === "media");
const artifacts = wiki.pages.filter((page) => page.group === "source");
pageCount.textContent = pages.length.toLocaleString();
artifactCount.textContent = artifacts.length.toLocaleString();
voiceCount.textContent = voices.length.toLocaleString();
mediaCount.textContent = media.length.toLocaleString();
pageList.innerHTML = pages.map(renderNavItem).join("");
voiceList.innerHTML = voices.map(renderNavItem).join("");
mediaList.innerHTML = media.map(renderNavItem).join("");
artifactList.innerHTML = artifacts.map(renderNavItem).join("");
filterNavigation(navFilter.value);
}
@@ -1246,30 +1410,77 @@ HTML_TEMPLATE = r"""<!doctype html>
function filterNavigation(query) {
const q = query.trim().toLowerCase();
let pagesVisible = 0;
let voicesVisible = 0;
let mediaVisible = 0;
let artifactsVisible = 0;
pageList.querySelectorAll("li").forEach((item) => {
const show = !q || item.dataset.title.includes(q);
item.classList.toggle("hidden", !show);
if (show) pagesVisible++;
});
voiceList.querySelectorAll("li").forEach((item) => {
const show = !q || item.dataset.title.includes(q);
item.classList.toggle("hidden", !show);
if (show) voicesVisible++;
});
mediaList.querySelectorAll("li").forEach((item) => {
const show = !q || item.dataset.title.includes(q);
item.classList.toggle("hidden", !show);
if (show) mediaVisible++;
});
artifactList.querySelectorAll("li").forEach((item) => {
const show = !q || item.dataset.title.includes(q);
item.classList.toggle("hidden", !show);
if (show) artifactsVisible++;
});
visiblePageCount.textContent = pagesVisible.toLocaleString();
visibleVoiceCount.textContent = voicesVisible.toLocaleString();
visibleMediaCount.textContent = mediaVisible.toLocaleString();
visibleArtifactCount.textContent = artifactsVisible.toLocaleString();
}
function search(query) {
const q = query.trim().toLowerCase();
const scope = searchScope.value;
if (!q) {
searchResults.classList.remove("open");
searchResults.innerHTML = "";
return;
}
if (scope === "sounds") {
const terms = q.split(/\s+/).filter(Boolean);
const matches = wiki.sounds.map((sound) => {
const title = sound.title.toLowerCase();
const filename = sound.filename.toLowerCase();
const haystack = `${title} ${filename} ${sound.meta.toLowerCase()}`;
if (!terms.every((term) => haystack.includes(term))) return null;
let score = 0;
if (title === q) score += 100;
if (title.startsWith(q)) score += 40;
if (title.includes(q)) score += 20;
if (filename.includes(q)) score += 10;
return { sound, score };
}).filter(Boolean);
matches.sort((a, b) => b.score - a.score || a.sound.title.localeCompare(b.sound.title));
const cards = matches.slice(0, 24).map(({ sound }) => (
`<div class="sound-result" data-sound-id="${escapeAttr(sound.id)}">
<strong>${escapeHtml(sound.title)}</strong>
<small>${escapeHtml(sound.meta)}</small>
<audio controls preload="none" src="${escapeAttr(sound.src)}"></audio>
<div class="sound-result-actions">
<a class="sound-download" href="${escapeAttr(sound.src)}" download="${escapeAttr(sound.filename)}">Download WAV</a>
<code title="${escapeAttr(sound.filename)}">${escapeHtml(sound.filename)}</code>
</div>
</div>`
)).join("");
searchResults.innerHTML = `<div class="sound-search-summary"><strong>${matches.length.toLocaleString()}</strong> matches in ${wiki.sounds.length.toLocaleString()} indexed sounds${matches.length > 24 ? "; showing the first 24" : ""}.</div>` +
(cards || '<div class="sound-result"><strong>No matching sounds</strong><small>Try a character, callout phrase, object number, source, or filename.</small></div>');
searchResults.classList.add("open");
return;
}
const hits = [];
for (const page of wiki.pages) {
if (scope !== "all" && page.group !== scope) continue;
const titleHit = page.title.toLowerCase().includes(q);
const idx = page.text.toLowerCase().indexOf(q);
if (!titleHit && idx === -1) continue;
@@ -1282,6 +1493,7 @@ HTML_TEMPLATE = r"""<!doctype html>
}
hits.push({ page, snippet, titleHit });
}
hits.sort((a, b) => Number(b.titleHit) - Number(a.titleHit) || a.page.title.localeCompare(b.page.title));
searchResults.innerHTML = hits.slice(0, 12).map(({ page, snippet }) => (
`<a href="#/${escapeAttr(page.path)}"><strong>${escapeHtml(page.title)}</strong><small>${snippet}</small></a>`
)).join("") || '<a href="#/README.md"><strong>No matches</strong><small>Try a path, hash, service name, or binary name.</small></a>';
@@ -1295,8 +1507,14 @@ HTML_TEMPLATE = r"""<!doctype html>
mobileNav.addEventListener("click", () => setNavOpen(!document.body.classList.contains("nav-open")));
scrim.addEventListener("click", () => setNavOpen(false));
searchInput.addEventListener("input", (event) => search(event.target.value));
searchScope.addEventListener("change", () => {
const labels = { wiki: "research pages", sounds: "all sounds", voice: "voice pages", media: "media galleries", all: "everything" };
searchInput.placeholder = `Search ${labels[searchScope.value]}`;
search(searchInput.value);
});
searchInput.addEventListener("keydown", (event) => {
if (event.key !== "Enter") return;
if (searchScope.value === "sounds") return;
const first = searchResults.querySelector("a");
if (first) first.click();
});
@@ -1315,9 +1533,12 @@ HTML_TEMPLATE = r"""<!doctype html>
def main() -> None:
data = {"pages": build_pages()}
sound_index = build_sound_index()
SOUND_INDEX_OUT.write_text(json.dumps(sound_index, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
data = {"pages": build_pages(), "sounds": sound_index["sounds"]}
encoded = json.dumps(data, ensure_ascii=False, separators=(",", ":")).replace("</", "<\\/")
OUT.write_text(HTML_TEMPLATE.replace("__WIKI_DATA__", encoded), encoding="utf-8")
print(f"wrote {SOUND_INDEX_OUT.relative_to(ROOT.parent)} ({sound_index['count']} sounds)")
print(f"wrote {OUT.relative_to(ROOT.parent)}")