/
Sm1rn0
/
Monitoring_bot
Обзор
Документация
Войти
/
Sm1rn0
/
Monitoring_bot
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
bot.py
363 строки
15 KB
Sm1rn0
upload files
12 дек 2025, 16:35
12 дек 2025, 16:35
24e48c9
Код
Авторство
О чём код?
import base64 import os import asyncio import logging import io import json import pickle from datetime import datetime import aiohttp from PIL import Image from aiogram import Bot, Dispatcher, F from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardRemove from aiogram.filters import Command from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from google.auth.transport.requests import Request from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.discovery import build from googleapiclient.http import MediaIoBaseUpload # ===================== Настройки ===================== BOT_TOKEN = "8339440609:AAFjAJ0QcyYEmDAnXlUbuGpTrLR6MjwYcaE" YANDEX_GEOCODER_API_KEY = "175aa64a-b48f-457a-97df-084858348a3e" SCOPES = ['https://www.googleapis.com/auth/drive.file', 'https://www.googleapis.com/auth/spreadsheets'] SPREADSHEET_ID = "1ASjmkmwmCEhUM4RNnTfvU8N6j-2QODcZef3zwWRXZYc" MAIN_FOLDER_ID = "15IrfvCu1gaXNHdcJaekg-NGClOO_oViW" TOKEN_FILE = 'token.pickle' CREDENTIALS_FILE = 'credentials.json' logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) bot = Bot(token=BOT_TOKEN) dp = Dispatcher() # ===================== FSM ===================== class AddressState(StatesGroup): waiting_for_address = State() waiting_for_photo = State() user_data = {} # {user_id: {"address": str, "city": str, "street": str, "lat": float, "lon": float}} temp_results = {} # {message_id: список адресов} # ===================== Google API ===================== def get_drive_service(): creds = None if os.path.exists(TOKEN_FILE): with open(TOKEN_FILE, 'rb') as f: creds = pickle.load(f) if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) else: flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_FILE, SCOPES) creds = flow.run_local_server(port=0, success_message='Авторизация прошла! Можно закрыть окно.') with open(TOKEN_FILE, 'wb') as f: pickle.dump(creds, f) drive_service = build('drive', 'v3', credentials=creds) sheets_service = build('sheets', 'v4', credentials=creds) print("Google API подключён через OAuth ✅") return drive_service, sheets_service drive_service, sheets_service = get_drive_service() def find_file_in_folder(name: str, parent_id: str): safe_name = name.replace("'", "\\'") q = f"'{parent_id}' in parents and name = '{safe_name}' and trashed = false" res = drive_service.files().list(q=q, fields="files(id,name,mimeType)").execute() files = res.get("files", []) return files[0] if files else None def create_folder(name: str, parent_id: str): body = {"name": name, "mimeType": "application/vnd.google-apps.folder", "parents": [parent_id]} return drive_service.files().create(body=body, fields="id").execute()["id"] def get_or_create_folder(city: str, street: str, date_str: str): # Город city_folder = find_file_in_folder(city, MAIN_FOLDER_ID) if not city_folder: city_folder_id = create_folder(city, MAIN_FOLDER_ID) else: city_folder_id = city_folder["id"] # Адрес street_folder = find_file_in_folder(street, city_folder_id) if not street_folder: street_folder_id = create_folder(street, city_folder_id) else: street_folder_id = street_folder["id"] # Дата date_folder = find_file_in_folder(date_str, street_folder_id) if not date_folder: date_folder_id = create_folder(date_str, street_folder_id) else: date_folder_id = date_folder["id"] return date_folder_id def upload_bytesio_to_drive(bio: io.BytesIO, filename: str, folder_id: str) -> str: bio.seek(0) media = MediaIoBaseUpload(bio, mimetype="image/jpeg", resumable=True) file = drive_service.files().create( body={"name": filename, "parents": [folder_id]}, media_body=media, fields="id" ).execute() file_id = file["id"] drive_service.permissions().create(fileId=file_id, body={"type": "anyone", "role": "reader"}).execute() return f"https://drive.google.com/file/d/{file_id}/view" def ensure_sheet_headers(): sheet = sheets_service.spreadsheets() result = sheet.values().get(spreadsheetId=SPREADSHEET_ID, range="monitoring_data!A1:I1").execute() values = result.get("values", []) if not values: headers = [[ "Адрес", "Геоточка", "Время", "Контейнеров всего", "Переполнено", "Мусор рядом", "Мусор на земле", "Комментарий", "Ссылка на фото" ]] sheet.values().update( spreadsheetId=SPREADSHEET_ID, range="monitoring_data!A1:I1", valueInputOption="RAW", body={"values": headers} ).execute() def append_to_sheet(record: dict): ensure_sheet_headers() values = [[ record.get("Адрес", ""), record.get("Геоточка", ""), record.get("Время", ""), record.get("Контейнеров всего", ""), record.get("Переполнено", ""), record.get("Мусор рядом", ""), record.get("Мусор на земле", ""), record.get("Комментарий", ""), record.get("Ссылка на фото", "") ]] body = {"values": values} sheets_service.spreadsheets().values().append( spreadsheetId=SPREADSHEET_ID, range="monitoring_data!A:I", valueInputOption="RAW", insertDataOption="INSERT_ROWS", body=body ).execute() # ===================== Яндекс Геокодер ===================== async def geocode_address(address: str): url = f"https://geocode-maps.yandex.ru/1.x/?apikey={YANDEX_GEOCODER_API_KEY}&geocode={address}&format=json&results=5" async with aiohttp.ClientSession() as session: async with session.get(url) as resp: if resp.status != 200: return [] data = await resp.json() members = data["response"]["GeoObjectCollection"]["featureMember"] results = [] for item in members: geo = item["GeoObject"] name = geo["name"] desc = geo.get("description", "") full = f"{desc}, {name}" if desc else name coords = geo["Point"]["pos"].split() results.append({ "address": full.strip(), "lat": float(coords[1]), "lon": float(coords[0]), "city": desc.split(",")[0] if desc else "", "street": name }) return results return [] # ===================== GPT-4o Vision ===================== class gpt4o: def __init__(self): self.api_key = "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" async def analyze_image_from_bytes(self, image_bytes: bytes) -> dict: try: with Image.open(io.BytesIO(image_bytes)) as img: img = img.convert("RGB") img.thumbnail((1200, 1200)) buf = io.BytesIO() img.save(buf, format="JPEG", quality=85) base64_img = base64.b64encode(buf.getvalue()).decode() prompt = """ Ты — эксперт по анализу фото мусорных площадок. Ответь строго JSON: { "containers_total": int, "containers_overfilled": int, "trash_outside_containers": true/false, "trash_on_ground": true/false, "general_comment": "короткий комментарий на русском" } """ payload = { "model": "gpt-4o-mini", "messages": [{"role": "user", "content": [ {"type": "text", "text": prompt}, {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_img}"}}]}], "temperature": 0.2 } async with aiohttp.ClientSession() as session: async with session.post("https://api.openai.com/v1/chat/completions", headers={"Authorization": f"Bearer {self.api_key}"}, json=payload, timeout=60) as resp: if resp.status == 200: data = await resp.json() text = data["choices"][0]["message"]["content"] cleaned = text.strip().removeprefix("```json").removesuffix("```").strip() return json.loads(cleaned) except Exception as e: logger.error(f"GPT error: {e}") return {"containers_total": 0, "containers_overfilled": 0, "trash_outside_containers": False, "trash_on_ground": False, "general_comment": "Не удалось проанализировать"} vlm = gpt4o() # ===================== Клавиатура ===================== def get_after_keyboard(): return InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Сменить адрес", callback_data="change_address"), InlineKeyboardButton(text="Ещё фото", callback_data="send_more")], [InlineKeyboardButton(text="Главное меню", callback_data="main_menu")] ]) # ===================== Хендлеры ===================== @dp.message(Command("start")) async def start(message: Message, state: FSMContext): await message.answer( "Привет! Я бот для мониторинга мусорных площадок.\n\n" "Введите адрес площадки (например: Ленина 15):", reply_markup=ReplyKeyboardRemove() ) await state.set_state(AddressState.waiting_for_address) @dp.message(AddressState.waiting_for_address) async def process_address(message: Message, state: FSMContext): address = message.text.strip() await message.answer("Ищу адрес на карте...") results = await geocode_address(address) if not results: await message.answer("Адрес не найден. Попробуйте ещё раз:") return kb = InlineKeyboardMarkup(inline_keyboard=[]) for i, r in enumerate(results): kb.inline_keyboard.append([InlineKeyboardButton(text=r["address"][:100], callback_data=f"addr_{i}")]) sent = await message.answer("Выберите точный адрес:", reply_markup=kb) temp_results[sent.message_id] = results @dp.callback_query(F.data.startswith("addr_")) async def address_selected(cb: CallbackQuery, state: FSMContext): idx = int(cb.data.split("_")[1]) results = temp_results.get(cb.message.message_id) if not results or idx >= len(results): await cb.answer("Ошибка", show_alert=True) return r = results[idx] user_data[cb.from_user.id] = { "address": r["address"], "lat": r["lat"], "lon": r["lon"], "city": r["city"], "street": r["street"] } await cb.message.edit_text( f"Адрес зафиксирован:\n{r['address']}\n\nОтправьте фото площадки:", reply_markup=get_after_keyboard() ) await state.set_state(AddressState.waiting_for_photo) await cb.answer() @dp.callback_query(F.data == "change_address") async def change_address(cb: CallbackQuery, state: FSMContext): await cb.message.edit_text("Введите новый адрес:") await state.set_state(AddressState.waiting_for_address) await cb.answer() @dp.callback_query(F.data == "send_more") async def send_more(cb: CallbackQuery, state: FSMContext): await cb.message.edit_text("Отправьте ещё фото для этого адреса:") await state.set_state(AddressState.waiting_for_photo) await cb.answer() @dp.callback_query(F.data == "main_menu") async def main_menu(cb: CallbackQuery, state: FSMContext): await start(cb.message, state) await cb.answer() @dp.message(F.photo, AddressState.waiting_for_photo) async def handle_photo(message: Message, state: FSMContext): user_id = message.from_user.id if user_id not in user_data: await message.answer("Сначала выберите адрес через /start") return data = user_data[user_id] await message.answer("Анализирую фото...") photo = message.photo[-1] file = await bot.get_file(photo.file_id) img_bytes = io.BytesIO() await bot.download_file(file.file_path, img_bytes) img_bytes.seek(0) # Анализируем через GPT result = await vlm.analyze_image_from_bytes(img_bytes.getvalue()) # Если контейнеров нет, не сохраняем if result.get("containers_total", 0) == 0: await message.answer("На фото не обнаружены контейнеры, фото не сохраняется.") return # Сохраняем в Google Drive date_str = datetime.now().strftime("%Y-%m-%d") folder_id = get_or_create_folder(data["city"], data["street"], date_str) filename = f"{datetime.now().strftime('%H-%M')}.jpg" img_bytes.seek(0) drive_link = await asyncio.to_thread(upload_bytesio_to_drive, img_bytes, filename, folder_id) # Записываем в т аблицу record = { "Адрес": data["address"], "Геоточка": f"[{data['lat']},{data['lon']}]", "Время": datetime.now().strftime("%Y-%m-%d %H:%M"), "Контейнеров всего": result.get("containers_total", 0), "Переполнено": result.get("containers_overfilled", 0), "Мусор рядом": "Да" if result.get("trash_outside_containers") else "Нет", "Мусор на земле": "Да" if result.get("trash_on_ground") else "Нет", "Комментарий": result.get("general_comment", ""), "Ссылка на фото": drive_link } await asyncio.to_thread(append_to_sheet, record) # Ответ пользователю без ссылки msg_text = ( f"Фото обработано!\n{data['address']}\n\n" f"Контейнеров: {record['Контейнеров всего']}\n" f"Переполнено: {record['Переполнено']}\n" f"Мусор рядом: {record['Мусор рядом']}\n" f"Мусор на земле: {record['Мусор на земле']}\n\n" f"{record['Комментарий']}" ) await message.answer(msg_text, reply_markup=get_after_keyboard()) # ===================== Запуск ===================== async def main(): print("Бот запущен — готов к работе!") await dp.start_polling(bot) if __name__ == "__main__": asyncio.run(main())