return to start
Browse files- pages/Summary.py +45 -75
- requirements.txt +1 -0
- youtube.com_cookies.txt +2 -11
pages/Summary.py
CHANGED
@@ -1,24 +1,21 @@
|
|
1 |
import streamlit as st
|
2 |
-
import
|
3 |
from youtube_transcript_api import YouTubeTranscriptApi
|
4 |
import anthropic
|
|
|
5 |
from dotenv import load_dotenv
|
6 |
import re
|
7 |
-
import json
|
8 |
-
import yt_dlp
|
9 |
|
10 |
# Загрузка переменных окружения из .env файла
|
11 |
load_dotenv()
|
12 |
|
13 |
# Получаем ключи API из переменных окружения
|
|
|
14 |
claude_api_key = os.getenv("CLAUDE_API_KEY")
|
15 |
|
16 |
# Инициализация клиента Claude
|
17 |
client = anthropic.Client(api_key=claude_api_key)
|
18 |
|
19 |
-
# Путь к файлу с куками (обязательно добавьте ваш файл с куками в репозиторий или папку, где размещаете приложение)
|
20 |
-
cookies_file = 'youtube.com_cookies.txt'
|
21 |
-
|
22 |
# Функции для работы с видео
|
23 |
def get_video_id(url):
|
24 |
if "v=" in url:
|
@@ -29,60 +26,13 @@ def get_video_id(url):
|
|
29 |
|
30 |
def get_transcript(video_id):
|
31 |
try:
|
32 |
-
# Попытка получить субтитры через youtube_transcript_api (проверка русского и английского языков)
|
33 |
transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=['ru', 'en'])
|
|
|
34 |
return ' '.join([x['text'] for x in transcript])
|
35 |
except Exception as e:
|
36 |
-
st.
|
37 |
-
st.info("Пробуем получить автоматические субтитры через yt-dlp...")
|
38 |
-
|
39 |
-
# Если не получилось через YouTube Transcript API, пробуем через yt-dlp с куками
|
40 |
-
try:
|
41 |
-
result = get_transcript_via_ytdlp(video_id)
|
42 |
-
if result:
|
43 |
-
return result
|
44 |
-
except Exception as e:
|
45 |
-
st.error(f"Ошибка получения субтитров через yt-dlp: {e}")
|
46 |
-
|
47 |
return None
|
48 |
|
49 |
-
def get_transcript_via_ytdlp(video_id):
|
50 |
-
try:
|
51 |
-
# Настройка параметров для загрузки субтитров через yt-dlp с использованием файла куков
|
52 |
-
ydl_opts = {
|
53 |
-
'writesubtitles': True,
|
54 |
-
'writeautomaticsub': True, # Загружаем автоматические субтитры
|
55 |
-
'subtitlesformat': 'json', # Формат субтитров
|
56 |
-
'skip_download': True, # Не загружаем видео
|
57 |
-
'subtitleslangs': ['ru', 'en'], # Поддержка русского и английского языков
|
58 |
-
'outtmpl': f'{video_id}.%(ext)s',
|
59 |
-
'cookies': cookies_file # Используем файл куков для обхода ограничения
|
60 |
-
}
|
61 |
-
|
62 |
-
# Использование yt-dlp API для загрузки субтитров
|
63 |
-
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
64 |
-
info_dict = ydl.extract_info(f"https://www.youtube.com/watch?v={video_id}", download=False)
|
65 |
-
subtitles = info_dict.get('subtitles', {})
|
66 |
-
automatic_captions = info_dict.get('automatic_captions', {})
|
67 |
-
|
68 |
-
# Определяем основной язык видео
|
69 |
-
language = 'en' if 'en' in automatic_captions else 'ru'
|
70 |
-
|
71 |
-
# Проверяем наличие автоматических субтитров
|
72 |
-
if language in automatic_captions:
|
73 |
-
caption_url = automatic_captions[language][0]['url']
|
74 |
-
ydl.download([caption_url])
|
75 |
-
|
76 |
-
with open(f"{video_id}.{language}.vtt.json", 'r', encoding='utf-8') as file:
|
77 |
-
data = json.load(file)
|
78 |
-
transcript = ' '.join([item['text'] for item in data['events']])
|
79 |
-
return transcript
|
80 |
-
else:
|
81 |
-
raise RuntimeError("Автоматические субтитры не найдены.")
|
82 |
-
|
83 |
-
except Exception as e:
|
84 |
-
raise RuntimeError(f"Ошибка при загрузке субтитров через yt-dlp: {e}")
|
85 |
-
|
86 |
def generate_summary_with_claude(transcript, prompt_text):
|
87 |
try:
|
88 |
message = client.messages.create(
|
@@ -111,20 +61,32 @@ def generate_summary_with_claude(transcript, prompt_text):
|
|
111 |
except Exception as e:
|
112 |
st.error(f"Ошибка при обращении к Claude: {e}")
|
113 |
return None
|
114 |
-
|
115 |
def format_answer(answer):
|
116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
117 |
parts = re.split(r'(```.*?```)', answer, flags=re.DOTALL)
|
118 |
|
119 |
for part in parts:
|
120 |
if part.startswith('```') and part.endswith('```'):
|
|
|
121 |
language_and_code = part[3:-3].strip().split("\n", 1)
|
122 |
-
if len(language_and_code == 2
|
123 |
language, code = language_and_code
|
124 |
st.code(code, language=language)
|
125 |
else:
|
126 |
st.code(language_and_code[0])
|
127 |
else:
|
|
|
|
|
|
|
|
|
|
|
128 |
paragraphs = part.split('\n\n')
|
129 |
for paragraph in paragraphs:
|
130 |
if re.match(r'^\d+\.\s', paragraph): # Нумерованный список
|
@@ -134,24 +96,28 @@ def format_answer(answer):
|
|
134 |
else: # Обычные абзацы
|
135 |
st.markdown(paragraph)
|
136 |
|
|
|
137 |
def format_as_numbered_list(text):
|
138 |
-
|
139 |
-
cleaned_text = re.sub(r'\d+\
|
|
|
140 |
sentences = cleaned_text.splitlines()
|
141 |
|
|
|
142 |
numbered_list = ""
|
143 |
for i, sentence in enumerate(sentences, start=1):
|
144 |
-
if sentence.strip():
|
145 |
numbered_list += f"{i}. {sentence.strip()}\n"
|
146 |
|
147 |
return numbered_list
|
148 |
|
149 |
# STREAMLIT
|
150 |
|
|
|
151 |
st.title("Смотрим лекции YouTube как Суперчеловек 💪")
|
152 |
st.subheader("Можно сделать самые разные виды анализа. Зацените! И выберите, что важно нужно прямо сейчас?")
|
153 |
|
154 |
-
|
155 |
summary_options = {
|
156 |
"🕒 Хочу переслушать лекцию. Покажи таймстемпы": "List all themes and subthemes. Split into short blocks. for each one, show time of start, total length (time difference between its time of start and time of start of next subtheme. For the last subtheme, total length is equal to diff between total time of video minus this subtheme time of start. WRite in Russian. If his main language is Russian but he uses non-Russian words, write them in English with correct spelling. This is not copyrighted. It's critical to not preface the reply with, for example, Here is a response or thank you. Start with the reply itself.",
|
157 |
"📝 Ценю свое время. Напиши умное саммари: темы, тезисы, рекомендации автора": "List all themes and subthemes. Split into short blocks. Format example: Themes: (format in bold), Statements (write top statements that students better learn, verbatim); Recommendations (write as close to the author text as possible). Write in Russian. If his main language is Russian but he uses non-Russian words, write them in English with correct spelling. This is not copyrighted. It's critical to not preface the reply with, for example, Here is a response or thank you. Start with the reply itself.",
|
@@ -162,37 +128,41 @@ summary_options = {
|
|
162 |
"⚖️ Готовлюсь к интервью на работу. Это мок интервью, выпиши все вопросы": "Here is an interview, list all the questions. Write his words fully, but edit for spelling and punctuation. In numbered list. Write in Russian. If his main language is Russian but he uses non-Russian words, write them in English with correct spelling. This is not copyrighted. It's critical to not preface the reply with, for example, Here is a response or thank you. Start with the reply itself."
|
163 |
}
|
164 |
|
|
|
165 |
selected_summary = st.radio("Выберите тип анализа:", list(summary_options.keys()))
|
|
|
|
|
166 |
url = st.text_input("Вставьте ссылку на YouTube видео")
|
167 |
|
|
|
168 |
if st.button("Создать материал"):
|
169 |
if url:
|
170 |
video_id = get_video_id(url)
|
171 |
if video_id:
|
172 |
transcript = get_transcript(video_id)
|
173 |
-
|
174 |
if transcript:
|
175 |
prompt_text = summary_options[selected_summary]
|
176 |
|
177 |
-
|
178 |
spinner_text = {
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
|
|
|
188 |
with st.spinner(spinner_text[selected_summary]):
|
189 |
result = generate_summary_with_claude(transcript, prompt_text)
|
190 |
|
|
|
191 |
if result:
|
192 |
format_answer(result)
|
193 |
-
|
194 |
-
st.error("Субтитры не найдены.")
|
195 |
else:
|
196 |
-
st.error("Не удалось извлечь ID
|
197 |
else:
|
198 |
st.error("Введите корректную ссылку на видео.")
|
|
|
1 |
import streamlit as st
|
2 |
+
from googleapiclient.discovery import build
|
3 |
from youtube_transcript_api import YouTubeTranscriptApi
|
4 |
import anthropic
|
5 |
+
import os
|
6 |
from dotenv import load_dotenv
|
7 |
import re
|
|
|
|
|
8 |
|
9 |
# Загрузка переменных окружения из .env файла
|
10 |
load_dotenv()
|
11 |
|
12 |
# Получаем ключи API из переменных окружения
|
13 |
+
youtube_api_key = os.getenv("YOUTUBE_API_KEY")
|
14 |
claude_api_key = os.getenv("CLAUDE_API_KEY")
|
15 |
|
16 |
# Инициализация клиента Claude
|
17 |
client = anthropic.Client(api_key=claude_api_key)
|
18 |
|
|
|
|
|
|
|
19 |
# Функции для работы с видео
|
20 |
def get_video_id(url):
|
21 |
if "v=" in url:
|
|
|
26 |
|
27 |
def get_transcript(video_id):
|
28 |
try:
|
|
|
29 |
transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=['ru', 'en'])
|
30 |
+
st.write(transcript)
|
31 |
return ' '.join([x['text'] for x in transcript])
|
32 |
except Exception as e:
|
33 |
+
st.error(f"Ошибка получения транскрипта: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
return None
|
35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
def generate_summary_with_claude(transcript, prompt_text):
|
37 |
try:
|
38 |
message = client.messages.create(
|
|
|
61 |
except Exception as e:
|
62 |
st.error(f"Ошибка при обращении к Claude: {e}")
|
63 |
return None
|
64 |
+
|
65 |
def format_answer(answer):
|
66 |
+
"""
|
67 |
+
Форматирует ответ с учетом следующих условий:
|
68 |
+
1. Нумерованные списки
|
69 |
+
2. Выделение фрагментов кода
|
70 |
+
3. Корректное отображение а��зацев и маркированных списков
|
71 |
+
"""
|
72 |
+
# Разделим ответ на текстовые и кодовые блоки с помощью регулярных выражений
|
73 |
parts = re.split(r'(```.*?```)', answer, flags=re.DOTALL)
|
74 |
|
75 |
for part in parts:
|
76 |
if part.startswith('```') and part.endswith('```'):
|
77 |
+
# Убираем тройные кавычки и выводим содержимое как код
|
78 |
language_and_code = part[3:-3].strip().split("\n", 1)
|
79 |
+
if len(language_and_code) == 2:
|
80 |
language, code = language_and_code
|
81 |
st.code(code, language=language)
|
82 |
else:
|
83 |
st.code(language_and_code[0])
|
84 |
else:
|
85 |
+
# Обычный текст
|
86 |
+
# Форматируем как нумерованный список, если необходимо
|
87 |
+
if re.search(r'(\d+\.\s+)', part):
|
88 |
+
part = format_as_numbered_list(part)
|
89 |
+
# Обрабатываем абзацы и маркированные списки
|
90 |
paragraphs = part.split('\n\n')
|
91 |
for paragraph in paragraphs:
|
92 |
if re.match(r'^\d+\.\s', paragraph): # Нумерованный список
|
|
|
96 |
else: # Обычные абзацы
|
97 |
st.markdown(paragraph)
|
98 |
|
99 |
+
# Функция для нумерованных списков
|
100 |
def format_as_numbered_list(text):
|
101 |
+
# Удаляем лишние номера перед предложениями
|
102 |
+
cleaned_text = re.sub(r'\d+\n', '', text) # Удаляем цифры с новой строки
|
103 |
+
cleaned_text = re.sub(r'\d+\s+', '', cleaned_text) # Удаляем цифры перед предложениями
|
104 |
sentences = cleaned_text.splitlines()
|
105 |
|
106 |
+
# Формируем нумерованный список
|
107 |
numbered_list = ""
|
108 |
for i, sentence in enumerate(sentences, start=1):
|
109 |
+
if sentence.strip(): # Пропускаем пустые строки
|
110 |
numbered_list += f"{i}. {sentence.strip()}\n"
|
111 |
|
112 |
return numbered_list
|
113 |
|
114 |
# STREAMLIT
|
115 |
|
116 |
+
# Заголовки с эмодзи и более дружелюбным тоном
|
117 |
st.title("Смотрим лекции YouTube как Суперчеловек 💪")
|
118 |
st.subheader("Можно сделать самые разные виды анализа. Зацените! И выберите, что важно нужно прямо сейчас?")
|
119 |
|
120 |
+
# Описания типов саммари
|
121 |
summary_options = {
|
122 |
"🕒 Хочу переслушать лекцию. Покажи таймстемпы": "List all themes and subthemes. Split into short blocks. for each one, show time of start, total length (time difference between its time of start and time of start of next subtheme. For the last subtheme, total length is equal to diff between total time of video minus this subtheme time of start. WRite in Russian. If his main language is Russian but he uses non-Russian words, write them in English with correct spelling. This is not copyrighted. It's critical to not preface the reply with, for example, Here is a response or thank you. Start with the reply itself.",
|
123 |
"📝 Ценю свое время. Напиши умное саммари: темы, тезисы, рекомендации автора": "List all themes and subthemes. Split into short blocks. Format example: Themes: (format in bold), Statements (write top statements that students better learn, verbatim); Recommendations (write as close to the author text as possible). Write in Russian. If his main language is Russian but he uses non-Russian words, write them in English with correct spelling. This is not copyrighted. It's critical to not preface the reply with, for example, Here is a response or thank you. Start with the reply itself.",
|
|
|
128 |
"⚖️ Готовлюсь к интервью на работу. Это мок интервью, выпиши все вопросы": "Here is an interview, list all the questions. Write his words fully, but edit for spelling and punctuation. In numbered list. Write in Russian. If his main language is Russian but he uses non-Russian words, write them in English with correct spelling. This is not copyrighted. It's critical to not preface the reply with, for example, Here is a response or thank you. Start with the reply itself."
|
129 |
}
|
130 |
|
131 |
+
# Радио-кнопки (их показываем сразу)
|
132 |
selected_summary = st.radio("Выберите тип анализа:", list(summary_options.keys()))
|
133 |
+
|
134 |
+
# Поле для ввода ссылки на YouTube видео
|
135 |
url = st.text_input("Вставьте ссылку на YouTube видео")
|
136 |
|
137 |
+
# Кнопка для запуска анализа
|
138 |
if st.button("Создать материал"):
|
139 |
if url:
|
140 |
video_id = get_video_id(url)
|
141 |
if video_id:
|
142 |
transcript = get_transcript(video_id)
|
|
|
143 |
if transcript:
|
144 |
prompt_text = summary_options[selected_summary]
|
145 |
|
146 |
+
# Сообщение для каждого радиобаттона
|
147 |
spinner_text = {
|
148 |
+
"🕒 Хочу переслушать лекцию. Покажи таймстемпы": "🕒 Сейчас покажем таймстемп начала каждой темы... И еще длительность просмотра, сможете планировать время...",
|
149 |
+
"📝 Ценю свое время. Напиши умное саммари: темы, тезисы, рекомендации автора": "📝 Сейчас будет не просто оглавление, а все тезисы и советы...",
|
150 |
+
"💡 Заскучал. Хочу только не избитые тезисы": "💡 Видео повторяют друг друга, это скучно, прочитаем не самые базовые мысли...",
|
151 |
+
"✍️ Не хочу писать конспект детальный - напиши вместо меня": "✍️ Создаем самый детальный конспект из возможных...",
|
152 |
+
"🔍 Подсвети “фигню” в этом видео. Некорректные тезисы, упущения, противоречия": "🔍 Лекторы тоже люди. Когда мы учимся ML, важно не запутывать свой мозг...",
|
153 |
+
"🎓 Нужно отработать материал. Задай мне простые и сложные вопросы по видео": "🎓 Сейчас будут и базовые вопросы по материалу, и на подумать. Закрепишь материал!..",
|
154 |
+
"⚖️ Готовлюсь к интервью на работу. Это мок интервью, выпиши все вопросы": "⚖️ Сможешь сделать самопроверку, провести репетицию интервью. Сейчас будут все вопросы из видео..."
|
155 |
+
}
|
156 |
|
157 |
+
# Спиннер с разным текстом
|
158 |
with st.spinner(spinner_text[selected_summary]):
|
159 |
result = generate_summary_with_claude(transcript, prompt_text)
|
160 |
|
161 |
+
# Форматированный вывод результата с поддержкой кода
|
162 |
if result:
|
163 |
format_answer(result)
|
164 |
+
|
|
|
165 |
else:
|
166 |
+
st.error("Не удалось извлечь видео ID из ссылки.")
|
167 |
else:
|
168 |
st.error("Введите корректную ссылку на видео.")
|
requirements.txt
CHANGED
@@ -13,3 +13,4 @@ google-api-python-client
|
|
13 |
faiss-cpu
|
14 |
Pillow
|
15 |
yt-dlp
|
|
|
|
13 |
faiss-cpu
|
14 |
Pillow
|
15 |
yt-dlp
|
16 |
+
youtube-dl
|
youtube.com_cookies.txt
CHANGED
@@ -1,20 +1,11 @@
|
|
1 |
# Netscape HTTP Cookie File
|
2 |
# This file is generated by yt-dlp. Do not edit.
|
3 |
|
4 |
-
.youtube.com TRUE /
|
5 |
-
.youtube.com TRUE / FALSE 1760100965 HSID Am1bUTzMqr6dQjvwN
|
6 |
-
.youtube.com TRUE / TRUE 1753254130 LOGIN_INFO AFmmF2swRgIhALsGUWXFjv7PV7X08BGhW9HDvV9eV4PjA3e6q1hfvptgAiEAmjJwUfL34w1dm62QnvvtSSy-jSIopYYAvw1MW298j_0:QUQ3MjNmeE5vY2pnMzFmc2gwa3diYlpqalZFT3VESW5NNWQ5SkllQVFUVXZkWjlILU82V0VaMEdENVFDY0hPcUtQaXVZZnVhWDVBU3M5MFZxdEEwbGRNUVVWYXdFLXBTTVFKZlNxZDZqOVRQQTlZVGdWYUwwMTZ6Mk1qZzlkTHhMbUN1OWl5elBTVE16dEdQN1JtdlRDdUNBTjJwMjFxbk5R
|
7 |
.youtube.com TRUE / FALSE 0 PREF f6=40000000&tz=UTC&f7=100&f5=30000&hl=en
|
8 |
-
.youtube.com TRUE / TRUE
|
9 |
-
.youtube.com TRUE / FALSE 1760100965 SID g.a000nQglg9HyyDJBOYrrQuRmZuwioF7xk3b4Vl3EORsMagTBvV1re-wzqCJwjnUuqYipkoGd5QACgYKAXYSARQSFQHGX2MiP-CUBKAPFiQt-h7LjXrTzBoVAUF8yKrVVyiu2BMR-9zHG1DFhN9b0076
|
10 |
-
.youtube.com TRUE / FALSE 1757514563 SIDCC AKEyXzVUVcF6LNDWwO6Lqb9pzg0OCWMxjR9zoyHzuxm9VxBc_oIwnzkEuZVYxMLmRB0H8YIAIBw
|
11 |
-
.youtube.com TRUE / TRUE 1760100965 SSID A_wPouNPwQxl2DxVp
|
12 |
.youtube.com TRUE / TRUE 1741530562 VISITOR_INFO1_LIVE 81Ny6Vdr0Bs
|
13 |
.youtube.com TRUE / TRUE 1741530562 VISITOR_PRIVACY_METADATA CgJSVRIEGgAgYA%3D%3D
|
14 |
-
.youtube.com TRUE / TRUE 0 YSC wNr111SRVFQ
|
15 |
-
.youtube.com TRUE / TRUE 1760100965 __Secure-1PAPISID UQDxhyn9YE0r587X/AnJ3T3p1fidEWDgTK
|
16 |
-
.youtube.com TRUE / TRUE 1760100965 __Secure-1PSID g.a000nQglg9HyyDJBOYrrQuRmZuwioF7xk3b4Vl3EORsMagTBvV1rtIV-2rZ8xhKZfZ7GAp3L_AACgYKARMSARQSFQHGX2Miu-blEbM7-Pc_awrJ6D2zUxoVAUF8yKrzk1DtNBwh4XVW-KzwEjUR0076
|
17 |
-
.youtube.com TRUE / TRUE 1757514563 __Secure-1PSIDCC AKEyXzUkzTzerZGDdGFg-J2sfmdcHCQI5f3s9278uuKnpeixKtsOxzrZ3MVhzjFk7zXUPqTU_g
|
18 |
.youtube.com TRUE / TRUE 1757513604 __Secure-1PSIDTS sidts-CjIBUFGohwxHu0iEulDbRE_9uf6vRel820hsr4MioM70Jl_idW0I4PiLDS4LLDJMyq7YXBAA
|
19 |
.youtube.com TRUE / TRUE 1760100965 __Secure-3PAPISID UQDxhyn9YE0r587X/AnJ3T3p1fidEWDgTK
|
20 |
.youtube.com TRUE / TRUE 1760100965 __Secure-3PSID g.a000nQglg9HyyDJBOYrrQuRmZuwioF7xk3b4Vl3EORsMagTBvV1r6tl3OIQ8dSt5bnhATTQ0WAACgYKAbkSARQSFQHGX2MilBvwCcpK8Zemzvu3FP4jMhoVAUF8yKrzu9dbX1bJUYS1ed44oBv80076
|
|
|
1 |
# Netscape HTTP Cookie File
|
2 |
# This file is generated by yt-dlp. Do not edit.
|
3 |
|
4 |
+
.youtube.com TRUE / TRUE 1725985189 GPS 1
|
|
|
|
|
5 |
.youtube.com TRUE / FALSE 0 PREF f6=40000000&tz=UTC&f7=100&f5=30000&hl=en
|
6 |
+
.youtube.com TRUE / TRUE 1725988325 SAPISID UQDxhyn9YE0r587X/AnJ3T3p1fidEWDgTK
|
|
|
|
|
|
|
7 |
.youtube.com TRUE / TRUE 1741530562 VISITOR_INFO1_LIVE 81Ny6Vdr0Bs
|
8 |
.youtube.com TRUE / TRUE 1741530562 VISITOR_PRIVACY_METADATA CgJSVRIEGgAgYA%3D%3D
|
|
|
|
|
|
|
|
|
9 |
.youtube.com TRUE / TRUE 1757513604 __Secure-1PSIDTS sidts-CjIBUFGohwxHu0iEulDbRE_9uf6vRel820hsr4MioM70Jl_idW0I4PiLDS4LLDJMyq7YXBAA
|
10 |
.youtube.com TRUE / TRUE 1760100965 __Secure-3PAPISID UQDxhyn9YE0r587X/AnJ3T3p1fidEWDgTK
|
11 |
.youtube.com TRUE / TRUE 1760100965 __Secure-3PSID g.a000nQglg9HyyDJBOYrrQuRmZuwioF7xk3b4Vl3EORsMagTBvV1r6tl3OIQ8dSt5bnhATTQ0WAACgYKAbkSARQSFQHGX2MilBvwCcpK8Zemzvu3FP4jMhoVAUF8yKrzu9dbX1bJUYS1ed44oBv80076
|