#!/usr/bin/env python3 """ Сервис интеграции с Google NotebookLM и генерации аудио-подкастов (Audio Overviews) для проекта «Домовой 2.0: Юридический Ядерный Щит МКД». Возможности: 1. Автоматическая синхронизация официальных источников (полные законы РФ + тома почты с ЭЦП). 2. Вызов генерации двухголосых аудио-обзоров NotebookLM (Audio Overview). 3. Скачивание аудио-подкастов (.m4a/.mp3) в локальное хранилище. 4. Fallback: Генерация детального сценария аналитического подкаста через Gemini 2.5 Pro. """ import os import sys import json import logging import subprocess from pathlib import Path from typing import Dict, Any, Optional, List logger = logging.getLogger("notebooklm_podcast") logger.setLevel(logging.INFO) NOTEBOOKLM_BIN = "/home/matrixhasyou/swarm-services/domovoy-bot/venv-ai/bin/notebooklm" PODCASTS_DIR = Path("/home/matrixhasyou/swarm-services/domovoy-bot/podcasts") LEGAL_SOURCES_DIR = Path("/home/matrixhasyou/mutt/legal_sources") MEGA_VOLUMES_DIR = Path("/home/matrixhasyou/mutt/mega_volumes") DEFAULT_NOTEBOOK_NAME = "МКД Ливанова 37 (Суды, Прокуратура, ЖКХ)" class NotebookLMPodcastGenerator: def __init__(self, notebook_name: str = DEFAULT_NOTEBOOK_NAME): self.notebook_name = notebook_name self.cli_bin = NOTEBOOKLM_BIN PODCASTS_DIR.mkdir(parents=True, exist_ok=True) def is_authenticated(self) -> bool: """Проверяет, авторизован ли CLI NotebookLM.""" storage_file = Path(os.path.expanduser("~/.notebooklm/profiles/default/storage_state.json")) if not storage_file.exists() and not os.getenv("NOTEBOOKLM_AUTH_JSON"): return False try: res = subprocess.run([self.cli_bin, "list"], capture_output=True, text=True, timeout=10) return res.returncode == 0 and "Not logged in" not in res.stderr and "Not logged in" not in res.stdout except Exception: return False def list_notebooks(self) -> List[Dict[str, str]]: """Возвращает список блокнотов из NotebookLM.""" if not self.is_authenticated(): return [] try: res = subprocess.run([self.cli_bin, "list", "--json"], capture_output=True, text=True, timeout=15) if res.returncode == 0: data = json.loads(res.stdout) return data if isinstance(data, list) else [] except Exception as e: logger.error(f"Ошибка получения списка блокнотов: {e}") return [] def get_or_create_notebook(self) -> Optional[str]: """Находит существующий блокнот дома или создает новый.""" notebooks = self.list_notebooks() for nb in notebooks: if self.notebook_name.lower() in nb.get("title", "").lower(): return nb.get("id") # Создаем новый try: res = subprocess.run([self.cli_bin, "create", self.notebook_name], capture_output=True, text=True, timeout=20) if res.returncode == 0: # Находим созданный for nb in self.list_notebooks(): if self.notebook_name.lower() in nb.get("title", "").lower(): return nb.get("id") except Exception as e: logger.error(f"Ошибка создания блокнота: {e}") return None def upload_source_file(self, file_path: str, notebook_id: Optional[str] = None) -> bool: """Загружает PDF-файл (закон или том почты) в NotebookLM.""" if not os.path.exists(file_path): logger.error(f"Файл не найден: {file_path}") return False args = [self.cli_bin, "source", "add", file_path] if notebook_id: args.extend(["--notebook", notebook_id]) try: res = subprocess.run(args, capture_output=True, text=True, timeout=180) return res.returncode == 0 except Exception as e: logger.error(f"Ошибка загрузки источника {file_path}: {e}") return False def sync_all_sources(self) -> Dict[str, Any]: """Синхронизирует все полные законы и тома мега-PDF с NotebookLM.""" if not self.is_authenticated(): return { "success": False, "error": "NotebookLM CLI не авторизован. Требуется импорт профиля в ~/.notebooklm/profiles/default/" } nb_id = self.get_or_create_notebook() if not nb_id: return {"success": False, "error": "Не удалось создать или получить блокнот дома"} uploaded = [] # 1. Законы РФ if LEGAL_SOURCES_DIR.exists(): for pdf in sorted(LEGAL_SOURCES_DIR.glob("*Полный*.pdf")): if self.upload_source_file(str(pdf), nb_id): uploaded.append(pdf.name) # 2. Мега-тома писем if MEGA_VOLUMES_DIR.exists(): for pdf in sorted(MEGA_VOLUMES_DIR.glob("*.pdf")): if self.upload_source_file(str(pdf), nb_id): uploaded.append(pdf.name) return { "success": True, "notebook_id": nb_id, "uploaded_sources": uploaded } def generate_audio_overview(self, notebook_id: Optional[str] = None) -> Dict[str, Any]: """Запускает генерацию двухголосного подкаста в NotebookLM.""" if not self.is_authenticated(): return { "success": False, "error": "NotebookLM не авторизован. Доступен fallback-режим генерации через Gemini 2.5." } args = [self.cli_bin, "generate", "audio"] if notebook_id: args.extend(["--notebook", notebook_id]) try: res = subprocess.run(args, capture_output=True, text=True, timeout=300) if res.returncode == 0: return {"success": True, "output": res.stdout.strip()} return {"success": False, "error": res.stderr.strip() or res.stdout.strip()} except Exception as e: return {"success": False, "error": str(e)} def download_latest_audio(self, output_filename: str = "latest_podcast.m4a") -> Optional[str]: """Скачивает готовый сгенерированный подкаст.""" dest = PODCASTS_DIR / output_filename args = [self.cli_bin, "download", "audio", "--output", str(dest)] try: res = subprocess.run(args, capture_output=True, text=True, timeout=120) if res.returncode == 0 and dest.exists(): return str(dest) except Exception as e: logger.error(f"Ошибка скачивания аудио: {e}") return None def generate_fallback_dialogue_script(self, context_summary: str) -> Dict[str, Any]: """ Fallback генератор: при отсутствии сессии NotebookLM формирует полноценный драматический сценарий двухголосного подкаста через Gemini 2.5 Pro. """ from google import genai from google.genai import types api_key = os.getenv("GOOGLE_API_KEY", "AIzaSyAJMfQ_mRZ1picRXVFNaeFJ7fpAkZxUKyk") client = genai.Client(api_key=api_key) prompt = f"""Ты — ведущий сценарист и главный аналитик юридического аудио-подкаста дома «Ливанова 37: Голос Закона». Твоя задача — составить живой, увлекательный, юридически безупречный сценарий подкаста на 7-10 минут (в формате диалога двух экспертов): Ведущие: 1. Александр (Ведущий / Председатель Совета Дома) — задает острые вопросы жителей, приводит факты из писем госорганов, вскрывает уловки УК и прокуратуры. 2. Майя (Юрист-аналитик по жилищному праву) — профессионально, с юмором и железобетонными ссылками на законы (ЖК РФ, 59-ФЗ, ПП 354, Госстрой 170) объясняет, в чем неправы чиновники и как жителям защитить свои права. Контекст переписки и правовой базы дома: {context_summary} Формат ответа: Заголовок подкаста. Текст реплик с пометками [Александр] и [Майя]. В конце — четкие выводы и 3 практических шага для жителей дома. """ response = client.models.generate_content( model="gemini-2.5-pro", contents=prompt, config=types.GenerateContentConfig( temperature=0.4 ) ) return { "success": True, "script": response.text, "engine": "gemini-2.5-pro-fallback" } if __name__ == "__main__": generator = NotebookLMPodcastGenerator() print("NotebookLM Auth:", generator.is_authenticated()) if not generator.is_authenticated(): print("Running fallback test...") res = generator.generate_fallback_dialogue_script( "Судебные тяжбы с УК по перерасчету отопления, игнорирование прокуратурой Заволжского района сроков 59-ФЗ." ) print("Generated script snippet:\n", res["script"][:400])