Add a phonology maker
This commit is contained in:
parent
dd0a756a6b
commit
cc7973df2b
|
@ -0,0 +1,589 @@
|
||||||
|
#! /usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import phonagen
|
||||||
|
import random
|
||||||
|
|
||||||
|
class Stress:
|
||||||
|
"""Stress representation"""
|
||||||
|
def __init__(self):
|
||||||
|
self.transcriptions = {}
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "\u02C8"
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "#stress"
|
||||||
|
|
||||||
|
class SyllableBreak:
|
||||||
|
"""Syllable break representation"""
|
||||||
|
def __init__(self):
|
||||||
|
self.transcriptions = {}
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "."
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "#syllable-break"
|
||||||
|
|
||||||
|
###
|
||||||
|
# Vowels representation and generation
|
||||||
|
class Vowel:
|
||||||
|
"""Vowel representation"""
|
||||||
|
|
||||||
|
# Simplified vowel model
|
||||||
|
matrixPhoneme = [
|
||||||
|
["i", "y", "ɨ", "ɯ", "u"], # close
|
||||||
|
["e", "ø", "ə", "ɤ", "o"], # mid close
|
||||||
|
["ɛ", "œ", "ɐ", "ʌ", "ɔ"], # mid open
|
||||||
|
["æ", "ɶ", "a", "ɑ", "ɒ"], # open
|
||||||
|
]
|
||||||
|
# Vowel height
|
||||||
|
close = 0
|
||||||
|
midClose = 1
|
||||||
|
midOpen = 2
|
||||||
|
open = 3
|
||||||
|
# Vowel backness (+ roundness)
|
||||||
|
frontUnrounded = 0
|
||||||
|
frontRounded = 1
|
||||||
|
central = 2
|
||||||
|
backUnrounded = 3
|
||||||
|
backRounded = 4
|
||||||
|
|
||||||
|
def __init__(self, height = midClose, backness = central):
|
||||||
|
"""Constructor"""
|
||||||
|
self.height = height
|
||||||
|
self.backness = backness
|
||||||
|
self.isNasal = False
|
||||||
|
self.isLong = False
|
||||||
|
self.isStressed = False
|
||||||
|
self.transcriptions = {}
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""To String operator: Get the phoneme representation in IPA"""
|
||||||
|
result = Vowel.matrixPhoneme[self.height][self.backness]
|
||||||
|
if self.isNasal:
|
||||||
|
result = result + "\u0303" # Conbining tilde
|
||||||
|
if self.isLong:
|
||||||
|
result = result + "ː"
|
||||||
|
#
|
||||||
|
return result
|
||||||
|
|
||||||
|
def clone(self):
|
||||||
|
"""Clone the vowel"""
|
||||||
|
result = Vowel(self.height, self.backness)
|
||||||
|
result.isNasal = self.isNasal
|
||||||
|
result.isLong = self.isLong
|
||||||
|
result.isStressed = self.isStressed
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
result = "#vowel"
|
||||||
|
if self.isStressed:
|
||||||
|
result = result + " #stressed"
|
||||||
|
else:
|
||||||
|
result = result + " #unstressed"
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Common vowels
|
||||||
|
Vowel.A = Vowel(Vowel.open, Vowel.central)
|
||||||
|
Vowel.E = Vowel(Vowel.midClose, Vowel.frontUnrounded)
|
||||||
|
Vowel.I = Vowel(Vowel.close, Vowel.frontUnrounded)
|
||||||
|
Vowel.O = Vowel(Vowel.midClose, Vowel.backRounded)
|
||||||
|
Vowel.U = Vowel(Vowel.close, Vowel.backRounded)
|
||||||
|
Vowel.Schwa = Vowel(Vowel.midClose, Vowel.central)
|
||||||
|
# Less common vowels
|
||||||
|
Vowel.openO = Vowel(Vowel.midOpen, Vowel.backRounded)
|
||||||
|
Vowel.openE = Vowel(Vowel.midOpen, Vowel.frontUnrounded)
|
||||||
|
Vowel.Y = Vowel(Vowel.close, Vowel.frontRounded)
|
||||||
|
Vowel.W = Vowel(Vowel.close, Vowel.backUnrounded)
|
||||||
|
Vowel.OE = Vowel(Vowel.midClose, Vowel.frontRounded)
|
||||||
|
Vowel.openOE = Vowel(Vowel.midOpen, Vowel.frontRounded)
|
||||||
|
Vowel.AE = Vowel(Vowel.open, Vowel.frontUnrounded)
|
||||||
|
Vowel.AO = Vowel(Vowel.open, Vowel.backRounded)
|
||||||
|
|
||||||
|
# Distributions of vowel features
|
||||||
|
# Stress
|
||||||
|
stressDistribution = phonagen.Distribution()
|
||||||
|
stressDistribution.addTo(True, 4)
|
||||||
|
stressDistribution.addTo(False, 6)
|
||||||
|
# Long vowels
|
||||||
|
longVowelDistribution = phonagen.Distribution()
|
||||||
|
longVowelDistribution.addTo(True, 2)
|
||||||
|
longVowelDistribution.addTo(False, 8)
|
||||||
|
# Nasal vowels
|
||||||
|
nasalVowelDistribution = phonagen.Distribution()
|
||||||
|
nasalVowelDistribution.addTo(True, 2)
|
||||||
|
nasalVowelDistribution.addTo(False, 8)
|
||||||
|
|
||||||
|
# Base vowels
|
||||||
|
def pickBoolean():
|
||||||
|
return random.choice([True,False])
|
||||||
|
|
||||||
|
# Generative functions
|
||||||
|
def twoVowelSet():
|
||||||
|
if pickBoolean():
|
||||||
|
return (Vowel.A, Vowel.Schwa,) # Open/Close contrast
|
||||||
|
else:
|
||||||
|
return (Vowel.E, Vowel.O,) # Front/Back contrast
|
||||||
|
|
||||||
|
def threeVowelSet():
|
||||||
|
return (Vowel.A, Vowel.I, Vowel.U,) # Extreme of the vowel triangle
|
||||||
|
|
||||||
|
def fourVowelSet():
|
||||||
|
if pickBoolean():
|
||||||
|
return (Vowel.A, Vowel.I, Vowel.U, Vowel.Schwa,) # Extreme + central
|
||||||
|
else:
|
||||||
|
# Choose wether contrast is between close and midOpen or midClose and open
|
||||||
|
heightClose = random.choice([Vowel.close, Vowel.midClose])
|
||||||
|
return (Vowel(heightClose, Vowel.frontUnrounded),
|
||||||
|
Vowel(heightClose, Vowel.backRounded),
|
||||||
|
Vowel(heightClose + 2, Vowel.frontUnrounded),
|
||||||
|
Vowel(heightClose + 2, Vowel.backRounded),)
|
||||||
|
|
||||||
|
def fiveVowelSet():
|
||||||
|
if pickBoolean():
|
||||||
|
return (Vowel.A, Vowel.E, Vowel.I, Vowel.O, Vowel.U,)
|
||||||
|
else:
|
||||||
|
# Choose wether contrast is between close and midOpen or midClose and open
|
||||||
|
heightClose = random.choice([Vowel.close, Vowel.midClose])
|
||||||
|
return (Vowel.Schwa,
|
||||||
|
Vowel(heightClose, Vowel.frontUnrounded),
|
||||||
|
Vowel(heightClose, Vowel.backRounded),
|
||||||
|
Vowel(heightClose + 2, Vowel.frontUnrounded),
|
||||||
|
Vowel(heightClose + 2, Vowel.backRounded),)
|
||||||
|
|
||||||
|
def sixVowelSet():
|
||||||
|
if pickBoolean():
|
||||||
|
return (Vowel.A, Vowel.E, Vowel.I, Vowel.O, Vowel.U, Vowel.Schwa,)
|
||||||
|
else:
|
||||||
|
return (Vowel.I, Vowel.U, Vowel.E, Vowel.O, Vowel.AE, Vowel.AO,)
|
||||||
|
|
||||||
|
def sevenVowelSet():
|
||||||
|
if pickBoolean():
|
||||||
|
return (Vowel.A, Vowel.E, Vowel.I, Vowel.O, Vowel.U, Vowel.openE, Vowel.openO,)
|
||||||
|
else:
|
||||||
|
return (Vowel.I, Vowel.U, Vowel.E, Vowel.O, Vowel.AE, Vowel.AO, Vowel.Schwa,)
|
||||||
|
|
||||||
|
def eightVowelSet():
|
||||||
|
rnd = random.randrange(3)
|
||||||
|
if rnd == 0:
|
||||||
|
return (Vowel.A, Vowel.E, Vowel.I, Vowel.O, Vowel.U, Vowel.openE, Vowel.openO, Vowel.Schwa)
|
||||||
|
elif rnd == 1:
|
||||||
|
central = random.choice([Vowel.central, Vowel.backUnrounded])
|
||||||
|
open = random.choice([Vowel.midOpen, Vowel.open])
|
||||||
|
close = random.choice([Vowel.midClose, Vowel.close])
|
||||||
|
return (Vowel.I, Vowel.E, Vowel.Y, Vowel.openOE,
|
||||||
|
Vowel.U, Vowel.O, Vowel(close, central), Vowel(open, central),)
|
||||||
|
else:
|
||||||
|
return (Vowel.I, Vowel.E, Vowel.openE, Vowel.AE,
|
||||||
|
Vowel.U, Vowel.O, Vowel.openO, Vowel.AO,)
|
||||||
|
|
||||||
|
def nineVowelSet():
|
||||||
|
if pickBoolean():
|
||||||
|
return (Vowel.I, Vowel.E, Vowel.openE, Vowel.AE,
|
||||||
|
Vowel.U, Vowel.O, Vowel.openO, Vowel.AO,
|
||||||
|
Vowel.Schwa,)
|
||||||
|
else:
|
||||||
|
return (Vowel.I, Vowel.E, Vowel.openE,
|
||||||
|
Vowel(Vowel.close, Vowel.central), Vowel.Schwa, Vowel.A,
|
||||||
|
Vowel.U, Vowel.O, Vowel.openO,)
|
||||||
|
|
||||||
|
def tenVowelSet():
|
||||||
|
if pickBoolean():
|
||||||
|
return (Vowel.I, Vowel.E, Vowel.openE,
|
||||||
|
Vowel.Y, Vowel.OE, Vowel.openOE,
|
||||||
|
Vowel.A,
|
||||||
|
Vowel.U, Vowel.O, Vowel.openO,)
|
||||||
|
else:
|
||||||
|
return (Vowel.I, Vowel.Y, Vowel.Schwa, Vowel.W, Vowel.U,
|
||||||
|
Vowel.E, Vowel.openOE, Vowel.A, Vowel(Vowel.midOpen, Vowel.backUnrounded), Vowel.O,)
|
||||||
|
|
||||||
|
def elevenVowelSet():
|
||||||
|
if pickBoolean():
|
||||||
|
return (Vowel.I, Vowel.E, Vowel.openE,
|
||||||
|
Vowel.Y, Vowel.OE, Vowel.openOE,
|
||||||
|
Vowel.A, Vowel.Schwa,
|
||||||
|
Vowel.U, Vowel.O, Vowel.openO,)
|
||||||
|
else:
|
||||||
|
return (Vowel.I, Vowel.E, Vowel.openE, Vowel.AE,
|
||||||
|
Vowel.Y, Vowel.OE, Vowel.openOE,
|
||||||
|
Vowel.U, Vowel.O, Vowel.openO, Vowel.AO,)
|
||||||
|
|
||||||
|
# Distribution
|
||||||
|
baseVowelDistribution = phonagen.Distribution()
|
||||||
|
baseVowelDistribution.addTo(twoVowelSet, 2)
|
||||||
|
baseVowelDistribution.addTo(threeVowelSet, 6)
|
||||||
|
baseVowelDistribution.addTo(fourVowelSet, 8)
|
||||||
|
baseVowelDistribution.addTo(fiveVowelSet, 10)
|
||||||
|
baseVowelDistribution.addTo(sixVowelSet, 8)
|
||||||
|
baseVowelDistribution.addTo(sevenVowelSet, 8)
|
||||||
|
baseVowelDistribution.addTo(eightVowelSet, 6)
|
||||||
|
baseVowelDistribution.addTo(nineVowelSet, 4)
|
||||||
|
baseVowelDistribution.addTo(tenVowelSet, 2)
|
||||||
|
baseVowelDistribution.addTo(elevenVowelSet, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def generateVowelSet():
|
||||||
|
"""Generate a set of vowels for a phonology"""
|
||||||
|
# Choose some language features on the vowel set
|
||||||
|
isStressPhonemic = stressDistribution.pickFrom()
|
||||||
|
isLongVowelPhonemic = longVowelDistribution.pickFrom()
|
||||||
|
isNasalVowelPhonemic = nasalVowelDistribution.pickFrom()
|
||||||
|
# Generate a set of base vowels
|
||||||
|
baseVowelSet = baseVowelDistribution.pickFrom()()
|
||||||
|
# Is stress on long vowel ?
|
||||||
|
isLongStressed = False
|
||||||
|
if isStressPhonemic and isLongVowelPhonemic and pickBoolean():
|
||||||
|
isLongStressed = True
|
||||||
|
#
|
||||||
|
result = []
|
||||||
|
for v in baseVowelSet:
|
||||||
|
result.append(v)
|
||||||
|
if isLongStressed:
|
||||||
|
vls = v.clone()
|
||||||
|
vls.isStressed = True
|
||||||
|
vls.isLong = True
|
||||||
|
result.append(vls)
|
||||||
|
if isNasalVowelPhonemic:
|
||||||
|
vlsn = vls.clone()
|
||||||
|
vlsn.isNasal = True
|
||||||
|
result.append(vlsn)
|
||||||
|
if (not isLongStressed) and isLongVowelPhonemic:
|
||||||
|
vl = v.clone()
|
||||||
|
vl.isLong = True
|
||||||
|
result.append(vl)
|
||||||
|
if isNasalVowelPhonemic:
|
||||||
|
vln = vl.clone()
|
||||||
|
vln.isNasal = True
|
||||||
|
result.append(vln)
|
||||||
|
if (not isLongStressed) and isStressPhonemic:
|
||||||
|
vs = v.clone()
|
||||||
|
vs.isStressed = True
|
||||||
|
result.append(vs)
|
||||||
|
if isNasalVowelPhonemic:
|
||||||
|
vsn = vs.clone()
|
||||||
|
vsn.isNasal = True
|
||||||
|
result.append(vsn)
|
||||||
|
if isNasalVowelPhonemic:
|
||||||
|
vn = v.clone()
|
||||||
|
vn.isNasal = True
|
||||||
|
result.append(vn)
|
||||||
|
return result
|
||||||
|
|
||||||
|
###
|
||||||
|
# Consonants representation and generation
|
||||||
|
class Consonant:
|
||||||
|
"""Consonant representation"""
|
||||||
|
|
||||||
|
# Simplified model
|
||||||
|
matrixPhonemes = [
|
||||||
|
["m", "ɱ", "n", "ɳ", "ɲ", "ŋ", "ɴ", "ɴ"], # nasal
|
||||||
|
["p", "p̪", "t", "ʈ", "c", "k", "q", "ʔ"], # stop voiceless
|
||||||
|
["b", "b̪", "d", "ɖ", "ɟ", "ɡ", "ɢ", "ɢ"], # stop voiced
|
||||||
|
["ɓ", "ɓ̪", "ɗ", "ᶑ", "ʄ", "ɠ", "ʛ", "ʛ"], # stop implosive
|
||||||
|
["pf", "tθ", "ts", "ʈʂ", "tʃ", "kx", "qχ", "ʔh"], # affricate voiceless
|
||||||
|
["bv", "dð", "dz", "ɖʐ", "dʒ", "ɡɣ", "ɢʁ", "ʡʕ" ], # affricate voiced
|
||||||
|
["f", "θ", "s", "ʂ", "ʃ", "x", "χ", "h"], # fricative voiceless
|
||||||
|
["v", "ð", "z", "ʐ", "ʒ", "ɣ", "ʁ", "ɦ"], # fricative voiced
|
||||||
|
["β", "ʋ", "ɹ", "ɻ", "j", "w", "ʁ", "ʕ"], # approximant
|
||||||
|
["ⱱ", "ⱱ", "ɾ", "ɽ", "ɾ", "ɢ̆", "ɢ̆", "ʡ̮"], # tap/flap
|
||||||
|
["ʙ", "ʙ̪", "r", "ɽr", "r", "ʀ", "ʀ", "ʢ"], # trill
|
||||||
|
["l", "l", "l", "ɭ", "ʎ", "ʟ", "ʟ̠", "ʟ̠"], # lateral
|
||||||
|
["ʘ", "ǀ", "ǃ", "ǁ", "ǂ", "ʞ", "ʞ", "ʞ"], # click
|
||||||
|
]
|
||||||
|
|
||||||
|
# left>right: place of articulation:
|
||||||
|
labial = 0
|
||||||
|
dental = 1
|
||||||
|
alveolar = 2
|
||||||
|
retroflex = 3
|
||||||
|
palatal = 4
|
||||||
|
velar = 5
|
||||||
|
uvular = 6
|
||||||
|
glottal = 7
|
||||||
|
# top>bottom: manner
|
||||||
|
nasal = 0
|
||||||
|
stopVoiceless = 1
|
||||||
|
stopVoiced = 2
|
||||||
|
implosive = 3
|
||||||
|
affricateVoiceless = 4
|
||||||
|
affricateVoiced = 5
|
||||||
|
fricativeVoiceless = 6
|
||||||
|
fricativeVoiced = 7
|
||||||
|
approximant = 8
|
||||||
|
tapFlap = 9
|
||||||
|
trill = 10
|
||||||
|
lateral = 11
|
||||||
|
click = 12
|
||||||
|
|
||||||
|
def __init__(self, manner = stopVoiceless, place = alveolar):
|
||||||
|
"""Constructor"""
|
||||||
|
# Primary features
|
||||||
|
self.place = place
|
||||||
|
self.manner = manner
|
||||||
|
# Secondary feature
|
||||||
|
# Phonation
|
||||||
|
self.isEjective = False
|
||||||
|
self.isAspirated = False # or murmured, for voiced
|
||||||
|
self.isGlotalized = False
|
||||||
|
# Secondary articulation
|
||||||
|
self.isLabialized = False
|
||||||
|
self.isPalatalized = False
|
||||||
|
self.isVelarized = False
|
||||||
|
self.isPharyngealized = False
|
||||||
|
#
|
||||||
|
self.transcriptions = {}
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""To String operator: Get the phoneme representation in IPA"""
|
||||||
|
result = Consonant.matrixPhonemes[self.manner][self.place]
|
||||||
|
if self.isEjective:
|
||||||
|
result = result + "ʼ"
|
||||||
|
if self.isAspirated:
|
||||||
|
result = result + "ʰ"
|
||||||
|
if self.isGlotalized:
|
||||||
|
result = result + "ˀ"
|
||||||
|
if self.isLabialized:
|
||||||
|
result = result + "ʷ"
|
||||||
|
if self.isPalatalized:
|
||||||
|
result = result + "ʲ"
|
||||||
|
if self.isVelarized:
|
||||||
|
result = result + "ˠ"
|
||||||
|
if self.isPharyngealized:
|
||||||
|
result = result + "ˤ"
|
||||||
|
#
|
||||||
|
return result
|
||||||
|
|
||||||
|
def clone(self):
|
||||||
|
"""Clone the consonant"""
|
||||||
|
result = Consonant()
|
||||||
|
result.place = self.place
|
||||||
|
result.manner = self.manner
|
||||||
|
result.isEjective = self.isEjective
|
||||||
|
result.isAspirated = self.isAspirated
|
||||||
|
result.isGlotalized = self.isGlotalized
|
||||||
|
result.isLabialized = self.isLabialized
|
||||||
|
result.isPalatalized = self.isPalatalized
|
||||||
|
result.isVelarized = self.isVelarized
|
||||||
|
result.isPharyngealized = self.isPharyngealized
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "#consonant"
|
||||||
|
|
||||||
|
# Has retroflex consonants ?
|
||||||
|
retroflexDistribution = phonagen.Distribution()
|
||||||
|
retroflexDistribution.addTo(True, 5)
|
||||||
|
retroflexDistribution.addTo(False, 15)
|
||||||
|
# Has glottal consonants ?
|
||||||
|
glottalDistribution = phonagen.Distribution()
|
||||||
|
glottalDistribution.addTo(True, 2)
|
||||||
|
glottalDistribution.addTo(False, 18)
|
||||||
|
# Has uvular consonants ?
|
||||||
|
uvularDistribution = phonagen.Distribution()
|
||||||
|
uvularDistribution.addTo(True, 2)
|
||||||
|
uvularDistribution.addTo(False, 18)
|
||||||
|
# Has dental consonants ?
|
||||||
|
dentalDistribution = phonagen.Distribution()
|
||||||
|
dentalDistribution.addTo(True, 1)
|
||||||
|
dentalDistribution.addTo(False, 19)
|
||||||
|
|
||||||
|
# Are the affricates distinguished from stops ?
|
||||||
|
affricateDistribution = phonagen.Distribution()
|
||||||
|
affricateDistribution.addTo(True, 2)
|
||||||
|
affricateDistribution.addTo(False, 18)
|
||||||
|
# Are voiced distinguished from unvoiced ?
|
||||||
|
voicedDistribution = phonagen.Distribution()
|
||||||
|
voicedDistribution.addTo(True, 15)
|
||||||
|
voicedDistribution.addTo(False, 5)
|
||||||
|
# Has click ?
|
||||||
|
clickDistribution = phonagen.Distribution()
|
||||||
|
clickDistribution.addTo(True, 1)
|
||||||
|
clickDistribution.addTo(False, 29)
|
||||||
|
|
||||||
|
# Rhotic realisation
|
||||||
|
rhoticRealisationDistribution = phonagen.Distribution()
|
||||||
|
rhoticRealisationDistribution.addTo(False, 10)
|
||||||
|
rhoticRealisationDistribution.addTo(Consonant.tapFlap, 30)
|
||||||
|
rhoticRealisationDistribution.addTo(Consonant.trill, 30)
|
||||||
|
rhoticRealisationDistribution.addTo(Consonant.approximant, 40)
|
||||||
|
rhoticRealisationDistribution.addTo(Consonant.fricativeVoiced, 20)
|
||||||
|
|
||||||
|
# Is aspiration phonemic ?
|
||||||
|
aspirationDistribution = phonagen.Distribution()
|
||||||
|
aspirationDistribution.addTo(True, 6)
|
||||||
|
aspirationDistribution.addTo(False, 14)
|
||||||
|
|
||||||
|
# TODO: other stuff ?
|
||||||
|
|
||||||
|
|
||||||
|
def generateConsonantSet():
|
||||||
|
"""Generate a set of consonants for a phonology"""
|
||||||
|
# Places features
|
||||||
|
hasRetroflex = retroflexDistribution.pickFrom()
|
||||||
|
hasGlottal = glottalDistribution.pickFrom()
|
||||||
|
hasUvular = uvularDistribution.pickFrom()
|
||||||
|
hasDental = dentalDistribution.pickFrom()
|
||||||
|
|
||||||
|
# Places of articulation
|
||||||
|
# Minimal set
|
||||||
|
places = [Consonant.labial, Consonant.alveolar, Consonant.palatal, Consonant.velar]
|
||||||
|
# Add the other positions
|
||||||
|
if hasRetroflex:
|
||||||
|
places.append(Consonant.retroflex)
|
||||||
|
if hasDental:
|
||||||
|
places.append(Consonant.dental)
|
||||||
|
if hasGlottal:
|
||||||
|
places.append(Consonant.glottal)
|
||||||
|
if hasUvular:
|
||||||
|
places.append(Consonant.uvular)
|
||||||
|
|
||||||
|
# Nominal place is alveolar: this place will get all the possible manners
|
||||||
|
# Other places will be more limited
|
||||||
|
nominalPlace = Consonant.alveolar
|
||||||
|
|
||||||
|
# Manner features
|
||||||
|
hasVoiced = voicedDistribution.pickFrom()
|
||||||
|
hasSeparateAffricates = affricateDistribution.pickFrom()
|
||||||
|
hasClick = clickDistribution.pickFrom()
|
||||||
|
rhoticRealisation = rhoticRealisationDistribution.pickFrom()
|
||||||
|
hasAspirated = aspirationDistribution.pickFrom()
|
||||||
|
|
||||||
|
# Minimal set of manners
|
||||||
|
manners = [Consonant.nasal, Consonant.stopVoiceless, Consonant.fricativeVoiceless, Consonant.approximant]
|
||||||
|
if hasSeparateAffricates:
|
||||||
|
manners.append(Consonant.affricateVoiceless)
|
||||||
|
if hasVoiced:
|
||||||
|
manners = manners + [Consonant.stopVoiced, Consonant.fricativeVoiced]
|
||||||
|
if hasSeparateAffricates:
|
||||||
|
manners.append(Consonant.affricateVoiced)
|
||||||
|
if hasClick:
|
||||||
|
manners.append(Consonant.click)
|
||||||
|
|
||||||
|
# Generate the set of consonants
|
||||||
|
result = []
|
||||||
|
rhoticAdded = False
|
||||||
|
for pl in places:
|
||||||
|
for mn in manners:
|
||||||
|
# there is a small chance that a phoneme not on the nominal place will be skipped
|
||||||
|
if (pl == nominalPlace) or (random.randrange(8) != 0):
|
||||||
|
cons = Consonant(mn, pl)
|
||||||
|
# there may be some modifications on the manner or place depending on how contrastive are the consonants
|
||||||
|
if (not hasSeparateAffricates) and (pl == Consonant.palatal) and (random.randrange(10) < 8):
|
||||||
|
if (mn == Consonant.stopVoiceless):
|
||||||
|
cons.manner = Consonant.affricateVoiceless
|
||||||
|
elif (mn == Consonant.stopVoiced):
|
||||||
|
cons.manner = Consonant.affricateVoiced
|
||||||
|
# TODO : other common modifications
|
||||||
|
result.append(cons)
|
||||||
|
# Rhotic added ?
|
||||||
|
if (mn == rhoticRealisation):
|
||||||
|
rhoticAdded =((mn != Consonant.fricativeVoiced) and (pl == nominalPlace)) or ((mn == Consonant.fricativeVoiced) and (pl == Consonant.uvular))
|
||||||
|
# Aspirated consonants
|
||||||
|
if hasAspirated and (mn >= Consonant.stopVoiceless) and (mn <= Consonant.fricativeVoiced):
|
||||||
|
asp = cons.clone()
|
||||||
|
asp.isAspirated = True
|
||||||
|
result.append(asp)
|
||||||
|
# lateral
|
||||||
|
if ((pl == nominalPlace) and (random.randrange(6) != 0)) or (random.randrange(20) == 0):
|
||||||
|
lat = Consonant(Consonant.lateral, pl)
|
||||||
|
result.append(lat)
|
||||||
|
|
||||||
|
# rhotic
|
||||||
|
if rhoticRealisation and (not rhoticAdded):
|
||||||
|
if (rhoticRealisation != Consonant.fricativeVoiced):
|
||||||
|
rhot = Consonant(rhoticRealisation, nominalPlace)
|
||||||
|
result.append(rhot)
|
||||||
|
else:
|
||||||
|
rhot = Consonant(rhoticRealisation, Consonant.uvular)
|
||||||
|
result.append(rhot)
|
||||||
|
#
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
# Transcriptions
|
||||||
|
def addSimpleLatinTranscription(transcriptions, phonemeList):
|
||||||
|
transcriptions.append('simple-latin')
|
||||||
|
vowelTranslationMatrix = [
|
||||||
|
["i", "ú", "ï", "í", "u"], # close
|
||||||
|
["e", "ê", "ë", "o", "o"], # mid close
|
||||||
|
["é", "ê", "ä", "ó", "ó"], # mid open
|
||||||
|
["á", "a", "a", "a", "â"], # open
|
||||||
|
]
|
||||||
|
consonantTranslationMatrix = [
|
||||||
|
["m", "ḿ", "n", "ň", "ñ", "ǹ", "ń", "ń"], # nasal
|
||||||
|
["p", "ṕ", "t", "ť", "c", "k", "q", "q"], # stop voiceless
|
||||||
|
["b", "ṕ", "d", "ď", "j", "ɡ", "ǵ", "ǵ"], # stop voiced
|
||||||
|
["b'", "b'", "d'", "ď'", "j'", "g'", "ǵ'", "ǵ'"], # stop implosive
|
||||||
|
["pf", "tŝ", "ts", "tš", "tś", "kx", "qẍ", "qh"], # affricate voiceless
|
||||||
|
["bv", "dẑ", "dz", "dž", "dź", "ǵĝ", "ǵr", "ǵh" ], # affricate voiced
|
||||||
|
["f", "ŝ", "s", "š", "ś", "x", "ẍ", "h"], # fricative voiceless
|
||||||
|
["v", "ẑ", "z", "ž", "ź", "ĝ", "r", "h"], # fricative voiced
|
||||||
|
["v", "v", "r", "ř", "y", "w", "r", "h"], # approximant
|
||||||
|
["ṽ", "ṽ", "r", "ř", "r", "gy", "gr", "hg"], # tap/flap
|
||||||
|
["br", "br", "rr", "řr", "ry", "ŕr", "ŕr", "hŕ"], # trill
|
||||||
|
["l", "l", "l", "ľ", "ly", "ĺl", "ĺl", "ĺl"], # lateral
|
||||||
|
["ʘ", "ǀ", "ǃ", "ǁ", "ǂ", "ʞ", "ʞ", "ʞ"], # click
|
||||||
|
]
|
||||||
|
nasalSign = random.choice(["\u0328", "\u0330", "n"]) # combining ogonek, combining tilde below, n
|
||||||
|
for ph in phonemeList:
|
||||||
|
tr = ""
|
||||||
|
if isinstance(ph, Vowel):
|
||||||
|
tr = vowelTranslationMatrix[ph.height][ph.backness]
|
||||||
|
if ph.isNasal:
|
||||||
|
tr = tr + nasalSign
|
||||||
|
if ph.isLong:
|
||||||
|
tr = tr + tr # Double
|
||||||
|
if isinstance(ph, Consonant):
|
||||||
|
tr = consonantTranslationMatrix[ph.manner][ph.place]
|
||||||
|
if ph.isAspirated:
|
||||||
|
tr = tr + "h"
|
||||||
|
ph.transcriptions.update({'simple-latin': tr})
|
||||||
|
|
||||||
|
def makePhonology(id, description):
|
||||||
|
phonology = phonagen.Phonology(id = id, description = description)
|
||||||
|
# Define phonemes, from their IPA notation
|
||||||
|
phonemeList = []
|
||||||
|
# Step 0: stress, syllable break
|
||||||
|
phonemeList.append(Stress())
|
||||||
|
phonemeList.append(SyllableBreak())
|
||||||
|
# Step 1: Vowels
|
||||||
|
phonemeList = phonemeList + generateVowelSet()
|
||||||
|
# Step 2: consonants
|
||||||
|
phonemeList = phonemeList + generateConsonantSet()
|
||||||
|
# Step 3: Transcriptions, and decide the main
|
||||||
|
transcriptions = ['phoneme']
|
||||||
|
addSimpleLatinTranscription(transcriptions, phonemeList)
|
||||||
|
# set main transcription
|
||||||
|
phonology.transcriptions = transcriptions
|
||||||
|
# TODO: change this
|
||||||
|
phonology.mainTranscription = 'simple-latin'
|
||||||
|
# Step 4: translate phoneme into phonology entries
|
||||||
|
for ph in phonemeList:
|
||||||
|
id = str(ph)
|
||||||
|
if isinstance(ph, Vowel) and ph.isStressed:
|
||||||
|
id = "'" + id
|
||||||
|
entry = {'id': id, 'description': ph.getDescription(), 'phoneme': str(ph)}
|
||||||
|
for tr in ph.transcriptions:
|
||||||
|
entry.update({tr: ph.transcriptions[tr]})
|
||||||
|
phonology.entries.update({entry['id']: entry})
|
||||||
|
return phonology
|
||||||
|
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
# Define argument parser
|
||||||
|
parser = argparse.ArgumentParser(description='Make a new phonology.')
|
||||||
|
parser.add_argument('--id', metavar='id', help='id of the phonology', required = True)
|
||||||
|
parser.add_argument('--description', metavar='description', help='description of the phonology; empty if not provided', default='')
|
||||||
|
parser.add_argument('--output', metavar='output-file', help='Output file for the generator. The file is printed to standard output if not given.', default='')
|
||||||
|
# Parse arguments
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
# Main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
args = parseArgs()
|
||||||
|
phonology = makePhonology(args.id, args.description)
|
||||||
|
outputFile = phonagen.PhonagenFile()
|
||||||
|
outputFile.addPhonology(phonology)
|
||||||
|
outputFile.writeTo(args.output)
|
Loading…
Reference in New Issue