use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use anyhow::Result;
use crate::config::Config;
use crate::project::ProjectLayout;
use crate::prose::resolve_prose_language;
use crate::store::NodeKind;
use crate::store::hierarchy::Hierarchy;
use crate::store::node::Node;
use super::detect::{DetectWindows, detect_chapter};
use super::store::TheologianStore;
fn chapters_of<'a>(h: &'a Hierarchy, book: &Node) -> Vec<&'a Node> {
h.children_of(Some(book.id))
.into_iter()
.filter(|n| n.kind == NodeKind::Chapter)
.collect()
}
fn chapter_paragraphs(layout: &ProjectLayout, h: &Hierarchy, chapter_id: uuid::Uuid) -> Vec<(String, String)> {
let mut out = Vec::new();
for id in h.collect_subtree(chapter_id) {
let Some(p) = h.get(id) else { continue };
if p.kind != NodeKind::Paragraph || p.content_type.as_deref() == Some("jinja") {
continue;
}
if let Some(rel) = p.file.as_ref() {
if let Ok(raw) = std::fs::read_to_string(layout.root.join(rel)) {
out.push((id.to_string(), crate::audiobook::typst_to_plain(&raw)));
}
}
}
out
}
fn hash_str(s: &str) -> u64 {
let mut h = std::collections::hash_map::DefaultHasher::new();
s.hash(&mut h);
h.finish()
}
pub(crate) fn run_fast_scan(
store: &TheologianStore,
layout: &ProjectLayout,
h: &Hierarchy,
cfg: &Config,
book: &Node,
win: DetectWindows,
sacred_levity: bool,
) -> Result<usize> {
let (lang, _note) = resolve_prose_language(None, &cfg.language);
let roster = crate::character::character_names(h);
let now = chrono::Utc::now().to_rfc3339();
let mut count = 0;
for (idx, ch) in chapters_of(h, book).iter().enumerate() {
let ord = (idx + 1) as u32;
let paras = chapter_paragraphs(layout, h, ch.id);
let findings = detect_chapter(ord, ¶s, &roster, &lang, win, sacred_levity);
store.clear_chapter(&book.slug, ord)?;
let hashes: HashMap<&str, u64> =
paras.iter().map(|(id, t)| (id.as_str(), hash_str(t))).collect();
for f in &findings {
let th = hashes.get(f.para_id.as_str()).copied().unwrap_or(0);
store.upsert_finding(&book.slug, f, th, &now)?;
count += 1;
}
}
Ok(count)
}