Nói lái bằng Python

Nhân ngày mưa gió lười chẳng ra đường chụp ảnh, anh bèn chia sẻ cách nói lái bằng Python.

Thế nào là nói lái?

Về cơ bản thì nói lái là hoán đổi một hoặc hai thành tố (âm đầu, vần, và thanh điệu) giữa hai âm tiết của từ ngữ ban đầu để tạo ra từ ngữ nói lái. Ví dụ:

  • “bí mật” <=> “bị mất”: hoán đổi thanh điệu
  • “bí mật” <=> “mất bị”: hoán đổi âm đầu và vần
  • “bí mật” <=> “bật mí”: hoán đổi vần và thanh điệu

Còn “bí mật” <=> “mật bí” không phải là nói lái mà chỉ là đảo vị trí cả hai âm tiết.

Người ta xác định là có sáu kiểu nói lái tất cả, cụ thể anh sẽ nêu ở phần sau. Và quan trọng là dù nói lái kiểu gì thì nếu A đã nói lái ra B thì B cũng phải nói lái được thành A.

Ngoài ra người ta còn nói lái từ ngữ có ba âm tiết kiểu như “ban lãnh đạo” <=> “bao lãnh đạn”. Thì thực ra là giữ nguyên âm tiết giữa và nói lái theo hai âm tiết đầu và cuối.

Giải pháp

Trước tiên phải xác định được âm đầu, vần và thanh điệu thì tương đối dễ, vì hôm trước anh đã giới thiệu cách xác định cấu trúc âm tiết rồi.

Bây giờ thử nói lái cụm từ “mèo cái”. Anh sẽ chia nó thành hai âm tiết, rồi xác định cấu trúc từng âm tiết:

word = "mèo cái"
_word = word.split(' ')

reversed_word = word.split(' ').reverse()

syllables = []
syllables.append(syllable_parser(_word[0]))
syllables.append(syllable_parser(_word[-1]))

Tiếp theo là anh định nghĩa một hàm để nói lái:

def make_spoonerism(syllables, mode):

    nucleuses = {
        'a':  ['a', 'à', 'ả', 'ã', 'á', 'ạ'],
        'ă':  ['ă', 'ằ', 'ẳ', 'ẵ', 'ắ', 'ặ'],
        'â':  ['â', 'ầ', 'ẩ', 'ẫ', 'ấ', 'ậ'],
        'e':  ['e', 'è', 'ẻ', 'ẽ', 'é', 'ẹ'],
        'ê':  ['ê', 'ề', 'ể', 'ễ', 'ế', 'ệ'],
        'i':  ['i', 'ì', 'ỉ', 'ĩ', 'í', 'ị'],
        'o':  ['o', 'ò', 'ỏ', 'õ', 'ó', 'ọ'],
        'oo': ['oo', 'oò', 'oỏ', 'oõ', 'oó', 'oọ'],
        'ô':  ['ô', 'ồ', 'ổ', 'ỗ', 'ố', 'ộ'],
        #'ôô':  ['ôô', 'ôồ', 'ôổ', 'ôỗ', 'ôố', 'ôộ'],
        'ơ':  ['ơ', 'ờ', 'ở', 'ỡ', 'ớ', 'ợ'],
        'u':  ['u', 'ù', 'ủ', 'ũ', 'ú', 'ụ'],
        'ư':  ['ư', 'ừ', 'ử', 'ữ', 'ứ', 'ự'],
        'y':  ['y', 'ỳ', 'ỷ', 'ỹ', 'ý', 'ỵ'],
        'ia': ['ia', 'ìa', 'ỉa', 'ĩa', 'ía', 'ịa'],
        'iê': ['iê', 'iề', 'iể', 'iễ', 'iế', 'iệ'],
        'ua': ['ua', 'ùa', 'ủa', 'ũa', 'úa', 'ụa'],
        'uô': ['uô', 'uồ', 'uổ', 'uỗ', 'uố', 'uộ'],
        'ưa': ['ưa', 'ừa', 'ửa', 'ữa', 'ứa', 'ựa'],
        'ươ': ['ươ', 'ườ', 'ưở', 'ưỡ', 'ướ', 'ượ'],
        'ya': ['ya', 'ỳa', 'ỷa', 'ỹa', 'ýa', 'ỵa'],
        'yê': ['yê', 'yề', 'yể', 'yễ', 'yế', 'yệ'],
    }
    
    swapped = [[], []]
    for j in range(0, 6):
        swapped[0].append(syllables[0][j])
        swapped[1].append(syllables[1][j])

Hàm này có hai tham số syllables (một list chứa cấu trúc của hai âm tiết) và mode (kiểu nói lái).

Biến nucleuses chứa các nguyên âm kèm theo thanh điệu. Biến swapped là một list mà giá trị ban đầu là sao chép cấu trúc hai âm tiết từ biến syllables.

Tiếp theo là định nghĩa các kiểu nói lái:

# [syllable, tone, onset, glide, nucleus, coda, rhyme]
#  0         1     2      3      4        5     6

match mode:
    # Đảo vần và thanh
    # mèo cái <=> mái kèo
    case 'rhyme_tone':
        swapped[0][1] = syllables[1][1]
        swapped[0][3] = syllables[1][3]
        swapped[0][4] = syllables[1][4]
        swapped[0][5] = syllables[1][5]
        
        swapped[1][1] = syllables[0][1]
        swapped[1][3] = syllables[0][3]
        swapped[1][4] = syllables[0][4]
        swapped[1][5] = syllables[0][5]

    # Đảo thanh
    # mèo cái <=> méo cài
    case 'tone':
        swapped[0][1] = syllables[1][1]

        swapped[1][1] = syllables[0][1]
    
    # Đảo âm đầu, thanh 
    # mèo cào <=> kèo mào
    case 'onset_tone':
        swapped[0][1] = syllables[1][1]
        swapped[0][2] = syllables[1][2]
        
        swapped[1][1] = syllables[0][1]
        swapped[1][2] = syllables[0][2]

    # Đảo vần
    # mèo cái <=> mài kéo
    case 'rhyme':
        swapped[0][3] = syllables[1][3]
        swapped[0][4] = syllables[1][4]
        swapped[0][5] = syllables[1][5]
        
        swapped[1][3] = syllables[0][3]
        swapped[1][4] = syllables[0][4]
        swapped[1][5] = syllables[0][5]

    # Đảo âm đầu
    # mèo cái <=> kèo mái
    case 'onset':
        swapped[0][2] = syllables[1][2]

        swapped[1][2] = syllables[0][2]

    # Đảo âm đầu, vần
    # mèo cái <=> cài méo
    case 'onset_rhyme':
        swapped[0][2] = syllables[1][2]
        swapped[0][3] = syllables[1][3]
        swapped[0][4] = syllables[1][4]
        swapped[0][5] = syllables[1][5]
        
        swapped[1][2] = syllables[0][2]
        swapped[1][3] = syllables[0][3]
        swapped[1][4] = syllables[0][4]
        swapped[1][5] = syllables[0][5]

Ở trường hợp hoán đổi âm đầu và vần, ví dụ “quần rách” sẽ thành “ràch quấn”, nhưng “ràch” là “âm tiết” không hợp lệ. Cái này thì (hình như theo âm vị học truyền thống) sẽ phải đổi âm cuối theo các cặp như này:

  • m – p: đâM thóc <=> đong thấP
  • n – t: liềN vách <=> viếT lành
  • ng – c: đồNG bạc <=> bàng độC
  • nh – ch: điển tíCH <=> đỉNH tiết

Tức là nếu âm cuối là p, t, c, ch (khép) mà thanh điệu không phải là sắc hay nặng thì phải đổi thành các âm cuối tương ứng là m, n, ng, nh (mở).

Nhưng nếu một âm tiết còn lại có âm cuối là /-zero/ (không biểu hiện bằng chữ viết) như “sử” chẳng hạn thì sẽ không áp dụng nguyên tắc trên. Bởi nếu “lịch sử” nói lái theo kiểu hoán đổi âm đầu và thanh điệu, đồng thời đổi âm cuối theo trường hợp vừa nên sẽ cho ra “sỉnh lự”. Nhưng “sử lịnh” lại không có cách nào nói lái ra “lịch sử” được. Do đó, trường hợp này được coi là không thể nói lái.

Những chuyện lằng nhằng này sẽ được xử lí như sau:

# Chuyển đổi âm cuối khép thành âm cuối mở tương ứng

limited_codas = [ ['n', 't'], ['m' , 'p'], ['ng', 'c'], ['nh', 'ch'] ]
for i in [0, 1]:        
    if swapped[i][5] in ['p', 't', 'c', 'ch'] and swapped[i][1] not in ['5', '6']:
        # và nếu một trong hai âm cuối là /-zero/ ...
        if swapped[i-1][5] == '':
            # ... thì không thể nói lái
            return False
        
        for coda in limited_codas:
            if swapped[i][5] in coda:
                swapped[i][5] = coda[0]
            if swapped[i-1][5] in coda:
                swapped[i-1][5] = coda[1]
        break

Sau cùng là nối lại các thành phần để ra từ nói lái. Tất nhiên là mọi sự nó cũng không hề xuôi chèo mát mái, mà còn phải xem lại Sự thể hiện bằng chữ quốc ngữ của các âm vị chiết đoạn tiếng Việt.

new_syllables = [{}, {}]
newword = ['', '']

for i in [0, 1]:
    structure = swapped[i]

    tone = int(structure[1]) - 1
    onset = structure[2]
    glide = structure[3]
    nuclear = structure[4]
    coda = structure[5]

    # -Y- => -I-, UI- > UY-
    if nuclear == 'y' and onset != '':
        nuclear = 'i'
    if nuclear == 'i' and glide != '':
        nuclear = 'y'

    # -YÊ- => -IÊ-, UIÊ- > UYÊ-, UIA > UYA
    if nuclear == 'yê' and onset != '':
        nuclear = 'iê'
    if nuclear == 'iê' and glide != '':
        nuclear = 'yê'
    if nuclear == 'ia' and glide != '':
        nuclear = 'ya'

    # Ka Ngho Ghu => Ca, Ngo, Gu 
    if onset in ['k', 'gh', 'ngh'] and nuclear not in ['i', 'e', 'ê', 'ia', 'iê'] and glide == '':
        onset_with_ie = {'k': 'c', 'ngh': 'ng', 'gh': 'g'}
        onset = onset_with_ie[onset]

    # Ci, Nge, Gê => Ki Nghe Ghê
    if nuclear in ['i', 'e', 'ê', 'ia', 'iê'] and onset in ['c', 'g', 'ng'] and glide == '':
        onset_with_ie = {'c': 'k', 'ng': 'ngh', 'g': 'gh'}
        onset = onset_with_ie[onset]

    # Glide
    if glide == 'o' and nuclear in ['â', 'i', 'ê', 'ơ', 'y', 'yê']:
        glide = 'u'
    if glide == 'u' and nuclear not in ['â', 'i', 'ê', 'ơ', 'y', 'yê']:
        glide = 'o'

    # Q- => C/K
    if onset == 'q' and glide == '':
        if nuclear not in ['i', 'e', 'ê', 'ia', 'iê']:
            onset = 'c'
        else:
            onset = 'k'
    
    # Cw/Kw => QU
    if onset in ['c', 'k'] and glide != '':
        onset = 'q'
        glide = 'u'

    # Qo => QU
    if onset == 'q' and glide == 'o':
        glide ='u'

    # GI + I/IA/IÊ
    if onset == 'gi' and nuclear in ['i', 'iê'] and glide == '':
        onset = 'g'
    if onset == 'gi' and nuclear == 'ia' and glide == '':
        onset = 'z' # gi-ia => gia != gia <-- gi-a

    nucleus = nucleuses.get(nuclear)
    new_syllables[i]['onset'] = onset
    new_syllables[i]['glide'] = glide 
    new_syllables[i]['nucleus'] = nucleus[tone]
    new_syllables[i]['coda'] = coda

    newword[i] = ''.join([new_syllables[i]['onset'], new_syllables[i]['glide'], new_syllables[i]['nucleus'], new_syllables[i]['coda']])

return ' '.join(newword)

Thế mới thấy học Ngôn ngữ học cũng có ích :v

Đã xong hàm nói lái. Giờ anh quay lại với cụm từ “mèo cái”, đối chiếu ba cặp thành tố thanh điệu, âm đầu, và vần liệu, nếu có dưới hai cặp giống nhau thì mới nói lái được. Và anh lần lượt cho nói lái theo 6 kiểu và

spoonerism = []
samefactor = 0
for s in [1, 2, 6]:
    if syllables[0][s] == syllables[1][s]:
        samefactor += 1

if samefactor < 2:
    for mode in ['onset', 'rhyme', 'tone', 'onset_rhyme', 'onset_tone', 'rhyme_tone']:
        p = make_spoonerism(syllables, mode)
        if p and p not in spoonerism and p != word and p != ' '.join(reversed_word):
            spoonerism.append(p)

Và in ra kết quả cuối cùng:

if len(spoonerism) > 0:
    print('\n'.join(spoonerism))

sẽ được:

kèo mái
mài kéo
méo cài
cài méo
kéo mài
mái kèo

Ứng dụng

Ngoài mục đích giải trí thì anh đem giải pháp này làm thành tính năng nói lái cho công cụ Tìm từ ngữ theo vần đôi của Dự án S.