Skip to content
Snippets Groups Projects
RealizationDescriptions.py 46.78 KiB
import datetime
import logging
import os

from collections import Counter, defaultdict
from itertools import chain

from shellvalier.settings import BASE_DIR, DEBUG

from meanings.models import LexicalUnit, Synset
from semantics.models import SemanticRole, RoleAttribute

from entries.phrase_descriptions.utils import get_form
from entries.phrase_descriptions.polish_strings import TO
from entries.phrase_descriptions.descriptions import make_phraseologisms

from importer.Phrase import Case, Preposition, Modification, Words, LexPhrase, Fixed, NP, LexNP, LexNumP, PrepNP, LexPrepNP, LexPrepGerP, AdjP, LexAdjP, LexPrepAdjP, PActP, LexPActP
from importer.RealizationDescriptionUtils import *

def get_prefs_list(argument):
    return sorted(
        map(str, argument.predefined.all())
    ) + sorted(
        map(str, argument.synsets.all())
    ) + sorted(
        map(str, argument.relations.all())
    )

LOCATION_ROLES = {'Location', 'Path'}

def select_predefined(predefs):
    if len(predefs) == 1:
        return predefs[0]
    return 'ALL'
    # TODO inne heurystyki?
    raise RealisationDescriptionError('couldn’t choose predef lemma: {}'.format('/'.join(predefs)))

def select_predefined_for_xp(predefs, role):
    if predefs == ['ISTOTY']:
        return 'ISTOTY'
    return 'ALL'
    # TODO heurystyki?
    raise RealisationDescriptionError('couldn’t choose predef lemma for XP: {}'.format('/'.join(predefs)))

def get_predefined_lemma(argument, xp=False):
    predefined = argument.predefined.all()
    if not predefined:
        return None
    predefs = sorted(p.name for p in predefined)
    role = argument.role.role.role
    if role not in LOCATION_ROLES and {'LUDZIE', 'PODMIOTY'}.intersection(predefs):
        return ['LUDZIE']
    if xp:
        return [select_predefined_for_xp(predefs, role)]
    else:
        return [select_predefined(predefs)]

def get_hyponyms(synset, seen=None, tab=' '):
    if seen is None:
        seen = set()
    hyponyms = set()
    for hypo in synset.hyponyms.all():
        if hypo not in seen:
            seen.add(hypo)
            hyponyms.add(hypo)
            hyponyms.update(get_hyponyms(hypo, seen, tab=tab + '  '))
    return hyponyms

# for benchmarking
BENCH3 = defaultdict(list)

# precalculated for the largest ones
HYPONYM_CACHE = {
    # sklep-1
    4747 : 46,
    # obiekt budowlany-1
    53426 : 590,
    # konstrukcja-1
    7218 : 614,
    # cecha człowieka-1
    36347 : 676,
    # aberracja-1 nieprawidłowość-1 zaburzenie-2 zakłócenie-3
    4127 : 700,
    # znak-1
    7416 : 732,
    # coś na ząb-1 jedzenie-2 pokarm-1 pożywienie-3 żywność-1
    10738 : 766,
    # materiał-1 tworzywo-1
    1612 : 879,
    # jednostka miary-1 jednostka-4 miano-2 miara-3
    1161 : 881,
    # związek chemiczny-1 związek-1
    19589 : 882,
    # zjawisko naturalne-1
    5351 : 901,
    # dzieło-2 praca-6
    7469 : 927,
    # część-1
    462 : 957,
    # cecha czynności-1 cecha działania-1
    5953 : 1033,
    # część-3
    104936 : 1056,
    # cecha fizyczna-1
    5464 : 1056,
    # wypowiedź-1
    3998 : 1062,
    # proces-1
    54253 : 1103,
    # ciąg wydarzeń-1 ciąg zdarzeń-1
    47401 : 1107,
    # grupa-2 zespół ludzi-1 zespół-2
    7653 : 1176,
    # człowiek charakteryzowany ze względu na kwalifikacje-1
    6779 : 1188,
    # substancja chemiczna-1
    5233 : 1206,
    # przyrząd-1
    7425 : 1260,
    # ilość-1
    1078 : 1427,
    # grupa ludzi-1 grupa-5 ludzie-1
    7702 : 1510,
    # kategoria-3 pojęcie-2
    8170 : 1522,
    # urządzenie-5
    7446 : 1524,
    # historia-3 wydarzenie-1 wypadek-3 zdarzenie-2
    6526 : 1533,
    # grupa istot-1
    103330 : 1585,
    # miejsce-1
    4750 : 1632,
    # stan-1
    3243 : 1761,
    # narzędzie-1
    7610 : 1800,
    # roślina-1
    4603 : 1928,
    # artefakt-1 twór-5 wytwór-2
    2605 : 2029,
    # człowiek ze względu na swoje zajęcie-1
    6797 : 2184,
    # nazwa człowieka uwzględniająca jego cechy-1 nosiciel cechy-1
    6778 : 2308,
    # płód-3 wytwór umysłu-1
    8137 : 2599,
    # człowiek ze względu na relacje społeczne-1
    6775 : 2642,
    # fenomen-1 zjawisko-1
    5371 : 2674,
    # środek-1
    28294 : 2793,
    # człowiek, który coś robi-1
    241977 : 2828,
    # substancja-1
    5236 : 2871,
    # zwierzę-1
    5621 : 2966,
    # materia-3
    247979 : 2970,
    # spowodowanie-1 sprawienie-1
    102579 : 4255,
    # atrybut-1 cecha-1 przymiot-1 własność-2 właściwość-1
    323 : 4579,
    # grupa-4 zbiór-1
    1282 : 4587,
    # uczynienie-1 zrobienie-1
    102576 : 4851,
    # całość-1 ogół-1
    2129 : 5668,
    # człowiek-1 istota ludzka-1 jednostka-2 osoba-1
    6047 : 6151,
    # osoba-4
    28688 : 6170,
    # wytwór-1
    2903 : 7230,
    # efekt-1 rezultat-1 skutek-1 wynik-1
    5195 : 7915,
    # przedmiot-1
    2646 : 7552,
    # istota żywa-1 stworzenie-5 twór-1
    6045 : 8448,
    # istota-1
    1027 : 8536,
    # czynność-1
    10765 : 8653,
    # rzecz-4
    103156 : 9480,
    # egzemplarz-1 indywiduum-1 jednostka-3 organizm-1 osobnik-2
    6731 : 10609,
    # obiekt-2
    234224 : 21435,
}

def select_synsets(synsets):
    by_num_hyponyms = defaultdict(set)
    for synset in synsets:
        sid = synset.id
        if sid not in HYPONYM_CACHE:
            #-------
            t1 = datetime.datetime.now()
            #-------
            hyponyms = get_hyponyms(synset)
            HYPONYM_CACHE[sid] = len(hyponyms)
            #-------
            t2 = datetime.datetime.now()
            # deciseconds :)
            d = round((t2 - t1).total_seconds() * 10)
            if DEBUG:
                BENCH3[d].append((HYPONYM_CACHE[sid], sid, synset))
            # ----
        N = HYPONYM_CACHE[sid]
        by_num_hyponyms[N].add(synset)
    M = max(by_num_hyponyms.keys())
    return list(by_num_hyponyms[M])

FREQ = Counter()
with open(os.path.join(BASE_DIR, 'data/freq/sgjp-freq-23032021.tab')) as f:
    for l in f:
        lemma, pos, freq = l.strip('\n').split('\t')
        if pos not in ('adj', 'subst'):
            continue
        freq = int(freq)
        if freq < 10:
            continue
        # this is inaccurate, but conflate multiple occurrences
        FREQ[lemma] += freq

def rank_units(units, ranker):
    buckets = defaultdict(set)
    for unit in units:
        buckets[ranker(unit)].add(unit)
    ranked = dict()
    for rank, (n, unts) in enumerate(sorted(buckets.items())):
        for unit in unts:
            ranked[unit] = rank
    return ranked

meaning_no_ranker = lambda unit: int(unit.sense)
# TODO lepiej mniej znaczeń (bardziej specyficzne -> precyzyjniejsze?) czy więcej (częstsze -> bardziej zrozumiałe?)
num_meanings_ranker = lambda unit: LexicalUnit.objects.filter(base=unit.base).count()
# w ten sposób nadajemy też najniższy priorytet wielowyrazowym, jeśli istnieje 1-wyrazowa notowana na liście frek.
freq_ranker = lambda unit: -FREQ.get(unit.base, 0)
words_ranker = lambda unit: len(unit.base.split())


# różnice przejrzane oczami na próbce dla:
# [meaning_no_ranker, freq_ranker, num_meanings_ranker]
# [freq_ranker, meaning_no_ranker, num_meanings_ranker] -> [freq_ranker, num_meanings_ranker, meaning_no_ranker] -> takie same wyniki na próbce, TODO sugestia Eli: druga opcja brzmi intuicyjniej
# [num_meanings_ranker, meaning_no_ranker, freq_ranker]
# [meaning_no_ranker, num_meanings_ranker, freq_ranker]

def select_units(units, rankers=[freq_ranker, num_meanings_ranker, meaning_no_ranker, words_ranker]):
    units = [unit for unit in units if (unit.base, unit.sense) not in UNIT_KILL_LIST]
    unit2rank = defaultdict(lambda: [0 for i in range(len(rankers))])
    for i, ranker in enumerate(rankers):
        for unit, rank in rank_units(units, ranker).items():
            unit2rank[unit][i] = rank
    by_rank = defaultdict(set)
    for unit, rank in unit2rank.items():
        by_rank[tuple(rank)].add(unit)
    #for rank, units in sorted(by_rank.items()):
    #    print('        ***', rank, units)
    return sorted(by_rank.items())[0][1]

LEMMA_CACHE = dict()

#returns [lemmata], is_predef
def get_synsets_lemma(argument, pos):
    synsets = argument.synsets.filter(lexical_units__pos=pos).distinct()
    synsets = [(Synset.objects.get(id=SYNSET_MAP[s.id]) if s.id in SYNSET_MAP else s) for s in synsets if s.id not in SYNSET_KILL_LIST]
    if not synsets:
        return None
    key = tuple(sorted(map(str, synsets)))
    if key in LEMMA_CACHE:
        return LEMMA_CACHE[key]
    synsets = synsets if len(synsets) == 1 else select_synsets(synsets)
    for synset in synsets:
        if synset.id in SYNSET2LEMMA:
            return [SYNSET2LEMMA[synset.id]], True
    units = list(chain.from_iterable(synset.lexical_units.all() for synset in synsets))
    units = [units[0]] if len(units) == 1 else select_units(units)
    ret = (sorted(unit.base for unit in units), False)
    if ret[0] == ['cecha czynności', 'cecha działania']:
        return (['cecha'], False)
    LEMMA_CACHE[key] = ret
    return ret

# for benchmarking
BENCH2 = defaultdict(list)

def get_argument_lemma(argument, xp=False):
    t1 = datetime.datetime.now()
    ret = get_argument_lemma2(argument, xp=xp)
    t2 = datetime.datetime.now()
    # deciseconds :)
    d = round((t2 - t1).total_seconds() * 10)
    if DEBUG:
        BENCH2[d].append((argument.predefined.all(), argument.synsets.all(), ret))
    return ret

def get_argument_lemma2(argument, xp=False):
    lemma = get_predefined_lemma(argument, xp=xp)
    if lemma:
        return lemma, True
    lemma = get_synsets_lemma(argument, 'noun')
    if lemma:
        # get_synsets_lemma returns [lemmata], is_predef
        return lemma
    lemma = get_synsets_lemma(argument, 'adj')
    if lemma:
        return lemma
    # TODO!!! np. akuratność
    return ['ALL'], True
    lemma = get_relations_lemma(argument)
    assert(lemma)
    return lemma, False

# nie powinny występować razem:
#   * LUDZIE + PODMIOTY
#   * MIEJSCE + OTOCZENIE + POŁOŻENIE

def process_lemma(lemma, phrase_type):
    mod = NATR
    if lemma in PREDEF2LEMMA:
        lemma, gend, num, pos, mod = PREDEF2LEMMA[lemma].get(phrase_type, PREDEF2LEMMA[lemma]['_'])
        return lemma, gend, num, pos, mod
    
    if ' ' in lemma:
        # eg. ‹środki pieniężne›
        words = lemma.split(' ')
        tags = []
        for i, word in enumerate(words):
            tags.append(sorted(get_simplified_tags(word)))
        if len(words) == 2 and 'subst:nom' in tags[0] and 'subst:gen' in tags[1]:
            # np. ‹dziedzina wiedzy›
            lemma = words[0]
            mod = make_npgen_mod(words[1])
        elif len(words) == 2 and 'subst:nom' in tags[0] and 'adj' in tags[1]:
            # np. ‹środki pieniężne›
            # ‹napój wyskokowy› -> ‹napój› również impt,
            # ‹stan psychiczny› -> ‹psychiczny› również subst,
            lemma = words[0]
            mod = make_adjp_mod(words[1])
            mod._order = 'post'
        elif len(words) == 2 and 'subst:nom' in tags[1] and 'adj' in tags[0]:
            # np. ‹zły uczynek›
            lemma = words[1]
            mod = make_adjp_mod(words[0])
        elif len(words) == 2 and 'subst:nom' in tags[0] and 'pact' in tags[1]:
            # np. ‹pojazd latający›
            lemma = words[0]
            mod = make_pactp_mod(words[1])
            mod._order = 'post'
        elif len(words) == 2 and 'subst:nom' in tags[0] and 'ger:gen' in tags[1]:
            # np. ‹język programowania›
            lemma = words[0]
            # nie mamy lexgerp, więc używamy fixed
            mod = make_fixed_mod(words[1])
            mod._order = 'post'
        elif len(words) == 3 and 'subst:nom' in tags[0] and 'prep:gen' in tags[1] and 'subst:gen' in tags[2]:
            # np. ‹maszyna do szycia›
            lemma = words[0]
            mod = make_prepnp_mod(words[2], words[1], 'gen')
        else:
            raise RealisationDescriptionError('couldn’t parse lemma: {} {}'.format(lemma, tags))
    
    if lemma == 'lata':
        return 'rok', 'm3', 'pl', 'subst', mod
    if lemma in GERUNDS:
        return lemma, 'n', 'sg', 'subst', mod
    
    subst_sg_interps = get_interps(lemma, lemma=lemma, tag_constraints=['subst', 'sg', 'nom'])
    if subst_sg_interps:
        return lemma, get_gender(subst_sg_interps), 'sg', 'subst', mod
    subst_pl_interps = get_interps(lemma, lemma=lemma, tag_constraints=['subst', 'pl', 'nom'])
    if subst_pl_interps:
        # lemat „mnogi” notowany w Morfeuszu jako plurale tantum, np. ‹środki›
        return lemma, get_gender(subst_pl_interps), 'pl', 'subst', mod
    pt_interps = get_interps(lemma, tag_constraints=['subst', 'pl', 'nom'])
    if pt_interps: 
        # lemat „mnogi” nie notowany w Morfeuszu, jako plurale tantum, np. ‹pieniądze›
        lemmata = set(lemma for lemma, tag in pt_interps)
        if len(lemmata) == 1:
            return lemmata.pop(), get_gender(pt_interps), 'pl', 'subst', mod
    if get_interps(lemma, lemma=lemma, tag_constraints=['adj', 'sg', 'nom', 'm1']):
        # przymiotnik
        return lemma, None, 'sg', 'adj', mod
    ger_interps = get_interps(lemma, tag_constraints=['ger', 'sg', 'nom'])
    if ger_interps:
        # gerundium
        lemmata = set(lemma for lemma, tag in ger_interps)
        if len(lemmata) == 1:
            return lemmata.pop(), 'n', 'sg', 'ger', mod
    
    raise RealisationDescriptionError('couldn’t process lemma: {} {}'.format(lemma, get_interps(lemma)))
    
    '''
    # TODO rodzaj w zależności od hiperonimów?
    if lemma == 'członek':
        return lemma, 'sg', 'subst', mod
    try:
        get_form(lemma, ['subst', 'sg', 'nom'])
        return lemma, 'sg', 'subst', mod
    except:
        pass
    try:
        # lemat „mnogi” notowany w Morfeuszu jako plurale tantum, np. ‹środki›
        get_form(lemma, ['subst', 'pl', 'nom'])
        return lemma, 'pl', 'subst', mod
    except:
        pass
    try:
        # przymiotnik
        get_form(lemma, ['adj', 'sg', 'nom', 'm1'])
        return lemma, 'sg', 'adj', mod
    except:
        # lemat „mnogi” nie notowany w Morfeuszu, jako plurale tantum, np. ‹pieniądze›
        subst_pl_nom_lemmata = set(interp[2][1].split(':')[0] for interp in morfeusz.analyse(lemma) if interp[2][2].startswith('subst:pl:nom'))
        if len(subst_pl_nom_lemmata) == 1:
            return subst_pl_nom_lemmata.pop(), 'pl', 'subst', mod
        print('============', lemma)
        print('============', subst_pl_nom_lemmata)
        raise
    '''

PREP_2GRAMS = Counter()
with open(os.path.join(BASE_DIR, 'data/freq/2grams_prep_nkjp')) as f:
    for l in f:
        digram, freq = l.strip('\n').split('\t')
        freq = int(freq)
        PREP_2GRAMS[digram] = freq

XP2PREPNP = {
    'abl'   : (('z', 'gen'),),
    # do domu / na basen
    'adl'   : (('do', 'gen'), ('na', 'acc'),),
    # w mieście, na wsi, u Janka
    'locat' : (('w', 'loc'), ('na', 'loc',), ('u', 'gen'),),
    'perl'  : (('przez', 'acc'),),
    'temp'  : (('podczas', 'gen'),),
    'dur'   : (('przez', 'acc'),),
}

def xp2prepnp(advcat, lemma, num):
    if advcat in XP2PREPNP:
        preps = XP2PREPNP[advcat]
        if len(preps) == 1:
            return preps[0]
        else:
            ranked = []
            for prep, case in preps:
                form = get_form(lemma, ['subst', num, case])[0]
                digram = '{} {}'.format(prep, form)
                ranked.append((-PREP_2GRAMS[digram], (prep, case)))
            return sorted(ranked)[0][1]
    else:
        return None, None

XP2COMPREPNP = {
     'caus'  : 'z powodu',
     # TODO: ożywione: dla ..., nieożywione: w celu ...
     'dest'  : 'w celu',
     'instr' : 'za pomocą',
}

def generate_phrases(function, negativity, phrase, lemma, is_predef, head_gender, controller=None, controller_grammar=None):
    phrase_type = phrase._name
    dummy_id = None
    
    # jak dotąd tylko jeden przypadek zagnieżdżonej frazy lex:
    # zależeć: _: : imperf: subj{np(str);ncp(str,int)} + {prepnp(od,gen);prepncp(od,gen,int)} + {xp(mod[comprepnp(na sposób);advp(mod);lex(prepnp(w,acc),sg,'sposób',atr({adjp(agr)}))])}
    if isinstance(phrase, LexPhrase):
        return make_phraseologisms(phrase, function, negativity, controller=controller, controller_grammar=controller_grammar), None, None
    
    if is_predef and phrase_type == 'xp' and not phrase._category._limitations:
        advcat = phrase._category._value
        # np. „komuś podobało się gdzieś”
        return [PREDEFXP[advcat][lemma]], 'n', 'sg'
    
    distrp = False
    processed_lemma, gend, num, pos, mod = process_lemma(lemma, phrase_type)
    if phrase_type in ('adjp', 'prepadjp') and pos != 'adj':
        # np. aborcja - Manner - lek - adjp(agr)/xp(instr) -> ‹jakaś aborcja›
        processed_lemma, gend, pos, mod = 'jakiś', None, 'adj', NATR
    if phrase_type == 'nonch':
        phrase_type = 'np'
        phrase = NP(Case('nom'), dummy_id)
        # bo nonch może być realizowana wyłącznie przez ‹coś› itp.
        processed_lemma, gend, pos, mod = 'coś', 'n', 'subst', NATR
        # i przetwarzanie dalej jako np
    if phrase_type == 'distrp':
        # ‘po jabłku’ byłoby OK, ale np. ‘po pieniądzach’ brMzmi idiotycznie, więc
        # robimy np(gen) i potem dokleimy ‘po ileś’ (czegoś)
        distrp = True
        phrase_type = 'np'
        phrase = NP(Case('gen'), dummy_id)
        # i przetwarzanie dalej jako np
    
    #print('PHRASE TYPE:', phrase_type, 'LEMMA:', processed_lemma, 'MODIFICATION:', mod, 'FUNCTION:', function)
    words = Words('concat', 'xor', [processed_lemma])
    
    # TODO
    if phrase_type in ('cp', 'ncp', 'prepncp'):
        cptype = phrase._type._value
        assert(cptype in ('int', 'rel') or not phrase._type._realisations)
        phr = None
        if cptype == 'int':
            if phrase._type._realisations:
                phr = '/'.join(phrase._type._realisations) + ''
            else:
                phr = 'kto/co/czy/… robi/się dzieje/…'
        elif cptype == 'rel':
            if phrase._type._realisations:
                phr = '/'.join(phrase._type._realisations) + ''
            else:
                phr = 'kto co robi/co się dzieje/…'
        elif cptype == 'żeby2':
            comp = 'że' if negativity != 'neg' else 'żeby'
            phr = 'że coś się stało'
        elif cptype in ('żeby', 'jakoby', 'jakby',):
            phr = '{} coś się stało'.format(cptype)
        elif cptype in ('że', 'bo', 'gdy', 'jak', 'jeśli', 'kiedy',):
            phr = '{} coś się dzieje'.format(cptype)
        elif cptype in ('', 'zanim',):
            phr = '{} coś się stanie'.format(cptype)
        else:
            print(phrase)
            1 / 0
        if phrase_type == 'cp':
            return [phr], 'n', 'sg'
        if phrase_type == 'ncp':
            return ['{}, {}'.format(TO[phrase._case._value], phr)], 'n', 'sg'
        if phrase_type == 'prepncp':
            return ['{} {}, {}'.format(phrase._prep._value, TO[phrase._prep._case._value], phr)], 'n', 'sg'
    if phrase_type == 'or':
        # TODO? absurd „coś się dzieje”? absurd: coś się dzieje?
        return ['„coś się dzieje”'], 'n', 'sg'
    if phrase_type in ('refl', 'recip'):
        # TODO?
        return ['się'], None, None
    if phrase_type == 'advp':
        # TODO!
        if pos == 'adj':
            return [adj2adv(processed_lemma)], None, None
        # dla nie-przymiotników i tak nic nie wymyślimy
        return ['jakoś'], None, None
    if phrase_type == 'infp':
        # TODO?
        return ['coś robić' if negativity != 'neg' else 'czegoś robić'], 'n', 'sg'
    if phrase_type == 'E':
        # TODO?
        return [''], 'n', 'sg'
    
    if pos == 'adj' and phrase_type not in ('possp', 'adjp', 'prepadjp',):
        # TODO? np. aktualizacja - Manner - automatyczny - xp(instr)
        # TODO źle się generuje dla chlastać, ale tam Instrument ma pref. przymiotnikową ‹ostry›, powinno być raczej ‹ostrze›
        phrase_type = 'adjp'
        phrase = AdjP(Case('agr'), dummy_id)
        # i przetwarzanie dalej jako adjp
    
    if phrase_type == 'possp' and processed_lemma == 'czyjś':
        return [get_form(processed_lemma, ['sg', 'nom', head_gender, 'pos'])[0]], None, None
    if phrase_type == 'comprepnp':
        # TODO wielowyrazowe! ‹abonament w wysokości środków pieniężnych›
        # TODO może ładniej by było „w czyjejś sprawie”, „na czyjąś rzecz”, ale
        # to trochę trudniejsze
        return make_comprepnp(phrase._prep._value, words, num, mod), None, None
        #return ['{} {}'.format(phrase._prep._value, get_form(lemma, [num, 'gen'])[0])]
    
    lex_phrases = []
    phrases = []
    
    if phrase_type == 'np':
        # gerundium; TODO? lista wyjątków jeśli więcej
        if (processed_lemma, function, phrase._case._value) == ('przyrządzanie', 'subj', 'str'):
            return ['przyrządzanie'], 'n', 'sg'
        if (processed_lemma, function, phrase._case._value) == ('szarpnięcie', None, 'inst'):
            return ['szarpnięciem'], 'n', 'sg'
        lex_phrases.append(LexNP(phrase, num, words, mod, dummy_id))
    if phrase_type == 'possp':
        np = NP(Case('gen'), dummy_id)
        lex_phrases.append(LexNP(np, num, words, mod, dummy_id))
    if phrase_type == 'prepnp':
        # gerundium; TODO? lista wyjątków jeśli więcej
        if (processed_lemma, phrase._prep._case._value, phrase._prep._value) == ('przyrządzanie', 'gen', 'do'):
            return ['do przyrządzania'], None, None
        if phrase._prep._value in ('między', 'pomiędzy', 'wśród', 'pośród') and processed_lemma not in ('ktoś', 'coś'):
            num = 'pl'
        if pos == 'subst':
            lex_phrases.append(LexPrepNP(phrase, num, words, mod, dummy_id))
        if pos == 'ger':
            lex_phrases.append(LexPrepGerP(phrase, num, 'aff', words, '', mod, dummy_id))
    if phrase_type == 'adjp':
        # TODO! gender & control
        lex_phrases.append(LexAdjP(phrase, 'sg', head_gender if head_gender else 'm1', 'pos', words, mod, dummy_id))
    if phrase_type == 'prepadjp':
        lex_phrases.append(LexPrepAdjP(phrase, 'sg', 'm1', 'pos', words, mod, dummy_id))
    if phrase_type == 'compar':
        lex_phrases.append(make_compar(phrase, words, num, mod, controller))
    if phrase_type == 'xp':
        if phrase._category._limitations:
            for realisation in phrase._category._limitations:
                phrs, g, n = generate_phrases(function, negativity, realisation, lemma, is_predef, head_gender)
                for phr in phrs:
                    if phr not in phrases:
                        phrases.append(phr)
            return phrases, 'n', 'sg'
        else:
            advcat = phrase._category._value
            if advcat == 'mod':
                phrase2 = NP(Case('inst'), dummy_id)
                lex_phrases.append(LexNP(phrase2, num, words, mod, dummy_id))
            prep, case = xp2prepnp(advcat, processed_lemma, num)
            if prep:
                phrase2 = PrepNP(Preposition(prep, Case(case)), dummy_id)
                lex_phrases.append(LexPrepNP(phrase2, num, words, mod, dummy_id))
            if advcat in XP2COMPREPNP:
                if pos == 'subst':
                    comprep = XP2COMPREPNP[advcat]
                    return make_comprepnp(comprep, words, num, mod), None, None
                if pos == 'ger':
                    assert(mod == NATR)
                    return ['{} {}'.format(comprep, get_form(processed_lemma, ['ger', num, 'gen', head_gender])[0])], 'n', 'sg'
    
    for lex_phrase in lex_phrases:
        for phr in make_phraseologisms(lex_phrase, function, negativity, controller=controller, controller_grammar=controller_grammar):
            if phr not in phrases:
                # TODO? porządna lista wyjątków, jeśli będzie więcej
                if phr == 'na członek rodziny':
                    phr = 'na członka rodziny'
                if distrp:
                    # po iluś facetów/po ileś dziewczyn/kotów...
                    phr = 'po {} {}'.format('iluś' if gend == 'm1' else 'ileś', phr)
                phrases.append(phr)
    
    assert(phrases)
    return phrases, gend if phrase_type == 'np' else None, num if phrase_type == 'np' else None

def get_lex_gender_number(phrase):
    if isinstance(phrase, LexNP):
        number = phrase._number
        # take the first lemma since first expansion is taken for whole meaning description
        lemma = phrase._words._lemmas[0]
        if lemma == 'siebie':
            gender = 'm1'
        elif lemma == 'łupień':
            gender = 'm2'
        else:
            interps = get_interps(lemma, lemma=lemma, tag_constraints=['subst', 'nom'])
            gender = get_gender(interps)
        return gender, number if number != '_' else 'sg'
        '''
        genders = list()
        for lemma in phrase._words._lemmas:
            if lemma == 'siebie':
                genders.append('m1')
            elif lemma == 'łupień':
                genders.append('m2')
            else:
                interps = get_interps(lemma, lemma=lemma, tag_constraints=['subst', 'nom'])
                genders.append(get_gender(interps))
        return genders[0], number if number != '_' else 'sg'
        '''
    if isinstance(phrase, LexNumP):
        # take the first lemma since first expansion is taken for whole meaning description
        lemma = phrase._words._lemmas[0]
        interps = get_interps(lemma, lemma=lemma, tag_constraints=['subst', 'nom'])
        gender = get_gender(interps)
        lemma = phrase._nums._lemmas[0]
        recs = set()
        if lemma == '2':
            recs.add('congr')
        else:
            for interp in get_interps(lemma, lemma=lemma, tag_constraints=['num', 'nom']):
                recs.add(interp[1].split(':')[-1])
        assert(len(recs) == 1)
        rec = recs.pop()
        if rec == 'rec':
            # wiele/pięciu/trzydzieści osiem kotów/facetów/kobiet przyszło
            return 'n', 'sg'
        else:
            # trzy kobiety/koty przyszły/trzej faceci przyszli
            return gender, 'pl'
    return None, None

PHRASE_CACHE = dict()

PHRASE_SEP = ' / '

# for benchmarking
BENCH = defaultdict(list)

def get_phrase_description(subentry, argument, position, phrase, controller_grammar=None):
    t1 = datetime.datetime.now()
    ret = get_phrase_description2(subentry, argument, position, phrase, controller_grammar=controller_grammar)
    t2 = datetime.datetime.now()
    # deciseconds :)
    d = round((t2 - t1).total_seconds() * 10)
    if DEBUG:
        BENCH[d].append((subentry.entry.name, argument.role.role.role, ret[0]))
    return ret

# subentry, argument: DB model objects
# schema, phrase: importer objects
def get_phrase_description2(subentry, argument, position, phrase, controller_grammar=None):
    #print()
    #print(argument)
    #print(phrase)
    gender, number = None, None
    function = position._function._value if position._function else None
    control = None
    if position._control:
        #assert(len(position._control) == 1)
        #control = position._control[0]._function
        ee = [c._function for c in position._control if c._function.endswith('controllee')]
        er = [c._function for c in position._control if c._function.endswith('controller')]
        assert(len(ee) <= 1)
        assert(len(er) <= 1)
        # e.g. ‹uznać› — controllee and pred_controller on the same position, take controllee
        if ee:
            control = ee[0]
        else:
            control = er[0]
    negativity = subentry.negativity.name if subentry.negativity else '_'
    head_lemma, head_gender = subentry.entry.name, None
    
    controller, controller_features, controller_function = None, None, None
    if control and control.endswith('controllee'):
        controller = position._schema.getController(control)
        try:
            controller_features = controller_grammar[controller]
        except KeyError:
            controller_features = ('m1', 'sg')
            logging.warning('{} couldn’t determine grammar features for {}: {} {}; assuming m1 sg'.format(subentry.entry.name, ' '.join(map(str, argument.frame.lexical_units.all())), control, phrase))
        controller_function = controller._function._value if controller._function else None
    
    if subentry.entry.pos.tag == 'noun':
        interps = get_interps(head_lemma, lemma=head_lemma, tag_constraints=['subst', 'nom'])
        head_gender = get_gender(interps)
    
    # TODO
    # TODO gender, number
    # TODO (‹jakieś›) oko * (‹jakieś›) oczy *błyszczy* z powodu substancji
    if isinstance(phrase, LexPhrase) or isinstance(phrase, Fixed):
        phrs = []
        # TODO to powinny być tylko brakujące [...] w lex(cp)
        try:
            for phr in make_phraseologisms(phrase, function, negativity, controller=controller, controller_grammar=controller_features):
                if phr not in phrs:
                    phrs.append(phr)
        except:
            phrs.append('!!!???')
        gender, number = get_lex_gender_number(phrase)
        return PHRASE_SEP.join(phrs), gender, number
    lemmata, is_predef = get_argument_lemma(argument, xp=(phrase._name == 'xp' and not phrase._category._limitations))
    if len(lemmata) != 1:
        raise RealisationDescriptionError('couldn’t choose single lemma: {}'.format('/'.join(lemmata)))
    phrases = []
    # TODO since there’s one lemma, drop the loop
    for lemma in lemmata:
        key = (function, negativity, str(phrase), lemma, str(head_gender), control, controller_features, controller_function)
        if key in PHRASE_CACHE:
            lemma_phrases, gender, number = PHRASE_CACHE[key]
        else:
            lemma_phrases, gender, number = generate_phrases(function, negativity, phrase, lemma, is_predef, head_gender, controller=controller, controller_grammar=controller_features)
            PHRASE_CACHE[key] = (lemma_phrases, gender, number)
        phrases += lemma_phrases
    return PHRASE_SEP.join(phrases), gender, number

def get_only_value(d):
    return list(d.values())[0]

PRIORITY, ATTR, SUBPRIORITY = 'priority', 'attr', 'subpriority'
LOW_PRIORITY = 200
CP_PRIO = {
    'żeby'   : 0, # że
    'kiedy'  : 0, # gdy, jak
    'żeby2'  : 1, # jak
    'że'     : 2, # jak
    # prefer phrases introduced by complementisers where present
    'int' : LOW_PRIORITY + 1,
}
PHRASE_PRIORITY = {
    'xp' : {
        PRIORITY : 10,
        ATTR : lambda phrase: phrase._category._value,
        SUBPRIORITY : {
            'adl'   : 0, # nawigacja xp(adl)/xp(locat)
            'locat' : 1, # powycierać xp(abl)/xp(locat)
            'caus'  : 2, # ucierpieć xp(caus)/xp(temp)
        },
    },
    'np' : {
        PRIORITY : 20,
        ATTR : lambda phrase: phrase._case._value,
        SUBPRIORITY : {
            'str' : 0,
        },
    },
    'prepnp' : {
        PRIORITY : 22,
        ATTR : lambda phrase: (phrase._prep._value, phrase._prep._case._value),
        SUBPRIORITY : {
            ('do', 'gen')      : 0, # adekwatny do/dla; kolejka do/za
            ('za', 'inst')     : 1, # agitować za/przeciw
            ('o', 'acc')       : 1, # apel o/przeciw
            ('w', 'acc')       : 1, # całować w/po
            ('w', 'loc')       : 1, # defilada w/na pojeździe
            ('między', 'inst') : 2, # debata między/z/wśród
            ('o', 'loc')       : 2, # debata o/wokół/nad
            ('wobec', 'gen')   : 2, # dług wobec/względem, konsekwentny wobec/dla
            ('dla', 'gen')     : 3, # certyfikat dla/za
            ('z', 'gen')       : 2, # dochód z/za/od
            ('o', 'acc')       : 3, # kampania o/za
            ('pod', 'inst')    : 4, # kruszyć się pod/od
            ('o', 'loc')       : 4, # książka o czymś/z czegoś
            ('po', 'loc')      : 5, # odlatywać od/po
            ('od', 'gen')      : 6, # podatek od/za
            ('przeciw', 'dat') : 7, # przestępstwo z/przeciw
            ('na', 'loc')      : 7, # skoncentrować się na/nad
            ('za', 'acc')      : 7, # zabulić na/za
            ('z', 'acc')       : LOW_PRIORITY + 1, # mandat – błąd w danych, jest tam też za:acc
        },
    },
    'comprepnp' : {
        PRIORITY : 24,
        ATTR : lambda phrase: phrase._prep._value,
        SUBPRIORITY : {
            'w sprawie'   : 0, # w kwestii
            'w zakresie'  : 0, # dyletant w zakresie/w kwestii
            'w kwestii'   : 1, # dyskrecja co do/w kwestii
            'z dziedziny' : 1, # referat w dziedzinie/z dziedziny
        },
    },
    'cp' : {
        PRIORITY : 30,
        ATTR : lambda phrase: phrase._type._value,
        SUBPRIORITY : CP_PRIO,
    },
    'ncp' : {
        PRIORITY : 32,
        ATTR : lambda phrase: phrase._type._value,
        SUBPRIORITY : CP_PRIO,
    },
    'prepncp' : {
        PRIORITY : 34,
        ATTR : lambda phrase: phrase._type._value,
        SUBPRIORITY : CP_PRIO,
    },
}

def get_phrase_priority(phrase):
    lex = False
    if isinstance(phrase, LexPhrase):
        lex = True
        phrase = phrase._lex_phrase()
    phrase_type = phrase._name
    if phrase_type == 'xp' and phrase._category._limitations:
        # TODO? heurystyka: bierzemy pierwszą
        phrase, phrase_type = phrase._category._limitations[0], phrase._category._limitations[0]._name
    if phrase_type not in PHRASE_PRIORITY:
        return (LOW_PRIORITY, LOW_PRIORITY)
    attr = PHRASE_PRIORITY[phrase_type][ATTR](phrase)
    # lower the priority by 1 for lexes, eg. dostępność prepnp(dla, gen)/lex(prepnp(‹dla kieszeni›))
    return (PHRASE_PRIORITY[phrase_type][PRIORITY] + (1 if lex else 0), PHRASE_PRIORITY[phrase_type][SUBPRIORITY].get(attr, LOW_PRIORITY))

# position: importer object
# phrase_descriptions: dict
#    key: phrase importer object
#    value: (description, gender, number)
# result: phrase description to use in the realisation description
def select_phrase_description(position, phrase_descriptions):
    #print(type(position))
    #print(phrase_descriptions)
    if len(phrase_descriptions) == 1:
        desc = get_only_value(phrase_descriptions)
        assert(desc[0] != '???')
        return desc
    by_priority = defaultdict(set)
    for p, d in phrase_descriptions.items():
        by_priority[get_phrase_priority(p)].add((p, d))
    min_priority_phrases = by_priority[min(by_priority.keys())]
    if len(min_priority_phrases) == 1:
        p, desc = min_priority_phrases.pop()
        assert (desc[0] != '???')
        return desc
    else:
        # all are lex phrases
        assert(all(isinstance(p, LexPhrase) for p, d in min_priority_phrases))
        # all have the same grammatical type
        assert(len(set(str(p._lex_phrase()) for p, d in min_priority_phrases)) == 1)
        # heuristic: return first lexicographically
        return sorted(min_priority_phrases, key=lambda x: x[1][0])[0][1]
        #raise RealisationDescriptionError('couldn’t select phrase description: {}'.format(' * '.join(desc[0] for desc in phrase_descriptions.values())))


FUNCTION_RANK = {
    'subj' : 0,
    'head' : 0,
    'obj'  : 2,
    None   : 4,
}

def is_np(phrase, case):
    if phrase._name != 'np':
        return False
    if isinstance(phrase, LexPhrase):
        return phrase._np._case._value == case
    else:
        return phrase._case._value == case

# TODO: possp na początku tylko, jeśli jest przymiotnikowe
def get_argument_realisation_priority(ar, entry_pos):
    position = ar._position
    function = position._function._value if position._function else None
    # first rank by subj or possp, obj, rest
    rank1 = FUNCTION_RANK[function]
    phrase_types = set(phrase._name for phrase in position._phrases)
    if (phrase_types == {'adjp'} and entry_pos == 'noun') or phrase_types == {'possp'}:
        # jakieś COŚ, ale UCZYNIĆ kogoś jakimś
        rank1 = 0
        # np(dat) after verb ‹ktoś daje komuś coś›
    if [p for p in ar._position._phrases if is_np(p, 'dat')]:
        rank1 = 1
    # np(str) without function (TODO? error in data, e.g. chwytać ustami *powietrze* – should be obj?)
    if function is None and [p for p in ar._position._phrases if is_np(p, 'str')]:
        rank1 = 3
    # clauses at the end
    if {'cp', 'ncp', 'prepncp'}.issuperset(phrase_types):
        rank1 = 5
    # then rank by phrase type: refl/recip, then nominal, then rest
    rank2 = 2
    if {'refl', 'recip'}.intersection(phrase_types):
        rank2 = 0
    elif 'np' in phrase_types:
        rank2 = 1
    # finally rank by semantic argument priority
    sem_role = ar._argument._semantic_role
    role_prio = SemanticRole.objects.get(role=sem_role._value).priority
    attribute_prio = RoleAttribute.objects.get(attribute=sem_role._attribute).priority if sem_role._attribute else 0
    rank3 = (role_prio, attribute_prio)
    return [rank1, rank2, rank3]

# jeśli nie ma nic na początku, a jest np(dat), to przesuwamy na początek
def rerank(ars):
    #print(ars)
    before, after, np_dat = [], [], []
    for rank, fallback, ar in ars:
        if rank[0] == 0:
            before.append((rank, fallback, ar))
        elif [p for p in ar._position._phrases if is_np(p, 'dat')]:
            np_dat.append((rank, fallback, ar))
        else:
            after.append((rank, fallback, ar))
    if before:
        return ars
    else:
        #assert(len(np_dat) <= 1) #TODO? hasło: daleki
        return [([0] + rank[1:], fallback, ar) for rank, fallback, ar in np_dat] + after

# for multi-position Lemma arguments, e.g. dostać się z deszczu pod rynnę

FALLBACK = {
    'z deszczu' : 1,
    'pod rynnę' : 2,
    'od ściany' : 1,
    'do ściany' : 2,
    'żywcem'   : 1,
    'ze skóry' : 2,
    'pięknym'    : 1,
    'za nadobne' : 2,
    'od Annasza'  : 1,
    'do Kajfasza' : 2,
    'z (brudnymi) buciorami / z (swoimi) buciorami / z (brudnymi swoimi) buciorami / z (brudnymi) butami / z (swoimi) butami / z (brudnymi swoimi) butami' : 1,
    'do łóżka / do łóżek'                                                                                                                                  : 2,
    'samego'            : 1,
    'w (‹jakieś›) ręce' : 2,
    'z (‹jakiejś›) radości / z (‹jakiegoś›) szczęścia' : 1,
    'pod sufit'                                        : 2,
    'z jednej skrajności' : 1,
    'w drugą'             : 2,
    'ze skrajności' : 1,
    'w skrajność'   : 2,
    'z motyką'  : 1,
    'na słońce' : 2,
    'z nogi'  : 1,
    'na nogę' : 2,
    'z pustego' : 1,
    'w próżne'  : 2,
    'z (‹jakiejś›) klasy'  : 1,
    'do (‹jakiejś›) klasy' : 2,
    'z (‹jakiegoś›) kwiatka' : 1,
    'na (‹jakiś›) kwiatek'   : 2,
    'w dno'    : 1,
    'od spodu' : 2,
    'po rozum' : 1,
    'do głowy' : 2,
    'z pazurami / z pięściami' : 1,
    'do oczu'                  : 2,
    'na ziemię' : 1,
    'z obłoków' : 2,
    'prosto' : 1,
    'w (‹jakieś›) serce / w (‹jakieś›) serca' : 2,
    'z rąk'  : 1,
    'do rąk' : 2,
    'z ręki'  : 1,
    'do ręki' : 2,
    'o pomstę' : 1,
    'do nieba' : 2,
    'ze zbiornika' : 1,
    'do zbiornika' : 2,
    'samo'                                     : 1,
    'do (‹jakiejś›) ręki / do (‹jakichś›) rąk' : 2,
    'sama'                                     : 1,
    'w (moje/pańskie/Anny/…) (‹jakieś›) ręce'  : 2,
    'sam'                                         : 1,
    'przed (moje/pańskie/Anny/…) (‹jakieś›) oczy' : 2,
    'sama'                                        : 1,
    'do (mojej/pańskiej/Anny/…) (‹jakiejś›) ręki / do (moich/pańskich/Anny/…) (‹jakichś›) rąk' : 2,
}

def fallback(description):
    return FALLBACK.get(description, 0)

WINIEN = ('powinien', 'winien',)

# realisation: importer object
# subentry: DB model object
# TODO wszystkie lex-y chyba powinny wejść do tej reprezentacji,
# np. ktoś babrze ‹sobie› ‹rączki›: ‹sobie› nie jest powiązane z argumentem...
def get_realisation_description(realisation, subentry, aspect):
    entry = subentry.entry
    ars = [(get_argument_realisation_priority(ar, entry.pos.tag), fallback(ar._description), ar) for ar in realisation._argument_realizations]
    #print([(p1, p2, ar._description) for p1, p2, ar in ars])
    try:
        ars = sorted(ars)
    except:
        raise RealisationDescriptionError('couldn’t order argument realisations: {}'.format(' * '.join('{}{} {}'.format(ar._argument._semantic_role._value, ar._argument._semantic_role._attribute, ar._description) for ar in realisation._argument_realizations)))
    if entry.pos.tag == 'verb':
        # dla innych nie przesuwamy np(dat): bliski *komuś*
        ars = rerank(ars)
    before = [('<b>{}</b>' if ar._argument._semantic_role._value == 'Lemma' else '{}').format(ar._description.split(PHRASE_SEP)[0]) for rank, fallback, ar in ars if rank[0] == 0]
    after = [('<b>{}</b>' if ar._argument._semantic_role._value == 'Lemma' else '{}').format(ar._description.split(PHRASE_SEP)[0]) for rank, fallback, ar in ars if rank[0] > 0]
    subj_ars = [ar for ar in realisation._argument_realizations if ar._position._function and ar._position._function._value == 'subj']
    if len(subj_ars) > 1:
        raise RealisationDescriptionError('> 1 subject argument realisations: {}'.format(' * '.join('{}{} {}'.format(ar._argument._semantic_role._value, ar._argument._semantic_role._attribute, ar._description) for ar in subj_ars)))
    subj_ar = subj_ars[0] if subj_ars else None
    head_ars = [ar for ar in realisation._argument_realizations if ar._position._function and ar._position._function._value == 'head']
    if len(head_ars) > 1:
        raise RealisationDescriptionError('> 1 head argument realisations: {}'.format(' * '.join('{}{} {}'.format(ar._argument._semantic_role._value, ar._argument._semantic_role._attribute, ar._description) for ar in head_ars)))
    head_ar = head_ars[0] if head_ars else None
    entry_form = entry.name
    if entry.name == 'naleźć':
        #TODO błąd w słowniku
        aspect = 'perf'
    if entry.pos.tag == 'adj' and head_ar:
        entry_form = get_form(entry.name, ['adj', head_ar._number, 'nom', head_ar._gender, 'pos'])[0]
    elif entry.name == 'bootować':
        # nienotowane w Morfeuszu
        entry_form = 'bootuje'
    elif entry.name == 'wtyczkować':
        # nienotowane w Morfeuszu
        entry_form = 'wtyczkuje'
    elif entry.pos.tag == 'verb':
        assert(aspect)
        entry_base = entry.name
        if entry_base == 'doprząc':
            entry_base = 'doprzęgnąć'
        if aspect == '_':
            # eg. aresztować
            aspect = 'imperf'
        try:
            subj_num = subj_ar._number if subj_ar else 'sg'
            if subj_ar and (aspect == 'perf' or entry_base in WINIEN):
                # potrzebne tylko dla dokonanych (zrobił/a/o) i winien/na
                if subj_ar._gender:
                    subj_gend = subj_ar._gender
                else:
                    raise RealisationDescriptionError('couldn’t determine subject’s gender: {} {} {}'.format(subj_ar, subj_ar._position._phrases, subj_ar._argument))
            else:
                # no subject: ‹jestem kotem — olśniło kogoś›
                subj_gend = 'n'
            if entry_base in WINIEN:
                entry_form = get_form(entry_base, ['winien', subj_num, subj_gend, 'imperf'])[0]
            elif aspect == 'imperf':
                # niedokonane: fin (cz. teraźnieszy)
                # TODO? lista wyjątków, jeśli będzie więcej
                if entry_base == 'sparować' and subj_num == 'sg':
                    # bokser sparuje — imperf nienotowane w Morfeuszu
                    entry_form = 'sparuje'
                else:
                    entry_form = get_form(entry_base, ['fin', subj_num, 'ter', 'imperf'])[0]
            else:
                # dokonane: praet (cz. przeszły)
                # TODO? lista wyjątków, jeśli będzie więcej
                if entry_base == 'nasuwać' and (subj_num, subj_gend) == ('sg', 'm1'):
                    # „Nasuwał się mebli przy odnawianiu mieszkania.” — perf nienotowane w Morfeuszu
                    entry_form = 'nasuwał'
                elif entry_base == 'wybzykać' and (subj_num, subj_gend) == ('sg', 'm1'):
                    # nienotowane w Morfeuszu
                    entry_form = 'wybzykał'
                elif entry_base == 'wytuszować' and (subj_num, subj_gend) == ('sg', 'm1'):
                    # nienotowane w Morfeuszu
                    entry_form = 'wytuszował'
                elif entry_base == 'zależeć' and (subj_num, subj_gend) == ('sg', 'm2'):
                    # nienotowane w Morfeuszu
                    entry_form = 'zależał'
                elif entry_base == 'zemdlić' and (subj_num, subj_gend) == ('sg', 'f'):
                    # formy inne niż „zemdliło” nienotowane w Morfeuszu
                    entry_form = 'zemdliła'
                else:
                    entry_form = get_form(entry_base, ['praet', subj_num, subj_gend, 'perf', ['nagl', '']])[0]
        except:
            entry_form = get_form(entry_base, ['pred'])[0]
        if entry.name == 'napaść' and {'wal_69620-mng', 'wal_80242-mng', 'wal_174604-mng', 'wal_174605-mng', 'wal_174603-mng', 'wal_174606-mng'}.issuperset(realisation._frame._meanings):
            # znaczenie ‹napaść (się) jedzeniem›
            entry_form = entry_form.replace('', '')
        if entry.name == 'oblec' and {'wal_85605-mng', 'wal_85615-mng'}.issuperset(realisation._frame._meanings):
            # znaczenie ‹oblec twierdzę›
            entry_form = entry_form.replace('', '')
        if entry.name == 'odpaść' and {'wal_68230-mng', 'wal_68225-mng', 'wal_79689-mng'}.issuperset(realisation._frame._meanings):
            # znaczenie ‹odpaść (się) jedzeniem›
            entry_form = entry_form.replace('', '')
        if entry.name == 'podpaść' and {'wal_86356-mng', 'wal_86350-mng', 'wal_174582-mng', 'wal_174584-mng', 'wal_174585-mng', 'wal_174586-mng'}.issuperset(realisation._frame._meanings):
            # znaczenie ‹podpaść (się) jedzeniem›
            entry_form = entry_form.replace('', '')
        if entry.name == 'popaść' and {'wal_174529-mng', 'wal_174530-mng'}.issuperset(realisation._frame._meanings):
            # znaczenie ‹popaść (się) jedzeniem›
            entry_form = entry_form.replace('', '')
    
    if subentry.negativity and subentry.negativity.name == 'neg':
        entry_form = 'nie ' + entry_form
    if subentry.inherent_sie.name == 'true':
        entry_form += ' się'
    elements = before + ['<b>{}</b>'.format(entry_form)] + after
    
    if entry_form[0] > 'z':
        #-------
        for t in sorted(BENCH3.keys()):
            if t > 4:
                print('    ************', t, len(BENCH3[t]), BENCH3[t][:10])
                for n, sid, synset in BENCH3[t]:
                    print('    ************', synset)
                    print('    ************', sid, ':', n)
        #for t in sorted(BENCH2.keys()):
        #    if t > 4:
        #        print('    ********', t, len(BENCH2[t]), BENCH2[t][:10])
        #for t in sorted(BENCH.keys()):
        #    if t > 4:
        #        print('    ****', t, len(BENCH[t]), BENCH[t][:10])
        #-------
    
    return ' '.join(elements)