mirror of
https://git.pumpin.online/funkpower/catena.git
synced 2026-10-22 18:57:14 +00:00
bindings: cueseg, so a transcript can be chaptered in process
This commit is contained in:
Generated
+1
@@ -234,6 +234,7 @@ dependencies = [
|
|||||||
"corpus-core",
|
"corpus-core",
|
||||||
"cuekit",
|
"cuekit",
|
||||||
"cuepack",
|
"cuepack",
|
||||||
|
"cueseg",
|
||||||
"idnorm",
|
"idnorm",
|
||||||
"mdscrub",
|
"mdscrub",
|
||||||
"mdsplit",
|
"mdsplit",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ corpus-core.workspace = true
|
|||||||
idnorm.workspace = true
|
idnorm.workspace = true
|
||||||
cuekit.workspace = true
|
cuekit.workspace = true
|
||||||
cuepack.workspace = true
|
cuepack.workspace = true
|
||||||
|
cueseg.workspace = true
|
||||||
mdscrub.workspace = true
|
mdscrub.workspace = true
|
||||||
mdsplit.workspace = true
|
mdsplit.workspace = true
|
||||||
rrf.workspace = true
|
rrf.workspace = true
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ except ValueError as why:
|
|||||||
| `count_tokens` | [`textkit`](../textkit/README.md) |
|
| `count_tokens` | [`textkit`](../textkit/README.md) |
|
||||||
| `parse_subtitles` | [`cuenorm`](../cuenorm/README.md) |
|
| `parse_subtitles` | [`cuenorm`](../cuenorm/README.md) |
|
||||||
| `pack_cues` | [`cuepack`](../cuepack/README.md) |
|
| `pack_cues` | [`cuepack`](../cuepack/README.md) |
|
||||||
|
| `segment_cues` | [`cueseg`](../cueseg/README.md) |
|
||||||
| `split_collection` | [`mdsplit`](../mdsplit/README.md) |
|
| `split_collection` | [`mdsplit`](../mdsplit/README.md) |
|
||||||
| `fuse_hits` | [`rrf`](../rrf/README.md) |
|
| `fuse_hits` | [`rrf`](../rrf/README.md) |
|
||||||
| `identify` | [`booksniff`](../booksniff/README.md) |
|
| `identify` | [`booksniff`](../booksniff/README.md) |
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ try {
|
|||||||
| `countTokens` | Tokens, by the encoding the budgets count in |
|
| `countTokens` | Tokens, by the encoding the budgets count in |
|
||||||
| `parseSubtitles` | SubRip, WebVTT or ASS into cues |
|
| `parseSubtitles` | SubRip, WebVTT or ASS into cues |
|
||||||
| `packCues` | Cues into passages, keeping the times they were said at |
|
| `packCues` | Cues into passages, keeping the times they were said at |
|
||||||
|
| `segmentCues` | A timed transcript into topic chapters |
|
||||||
| `splitCollection` | One markdown file into the works inside it |
|
| `splitCollection` | One markdown file into the works inside it |
|
||||||
| `fuseHits` | Several ranked lists into one |
|
| `fuseHits` | Several ranked lists into one |
|
||||||
| `identify` | What a file is, from its first bytes |
|
| `identify` | What a file is, from its first bytes |
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
// check, and nothing else would notice.
|
// check, and nothing else would notice.
|
||||||
import {
|
import {
|
||||||
chunkMarkdown, scrub, countTokens, parseSubtitles, packCues,
|
chunkMarkdown, scrub, countTokens, parseSubtitles, packCues,
|
||||||
splitCollection, fuseHits, identify, seriesOf, parseIdentifier,
|
splitCollection, fuseHits, identify, seriesOf, parseIdentifier, segmentCues,
|
||||||
type Chunk, type Cue, type Section, type Hit, type Doc, type SeriesProposal, type Identifier,
|
type Chunk, type Cue, type Section, type Hit, type Doc, type SeriesProposal, type Identifier, type Segment,
|
||||||
} from "./dist/web/catena.js";
|
} from "./dist/web/catena.js";
|
||||||
|
|
||||||
const chunks: Chunk[] = chunkMarkdown("# One\n\nbody\n", 512, 64, 50, 768);
|
const chunks: Chunk[] = chunkMarkdown("# One\n\nbody\n", 512, 64, 50, 768);
|
||||||
@@ -19,6 +19,10 @@ const cues: Cue[] = parseSubtitles("1\n00:00:01,000 --> 00:00:04,000\nline\n\n")
|
|||||||
const start: number = cues[0].start;
|
const start: number = cues[0].start;
|
||||||
const packed: Chunk[] = packCues(cues, 75, 512);
|
const packed: Chunk[] = packCues(cues, 75, 512);
|
||||||
|
|
||||||
|
const chapters: Segment[] = segmentCues(cues, 3600);
|
||||||
|
const chapterTitle: string = chapters[0].title;
|
||||||
|
segmentCues(cues);
|
||||||
|
|
||||||
const works: Section[] = splitCollection("# One\n\nbody\n");
|
const works: Section[] = splitCollection("# One\n\nbody\n");
|
||||||
const slug: string = works[0].slug;
|
const slug: string = works[0].slug;
|
||||||
|
|
||||||
@@ -42,4 +46,4 @@ seriesOf("Aquinas (Companion)", "Cambridge");
|
|||||||
const ident: Identifier | undefined = parseIdentifier("10.1000/182");
|
const ident: Identifier | undefined = parseIdentifier("10.1000/182");
|
||||||
const checksum: boolean | null | undefined = ident?.checksum;
|
const checksum: boolean | null | undefined = ident?.checksum;
|
||||||
|
|
||||||
export const checked = [volume, checksum, page, chapter, flags, clean, tokens, start, packed, slug, fused, kind, notes];
|
export const checked = [volume, checksum, chapterTitle, page, chapter, flags, clean, tokens, start, packed, slug, fused, kind, notes];
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from ._catena import (
|
|||||||
parse_identifier,
|
parse_identifier,
|
||||||
parse_subtitles,
|
parse_subtitles,
|
||||||
scrub,
|
scrub,
|
||||||
|
segment_cues,
|
||||||
series_of,
|
series_of,
|
||||||
split_collection,
|
split_collection,
|
||||||
)
|
)
|
||||||
@@ -34,6 +35,7 @@ __all__ = [
|
|||||||
"parse_identifier",
|
"parse_identifier",
|
||||||
"parse_subtitles",
|
"parse_subtitles",
|
||||||
"scrub",
|
"scrub",
|
||||||
|
"segment_cues",
|
||||||
"series_of",
|
"series_of",
|
||||||
"split_collection",
|
"split_collection",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ def pack_cues(
|
|||||||
seconds: float | None = ...,
|
seconds: float | None = ...,
|
||||||
max_tokens: int | None = ...,
|
max_tokens: int | None = ...,
|
||||||
) -> list[dict[str, Any]]: ...
|
) -> list[dict[str, Any]]: ...
|
||||||
|
def segment_cues(
|
||||||
|
cues: Iterable[Mapping[str, Any]], *, duration: float | None = ...
|
||||||
|
) -> list[dict[str, Any]]: ...
|
||||||
def split_collection(markdown: str) -> list[dict[str, Any]]: ...
|
def split_collection(markdown: str) -> list[dict[str, Any]]: ...
|
||||||
def fuse_hits(
|
def fuse_hits(
|
||||||
hits: Iterable[Mapping[str, Any]],
|
hits: Iterable[Mapping[str, Any]],
|
||||||
|
|||||||
@@ -259,6 +259,29 @@ pub unsafe extern "C" fn catena_pack_cues(
|
|||||||
give_json(&crate::pack_cues(&cues, budgets))
|
give_json(&crate::pack_cues(&cues, budgets))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cut a timed transcript into topic chapters. `cues` is a JSON array of
|
||||||
|
/// cues; returns a JSON array of segments.
|
||||||
|
///
|
||||||
|
/// A duration of zero or less is taken from the last cue.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// `cues` must be NUL-terminated UTF-8.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn catena_segment_cues(
|
||||||
|
cues: *const c_char,
|
||||||
|
duration: c_double,
|
||||||
|
) -> *mut c_char {
|
||||||
|
clear_error();
|
||||||
|
let Some(cues) = (unsafe { borrow(cues, "cues") }) else {
|
||||||
|
return ptr::null_mut();
|
||||||
|
};
|
||||||
|
let Some(cues) = take_json::<Vec<cuekit::Cue>>(cues, "cues") else {
|
||||||
|
return ptr::null_mut();
|
||||||
|
};
|
||||||
|
give_json(&crate::segment_cues(&cues, duration))
|
||||||
|
}
|
||||||
|
|
||||||
/// Split a collection into the works inside it. Returns a JSON array of
|
/// Split a collection into the works inside it. Returns a JSON array of
|
||||||
/// sections, or NULL when the document is one work rather than a collection —
|
/// sections, or NULL when the document is one work rather than a collection —
|
||||||
/// which is an answer, not a failure, and the reason is in
|
/// which is an answer, not a failure, and the reason is in
|
||||||
|
|||||||
@@ -128,6 +128,18 @@ pub fn pack_cues(cues: &[Cue], budgets: CueBudgets) -> Vec<Chunk> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cut a timed transcript into topic chapters.
|
||||||
|
///
|
||||||
|
/// `duration` is how long the recording is, in seconds; it decides how many
|
||||||
|
/// chapters to aim for. Zero or less means take it from the last cue.
|
||||||
|
pub fn segment_cues(cues: &[Cue], duration: f64) -> Vec<cueseg::Segment> {
|
||||||
|
let duration = match duration > 0.0 {
|
||||||
|
true => duration,
|
||||||
|
false => cues.last().map_or(0.0, |cue| cue.end),
|
||||||
|
};
|
||||||
|
cueseg::segment(cues, duration)
|
||||||
|
}
|
||||||
|
|
||||||
/// Split a collection into the works inside it.
|
/// Split a collection into the works inside it.
|
||||||
///
|
///
|
||||||
/// `Err` when the document is one work rather than a collection, which is the
|
/// `Err` when the document is one work rather than a collection, which is the
|
||||||
@@ -222,6 +234,28 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_transcript_comes_back_as_chapters() {
|
||||||
|
// Long enough to be worth chaptering at all: the segmenter refuses
|
||||||
|
// to cut a recording into pieces shorter than its minimum.
|
||||||
|
let cues: Vec<Cue> = (0..1200)
|
||||||
|
.map(|i| Cue {
|
||||||
|
v: corpus_core::V,
|
||||||
|
start: i as f64 * 5.0,
|
||||||
|
end: i as f64 * 5.0 + 5.0,
|
||||||
|
text: match i % 2 {
|
||||||
|
0 => "the mill stood at the northern end of the valley".into(),
|
||||||
|
_ => "parliament dissolved itself again amid objections".into(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let chapters = segment_cues(&cues, 6000.0);
|
||||||
|
assert!(!chapters.is_empty());
|
||||||
|
assert_eq!(chapters, cueseg::segment(&cues, 6000.0));
|
||||||
|
// The duration can be left to the cues themselves.
|
||||||
|
assert_eq!(segment_cues(&cues, 0.0), cueseg::segment(&cues, 6000.0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_document_that_is_one_work_says_so_rather_than_splitting() {
|
fn a_document_that_is_one_work_says_so_rather_than_splitting() {
|
||||||
assert!(split_collection(MARKDOWN).is_err());
|
assert!(split_collection(MARKDOWN).is_err());
|
||||||
|
|||||||
@@ -158,6 +158,21 @@ fn pack_cues<'py>(
|
|||||||
list_to_py(py, &crate::pack_cues(&cues, budgets))
|
list_to_py(py, &crate::pack_cues(&cues, budgets))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cut a timed transcript into topic chapters.
|
||||||
|
///
|
||||||
|
/// `duration` is the recording's length in seconds; omit it and the last cue
|
||||||
|
/// decides.
|
||||||
|
#[pyfunction]
|
||||||
|
#[pyo3(signature = (cues, *, duration = None))]
|
||||||
|
fn segment_cues<'py>(
|
||||||
|
py: Python<'py>,
|
||||||
|
cues: &Bound<'py, PyAny>,
|
||||||
|
duration: Option<f64>,
|
||||||
|
) -> PyResult<Bound<'py, PyList>> {
|
||||||
|
let cues: Vec<cuekit::Cue> = from_py_list(cues)?;
|
||||||
|
list_to_py(py, &crate::segment_cues(&cues, duration.unwrap_or(0.0)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Split a collection into the works inside it.
|
/// Split a collection into the works inside it.
|
||||||
///
|
///
|
||||||
/// Raises `ValueError` when the document is one work rather than a
|
/// Raises `ValueError` when the document is one work rather than a
|
||||||
@@ -228,6 +243,7 @@ fn _catena(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
|||||||
module.add_function(wrap_pyfunction!(count_tokens, module)?)?;
|
module.add_function(wrap_pyfunction!(count_tokens, module)?)?;
|
||||||
module.add_function(wrap_pyfunction!(parse_subtitles, module)?)?;
|
module.add_function(wrap_pyfunction!(parse_subtitles, module)?)?;
|
||||||
module.add_function(wrap_pyfunction!(pack_cues, module)?)?;
|
module.add_function(wrap_pyfunction!(pack_cues, module)?)?;
|
||||||
|
module.add_function(wrap_pyfunction!(segment_cues, module)?)?;
|
||||||
module.add_function(wrap_pyfunction!(split_collection, module)?)?;
|
module.add_function(wrap_pyfunction!(split_collection, module)?)?;
|
||||||
module.add_function(wrap_pyfunction!(fuse_hits, module)?)?;
|
module.add_function(wrap_pyfunction!(fuse_hits, module)?)?;
|
||||||
module.add_function(wrap_pyfunction!(identify, module)?)?;
|
module.add_function(wrap_pyfunction!(identify, module)?)?;
|
||||||
|
|||||||
@@ -50,6 +50,18 @@ export interface Cue {
|
|||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One topic chapter of a recording. */
|
||||||
|
export interface Segment {
|
||||||
|
v: number;
|
||||||
|
/** Seconds from the start of the recording. */
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
/** A title taken from the chapter's own first sentence. */
|
||||||
|
title: string;
|
||||||
|
/** The opening of the chapter, for a person choosing between them. */
|
||||||
|
excerpt: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** One work inside a collection. */
|
/** One work inside a collection. */
|
||||||
export interface Section {
|
export interface Section {
|
||||||
v: number;
|
v: number;
|
||||||
@@ -196,6 +208,16 @@ pub fn pack_cues(
|
|||||||
to_js(&crate::pack_cues(&cues, budgets))
|
to_js(&crate::pack_cues(&cues, budgets))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cut a timed transcript into topic chapters.
|
||||||
|
///
|
||||||
|
/// `duration` is the recording's length in seconds; omit it and the last cue
|
||||||
|
/// decides.
|
||||||
|
#[wasm_bindgen(js_name = segmentCues, skip_typescript)]
|
||||||
|
pub fn segment_cues(cues: JsValue, duration: Option<f64>) -> Result<JsValue, JsValue> {
|
||||||
|
let cues: Vec<cuekit::Cue> = from_js(cues, "cues")?;
|
||||||
|
to_js(&crate::segment_cues(&cues, duration.unwrap_or(0.0)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Split a collection into the works inside it.
|
/// Split a collection into the works inside it.
|
||||||
///
|
///
|
||||||
/// Throws when the document is one work rather than a collection, which is
|
/// Throws when the document is one work rather than a collection, which is
|
||||||
@@ -266,6 +288,7 @@ const SIGNATURES: &'static str = r#"
|
|||||||
export function chunkMarkdown(markdown: string, target?: number, overlap?: number, min_tokens?: number, hard_max?: number): Chunk[];
|
export function chunkMarkdown(markdown: string, target?: number, overlap?: number, min_tokens?: number, hard_max?: number): Chunk[];
|
||||||
export function parseSubtitles(content: string, keep_repeats?: boolean): Cue[];
|
export function parseSubtitles(content: string, keep_repeats?: boolean): Cue[];
|
||||||
export function packCues(cues: Cue[], seconds?: number, max_tokens?: number): Chunk[];
|
export function packCues(cues: Cue[], seconds?: number, max_tokens?: number): Chunk[];
|
||||||
|
export function segmentCues(cues: Cue[], duration?: number): Segment[];
|
||||||
export function splitCollection(markdown: string): Section[];
|
export function splitCollection(markdown: string): Section[];
|
||||||
export function fuseHits(hits: Hit[], k?: number, weights?: Record<string, number>, limit?: number): Hit[];
|
export function fuseHits(hits: Hit[], k?: number, weights?: Record<string, number>, limit?: number): Hit[];
|
||||||
export function identify(name: string, data: Uint8Array): Doc;
|
export function identify(name: string, data: Uint8Array): Doc;
|
||||||
|
|||||||
Reference in New Issue
Block a user