Telegram Chat Statistics based on JSON export. пиши комментарии в коде только на английском и только в обязательных случаях. избегай комментирования обновлений.

кажется, сейчас сломан счётчик сообщений, раньше код выдавал другие цифры (не знаю правильные ли). нужно исправить если неправильный
вот пример того как выглядит экспорт, сообщения, символы, слова и прочее ты должен считать только из "text":
{
"name": "TesChatStat",
"type": "private_group",
"id": 4527053047,
"messages": [
{
"id": 994579,
"type": "service",
"date": "2024-09-30T22:35:49",
"date_unixtime": "1727760949",
"actor": "Teslak",
"actor_id": "user391893063",
"action": "create_group",
"title": "TesChatStat",
"members": [
"Teslak"
],
"full_text": "",
"text": "",
"text_entities": []
},
{
"id": 994580,
"type": "message",
"date": "2024-09-30T22:35:53",
"date_unixtime": "1727760953",
"from": "Teslak",
"from_id": "user391893063",
"full_text": "привет",
"text": "привет",
"text_entities": [
{
"type": "plain",
"text": "привет"
}
]
},
{
"id": 994581,
"type": "message",
"date": "2024-09-30T22:36:07",
"date_unixtime": "1727760967",
"from": "Teslak",
"from_id": "user391893063",
"full_text": "123 оашвшщц шмцшгмуцшщ шргцукгш тест тест тест тест тест тест2 тест3",
"text": "123 оашвшщц шмцшгмуцшщ шргцукгш тест тест тест тест тест тест2 тест3",
"text_entities": [
{
"type": "plain",
"text": "123 оашвшщц шмцшгмуцшщ шргцукгш тест тест тест тест тест тест2 тест3"
}
]
},
{
"id": 994582,
"type": "message",
"date": "2024-09-30T22:36:08",
"date_unixtime": "1727760968",
"from": "Teslak",
"from_id": "user391893063",
"full_text": "пиздец",
"text": "пиздец",
"text_entities": [
{
"type": "plain",
"text": "пиздец"
}
]
},
{
"id": 994583,
"type": "message",
"date": "2024-09-30T22:37:00",
"date_unixtime": "1727761020",
"from": "Teslak",
"from_id": "user391893063",
"file": "(File not included. Change data exporting settings to download.)",
"media_type": "voice_message",
"mime_type": "audio/ogg",
"duration_seconds": 1,
"full_text": "",
"text": "",
"text_entities": []
},
{
"id": 994584,
"type": "message",
"date": "2024-09-30T22:37:22",
"date_unixtime": "1727761042",
"from": "Teslak",
"from_id": "user391893063",
"file": "(File not included. Change data exporting settings to download.)",
"thumbnail": "(File not included. Change data exporting settings to download.)",
"media_type": "video_message",
"mime_type": "video/mp4",
"duration_seconds": 1,
"width": 360,
"height": 360,
"full_text": "",
"text": "",
"text_entities": []
},
{
"id": 994585,
"type": "message",
"date": "2024-09-30T22:37:35",
"date_unixtime": "1727761055",
"from": "Teslak",
"from_id": "user391893063",
"file": "(File not included. Change data exporting settings to download.)",
"file_name": "SF4.mp4",
"media_type": "video_file",
"mime_type": "video/mp4",
"duration_seconds": 15,
"width": 720,
"height": 1316,
"full_text": "",
"text": "",
"text_entities": []
},
{
"id": 994586,
"type": "message",
"date": "2024-09-30T22:37:42",
"date_unixtime": "1727761062",
"from": "Teslak",
"from_id": "user391893063",
"file": "(File not included. Change data exporting settings to download.)",
"thumbnail": "(File not included. Change data exporting settings to download.)",
"media_type": "animation",
"mime_type": "video/mp4",
"duration_seconds": 4,
"width": 528,
"height": 640,
"full_text": "",
"text": "",
"text_entities": []
},
{
"id": 994587,
"type": "message",
"date": "2024-09-30T22:37:51",
"date_unixtime": "1727761071",
"from": "Teslak",
"from_id": "user391893063",
"file": "(File not included. Change data exporting settings to download.)",
"file_name": "EVE.EXE - Reach for EVE.m4a",
"media_type": "audio_file",
"mime_type": "audio/mp4",
"duration_seconds": 149,
"full_text": "",
"text": "",
"text_entities": []
},
{
"id": 994588,
"type": "message",
"date": "2024-09-30T22:38:26",
"date_unixtime": "1727761106",
"from": "Teslak",
"from_id": "user391893063",
"file": "(File not included. Change data exporting settings to download.)",
"file_name": "a_photo_of_floating_purple_obsidian_shards_against_7dYj_GWVTUSsnOSz.jpeg",
"thumbnail": "(File not included. Change data exporting settings to download.)",
"mime_type": "image/jpeg",
"width": 5120,
"height": 3200,
"full_text": "",
"text": "",
"text_entities": []
},
{
"id": 994589,
"type": "message",
"date": "2024-09-30T22:38:55",
"date_unixtime": "1727761135",
"from": "Teslak",
"from_id": "user391893063",
"forwarded_from": "Teslak Notes",
"full_text": "Нейросети нужно распробовать\n\nМногие люди их не используют или делают это крайне редко. Вероятно, есть задачи, которые можно решать с их помощью, но не сформировалось понимание, какие нейросети в каких ситуациях могут быть наиболее эффективными. А возможно, таких задач действительно нет, всякое бывает.",
"text": "Нейросети нужно распробовать\n\nМногие люди их не используют или делают это крайне редко. Вероятно, есть задачи, которые можно решать с их помощью, но не сформировалось понимание, какие нейросети в каких ситуациях могут быть наиболее эффективными. А возможно, таких задач действительно нет, всякое бывает.",
"text_entities": [
{
"type": "plain",
"text": "Нейросети нужно распробовать\n\nМногие люди их не используют или делают это крайне редко. Вероятно, есть задачи, которые можно решать с их помощью, но не сформировалось понимание, какие нейросети в каких ситуациях могут быть наиболее эффективными. А возможно, таких задач действительно нет, всякое бывает."
}
]
},
{
"id": 994590,
"type": "message",
"date": "2024-09-30T22:39:20",
"date_unixtime": "1727761160",
"from": "Teslak",
"from_id": "user391893063",
"file": "(File not included. Change data exporting settings to download.)",
"file_name": "sticker.webp",
"thumbnail": "(File not included. Change data exporting settings to download.)",
"media_type": "sticker",
"sticker_emoji": "🫵",
"mime_type": "image/webp",
"width": 512,
"height": 512,
"full_text": "",
"text": "",
"text_entities": []
},
{
"id": 994591,
"type": "message",
"date": "2024-09-30T22:39:25",
"date_unixtime": "1727761165",
"from": "Teslak",
"from_id": "user391893063",
"full_text": "/start",
"text": [
{
"type": "bot_command",
"text": "/start"
}
],
"text_entities": [
{
"type": "bot_command",
"text": "/start"
}
]
},
{
"id": 994592,
"type": "message",
"date": "2024-09-30T22:39:58",
"date_unixtime": "1727761198",
"from": "Teslak",
"from_id": "user391893063",
"full_text": "🥺",
"text": "🥺",
"text_entities": [
{
"type": "plain",
"text": "🥺"
}
]
},
{
"id": 994593,
"type": "message",
"date": "2024-09-30T22:40:14",
"date_unixtime": "1727761214",
"from": "Teslak",
"from_id": "user391893063",
"full_text": "мат: пиздец",
"text": "мат: пиздец",
"text_entities": [
{
"type": "plain",
"text": "мат: пиздец"
}
]
},
{
"id": 994594,
"type": "message",
"date": "2024-09-30T22:41:37",
"date_unixtime": "1727761297",
"from": "Teslak",
"from_id": "user391893063",
"poll": {
"question": "опрос",
"closed": false,
"total_voters": 1,
"answers": [
{
"text": "1",
"voters": 1,
"chosen": true
},
{
"text": "2",
"voters": 0,
"chosen": false
}
]
},
"full_text": "",
"text": "",
"text_entities": []
},
{
"id": 994597,
"type": "message",
"date": "2024-09-30T22:58:18",
"date_unixtime": "1727762298",
"from": "Teslak",
"from_id": "user391893063",
"photo": "(File not included. Change data exporting settings to download.)",
"width": 364,
"height": 346,
"media_group_id": 0,
"full_text": "",
"text": "",
"text_entities": []
},
{
"id": 994598,
"type": "message",
"date": "2024-09-30T22:58:24",
"date_unixtime": "1727762304",
"from": "Teslak",
"from_id": "user391893063",
"photo": "(File not included. Change data exporting settings to download.)",
"width": 49,
"height": 72,
"media_group_id": 0,
"full_text": "",
"text": "",
"text_entities": []
},
{
"id": 994737,
"type": "message",
"date": "2024-10-01T02:49:50",
"date_unixtime": "1727776190",
"from": "Teslak",
"from_id": "user391893063",
"full_text": "Статистика чата "Боттлситтинг (в ожиданий 3 мировой)" за период: 22.12.2017 – 30.09.2024\n\n✉️ Сообщений: 112 374 (58 813 не подряд)\n🔣 Символов: 6 260 881 (6 260 881 не подряд)\n💬 Символов в сообщении: 56\n\n🖼 Изображений: 7 296\n📹 Видео: 1 898\n📑 Файлы: 12\n🎧 Аудио: 100\n🔗 Ссылки: 962\n🎵 Голосовых: 312\n🎬 GIF: 252\n💌 Стикеров: 903\n🥵 Эмодзи: 3 627\n📊 Опросов: 61\n❗️ Команд: 971\n💢 Сообщений с матом: 526\n\n👥 Топ участников:\n1. Сергей Вакулин: 45 018 (15 869) · 3 339 804 (3 339 804)\n2. Teo: 14 563 (9 071) · 522 294 (522 294)\n3. Юра: 6 913 (3 656) · 374 054 (374 054)\n4. тим: 6 287 (4 511) · 324 016 (324 016)\n5. Maksim: 3 402 (2 184) · 83 441 (83 441)\n6. сглыпа): 2 769 (2 692) · 119 274 (119 274)\n7. slide: 2 741 (1 419) · 108 742 (108 742)\n8. zema: 2 561 (1 462) · 112 529 (112 529)\n9. ඞm️ bඞ ️: 2 308 (1 530) · 95 691 (95 691)\n10. ゼル五: 2 153 (1 295) · 59 649 (59 649)\n11. phrynus: 2 119 (925) · 42 955 (42 955)\n12. el vigilante: 2 081 (1 510) · 254 332 (254 332)\n13. Someone: 1 876 (1 254) · 82 044 (82 044)\n14. Vladislav: 1 866 (1 416) · 57 821 (57 821)\n15. брат: 1 567 (882) · 50 101 (50 101)\n16. Andrey: 1 271 (1 002) · 33 275 (33 275)\n17. Teslak: 1 045 (721) · 62 916 (62 916)\n18. pjerdoon: 946 (417) · 40 577 (40 577)\n19. Ivan Nedzvetskiy: 858 (689) · 45 982 (45 982)\n20. влад: 781 (476) · 47 564 (47 564)\n21. каюм: 719 (318) · 18 043 (18 043)\n22. Алексей: 674 (241) · 17 564 (17 564)\n23. .: 657 (414) · 19 467 (19 467)\n24. Артём (pina): 559 (257) · 39 884 (39 884)\n25. Сергей: 502 (449) · 68 463 (68 463)\n26. вжух: 494 (285) · 10 040 (10 040)\n27. Ivan Gordeev: 489 (422) · 13 004 (13 004)\n28. Stunner // ... ⁠⁠⁠⁠: 429 (230) · 12 726 (12 726)\n29. Unknown: 404 (280) · 13 393 (13 393)\n30. vesna никита: 344 (281) · 7 070 (7 070)\n31. Ch0c0: 322 (151) · 10 443 (10 443)\n32. Zaur Lumanov: 308 (217) · 11 420 (11 420)\n33. n4sty: 267 (185) · 7 955 (7 955)\n34. iFrame: 239 (116) · 12 284 (12 284)\n35. Alex: 238 (147) · 16 608 (16 608)\n36. Danil: 212 (170) · 18 594 (18 594)\n37. Kirill Erokhin: 199 (104) · 5 481 (5 481)\n38. Кирилл Райский: 182 (121) · 5 655 (5 655)\n39. Влад Лунин: 175 (89) · 10 027 (10 027)\n40. ㅤ: 164 (97) · 10 423 (10 423)\n41. Mark: 162 (115) · 14 336 (14 336)\n42. Ikram: 157 (133) · 6 343 (6 343)\n43. 😳🥺: 154 (103) · 4 128 (4 128)\n44. Андрей Рыжов: 136 (112) · 2 855 (2 855)\n45. Александр: 136 (104) · 15 351 (15 351)\n46. Рома Златьев: 111 (84) · 3 567 (3 567)\n47. JarkoVhate: 100 (80) · 2 231 (2 231)\n48. 🗿🗿🪨: 92 (62) · 6 412 (6 412)\n49. Павел: 86 (67) · 2 564 (2 564)\n50. Sergey: 78 (62) · 2 231 (2 231)\n51. Мясо("⁧;(" ㅤ ㅤ: 74 (54) · 1 360 (1 360)\n52. ️RK: 60 (40) · 2 752 (2 752)\n53. Columbus Utrigas Ника: 40 (35) · 756 (756)\n54. Wanna Cry: 39 (25) · 1 311 (1 311)\n55. Bo0oM: 32 (21) · 1 406 (1 406)\n56. ­: 28 (18) · 281 (281)\n57. retaker: 24 (17) · 2 043 (2 043)\n58. Dug: 22 (15) · 333 (333)\n59. А: 20 (16) · 866 (866)\n60. Austin Augie: 20 (15) · 575 (575)\n61. Nazar: 16 (13) · 757 (757)\n62. М: 16 (15) · 210 (210)\n63. Dmitry: 13 (7) · 508 (508)\n64. Gregory Klyushnikov: 12 (9) · 416 (416)\n65. unknown не работаю: 7 (5) · 326 (326)\n66. 𝕻𝖎'𝖊𝖗𝖗𝖊 𝖎𝖘 𝖞𝖔𝖚𝖗 𝖔𝖕𝖎𝖓𝖎𝖔𝖓.: 6 (4) · 109 (109)\n67. Максим Манаенков: 5 (4) · 129 (129)\n68. Евгений Котляров: 5 (3) · 196 (196)\n69. x҉TCry ̧̨̢̨̨̧̧̪͇͕̱̼̦̦̝̯̪̗̠̞̪̤̲̫͎̫͉͍̪̗̬̣̺͓̞̩͇͖̯̺̱̻̣̮̫͖̺̯͛͑̎͜ͅx̧̨̪͇͕̱̼͛͑̎: 4 (4) · 440 (440)\n70. Наталья: 4 (3) · 166 (166)\n71. Danila(xawdx) Chalykin: 2 (2) · 100 (100)\n72. Иван: 2 (2) · 83 (83)\n73. Rustem: 1 (1) · 22 (22)\n74. piousdeer: 1 (1) · 15 (15)\n75. )))): 1 (1) · 0 (0)\n76. Кирилл: 1 (1) · 3 (3)\n77. Rina: 1 (1) · 26 (26)\n78. Kirito Fire: 1 (1) · 18 (18)\n79. Даниил: 1 (1) · 15 (15)\n80. 🤡: 1 (1) · 6 (6)\n81. Алёша: 1 (1) · 30 (30)",
"text": "Статистика чата "Боттлситтинг (в ожиданий 3 мировой)" за период: 22.12.2017 – 30.09.2024\n\n✉️ Сообщений: 112 374 (58 813 не подряд)\n🔣 Символов: 6 260 881 (6 260 881 не подряд)\n💬 Символов в сообщении: 56\n\n🖼 Изображений: 7 296\n📹 Видео: 1 898\n📑 Файлы: 12\n🎧 Аудио: 100\n🔗 Ссылки: 962\n🎵 Голосовых: 312\n🎬 GIF: 252\n💌 Стикеров: 903\n🥵 Эмодзи: 3 627\n📊 Опросов: 61\n❗️ Команд: 971\n💢 Сообщений с матом: 526\n\n👥 Топ участников:\n1. Сергей Вакулин: 45 018 (15 869) · 3 339 804 (3 339 804)\n2. Teo: 14 563 (9 071) · 522 294 (522 294)\n3. Юра: 6 913 (3 656) · 374 054 (374 054)\n4. тим: 6 287 (4 511) · 324 016 (324 016)\n5. Maksim: 3 402 (2 184) · 83 441 (83 441)\n6. сглыпа): 2 769 (2 692) · 119 274 (119 274)\n7. slide: 2 741 (1 419) · 108 742 (108 742)\n8. zema: 2 561 (1 462) · 112 529 (112 529)\n9. ඞm️ bඞ ️: 2 308 (1 530) · 95 691 (95 691)\n10. ゼル五: 2 153 (1 295) · 59 649 (59 649)\n11. phrynus: 2 119 (925) · 42 955 (42 955)\n12. el vigilante: 2 081 (1 510) · 254 332 (254 332)\n13. Someone: 1 876 (1 254) · 82 044 (82 044)\n14. Vladislav: 1 866 (1 416) · 57 821 (57 821)\n15. брат: 1 567 (882) · 50 101 (50 101)\n16. Andrey: 1 271 (1 002) · 33 275 (33 275)\n17. Teslak: 1 045 (721) · 62 916 (62 916)\n18. pjerdoon: 946 (417) · 40 577 (40 577)\n19. Ivan Nedzvetskiy: 858 (689) · 45 982 (45 982)\n20. влад: 781 (476) · 47 564 (47 564)\n21. каюм: 719 (318) · 18 043 (18 043)\n22. Алексей: 674 (241) · 17 564 (17 564)\n23. .: 657 (414) · 19 467 (19 467)\n24. Артём (pina): 559 (257) · 39 884 (39 884)\n25. Сергей: 502 (449) · 68 463 (68 463)\n26. вжух: 494 (285) · 10 040 (10 040)\n27. Ivan Gordeev: 489 (422) · 13 004 (13 004)\n28. Stunner // ... ⁠⁠⁠⁠: 429 (230) · 12 726 (12 726)\n29. Unknown: 404 (280) · 13 393 (13 393)\n30. vesna никита: 344 (281) · 7 070 (7 070)\n31. Ch0c0: 322 (151) · 10 443 (10 443)\n32. Zaur Lumanov: 308 (217) · 11 420 (11 420)\n33. n4sty: 267 (185) · 7 955 (7 955)\n34. iFrame: 239 (116) · 12 284 (12 284)\n35. Alex: 238 (147) · 16 608 (16 608)\n36. Danil: 212 (170) · 18 594 (18 594)\n37. Kirill Erokhin: 199 (104) · 5 481 (5 481)\n38. Кирилл Райский: 182 (121) · 5 655 (5 655)\n39. Влад Лунин: 175 (89) · 10 027 (10 027)\n40. ㅤ: 164 (97) · 10 423 (10 423)\n41. Mark: 162 (115) · 14 336 (14 336)\n42. Ikram: 157 (133) · 6 343 (6 343)\n43. 😳🥺: 154 (103) · 4 128 (4 128)\n44. Андрей Рыжов: 136 (112) · 2 855 (2 855)\n45. Александр: 136 (104) · 15 351 (15 351)\n46. Рома Златьев: 111 (84) · 3 567 (3 567)\n47. JarkoVhate: 100 (80) · 2 231 (2 231)\n48. 🗿🗿🪨: 92 (62) · 6 412 (6 412)\n49. Павел: 86 (67) · 2 564 (2 564)\n50. Sergey: 78 (62) · 2 231 (2 231)\n51. Мясо("⁧;(" ㅤ ㅤ: 74 (54) · 1 360 (1 360)\n52. ️RK: 60 (40) · 2 752 (2 752)\n53. Columbus Utrigas Ника: 40 (35) · 756 (756)\n54. Wanna Cry: 39 (25) · 1 311 (1 311)\n55. Bo0oM: 32 (21) · 1 406 (1 406)\n56. ­: 28 (18) · 281 (281)\n57. retaker: 24 (17) · 2 043 (2 043)\n58. Dug: 22 (15) · 333 (333)\n59. А: 20 (16) · 866 (866)\n60. Austin Augie: 20 (15) · 575 (575)\n61. Nazar: 16 (13) · 757 (757)\n62. М: 16 (15) · 210 (210)\n63. Dmitry: 13 (7) · 508 (508)\n64. Gregory Klyushnikov: 12 (9) · 416 (416)\n65. unknown не работаю: 7 (5) · 326 (326)\n66. 𝕻𝖎'𝖊𝖗𝖗𝖊 𝖎𝖘 𝖞𝖔𝖚𝖗 𝖔𝖕𝖎𝖓𝖎𝖔𝖓.: 6 (4) · 109 (109)\n67. Максим Манаенков: 5 (4) · 129 (129)\n68. Евгений Котляров: 5 (3) · 196 (196)\n69. x҉TCry ̧̨̢̨̨̧̧̪͇͕̱̼̦̦̝̯̪̗̠̞̪̤̲̫͎̫͉͍̪̗̬̣̺͓̞̩͇͖̯̺̱̻̣̮̫͖̺̯͛͑̎͜ͅx̧̨̪͇͕̱̼͛͑̎: 4 (4) · 440 (440)\n70. Наталья: 4 (3) · 166 (166)\n71. Danila(xawdx) Chalykin: 2 (2) · 100 (100)\n72. Иван: 2 (2) · 83 (83)\n73. Rustem: 1 (1) · 22 (22)\n74. piousdeer: 1 (1) · 15 (15)\n75. )))): 1 (1) · 0 (0)\n76. Кирилл: 1 (1) · 3 (3)\n77. Rina: 1 (1) · 26 (26)\n78. Kirito Fire: 1 (1) · 18 (18)\n79. Даниил: 1 (1) · 15 (15)\n80. 🤡: 1 (1) · 6 (6)\n81. Алёша: 1 (1) · 30 (30)",
"text_entities": [
{
"type": "plain",
"text": "Статистика чата "Боттлситтинг (в ожиданий 3 мировой)" за период: 22.12.2017 – 30.09.2024\n\n✉️ Сообщений: 112 374 (58 813 не подряд)\n🔣 Символов: 6 260 881 (6 260 881 не подряд)\n💬 Символов в сообщении: 56\n\n🖼 Изображений: 7 296\n📹 Видео: 1 898\n📑 Файлы: 12\n🎧 Аудио: 100\n🔗 Ссылки: 962\n🎵 Голосовых: 312\n🎬 GIF: 252\n💌 Стикеров: 903\n🥵 Эмодзи: 3 627\n📊 Опросов: 61\n❗️ Команд: 971\n💢 Сообщений с матом: 526\n\n👥 Топ участников:\n1. Сергей Вакулин: 45 018 (15 869) · 3 339 804 (3 339 804)\n2. Teo: 14 563 (9 071) · 522 294 (522 294)\n3. Юра: 6 913 (3 656) · 374 054 (374 054)\n4. тим: 6 287 (4 511) · 324 016 (324 016)\n5. Maksim: 3 402 (2 184) · 83 441 (83 441)\n6. сглыпа): 2 769 (2 692) · 119 274 (119 274)\n7. slide: 2 741 (1 419) · 108 742 (108 742)\n8. zema: 2 561 (1 462) · 112 529 (112 529)\n9. ඞm️ bඞ ️: 2 308 (1 530) · 95 691 (95 691)\n10. ゼル五: 2 153 (1 295) · 59 649 (59 649)\n11. phrynus: 2 119 (925) · 42 955 (42 955)\n12. el vigilante: 2 081 (1 510) · 254 332 (254 332)\n13. Someone: 1 876 (1 254) · 82 044 (82 044)\n14. Vladislav: 1 866 (1 416) · 57 821 (57 821)\n15. брат: 1 567 (882) · 50 101 (50 101)\n16. Andrey: 1 271 (1 002) · 33 275 (33 275)\n17. Teslak: 1 045 (721) · 62 916 (62 916)\n18. pjerdoon: 946 (417) · 40 577 (40 577)\n19. Ivan Nedzvetskiy: 858 (689) · 45 982 (45 982)\n20. влад: 781 (476) · 47 564 (47 564)\n21. каюм: 719 (318) · 18 043 (18 043)\n22. Алексей: 674 (241) · 17 564 (17 564)\n23. .: 657 (414) · 19 467 (19 467)\n24. Артём (pina): 559 (257) · 39 884 (39 884)\n25. Сергей: 502 (449) · 68 463 (68 463)\n26. вжух: 494 (285) · 10 040 (10 040)\n27. Ivan Gordeev: 489 (422) · 13 004 (13 004)\n28. Stunner // ... ⁠⁠⁠⁠: 429 (230) · 12 726 (12 726)\n29. Unknown: 404 (280) · 13 393 (13 393)\n30. vesna никита: 344 (281) · 7 070 (7 070)\n31. Ch0c0: 322 (151) · 10 443 (10 443)\n32. Zaur Lumanov: 308 (217) · 11 420 (11 420)\n33. n4sty: 267 (185) · 7 955 (7 955)\n34. iFrame: 239 (116) · 12 284 (12 284)\n35. Alex: 238 (147) · 16 608 (16 608)\n36. Danil: 212 (170) · 18 594 (18 594)\n37. Kirill Erokhin: 199 (104) · 5 481 (5 481)\n38. Кирилл Райский: 182 (121) · 5 655 (5 655)\n39. Влад Лунин: 175 (89) · 10 027 (10 027)\n40. ㅤ: 164 (97) · 10 423 (10 423)\n41. Mark: 162 (115) · 14 336 (14 336)\n42. Ikram: 157 (133) · 6 343 (6 343)\n43. 😳🥺: 154 (103) · 4 128 (4 128)\n44. Андрей Рыжов: 136 (112) · 2 855 (2 855)\n45. Александр: 136 (104) · 15 351 (15 351)\n46. Рома Златьев: 111 (84) · 3 567 (3 567)\n47. JarkoVhate: 100 (80) · 2 231 (2 231)\n48. 🗿🗿🪨: 92 (62) · 6 412 (6 412)\n49. Павел: 86 (67) · 2 564 (2 564)\n50. Sergey: 78 (62) · 2 231 (2 231)\n51. Мясо("⁧;(" ㅤ ㅤ: 74 (54) · 1 360 (1 360)\n52. ️RK: 60 (40) · 2 752 (2 752)\n53. Columbus Utrigas Ника: 40 (35) · 756 (756)\n54. Wanna Cry: 39 (25) · 1 311 (1 311)\n55. Bo0oM: 32 (21) · 1 406 (1 406)\n56. ­: 28 (18) · 281 (281)\n57. retaker: 24 (17) · 2 043 (2 043)\n58. Dug: 22 (15) · 333 (333)\n59. А: 20 (16) · 866 (866)\n60. Austin Augie: 20 (15) · 575 (575)\n61. Nazar: 16 (13) · 757 (757)\n62. М: 16 (15) · 210 (210)\n63. Dmitry: 13 (7) · 508 (508)\n64. Gregory Klyushnikov: 12 (9) · 416 (416)\n65. unknown не работаю: 7 (5) · 326 (326)\n66. 𝕻𝖎'𝖊𝖗𝖗𝖊 𝖎𝖘 𝖞𝖔𝖚𝖗 𝖔𝖕𝖎𝖓𝖎𝖔𝖓.: 6 (4) · 109 (109)\n67. Максим Манаенков: 5 (4) · 129 (129)\n68. Евгений Котляров: 5 (3) · 196 (196)\n69. x҉TCry ̧̨̢̨̨̧̧̪͇͕̱̼̦̦̝̯̪̗̠̞̪̤̲̫͎̫͉͍̪̗̬̣̺͓̞̩͇͖̯̺̱̻̣̮̫͖̺̯͛͑̎͜ͅx̧̨̪͇͕̱̼͛͑̎: 4 (4) · 440 (440)\n70. Наталья: 4 (3) · 166 (166)\n71. Danila(xawdx) Chalykin: 2 (2) · 100 (100)\n72. Иван: 2 (2) · 83 (83)\n73. Rustem: 1 (1) · 22 (22)\n74. piousdeer: 1 (1) · 15 (15)\n75. )))): 1 (1) · 0 (0)\n76. Кирилл: 1 (1) · 3 (3)\n77. Rina: 1 (1) · 26 (26)\n78. Kirito Fire: 1 (1) · 18 (18)\n79. Даниил: 1 (1) · 15 (15)\n80. 🤡: 1 (1) · 6 (6)\n81. Алёша: 1 (1) · 30 (30)"
}
]
},
{
"id": 994738,
"type": "message",
"date": "2024-10-01T02:50:22",
"date_unixtime": "1727776222",
"from": "Teslak",
"from_id": "user391893063",
"file": "(File not included. Change data exporting settings to download.)",
"media_type": "voice_message",
"mime_type": "audio/ogg",
"duration_seconds": 2,
"full_text": "",
"text": "",
"text_entities": []
},
{
"id": 994739,
"type": "message",
"date": "2024-10-01T02:50:31",
"date_unixtime": "1727776231",
"from": "Teslak",
"from_id": "user391893063",
"file": "(File not included. Change data exporting settings to download.)",
"media_type": "voice_message",
"mime_type": "audio/ogg",
"duration_seconds": 3,
"full_text": "",
"text": "",
"text_entities": []
},
{
"id": 994740,
"type": "service",
"date": "2024-10-01T02:50:41",
"date_unixtime": "1727776241",
"actor": "Teslak",
"actor_id": "user391893063",
"action": "invite_members",
"members": [
"Искорка"
],
"full_text": "",
"text": "",
"text_entities": []
},
{
"id": 994741,
"type": "message",
"date": "2024-10-01T02:50:52",
"date_unixtime": "1727776252",
"from": "Искорка",
"from_id": "user5293199608",
"full_text": ".",
"text": ".",
"text_entities": [
{
"type": "plain",
"text": "."
}
]
},
{
"id": 994742,
"type": "message",
"date": "2024-10-01T02:50:53",
"date_unixtime": "1727776253",
"from": "Искорка",
"from_id": "user5293199608",
"full_text": "акуц",
"text": "акуц",
"text_entities": [
{
"type": "plain",
"text": "акуц"
}
]
},
{
"id": 994743,
"type": "message",
"date": "2024-10-01T02:50:54",
"date_unixtime": "1727776254",
"from": "Искорка",
"from_id": "user5293199608",
"full_text": "ауцкщлуц",
"text": "ауцкщлуц",
"text_entities": [
{
"type": "plain",
"text": "ауцкщлуц"
}
]
},
{
"id": 994744,
"type": "message",
"date": "2024-10-01T02:51:10",
"date_unixtime": "1727776270",
"from": "Teslak",
"from_id": "user391893063",
"full_text": ".",
"text": ".",
"text_entities": [
{
"type": "plain",
"text": "."
}
]
},
{
"id": 994745,
"type": "message",
"date": "2024-10-01T02:51:14",
"date_unixtime": "1727776274",
"from": "Искорка",
"from_id": "user5293199608",
"full_text": "2",
"text": "2",
"text_entities": [
{
"type": "plain",
"text": "2"
}
]
},
{
"id": 994746,
"type": "message",
"date": "2024-10-01T02:51:21",
"date_unixtime": "1727776281",
"from": "Teslak",
"from_id": "user391893063",
"file": "(File not included. Change data exporting settings to download.)",
"media_type": "voice_message",
"mime_type": "audio/ogg",
"duration_seconds": 1,
"full_text": "",
"text": "",
"text_entities": []
}
]
}
убери из отчёта пункт "Изменений заголовка:"
сформируй стоп-слова помимо русского ещё и на английском
сформируй оскорбительный список слов для того чтобы в отчёте правильно его считать, сейчас там 0
давай полный версии файлов

код проекта:
---
start.py:

import sys
import os
import time
import datetime
from collections import Counter, defaultdict
import locale

import config  # Import configuration

# Import modules
from modules.data_loader import load_json_file_streaming, merge_json_files
from modules.analyzer import analyze_messages
from modules.report_generator import generate_text_report, generate_json_report
from modules.visualization import generate_personal_chat_plots, generate_group_chat_plots
from modules.config_handler import configure_in_console, save_config_to_file

def main():
    # Author links
    author_github_link = 'https://github.com/TheTeslak/TesChatStat'
    author_telegram_channel = 'https://t.me/TesNot'

    version = "Version 1.1"

    # Load available languages from the locales folder
    locales_dir = os.path.join(os.path.dirname(__file__), 'locales')
    available_languages = []
    for filename in os.listdir(locales_dir):
        if filename.endswith('.py'):
            lang_code = os.path.splitext(filename)[0]
            available_languages.append(lang_code)
    current_language_index = 0  # Default to the first language in the list

    # Detect system language and default to it
    system_lang = locale.getdefaultlocale()[0]  # e.g., 'en_US'
    language_code = system_lang.split('_')[0]  # e.g., 'en'
    if language_code in available_languages:
        current_language_index = available_languages.index(language_code)
    else:
        current_language_index = 0  # Default to first language
    language = available_languages[current_language_index]

    # Function to load texts for the current language
    def load_texts(language_code):
        lang_module = {}
        lang_file = os.path.join(locales_dir, f'{language_code}.py')
        with open(lang_file, 'r', encoding='utf-8') as f:
            exec(f.read(), lang_module)
        return lang_module['texts']

    current_texts = load_texts(language)

    # Main loop
    while True:
        # Display ASCII art and information
        print(current_texts['ascii_art'])
        print(version)
        print(current_texts['description'])
        if config.show_author_links:
            print()
            print('⚡️')
            print(current_texts['author_links'].format(author_github_link, author_telegram_channel))
            print('⚡️')
            print()
        else:
            print()
        # Main menu
        print(current_texts['select_action'])
        for key, value in current_texts['menu_options'].items():
            print(f"{key}. {value}")
        choice = input(current_texts['prompt_choice']).strip()
        if not choice:
            choice = '1'

        if choice == '0':
            # Switch language
            current_language_index = (current_language_index + 1) % len(available_languages)
            language = available_languages[current_language_index]
            current_texts = load_texts(language)
            continue  # Show the menu again in the selected language
        elif choice == '3':
            start_time = time.time()
            merge_json_files(config.merge_folder, config.input_file, current_texts)
            elapsed_time = time.time() - start_time
            print(current_texts['processing_completed'].format(elapsed_time, config.input_file))
            input(current_texts.get('press_enter_to_return', 'Press Enter to return to the main menu...'))
            continue  # Return to main menu
        elif choice == '1' or choice == '2':
            save_json = True if choice == '2' else False

            # Load the JSON file here to determine chat type
            input_file = config.input_file
            try:
                # Get file size
                file_size_bytes = os.path.getsize(input_file)
                file_size_mb = file_size_bytes / (1024 * 1024)
                file_size_gb = file_size_bytes / (1024 * 1024 * 1024)
                # Display warnings based on file size
                if file_size_mb > 50 and file_size_gb < 1:
                    print(current_texts['file_size_warning'].format(round(file_size_mb, 2)))
                elif file_size_gb >= 1:
                    print(current_texts['file_size_large_warning'].format(round(file_size_gb, 2)))
                # Load JSON data
                header_data = load_json_file_streaming(input_file, header_only=True)
                if not header_data:
                    print(current_texts['invalid_json'])
                    input(current_texts.get('press_enter_to_return', 'Press Enter to return to the main menu...'))
                    continue
            except FileNotFoundError:
                print(current_texts['file_not_found'].format(input_file))
                input(current_texts.get('press_enter_to_return', 'Press Enter to return to the main menu...'))
                continue

            # Determine chat type
            chat_type = header_data.get('type', 'group')  # Default to 'group' if not specified
            is_personal_chat = chat_type == 'personal_chat'

            # Now proceed to configuration
            config_choice = input(current_texts['config_prompt']).strip()
            if not config_choice:
                config_choice = '1'
            if config_choice == '1':
                # Use standard config
                temp_config = vars(config).copy()
            elif config_choice == '2':
                # Configure in console, pass is_personal_chat flag
                temp_config = configure_in_console(config, current_texts, is_personal_chat)
                # Optionally save config
                save_config_choice = input(current_texts['save_config_prompt']).strip().lower()
                if save_config_choice == 'y':
                    save_config_to_file(temp_config)
            else:
                continue  # Invalid choice, show menu again

            # Start analysis
            print(current_texts['start_analysis'])
            start_time = time.time()

            # Analyze messages using streaming parsing
            analysis_results, error_info = analyze_messages(input_file, temp_config, current_texts, is_personal_chat, use_streaming=True)

            # Check if there are any messages to analyze
            if not analysis_results.get('total_messages', 0):
                print(current_texts['no_messages'])
                input(current_texts.get('press_enter_to_return', 'Press Enter to return to the main menu...'))
                continue

            # Prepare output filename
            chat_name_raw = analysis_results['chat_name']
            chat_name = chat_name_raw.replace(' ', '_').replace('/', '_')
            timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
            output_filename = temp_config['output_filename_pattern'].replace('<chat_name>', chat_name).replace('<timestamp>', timestamp)

            # Generate reports
            generate_text_report(analysis_results, temp_config, current_texts, output_filename, author_github_link, author_telegram_channel, is_personal_chat)
            if save_json:
                json_output_filename = output_filename.replace('.txt', '.json')
                generate_json_report(analysis_results, json_output_filename)

            # Generate communication dynamics graphs
            if is_personal_chat:
                plot_filename_template = output_filename.replace('.txt', '_plot_<year>.png')
                generate_personal_chat_plots(analysis_results, plot_filename_template, temp_config, current_texts)
                # Inform the user about saved graphs
                years = sorted(set(date.year for date in analysis_results['daily_user_messages'].keys()))
                for year in years:
                    plot_filename = plot_filename_template.replace('<year>', str(year))
                    print(current_texts['communication_graph_saved'].format(plot_filename))
            else:
                # For group chats, generate communication statistics plots
                plot_filename_template = output_filename.replace('.txt', '_group_plot_<year>.png')
                generate_group_chat_plots(analysis_results, plot_filename_template, temp_config, current_texts)
                # Inform the user about saved graphs
                years = sorted(set(date.year for date in analysis_results['date_messages'].keys()))
                for year in years:
                    plot_filename = plot_filename_template.replace('<year>', str(year))
                    print(current_texts['communication_graph_saved'].format(plot_filename))

            elapsed_time = time.time() - start_time
            print(current_texts['processing_completed'].format(elapsed_time, output_filename))

            # Handle errors
            errors, unprocessed_messages, total_messages = error_info['errors'], error_info['unprocessed_messages'], analysis_results['total_messages']
            if unprocessed_messages > 0:
                unprocessed_percentage = (unprocessed_messages / total_messages) * 100
                if len(errors) > 10:
                    error_log_filename = f"error_logs_{chat_name}_{timestamp}.txt"
                    with open(error_log_filename, 'w', encoding='utf-8') as error_log:
                        error_log.writelines(errors)
                    print(current_texts['unprocessed_messages'].format(unprocessed_percentage))
                    print(current_texts['error_count'].format(len(errors), error_log_filename))
                else:
                    print(current_texts['unprocessed_messages'].format(unprocessed_percentage))
                    for error in errors:
                        print(error)
            if temp_config.get('show_author_links', True):
                print()
                print('⚡️')
                print(current_texts['author_links'].format(author_github_link, author_telegram_channel))
                print('⚡️')
            input(current_texts.get('press_enter_to_return', 'Press Enter to return to the main menu...'))

        else:
            continue  # Invalid choice, show menu again

if __name__ == "__main__":
    main()

---
config.py:

# config.py

# -------------------------
# Settings
# -------------------------

input_file = 'result.json'  # Name of the main file for analysis

merge_folder = ''  # Folder containing JSON files for merging; leave as '' to use the current directory

# Output filename pattern
output_filename_pattern = '<chat_name>_<timestamp>.txt'  # Output report filename; <chat_name> will be replaced with chat name

# List of stop words to exclude from frequent words analysis
stop_words_type = 'minimal'  # 'minimal' or 'extended'

top_participants_count = None  # Set to None to display all participants
top_words_count = 100  # Number of top words to display
top_phrases_count = 100  # Number of top phrases to display

emojis = {
    'title': '💬',
    'participant': '👥',
    'word': '🔠',
    'phrase': '🔠',
    'activity': '📊',
    'list_item': '➡️',
    'messages': '✉️',
    'symbols': '🔣',
    'avg_symbols': '💬',
    'voice': '🎵',
    'forwarded': '📩',
    'pictures': '🖼',
    'videos': '🎥',
    'gif': '🎬',
    'audios': '🎧',
    'files': '📑',
    'sticker': '💌',
    'command': '❗',
    'emoji': '😊',
    'profanity': '💢',
    'links': '🔗',
    'poll': '📊',
}

top_days_count = 10  # Number of most active days to display

show_non_consecutive_counts = True  # Display messages non-consecutively in the report

exclude_bots = True  # Whether to exclude bots from analysis
bot_identifiers = ['Bot']  # Strings to identify bots

# Day names and month names in Russian and English
day_names = {
    'ru': ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'],
    'en': ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
}
month_names = {
    'ru': ['январь', 'февраль', 'март', 'апрель', 'май', 'июнь', 'июль', 'август', 'сентябрь', 'октябрь', 'ноябрь', 'декабрь'],
    'en': ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
}

# Command prefixes (e.g., '/')
commands_identifiers = ['/']

# Regular expression to find emojis
import re
emoji_pattern = re.compile("["
                           u"\U0001F600-\U0001F64F"  # Emoticons
                           u"\U0001F300-\U0001F5FF"  # Symbols & Pictographs
                           u"\U0001F680-\U0001F6FF"  # Transport & Map
                           u"\U0001F1E0-\U0001F1FF"  # Flags
                           u"\U0001F900-\U0001F9FF"  # Supplemental Symbols and Pictographs
                           u"\U0001FA70-\U0001FAFF"  # Symbols and Pictographs Extended-A
                           u"\U00002702-\U000027B0"  # Dingbats
                           u"\U000024C2-\U0001F251"  # Enclosed characters
                           "]+", flags=re.UNICODE)

# Regular expression to find URLs
url_pattern = re.compile(
    r'(?i)\b((?:https?:\/\/|www\d{0,3}[.]|telegram[.]me\/|t[.]me\/|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+)', re.IGNORECASE)

show_author_links = True  # Set to False to disable displaying author links

show_user_links = False  # False by default

first_message_interval_hours = 1  # Default interval in hours to consider who wrote first

words_dir = 'words'  # Directory where stop words and profanity lists are stored

# -------------------------
# End of Settings
# -------------------------

---
locales\en.py:

# locales/en.py

texts = {
    'ascii_art': "\033[32m" + r"""
  _______         _____ _           _    _____ _        _
 |__   __|       / ____| |         | |  / ____| |      | |
    | | ___  ___| |    | |__   __ _| |_| (___ | |_ __ _| |_
    | |/ _ \/ __| |    | '_ \ / _` | __|\___ \| __/ _` | __|
    | |  __/\__ \ |____| | | | (_| | |_ ____) | || (_| | |_
    |_|\___||___/\_____|_| |_|\__,_|\__|_____/ \__\__,_|\__|
""" + "\033[0m",
    'description': "Telegram chat statistics based on JSON export",
    'menu_options': {
        '0': "Switch language",
        '1': "Analyze file",
        '2': "Analyze and save results to TXT and JSON files",
        '3': "Merge resultNUMBER.json files and save to file",
    },
    'prompt_choice': "Enter your choice number and press Enter (default is 1): ",
    'start_analysis': "Starting analysis...",
    'start_analysis_save': "Starting analysis and saving to text and JSON files...",
    'file_not_found': "❌ File '{0}' not found.",
    'file_size_warning': "⚠️ File size is {0} MB, processing may take some time.",
    'file_size_large_warning': "⚠️ File size is {0} GB, processing may take a very long time on weak devices and may be unstable.",
    'invalid_json': "Error: Invalid JSON file format.",
    'no_messages': "The file contains no messages for analysis.",
    'total_messages': "Total messages in chat: {0}",
    'processing_completed': "✅ Analysis completed in {0:.2f} seconds. Results saved to '{1}'.",
    'unprocessed_messages': "Failed to process {0:.2f}% of messages.",
    'error_count': "Number of errors: {0}. Details in '{1}'.",
    'author_links': "⚡️ GitHub (check updates): {0}\n⚡️ Telegram channel: {1}",
    'select_action': "Select an action:",
    'processing': "Processed {0}/{1} messages... {2}",
    'date_range': "",
    'non_consecutive': "not consecutive",
    'symbols': "symbols",
    'avg_symbols_in_message': "Symbols per message",
    'voice_messages': "Voice messages",
    'forwards': "Forwards",
    'pictures': "Pictures",
    'videos': "Videos",
    'audios': "Audios",
    'files': "Files",
    'stickers': "Stickers",
    'commands': "Commands",
    'emojis': "Emoji",
    'profanity_messages': "Messages with profanity",
    'top_participants': "Top participants",
    'top_words': "Top words",
    'top_phrases': "Top phrases",
    'times': "times",
    'activity': "Activity",
    'most_active_days': "Most active days",
    'messages': "messages",
    'average_message_length': "average message length",
    'polls': "Polls",
    'links': "Links",
    'config_prompt': "1. Use standard settings from config.py\n2. Configure in console\nEnter your choice number and press Enter (default is 1): ",
    'save_config_prompt': "Save these settings to confignew.py? (y/n, default n): ",
    'creator_info': "Group creator: {0}, ID: {1}",
    'title_changes': "Title changes: {0}",
    'invite_top': "Top users by invitations",
    'personal_chat_stats': "{0} wrote more: {1} messages\n{2}: {3} messages",
    'reading_time_estimate': "To read the entire conversation, you will need approximately {0} seconds. This is {1} minutes or {2} hours{3}.",
    'includes_media': "Including media files: {0}",
    'hours': "hours",
    'days': " or {0} days",
    'communication_graph_saved': "Communication graph saved to '{0}'.",
    'press_enter_to_return': "Press Enter to return to the main menu...",
    'first_sender_triangle_label': "First Sender After Interval",
    'date_label': "Date",
    'message_count_label': "Number of Messages",
    'chat_activity_in_year': "Chat Activity in {0}",
    'group_chat_activity_in_year': "Group Chat Activity in {0}",
    'first_message_interval_prompt': "After what interval (in hours) to consider who wrote first? (default 1): ",
}

---
locales\ru.py:

# locales/ru.py

texts = {
    'ascii_art': "\033[32m" + r"""
  _______         _____ _           _    _____ _        _
 |__   __|       / ____| |         | |  / ____| |      | |
    | | ___  ___| |    | |__   __ _| |_| (___ | |_ __ _| |_
    | |/ _ \/ __| |    | '_ \ / _` | __|\___ \| __/ _` | __|
    | |  __/\__ \ |____| | | | (_| | |_ ____) | || (_| | |_
    |_|\___||___/\_____|_| |_|\__,_|\__|_____/ \__\__,_|\__|
""" + "\033[0m",
    'description': "Статистика чата Telegram на основе JSON-экспорта",
    'menu_options': {
        '0': "Сменить язык",
        '1': "Анализировать файл",
        '2': "Анализировать и сохранить результат в TXT и JSON файлы",
        '3': "Объединить файлы resultЦИФРА.json и сохранить в файл",
    },
    'prompt_choice': "Введите номер выбора и нажмите Enter (по умолчанию 1): ",
    'start_analysis': "Запуск анализа...",
    'start_analysis_save': "Запуск анализа и сохранение в текстовый и JSON файлы...",
    'file_not_found': "❌ Файл '{0}' не найден.",
    'file_size_warning': "⚠️ Размер файла {0} МБ, обработка займёт время.",
    'file_size_large_warning': "⚠️ Размер файла {0} ГБ, обработка может занять очень много времени на слабых устройствах и может быть нестабильной.",
    'invalid_json': "Ошибка: Неверный формат JSON файла.",
    'no_messages': "Файл не содержит сообщений для анализа.",
    'total_messages': "Общее количество сообщений в чате: {0}",
    'processing_completed': "✅ Анализ завершён за {0:.2f} секунд. Результаты сохранены в файл '{1}'.",
    'unprocessed_messages': "Не удалось обработать {0:.2f}% сообщений.",
    'error_count': "Количество ошибок: {0}. Подробности в '{1}'.",
    'author_links': "⚡️ GitHub (обновления): {0}\n⚡️ Телеграм-канал: {1}",
    'select_action': "Действие:",
    'processing': "Обработано {0}/{1} сообщений... {2}",
    'date_range': "",
    'non_consecutive': "не подряд",
    'symbols': "символов",
    'avg_symbols_in_message': "Символов в сообщении",
    'voice_messages': "Голосовых",
    'forwards': "Пересланных",
    'pictures': "Изображений",
    'videos': "Видео",
    'audios': "Аудио",
    'files': "Файлы",
    'stickers': "Стикеров",
    'commands': "Команд",
    'emojis': "Эмодзи",
    'profanity_messages': "Сообщений с нецензурной лексикой",
    'top_participants': "Топ участников",
    'top_words': "Топ слов",
    'top_phrases': "Топ фраз",
    'times': "раз",
    'activity': "Активность",
    'most_active_days': "Самые активные дни",
    'messages': "сообщений",
    'average_message_length': "средняя длина сообщения",
    'polls': "Опросов",
    'links': "Ссылки",
    'config_prompt': "1. Использовать стандартную настройку из config.py\n2. Настроить в консоли\nВведите номер выбора и нажмите Enter (по умолчанию 1): ",
    'save_config_prompt': "Сохранить эти настройки в файл confignew.py? (y/n, по умолчанию n): ",
    'creator_info': "Создатель группы: {0}, ID: {1}",
    'title_changes': "Изменений заголовка: {0}",
    'invite_top': "Топ пользователей по приглашениям",
    'personal_chat_stats': "{0} писал чаще: {1} сообщений\n{2}: {3} сообщений",
    'reading_time_estimate': "Чтобы прочитать всю переписку, вам понадобится примерно {0} секунд. Это {1} минут или {2} часов{3}.",
    'includes_media': "Включая медиа-файлы: {0}",
    'hours': "часов",
    'days': "или {0} дней",
    'communication_graph_saved': "График активности общения сохранён в '{0}'.",
    'press_enter_to_return': "Нажмите Enter, чтобы вернуться в главное меню...",
    'first_sender_triangle_label': "Первый отправитель после перерыва",
    'date_label': "Дата",
    'message_count_label': "Количество сообщений",
    'chat_activity_in_year': "Активность общения в {0} году",
    'group_chat_activity_in_year': "Активность группового чата в {0} году",
    'first_message_interval_prompt': "После какого перерыва (в часах) считать кто написал первым? (по умолчанию 1): ",
}

---
modules\analyzer.py:

# modules/analyzer.py

import datetime
from collections import Counter, defaultdict
import re
import os
import ijson
import json

def is_bot(user_name, bot_identifiers):
    """Check if the user is a bot based on identifiers."""
    return any(bot_id.lower() in user_name.lower() for bot_id in bot_identifiers)

def format_number(number):
    """Format number with spaces as thousands separators."""
    s = str(int(number))
    return ' '.join([s[max(i - 3, 0):i] for i in range(len(s), 0, -3)][::-1])

def load_word_list(file_path):
    """Load words from a file into a set."""
    words = set()
    if os.path.exists(file_path):
        with open(file_path, 'r', encoding='utf-8') as f:
            for line in f:
                word = line.strip().lower()
                if word:
                    words.add(word)
    return words

def analyze_messages(input_file, config, current_texts, is_personal_chat, use_streaming=False):
    """Analyze messages and collect statistics."""
    if use_streaming:
        # Use streaming JSON parsing
        messages = parse_messages_streaming(input_file)
        total_messages = 0  # Will be counted during processing
        header_data = load_json_header(input_file)
        chat_name = header_data.get('name', 'Chat Name')
    else:
        # Load entire JSON data
        data = load_json_file(input_file)
        messages = data.get('messages', [])
        total_messages = len(messages)
        print(current_texts['total_messages'].format(format_number(total_messages)))
        if total_messages == 0:
            return {}, {'errors': [], 'unprocessed_messages': 0}
        chat_name = data.get('name', 'Chat Name')

    # Initialize variables for analysis
    user_counts = Counter()
    user_symbols = Counter()
    non_consecutive_counts = Counter()
    non_consecutive_symbols = Counter()
    user_ids = {}
    prev_user = None
    prev_time = None
    words = []
    phrases_2 = []
    phrases_3 = []
    hours = Counter()
    weekdays = Counter()
    months = Counter()
    years = Counter()
    dates = Counter()
    date_messages = defaultdict(int)
    date_symbols = defaultdict(int)
    # For personal chat plots
    daily_user_messages = defaultdict(lambda: Counter())
    daily_first_sender = {}
    # For tracking invitations
    invite_counts = Counter()
    title_change_count = 0  # Will no longer be used in the report
    creator_name = None
    creator_id = None
    includes_media = 0  # For estimating reading time
    chain_started = False  # For non-consecutive message counting

    # Load stop words and profanity words from files
    words_dir = config.get('words_dir', 'words')
    stop_words_type = config.get('stop_words_type', 'minimal')
    stop_words_file = os.path.join(words_dir, f'stop_words_{stop_words_type}.txt')
    profanity_words_file = os.path.join(words_dir, 'profanity_words.txt')

    # Load stop words and profanity words
    stop_words = load_word_list(stop_words_file)
    profanity_words = load_word_list(profanity_words_file)

    # Ensure default stop words if file not found
    if not stop_words:
        stop_words = set(['и', 'в', 'не', 'на', 'с', 'что', 'а', 'как', 'это', 'по', 'но',
                          'из', 'у', 'за', 'о', 'же', 'то', 'к', 'для', 'до', 'вы', 'мы',
                          'они', 'он', 'она', 'оно', 'так', 'было', 'только', 'бы', 'когда',
                          'уже', 'ли', 'или', 'со'])

    # Adding common English stop words
    # Load English stop words from a file or define them
    english_stop_words_file = os.path.join(words_dir, 'stop_words_english.txt')
    english_stop_words = load_word_list(english_stop_words_file)
    stop_words.update(english_stop_words)

    # Other configurations and patterns
    commands_identifiers = set(config.get('commands_identifiers', ['/']))
    emoji_pattern = config['emoji_pattern']
    url_pattern = config['url_pattern']

    # Initialize message counts
    message_counts = {
        'text': 0,
        'sticker': 0,
        'picture': 0,
        'video': 0,
        'gif': 0,
        'voice_message': 0,
        'audio': 0,
        'file': 0,
        'commands': 0,
        'forwards': 0,
        'emojis': 0,
        'profanity': 0,
        'replies': 0,
        'poll': 0,
        'links': 0,
    }

    first_date = None
    last_date = None

    unprocessed_messages = 0
    error_count = 0
    errors = []

    spinner = ['|', '/', '-', '\\']
    spinner_index = 0

    def print_progress(current):
        nonlocal spinner_index
        current_formatted = format_number(current)
        progress = current_texts['processing'].format(current_formatted, 'Unknown', spinner[spinner_index % len(spinner)])
        spinner_index += 1
        print(progress, end='\r', flush=True)

    # Get the interval from config (default to 1 hour)
    first_message_interval_seconds = config.get('first_message_interval_hours', 1) * 3600

    total_messages_processed = 0

    for message in messages:
        total_messages_processed += 1
        idx = total_messages_processed - 1

        # Skip if message is None
        if message is None:
            unprocessed_messages += 1
            continue

        try:
            # Skip messages without 'from' or 'actor'
            if not isinstance(message, dict):
                unprocessed_messages += 1
                continue

            if 'from' not in message and 'actor' not in message:
                unprocessed_messages += 1
                continue
            user = message.get('from', '') or message.get('actor', 'Unknown')
            from_id = message.get('from_id', '') or message.get('actor_id', '') or message.get('id', '')
            if from_id:
                if isinstance(from_id, str):
                    if from_id.startswith('user'):
                        user_id = from_id.replace('user', '')
                    elif from_id.startswith('channel'):
                        user_id = from_id.replace('channel', '')
                    else:
                        user_id = from_id
                else:
                    user_id = str(from_id)
                user_ids[user] = user_id
            else:
                user_id = ''
                user_ids[user] = user_id

            # Exclude bots only in group chats
            if not is_personal_chat and config.get('exclude_bots', True) and is_bot(user, config.get('bot_identifiers', [])):
                continue

            text = message.get('text', '')
            if isinstance(text, list):
                text_pieces = []
                for t_piece in text:
                    if isinstance(t_piece, dict):
                        text_pieces.append(t_piece.get('text', ''))
                    elif isinstance(t_piece, str):
                        text_pieces.append(t_piece)
                text = ''.join(text_pieces)
            elif text is None:
                text = ''

            # Only process messages where 'text' field is present
            if message.get('type') == 'message':
                symbols = len(text)
                user_counts[user] += 1
                user_symbols[user] += symbols

                if prev_user != user:
                    non_consecutive_counts[user] += 1
                    non_consecutive_symbols[user] += symbols
                    chain_started = True
                else:
                    chain_started = False

                # Process message date and time
                message_date = message.get('date')
                if message_date:
                    try:
                        date_time = datetime.datetime.fromisoformat(message_date)
                        if first_date is None or date_time < first_date:
                            first_date = date_time
                        if last_date is None or date_time > last_date:
                            last_date = date_time

                        # For personal chat, track daily message counts and first sender
                        if is_personal_chat:
                            date_only = date_time.date()
                            daily_user_messages[date_only][user] += 1
                            if date_only not in daily_first_sender or (prev_time and (date_time - prev_time).total_seconds() > first_message_interval_seconds):
                                daily_first_sender[date_only] = user
                        # For group chats, collect date_messages
                        date_only = date_time.date()
                        date_messages[date_only] +=1
                        date_symbols[date_only] += symbols

                        hours[date_time.hour] += 1
                        weekday_index = date_time.weekday()
                        weekday_name = config['day_names']['ru'][weekday_index]
                        weekdays[weekday_name] += 1
                        month_name = f"{config['month_names']['ru'][date_time.month -1]} {date_time.year}"
                        months[month_name] +=1
                        years[date_time.year] +=1
                        dates[date_only] += 1
                    except (ValueError, KeyError) as e:
                        error_count +=1
                        errors.append(f"Error processing date in message id {message.get('id')}: {e}\n")
                else:
                    prev_time = None

                prev_user = user
                if 'date_time' in locals():
                    prev_time = date_time

                # Collect words for frequency analysis
                if text:
                    text_clean = re.sub(r'[^\w\s]', '', text.lower())
                    words_in_text = [word for word in text_clean.split() if word.isalpha()]

                    # Collect phrases (2- and 3-word sequences)
                    words_filtered = [word for word in words_in_text if word not in stop_words]
                    for i in range(len(words_filtered) - 1):
                        phrase_2 = f"{words_filtered[i]} {words_filtered[i+1]}"
                        phrases_2.append(phrase_2)
                        if i < len(words_filtered) - 2:
                            phrase_3 = f"{words_filtered[i]} {words_filtered[i+1]} {words_filtered[i+2]}"
                            phrases_3.append(phrase_3)

                    # Add to words list
                    words.extend([word for word in words_in_text if word not in stop_words])

                    # Check for commands
                    if text.strip().startswith(tuple(commands_identifiers)):
                        message_counts['commands'] += 1

                    # Check for emojis
                    if emoji_pattern.search(text):
                        message_counts['emojis'] += 1

                    # Check for profanity
                    if profanity_words.intersection(set(words_in_text)):
                        message_counts['profanity'] += 1

                    # Check for links
                    if url_pattern.search(text):
                        message_counts['links'] +=1

                # Check for forwarded messages
                if 'forwarded_from' in message:
                    message_counts['forwards'] += 1

                # Check for replies
                if 'reply_to_message_id' in message:
                    message_counts['replies'] += 1

                # Count media types
                if 'media_type' in message:
                    media_type = message['media_type']
                    if media_type == 'sticker':
                        message_counts['sticker'] += 1
                        includes_media += 1
                    elif media_type == 'photo':
                        message_counts['picture'] += 1
                        includes_media += 1
                    elif media_type == 'video_file':
                        file_name = message.get('file', '') or message.get('file_name', '')
                        if 'gif' in file_name.lower():
                            message_counts['gif'] += 1
                        else:
                            message_counts['video'] += 1
                        includes_media += 1
                    elif media_type == 'voice_message' or media_type == 'video_message':
                        message_counts['voice_message'] += 1
                        includes_media += 1
                    elif media_type == 'audio_file':
                        message_counts['audio'] += 1
                        includes_media += 1
                    elif media_type == 'document':
                        mime_type = message.get('mime_type', '')
                        if mime_type and mime_type.startswith('image/'):
                            message_counts['picture'] += 1
                        else:
                            message_counts['file'] += 1
                        includes_media += 1
                    elif media_type == 'animation':
                        message_counts['gif'] +=1
                        includes_media += 1
                else:
                    # Check for media in messages without 'media_type' field
                    if 'photo' in message:
                        message_counts['picture'] += 1
                        includes_media += 1
                    elif 'file' in message:
                        mime_type = message.get('mime_type', '')
                        if mime_type and mime_type.startswith('image/'):
                            message_counts['picture'] += 1
                        else:
                            message_counts['file'] += 1
                        includes_media += 1
                    else:
                        message_counts['text'] += 1

            elif message.get('type') == 'service':
                # Handle service messages
                action = message.get('action', '')
                if action == 'create_group':
                    creator_name = message.get('actor', '')
                    creator_id = message.get('actor_id', '')
                    if isinstance(creator_id, str) and creator_id.startswith('user'):
                        creator_id = creator_id.replace('user', '')
                elif action == 'invite_member' or action == 'invite_members':
                    inviter = message.get('actor', 'Unknown')
                    invite_counts[inviter] += 1
                # We are no longer tracking title changes
                # elif action == 'edit_title':
                #     title_change_count +=1
                else:
                    pass  # Handle other service actions if needed
            else:
                unprocessed_messages += 1
                continue

            # Update progress
            if total_messages_processed % 1000 == 0:
                print_progress(total_messages_processed)

        except Exception as e:
            unprocessed_messages += 1
            error_count += 1
            msg_id = message.get('id', 'Unknown') if isinstance(message, dict) else 'Unknown'
            errors.append(f"Error processing message id {msg_id}: {e}\n")

    # Final progress update
    print_progress(total_messages_processed)
    print('\n')

    total_messages = total_messages_processed  # Total messages processed
    print(current_texts['total_messages'].format(format_number(total_messages)))

    # Calculate statistics
    total_msgs = sum(user_counts.values())
    total_symbols = sum(user_symbols.values())
    total_non_consecutive_msgs = sum(non_consecutive_counts.values())
    total_non_consecutive_symbols = sum(non_consecutive_symbols.values())
    avg_message_length = total_symbols / total_msgs if total_msgs else 0
    common_words = Counter(words).most_common(config.get('top_words_count', 100))
    common_phrases = Counter(phrases_2 + phrases_3).most_common(config.get('top_phrases_count', 100))
    activity = {
        'hours': hours.most_common(3) if hours else [],
        'weekdays': weekdays.most_common(3) if weekdays else [],
        'months': months.most_common(3) if months else [],
        'years': years.most_common(3) if years else [],
    }
    # Top most active days
    top_days = dates.most_common(config.get('top_days_count', 10))

    analysis_results = {
        'chat_name': chat_name,
        'total_messages': total_msgs,
        'total_symbols': total_symbols,
        'total_non_consecutive_messages': total_non_consecutive_msgs,
        'total_non_consecutive_symbols': total_non_consecutive_symbols,
        'user_counts': user_counts,
        'user_symbols': user_symbols,
        'non_consecutive_counts': non_consecutive_counts,
        'non_consecutive_symbols': non_consecutive_symbols,
        'user_ids': user_ids,
        'first_date': first_date,
        'last_date': last_date,
        'avg_message_length': avg_message_length,
        'common_words': common_words,
        'common_phrases': common_phrases,
        'activity': activity,
        'top_days': top_days,
        'message_counts': message_counts,
        'invite_counts': invite_counts,
        'creator_name': creator_name,
        'creator_id': creator_id,
        'date_symbols': date_symbols,
        'includes_media': includes_media,
        'dates': dates,
        'date_messages': date_messages,
        'daily_user_messages': daily_user_messages,
        'daily_first_sender': daily_first_sender,
    }

    error_info = {
        'errors': errors,
        'unprocessed_messages': unprocessed_messages,
    }

    return analysis_results, error_info

def parse_messages_streaming(input_file):
    """Parse messages from JSON file using streaming parsing."""
    with open(input_file, 'r', encoding='utf-8') as f:
        messages = ijson.items(f, 'messages.item')
        for message in messages:
            if message is not None:
                yield message

def load_json_header(input_file):
    """Load only the header information from the JSON file."""
    header_fields = ['name', 'type', 'id']
    header_data = {}
    with open(input_file, 'r', encoding='utf-8') as f:
        parser = ijson.parse(f)
        current_field = None
        for prefix, event, value in parser:
            if prefix == '' and event == 'map_key':
                current_field = value
            elif current_field in header_fields and event in ('string', 'number'):
                header_data[current_field] = value
            if all(field in header_data for field in header_fields):
                break  # Stop parsing after collecting all header fields
    return header_data

def load_json_file(input_file):
    """Load the entire JSON file."""
    with open(input_file, 'r', encoding='utf-8') as f:
        data = json.load(f)
    return data

---
modules\config_handler.py:

# modules/config_handler.py

def configure_in_console(config, current_texts, is_personal_chat):
    """Configure settings in the console, adjusting for chat type."""
    temp_config = {}
    print(current_texts['select_action'])

    # Configurations specific to personal chats
    if is_personal_chat:
        # Configurable interval for 'who wrote first'
        interval_input = input(current_texts.get('first_message_interval_prompt', "After what interval (in hours) to consider who wrote first? (default 1): ")).strip()
        if interval_input.isdigit():
            temp_config['first_message_interval_hours'] = int(interval_input)
        else:
            temp_config['first_message_interval_hours'] = getattr(config, 'first_message_interval_hours', 1)

    # Choose stop words type
    stop_words_default = getattr(config, 'stop_words_type', 'minimal')
    stop_words_choice = input(f"Choose stop words list (1-minimal, 2-extended, default {stop_words_default}): ").strip()
    if stop_words_choice == '1':
        temp_config['stop_words_type'] = 'minimal'
    elif stop_words_choice == '2':
        temp_config['stop_words_type'] = 'extended'
    else:
        temp_config['stop_words_type'] = stop_words_default

    # Common configurations
    top_words_count = input(f"Number of top words to display (default {config.top_words_count}): ").strip()
    temp_config['top_words_count'] = int(top_words_count) if top_words_count.isdigit() else config.top_words_count

    top_phrases_count = input(f"Number of top phrases to display (default {config.top_phrases_count}): ").strip()
    temp_config['top_phrases_count'] = int(top_phrases_count) if top_phrases_count.isdigit() else config.top_phrases_count

    # Configurations specific to group chats
    if not is_personal_chat:
        exclude_bots = input(f"Exclude bots? (y/n, default {'Yes' if config.exclude_bots else 'No'}): ").strip().lower()
        temp_config['exclude_bots'] = exclude_bots != 'n'  # Default to True if empty or 'y'

        show_non_consecutive_counts = input(f"Show non-consecutive message counts? (y/n, default {'Yes' if config.show_non_consecutive_counts else 'No'}): ").strip().lower()
        temp_config['show_non_consecutive_counts'] = show_non_consecutive_counts != 'n'

        top_participants_count = input(f"Number of top participants to display (default {config.top_participants_count}): ").strip()
        temp_config['top_participants_count'] = int(top_participants_count) if top_participants_count.isdigit() else config.top_participants_count

        show_user_links = input(f"Show user links? (y/n, default {'Yes' if config.show_user_links else 'No'}): ").strip().lower()
        temp_config['show_user_links'] = show_user_links != 'n'

    # Copy other configurations
    temp_config['input_file'] = config.input_file
    temp_config['merge_folder'] = config.merge_folder
    temp_config['output_filename_pattern'] = config.output_filename_pattern
    temp_config['show_author_links'] = config.show_author_links
    temp_config['commands_identifiers'] = config.commands_identifiers
    temp_config['emoji_pattern'] = config.emoji_pattern
    temp_config['url_pattern'] = config.url_pattern
    temp_config['day_names'] = config.day_names
    temp_config['month_names'] = config.month_names
    temp_config['top_days_count'] = config.top_days_count
    temp_config['bot_identifiers'] = config.bot_identifiers
    temp_config['emojis'] = config.emojis
    temp_config['words_dir'] = config.words_dir

    # For personal chats, if the interval wasn't set (because we didn't ask), fetch from config
    if is_personal_chat and 'first_message_interval_hours' not in temp_config:
        temp_config['first_message_interval_hours'] = getattr(config, 'first_message_interval_hours', 1)

    return temp_config

def save_config_to_file(temp_config):
    """Save the temporary configuration to a new file."""
    with open('confignew.py', 'w', encoding='utf-8') as f:
        for key, value in temp_config.items():
            if isinstance(value, str):
                f.write(f"{key} = '{value}'\n")
            else:
                f.write(f"{key} = {value}\n")
    print("Settings have been saved to confignew.py")

---
modules\data_loader.py:

import json
import os
import re
import ijson

def load_json_file_streaming(input_file, header_only=False):
    """Load JSON data using streaming parsing."""
    try:
        with open(input_file, 'r', encoding='utf-8') as f:
            parser = ijson.parse(f)
            data = {}
            current_key = None
            for prefix, event, value in parser:
                if prefix == '' and event == 'map_key':
                    current_key = value
                elif current_key == 'name' and prefix == 'name' and event == 'string':
                    data['name'] = value
                elif current_key == 'type' and prefix == 'type' and event == 'string':
                    data['type'] = value
                elif current_key == 'id' and prefix == 'id' and event in ('number', 'string'):
                    data['id'] = value
                elif current_key == 'messages' and event == 'start_array':
                    if header_only:
                        break
                    else:
                        data['messages'] = []
                        for msg in ijson.items(f, 'messages.item'):
                            data['messages'].append(msg)
                    break  # We have everything we need
            return data
    except Exception as e:
        print(f"Error reading JSON file: {e}")
        return None

def merge_json_files(folder_path, output_file, current_texts):
    """Merge multiple JSON files matching a pattern into a single output file."""
    merged_messages = []
    pattern = re.compile(r'^result\d+\.json$')
    header_data = None
    if not os.path.isdir(folder_path) or folder_path == '':
        folder_path = '.'  # Use current directory if folder is not specified or does not exist
    for filename in os.listdir(folder_path):
        if pattern.match(filename):
            file_path = os.path.join(folder_path, filename)
            if os.path.isfile(file_path):
                data = load_json_file_streaming(file_path)
                if data:
                    if header_data is None:
                        header_data = {k: data[k] for k in data if k != 'messages'}
                    merged_messages.extend(data.get('messages', []))
    if header_data and merged_messages:
        header_data['messages'] = merged_messages
        with open(output_file, 'w', encoding='utf-8') as f:
            json.dump(header_data, f, ensure_ascii=False)
        print(f"✅ Files matching 'result*.json' pattern in '{folder_path}' have been merged into '{output_file}'.")
    else:
        print("❌ No data found to merge.")

---
modules\report_generator.py:

def format_number(number):
    """Format number with spaces as thousands separators."""
    s = str(int(number))
    return ' '.join([s[max(i - 3, 0):i] for i in range(len(s), 0, -3)][::-1])

def generate_text_report(analysis_results, config, current_texts, output_filename, author_github_link, author_telegram_channel, is_personal_chat):
    """Generate the textual report based on analysis results."""
    with open(output_filename, 'w', encoding='utf-8') as f:
        first_date = analysis_results['first_date']
        last_date = analysis_results['last_date']
        date_range_str = ''
        if first_date and last_date:
            date_range_str = f"за период: {first_date.strftime('%d.%m.%Y')} – {last_date.strftime('%d.%m.%Y')}"

        chat_name = analysis_results.get('chat_name', 'Chat Name')
        f.write(f"Статистика чата \"{chat_name}\" {date_range_str}\n\n")

        # Messages and symbols statistics
        total_msgs_formatted = format_number(analysis_results['total_messages'])
        total_symbols_formatted = format_number(analysis_results['total_symbols'])
        avg_message_length = analysis_results['avg_message_length']

        show_non_consecutive = config.get('show_non_consecutive_counts', True)
        if show_non_consecutive:
            total_non_consecutive_msgs_formatted = format_number(analysis_results['total_non_consecutive_messages'])
            total_non_consecutive_symbols_formatted = format_number(analysis_results['total_non_consecutive_symbols'])
            f.write(f"{config['emojis'].get('messages','')} {current_texts['messages'].capitalize()}: {total_msgs_formatted} ({total_non_consecutive_msgs_formatted} {current_texts.get('non_consecutive', 'not consecutive')})\n")
            f.write(f"{config['emojis'].get('symbols','')} {current_texts.get('symbols', 'symbols').capitalize()}: {total_symbols_formatted} ({total_non_consecutive_symbols_formatted} {current_texts.get('non_consecutive', 'not consecutive')})\n")
        else:
            f.write(f"{config['emojis'].get('messages','')} {current_texts['messages'].capitalize()}: {total_msgs_formatted}\n")
            f.write(f"{config['emojis'].get('symbols','')} {current_texts.get('symbols', 'symbols').capitalize()}: {total_symbols_formatted}\n")

        f.write(f"{config['emojis'].get('avg_symbols','')} {current_texts.get('avg_symbols_in_message', 'Symbols per message')}: {avg_message_length:.0f}\n\n")

        # Message counts by type
        message_counts = analysis_results['message_counts']
        f.write(f"{config['emojis'].get('pictures','')} {current_texts.get('pictures', 'Pictures')}: {format_number(message_counts['picture'])}\n")
        f.write(f"{config['emojis'].get('videos','')} {current_texts.get('videos', 'Videos')}: {format_number(message_counts['video'])}\n")
        f.write(f"{config['emojis'].get('files','')} {current_texts.get('files', 'Files')}: {format_number(message_counts['file'])}\n")
        f.write(f"{config['emojis'].get('audios','')} {current_texts.get('audios', 'Audios')}: {format_number(message_counts['audio'])}\n")
        f.write(f"{config['emojis'].get('links','')} {current_texts.get('links', 'Links')}: {format_number(message_counts['links'])}\n")
        f.write(f"{config['emojis'].get('voice','')} {current_texts.get('voice_messages', 'Voice messages')}: {format_number(message_counts['voice_message'])}\n")
        f.write(f"{config['emojis'].get('gif','')} GIF: {format_number(message_counts['gif'])}\n")
        f.write(f"{config['emojis'].get('sticker','')} {current_texts.get('stickers', 'Stickers')}: {format_number(message_counts['sticker'])}\n")
        f.write(f"{config['emojis'].get('emoji','')} {current_texts.get('emojis', 'Emoji')}: {format_number(message_counts['emojis'])}\n")
        f.write(f"{config['emojis'].get('poll','')} {current_texts.get('polls', 'Polls')}: {format_number(message_counts['poll'])}\n")
        f.write(f"{config['emojis'].get('command','')} {current_texts.get('commands', 'Commands')}: {format_number(message_counts['commands'])}\n")
        f.write(f"{config['emojis'].get('profanity','')} {current_texts.get('profanity_messages', 'Messages with profanity')}: {format_number(message_counts['profanity'])}\n\n")

        if is_personal_chat:
            # Personal chat statistics
            participants = list(analysis_results['user_counts'].keys())
            if len(participants) == 2:
                user_counts = analysis_results['user_counts']
                user1, user2 = participants
                count1 = user_counts[user1]
                count2 = user_counts[user2]
                if count1 > count2:
                    f.write(f"{current_texts['personal_chat_stats'].format(user1, format_number(count1), user2, format_number(count2))}\n")
                else:
                    f.write(f"{current_texts['personal_chat_stats'].format(user2, format_number(count2), user1, format_number(count1))}\n")
                # Estimate reading time
                total_symbols = analysis_results['total_symbols']
                total_reading_seconds = (total_symbols / 1000) * 60  # Assuming 1000 chars per minute
                total_reading_minutes = total_reading_seconds / 60
                total_reading_hours = total_reading_minutes / 60
                total_reading_days = total_reading_hours / 24
                total_reading_days_formatted = f"{total_reading_days:.2f}" if total_reading_days >= 1 else ""
                days_part = current_texts['days'].format(total_reading_days_formatted) if total_reading_days_formatted else ""
                f.write(f"\n{current_texts['reading_time_estimate'].format(int(total_reading_seconds), int(total_reading_minutes), int(total_reading_hours), days_part)}\n")
            else:
                f.write("Unexpected number of participants in personal chat.\n")
        else:
            # Top participants
            f.write(f"{config['emojis'].get('participant', '')} {current_texts.get('top_participants', 'Top participants')}:\n")
            sorted_users = analysis_results['user_counts'].most_common(config.get('top_participants_count'))
            rank = 1
            for user, count in sorted_users:
                non_consecutive_count = analysis_results['non_consecutive_counts'][user]
                symbols = analysis_results['user_symbols'][user]
                non_consecutive_symbols = analysis_results['non_consecutive_symbols'][user]
                user_id = analysis_results['user_ids'].get(user, '')
                if config.get('show_non_consecutive_counts', True):
                    if config.get('show_user_links', False):
                        user_link = f"tg://openmessage?user_id={user_id}" if user_id else ''
                        f.write(f"{rank}. {user} ({user_link}): {format_number(count)} ({format_number(non_consecutive_count)}) · {format_number(symbols)} ({format_number(non_consecutive_symbols)})\n")
                    else:
                        f.write(f"{rank}. {user}: {format_number(count)} ({format_number(non_consecutive_count)}) · {format_number(symbols)} ({format_number(non_consecutive_symbols)})\n")
                else:
                    if config.get('show_user_links', False):
                        user_link = f"tg://openmessage?user_id={user_id}" if user_id else ''
                        f.write(f"{rank}. {user} ({user_link}): {format_number(count)} · {format_number(symbols)}\n")
                    else:
                        f.write(f"{rank}. {user}: {format_number(count)} · {format_number(symbols)}\n")
                rank += 1
            f.write("\n")

            # Top inviters
            invite_counts = analysis_results['invite_counts']
            if invite_counts:
                f.write(f"{current_texts['invite_top']}:\n")
                sorted_invites = invite_counts.most_common()
                rank = 1
                for inviter, invite_count in sorted_invites:
                    inviter_id = analysis_results['user_ids'].get(inviter, '')
                    if config.get('show_user_links', False):
                        user_link = f"tg://openmessage?user_id={inviter_id}" if inviter_id else ''
                        f.write(f"{rank}. {inviter} ({user_link}): {format_number(invite_count)} приглашений\n")
                    else:
                        f.write(f"{rank}. {inviter}: {format_number(invite_count)} приглашений\n")
                    rank +=1
                f.write("\n")

            # Title changes
            title_change_count = analysis_results['title_change_count']
            f.write(f"{current_texts['title_changes'].format(format_number(title_change_count))}\n\n")

        # Top words
        common_words = analysis_results['common_words']
        f.write(f"{config['emojis'].get('word', '')} {current_texts.get('top_words', 'Top words')}:\n")
        rank = 1
        for word, freq in common_words:
            f.write(f"{rank}. {word}: {format_number(freq)} {current_texts.get('times', 'times')}\n")
            rank += 1
        f.write("\n")

        # Top phrases
        common_phrases = analysis_results['common_phrases']
        f.write(f"{config['emojis'].get('phrase', '')} {current_texts.get('top_phrases', 'Top phrases')}:\n")
        rank = 1
        for phrase, freq in common_phrases:
            f.write(f"{rank}. {phrase}: {format_number(freq)} {current_texts.get('times', 'times')}\n")
            rank += 1
        f.write("\n")

        # Activity statistics
        f.write(f"{config['emojis'].get('activity', '')} {current_texts.get('activity', 'Activity')}:\n")
        activity = analysis_results['activity']
        # Top hours
        hours_list = ', '.join([f"{config['emojis'].get('list_item', '')} {hour}:00–{hour}:59" for hour, _ in activity['hours']])
        f.write(f"{hours_list}\n")
        # Top weekdays
        weekdays_list = ', '.join([f"{config['emojis'].get('list_item', '')} {weekday}" for weekday, _ in activity['weekdays']])
        f.write(f"{weekdays_list}\n")
        # Top months
        months_list = ', '.join([f"{config['emojis'].get('list_item', '')} {month.capitalize()}" for month, _ in activity['months']])
        f.write(f"{months_list}\n")
        # Top years
        years_list = ', '.join([f"{config['emojis'].get('list_item', '')} {year}" for year, _ in activity['years']])
        f.write(f"{years_list}\n\n")

        # Most active days
        f.write(f"{config['emojis'].get('activity', '')} {current_texts.get('most_active_days', 'Most active days')}:\n")
        rank = 1
        date_symbols = analysis_results['date_symbols']
        for date, msg_count in analysis_results['top_days']:
            symbol_count = date_symbols[date]
            avg_length = symbol_count / msg_count if msg_count else 0
            date_str = date.strftime('%d.%m.%Y')
            f.write(f"{rank}. {date_str}: ✉️ {format_number(msg_count)}, 🔣 {format_number(symbol_count)}, 💬 {avg_length:.1f}\n")
            rank += 1
        f.write("\n")

        # Add group creator info
        if analysis_results['creator_name'] and analysis_results['creator_id']:
            f.write(f"{current_texts['creator_info'].format(analysis_results['creator_name'], analysis_results['creator_id'])}\n")

        if config.get('show_author_links', True):
            f.write('\n\n')
            f.write('⚡️\n')
            f.write(current_texts['author_links'].format(author_github_link, author_telegram_channel))
            f.write('\n⚡️\n')

def generate_json_report(analysis_results, json_output_filename):
    """Generate the JSON report (currently empty)."""
    json_output_data = {
        # Include any data you want to output to the JSON file
    }
    with open(json_output_filename, 'w', encoding='utf-8') as jf:
        json.dump(json_output_data, jf, ensure_ascii=False, indent=4)

---
modules\visualization.py:

import matplotlib.pyplot as plt

def generate_personal_chat_plots(analysis_results, plot_filename_template, config, current_texts):
    """Generate communication dynamics plots per year for personal chats."""
    daily_user_messages = analysis_results['daily_user_messages']
    daily_first_sender = analysis_results['daily_first_sender']

    all_dates = sorted(daily_user_messages.keys())
    years = sorted(set(date.year for date in all_dates))

    participants = list(analysis_results['user_counts'].keys())
    if len(participants) != 2:
        # Handle unexpected number of participants
        return
    user1, user2 = participants

    chat_name = analysis_results.get('chat_name', 'Chat')

    for year in years:
        dates = [date for date in all_dates if date.year == year]
        user1_counts = [daily_user_messages[date].get(user1, 0) for date in dates]
        user2_counts = [daily_user_messages[date].get(user2, 0) for date in dates]

        # Prepare the plot
        plt.figure(figsize=(12, 6))
        # Plot with smooth lines and transparency
        plt.plot(dates, user1_counts, label=user1, linewidth=2, alpha=0.7)
        plt.plot(dates, user2_counts, label=user2, linewidth=2, alpha=0.7)

        # Adjust the y-axis limits to prevent overlap with annotations
        plt.ylim(bottom=0)

        # Indicate the first sender of each day
        max_count = max(user1_counts + user2_counts) if (user1_counts + user2_counts) else 1
        triangle_y = max_count * 1.05  # Position for triangles
        first_sender_dates = [date for date in dates if daily_first_sender.get(date)]
        sender_colors = ['blue' if daily_first_sender[date] == user1 else 'orange' for date in first_sender_dates]
        plt.scatter(first_sender_dates, [triangle_y]*len(first_sender_dates), color=sender_colors, marker='^', s=50, zorder=5)

        # Add annotations in the current language
        plt.xlabel(current_texts.get('date_label', 'Date'))
        plt.ylabel(current_texts.get('message_count_label', 'Number of Messages'))
        plt.title(f"{chat_name} - {current_texts.get('chat_activity_in_year', 'Chat Activity in {0}').format(year)}")

        # Add legend explaining the triangles
        triangle_label = current_texts.get('first_sender_triangle_label', 'First Sender After Interval')
        plt.scatter([], [], color='grey', marker='^', label=triangle_label)

        plt.legend()
        plt.tight_layout()
        # Save the plot with the year in the filename
        plot_filename = plot_filename_template.replace('<year>', str(year))
        plt.savefig(plot_filename)
        plt.close()

def generate_group_chat_plots(analysis_results, plot_filename_template, config, current_texts):
    """Generate communication dynamics plots per year for group chats."""
    date_messages = analysis_results['date_messages']

    all_dates = sorted(date_messages.keys())
    years = sorted(set(date.year for date in all_dates))

    # Collect all daily message counts to find the global maximum
    all_daily_counts = [date_messages[date] for date in all_dates]
    max_daily_messages = max(all_daily_counts) if all_daily_counts else 1

    chat_name = analysis_results.get('chat_name', 'Chat')

    for year in years:
        dates = [date for date in all_dates if date.year == year]
        daily_counts = [date_messages[date] for date in dates]

        # Prepare the plot
        plt.figure(figsize=(12, 6))
        plt.plot(dates, daily_counts, linewidth=2, alpha=0.7)

        # Set consistent y-axis limits across years
        plt.ylim(0, max_daily_messages * 1.1)  # Slightly higher for visual comfort

        # Add labels and title in the current language
        plt.xlabel(current_texts.get('date_label', 'Date'))
        plt.ylabel(current_texts.get('message_count_label', 'Number of Messages'))
        plt.title(f"{chat_name} - {current_texts.get('group_chat_activity_in_year', 'Group Chat Activity in {0}').format(year)}")

        plt.tight_layout()
        # Save the plot with the year in the filename
        plot_filename = plot_filename_template.replace('<year>', str(year))
        plt.savefig(plot_filename)
        plt.close()

---
words\profanity_words.txt:



---
words\stop_words_extended.txt:

и
в
не
на
с
что
а
как
это
по
но
из
у
за
о
же
то
к
для
до
вы
мы
они
он
она
оно
так
было
только
бы
когда
уже
ли
или
со
я
ты
ну
там
вот
от
тут
типа
этого
того
под

---
words\stop_words_minimal.txt:

и
в
не
на
с
что
а
как
это
по
но
из
у
за
о
же
то
к
для
до
вы
мы
они
он
она
оно
так
было
только
бы
когда
уже
ли
или
со