206 lines
10 KiB
Python
206 lines
10 KiB
Python
#!/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])
|