File size: 28,503 Bytes
e40efc2
4cc69e2
e40efc2
 
 
 
 
 
5279e57
 
 
 
e40efc2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5279e57
 
e40efc2
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
from __future__ import annotations
from typing import Dict, List, Tuple, Any
from functools import reduce
import operator
import pandas as pd

import gradio as gr

from src.utility import load_json_obj
from src.pandas_utility import read_csv_df
from src.pipeline import NaturalLanguageProcessing
from src.my_gradio import GrBlocks, GrLayout, GrComponent, GrListener

class App(GrBlocks):
    """
    アプリのクラス
    """
    @staticmethod
    def _create_children_and_listeners(
            model_dir: str, cuisine_df_path: str, unify_dics_path: str
    ) -> Tuple[Dict[str, Any] | List[Any], List[Any]]:
        """
        子要素とイベントリスナーの作成

        Parameters
        ----------
        model_dir : str
            ファインチューニング済みモデルが保存されているディレクトリ
        cuisine_df_path : str
            料理のデータフレームが保存されているパス
        unify_dics_path : str
            表記ゆれ統一用辞書が保存されているパス

        Returns
        -------
        Tuple[Dict[str, Any] | List[Any], List[Any]]
            子要素とイベントリスナーのタプル
        """
        cuisine_infos_num = 10
        label_info_dics: Dict[str, str | List[str]] = {
            'AREA': {
                'jp': '都道府県/地方',
                'color': 'red',
                'df_cols': ['Prefecture', 'Areas']
            },
            'TYPE': {
                'jp': '種類',
                'color': 'green',
                'df_cols': ['Types']

            },
            'SZN': {
                'jp': '季節',
                'color': 'blue',
                'df_cols': ['Seasons']
            },
            'INGR': {
                'jp': '食材',
                'color': 'yellow',
                'df_cols': ['Ingredients list']
            }
        }

        input = InputTextbox(
            model_dir, label_info_dics, cuisine_df_path, unify_dics_path,
            cuisine_infos_num
        )
        input_samples = InputSamplesDataset()
        extracted_words = ExtractedWordsHighlightedText(label_info_dics)
        cuisine_infos = CuisineInfos(cuisine_infos_num)

        input_submitted = GrListener(
            trigger=input.comp.submit,
            fn=input.submitted,
            inputs=input,
            outputs=[extracted_words, cuisine_infos],
            scroll_to_output=True
        )

        input_samples_selected = GrListener(
            trigger=input_samples.comp.select,
            fn=InputSamplesDataset.selected,
            outputs=input,
            thens=input_submitted
        )

        children = [input, input_samples, extracted_words, cuisine_infos]
        listeners = [input_submitted, input_samples_selected]

        return children, listeners


class InputTextbox(GrComponent):
    """
    入力欄のクラス

    Attributes
    ----------
    _nlp : NaturalLanguageProcessing
        固有表現を抽出するオブジェクト
    _jp_label_dic : Dict[str, str]
        固有表現のラベルとその日本語訳の辞書
    _cuisine_info_dics_maker : CuisineInfoDictionariesMaker
        検索結果の料理の情報の辞書のリストを作成するオブジェクト
    """
    def __init__(
            self,
            model_dir: str,
            label_info_dics: Dict[str, str | List[str]],
            cuisine_df_path: str,
            unify_dics_path: str,
            cuisine_infos_num: int
    ):
        """
        コンストラクタ

        Parameters
        ----------
        model_dir : str
            ファインチューニング済みモデルが保存されているディレクトリ
        label_info_dics : Dict[str, str  |  List[str]]
            固有表現のラベルとラベルに対する各種設定情報の辞書
        cuisine_df_path : str
            料理のデータフレームが保存されているパス
        unify_dics_path : str
            表記ゆれ統一用辞書が保存されているパス
        cuisine_infos_num : int
            表示する料理検索結果の最大数
        """
        self._nlp = NaturalLanguageProcessing(model_dir)
        self._jp_label_dic: Dict[str, str] = {
            label: dic['jp'] for label, dic in label_info_dics.items()
        }

        self._cuisine_info_dics_maker = CuisineInfoDictionariesMaker(
            cuisine_df_path, unify_dics_path, label_info_dics, cuisine_infos_num
        )

        super().__init__()

    def _create(self) -> gr.Textbox:
        """
        コンポーネントの作成

        Returns
        -------
        gr.Textbox
            入力欄のコンポーネント
        """
        label = self._create_label()
        placeholder = 'どんな料理をお探しでしょうか?'
        comp = gr.Textbox(placeholder=placeholder, label=label)

        return comp

    def _create_label(self) -> str:
        """
        ラベルの作成

        Returns
        -------
        str
            コンポーネントのラベル
        """
        categories = [f'"{jp}"' for jp in self._jp_label_dic.values()]
        label = f'入力文から、料理の{"、".join(categories)}を示す語彙を検出します'

        return label

    def submitted(
            self, classifying_text: str
    ) -> List[List[Tuple[str, str]] | List[gr.Textbox | gr.Button]]:
        """
        submitイベントリスナーの関数

        Parameters
        ----------
        classifying_text : str
            固有表現抽出対象

        Returns
        -------
        List[List[Tuple[str, str]] | List[gr.Textbox | gr.Button]]
            抽出結果のリストと、料理検索結果に応じた
            テキストボックスとボタンのリストのリスト
        """
        classified_words: Dict[str, List[str]] = self._nlp.classify(classifying_text)

        pos_tokens = [
            (word, self._jp_label_dic[label])
            for label, words in classified_words.items()
            for word in words
        ]

        cuisine_info_dics = self._cuisine_info_dics_maker.create(classified_words)
        cuisine_infos = CuisineInfos.update(cuisine_info_dics)

        return [pos_tokens] + cuisine_infos


class InputSamplesDataset(GrComponent):
    """
    入力例のクラス
    """
    def _create(self) -> gr.Dataset:
        """
        コンポーネントの作成

        Returns
        -------
        gr.Dataset
            入力例のコンポーネント
        """
        label = 'こんな風に聞いてみてください'
        input_samples = [
            'オオカミとムカデを使った肉料理を教えてください',
            '野菜料理で仙豆を使用したものはありますか?',
            'オールマイトの髪の毛を使った料理は?',
            '仙台の、宿儺の指を使った、夏に食べられる肉料理',
            '呪胎九相図が使われている料理を探しています'
        ]

        comp = gr.Dataset(
            label=label,
            components=[gr.Textbox()],
            samples=[[sample] for sample in input_samples]
        )

        return comp

    @staticmethod
    def selected(input: gr.SelectData) -> str:
        """
        selectイベントリスナーの関数

        Parameters
        ----------
        input : gr.SelectData
            _description_

        Returns
        -------
        str
            選択した入力例
        """
        return input.value[0]


class ExtractedWordsHighlightedText(GrComponent):
    """
    抽出結果のクラス
    """
    def __init__(self, label_info_dics: Dict[str, str | List[str]]):
        """
        コンストラクタ

        Parameters
        ----------
        label_info_dics : Dict[str, str  |  List[str]]
            固有表現のラベルとラベルに対する各種設定情報の辞書
        """
        super().__init__(label_info_dics)

    def _create(
            self, label_info_dics: Dict[str, str | List[str]]
    ) -> gr.HighlightedText:
        """
        コンポーネントの作成

        Parameters
        ----------
        label_info_dics : Dict[str, str  |  List[str]]
            固有表現のラベルとラベルに対する各種設定情報の辞書

        Returns
        -------
        gr.HighlightedText
            抽出結果のコンポーネント
        """
        color_map: Dict[str, str] = {
            dic['jp']: dic['color'] for dic in label_info_dics.values()
        }

        comp = gr.HighlightedText(
            color_map=color_map,
            combine_adjacent=True,
            adjacent_separator='、',
            label='検出語彙一覧'
        )

        return comp


class CuisineInfos(GrLayout):
    """
    全検索結果のクラス

    Attributes
    ----------
    layout_type : gr.Column
        GradioのColumn
    """
    layout_type = gr.Column

    def _create(self, cuisine_infos_num: int) -> List[CuisineInfo]:
        """
        子要素の作成

        Parameters
        ----------
        cuisine_infos_num : int
            表示する料理検索結果の最大数

        Returns
        -------
        List[CuisineInfo]
            全検索結果
        """
        children = [CuisineInfo() for _ in range(cuisine_infos_num)]

        return children

    @staticmethod
    def update(
            cuisine_info_dics: List[Dict[str, str]]
    ) -> List[gr.Textbox | gr.Button]:
        """
        全検索結果の更新

        Parameters
        ----------
        cuisine_info_dics : List[Dict[str, str]]
            検索で見つかった料理の情報を持つ辞書のリスト

        Returns
        -------
        List[gr.Textbox | gr.Button]
            全料理の情報のテキストボックスと、詳細ページへのボタンのリスト
        """
        cuisine_infos: List[gr.Textbox | gr.Button] = []

        for cuisine_info_dic in cuisine_info_dics:
            cuisine_info = CuisineInfo.update(cuisine_info_dic)

            cuisine_infos.extend(cuisine_info)

        return cuisine_infos


class CuisineInfo(GrLayout):
    """
    料理の情報とURLボタンのクラス

    Attributes
    ----------
    layout_type : gr.Row
        GradioのRow
    """
    layout_type = gr.Row

    def _create(self) -> List[InfoTextbox | UrlButton]:
        """
        子要素の作成

        Returns
        -------
        List[InfoTextbox | UrlButton]
            料理の情報のテキストボックスと、詳細ページへのボタンのリスト
        """
        info_textbox = InfoTextbox()
        url_btn = UrlButton()

        children = [info_textbox, url_btn]

        return children

    @staticmethod
    def update(cuisine_info_dic: Dict[str, str]) -> List[gr.Textbox | gr.Button]:
        """
        料理の情報とURLボタンの更新

        Parameters
        ----------
        cuisine_info_dic : Dict[str, str]
            検索で見つかった料理の情報を持つ辞書

        Returns
        -------
        List[gr.Textbox | gr.Button]
            料理の情報のテキストボックスと、詳細ページへのボタンのリスト
        """
        if cuisine_info_dic:
            textbox, btn = CuisineInfo._reset(cuisine_info_dic)

        else:
            textbox, btn = CuisineInfo._hide()

        return [textbox, btn]

    @staticmethod
    def _reset(cuisine_info_dic: Dict[str, str]) -> Tuple[gr.Textbox, gr.Button]:
        """
        料理の変更

        Parameters
        ----------
        cuisine_info_dic : Dict[str, str]
            検索で見つかった料理の情報を持つ辞書

        Returns
        -------
        Tuple[gr.Textbox, gr.Button]
            料理の情報のテキストボックスと、詳細ページへのボタンのタプル
        """
        cuisine_name = cuisine_info_dic['name']
        cuisine_info = cuisine_info_dic['info']
        cuisine_url = cuisine_info_dic['url']

        textbox = InfoTextbox.reset(cuisine_name, cuisine_info)
        btn = UrlButton.reset(cuisine_name, cuisine_url)

        return textbox, btn

    @staticmethod
    def _hide() -> Tuple[gr.Textbox, gr.Button]:
        """
        料理の非表示

        Returns
        -------
        Tuple[gr.Textbox, gr.Button]
            料理の情報のテキストボックスと、詳細ページへのボタンのタプル
        """
        textbox = InfoTextbox.hide()
        btn = UrlButton.hide()

        return textbox, btn


class InfoTextbox(GrComponent):
    """
    料理の情報のクラス
    """
    def _create(self) -> gr.Textbox:
        """
        コンポーネントの作成

        Returns
        -------
        gr.Textbox
            料理の情報のコンポーネント
        """
        comp = gr.Textbox(scale=9, visible=False)

        return comp

    @staticmethod
    def reset(cuisine_name: str, cuisine_info: str) -> gr.Textbox:
        """
        料理の情報の変更

        Parameters
        ----------
        cuisine_name : str
            料理名
        cuisine_info : str
            料理の情報

        Returns
        -------
        gr.Textbox
            料理の情報のコンポーネント
        """
        comp = gr.Textbox(value=cuisine_info, label=cuisine_name, visible=True)

        return comp

    @staticmethod
    def hide() -> gr.Textbox:
        """
        料理の情報の非表示

        Returns
        -------
        gr.Textbox
            非表示になった料理の情報のコンポーネント
        """
        comp = gr.Textbox(visible=False)

        return comp


class UrlButton(GrComponent):
    """
    URLボタンのクラス
    """
    def _create(self) -> gr.Button:
        """
        コンポーネントの作成

        Returns
        -------
        gr.Button
            詳細ページへのボタンのコンポーネント
        """
        comp = gr.Button(scale=1, visible=False)

        return comp

    @staticmethod
    def reset(cuisine_name: str, cuisine_url: str) -> gr.Button:
        """
        料理のボタンの更新

        Parameters
        ----------
        cuisine_name : str
            料理名
        cuisine_url : str
            料理の詳細ページへのURL

        Returns
        -------
        gr.Button
            詳細ページへのボタンのコンポーネント
        """
        value = cuisine_name + '\n詳細ページ'
        comp = gr.Button(value=value, link=cuisine_url, visible=True)

        return comp

    @staticmethod
    def hide() -> gr.Button:
        """
        料理のボタンの非表示

        Returns
        -------
        gr.Button
            非表示になった詳細ページへのボタンのコンポーネント
        """
        comp = gr.Button(visible=False)

        return comp


class CuisineInfoDictionariesMaker:
    """
    料理検索結果の辞書のリスト作成用クラス

    Attributes
    ----------
    _cuisine_searcher : CuisineSearcher
        料理を検索するオブジェクト
    _word_unifier : WordUnifier
        抽出結果の表記ゆれを統一するオブジェクト
    """
    def __init__(
            self,
            cuisine_df_path: str,
            unify_dics_path: str,
            label_info_dics: Dict[str, str | List[str]],
            cuisine_infos_num: int
    ):
        """
        コンストラクタ

        Parameters
        ----------
        cuisine_df_path : str
            料理のデータフレームが保存されているパス
        unify_dics_path : str
            表記ゆれ統一用辞書が保存されているパス
        label_info_dics : Dict[str, str  |  List[str]]
            固有表現のラベルとラベルに対する各種設定情報の辞書
        cuisine_infos_num : int
            表示する料理検索結果の最大数
        """
        self._cuisine_searcher = CuisineSearcher(
            cuisine_df_path, label_info_dics, cuisine_infos_num
        )
        self._word_unifier = WordUnifier(unify_dics_path)

    def create(
            self, classified_words: Dict[str, List[str]]
    ) -> List[Dict[str, str]]:
        """
        料理検索結果の辞書の作成

        Parameters
        ----------
        classified_words : Dict[str, List[str]]
            ラベルと、そのラベルに分類された固有表現の辞書

        Returns
        -------
        List[Dict[str, str]]
            料理検索結果の辞書のリスト
        """
        unified_words = self._word_unifier.unify(classified_words)
        cuisine_info_dics = self._cuisine_searcher.search(unified_words)

        return cuisine_info_dics


class CuisineSearcher:
    """
    料理検索用のクラス

    Attributes
    ----------
    _search_infos : List[str]
        料理のどの情報を取ってくるか示したリスト
    _df : pd.DataFrame
        料理のデータフレーム
    _label_to_col : Dict[str, List[str]]
        固有表現のラベルに対して、検索するデータフレームの列のリストの辞書
    _words_dic : Dict[str, List[str]]
        データフレームの列と、列に含まれる全ての要素の辞書
    _cuisine_infos_num : int
        表示する料理検索結果の最大数
    """
    _search_infos = [
        'Name', 'Prefecture', 'Types', 'Seasons', 'Ingredients', 'Detail URL'
    ]

    def __init__(
            self,
            cuisine_df_path: str,
            label_info_dics: Dict[str, str | List[str]],
            cuisine_infos_num: int
    ):
        """
        コンストラクタ

        Parameters
        ----------
        cuisine_df_path : str
            料理のデータフレームが保存されているパス
        label_info_dics : Dict[str, str  |  List[str]]
            固有表現のラベルとラベルに対する各種設定情報の辞書
        cuisine_infos_num : int
            表示する料理検索結果の最大数
        """
        self._df = read_csv_df(cuisine_df_path)
        self._label_to_col = self._create_label_to_col(label_info_dics)
        self._words_dic = {
            col: self._find_words(col)
            for cols in self._label_to_col.values() for col in cols
        }
        self._cuisine_infos_num = cuisine_infos_num

    def _create_label_to_col(
            self, label_info_dics: Dict[str, str | List[str]]
    ) -> Dict[str, List[str]]:
        """
        label_to_colの作成

        固有表現のラベルに対応したデータフレームの列を
        特定するための辞書を作成する

        Parameters
        ----------
        label_info_dics : Dict[str, str  |  List[str]]
            固有表現のラベルとラベルに対する各種設定情報の辞書

        Returns
        -------
        Dict[str, List[str]]
            固有表現のラベルに対して、検索するデータフレームの列のリストの辞書

        Raises
        ------
        ValueError
            label_info_dicsに、データフレームに存在しない列名が含まれている場合
        """
        label_to_col: Dict[str, List[str]] = {
            label: dic['df_cols'] for label, dic in label_info_dics.items()
        }

        df_cols = self._df.columns.tolist()
        for cols in label_to_col.values():
            for col in cols:
                if col not in df_cols:
                    raise ValueError(f'"{col}"という列名は存在しません')

        return label_to_col

    def _find_words(self, col: str) -> List[str]:
        """
        列に含まれる全要素の取得

        Parameters
        ----------
        col : str
            列名

        Returns
        -------
        List[str]
            列に含まれる全ての要素のリスト
        """
        words: List[str, List[str]] = self._df[col].value_counts().index.tolist()

        if isinstance(words[0], list):
            words_lst = words
            unique_words: List[str] = []

            for words in words_lst:
                for word in words:
                    if word not in unique_words:
                        unique_words.append(word)

            return unique_words

        return words

    def search(self, unified_words: Dict[str, List[str]]) -> List[Dict[str, str]]:
        """
        料理の検索

        Parameters
        ----------
        unified_words : Dict[str, List[str]]
            表記ゆれが統一された固有表現の辞書

        Returns
        -------
        List[Dict[str, str]]
            検索結果の料理の情報を持つ辞書のリスト
        """
        on_df_words_dic = self._create_on_df_words_dic(unified_words)

        if not on_df_words_dic:
            gr.Info('いずれの語彙もデータに存在しませんでした')

            return self._create_empty_dics()

        cuisine_info_dics = self._create_cuisine_info_dics(on_df_words_dic)

        return cuisine_info_dics

    def _create_on_df_words_dic(
            self, unified_words: Dict[str, List[str]]
    ) -> Dict[str, List[str]]:
        """
        データフレームに存在する固有表現だけの辞書の作成

        Parameters
        ----------
        unified_words : Dict[str, List[str]]
            表記ゆれが統一された固有表現の辞書

        Returns
        -------
        Dict[str, List[str]]
            データフレームに存在する表記ゆれが統一された固有表現の辞書
        """
        on_df_words_dic = {col: [] for col in self._words_dic}
        not_on_df_words: List[str] = []

        for label, words in unified_words.items():
            search_cols = self._label_to_col[label]

            for word in words:
                not_on_df = True

                for col in search_cols:
                    if word in self._words_dic[col]:
                        on_df_words_dic[col].append(word)

                        not_on_df = False

                        break

                if not_on_df:
                    not_on_df_words.append(word)

        if not_on_df_words:
            CuisineSearcher._show_not_on_df_words(not_on_df_words)

        on_df_words_dic = {
            col: words for col, words in on_df_words_dic.items() if words
        }

        return on_df_words_dic

    @staticmethod
    def _show_not_on_df_words(not_on_df_words: List[str]) -> None:
        """
        データフレームに存在しなかった固有表現の表示

        Parameters
        ----------
        not_on_df_words : List[str]
            データフレームに存在しなかった固有表現のリスト
        """
        words = '、'.join(not_on_df_words)
        message = f'無効な語彙: {words}'

        gr.Info(message)

    def _create_empty_dics(self) -> List[Dict[Any, Any]]:
        """
        空の辞書のリストの作成

        検索結果に該当する料理がなかった場合は、CuisineInfosを非表示にする
        CuisineInfo.update()に空の辞書を渡すと、
        InfoTextboxとUrlButtonが非表示になる

        Returns
        -------
        List[Dict[Any, Any]]
            空の辞書のリスト
        """
        return [{} for _ in range(self._cuisine_infos_num)]

    def _create_cuisine_info_dics(
            self, words_dic: Dict[str, List[str]]
    ) -> List[Dict[str, str]]:
        """
        料理の情報を持つ辞書の作成

        Parameters
        ----------
        words_dic : Dict[str, List[str]]
            検索ワードのリストを持つ辞書

        Returns
        -------
        List[Dict[str, str]]
            料理の情報を持つ辞書のリスト
        """
        condition_lst: List[pd.Series] = []

        for col, words in words_dic.items():
            condition = self._create_condition(col, words)
            condition_lst.append(condition)

        conditions = reduce(operator.and_, condition_lst)

        cuisine_infos_lst = self._df.loc[conditions, self._search_infos].values.tolist()

        if len(cuisine_infos_lst) > self._cuisine_infos_num:
            cuisine_infos_lst = cuisine_infos_lst[:self._cuisine_infos_num]

        if not cuisine_infos_lst:
            gr.Info('検索条件が厳しすぎて、該当料理が見つかりませんでした')

            return self._create_empty_dics()

        cuisine_info_dics = self._lst_to_dics(cuisine_infos_lst)

        return cuisine_info_dics

    def _create_condition(self, col: str, words: List[str]) -> pd.Series:
        """
        検索条件の作成

        Parameters
        ----------
        col : str
            絞り込み対象列
        words : List[str]
            検索ワード

        Returns
        -------
        pd.Series
            該当料理の行がTrueになったboolのシリーズ
        """
        value_type = type(self._df.at[0, col])

        if value_type is list:
            condition = self._df[col].apply(
                lambda values: any(word in values for word in words)
            )

        else:
            conditions = [self._df[col] == word for word in words]
            condition = reduce(operator.or_, conditions)

        return condition

    def _lst_to_dics(
            self, infos_lst: List[List[str | List[str]]]
    ) -> List[Dict[str, str]]:
        """
        リストから辞書への変換

        料理の情報のリストのリストを、料理の情報の辞書のリストに変換する

        Parameters
        ----------
        infos_lst : List[List[str  |  List[str]]]
            料理の情報のリストのリスト

        Returns
        -------
        List[Dict[str, str]]
            料理の情報の辞書のリスト
        """
        dics: List[Dict[str, str]] = []

        for infos in infos_lst:
            infos = [
                '、'.join(info) if isinstance(info, list) else info
                for info in infos
            ]

            name = infos[0]
            info = ' | '.join(infos[1:-1])
            url = infos[-1]

            dic = {'name': name, 'info': info, 'url': url}
            dics.append(dic)

        dics_len = len(dics)

        if dics_len < self._cuisine_infos_num:
            dics = dics + [{} for _ in range(self._cuisine_infos_num - dics_len)]

        return dics


class WordUnifier:
    """
    表記ゆれ統一用のクラス

    Attributes
    ----------
    _not_unify_labels : List[str]
        表記ゆれ統一対象ではない固有表現のラベルのリスト
    _unify_dics : Dict[str, Dict[str, str]]
        ラベルと、そのラベルの固有表現の表記ゆれ統一用の辞書の辞書
    """
    _not_unify_labels = ['SZN']

    def __init__(self, unify_dics_path: str):
        """
        コンストラクタ

        Parameters
        ----------
        unify_dics_path : str
            表記ゆれ統一用辞書が保存されているパス
        """
        self._unify_dics: Dict[str, Dict[str, str]] = load_json_obj(unify_dics_path)

    def unify(
            self, classified_words: Dict[str, List[str]]
    ) -> Dict[str, List[str]]:
        """
        表記ゆれの統一

        Parameters
        ----------
        classified_words : Dict[str, List[str]]
            ラベルと、そのラベルに分類された固有表現の辞書

        Returns
        -------
        Dict[str, List[str]]
            表記ゆれが統一された固有表現の辞書
        """
        for label, words in classified_words.items():
            if label in self._not_unify_labels:
                continue

            unify_dic = self._unify_dics[label]

            unified_words = [
                unify_dic[word] if word in unify_dic else word for word in words
            ]

            classified_words[label] = unified_words

        return classified_words


model_name = 'wolf4032/bert-japanese-token-classification-search-local-cuisine'
cuisine_df_path = 'src/local_cuisine_dataframe.csv'
unify_dics_path = 'src/unifying_dictionaries.json'

app = App.create_and_launch(model_name, cuisine_df_path, unify_dics_path)