mirror of
https://git.pumpin.online/funkpower/catena.git
synced 2026-10-23 03:07:13 +00:00
bindings: the author a record's own text can offer
book-pipeline had its own copy of bibclean's author recovery. Bound on all four surfaces so it can stop. Checked against that Python over all 5,385 books in the library: 5,375 identical. On the other ten the Python had taken a translator, an illustrator or the publisher off a copyright line — Sally-Ann Spencer for *The Dwarves*, Thomas Nelson for a table of contents, C. S. Lewis for a book about Tolkien — and bibclean either finds the real author in the Library of Congress block or says nothing. Every one of the ten was read by hand.
This commit is contained in:
@@ -417,6 +417,35 @@ pub unsafe extern "C" fn catena_identify(
|
||||
give_json(&crate::identify(name, bytes))
|
||||
}
|
||||
|
||||
/// The best author a record's own title and front matter can offer. Returns
|
||||
/// a JSON suggestion, or NULL when neither says anything that reads as a
|
||||
/// name — an answer, not an error.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `title` must be NUL-terminated UTF-8; `front_matter` likewise, or NULL.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn catena_suggest_author(
|
||||
title: *const c_char,
|
||||
front_matter: *const c_char,
|
||||
) -> *mut c_char {
|
||||
clear_error();
|
||||
let Some(title) = (unsafe { borrow(title, "title") }) else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
let front = match front_matter.is_null() {
|
||||
true => None,
|
||||
false => match unsafe { borrow(front_matter, "front_matter") } {
|
||||
Some(front) => Some(front),
|
||||
None => return ptr::null_mut(),
|
||||
},
|
||||
};
|
||||
match crate::suggest_author(title, front) {
|
||||
Some(found) => give_json(&found),
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a chunk of body text carries footnote or endnote citations.
|
||||
/// Returns 1 for yes, 0 for no, -1 on error.
|
||||
///
|
||||
|
||||
@@ -215,6 +215,17 @@ pub fn parse_identifier(text: &str) -> Option<idnorm::Identifier> {
|
||||
idnorm::parse(text)
|
||||
}
|
||||
|
||||
/// The best author a record's own title and front matter can offer.
|
||||
///
|
||||
/// `None` when neither says anything that reads as a person's name, which is
|
||||
/// the honest answer for a scanned book with no title page.
|
||||
pub fn suggest_author(
|
||||
title: &str,
|
||||
front_matter: Option<&str>,
|
||||
) -> Option<bibclean::author::Suggestion> {
|
||||
bibclean::author::best(title, front_matter)
|
||||
}
|
||||
|
||||
/// Whether a chunk of body text carries footnote or endnote citations.
|
||||
///
|
||||
/// The structural signal a converter leaves on EPUB3 note definitions first;
|
||||
@@ -269,6 +280,17 @@ mod tests {
|
||||
assert_eq!(count_tokens("a passage"), textkit::count("a passage"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_author_surface_agrees_with_the_library_underneath() {
|
||||
const TITLE: &str = "Aquinas (Companion)";
|
||||
const FRONT: &str = "THE CAMBRIDGE COMPANION TO AQUINAS\n\nedited by Norman Kretzmann";
|
||||
assert_eq!(
|
||||
suggest_author(TITLE, Some(FRONT)),
|
||||
bibclean::author::best(TITLE, Some(FRONT))
|
||||
);
|
||||
assert!(suggest_author("", None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_footnote_signal_agrees_with_the_library_underneath() {
|
||||
const NOTES: &str = "1. John Smith, *A Title* (Oxford: Clarendon Press, 1998), 183.\n\
|
||||
|
||||
@@ -220,6 +220,22 @@ fn identify<'py>(py: Python<'py>, name: &str, data: &[u8]) -> PyResult<Bound<'py
|
||||
to_py(py, &crate::identify(name, data))
|
||||
}
|
||||
|
||||
/// The best author a record's own title and front matter can offer.
|
||||
///
|
||||
/// `None` when neither says anything that reads as a person's name.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (title, front_matter = None))]
|
||||
fn suggest_author<'py>(
|
||||
py: Python<'py>,
|
||||
title: &str,
|
||||
front_matter: Option<&str>,
|
||||
) -> PyResult<Option<Bound<'py, PyAny>>> {
|
||||
match crate::suggest_author(title, front_matter) {
|
||||
Some(found) => Ok(Some(to_py(py, &found)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a chunk of body text carries footnote or endnote citations.
|
||||
#[pyfunction]
|
||||
fn looks_like_footnote_citations(text: &str) -> bool {
|
||||
@@ -297,6 +313,7 @@ fn _catena(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
module.add_function(wrap_pyfunction!(identify, module)?)?;
|
||||
module.add_function(wrap_pyfunction!(find_citations, module)?)?;
|
||||
module.add_function(wrap_pyfunction!(looks_like_footnote_citations, module)?)?;
|
||||
module.add_function(wrap_pyfunction!(suggest_author, module)?)?;
|
||||
module.add_function(wrap_pyfunction!(citation_prompt, module)?)?;
|
||||
module.add_function(wrap_pyfunction!(merge_citation_reply, module)?)?;
|
||||
module.add_function(wrap_pyfunction!(series_of, module)?)?;
|
||||
|
||||
@@ -97,6 +97,17 @@ export interface Identifier {
|
||||
url?: string;
|
||||
}
|
||||
|
||||
/** A name a record's own text can offer, and how far to trust it. */
|
||||
export interface AuthorSuggestion {
|
||||
author: string;
|
||||
/** Which signal produced it. */
|
||||
method: string;
|
||||
confidence: number;
|
||||
/** The text it was read from, so a person can check it. */
|
||||
evidence: string;
|
||||
isbn: string | null;
|
||||
}
|
||||
|
||||
/** One reference out of a bibliography or a page of notes. */
|
||||
export interface Reference {
|
||||
v: number;
|
||||
@@ -305,6 +316,17 @@ pub fn identify(name: &str, data: &[u8]) -> Result<JsValue, JsValue> {
|
||||
to_js(&crate::identify(name, data))
|
||||
}
|
||||
|
||||
/// The best author a record's own title and front matter can offer.
|
||||
///
|
||||
/// `undefined` when neither says anything that reads as a person's name.
|
||||
#[wasm_bindgen(js_name = suggestAuthor, skip_typescript)]
|
||||
pub fn suggest_author(title: &str, front_matter: Option<String>) -> Result<JsValue, JsValue> {
|
||||
match crate::suggest_author(title, front_matter.as_deref()) {
|
||||
Some(found) => to_js(&found),
|
||||
None => Ok(JsValue::UNDEFINED),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a chunk of body text carries footnote or endnote citations.
|
||||
#[wasm_bindgen(js_name = looksLikeFootnoteCitations)]
|
||||
pub fn looks_like_footnote_citations(text: &str) -> bool {
|
||||
@@ -371,6 +393,7 @@ export function cueWindows(cues: Cue[]): string[];
|
||||
export function splitCollection(markdown: string): Section[];
|
||||
export function fuseHits(hits: Hit[], k?: number, weights?: Record<string, number>, limit?: number): Hit[];
|
||||
export function identify(name: string, data: Uint8Array): Doc;
|
||||
export function suggestAuthor(title: string, front_matter?: string): AuthorSuggestion | undefined;
|
||||
export function findCitations(text: string): Reference[];
|
||||
export function citationPrompt(references: Reference[]): string | undefined;
|
||||
export function mergeCitationReply(references: Reference[], reply: string): Reference[];
|
||||
|
||||
Reference in New Issue
Block a user