Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions extensions/decoder/decoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1736,6 +1736,7 @@ void pybind_training_graph_compiler(py::module &m) {
py::arg("out_fst"))
.def("CompileGraphFromLG",
[](PyClass& gc, const fst::VectorFst<fst::StdArc> &phone2word_fst){
py::gil_scoped_release gil_release;

VectorFst<StdArc> decode_fst;

Expand All @@ -1745,7 +1746,8 @@ void pybind_training_graph_compiler(py::module &m) {
return decode_fst;
},
"Same as `CompileGraph`, but uses an external LG fst.",
py::arg("phone2word_fst"))
py::arg("phone2word_fst"),
py::return_value_policy::take_ownership)
.def("CompileGraphFromLG",
[](PyClass& gc, py::object fst){
auto pywrapfst_mod = py::module_::import("pywrapfst");
Expand All @@ -1760,7 +1762,8 @@ void pybind_training_graph_compiler(py::module &m) {
return decode_fst;
},
"Same as `CompileGraph`, but uses an external LG fst.",
py::arg("phone2word_fst"))
py::arg("phone2word_fst"),
py::return_value_policy::take_ownership)
.def("CompileGraphFromLG",
&PyClass::CompileGraphFromLG,
"Same as `CompileGraph`, but uses an external LG fst.",
Expand Down Expand Up @@ -1798,7 +1801,8 @@ void pybind_training_graph_compiler(py::module &m) {
}
return decode_fst;
},
py::arg("transcript"))
py::arg("transcript"),
py::return_value_policy::take_ownership)
.def("CompileGraphsFromText",
&PyClass::CompileGraphsFromText,
"This function creates FSTs from the text and calls CompileGraphs.",
Expand Down Expand Up @@ -1899,7 +1903,8 @@ py::module m = _m.def_submodule("decoder", "pybind for decoder");
// We'll write the lattice without acoustic scaling.
if (acoustic_scale != 0.0)
fst::ScaleLattice(fst::AcousticLatticeScale(1.0 / acoustic_scale), &clat);
return py::make_tuple(true, alignment, words, clat);
ConvertLattice(clat,&lat);
return py::make_tuple(true, alignment, words, lat);
} else {
// We'll write the lattice without acoustic scaling.
if (acoustic_scale != 0.0)
Expand Down
1 change: 1 addition & 0 deletions extensions/fstext/pybind_fstext.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "fst/script/print-impl.h"
#include "fst/fstlib.h"
#include "fstext/fstext-utils.h"
#include "fstext/table-matcher.h"
#include "fstext/kaldi-fst-io.h"
#include "fstext/lattice-utils.h"
#include "fstext/lattice-utils-inl.h"
Expand Down
2 changes: 1 addition & 1 deletion kalpy/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class Segment:
Data class for information about acoustic segments
"""

file_path: str
file_path: PathLike
begin: typing.Optional[float] = 0.0
end: typing.Optional[float] = None
channel: typing.Optional[int] = 0
Expand Down
61 changes: 6 additions & 55 deletions kalpy/decoder/training_graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ def __init__(
transition_scale: float = 0.0,
self_loop_scale: float = 0.0,
batch_size: int = 1000,
use_g2p: bool = False,
disambiguation_symbols: typing.List[int] = None,
oov_word: str = "<unk>",
):
Expand All @@ -78,7 +77,6 @@ def __init__(
self.batch_size = batch_size
self.options = TrainingGraphCompilerOptions(transition_scale, self_loop_scale)
self._compiler = None
self.use_g2p = use_g2p
self.lexicon_path = None
self.lexicon_compiler = lexicon_compiler
self.oov_word = oov_word
Expand Down Expand Up @@ -151,7 +149,7 @@ def export_graphs(
write_scp: bool
Flag for whether an SCP file should be generated as well
callback: callable, optional
Optional callback function for progress updates
Callback function for progress updates
interjection_words: list[str], optional
List of words to add as interjections to the transcripts
cutoff_pattern: str, optional
Expand All @@ -168,20 +166,15 @@ def export_graphs(
for key, transcript in transcripts:
keys.append(key)
original_transcripts.append(transcript)
if self.use_g2p:
transcript_batch.append(transcript)
elif interjection_words:
logger.debug(f"{key}: {transcript}")
if interjection_words:
transcript_batch.append(
self.generate_utterance_graph(transcript, interjection_words, cutoff_pattern)
)
else:
transcript_batch.append([self.to_int(x) for x in transcript.split()])
if len(keys) >= self.batch_size:
if self.use_g2p:
fsts = []
for t in transcript_batch:
fsts.append(self.compile_fst(t))
elif interjection_words:
if interjection_words:
fsts = self.compiler.CompileGraphs(transcript_batch)
else:
fsts = self.compiler.CompileGraphsFromText(transcript_batch)
Expand Down Expand Up @@ -211,11 +204,7 @@ def export_graphs(
original_transcripts = []
del fsts
if keys:
if self.use_g2p:
fsts = []
for t in transcript_batch:
fsts.append(self.compile_fst(t))
elif interjection_words:
if interjection_words:
fsts = self.compiler.CompileGraphs(transcript_batch)
else:
fsts = self.compiler.CompileGraphsFromText(transcript_batch)
Expand Down Expand Up @@ -376,45 +365,7 @@ def compile_fst(
:class:`_kalpy.fstext.VectorFst`
Training graph of transcript
"""
if self.use_g2p:
g_fst = pynini.accep(transcript, token_type=self.word_table)
lg_fst = pynini.compose(g_fst, self._fst, compose_filter="alt_sequence")
lg_fst = lg_fst.project("output").rmepsilon()
weight_type = lg_fst.weight_type()
weight_threshold = pywrapfst.Weight(weight_type, 2.0)
state_threshold = 256 + 2 * lg_fst.num_states()
lg_fst = pynini.determinize(lg_fst, nstate=state_threshold, weight=weight_threshold)
lg_fst = VectorFst.from_pynini(lg_fst)
disambig_syms_in = (
[]
if not self.lexicon_compiler.disambiguation
else self.lexicon_compiler.disambiguation_symbols
)
lg_fst = fst_determinize_star(lg_fst, use_log=True)
fst_minimize_encoded(lg_fst)
fst_push_special(lg_fst)
clg_fst, disambig_out, ilabels = fst_compose_context(
lg_fst,
disambig_syms_in,
self.tree.ContextWidth(),
self.tree.CentralPosition(),
)
fst_arc_sort(clg_fst, sort_type="ilabel")
h, disambig = make_h_transducer(self.tree, self.transition_model, ilabels)
fst = fst_table_compose(h, clg_fst)
if fst.Start() == pywrapfst.NO_STATE_ID:
logger.debug(f"Falling back to pynini compose for '{transcript}")
h = kaldi_to_pynini(h)
clg_fst = kaldi_to_pynini(clg_fst)
fst = pynini_to_kaldi(pynini.compose(h, clg_fst))
fst_determinize_star(fst, use_log=True)
fst_rm_symbols(fst, disambig)
fst_rm_eps_local(fst)
fst_minimize_encoded(fst)
fst_add_self_loops(
fst, self.transition_model, disambig_syms_in, self.options.self_loop_scale
)
elif interjection_words:
if interjection_words:
g = self.generate_utterance_graph(transcript, interjection_words, cutoff_pattern)
# fst = VectorFst()
# self.compiler.CompileGraph(g, fst)
Expand Down
117 changes: 114 additions & 3 deletions kalpy/evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,117 @@ def fix_many_to_one_alignments(
return new_ref, new_test


def naive_boundary_f1(
ref: typing.List[CtmInterval], test: typing.List[CtmInterval], threshold: float = 0.02
):
"""
Performs a calculation of naive precision, recall, and F1 score for two sets of alignments

Parameters
----------
ref: list[:class:`~montreal_forced_aligner.data.CtmInterval`]
List of CTM intervals as reference
test: list[:class:`~montreal_forced_aligner.data.CtmInterval`]
List of CTM intervals to compare to reference
threshold: float, optional
Threshold in seconds for precision/recall calculation, defaults to 0.02 seconds (20 ms)

Returns
-------
float
Precision, defined as the percentage of boundaries in the test alignments that have a corresponding boundary
in the reference alignments within the threshold specified
float
Recall, defined as the percentage of boundaries in the reference alignments that have a corresponding boundary
in the test alignments within the threshold specified
float
F1 score, defined as the harmonic mean of precision and recall
int
Difference in number of intervals between reference and test alignments
list[dict[str, any]]
Data as part of precision calculation
list[dict[str, any]]
Data as part of recall calculation
"""
if not test:
return "n/a", "n/a", "n/a", len(ref) - len(test), [], []
recall_data = []
recall_sum = 0
for i, r in enumerate(ref):
if i == 0:
continue
ref_boundary = r.begin
previous_distance = None
comparison = None
previous_comparison = None
for t in test:
distance = abs(ref_boundary - t.begin)
if previous_distance is None or distance < previous_distance:
previous_comparison = comparison
comparison = t
previous_distance = distance
elif previous_distance is not None:
break
if comparison is None:
print(ref)
print(test)
print(i, r)
error
error = round(r.begin - comparison.begin, 3)
if error <= threshold:
recall_sum += 1
recall_data.append(
{
"following_reference_phone": r.label,
"following_test_phone": comparison.label,
"previous_reference_phone": ref[i - 1].label,
"previous_test_phone": previous_comparison.label
if previous_comparison is not None
else "N/A",
"boundary_error": error,
"reference_boundary": round(r.begin, 3),
"test_boundary": round(comparison.begin, 3),
}
)
precision_data = []
precision_sum = 0
for i, t in enumerate(test):
if i == 0:
continue
test_boundary = t.begin
previous_distance = None
comparison = None
previous_comparison = None
for r in ref:
distance = abs(test_boundary - r.begin)
if previous_distance is None or distance < previous_distance:
previous_comparison = comparison
comparison = r
previous_distance = distance
elif previous_distance is not None:
break
error = round(t.begin - comparison.begin, 3)
if error <= threshold:
precision_sum += 1
precision_data.append(
{
"following_reference_phone": comparison.label,
"following_test_phone": t.label,
"previous_reference_phone": previous_comparison.label
if previous_comparison is not None
else "N/A",
"previous_test_phone": test[i - 1].label,
"boundary_error": error,
"reference_boundary": round(comparison.begin, 3),
"test_boundary": round(t.begin, 3),
}
)
recall = recall_sum / len(ref)
precision = precision_sum / len(test)
f1 = 2 * (precision * recall) / (precision + recall)
return precision, recall, f1, len(ref) - len(test), precision_data, recall_data


def align_phones(
ref: typing.List[CtmInterval],
test: typing.List[CtmInterval],
Expand All @@ -149,7 +260,7 @@ def align_phones(
float,
float,
typing.Dict[typing.Tuple[str, str], int],
float,
IntervalAlignment,
typing.List[typing.Dict[str, typing.Any]],
]:
"""
Expand Down Expand Up @@ -261,14 +372,14 @@ def align_phones(
if debug:
import logging

logger = logging.getLogger("mfa")
logger = logging.getLogger("kalpy.evaluate")
if errors:
logger.debug(
f"PER: {phone_error_rate}\nErrors: {errors}\n{format_alignment(alignment)}"
)
else:
logger.debug(f"PER: {phone_error_rate}\n{format_alignment(alignment)}")
return score, phone_error_rate, errors, alignment.score, boundary_errors
return score, phone_error_rate, errors, alignment, boundary_errors


def fix_unk_words(
Expand Down
Loading
Loading