FALCON / panphon /permissive.py
MLSpeech's picture
Vendor patched panphon (py3.8-compatible import)
787be4d verified
Raw
History Blame Contribute Delete
7.28 kB
from __future__ import absolute_import, print_function, unicode_literals
import codecs
import copy
import os.path
import pkg_resources
import yaml
import regex as re
import unicodecsv as csv
from . import _panphon, xsampa
def flip(s):
return [(b, a) for (a, b) in s]
def update_ft_set(seg, dia):
seg = dict(flip(seg))
seg.update(dia)
return flip(set(seg.items()))
class PermissiveFeatureTable(_panphon.FeatureTable):
"""Encapsulate the segment <=> feature vector mapping implied by the files
data/ipa_all.csv and diacritic_definitions.yml. Uses a more permissive
algorithm for identifying base+diacritic combinations. To avoid a
combinatorial explosion, it never generates all of the base-diacritic-
modifier combinations, meaning it cannot easily make statements about the
whole set of segments."""
def __init__(self,
feature_set='spe+',
feature_model='strict',
ipa_bases=os.path.join('data', 'ipa_bases.csv'),
dias=os.path.join('data', 'diacritic_definitions.yml'),
):
"""Construct a PermissiveFeatureTable object
Args:
feature_set (str): feature system (for API compatibility)
feature_model (str): feature parsing model (for API compatibility)
ipa_bases (str): path from panphon root to CSV file definining
features of bases (unmodified consonants and
vowels)
dias (str): path from panphon root to YAML file containing rules for
diacritics and modifiers
"""
dias = pkg_resources.resource_filename(__name__, dias)
self.bases, self.names = self._read_ipa_bases(ipa_bases)
self.prefix_dias, self.postfix_dias = self._read_dias(dias)
self.pre_regex, self.post_regex, self.seg_regex = self._compile_seg_regexes(self.bases, self.prefix_dias, self.postfix_dias)
self.xsampa = xsampa.XSampa()
self.weights = self._read_weights()
def _read_ipa_bases(self, fn):
fn = pkg_resources.resource_filename(__name__, fn)
with open(fn, 'rb') as f:
reader = csv.reader(f, encoding='utf-8', delimiter=str(','))
names = next(reader)[1:]
bases = {}
for row in reader:
seg, vals = row[0], row[1:]
bases[seg] = (set(zip(vals, names)))
return bases, names
def _read_dias(self, fn):
prefix, postfix = {}, {}
with codecs.open(fn, 'r', 'utf-8') as f:
defs = yaml.load(f.read(), Loader=yaml.FullLoader)
for dia in defs['diacritics']:
if dia['position'] == 'pre':
prefix[dia['marker']] = dia['content']
else:
postfix[dia['marker']] = dia['content']
return prefix, postfix
def _compile_seg_regexes(self, bases, prefix, postfix):
pre_jnd = '|'.join(prefix.keys())
post_jnd = '|'.join(postfix.keys())
bases_jnd = '|'.join(bases.keys())
pre_re = '({})'.format(pre_jnd)
post_re = '({})'.format(post_jnd)
seg_re = '(?P<all>(?P<pre>({})*)(?P<base>{})(?P<post>({})*))'.format(pre_jnd, bases_jnd, post_jnd)
return re.compile(pre_re), re.compile(post_re), re.compile(seg_re)
def _build_seg_regex(self):
return self.seg_regex
def _read_weights(self, filename=os.path.join('data', 'feature_weights.csv')):
filename = pkg_resources.resource_filename(
__name__, filename)
with open(filename, 'rb') as f:
reader = csv.reader(f, encoding='utf-8')
next(reader)
weights = [float(x) for x in next(reader)]
return weights
def fts(self, segment):
"""Return features corresponding to segment as list of (value,
feature) tuples
Args:
segment (unicode): segment for which features are to be returned as
Unicode string
Returns:
list: None if `segment` cannot be parsed; otherwise, a list of the
features of `segment` as (value, feature) pairs
"""
match = self.seg_regex.match(segment)
if match:
pre, base, post = match.group('pre'), match.group('base'), match.group('post')
seg = copy.deepcopy(self.bases[base])
for m in reversed(pre):
seg = update_ft_set(seg, self.prefix_dias[m])
for m in post:
seg = update_ft_set(seg, self.postfix_dias[m])
return set(seg)
else:
return None
def fts_match(self, fts_mask, segment):
"""Evaluates whether a set of features 'match' a segment (are a subset
of that segment's features)
Args:
fts_mask (list): list of (value, feature) tuples
segment (unicode): IPA string corresponding to segment (consonant or
vowel)
Returns:
bool: None if `segment` cannot be parsed; True if the feature values
of `fts_mask` are a subset of those for `segment`
"""
fts_seg = self.fts(segment)
if fts_seg:
fts_mask = set(fts_mask)
return fts_mask <= fts_seg
else:
return None
def longest_one_seg_prefix(self, word):
"""Return longest IPA Unicode prefix of `word`
Args:
word (unicode): word as IPA string
Returns:
unicode: longest single-segment prefix of `word`
"""
match = self.seg_regex.match(word)
if match:
return match.group(0)
else:
return ''
def seg_known(self, segment):
"""Return True if the segment is valid
Args:
segment (unicode): a string which may or may not be a valid segment
Returns:
bool: True if segment can be parsed given the database of bases and
diacritics
"""
if self.seg_regex.match(segment):
return True
else:
return False
def filter_segs(self, segs):
"""Given list of strings, return only those which are valid segments.
Args:
segs (list): list of unicode values
Returns:
list: values in `segs` that are valid segments (according to the
definititions of bases and diacritics/modifiers known to the
object
"""
def whole_seg(seg):
m = self.seg_regex.match(seg)
if m and m.group(0) == seg:
return True
else:
return False
return list(filter(whole_seg, segs))
def segment_word_segments(self, word):
def n2s(s):
if s is None:
return ''
else:
return s
return ((n2s(m.group('pre')), n2s(m.group('base')), n2s(m.group('post')))
for m in self.seg_regex.finditer(word))
@property
def all_segs_matching_fts(self):
raise AttributeError("'PermissiveFeatureTable' object has no attribute 'all_segs_matching_fts'")