Phone inventory, segmenter and weksa lower --phones (the Weksa->AquaSynth phones line) #16
8 changed files with 419 additions and 5 deletions
|
|
@ -9,6 +9,7 @@ pub mod form;
|
|||
pub mod inventory;
|
||||
pub mod lower;
|
||||
pub mod packet;
|
||||
pub mod phones;
|
||||
pub mod schema;
|
||||
pub mod score;
|
||||
pub mod split;
|
||||
|
|
@ -22,7 +23,8 @@ pub use inventory::{
|
|||
Change, Family, InvId, Inventory, InventoryEntry, InventoryRevision, OptionLabel, OptionSet,
|
||||
Slot, ValueKind,
|
||||
};
|
||||
pub use lower::{EntityForms, LowerError, TARGET_LANGUAGE, lower, pronounce};
|
||||
pub use lower::{EntityForms, LowerError, PhoneLine, TARGET_LANGUAGE, lower, phones, pronounce};
|
||||
pub use phones::{Phone, PhoneInventory, SegmentError, Stress};
|
||||
pub use packet::{
|
||||
EncoderId, EntityId, Grade, NodeId, Packet, PacketId, PacketKind, SCHEMA_VERSION, Stance,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,9 +31,12 @@
|
|||
|
||||
use std::fmt;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::entity::Entity;
|
||||
use crate::inventory::{Family, InvId, Inventory};
|
||||
use crate::packet::{EntityId, NodeId, Predication};
|
||||
use crate::phones::{Phone, PhoneInventory, SegmentError};
|
||||
use crate::store::EntityStore;
|
||||
use crate::validate::LoweringInput;
|
||||
|
||||
|
|
@ -112,6 +115,13 @@ pub enum LowerError {
|
|||
NoPronunciation { entity: EntityId },
|
||||
/// IPA was requested for a packet that cites no entity.
|
||||
NoEntityReferent,
|
||||
/// Phones were requested for an entity whose ruled IPA holds a symbol the
|
||||
/// language's phone inventory does not list (or the language has no
|
||||
/// inventory). The lowerer does not guess a phone.
|
||||
UnknownPhone { entity: EntityId, at: usize, text: String },
|
||||
/// Phones were requested for an entity whose ruled IPA has a stress mark
|
||||
/// with no vowel after it.
|
||||
DanglingStress { entity: EntityId, at: usize },
|
||||
/// A request predication the packet does not carry the addressee's consent
|
||||
/// to: no consent, or a consent that names another predication. The
|
||||
/// lowerer does not guess who asked.
|
||||
|
|
@ -271,13 +281,54 @@ pub fn pronounce(
|
|||
input: &LoweringInput,
|
||||
entities: &dyn EntityForms,
|
||||
) -> Result<Vec<String>, LowerError> {
|
||||
Ok(ruled(input, entities)?.into_iter().map(|(_, ipa)| ipa).collect())
|
||||
}
|
||||
|
||||
/// One entity's ruled pronunciation as the phones line a sound consumer reads:
|
||||
/// the language tag, the stored IPA it came from, and the segmented phones.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct PhoneLine {
|
||||
pub language: &'static str,
|
||||
pub source: String,
|
||||
pub phones: Vec<Phone>,
|
||||
}
|
||||
|
||||
/// The phones line of every entity a packet cites, in [`pronounce`] order, each
|
||||
/// segmented by [`TARGET_LANGUAGE`]'s phone inventory. A symbol the inventory
|
||||
/// does not list refuses the whole call.
|
||||
pub fn phones(
|
||||
input: &LoweringInput,
|
||||
entities: &dyn EntityForms,
|
||||
) -> Result<Vec<PhoneLine>, LowerError> {
|
||||
let inventory = PhoneInventory::for_language(TARGET_LANGUAGE);
|
||||
ruled(input, entities)?
|
||||
.into_iter()
|
||||
.map(|(entity, source)| {
|
||||
let unknown = |at, text| LowerError::UnknownPhone { entity: entity.clone(), at, text };
|
||||
let inventory = inventory.ok_or_else(|| unknown(0, source.clone()))?;
|
||||
let phones = inventory.segment(&source).map_err(|e| match e {
|
||||
SegmentError::UnknownPhone { at, text } => unknown(at, text),
|
||||
SegmentError::DanglingStress { at } => {
|
||||
LowerError::DanglingStress { entity: entity.clone(), at }
|
||||
}
|
||||
})?;
|
||||
Ok(PhoneLine { language: TARGET_LANGUAGE, source, phones })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `(entity, ruled IPA)` for each entity referent, in referent order.
|
||||
fn ruled(
|
||||
input: &LoweringInput,
|
||||
entities: &dyn EntityForms,
|
||||
) -> Result<Vec<(EntityId, String)>, LowerError> {
|
||||
let mut out = Vec::new();
|
||||
for id in input.referents().iter().filter_map(|r| r.entity.as_ref()) {
|
||||
let entity = entity(entities, id)?;
|
||||
let p = entity
|
||||
.pronunciation_in(TARGET_LANGUAGE)
|
||||
.ok_or_else(|| LowerError::NoPronunciation { entity: id.clone() })?;
|
||||
out.push(p.ipa.clone());
|
||||
out.push((id.clone(), p.ipa.clone()));
|
||||
}
|
||||
if out.is_empty() {
|
||||
return Err(LowerError::NoEntityReferent);
|
||||
|
|
|
|||
|
|
@ -53,8 +53,13 @@ enum Command {
|
|||
key: String,
|
||||
/// Print the ruled IPA of each entity the packet cites, one per line,
|
||||
/// instead of the text; refused for an entity with no ruled pronunciation.
|
||||
#[arg(long)]
|
||||
#[arg(long, conflicts_with = "phones")]
|
||||
ipa: bool,
|
||||
/// Print the phones line of each entity the packet cites, one JSON
|
||||
/// object per line ({language, source, phones: [{ipa, stress}]}),
|
||||
/// instead of the text; refused for a phone the inventory lacks.
|
||||
#[arg(long)]
|
||||
phones: bool,
|
||||
},
|
||||
/// Score output text (stdin, or --output) against corpus items, one embed call.
|
||||
Score(ScoreArgs),
|
||||
|
|
@ -527,7 +532,7 @@ fn main() -> Result<()> {
|
|||
.map_err(refused)?;
|
||||
println!("admitted {key}");
|
||||
}
|
||||
Command::Lower { store, key, ipa } => {
|
||||
Command::Lower { store, key, ipa, phones } => {
|
||||
let inventory_store = InventoryStore::open(&cli.inventory)?;
|
||||
let inventory = inventory_store.inventory()?;
|
||||
let mut packets = PacketStore::open(&store)?;
|
||||
|
|
@ -549,6 +554,10 @@ fn main() -> Result<()> {
|
|||
for line in pronounce(&view, &entities)? {
|
||||
println!("{line}");
|
||||
}
|
||||
} else if phones {
|
||||
for line in weksa::phones(&view, &entities)? {
|
||||
println!("{}", serde_json::to_string(&line)?);
|
||||
}
|
||||
} else {
|
||||
println!("{}", lower(&view, &inventory, &entities)?);
|
||||
}
|
||||
|
|
|
|||
152
crates/weksa/src/phones.rs
Normal file
152
crates/weksa/src/phones.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
//! The phone inventory and the segmenter: a ruled IPA string to an ordered list of
|
||||
//! phones with stress, for the consumer that turns phones into sound. The
|
||||
//! inventory is data (`data/phonology/<language>.phones.tsv`); this module reads
|
||||
//! it and owns one rule, that every symbol of the string is a phone the inventory
|
||||
//! lists. A symbol it does not list is refused: no nearest phone, no spelling
|
||||
//! rule, no guess. Tense/lax is a column of the inventory, so /i/ and /ɪ/ are two
|
||||
//! phones here, not one phone and a flag on an entity.
|
||||
//!
|
||||
//! Stress marks are not phones. `ˈ` (primary) and `ˌ` (secondary) put their
|
||||
//! stress on the next vowel phone, the syllable's nucleus; every other phone is
|
||||
//! `none`. A mark with no vowel after it is refused. Durations, pitch and
|
||||
//! acoustics are the consumer's, never read or written here.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
const EN_US: &str = include_str!("../../../data/phonology/en-US.phones.tsv");
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Stress {
|
||||
None,
|
||||
Primary,
|
||||
Secondary,
|
||||
}
|
||||
|
||||
/// One phone of a segmented string: its IPA symbol as the inventory spells it
|
||||
/// and its stress.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct Phone {
|
||||
pub ipa: String,
|
||||
pub stress: Stress,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Tenseness {
|
||||
Tense,
|
||||
Lax,
|
||||
Reduced,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Class {
|
||||
Consonant,
|
||||
Vowel(Tenseness),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum SegmentError {
|
||||
/// The symbol (or the text from it on) matches no inventory phone.
|
||||
UnknownPhone { at: usize, text: String },
|
||||
/// A stress mark (`at` is its byte offset) that finds no vowel before the
|
||||
/// string ends or another mark comes.
|
||||
DanglingStress { at: usize },
|
||||
}
|
||||
|
||||
pub struct PhoneInventory {
|
||||
phones: BTreeMap<String, Class>,
|
||||
longest: usize,
|
||||
}
|
||||
|
||||
impl PhoneInventory {
|
||||
/// The inventory for a language tag, if one is committed.
|
||||
pub fn for_language(language: &str) -> Option<&'static PhoneInventory> {
|
||||
static EN_US_INV: OnceLock<PhoneInventory> = OnceLock::new();
|
||||
match language {
|
||||
"en-US" => Some(EN_US_INV.get_or_init(|| {
|
||||
PhoneInventory::parse(EN_US).expect("the committed en-US phone inventory parses")
|
||||
})),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the tab-separated inventory. Malformed lines are errors, as are
|
||||
/// repeated symbols.
|
||||
pub fn parse(text: &str) -> Result<Self, String> {
|
||||
let mut phones = BTreeMap::new();
|
||||
for (n, line) in text.lines().enumerate() {
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let cols: Vec<&str> = line.split('\t').collect();
|
||||
let [ipa, class, tenseness, gloss] = cols[..] else {
|
||||
return Err(format!("line {}: want 4 tab-separated columns", n + 1));
|
||||
};
|
||||
let class = match (class, tenseness) {
|
||||
("consonant", "-") => Class::Consonant,
|
||||
("vowel", "tense") => Class::Vowel(Tenseness::Tense),
|
||||
("vowel", "lax") => Class::Vowel(Tenseness::Lax),
|
||||
("vowel", "reduced") => Class::Vowel(Tenseness::Reduced),
|
||||
_ => return Err(format!("line {}: bad class/tenseness {class}/{tenseness}", n + 1)),
|
||||
};
|
||||
if ipa.is_empty() || gloss.is_empty() || ipa.chars().any(|c| matches!(c, 'ˈ' | 'ˌ')) {
|
||||
return Err(format!("line {}: empty or stress-marked entry", n + 1));
|
||||
}
|
||||
if phones.insert(ipa.to_string(), class).is_some() {
|
||||
return Err(format!("line {}: {ipa} repeats", n + 1));
|
||||
}
|
||||
}
|
||||
let longest = phones.keys().map(|k| k.chars().count()).max().unwrap_or(0);
|
||||
Ok(Self { phones, longest })
|
||||
}
|
||||
|
||||
pub fn class(&self, ipa: &str) -> Option<&Class> {
|
||||
self.phones.get(ipa)
|
||||
}
|
||||
|
||||
/// Segment `ipa` into phones: longest inventory match at each position.
|
||||
pub fn segment(&self, ipa: &str) -> Result<Vec<Phone>, SegmentError> {
|
||||
let chars: Vec<(usize, char)> = ipa.char_indices().collect();
|
||||
let mut out: Vec<Phone> = Vec::new();
|
||||
let mut pending: Option<(Stress, usize)> = None;
|
||||
let mut i = 0;
|
||||
while i < chars.len() {
|
||||
let at = chars[i].0;
|
||||
let mark = match chars[i].1 {
|
||||
'ˈ' => Some(Stress::Primary),
|
||||
'ˌ' => Some(Stress::Secondary),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(m) = mark {
|
||||
if let Some((_, was)) = pending {
|
||||
return Err(SegmentError::DanglingStress { at: was });
|
||||
}
|
||||
pending = Some((m, at));
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let hit = (1..=self.longest.min(chars.len() - i)).rev().find_map(|len| {
|
||||
let end = chars.get(i + len).map_or(ipa.len(), |c| c.0);
|
||||
let s = &ipa[at..end];
|
||||
self.phones.get(s).map(|c| (len, s, c))
|
||||
});
|
||||
let Some((len, s, class)) = hit else {
|
||||
return Err(SegmentError::UnknownPhone { at, text: ipa[at..].to_string() });
|
||||
};
|
||||
let stress = if matches!(class, Class::Vowel(_)) {
|
||||
pending.take().map_or(Stress::None, |(m, _)| m)
|
||||
} else {
|
||||
Stress::None
|
||||
};
|
||||
out.push(Phone { ipa: s.to_string(), stress });
|
||||
i += len;
|
||||
}
|
||||
if let Some((_, at)) = pending {
|
||||
return Err(SegmentError::DanglingStress { at });
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
|
@ -876,6 +876,18 @@ fn proof_packet_lowers_and_the_lowering_scores_against_items() {
|
|||
let ipa = weksa(d.path(), &args);
|
||||
assert!(ipa.status.success(), "{}", out(&ipa));
|
||||
assert_eq!(String::from_utf8(ipa.stdout).unwrap(), "ˈnibu\n");
|
||||
// The phones line is the committed fixture, byte for byte, and is a different
|
||||
// output from --ipa, so the two flags cannot be combined.
|
||||
let phones = weksa(d.path(), &["lower", "./p.cc", "packet:6e6962752d303031", "--phones"]);
|
||||
assert!(phones.status.success(), "{}", out(&phones));
|
||||
let fixture = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../data/phonology/fixtures/nibu-en-US.phones.json"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(String::from_utf8(phones.stdout).unwrap(), fixture);
|
||||
let both = weksa(d.path(), &["lower", "./p.cc", "packet:6e6962752d303031", "--phones", "--ipa"]);
|
||||
assert!(!both.status.success(), "{}", out(&both));
|
||||
let again = weksa(d.path(), &["lower", "./p.cc", "packet:6e6962752d303031"]);
|
||||
assert_eq!(String::from_utf8(again.stdout).unwrap(), text);
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use weksa::inventory::proof_revision;
|
|||
use weksa::packet::{EntityId, FeatValue, FeatureValue, Packet, proof_packet};
|
||||
use weksa::{
|
||||
CorpusStore, EntityForms, EntityStore, Inventory, InventoryStore, LowerError, LoweringInput,
|
||||
PacketStore, Pronunciation, lower, pronounce, validate,
|
||||
PacketStore, PhoneInventory, PhoneLine, Pronunciation, Stress, lower, pronounce, validate,
|
||||
};
|
||||
|
||||
const PROOF: &str = "Warning: you may endanger yourself by touching the control. \
|
||||
|
|
@ -437,6 +437,143 @@ fn ipa_needs_an_entity_the_port_holds_and_a_packet_that_cites_one() {
|
|||
);
|
||||
}
|
||||
|
||||
// ---- the phones line: the phone inventory and the segmenter ----
|
||||
|
||||
fn phones_of(ipa: &str) -> Result<Vec<PhoneLine>, LowerError> {
|
||||
let mut n = nibu(&[("en-US", &["Nibu"])]);
|
||||
n.forms[0].pronunciation = Some(ruled(ipa, &[]));
|
||||
let mut e = Entities::default();
|
||||
e.held.push(n);
|
||||
let (inv, _dir) = committed();
|
||||
weksa::phones(&view(&proof_packet(), &inv), &e)
|
||||
}
|
||||
|
||||
fn seg(ipa: &str) -> Vec<(String, Stress)> {
|
||||
let inv = PhoneInventory::for_language("en-US").unwrap();
|
||||
inv.segment(ipa)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|p| (p.ipa, p.stress))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn pairs(v: &[(&str, Stress)]) -> Vec<(String, Stress)> {
|
||||
v.iter().map(|(i, s)| ((*i).to_string(), *s)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nibus_committed_ruling_lowers_to_the_committed_phones_fixture() {
|
||||
let (inv, _dir) = committed();
|
||||
let lines = weksa::phones(&view(&proof_packet(), &inv), &committed_entities()).unwrap();
|
||||
let fixture = std::fs::read_to_string(
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../data/phonology/fixtures/nibu-en-US.phones.json"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(lines.len(), 1);
|
||||
assert_eq!(serde_json::to_string(&lines[0]).unwrap(), fixture.trim_end());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tense_i_is_a_phone_and_the_rejected_lax_form_is_a_different_one() {
|
||||
use weksa::phones::{Class, Tenseness};
|
||||
let inv = PhoneInventory::for_language("en-US").unwrap();
|
||||
assert_eq!(inv.class("i"), Some(&Class::Vowel(Tenseness::Tense)));
|
||||
assert_eq!(inv.class("ɪ"), Some(&Class::Vowel(Tenseness::Lax)));
|
||||
assert_eq!(inv.class("u"), Some(&Class::Vowel(Tenseness::Tense)));
|
||||
let ruled_form = seg("ˈnibu");
|
||||
let lax = seg("ˈnɪbu");
|
||||
assert_ne!(ruled_form, lax);
|
||||
assert_eq!(lax[1], ("ɪ".to_string(), Stress::Primary));
|
||||
// The committed seed rejects the lax form, and it is not what the seed emits.
|
||||
let seed = committed_entities();
|
||||
let nibu = seed.get(&EntityId::parse("entity:nibu").unwrap()).unwrap().unwrap();
|
||||
let p = nibu.pronunciation_in("en-US").unwrap();
|
||||
assert_eq!(p.rejected, ["ˈnɪbu"]);
|
||||
assert_eq!(seg(&p.ipa), ruled_form);
|
||||
assert!(p.rejected.iter().all(|r| seg(r) != seg(&p.ipa)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stress_lands_on_the_vowel_after_the_mark_and_nowhere_else() {
|
||||
use Stress::{None as N, Primary as P, Secondary as S};
|
||||
assert_eq!(seg("ˈnibu"), pairs(&[("n", N), ("i", P), ("b", N), ("u", N)]));
|
||||
assert_eq!(seg("nˈibu"), seg("ˈnibu"));
|
||||
assert_eq!(seg("bˈunˌi"), pairs(&[("b", N), ("u", P), ("n", N), ("i", S)]));
|
||||
assert_eq!(seg("nibu"), pairs(&[("n", N), ("i", N), ("b", N), ("u", N)]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_character_phones_are_one_phone_by_longest_match() {
|
||||
use Stress::{None as N, Primary as P};
|
||||
assert_eq!(seg("ˈtʃeɪ"), pairs(&[("tʃ", N), ("eɪ", P)]));
|
||||
assert_eq!(seg("dʒaʊ"), pairs(&[("dʒ", N), ("aʊ", N)]));
|
||||
// /t/ then /ʃ/ is not what a lone "t" and "ʃ" mean when the affricate is listed,
|
||||
// but a symbol sequence the inventory cannot join still splits.
|
||||
assert_eq!(seg("ɔɪt"), pairs(&[("ɔɪ", N), ("t", N)]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_phone_the_inventory_lacks_is_refused_with_where_and_never_guessed() {
|
||||
let id = EntityId::parse("entity:nibu").unwrap();
|
||||
for (ipa, at, text) in [
|
||||
("ˈnixu", "ˈni".len(), "xu"),
|
||||
("ˈniːbu", "ˈni".len(), "ːbu"),
|
||||
("ˈnibu.", "ˈnibu".len(), "."),
|
||||
("ˈni bu", "ˈni".len(), " bu"),
|
||||
] {
|
||||
assert_eq!(
|
||||
phones_of(ipa),
|
||||
Err(LowerError::UnknownPhone { entity: id.clone(), at, text: text.into() }),
|
||||
"{ipa}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stress_mark_with_no_vowel_to_land_on_is_refused() {
|
||||
let id = EntityId::parse("entity:nibu").unwrap();
|
||||
for (ipa, at) in [("nibuˈ", "nibu".len()), ("ˈˈnibu", 0), ("ˈnˈibu", 0), ("ˈnb", 0)] {
|
||||
assert_eq!(
|
||||
phones_of(ipa),
|
||||
Err(LowerError::DanglingStress { entity: id.clone(), at }),
|
||||
"{ipa}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phones_need_a_ruling_and_an_entity_exactly_as_ipa_does() {
|
||||
assert_eq!(
|
||||
weksa::phones(&view(&proof_packet(), &committed().0), &Entities::default()),
|
||||
Err(LowerError::UnknownEntity { entity: EntityId::parse("entity:nibu").unwrap() })
|
||||
);
|
||||
let mut e = Entities::default();
|
||||
e.held.push(nibu(&[("en-US", &["Nibu"])]));
|
||||
assert_eq!(
|
||||
weksa::phones(&view(&proof_packet(), &committed().0), &e),
|
||||
Err(LowerError::NoPronunciation { entity: EntityId::parse("entity:nibu").unwrap() })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_inventory_parser_refuses_what_it_cannot_read() {
|
||||
for bad in [
|
||||
"i\tvowel\ttense",
|
||||
"i\tvowel\t-\tx",
|
||||
"n\tconsonant\ttense\tx",
|
||||
"i\tvowel\tmid\tx",
|
||||
"ˈi\tvowel\ttense\tx",
|
||||
"\tvowel\ttense\tx",
|
||||
"i\tvowel\ttense\t",
|
||||
"i\tvowel\ttense\ta\ni\tvowel\tlax\tb",
|
||||
] {
|
||||
assert!(PhoneInventory::parse(bad).is_err(), "{bad:?}");
|
||||
}
|
||||
assert!(PhoneInventory::parse("# c\n\ni\tvowel\ttense\ta\n").is_ok());
|
||||
assert!(PhoneInventory::for_language("sa-Latn").is_none());
|
||||
}
|
||||
|
||||
/// `nibu-proof-001` re-cut as the request: the addressee asked Nibu to stop,
|
||||
/// one frame:3 predication (n4), consent naming it, and no other claim.
|
||||
fn asked() -> Packet {
|
||||
|
|
|
|||
50
data/phonology/en-US.phones.tsv
Normal file
50
data/phonology/en-US.phones.tsv
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Weksa phone inventory, en-US (General American, broad). One phone per line, tab separated:
|
||||
# ipa <TAB> class <TAB> tenseness <TAB> gloss
|
||||
# class: consonant | vowel. tenseness: tense | lax | reduced for a vowel, `-` for a consonant. A multi-character
|
||||
# phone (affricate, diphthong) is one entry and one phone. The segmenter reads only this file: an IPA symbol with no
|
||||
# line here is refused, never guessed. Tense/lax is the textbook classification; it is what separates /i/ (nee) from
|
||||
# /ɪ/ (nih).
|
||||
# Provenance of the i/ɪ rows: /i/ is tense and /ɪ/ is lax. Nibu's own ruling of 2026-11-07 (source
|
||||
# weksa-body:follow_up:chat-s3981-3983, #minds: tense vowel, two syllables, stress on the first) chose ˈnibu and rejected
|
||||
# ˈnɪbu; the entity entry records it, this file is what makes the two forms two different phones.
|
||||
# Stress is not a phone: ˈ and ˌ are read by the segmenter and put on the vowel that follows.
|
||||
p consonant - voiceless bilabial stop
|
||||
b consonant - voiced bilabial stop
|
||||
t consonant - voiceless alveolar stop
|
||||
d consonant - voiced alveolar stop
|
||||
k consonant - voiceless velar stop
|
||||
g consonant - voiced velar stop
|
||||
f consonant - voiceless labiodental fricative
|
||||
v consonant - voiced labiodental fricative
|
||||
θ consonant - voiceless dental fricative
|
||||
ð consonant - voiced dental fricative
|
||||
s consonant - voiceless alveolar fricative
|
||||
z consonant - voiced alveolar fricative
|
||||
ʃ consonant - voiceless postalveolar fricative
|
||||
ʒ consonant - voiced postalveolar fricative
|
||||
h consonant - voiceless glottal fricative
|
||||
tʃ consonant - voiceless postalveolar affricate
|
||||
dʒ consonant - voiced postalveolar affricate
|
||||
m consonant - bilabial nasal
|
||||
n consonant - alveolar nasal
|
||||
ŋ consonant - velar nasal
|
||||
l consonant - alveolar lateral approximant
|
||||
ɹ consonant - alveolar approximant
|
||||
j consonant - palatal approximant
|
||||
w consonant - labial-velar approximant
|
||||
i vowel tense close front unrounded (fleece, nee)
|
||||
ɪ vowel lax near-close front unrounded (kit, nih)
|
||||
ɛ vowel lax open-mid front unrounded (dress)
|
||||
æ vowel lax near-open front unrounded (trap)
|
||||
ɑ vowel tense open back unrounded (palm)
|
||||
ɔ vowel tense open-mid back rounded (thought)
|
||||
ʊ vowel lax near-close back rounded (foot)
|
||||
u vowel tense close back rounded (goose, boo)
|
||||
ʌ vowel lax open-mid back unrounded (strut)
|
||||
ə vowel reduced schwa
|
||||
ɚ vowel reduced r-coloured schwa
|
||||
eɪ vowel tense face diphthong
|
||||
oʊ vowel tense goat diphthong
|
||||
aɪ vowel tense price diphthong
|
||||
aʊ vowel tense mouth diphthong
|
||||
ɔɪ vowel tense choice diphthong
|
||||
|
Can't render this file because it has a wrong number of fields in line 11.
|
1
data/phonology/fixtures/nibu-en-US.phones.json
Normal file
1
data/phonology/fixtures/nibu-en-US.phones.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"language":"en-US","source":"ˈnibu","phones":[{"ipa":"n","stress":"none"},{"ipa":"i","stress":"primary"},{"ipa":"b","stress":"none"},{"ipa":"u","stress":"none"}]}
|
||||
Loading…
Reference in a new issue