diff --git a/.env b/.env new file mode 100644 index 0000000..222483e --- /dev/null +++ b/.env @@ -0,0 +1,15 @@ + +TEAMS_TENANT_ID = "40ad34a2-4df4-499f-a628-c865a29a7782" +TEAMS_CLIENT_ID = "4aa51599-98de-4a0a-8006-6939d24a18f4" +TEAMS_CLIENT_SECRET = "T5l8Q~_mmVp_.qHDlRkUKenBCELcceLYPFnDUcGF" +TEAMS_REDIRECT_URI = "https://login.microsoftonline.com/common/oauth2/nativeclient" + +ZOOM_CLIENT_ID="FA1BsrIaTHyB5zmz2hukmg" +ZOOM_CLIENT_SECRET="dpQeeLxZBAqFFaEpevt2WmAv1J81jtmJ" +ZOOM_REDIRECT_URI="http://localhost:5000/zoom/callback" + +# dane do maila +SMTP_SERVER=smtp.gmail.com +SMTP_PORT=587 +EMAIL_USERNAME=jfurtak03@gmail.com +EMAIL_PASSWORD=blmr hpes ktzy wnti \ No newline at end of file diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..26b38dc --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +note.py \ No newline at end of file diff --git a/README.md b/README.md index bb2b8f1..9360d98 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,69 @@ -# Aplikacja do Sporządzania Notatek ze Spotkań - -Aplikacja umożliwia nagrywanie spotkań, tworzenie z nich notatek oraz informowanie nieobecnych uczestników o ich przebiegu. - -## Wymagania - -Przed uruchomieniem aplikacji należy zainstalować wymagane zależności oraz skonfigurować środowisko. Szczegóły poniżej. - -### Instalacja zależności - -1. **Z katalogu projekt_2025/apka i projekt_2025/apka/app**, uruchom następujące polecenia w terminalu: - - ```bash - pip install -r requirements.txt - ``` - -2. **Zainstaluj FFmpeg (Essentials Build):** - - Przejdź do katalogu `/app` i wykonaj polecenie: - - ```bash - winget install "FFmpeg (Essentials Build)" - ``` - - Po zakończeniu instalacji zamknij i ponownie otwórz terminal. Przejdź do katalogu `/app` i wpisz: - - ```bash - ffmpeg - ``` - - Jeśli pojawią się informacje o rozszerzeniu FFmpeg, oznacza to, że instalacja przebiegła pomyślnie. - -### Możliwy błąd z FFmpeg - -W niektórych przypadkach ścieżka do FFmpeg może wymagać ręcznej modyfikacji. Jeśli napotkasz problemy z uruchomieniem, otwórz plik `recording.py` znajdujący się w folderze `backend` i zmodyfikuj linię 67: - -Z: - -```python -'ffmpeg' -``` - -Na: - -```python -'ffmpeg/bin/ffmpeg' -``` - -## Uruchamianie aplikacji - -1. Z katalogu proejkt_2025/apka/app uruchom aplikację poleceniem: - - ```bash - python -m main - ``` - -2. Aplikacja powinna uruchomić się i być gotowa do użycia. - -## Funkcjonalności aplikacji - -- **Nagrywanie spotkań:** Możliwość rejestrowania audio. -- **Tworzenie notatek:** Generowanie notatek na podstawie nagranych materiałów. - -## Uwagi - -- Upewnij się, że wszystkie wymagania zostały poprawnie zainstalowane -- W razie problemów z FFmpeg, sprawdź ścieżkę i upewnij się, że jest poprawna - ---- +# Aplikacja do Sporządzania Notatek ze Spotkań + +Aplikacja umożliwia nagrywanie spotkań, tworzenie z nich notatek oraz informowanie nieobecnych uczestników o ich przebiegu. + +## Wymagania + +Przed uruchomieniem aplikacji należy zainstalować wymagane zależności oraz skonfigurować środowisko. Szczegóły poniżej. + +### Instalacja zależności + +1. **Z katalogu projekt_2025/apka i projekt_2025/apka/app**, uruchom następujące polecenia w terminalu: + + ```bash + pip install -r requirements.txt + ``` + +2. **Zainstaluj FFmpeg (Essentials Build):** + + Przejdź do katalogu `/app` i wykonaj polecenie: + + ```bash + winget install "FFmpeg (Essentials Build)" + ``` + + Po zakończeniu instalacji zamknij i ponownie otwórz terminal. Przejdź do katalogu `/app` i wpisz: + + ```bash + ffmpeg + ``` + + Jeśli pojawią się informacje o rozszerzeniu FFmpeg, oznacza to, że instalacja przebiegła pomyślnie. + +### Możliwy błąd z FFmpeg + +W niektórych przypadkach ścieżka do FFmpeg może wymagać ręcznej modyfikacji. Jeśli napotkasz problemy z uruchomieniem, otwórz plik `recording.py` znajdujący się w folderze `backend` i zmodyfikuj linię 67: + +Z: + +```python +'ffmpeg' +``` + +Na: + +```python +'ffmpeg/bin/ffmpeg' +``` + +## Uruchamianie aplikacji + +1. Z katalogu proejkt_2025/apka/app uruchom aplikację poleceniem: + + ```bash + python -m main + ``` + +2. Aplikacja powinna uruchomić się i być gotowa do użycia. + +## Funkcjonalności aplikacji + +- **Nagrywanie spotkań:** Możliwość rejestrowania audio. +- **Tworzenie notatek:** Generowanie notatek na podstawie nagranych materiałów. + +## Uwagi + +- Upewnij się, że wszystkie wymagania zostały poprawnie zainstalowane +- W razie problemów z FFmpeg, sprawdź ścieżkę i upewnij się, że jest poprawna + +--- diff --git a/apka/app/__pycache__/main.cpython-310.pyc b/apka/app/__pycache__/main.cpython-310.pyc index 6238559..91d36df 100644 Binary files a/apka/app/__pycache__/main.cpython-310.pyc and b/apka/app/__pycache__/main.cpython-310.pyc differ diff --git a/apka/app/__pycache__/main.cpython-311.pyc b/apka/app/__pycache__/main.cpython-311.pyc index 5105400..7251526 100644 Binary files a/apka/app/__pycache__/main.cpython-311.pyc and b/apka/app/__pycache__/main.cpython-311.pyc differ diff --git a/apka/app/__pycache__/main.cpython-312.pyc b/apka/app/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..538fe3a Binary files /dev/null and b/apka/app/__pycache__/main.cpython-312.pyc differ diff --git a/apka/app/backend/__init__.py b/apka/app/backend/__init__.py index 62756fd..211a661 100644 --- a/apka/app/backend/__init__.py +++ b/apka/app/backend/__init__.py @@ -1,10 +1,40 @@ +import os from flask import Flask +from flask_sqlalchemy import SQLAlchemy -UPLOAD_FOLDER = 'uploads' +# Inicjalizacja SQLAlchemy (jedna, wspólna instancja) +db = SQLAlchemy() + +UPLOAD_FOLDER = 'uploads' def create_app() -> Flask: app = Flask(__name__, static_folder="../frontend/static", template_folder="../frontend/templates") - from .routes import main + app.secret_key = "secret_key" + + # Upewnij się, że mamy folder "instance" (jeśli nie istnieje, to go twórz) + instance_folder = os.path.join(os.getcwd(), 'instance') + if not os.path.exists(instance_folder): + os.makedirs(instance_folder) + + # Konfiguracja ścieżki do pliku bazy w folderze instance + database_path = os.path.join(instance_folder, 'database.db') + app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{database_path}" + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER + + # Inicjalizacja SQLAlchemy z aplikacją Flask + db.init_app(app) + + with app.app_context(): + # Importujemy modele PRZED wywołaniem create_all() + from .models import Email, Note + + print("Sprawdzam i tworzę tabele w bazie (o ile ich brak)...") + db.create_all() # Teraz SQLAlchemy zna klasy Email, Note + print("Gotowe!") + + # Rejestrowanie blueprintu + from .routes import main app.register_blueprint(main) + return app diff --git a/apka/app/backend/__pycache__/__init__.cpython-310.pyc b/apka/app/backend/__pycache__/__init__.cpython-310.pyc index 963a6e9..b2e980b 100644 Binary files a/apka/app/backend/__pycache__/__init__.cpython-310.pyc and b/apka/app/backend/__pycache__/__init__.cpython-310.pyc differ diff --git a/apka/app/backend/__pycache__/__init__.cpython-311.pyc b/apka/app/backend/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..5135bd5 Binary files /dev/null and b/apka/app/backend/__pycache__/__init__.cpython-311.pyc differ diff --git a/apka/app/backend/__pycache__/__init__.cpython-312.pyc b/apka/app/backend/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..0646956 Binary files /dev/null and b/apka/app/backend/__pycache__/__init__.cpython-312.pyc differ diff --git a/apka/app/backend/__pycache__/calendar_integration.cpython-310.pyc b/apka/app/backend/__pycache__/calendar_integration.cpython-310.pyc index 9257e38..2f2f720 100644 Binary files a/apka/app/backend/__pycache__/calendar_integration.cpython-310.pyc and b/apka/app/backend/__pycache__/calendar_integration.cpython-310.pyc differ diff --git a/apka/app/backend/__pycache__/calendar_integration.cpython-311.pyc b/apka/app/backend/__pycache__/calendar_integration.cpython-311.pyc new file mode 100644 index 0000000..ed12bd0 Binary files /dev/null and b/apka/app/backend/__pycache__/calendar_integration.cpython-311.pyc differ diff --git a/apka/app/backend/__pycache__/google_calendar_integration.cpython-310.pyc b/apka/app/backend/__pycache__/google_calendar_integration.cpython-310.pyc new file mode 100644 index 0000000..5847594 Binary files /dev/null and b/apka/app/backend/__pycache__/google_calendar_integration.cpython-310.pyc differ diff --git a/apka/app/backend/__pycache__/google_calendar_integration.cpython-311.pyc b/apka/app/backend/__pycache__/google_calendar_integration.cpython-311.pyc new file mode 100644 index 0000000..73746fc Binary files /dev/null and b/apka/app/backend/__pycache__/google_calendar_integration.cpython-311.pyc differ diff --git a/apka/app/backend/__pycache__/google_calendar_integration.cpython-312.pyc b/apka/app/backend/__pycache__/google_calendar_integration.cpython-312.pyc new file mode 100644 index 0000000..79abea7 Binary files /dev/null and b/apka/app/backend/__pycache__/google_calendar_integration.cpython-312.pyc differ diff --git a/apka/app/backend/__pycache__/models.cpython-311.pyc b/apka/app/backend/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000..79440f0 Binary files /dev/null and b/apka/app/backend/__pycache__/models.cpython-311.pyc differ diff --git a/apka/app/backend/__pycache__/models.cpython-312.pyc b/apka/app/backend/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..9e39bcf Binary files /dev/null and b/apka/app/backend/__pycache__/models.cpython-312.pyc differ diff --git a/apka/app/backend/__pycache__/ms_calendar_integration.cpython-310.pyc b/apka/app/backend/__pycache__/ms_calendar_integration.cpython-310.pyc new file mode 100644 index 0000000..62c3bb1 Binary files /dev/null and b/apka/app/backend/__pycache__/ms_calendar_integration.cpython-310.pyc differ diff --git a/apka/app/backend/__pycache__/ms_calendar_integration.cpython-311.pyc b/apka/app/backend/__pycache__/ms_calendar_integration.cpython-311.pyc new file mode 100644 index 0000000..5954a9f Binary files /dev/null and b/apka/app/backend/__pycache__/ms_calendar_integration.cpython-311.pyc differ diff --git a/apka/app/backend/__pycache__/ms_calendar_integration.cpython-312.pyc b/apka/app/backend/__pycache__/ms_calendar_integration.cpython-312.pyc new file mode 100644 index 0000000..5e041cb Binary files /dev/null and b/apka/app/backend/__pycache__/ms_calendar_integration.cpython-312.pyc differ diff --git a/apka/app/backend/__pycache__/note.cpython-310.pyc b/apka/app/backend/__pycache__/note.cpython-310.pyc index 3e49c1f..b736b9d 100644 Binary files a/apka/app/backend/__pycache__/note.cpython-310.pyc and b/apka/app/backend/__pycache__/note.cpython-310.pyc differ diff --git a/apka/app/backend/__pycache__/note.cpython-311.pyc b/apka/app/backend/__pycache__/note.cpython-311.pyc new file mode 100644 index 0000000..d29c75f Binary files /dev/null and b/apka/app/backend/__pycache__/note.cpython-311.pyc differ diff --git a/apka/app/backend/__pycache__/note.cpython-312.pyc b/apka/app/backend/__pycache__/note.cpython-312.pyc new file mode 100644 index 0000000..73ba3dc Binary files /dev/null and b/apka/app/backend/__pycache__/note.cpython-312.pyc differ diff --git a/apka/app/backend/__pycache__/recording.cpython-310.pyc b/apka/app/backend/__pycache__/recording.cpython-310.pyc index d2f37d0..da48218 100644 Binary files a/apka/app/backend/__pycache__/recording.cpython-310.pyc and b/apka/app/backend/__pycache__/recording.cpython-310.pyc differ diff --git a/apka/app/backend/__pycache__/recording.cpython-311.pyc b/apka/app/backend/__pycache__/recording.cpython-311.pyc new file mode 100644 index 0000000..cb0732b Binary files /dev/null and b/apka/app/backend/__pycache__/recording.cpython-311.pyc differ diff --git a/apka/app/backend/__pycache__/recording.cpython-312.pyc b/apka/app/backend/__pycache__/recording.cpython-312.pyc new file mode 100644 index 0000000..4f6b863 Binary files /dev/null and b/apka/app/backend/__pycache__/recording.cpython-312.pyc differ diff --git a/apka/app/backend/__pycache__/routes.cpython-310.pyc b/apka/app/backend/__pycache__/routes.cpython-310.pyc index 570b101..f588982 100644 Binary files a/apka/app/backend/__pycache__/routes.cpython-310.pyc and b/apka/app/backend/__pycache__/routes.cpython-310.pyc differ diff --git a/apka/app/backend/__pycache__/routes.cpython-311.pyc b/apka/app/backend/__pycache__/routes.cpython-311.pyc new file mode 100644 index 0000000..d65fa9e Binary files /dev/null and b/apka/app/backend/__pycache__/routes.cpython-311.pyc differ diff --git a/apka/app/backend/__pycache__/routes.cpython-312.pyc b/apka/app/backend/__pycache__/routes.cpython-312.pyc new file mode 100644 index 0000000..1624601 Binary files /dev/null and b/apka/app/backend/__pycache__/routes.cpython-312.pyc differ diff --git a/apka/app/backend/__pycache__/screenshot.cpython-310.pyc b/apka/app/backend/__pycache__/screenshot.cpython-310.pyc index db57353..0d483b3 100644 Binary files a/apka/app/backend/__pycache__/screenshot.cpython-310.pyc and b/apka/app/backend/__pycache__/screenshot.cpython-310.pyc differ diff --git a/apka/app/backend/__pycache__/screenshot.cpython-311.pyc b/apka/app/backend/__pycache__/screenshot.cpython-311.pyc new file mode 100644 index 0000000..1a46f9f Binary files /dev/null and b/apka/app/backend/__pycache__/screenshot.cpython-311.pyc differ diff --git a/apka/app/backend/__pycache__/screenshot.cpython-312.pyc b/apka/app/backend/__pycache__/screenshot.cpython-312.pyc new file mode 100644 index 0000000..0165ef1 Binary files /dev/null and b/apka/app/backend/__pycache__/screenshot.cpython-312.pyc differ diff --git a/apka/app/backend/__pycache__/summary.cpython-310.pyc b/apka/app/backend/__pycache__/summary.cpython-310.pyc new file mode 100644 index 0000000..c9aeb53 Binary files /dev/null and b/apka/app/backend/__pycache__/summary.cpython-310.pyc differ diff --git a/apka/app/backend/__pycache__/summary.cpython-311.pyc b/apka/app/backend/__pycache__/summary.cpython-311.pyc new file mode 100644 index 0000000..990c253 Binary files /dev/null and b/apka/app/backend/__pycache__/summary.cpython-311.pyc differ diff --git a/apka/app/backend/__pycache__/summary.cpython-312.pyc b/apka/app/backend/__pycache__/summary.cpython-312.pyc new file mode 100644 index 0000000..6a5e0d4 Binary files /dev/null and b/apka/app/backend/__pycache__/summary.cpython-312.pyc differ diff --git a/apka/app/backend/__pycache__/teams_integration.cpython-310.pyc b/apka/app/backend/__pycache__/teams_integration.cpython-310.pyc new file mode 100644 index 0000000..f58cd44 Binary files /dev/null and b/apka/app/backend/__pycache__/teams_integration.cpython-310.pyc differ diff --git a/apka/app/backend/__pycache__/teams_integration.cpython-311.pyc b/apka/app/backend/__pycache__/teams_integration.cpython-311.pyc new file mode 100644 index 0000000..09e4441 Binary files /dev/null and b/apka/app/backend/__pycache__/teams_integration.cpython-311.pyc differ diff --git a/apka/app/backend/__pycache__/teams_integration.cpython-312.pyc b/apka/app/backend/__pycache__/teams_integration.cpython-312.pyc new file mode 100644 index 0000000..0283ed6 Binary files /dev/null and b/apka/app/backend/__pycache__/teams_integration.cpython-312.pyc differ diff --git a/apka/app/backend/__pycache__/zoom_integration.cpython-310.pyc b/apka/app/backend/__pycache__/zoom_integration.cpython-310.pyc new file mode 100644 index 0000000..69e69ee Binary files /dev/null and b/apka/app/backend/__pycache__/zoom_integration.cpython-310.pyc differ diff --git a/apka/app/backend/__pycache__/zoom_integration.cpython-311.pyc b/apka/app/backend/__pycache__/zoom_integration.cpython-311.pyc new file mode 100644 index 0000000..9953143 Binary files /dev/null and b/apka/app/backend/__pycache__/zoom_integration.cpython-311.pyc differ diff --git a/apka/app/backend/__pycache__/zoom_integration.cpython-312.pyc b/apka/app/backend/__pycache__/zoom_integration.cpython-312.pyc new file mode 100644 index 0000000..66b602a Binary files /dev/null and b/apka/app/backend/__pycache__/zoom_integration.cpython-312.pyc differ diff --git a/apka/app/backend/calendar_integration.py b/apka/app/backend/calendar_integration.py deleted file mode 100644 index c1c1d7b..0000000 --- a/apka/app/backend/calendar_integration.py +++ /dev/null @@ -1,69 +0,0 @@ -import datetime -import os -from google.auth.transport.requests import Request -from google.oauth2.credentials import Credentials -from google_auth_oauthlib.flow import InstalledAppFlow -from googleapiclient.discovery import build -from googleapiclient.errors import HttpError -from dateutil import parser - -SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"] - -def get_calendar_events(): - token_path = os.path.join(os.path.dirname(__file__), "token.json") - - if os.path.exists(token_path): - print(f"Removing existing token file at {token_path}...") - os.remove(token_path) - - creds = None - - # Zawsze generuj nowy token - flow = InstalledAppFlow.from_client_secrets_file( - os.path.join(os.path.dirname(__file__), "credentials.json"), SCOPES - ) - creds = flow.run_local_server(port=5001, access_type="offline", prompt="consent") - - # Zapisz nowo wygenerowany token - with open(token_path, "w") as token: - print(f"Saving new token file to {token_path}...") - token.write(creds.to_json()) - - try: - service = build("calendar", "v3", credentials=creds) - now = datetime.datetime.utcnow().isoformat() + "Z" - print("Getting the upcoming 10 events") - events_result = ( - service.events() - .list( - calendarId="primary", - timeMin=now, - maxResults=10, - singleEvents=True, - orderBy="startTime", - ) - .execute() - ) - events = events_result.get("items", []) - - if not events: - print("No upcoming events found.") - return - - formatted_events = [] - for event in events: - start = event["start"].get("dateTime", event["start"].get("date")) - summary = event.get("summary", "Brak nazwy wydarzenia") - - # Konwertuj datę i godzinę na czytelny format - if "T" in start: # Jeśli jest pełna data i godzina - start_time = parser.parse(start).strftime("%Y-%m-%d %H:%M") - else: # Jeśli tylko data - start_time = start - - formatted_events.append({"start": start_time, "summary": summary}) - - return formatted_events - except HttpError as error: - print(f"An error occurred: {error}") - return [] \ No newline at end of file diff --git a/apka/app/backend/credentials.json b/apka/app/backend/credentials.json deleted file mode 100644 index 008bb3d..0000000 --- a/apka/app/backend/credentials.json +++ /dev/null @@ -1 +0,0 @@ -{"web":{"client_id":"907999041253-2eqttdeuvd1inb1oh6a81vptdk1sh9ao.apps.googleusercontent.com","project_id":"kinetic-catfish-443219-q4","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"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","redirect_uris":["http://localhost:5001/"]}} \ No newline at end of file diff --git a/apka/app/backend/credentials_2.json b/apka/app/backend/credentials_2.json deleted file mode 100644 index 008bb3d..0000000 --- a/apka/app/backend/credentials_2.json +++ /dev/null @@ -1 +0,0 @@ -{"web":{"client_id":"907999041253-2eqttdeuvd1inb1oh6a81vptdk1sh9ao.apps.googleusercontent.com","project_id":"kinetic-catfish-443219-q4","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"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","redirect_uris":["http://localhost:5001/"]}} \ No newline at end of file diff --git a/apka/app/backend/credentialsstare.json b/apka/app/backend/credentialsstare.json deleted file mode 100644 index ae33ce3..0000000 --- a/apka/app/backend/credentialsstare.json +++ /dev/null @@ -1 +0,0 @@ -{"web":{"client_id":"907999041253-2eqttdeuvd1inb1oh6a81vptdk1sh9ao.apps.googleusercontent.com","project_id":"kinetic-catfish-443219-q4","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"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"}} \ No newline at end of file diff --git a/apka/app/backend/google_calendar_integration.py b/apka/app/backend/google_calendar_integration.py new file mode 100644 index 0000000..c6845ea --- /dev/null +++ b/apka/app/backend/google_calendar_integration.py @@ -0,0 +1,114 @@ +# google_calendar_integration.py +import os +import requests +from dotenv import load_dotenv +from google_auth_oauthlib.flow import Flow +from google.oauth2.credentials import Credentials +from googleapiclient.discovery import build +from datetime import datetime, timezone +import pytz + + +load_dotenv() + +GOOGLE_CLIENT_ID = "907999041253-2eqttdeuvd1inb1oh6a81vptdk1sh9ao.apps.googleusercontent.com" +GOOGLE_CLIENT_SECRET = "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" +GOOGLE_REDIRECT_URI = "http://localhost:5000/google-calendar/callback" + +SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"] + +def get_google_authorize_url(): + """ + Generuje link do logowania w Google. + """ + print("DEBUG REDIRECT URI =", GOOGLE_REDIRECT_URI) + flow = Flow.from_client_config( + { + "web": { + "client_id": GOOGLE_CLIENT_ID, + "client_secret": GOOGLE_CLIENT_SECRET, + "redirect_uris": [GOOGLE_REDIRECT_URI], + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token" + } + }, + scopes=SCOPES, + redirect_uri=GOOGLE_REDIRECT_URI + ) + auth_url, _ = flow.authorization_url( + access_type="offline", + prompt="consent" + ) + print(auth_url) + return auth_url + +def exchange_code_for_token2(code): + """ + Wymienia 'code' na obiekt Credentials (zawierający access_token). + """ + flow = Flow.from_client_config( + { + "web": { + "client_id": GOOGLE_CLIENT_ID, + "client_secret": GOOGLE_CLIENT_SECRET, + "redirect_uris": [GOOGLE_REDIRECT_URI], + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token" + } + }, + scopes=SCOPES, + redirect_uri=GOOGLE_REDIRECT_URI, + ) + flow.fetch_token(code=code) + creds = flow.credentials + print("credentials",creds) + return creds # obiekt Credentials + +def get_google_events(credentials): + """ + Pobiera listę wydarzeń z kalendarza. + Zwraca tablicę obiektów [ {summary, start, end, attendees, ...}, ... ] + """ + service = build("calendar", "v3", credentials=credentials) + now = datetime.now(timezone.utc).isoformat() + + # Wydarzenia z 'primary' kalendarza + events_result = service.events().list( + calendarId="primary", + maxResults=10, + singleEvents=True, + orderBy="startTime", + timeMin=now # example, usun lub param + ).execute() + events = events_result.get("items", []) + + for event in events: + start = event.get("start", {}) + end = event.get("end", {}) + + event["start"] = format_event_datetime(start.get("dateTime") or start.get("date")) + event["end"] = format_event_datetime(end.get("dateTime") or end.get("date")) + + return events + +def get_google_event_details(credentials, event_id): + """ + Pobiera szczegóły jednego wydarzenia z ID = event_id. + """ + service = build("calendar", "v3", credentials=credentials) + event = service.events().get(calendarId="primary", eventId=event_id).execute() + return event + + +def format_event_datetime(dt_str): + """ Konwertuje datę z ISO 8601 na czytelny format DD.MM.YYYY, HH:MM """ + if not dt_str: + return "Brak daty" + + try: + dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00")) # Obsługa UTC + dt = dt.astimezone(pytz.timezone("Europe/Warsaw")) # Konwersja do PL + return dt.strftime("%d.%m.%Y, %H:%M") # Format DD.MM.YYYY, HH:MM + except Exception as e: + print("Błąd konwersji daty:", e) + return dt_str # Zwraca oryginalną wartość w razie błędu \ No newline at end of file diff --git a/apka/app/backend/models.py b/apka/app/backend/models.py new file mode 100644 index 0000000..6c933e0 --- /dev/null +++ b/apka/app/backend/models.py @@ -0,0 +1,11 @@ +# Zamiast: from flask_sqlalchemy import SQLAlchemy +# Używamy istniejącego obiektu z init.py +from . import db # <-- importujemy "db" z __init__.py + +class Email(db.Model): + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(255), unique=True, nullable=False) + +class Note(db.Model): + id = db.Column(db.Integer, primary_key=True) + filename = db.Column(db.String(255), unique=True, nullable=False) diff --git a/apka/app/backend/ms_calendar_integration.py b/apka/app/backend/ms_calendar_integration.py new file mode 100644 index 0000000..685d323 --- /dev/null +++ b/apka/app/backend/ms_calendar_integration.py @@ -0,0 +1,87 @@ +# ms_calendar_integration.py + +import os +import requests +from dotenv import load_dotenv + +load_dotenv() + +TENANT_ID = os.getenv("MS_CALENDAR_TENANT_ID") +CLIENT_ID = os.getenv("MS_CALENDAR_CLIENT_ID") +CLIENT_SECRET = os.getenv("MS_CALENDAR_CLIENT_SECRET") +REDIRECT_URI = os.getenv("MS_CALENDAR_REDIRECT_URI", "http://localhost:5000/ms-calendar/callback") + +TENANT_ID="40ad34a2-4df4-499f-a628-c865a29a7782" +CLIENT_ID="4aa51599-98de-4a0a-8006-6939d24a18f4" +CLIENT_SECRET="T5l8Q~_mmVp_.qHDlRkUKenBCELcceLYPFnDUcGF" +REDIRECT_URI="https://login.microsoftonline.com/common/oauth2/nativeclient" + +def get_ms_calendar_authorize_url_ms(): + """ + Generuje link, gdzie user się loguje i wyraża zgodę na dostęp do kalendarza. + Microsoft Identity Platform v2 endpoint: + https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize + """ + base_url = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize" + scope = "Calendars.Read offline_access" + + return ( + f"{base_url}" + f"?client_id={CLIENT_ID}" + f"&response_type=code" + f"&redirect_uri={REDIRECT_URI}" + f"&response_mode=query" + f"&scope={scope}" + ) + +def exchange_code_for_token_ms(code): + """ + Wymienia code na access_token używając endpointu: + POST /{tenant}/oauth2/v2.0/token + """ + token_url = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token" + data = { + "client_id": CLIENT_ID, + "scope": "Calendars.Read offline_access", + "code": code, + "redirect_uri": REDIRECT_URI, + "grant_type": "authorization_code", + "client_secret": CLIENT_SECRET + } + + resp = requests.post(token_url, data=data) + token_json = resp.json() + if "access_token" not in token_json: + raise Exception(f"Błąd wymiany code na token: {token_json}") + return token_json + +def get_ms_calendar_events_ms(access_token): + """ + Pobiera listę wydarzeń z kalendarza użytkownika: + GET https://graph.microsoft.com/v1.0/me/events + """ + url = "https://graph.microsoft.com/v1.0/me/events" + headers = { + "Authorization": f"Bearer {access_token}" + } + resp = requests.get(url, headers=headers) + data = resp.json() + if "value" not in data: + raise Exception(f"Błąd przy pobieraniu eventów: {data}") + return data["value"] + +def get_ms_calendar_event_details_ms(access_token, event_id): + """ + Pobiera szczegóły wybranego wydarzenia: + GET /me/events/{event_id} + Zwraca obiekt zawierający m.in. 'attendees', 'start', 'end', 'subject' + """ + url = f"https://graph.microsoft.com/v1.0/me/events/{event_id}" + headers = { + "Authorization": f"Bearer {access_token}" + } + resp = requests.get(url, headers=headers) + data = resp.json() + if "id" not in data: + raise Exception(f"Nie znaleziono eventu {event_id} lub błąd: {data}") + return data diff --git a/apka/app/backend/note.py b/apka/app/backend/note.py index 3313ff3..8fdcb4d 100644 --- a/apka/app/backend/note.py +++ b/apka/app/backend/note.py @@ -4,44 +4,48 @@ import torchaudio from pydub import AudioSegment from docx.shared import Inches - +from pyannote.audio import Pipeline +from collections import defaultdict +from .summary import generate_summary # Inicjalizacja modelu Whisper -model = load_model("base") #ew "small" +token = "" +model = load_model("base") + -def save_transcription_to_docx(transcription, docx_path, screenshots_folder): +def save_transcription_to_docx(transcription, docx_path, screenshots_folder, speaker_stats=None): """ Zapisuje transkrypcję i screenshoty w pliku .docx. """ doc = Document() - - # Dodaj transkrypcję doc.add_paragraph("Transkrypcja wykładu:", style='Heading 1') doc.add_paragraph(transcription) - - # Dodaj screenshoty doc.add_paragraph("Zrzuty ekranu:", style='Heading 1') + if os.path.exists(screenshots_folder): for screenshot in sorted(os.listdir(screenshots_folder)): screenshot_path = os.path.join(screenshots_folder, screenshot) doc.add_picture(screenshot_path, width=Inches(5)) - doc.add_paragraph(f"Zrzut ekranu: {os.path.basename(screenshot)}") else: doc.add_paragraph("Brak zrzutów ekranu.") - # Zapisz plik docx + if speaker_stats: + doc.add_paragraph("Statystyki mówców:", style='Heading 1') + for speaker, (duration, speed) in speaker_stats.items(): + doc.add_paragraph(f"{speaker}: {duration:.2f} sekund, {speed:.2f} słów/sek") + doc.save(docx_path) print(f"Zapisano plik .docx: {docx_path}") -# Funkcja do wczytania pliku audio i jego konwersji na WAV (16000 Hz, mono) + +# Konwersja audio do 16kHz WAV + def load_and_convert_audio(audio_path): file_extension = os.path.splitext(audio_path)[1].lower() - - if file_extension == '.mp3': # Konwersja MP3 na WAV + if file_extension == '.mp3': wav_path = audio_path.replace('.mp3', '.wav') audio = AudioSegment.from_mp3(audio_path) - audio = audio.set_frame_rate(16000) - audio = audio.set_channels(1) + audio = audio.set_frame_rate(16000).set_channels(1) audio.export(wav_path, format="wav") audio_path = wav_path @@ -50,43 +54,94 @@ def load_and_convert_audio(audio_path): signal = torchaudio.transforms.Resample(orig_freq=sample_rate, new_freq=16000)(signal) return signal, 16000 -# Funkcja do transkrypcji mowy na tekst + +def diarize_audio(audio_path, token): + """Diarizacja audio""" + pipeline = Pipeline.from_pretrained("pyannote/speaker-diarization-3.1", use_auth_token=token) + diarization = pipeline(audio_path) + return diarization + + def transcribe_audio(audio_path): signal, sample_rate = load_and_convert_audio(audio_path) transcription = model.transcribe(audio_path) - return transcription['text'] + return transcription['segments'] + + +def combine_transcription_with_diarization(transcription_segments, diarization): + diarized_text = [] + speaker_durations = defaultdict(float) + speaker_word_counts = defaultdict(int) + current_speaker = None + + for segment in transcription_segments: + start_time, end_time, text = segment['start'], segment['end'], segment['text'] + word_count = len(text.split()) + + speaker = None + for turn, _, speaker_id in diarization.itertracks(yield_label=True): + if turn.start <= start_time <= turn.end or turn.start <= end_time <= turn.end: + speaker = speaker_id + break + + if speaker is None: + continue + + speaker_durations[speaker] += (end_time - start_time) + speaker_word_counts[speaker] += word_count + + # Jeżeli zmienił się mówca, dodajemy jego identyfikator + if speaker != current_speaker: + diarized_text.append(f"\n{speaker}:") + current_speaker = speaker + + diarized_text.append(f" {text}") + + # Tworzenie końcowego tekstu + formatted_transcription = "".join(diarized_text) + speaker_speeds = {spk: speaker_word_counts[spk] / speaker_durations[spk] for spk in speaker_durations} + + return formatted_transcription, speaker_durations, speaker_speeds + -# Funkcja do przetwarzania audio i zapisywania transkrypcji def process_audio_and_save_transcription(audio_folder, docx_folder, screenshots_folder): if not os.path.exists(docx_folder): os.makedirs(docx_folder) for filename in os.listdir(audio_folder): - if filename.endswith(".mp3") or filename.endswith(".wav"): # Obsługuje pliki audio + if filename.endswith(".mp3") or filename.endswith(".wav"): audio_file_path = os.path.join(audio_folder, filename) - docx_filename = os.path.join(docx_folder, f"{os.path.splitext(filename)[0]}.docx") + raw_docx_filename = os.path.join(docx_folder, f"raw_{os.path.splitext(filename)[0]}.docx") + summary_filename = os.path.join(docx_folder, f"summary_{os.path.splitext(filename)[0]}.docx") + if os.path.exists(docx_filename): print(f"Pominięto {filename}, transkrypcja już istnieje.") - continue # Pomijamy przetwarzanie, jeśli plik istnieje + continue try: - transcription = transcribe_audio(audio_file_path) - print(f"Zakończona transkrypcja: {filename}") + transcription_segments = transcribe_audio(audio_file_path) + raw_transcription_text = " ".join([seg['text'] for seg in transcription_segments]) + save_transcription_to_docx(raw_transcription_text, raw_docx_filename, screenshots_folder) + + diarization = diarize_audio(audio_file_path, token) + diarized_text, speaker_durations, speaker_speeds = combine_transcription_with_diarization( + transcription_segments, diarization) - # Nazwa pliku docx na podstawie nazwy pliku audio - docx_filename = os.path.join(docx_folder, f"{os.path.splitext(filename)[0]}.docx") + speaker_stats = {spk: (speaker_durations[spk], speaker_speeds[spk]) for spk in speaker_durations} - save_transcription_to_docx(transcription, docx_filename, screenshots_folder) - print(f"Zapisano transkrypcję do: {docx_filename}") + save_transcription_to_docx(diarized_text, docx_filename, screenshots_folder, speaker_stats) + generate_summary(raw_docx_filename, summary_filename) + print(f"Zapisano transkrypcję i podsumowanie do: {docx_filename}, {summary_filename}") except Exception as e: print(f"Błąd przy przetwarzaniu {filename}: {e}") + if __name__ == "__main__": audio_folder = r"recordings" docx_folder = os.path.join(os.getcwd(), 'recordings', 'notes') - screenshots_folder = os.path.join(os.getcwd(), 'recordings', 'screenshots') # Folder zrzutów ekranu + screenshots_folder = os.path.join(os.getcwd(), 'recordings', 'screenshots') if not os.path.exists(docx_folder): os.makedirs(docx_folder) diff --git a/apka/app/backend/routes.py b/apka/app/backend/routes.py index 4aaed32..9beb5bf 100644 --- a/apka/app/backend/routes.py +++ b/apka/app/backend/routes.py @@ -1,49 +1,385 @@ -from flask import Blueprint, Flask, jsonify, render_template, current_app, request, send_from_directory -from .calendar_integration import get_calendar_events +from docx import Document +import re +from flask import Blueprint, Flask, jsonify, render_template, current_app, request, send_from_directory, session, redirect, url_for, flash +import requests import pygetwindow as gw from .recording import record_window, start_recording_thread, stop_recording, save_recording, setup_upload_folder -from .screenshot import extract_screenshots_from_video import os from werkzeug.utils import secure_filename from .note import process_audio_and_save_transcription +from .screenshot import extract_screenshots_from_video +import base64 +from .teams_integration import get_teams_events +from . import db +from .models import db, Email +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email.mime.application import MIMEApplication +from google.oauth2.credentials import Credentials +from dotenv import load_dotenv +from flask import jsonify + +# Import funkcji z plików integracyjnych +from .zoom_integration import ( + get_zoom_authorize_url, + exchange_code_for_token, + get_zoom_meetings, + get_personal_meeting_details, + get_zoom_participants +) +from .teams_integration import ( + exchange_code_for_token, + get_teams_events, + get_teams_event_details +) +from .ms_calendar_integration import ( + get_ms_calendar_authorize_url_ms, + exchange_code_for_token_ms, + get_ms_calendar_events_ms, + get_ms_calendar_event_details_ms +) +from .google_calendar_integration import ( + get_google_authorize_url, + exchange_code_for_token2, + get_google_events, + get_google_event_details +) + +from dotenv import find_dotenv +load_dotenv() +print("Ścieżka do pliku .env:", find_dotenv()) + + +ZOOM_CLIENT_ID = os.getenv("ZOOM_CLIENT_ID") +ZOOM_CLIENT_SECRET = os.getenv("ZOOM_CLIENT_SECRET") +ZOOM_REDIRECT_URI = os.getenv("ZOOM_REDIRECT_URI") + +# Nadpisanie do testów +ZOOM_CLIENT_ID = "FA1BsrIaTHyB5zmz2hukmg" +ZOOM_CLIENT_SECRET = "dpQeeLxZBAqFFaEpevt2WmAv1J81jtmJ" +ZOOM_REDIRECT_URI = "http://localhost:5000/zoom/callback" + +TEAMS_TENANT_ID = "40ad34a2-4df4-499f-a628-c865a29a7782" +TEAMS_CLIENT_ID = "4aa51599-98de-4a0a-8006-6939d24a18f4" +TEAMS_CLIENT_SECRET = "T5l8Q~_mmVp_.qHDlRkUKenBCELcceLYPFnDUcGF" +TEAMS_REDIRECT_URI = "https://login.microsoftonline.com/common/oauth2/nativeclient" + +UPLOAD_FOLDER = os.path.join(os.getcwd(), 'recordings') +SCREENSHOT_FOLDER = os.path.join(UPLOAD_FOLDER, 'screenshots') +ALLOWED_EXTENSIONS = {'webm', 'mp4', 'avi'} + +# Folder na notatki (DOCX) +NOTES_FOLDER = os.path.join(UPLOAD_FOLDER, 'notes') +os.makedirs(NOTES_FOLDER, exist_ok=True) main = Blueprint('main', __name__, template_folder="../frontend/templates", static_folder="../frontend/static") +# STRONA GŁÓWNA @main.route("/") def index(): return render_template("index.html") -@main.route("/record") -def record(): - return render_template("record.html") +# ZOOM INTEGRATION +def get_zoom_authorize_url(): + base_url = "https://zoom.us/oauth/authorize" + print("ZOOM_CLIENT_ID =", ZOOM_CLIENT_ID) + print("ZOOM_REDIRECT_URI =", ZOOM_REDIRECT_URI) + + return ( + f"{base_url}?" + f"response_type=code" + f"&client_id={ZOOM_CLIENT_ID}" + f"&redirect_uri={ZOOM_REDIRECT_URI}" + ) + +def exchange_code_for_token(code): + url_token = "https://zoom.us/oauth/token" + params = { + "grant_type": "authorization_code", + "code": code, + "redirect_uri": ZOOM_REDIRECT_URI, + } + creds = f"{ZOOM_CLIENT_ID}:{ZOOM_CLIENT_SECRET}" + b64_creds = base64.b64encode(creds.encode()).decode() + headers = { + "Authorization": f"Basic {b64_creds}" + } + + print("DEBUG: Wysyłamy request do Zoom z parametrami:", params) + print("DEBUG: Authorization:", headers["Authorization"]) + + resp = requests.post(url_token, headers=headers, params=params) + data = resp.json() + print("DEBUG: Odpowiedź Zoom:", data) + + if "access_token" not in data: + raise Exception(f"Błąd wymiany code na token: {data}") + + return data['access_token'] + + +@main.route("/zoom/login") +def zoom_login(): + # Generujemy URL autoryzacji i przekierowujemy + auth_url = get_zoom_authorize_url() + return redirect(auth_url) + +@main.route("/zoom/callback") +def zoom_callback(): + code = request.args.get("code") + if not code: + return "Brak parametru 'code' w callbacku", 400 + + try: + token_data = exchange_code_for_token(code) + session["zoom_access_token"] = token_data + return render_template("my_events.html") + except Exception as e: + return f"Błąd podczas wymiany code na token: {e}", 400 + + +@main.route("/zoom-events") +def zoom_events(): + # Pobieramy spotkania z Zoom + access_token = session.get("zoom_access_token") + if not access_token: + return jsonify({"error": "Brak access_token – zaloguj się przez /zoom/login"}), 401 + + try: + meetings = get_zoom_meetings(access_token) + return jsonify(meetings) + except Exception as e: + return jsonify({"error": str(e)}), 400 + + +@main.route("/zoom-meetings") +def zoom_meetings(): + try: + access_token = session.get("zoom_access_token") + meetings = get_zoom_meetings(access_token) + return jsonify(meetings) # np. [{topic, start_time, join_url}, ...] + except Exception as e: + print("Błąd w /zoom-meetings:", e) + return jsonify({"error": str(e)}), 400 + + +@main.route("/zoom-personal-meeting") +def zoom_personal_meeting(): + # Szczegóły personalnego meetingu Zoom + access_token = session.get("zoom_access_token") + if not access_token: + return jsonify({"error": "Brak access_token – zaloguj się przez /zoom/login"}), 401 + + personal_meeting_id = request.args.get("pmid") + if not personal_meeting_id: + return jsonify({"error": "Podaj ?pmid=... w URL"}), 400 + + try: + meeting_data = get_personal_meeting_details(access_token, personal_meeting_id) + return jsonify(meeting_data) + except Exception as e: + return jsonify({"error": str(e)}), 400 + + +@main.route("/zoom-meeting-participants") +def zoom_meeting_participants(): + access_token = session.get("zoom_access_token") + if not access_token: + return jsonify({"error": "Nie jesteś zalogowany w Zoom."}), 401 + + meeting_id = request.args.get("meetingId") + if not meeting_id: + return jsonify({"error": "Brak parametru meetingId"}), 400 + + try: + participants = get_zoom_participants(meeting_id, access_token) + return jsonify(participants) + except Exception as e: + return jsonify({"error": str(e)}), 400 + + +# TEAMS INTEGRATION +def get_teams_authorize_url(): + base_url = f"https://login.microsoftonline.com/{TEAMS_TENANT_ID}/oauth2/v2.0/authorize" + scope = "Calendars.Read offline_access User.Read" + return ( + f"{base_url}?" + f"client_id={TEAMS_CLIENT_ID}" + f"&response_type=code" + f"&redirect_uri={TEAMS_REDIRECT_URI}" + f"&response_mode=query" + f"&scope={scope}" + ) + +@main.route("/teams/login") +def teams_login(): + auth_url = get_teams_authorize_url() + return redirect(auth_url) + +@main.route("/teams/callback") +def teams_callback(): + code = request.args.get("code") + if not code: + return "Brak parametru 'code'", 400 + + try: + token_json = exchange_code_for_token(code) + session["teams_access_token"] = token_json["access_token"] + return "Autoryzacja z Teams zakończona! Możesz pobrać swoje eventy." + except Exception as e: + return f"Błąd: {e}", 400 + +@main.route("/teams-events") +def teams_events(): + access_token = session.get("teams_access_token") + if not access_token: + return jsonify({"error": "Niezalogowany w Teams"}), 401 + + try: + events = get_teams_events(access_token) + return jsonify(events) + except Exception as e: + return jsonify({"error": str(e)}), 400 + +@main.route("/teams-event-details") +def teams_event_details_route(): + access_token = session.get("teams_access_token") + if not access_token: + return jsonify({"error": "Niezalogowany w Teams"}), 401 + + event_id = request.args.get("eventId") + if not event_id: + return jsonify({"error": "Brak parametru eventId"}), 400 + + try: + event_details = get_teams_event_details(access_token, event_id) + return jsonify(event_details) + except Exception as e: + return jsonify({"error": str(e)}), 400 + + +# MS CALENDAR INTEGRATION +@main.route("/ms-calendar/login") +def ms_calendar_login(): + auth_url = get_ms_calendar_authorize_url_ms() + return redirect(auth_url) +@main.route("/ms-calendar/callback") +def ms_calendar_callback(): + code = request.args.get("code") + if not code: + return "Brak parametru code w callbacku", 400 + try: + token_json = exchange_code_for_token_ms(code) + session["ms_calendar_access_token"] = token_json["access_token"] + return "Zalogowano do MS Calendar! Teraz możesz pobrać wydarzenia." + except Exception as e: + return f"Błąd: {e}", 400 + +@main.route("/ms-calendar/events") +def ms_calendar_events(): + access_token = session.get("ms_calendar_access_token") + if not access_token: + return jsonify({"error": "Brak zalogowania do MS Calendar"}), 401 + try: + events_list = get_ms_calendar_events_ms(access_token) + return jsonify(events_list) + except Exception as e: + return jsonify({"error": str(e)}), 400 + +@main.route("/ms-calendar/event-details") +def ms_calendar_event_details_route(): + access_token = session.get("ms_calendar_access_token") + if not access_token: + return jsonify({"error": "Brak zalogowania do MS Calendar"}), 401 -@main.route('/events') -def events(): + event_id = request.args.get("eventId") + if not event_id: + return jsonify({"error": "Brak parametru eventId"}), 400 + + try: + event_data = get_ms_calendar_event_details_ms(access_token, event_id) + return jsonify(event_data) + except Exception as e: + return jsonify({"error": str(e)}), 400 + +@main.route("/my_ms_calendar") +def ms_calendar_page(): + return render_template("my_ms_calendar.html") + +# GOOGLE CALENDAR INTEGRATION +@main.route("/google-calendar/login") +def google_calendar_login(): + auth_url = get_google_authorize_url() + print("przystanek") + return redirect(auth_url) + +@main.route("/google-calendar/callback") +def google_calendar_callback(): + code = request.args.get("code") + print(code) + if not code: + return "Brak parametru 'code'", 400 try: - events = get_calendar_events() - return render_template('events.html', events=events) + creds = exchange_code_for_token2(code) + # Zapisz w session + session["google_creds"] = { + "token": creds.token, + "refresh_token": creds.refresh_token, + "token_uri": creds.token_uri, + "client_id": creds.client_id, + "client_secret": creds.client_secret, + "scopes": creds.scopes + } + return render_template("my_google_calendar.html") except Exception as e: - return jsonify({"error": str(e)}), 500 + return f"Błąd: {e}", 400 + +@main.route("/google-calendar/events") +def google_calendar_events(): + if "google_creds" not in session: + return jsonify({"error": "Niezalogowany w Google Calendar"}), 401 + creds_info = session["google_creds"] + creds = Credentials(**creds_info) -@main.route("/ms-calendar") -def ms_calendar(): try: - # Zastąp odpowiednimi wartościami - client_id = "your_client_id" - tenant_id = "your_tenant_id" - client_secret = "your_client_secret" - token = "access_token" + events = get_google_events(creds) + return jsonify(events) + except Exception as e: + return jsonify({"error": str(e)}), 400 + +@main.route("/google-calendar/event-details") +def google_calendar_event_details(): + if "google_creds" not in session: + return jsonify({"error": "Niezalogowany w Google Calendar"}), 401 - events = get_ms_calendar_events(client_id, tenant_id, client_secret, token) - return {"events": events} + event_id = request.args.get("eventId") + if not event_id: + return jsonify({"error": "Brak parametru eventId"}), 400 + + creds_info = session["google_creds"] + creds = Credentials(**creds_info) + + try: + event = get_google_event_details(creds, event_id) + return jsonify(event) except Exception as e: - return {"error": str(e)}, 400 + return jsonify({"error": str(e)}), 400 + +@main.route("/my_google_calendar") +def google_calendar_page(): + return render_template("my_google_calendar.html") +# OBSŁUGA NAGRYWANIA +@main.route("/record") +def record(): + return render_template("record.html") + @main.route("/list_windows", methods=["GET"]) def list_windows(): """Zwróć listę okien.""" @@ -51,12 +387,6 @@ def list_windows(): windows = [w for w in windows if w] return jsonify(windows) - -UPLOAD_FOLDER = os.path.join(os.getcwd(), 'recordings') -SCREENSHOT_FOLDER = os.path.join(UPLOAD_FOLDER, 'screenshots') -ALLOWED_EXTENSIONS = {'webm', 'mp4', 'avi'} - - @main.route("/record/record_window", methods=["POST"]) def record_window_route(): setup_upload_folder() @@ -71,7 +401,6 @@ def record_window_route(): return jsonify({"message": f"Rozpoczęto nagrywanie okna: {window_title}"}) - @main.route("/record/stop_recording", methods=["POST"]) def stop_recording_route(): try: @@ -80,21 +409,21 @@ def stop_recording_route(): except Exception as e: return jsonify({"message": f"Błąd podczas zatrzymywania nagrywania: {str(e)}"}), 500 - @main.route('/save', methods=['POST']) def save_recording_route(): setup_upload_folder() - """Zapisz nagranie i przekonwertuj na MP4.""" + """Zapisz nagranie i przekonwertuj na MP4, następnie wyciągnij screeny i przetwarzaj audio na notatki.""" try: file = request.files['file'] title = request.form.get('title', 'recording') mp4_path, wav_path = save_recording(file, title) + # Zrzuty ekranu screenshots_folder = os.path.join(SCREENSHOT_FOLDER, title) os.makedirs(screenshots_folder, exist_ok=True) + extract_screenshots_from_video(mp4_path, screenshots_folder, fps=1) - extract_screenshots_from_video(mp4_path, screenshots_folder, fps=1 / 10) # Jedna klatka co 10 sekund - + # Generowanie notatek (DOCX) docx_folder = os.path.join(UPLOAD_FOLDER, 'notes') os.makedirs(docx_folder, exist_ok=True) process_audio_and_save_transcription(UPLOAD_FOLDER, docx_folder, screenshots_folder) @@ -107,7 +436,6 @@ def save_recording_route(): except Exception as e: return jsonify({'error': 'Nieoczekiwany błąd.', 'details': str(e)}), 500 - @main.route('/my_recordings') def show_recordings(): setup_upload_folder() @@ -116,7 +444,6 @@ def show_recordings(): recordings = [os.path.splitext(file)[0] for file in recordings] return render_template('my_recordings.html', recordings=recordings) - @main.route('/recordings/') def get_recording(filename): try: @@ -125,24 +452,7 @@ def get_recording(filename): return "File not found", 404 -@main.route('/my_notes') -def show_notes(): - docx_folder = os.path.join(os.getcwd(), 'recordings', - 'notes') # Upewnij się, że folder istnieje w 'recordings/notes' - if not os.path.exists(docx_folder): - return render_template('my_notes.html', notes=[]) - - # Pobierz pliki `.docx` z folderu - notes = [f for f in os.listdir(docx_folder) if f.endswith('.docx')] - notes = [os.path.splitext(file)[0] for file in notes] - return render_template('my_notes.html', notes=notes) - - -@main.route('/debug/recordings') -def debug_recordings(): - return str(os.listdir(UPLOAD_FOLDER)) - - +# OBSŁUGA NOTATEK @main.route('/generate_notes', methods=['POST']) def generate_notes(): """Generuj transkrypcję notatek na podstawie istniejącego pliku .wav.""" @@ -162,8 +472,10 @@ def generate_notes(): # Generowanie transkrypcji process_audio_and_save_transcription(wav_path, notes_folder) - return jsonify({'message': f"Transkrypcja wygenerowana dla {title}.wav!", - 'path': os.path.join(notes_folder, f"{title}.docx")}) + return jsonify({ + 'message': f"Transkrypcja wygenerowana dla {title}.wav!", + 'path': os.path.join(notes_folder, f"{title}.docx") + }) except ValueError as e: return jsonify({'error': str(e)}), 400 except RuntimeError as e: @@ -172,14 +484,218 @@ def generate_notes(): return jsonify({'error': 'Nieoczekiwany błąd.', 'details': str(e)}), 500 +# WYSZUKIWANIE W NOTATKACH +def search_in_docx(file_path, query): + """Sprawdza, czy dany plik .docx zawiera tekst pasujący do zapytania.""" + try: + doc = Document(file_path) + for para in doc.paragraphs: + if query in para.text.lower(): + return True + except Exception as e: + print(f"Błąd odczytu pliku {file_path}: {e}") + return False + +@main.route('/search_docx') +def search_docx(): + query = request.args.get('query', '').lower() + matching_files = [] + + if not query: + return jsonify([]) + + for filename in os.listdir(NOTES_FOLDER): + if filename.endswith('.docx'): + file_path = os.path.join(NOTES_FOLDER, filename) + if search_in_docx(file_path, query): + matching_files.append(filename.replace(".docx", "")) + + return jsonify(matching_files) + + +# OBSŁUGA WYŚWIETLANIA WYDARZEŃ +@main.route("/my_events") +def events_page(): + return render_template("my_events.html") +@main.route("/my_events2") +def events_page2(): + return render_template("my_events2.html") + + + +# OBSŁUGA E-MAILI I NOTATEK +@main.route("/my_notes") +def show_notes(): + """ + Wyświetla listę wygenerowanych notatek (plików .docx) + oraz listę adresów e-mail z bazy danych. + """ + if not os.path.exists(NOTES_FOLDER): + return render_template('my_notes.html', notes=[], emails=[]) + + notes = [f for f in os.listdir(NOTES_FOLDER) if f.endswith('.docx')] + notes = [os.path.splitext(file)[0] for file in notes] + + all_emails = Email.query.all() + return render_template('my_notes.html', notes=notes, emails=all_emails) + +@main.route("/add_email", methods=["POST"]) +def add_email(): + """ + Dodaje nowy adres e-mail do bazy. + """ + data = request.get_json() + email_value = data.get("email", "").strip() + + if not email_value: + return jsonify({"error": "Nie podano adresu e-mail!"}), 400 + + existing_email = Email.query.filter_by(email=email_value).first() + if existing_email: + return jsonify({"error": "Ten adres e-mail już istnieje!"}), 409 + + try: + new_email = Email(email=email_value) + db.session.add(new_email) + db.session.commit() + return jsonify({"message": "Adres e-mail dodany!"}), 201 + except Exception as e: + db.session.rollback() + return jsonify({"error": "Wystąpił błąd podczas dodawania e-maila."}), 500 + +@main.route("/delete_email", methods=["POST"]) +def delete_email(): + """ + Usuwa wybrane adresy e-mail z bazy danych. + """ + try: + data = request.get_json() + email_addresses = data.get("emails", []) + + if not email_addresses: + return jsonify({"error": "Nie wybrano żadnych e-maili do usunięcia."}), 400 + + for email in email_addresses: + email_obj = Email.query.filter_by(email=email).first() + if email_obj: + db.session.delete(email_obj) + else: + print(f"E-mail {email} nie znaleziony w bazie.") + + db.session.commit() + return jsonify({"message": "E-maile zostały usunięte."}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Błąd podczas usuwania e-maili: {str(e)}"}), 500 + +def is_valid_email(email): + email_regex = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$' + return re.match(email_regex, email) +@main.route('/send_notes', methods=['POST']) +def send_notes(): + data = request.json + emails = data.get('emails', []) # Lista zaznaczonych e-maili + notes = data.get('notes', []) # Lista zaznaczonych notatek + + print("Odebrane e-maile:", emails) + print("Odebrane notatki:", notes) + + if not emails or not notes: + return jsonify({'error': 'Brak wybranych notatek lub odbiorców'}), 400 + invalid_emails = [email for email in emails if not is_valid_email(email)] + if invalid_emails: + return jsonify({'error': f'Nieprawidłowe adresy e-mail: {invalid_emails}'}), 400 + smtp_server = os.getenv('SMTP_SERVER') + smtp_port = os.getenv('SMTP_PORT') + email_username = os.getenv('EMAIL_USERNAME') + email_password = os.getenv('EMAIL_PASSWORD') + + print("SMTP_SERVER:", smtp_server) + print("SMTP_PORT:", smtp_port) + print("EMAIL_USERNAME:", email_username) + + if not smtp_server or not smtp_port or not email_username or not email_password: + return jsonify({'error': 'Brakuje konfiguracji SMTP w pliku .env'}), 500 + + try: + smtp_port = int(smtp_port) + with smtplib.SMTP(smtp_server, smtp_port) as server: + server.starttls() # Uruchom TLS + server.login(email_username, email_password) + + for email in emails: + msg = MIMEMultipart() + msg['From'] = email_username + msg['To'] = email + msg['Subject'] = "Wybrane notatki" + + body = "Załączam wybrane notatki jako pliki." + msg.attach(MIMEText(body, 'plain')) + + for note in notes: + file_path = os.path.join(NOTES_FOLDER, f"{note}.docx") + if os.path.exists(file_path): + with open(file_path, "rb") as attachment: + part = MIMEApplication(attachment.read(), Name=f"{note}.docx") + part['Content-Disposition'] = f'attachment; filename="{note}.docx"' + msg.attach(part) + else: + print(f"Plik {file_path} nie istnieje i nie zostanie dołączony.") + + server.sendmail(email_username, email, msg.as_string()) + + return jsonify({'message': 'Notatki wysłane pomyślnie!'}), 200 + + except ValueError as ve: + print("Błąd konfiguracji SMTP:", ve) + return jsonify({'error': str(ve)}), 500 + except Exception as e: + print("Błąd podczas wysyłania e-maili:", e) + return jsonify({'error': 'Wystąpił błąd podczas wysyłania e-maili'}), 500 + + + @main.route('/notes/') def get_note(filename): - docx_folder = os.path.join(os.getcwd(), 'recordings', 'notes') + """ + Pobranie wygenerowanej notatki jako pliku do ściągnięcia. + """ try: - return send_from_directory(docx_folder, filename + ".docx", as_attachment=True) # Pobierz plik jako załącznik + return send_from_directory(NOTES_FOLDER, filename + ".docx", as_attachment=True) except FileNotFoundError: return "File not found", 404 +@main.route("/send_invitations", methods=["POST"]) +def send_invitations(): + data = request.json + recipients = data.get("recipients", []) + subject = data.get("subject", "Zaproszenie") + if not recipients: + return jsonify({"error": "Nie podano odbiorców"}), 400 + + try: + # serwer smtp + smtp_server = os.getenv('SMTP_SERVER') + smtp_port = int(os.getenv('SMTP_PORT', 587)) + email_username = os.getenv('EMAIL_USERNAME') + email_password = os.getenv('EMAIL_PASSWORD') + + if not smtp_server or not email_username or not email_password: + return jsonify({"error": "Brakuje konfiguracji SMTP w .env"}), 500 + with smtplib.SMTP(smtp_server, smtp_port) as server: + server.starttls() + server.login(email_username, email_password) + + for recipient in recipients: + message = f"Subject: {subject}\n\nZapraszamy na wydarzenie!" + server.sendmail(email_username, recipient, message) + + return jsonify({"message": "Wiadomości wysłane pomyślnie!"}), 200 + except Exception as e: + return jsonify({"error": f"Błąd podczas wysyłania: {str(e)}"}), 500 -if __name__ == "__main__": - main.run(debug=True) \ No newline at end of file +# Debug / test +@main.route('/debug/recordings') +def debug_recordings(): + return str(os.listdir(UPLOAD_FOLDER)) diff --git a/apka/app/backend/screenshot.py b/apka/app/backend/screenshot.py index a4bd95d..3629e30 100644 --- a/apka/app/backend/screenshot.py +++ b/apka/app/backend/screenshot.py @@ -14,8 +14,6 @@ def calculate_image_hash(image_path): return str(imagehash.average_hash(image)) - - def is_duplicate_image(new_image_path, last_image_path): """ Sprawdza, czy nowy obraz jest duplikatem ostatniego obrazu na podstawie hashy. @@ -55,7 +53,7 @@ def detect_presentation_area(image_path): print(f"Błąd podczas wykrywania obszaru prezentacji: {e}") -def extract_screenshots_from_video(video_path, output_folder, fps=1): +def extract_screenshots_from_video(video_path, output_folder, fps=10): """ Generuje unikalne zrzuty ekranu z wideo w regularnych odstępach czasu. """ diff --git a/apka/app/backend/summary.py b/apka/app/backend/summary.py new file mode 100644 index 0000000..8b1d814 --- /dev/null +++ b/apka/app/backend/summary.py @@ -0,0 +1,108 @@ +from transformers import pipeline +from docx import Document +import os + + +# Inicjalizacja modelu do podsumowywania +summarizer = pipeline("summarization", model="facebook/bart-large-cnn") + +def split_text(text, max_length=1000): + + words = text.split(" ") + segments = [] + segment = [] + + for word in words: + segment.append(word) + if len(" ".join(segment)) > max_length: + segments.append(" ".join(segment)) + segment = [] + + if segment: + segments.append(" ".join(segment)) + + return segments + + +def generate_summary(docx_path, summary_docx_path): + """ + Tworzy podsumowanie treści pliku .docx i zapisuje je w nowym pliku. + """ + try: + if not os.path.exists(docx_path): + print(f"Plik {docx_path} nie istnieje.") + return + + doc = Document(docx_path) + full_text = "\n".join([para.text for para in doc.paragraphs]) + + full_text = "\n".join([para.text for para in doc.paragraphs]) + + # Usunięcie "Transkrypcja wykładu:" oraz wszystkiego po "Zrzuty ekranu:" + start_marker = "Transkrypcja wykładu:" + end_marker = "Zrzuty ekranu:" + + if start_marker in full_text: + full_text = full_text.split(start_marker, 1)[-1] # Pobiera wszystko po "Transkrypcja wykładu:" + + if end_marker in full_text: + full_text = full_text.split(end_marker, 1)[0] # Usuwa wszystko po "Zrzuty ekranu:" + + if len(full_text.strip()) < 50: + print(f"Plik {docx_path} zawiera za mało treści do podsumowania.") + return + + # Dzielimy tekst na mniejsze fragmenty + segments = split_text(full_text, max_length=1000) + if not segments: + print(f"Brak segmentów do podsumowania dla {docx_path}") + return + + summarized_segments = [] + print(f"📄 Przetwarzanie pliku: {docx_path}") + print(f"📜 Oryginalny tekst ({len(full_text)} znaków):\n{full_text[:1000]}...\n") # Podgląd pierwszych 1000 znaków + print(f"🔍 Podzielony na {len(segments)} segmentów") + + for i, segment in enumerate(segments): + print(f"📝 Segment {i} ({len(segment)} znaków): {segment[:300]}...") # Podgląd pierwszych 300 znaków segmentu + if len(segment.strip()) == 0: + print(f"Pominięto pusty segment {i} w {docx_path}") + continue + + try: + summary = summarizer(segment, max_length=500, min_length=200, do_sample=False) + if summary and 'summary_text' in summary[0]: + summarized_segments.append(summary[0]['summary_text']) + else: + print(f"Nie udało się wygenerować podsumowania dla segmentu {i} w {docx_path}") + except Exception as e: + print(f"Błąd w summarizerze dla segmentu {i}: {e}") + + if not summarized_segments: + print(f"Nie wygenerowano żadnych podsumowań dla {docx_path}") + return + + # Połączenie podsumowań w jeden dokument + final_summary = "\n\n".join(summarized_segments) + + # Tworzenie nowego pliku z podsumowaniem + summary_doc = Document() + summary_doc.add_paragraph("Podsumowanie:", style='Heading 1') + summary_doc.add_paragraph(final_summary) + summary_doc.save(summary_docx_path) + + print(f"Zapisano podsumowanie do: {summary_docx_path}") + + if os.path.exists(docx_path): + os.remove(docx_path) + print(f"Usunięto plik tymczasowy: {docx_path}") + + except Exception as e: + print(f"Błąd podczas generowania podsumowania dla {docx_path}: {e}") + +if __name__ == "__main__": + # Testowanie na jednym pliku + test_file = "C:\\Users\\ola_a\\Documents\\GitHub\\projekt_2025_once again\\apka\\app\\recordings\\notes\\2025-02-01_14-05-17.docx" + summary_output = "C:\\Users\\ola_a\\Documents\\GitHub\\projekt_2025_once again\\apka\\app\\recordings\\summary_2025-02-01_14-05-17.docx" + + generate_summary(test_file, summary_output) diff --git a/apka/app/backend/teams_integration.py b/apka/app/backend/teams_integration.py new file mode 100644 index 0000000..18621d5 --- /dev/null +++ b/apka/app/backend/teams_integration.py @@ -0,0 +1,60 @@ +import os +import requests +import base64 +from dotenv import load_dotenv + +load_dotenv() + +TENANT_ID = os.getenv("TEAMS_TENANT_ID") +CLIENT_ID = os.getenv("TEAMS_CLIENT_ID") +CLIENT_SECRET = os.getenv("TEAMS_CLIENT_SECRET") +TEAMS_REDIRECT_URI = os.getenv("TEAMS_REDIRECT_URI", "http://localhost:5000/teams/callback") + + +def exchange_code_for_token(code): + """ + Wymienia code (otrzymany w /teams/callback) na access_token. + """ + url_token = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token" + data = { + "client_id": CLIENT_ID, + "scope": "Calendars.Read offline_access User.Read", + "code": code, + "redirect_uri": TEAMS_REDIRECT_URI, + "grant_type": "authorization_code", + "client_secret": CLIENT_SECRET + } + + resp = requests.post(url_token, data=data) + token_json = resp.json() + if "access_token" not in token_json: + raise Exception(f"Błąd przy wymianie code na token: {token_json}") + + return token_json # Zwraca np. { "access_token": "...", "refresh_token": "...", ... } + +def get_teams_events(access_token): + """ + Pobiera listę eventów z kalendarza (Teams/Outlook). + W Microsoft Graph: GET https://graph.microsoft.com/v1.0/me/events + """ + url = "https://graph.microsoft.com/v1.0/me/events" + headers = {"Authorization": f"Bearer {access_token}"} + resp = requests.get(url, headers=headers) + data = resp.json() + if "value" not in data: + raise Exception(f"Błąd podczas pobierania eventów: {data}") + + return data["value"] # lista eventów + +def get_teams_event_details(access_token, event_id): + """ + Pobiera szczegółowe dane wybranego eventu po id, w tym attendees. + GET /me/events/{event_id} + """ + url = f"https://graph.microsoft.com/v1.0/me/events/{event_id}" + headers = {"Authorization": f"Bearer {access_token}"} + resp = requests.get(url, headers=headers) + data = resp.json() + if "id" not in data: + raise Exception(f"Nie znaleziono eventu lub błąd: {data}") + return data diff --git a/apka/app/backend/token.json b/apka/app/backend/token.json deleted file mode 100644 index c540d63..0000000 --- a/apka/app/backend/token.json +++ /dev/null @@ -1 +0,0 @@ -{"token": "ya29.a0ARW5m74CZNbqtORp_wZD-ltT6nAcaSKmtr4WaUBGA7AOBytfb0F5rzSZvFkHLBtpKUJP2q3oeM3g9aU1J6qIa0tfDP2z3b07PcPAJnZoobldDZP6TDTF5bP_U3IaDjVy5amNqxspqIhyCkP-Txnd3H91Bh_xTixMNnqpiDseaCgYKAUkSARASFQHGX2MiVh8scH16nncKhyDuiWFFDA0175", "refresh_token": "1//09n9qsVDbf9xPCgYIARAAGAkSNwF-L9IrRuHlGuccqUZYbi5ePop6H_zkWyCqkaZYxeQadSK-3ZU4qhMIeN9w9__PxqdLaXQl38A", "token_uri": "https://oauth2.googleapis.com/token", "client_id": "907999041253-2eqttdeuvd1inb1oh6a81vptdk1sh9ao.apps.googleusercontent.com", "client_secret": "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", "scopes": ["https://www.googleapis.com/auth/calendar.readonly"], "universe_domain": "googleapis.com", "account": "", "expiry": "2025-01-23T16:10:40.589250Z"} \ No newline at end of file diff --git a/apka/app/backend/zoom_integration.py b/apka/app/backend/zoom_integration.py new file mode 100644 index 0000000..5196728 --- /dev/null +++ b/apka/app/backend/zoom_integration.py @@ -0,0 +1,142 @@ +# zoom_integration.py +import os +import base64 +import requests +from dotenv import load_dotenv + +load_dotenv() + +ZOOM_CLIENT_ID = os.getenv("ZOOM_CLIENT_ID") +print("ZOOM_CLIENT_ID z .env to:", ZOOM_CLIENT_ID) # Debug +ZOOM_CLIENT_SECRET = os.getenv("ZOOM_CLIENT_SECRET") +ZOOM_REDIRECT_URI = os.getenv("ZOOM_REDIRECT_URI") + +def get_zoom_authorize_url(): + """ + Zwraca URL do którego przekierujesz użytkownika, + by uzyskać "code" w klasycznym OAuth2 flow. + """ + base_url = "https://zoom.us/oauth/authorize" + # Gdy user wchodzi na ten link, Zoom pyta go o zgodę i na końcu + # wywołuje /zoom/callback?code=XYZ + return f"{base_url}?response_type=code&client_id={ZOOM_CLIENT_ID}&redirect_uri={ZOOM_REDIRECT_URI}" + +def exchange_code_for_token(code): + """ + Otrzymuje "code" z query param w URL i wymienia go na access_token. + """ + url_token = "https://zoom.us/oauth/token" + # W OAuth user-level parametry: + # grant_type=authorization_code + # code=... + # redirect_uri=... + params = { + "grant_type": "authorization_code", + "code": code, + "redirect_uri": ZOOM_REDIRECT_URI, + } + + # Zoom wymaga Basic Auth z (client_id:client_secret) + creds = f"{ZOOM_CLIENT_ID}:{ZOOM_CLIENT_SECRET}" + b64_creds = base64.b64encode(creds.encode()).decode() + headers = { + "Authorization": f"Basic {b64_creds}" + } + + resp = requests.post(url_token, headers=headers, params=params) + data = resp.json() + if "access_token" not in data: + raise Exception(f"Błąd wymiany code na token: {data}") + + # data zawiera: + # { + # "access_token": "...", + # "token_type": "bearer", + # "refresh_token": "...", + # "expires_in": 3599, + # ... + # } + return data # Zwracamy cały JSON (możesz zapisać go w sesji, bazie, itp.) + +def get_zoom_meetings(access_token): + """ + Pobiera listę spotkań użytkownika Zoom + korzystając z otrzymanego access_token. + """ + url = "https://api.zoom.us/v2/users/me/meetings" + headers = { + "Authorization": f"Bearer {access_token}" + } + resp = requests.get(url, headers=headers) + data = resp.json() + if "meetings" not in data: + raise Exception(f"Błąd podczas pobierania spotkań: {data}") + + return data["meetings"] # Zwracamy listę meetingów + +def get_personal_meeting_details(access_token, personal_meeting_id): + """ + Pobiera szczegóły konkretnego spotkania, np. personal meeting ID, + jeśli chcesz np. /meetings/. + """ + url = f"https://api.zoom.us/v2/meetings/{personal_meeting_id}" + headers = { + "Authorization": f"Bearer {access_token}" + } + resp = requests.get(url, headers=headers) + data = resp.json() + if "id" not in data: + raise Exception(f"Błąd podczas pobierania spotkania: {data}") + return data + +def get_zoom_participants(meeting_id, access_token): + url = f"https://api.zoom.us/v2/report/meetings/{meeting_id}/participants" + headers = { + "Authorization": f"Bearer {access_token}" + } + resp = requests.get(url, headers=headers) + data = resp.json() + # Gdy spotkanie jest jeszcze w toku, to 'report' nie zadziała + # lub jeśli brakuje licencji/pro planu - sprawdź w doc. Zoom + if "participants" not in data: + # jeśli chcesz zwracać pustą listę zamiast błędu: + return [] + return data["participants"] # np. [{user_email: "..."}] + +def get_zoom_authorize_url(): + base_url = "https://zoom.us/oauth/authorize" + # Debug: sprawdź, co zostało wczytane + print("ZOOM_CLIENT_ID =", ZOOM_CLIENT_ID) + print("ZOOM_REDIRECT_URI =", ZOOM_REDIRECT_URI) + + return ( + f"{base_url}" + f"?response_type=code" + f"&client_id={ZOOM_CLIENT_ID}" + f"&redirect_uri={ZOOM_REDIRECT_URI}" + ) + +def exchange_code_for_token(code): + url_token = "https://zoom.us/oauth/token" + params = { + "grant_type": "authorization_code", + "code": code, + "redirect_uri": ZOOM_REDIRECT_URI, + } + creds = f"{ZOOM_CLIENT_ID}:{ZOOM_CLIENT_SECRET}" + b64_creds = base64.b64encode(creds.encode()).decode() + headers = { + "Authorization": f"Basic {b64_creds}" + } + + print("DEBUG: Wysyłamy request do Zoom z parametrami:", params) + print("DEBUG: Authorization:", headers["Authorization"]) + + resp = requests.post(url_token, headers=headers, params=params) + data = resp.json() + print("DEBUG: Odpowiedź Zoom:", data['access_token']) + + if "access_token" not in data: + raise Exception(f"Błąd wymiany code na token: {data['access_token']}") + + return data['access_token'] diff --git a/apka/app/frontend/static/events.js b/apka/app/frontend/static/events.js new file mode 100644 index 0000000..b4e6a3d --- /dev/null +++ b/apka/app/frontend/static/events.js @@ -0,0 +1,30 @@ +document.getElementById("load-teams").addEventListener("click", function() { + fetch("/teams-events") + .then(resp => resp.json()) + .then(events => { + const list = document.getElementById("eventsList"); + list.innerHTML = ""; + events.forEach(evt => { + const li = document.createElement("li"); + li.textContent = `Tytuł: ${evt.title}, start: ${evt.start}, uczestnicy: ${evt.attendees.join(", ")}`; + list.appendChild(li); + }); + }) + .catch(err => console.error(err)); + }); + + document.getElementById("load-zoom").addEventListener("click", function() { + fetch("/zoom-meetings") + .then(resp => resp.json()) + .then(meetings => { + const list = document.getElementById("eventsList"); + list.innerHTML = ""; + meetings.forEach(m => { + const li = document.createElement("li"); + li.textContent = `Spotkanie: ${m.topic}, start: ${m.start_time}, link: ${m.join_url}`; + list.appendChild(li); + }); + }) + .catch(err => console.error(err)); + }); + \ No newline at end of file diff --git a/apka/app/frontend/static/my_notes.js b/apka/app/frontend/static/my_notes.js new file mode 100644 index 0000000..c8627e4 --- /dev/null +++ b/apka/app/frontend/static/my_notes.js @@ -0,0 +1,160 @@ +function searchItems(type) { + const input = document.getElementById(type === 'note' ? 'noteSearchBox' : 'emailSearchBox').value.toLowerCase(); + const searchInContent = type === 'note' ? document.getElementById('searchInContent').checked : false; + const listItems = document.querySelectorAll(`.${type === 'note' ? 'notes-list' : 'email-list'} .list-item`); + + listItems.forEach(item => item.style.display = ''); + + if (!searchInContent || type !== 'note') { + listItems.forEach(item => { + const text = item.querySelector('span').textContent.toLowerCase(); + if (!text.includes(input)) { + item.style.display = 'none'; + } + }); + } else { + fetch(`/search_docx?query=${encodeURIComponent(input)}`) + .then(response => response.json()) + .then(matchingNotes => { + const matchingSet = new Set(matchingNotes); + listItems.forEach(item => { + const text = item.querySelector('span a').textContent; + if (!matchingSet.has(text)) { + item.style.display = 'none'; + } + }); + }) + .catch(error => { + console.error("Błąd podczas wyszukiwania w treści:", error); + }); + } +} + + + +function deleteItems(type) { + if (type !== 'email') return; + + const selectedEmails = Array.from(document.querySelectorAll('input[name="email_checkbox"]:checked')).map(cb => cb.value); + + if (selectedEmails.length === 0) { + alert("Wybierz co najmniej jeden e-mail do usunięcia."); + return; + } + + fetch('/delete_email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ emails: selectedEmails }), + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + alert(`Błąd: ${data.error}`); + } else { + alert("E-maile zostały usunięte."); + selectedEmails.forEach(email => { + const checkbox = document.querySelector(`input[value="${email}"]`); + if (checkbox) { + const listItem = checkbox.closest('.list-item'); + listItem.remove(); + } + }); + } + }) + .catch(error => { + console.error("Błąd podczas usuwania e-maili:", error); + alert("Wystąpił błąd podczas usuwania e-maili."); + }); +} + + + + function addEmail() { + const emailInput = document.getElementById('newEmail').value.trim(); + + if (!emailInput) { + alert("Proszę wprowadzić adres e-mail."); + return; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(emailInput)) { + alert("Nieprawidłowy adres e-mail!"); + return; + } + + fetch('/add_email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email: emailInput }), + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + alert(`Błąd: ${data.error}`); + } else { + alert("Adres e-mail dodany pomyślnie!"); + const emailList = document.querySelector('.email-list'); + const listItem = document.createElement('li'); + listItem.classList.add('list-item'); + listItem.innerHTML = ` + ${emailInput} + + `; + emailList.appendChild(listItem); + + document.getElementById('newEmail').value = ""; + } + }) + .catch(error => { + console.error("Błąd podczas dodawania e-maila:", error); + alert("Wystąpił błąd podczas dodawania e-maila."); + }); +} + + +function sendSelected() { + const selectedEmails = Array.from(document.querySelectorAll('input[name="email_checkbox"]:checked')).map(cb => cb.value); + const selectedNotes = Array.from(document.querySelectorAll('input[name="note_checkbox"]:checked')).map(cb => cb.value); + + if (selectedEmails.length === 0 || selectedNotes.length === 0) { + alert("Wybierz co najmniej jeden adres e-mail oraz jedną notatkę."); + return; + } + + fetch('/send_notes', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ emails: selectedEmails, notes: selectedNotes }), + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + alert(`Błąd: ${data.error}`); + } else { + alert("Notatki zostały pomyślnie wysłane!"); + } + }) + .catch(error => { + console.error("Błąd podczas wysyłania notatek:", error); + alert("Wystąpił błąd podczas wysyłania notatek."); + }); +} +document.addEventListener("DOMContentLoaded", function () { + const backToHomeButton = document.querySelector(".back-to-home"); + + if (backToHomeButton) { + backToHomeButton.addEventListener("click", function (event) { + console.log("Kliknięto przycisk powrotu!"); + event.preventDefault(); + window.location.href = "/"; + }); + } +}); diff --git a/apka/app/frontend/static/notes.js b/apka/app/frontend/static/notes.js index ed6badf..c30df82 100644 --- a/apka/app/frontend/static/notes.js +++ b/apka/app/frontend/static/notes.js @@ -1,34 +1,35 @@ -function loadNotes() { - const notesList = document.getElementById('notes-list'); - - fetch('/my_notes') - .then(response => { - if (!response.ok) { - throw new Error('Nie udało się załadować notatek.'); - } - return response.json(); // Zwraca listę plików w formacie JSON - }) - .then(notes => { - if (notes.length === 0) { - notesList.innerHTML = '
  • Brak notatek do wyświetlenia.
  • '; - return; - } - - notes.forEach(note => { - const noteItem = document.createElement('li'); - const noteLink = document.createElement('a'); - - noteLink.href = `/notes/${note}`; - noteLink.textContent = note; - noteLink.download = note; // Dodanie możliwości pobrania pliku - - noteItem.appendChild(noteLink); - notesList.appendChild(noteItem); - }); - }) - .catch(error => { - notesList.innerHTML = `
  • Błąd: ${error.message}
  • `; - }); -} - -document.addEventListener('DOMContentLoaded', loadNotes); +function loadNotes() { + const notesList = document.getElementById('notes-list'); + + fetch('/my_notes') + .then(response => { + if (!response.ok) { + throw new Error('Nie udało się załadować notatek.'); + } + return response.json(); // Zwraca listę plików w formacie JSON + }) + .then(notes => { + if (notes.length === 0) { + notesList.innerHTML = '
  • Brak notatek do wyświetlenia.
  • '; + return; + } + + notes.forEach(note => { + const noteItem = document.createElement('li'); + const noteLink = document.createElement('a'); + + noteLink.href = `/notes/${note}`; + noteLink.textContent = note; + noteLink.download = note; // Dodanie możliwości pobrania pliku + + noteItem.appendChild(noteLink); + notesList.appendChild(noteItem); + }); + }) + .catch(error => { + notesList.innerHTML = `
  • Błąd: ${error.message}
  • `; + }); +} + +document.addEventListener('DOMContentLoaded', loadNotes); + diff --git a/apka/app/frontend/static/styles.css b/apka/app/frontend/static/styles.css index 9ed5edc..29ada4d 100644 --- a/apka/app/frontend/static/styles.css +++ b/apka/app/frontend/static/styles.css @@ -61,7 +61,7 @@ button:disabled { background-color: gray; } -#events-btn, #recordings-btn, #notes-btn, #record-btn{ +#recordings-btn, #notes-btn, #record-btn{ background-color: rgb(226, 199, 218); margin-top: 10px; color: black; @@ -71,12 +71,92 @@ button:disabled { transition: background-color 0.3s, transform 0.3s; } -#record-btn:hover, #events-btn:hover, #notes-btn:hover,#recordings-btn:hover { +#record-btn:hover, #notes-btn:hover,#recordings-btn:hover { background-color: #ea72c8; transform: scale(1.2); } -#record-btn:active, #events-btn:active, #notes-btn:active, #recordings-btn:active { +#record-btn:active, #notes-btn:active, #recordings-btn:active { background-color: #9e4f8a; -} \ No newline at end of file +} + +#zoom-btn { + background-color: rgb(226, 178, 212); + margin-top: 10px; + color: black; + padding: 10px 20px; + font-size: 20px; + cursor: pointer; + transition: background-color 0.3s, transform 0.3s; +} + +#teams-btn { + background-color: rgb(226, 178, 212); + margin-top: 10px; + color: black; + padding: 10px 20px; + font-size: 20px; + cursor: pointer; + transition: background-color 0.3s, transform 0.3s; +} + +#cal-btn { + background-color: rgb(226, 178, 212); + margin-top: 10px; + color: black; + padding: 10px 20px; + font-size: 20px; + cursor: pointer; + transition: background-color 0.3s, transform 0.3s; +} + +#google-btn { + background-color: rgb(226, 178, 212); + margin-top: 10px; + color: black; + padding: 10px 20px; + font-size: 20px; + cursor: pointer; + transition: background-color 0.3s, transform 0.3s; +} +#zoom-btn:hover { + background-color: #ea72c8; + transform: scale(1.2); +} + +#teams-btn:hover { + background-color: #ea72c8; + transform: scale(1.2); +} +#cal-btn:hover { + background-color: #ea72c8; + transform: scale(1.2); +} + +#google-btn:hover { + background-color: #ea72c8; + transform: scale(1.2); +} +#zoom-btn:active { + background-color: #9e4f8a; +} + +#teams-btn:active { + background-color: #9e4f8a; +} + +#cal-btn:active { + background-color: #9e4f8a; +} + +#google-btn:active { + background-color: #9e4f8a; +} +input[type="text"] { + padding: 10px; + margin: 10px; + width: 300px; + border: 1px solid #ccc; + border-radius: 5px; + } diff --git a/apka/app/frontend/static/styles2.css b/apka/app/frontend/static/styles2.css index 7ed41d4..66e7954 100644 --- a/apka/app/frontend/static/styles2.css +++ b/apka/app/frontend/static/styles2.css @@ -160,3 +160,35 @@ body { background-color: darkred; box-shadow: 0 0 5px rgba(0, 0, 0, 0.8); } + +#searchBox { + width: 80%; + padding: 10px; + margin: 10px 0; + border: 2px solid #ddd; + border-radius: 8px; + font-size: 16px; + outline: none; + transition: all 0.3s ease-in-out; +} + +#searchBox:focus { + border-color: #007bff; + box-shadow: 0 0 8px rgba(0, 123, 255, 0.5); +} + +.search-container { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 15px; +} + +#searchInside { + margin-top: 5px; +} + +label { + font-size: 14px; + cursor: pointer; +} \ No newline at end of file diff --git a/apka/app/frontend/static/stylesEvents.css b/apka/app/frontend/static/stylesEvents.css index f368af6..62c6354 100644 --- a/apka/app/frontend/static/stylesEvents.css +++ b/apka/app/frontend/static/stylesEvents.css @@ -1,80 +1,124 @@ -body { - font-family: Arial, sans-serif; - display: flex; - justify-content: flex-start; - align-items: flex-start; - height: 100vh; - margin: 0; - background-image: url('tlo.png'); - background-size: cover; - background-position: center; - background-attachment: fixed; -} - -.container { - width: 60%; - height: 50vh; - overflow-y: auto; - position: relative; - top: 15%; - left: 7%; - background-color: rgba(139, 98, 149, 0.6); - box-shadow: 0 0 10px rgba(167, 17, 201, 0.204); - padding: 20px; - border-radius: 10px; - margin: 20px; - text-align: center; -} - -h1 { - font-size: 2rem; - margin-bottom: 20px; -} - -.events-list { - display: flex; - flex-direction: column; - gap: 10px; - margin-top: 20px; -} - -.event { - background-color: #e9e9e9; - padding: 15px; - border-radius: 8px; - text-align: left; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.event-time { - font-size: 1.1rem; - color: #555; -} - -.event-summary { - font-size: 1.3rem; - font-weight: bold; - color: #333; -} - -.top-nav { - position: absolute; - top: 20px; - right: 20px; -} - -.back-to-home { - background-color: #572468; - color: white; - padding: 10px 20px; - text-decoration: none; - border-radius: 15px; - font-size: 16px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); - transition: transform 0.2s ease, background-color 0.2s ease; -} - -.back-to-home:hover { - background-color: #7f4598; - transform: scale(1.2); -} \ No newline at end of file +body { + font-family: Arial, sans-serif; + background-image: url('tlo.png'); + background-size: cover; + background-position: center; + background-attachment: fixed; + margin: 0; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + flex-direction: column; +} + +.container { + width: 55%; + height: 50vh; + padding: 20px; + text-align: center; + background-color: rgba(139, 98, 149, 0.6); + border-radius: 10px; + box-shadow: 0 0 10px rgba(167, 17, 201, 0.204); + overflow-y: auto; + position: relative; + top: 20%; + right: 14%; + transform: translateY(-50%); +} + +h1 { + font-size: 2rem; + margin-bottom: 20px; + color: white; +} + +.btn { + background-color: #572468; + color: white; + padding: 10px 20px; + border: none; + border-radius: 15px; + font-size: 16px; + cursor: pointer; + margin: 10px; + transition: transform 0.2s ease, background-color 0.2s ease; +} + +.btn:hover { + background-color: #7f4598; + transform: scale(1.1); +} + +.events-list { + margin-top: 20px; + max-height: 300px; + overflow-y: auto; + background: rgba(255, 255, 255, 0.8); + border-radius: 10px; + padding: 10px; +} + +ul { + list-style: none; + padding: 0; +} + +li { + background-color: #e9e9e9; + padding: 10px; + border-radius: 5px; + margin: 5px 0; + display: flex; + justify-content: space-between; + align-items: center; +} + +.details-btn { + background-color: #3498db; + color: white; + border: none; + padding: 5px 10px; + border-radius: 5px; + cursor: pointer; +} + +.details-btn:hover { + background-color: #2980b9; +} + +.send-email-btn { + background-color: #27ae60; + color: white; + border: none; + padding: 10px; + border-radius: 5px; + cursor: pointer; + margin-top: 10px; +} + +.send-email-btn:hover { + background-color: #219150; +} + +.top-nav { + position: absolute; + top: 20px; + right: 20px; +} + +.back-to-home { + background-color: #572468; + color: white; + padding: 10px 20px; + text-decoration: none; + border-radius: 15px; + font-size: 16px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); + transition: transform 0.2s ease, background-color 0.2s ease; +} + +.back-to-home:hover { + background-color: #7f4598; + transform: scale(1.2); +} diff --git a/apka/app/frontend/static/stylesRecord.css b/apka/app/frontend/static/stylesRecord.css index 90096f4..07822e0 100644 --- a/apka/app/frontend/static/stylesRecord.css +++ b/apka/app/frontend/static/stylesRecord.css @@ -179,3 +179,4 @@ input[type="text"] { justify-content: center; font-weight: bold; } + \ No newline at end of file diff --git a/apka/app/frontend/static/styles_do_my_notes.css b/apka/app/frontend/static/styles_do_my_notes.css new file mode 100644 index 0000000..216167e --- /dev/null +++ b/apka/app/frontend/static/styles_do_my_notes.css @@ -0,0 +1,274 @@ +/* Ogólne ustawienia strony */ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-image: url('tlo.png'); + background-size: cover; + background-position: center; + background-attachment: fixed; +} + +/* Główna sekcja */ +.main-container { + display: flex; + justify-content: space-between; + align-items: flex-start; + width: 85%; + max-width: 1300px; + gap: 30px; + position: relative; + padding-bottom: 120px; + padding-top: 80px; +} + +/* Fioletowy kontener (Notatki) */ +.notes-section { + flex: 1; + padding: 20px; + background-color: rgba(139, 98, 149, 0.8); + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); + height: 65vh; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; +} + +/* Niebieski kontener (Maile) */ +.email-panel { + flex: 1; + padding: 20px; + background-color: rgba(135, 206, 250, 0.8); + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); + height: 65vh; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; +} + +/* Nagłówki */ +.notes-section h1, .email-panel h1 { + text-align: center; + color: white; + margin-bottom: 15px; +} + +/* Styl przewijanego okna (notatki i e-maile) */ +.scrollable-list { + width: 100%; + max-height: 250px; + overflow-y: auto; + background-color: rgba(255, 255, 255, 0.9); + padding: 10px; + border-radius: 8px; + margin-top: 10px; +} + +/* Pasek przewijania */ +.scrollable-list::-webkit-scrollbar { + width: 8px; +} + +.scrollable-list::-webkit-scrollbar-thumb { + background-color: rgba(139, 98, 149, 0.8); + border-radius: 8px; +} + +.scrollable-list::-webkit-scrollbar-track { + background-color: rgba(255, 255, 255, 0.8); +} + +/* Lista elementów (notatki i e-maile) */ +.list-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px; + border-radius: 5px; + background: rgba(255, 255, 255, 0.2); + margin-bottom: 5px; +} + +/* Tekst po lewej */ +.list-item span { + flex: 1; + text-align: left; + color: black; +} + +/* Checkbox (dla e-maili i notatek) */ +.list-item input[type="checkbox"], +.list-item input[type="radio"] { + margin-right: 5px; + transform: scale(1.2); + appearance: none; + border: 2px solid #572468; + border-radius: 50%; + width: 18px; + height: 18px; + outline: none; + background-color: white; + cursor: pointer; + transition: background-color 0.2s ease, box-shadow 0.2s ease; +} + +.list-item input[type="checkbox"]:checked, +.list-item input[type="radio"]:checked { + background-color: #572468; + box-shadow: 0 0 4px #572468; +} + +/* Styl linków */ +.list-item span a { + text-decoration: underline; + color: #572468; +} + +.list-item span a:hover { + color: #ffcccc; +} + +/* Styl sekcji wyszukiwania */ +.search-container { + margin-bottom: 15px; + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.search-container input { + width: 80%; + padding: 8px; + border: 1px solid #ccc; + border-radius: 5px; + margin-bottom: 10px; +} + +.search-container input:focus { + border-color: #007bff; + box-shadow: 0 0 8px rgba(0, 123, 255, 0.5); +} + +/* Styl dla checkboxa i etykiety */ + +.search-checkbox { + display: flex; + align-items: center; + gap: 5px; + margin-top: 5px; + width: auto; + white-space: nowrap; +} +.search-checkbox input[type="checkbox"] { + margin: 0; + transform: scale(1.2); +} + +.search-checkbox label { + font-size: 14px; + color: white; + margin: 0; + line-height: 1.2; + white-space: nowrap; +} + +/* Pole "Dodaj nowy email" */ +.email-actions { + display: flex; + gap: 10px; + margin-bottom: 30px; + width: 80%; + position: left; +} + +.add-email-button { + color: blue; + background-color: pink; +} + +.email-actions input { + flex: 2; + padding: 10px; + font-size: 14px; + border-radius: 8px; + border: 1px solid #ccc; +} + +.email-actions input:focus { + border-color: #007bff; + box-shadow: 0 0 8px rgba(0, 123, 255, 0.5); +} + +/* Styl przycisków */ +button { + padding: 10px 15px; + font-size: 14px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background 0.2s ease-in-out; +} + +button:hover { + background-color: #0056b3; + color: white; +} + +/* Przyciski nawigacji */ +#deleteEmails { + background-color: #572468; + color: white; + margin-top: 10px; +} + +#deleteEmails:hover { + background-color: #7f4598; +} + +#sendButton { + position: absolute; + bottom: 50px; + left: 50%; + transform: translateX(-50%); + background-color: red; + color: white; + font-size: 18px; + padding: 15px 40px; + border-radius: 25px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); +} + +#sendButton:hover { + background-color: #b30000; +} + +/* Styl przycisku powrotu */ +.top-nav { + position: absolute; + top: 20px; + right: 20px; +} + +.back-to-home { + background-color: #572468; + color: white; + padding: 10px 20px; + text-decoration: none; + border-radius: 15px; + font-size: 16px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); + transition: transform 0.2s ease, background-color 0.2s ease; +} + +.back-to-home:hover { + background-color: #7f4598; + transform: scale(1.2); +} diff --git a/apka/app/frontend/templates/events.html b/apka/app/frontend/templates/events.html deleted file mode 100644 index e35c533..0000000 --- a/apka/app/frontend/templates/events.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - NoteWriter - - - - - -
    -

    Twoje nadchodzące wydarzenia

    -
    - {% if events %} - {% for event in events %} -
    -
    {{ event['start'] }}
    -
    {{ event['summary'] }}
    -
    - {% endfor %} - {% else %} -

    Brak nadchodzących wydarzeń.

    - {% endif %} -
    -
    - - diff --git a/apka/app/frontend/templates/index.html b/apka/app/frontend/templates/index.html index 9056b46..8c96afe 100644 --- a/apka/app/frontend/templates/index.html +++ b/apka/app/frontend/templates/index.html @@ -10,9 +10,6 @@ diff --git a/apka/app/frontend/templates/my_events.html b/apka/app/frontend/templates/my_events.html new file mode 100644 index 0000000..f133fe2 --- /dev/null +++ b/apka/app/frontend/templates/my_events.html @@ -0,0 +1,117 @@ + + + + + + NoteWriter + + + + + +
    +

    Twoje spotkania Zoom

    + + + +
    +
      +
      +
      + + + + diff --git a/apka/app/frontend/templates/my_events2.html b/apka/app/frontend/templates/my_events2.html new file mode 100644 index 0000000..04868bb --- /dev/null +++ b/apka/app/frontend/templates/my_events2.html @@ -0,0 +1,112 @@ + + + + + + NoteWriter + + + + + +
      +

      Twoje wydarzenia Teams

      + + +
      +
        +
        +
        + + + diff --git a/apka/app/frontend/templates/my_google_calendar.html b/apka/app/frontend/templates/my_google_calendar.html new file mode 100644 index 0000000..c4050a5 --- /dev/null +++ b/apka/app/frontend/templates/my_google_calendar.html @@ -0,0 +1,117 @@ + + + + + + NoteWriter + + + + + +
        +

        Twoje wydarzenia Google Calendar

        + + + +
        +
          +
          +
          + + + + diff --git a/apka/app/frontend/templates/my_ms_calendar.html b/apka/app/frontend/templates/my_ms_calendar.html new file mode 100644 index 0000000..4c8bb4c --- /dev/null +++ b/apka/app/frontend/templates/my_ms_calendar.html @@ -0,0 +1,84 @@ + + + + + + NoteWriter + + + + + +
          +

          Wydarzenia w MS Calendar

          + + +
            +
            + + + + diff --git a/apka/app/frontend/templates/my_notes.html b/apka/app/frontend/templates/my_notes.html index ca5c5aa..68feca9 100644 --- a/apka/app/frontend/templates/my_notes.html +++ b/apka/app/frontend/templates/my_notes.html @@ -1,28 +1,75 @@ - - - - - - Moje Notatki - - - - - - - -
            -

            Moje Notatki

            -
              - {% for note in notes %} -
            • - {{ note }} -
            • - {% endfor %} -
            -
            - - - + + + + + + Moje Notatki + + + + + + + + +
            + +
            +

            Moje Notatki

            + +
            + +
            + + +
            +
            + + +
            +
              + {% for note in notes %} +
            • + {{ note }} + +
            • + {% endfor %} +
            +
            +
            + + + +
            + +
            + +
            + + diff --git a/apka/app/frontend/templates/my_recordings.html b/apka/app/frontend/templates/my_recordings.html index e8dfc28..406155b 100644 --- a/apka/app/frontend/templates/my_recordings.html +++ b/apka/app/frontend/templates/my_recordings.html @@ -10,7 +10,7 @@
            diff --git a/apka/app/frontend/templates/record.html b/apka/app/frontend/templates/record.html index b3480fb..1980a3e 100644 --- a/apka/app/frontend/templates/record.html +++ b/apka/app/frontend/templates/record.html @@ -30,4 +30,3 @@

            Zapisz nagranie

            - \ No newline at end of file diff --git a/apka/app/instance/database.db b/apka/app/instance/database.db new file mode 100644 index 0000000..3cd277c Binary files /dev/null and b/apka/app/instance/database.db differ diff --git a/apka/app/main.py b/apka/app/main.py index 413c67f..99f88a5 100644 --- a/apka/app/main.py +++ b/apka/app/main.py @@ -6,4 +6,5 @@ app = create_app() if __name__ == "__main__": - app.run(debug=True, port=8080) + + app.run(debug=True, port=5000) diff --git a/apka/app/requirements.txt b/apka/app/requirements.txt index 287283f..091e9a1 100644 --- a/apka/app/requirements.txt +++ b/apka/app/requirements.txt @@ -23,6 +23,7 @@ einops==0.8.0 ffmpeg-python==0.2.0 filelock==3.16.1 Flask==3.1.0 +Flask-SQLAlchemy==3.1.1 fonttools==4.55.3 frozenlist==1.5.0 fsspec==2024.10.0 @@ -137,6 +138,7 @@ torch-audiomentations==0.11.1 torch_pitch_shift==1.2.5 torchaudio==2.5.1 torchmetrics==1.6.1 +transformers tqdm==4.67.1 typer==0.15.1 typing_extensions==4.12.2 diff --git a/dokumentacja/api_endpoints.md b/dokumentacja/api_endpoints.md index 83fd35e..03ce6e8 100644 --- a/dokumentacja/api_endpoints.md +++ b/dokumentacja/api_endpoints.md @@ -1,492 +1,771 @@ -# API endpoints - -**Wersja:** 2.2 - -**Data utworzenia:** 2025-01-25 T09:12:31Z - -**Data ostatniej aktualizacji:** 2025-02-05 T19:49:28Z - -## `/` - -### Opis -- renderuje stronę główną - -### Metoda - -`GET` - -## `/record` - -### Opis -- renderuje podstronę do nagrywania - -### Metoda - -`GET` - -## `/record/record_window` - -### Opis -- rozpoczyna nagrywanie wybranego okna w osobnym wątku - -### Metoda -`POST` - -### Nagłówki -- `Content-Type: application/json` - -### Body (JSON) -| Pole | Typ | Wymagane | Opis | -|----------------|---------|----------|------------------------------------------| -| `window_title` | string | Tak | Tytuł okna, które ma być nagrywane. | - -#### Przykład zapytania -```json -{ - "window_title": "Mój Dokument - Microsoft Word" -} -``` - -#### Odpowiedzi - -**_Sukces (200 OK)_** - -```json -{ - "message": "Rozpoczęto nagrywanie okna: Mój Dokument - Microsoft Word" -} -``` - -**_Błąd (400 Bad Request)_** - -Jeśli nie podano `window_title`: - -```json -{ - "message": "Nie podano tytułu okna." -} -``` - -## `/record/stop_recording` - -### Opis -- zatrzymuje aktualnie trwające nagrywanie - -### Metoda -`POST` - -### Nagłówki -- brak wymaganych nagłówków - -#### Przykład zapytania -- brak danych w treści zapytania - -#### Odpowiedzi - -**_Sukces (200 OK)_** - -```json -{ - "message": "Nagrywanie zakończone pomyślnie." -} -``` - -**_Błąd (500 Internal Server Error)_** - -W przypadku nieoczekiwanego błędu: - -```json -{ - "message": "Błąd podczas zatrzymywania nagrywania: " -} -``` - -## `/record/save` - -### Opis -- zapisuje nagranie przesłane jako plik i konwertuje je na format `mp4` - dodatkowo wyzwala generowanie transkrypcji w formacie `.docx` - -### Metoda - -`POST` - -### Nagłówki - -- `Content-Type: multipart/form-data` - -### Body (JSON) - -| Pole | Typ | Wymagane | Opis | -|----------------|---------|----------|-----------------------------------------------------------------------------| -| `file` | file | Tak | Nagranie video do zapisania (format: `.webm`) | -| `title` | string | Nie | Opcjonalna nazwa pliku - jeśli nie podano, używana jest bieżąca data i czas | - -#### Odpowiedzi - -**_Sukces (200 OK)_** - -``` -Strona HTML z komunikatem: "Nagranie zapisane!". -``` - -**_Błąd (400 Bad Request)_** - -Jeśli brak pliku: - -```json -{ - "error": "Nie przekazano żadnego pliku!" -} -``` - -**_Błąd (500 Internal Server Error)_** - -W przypadku błędu konwersji: - -```json -{ - "error": "Konwersja do MP4 nie powiodła się." -} -``` - -## `/my_recordings` - -### Opis - -- zwraca listę zapisanych nagrań `mp4` - -### Metoda - -`GET` - -### Nagłówki - -- brak wymaganych nagłówków - -#### Przykład zapytania - -- brak danych w treści zapytania - -#### Odpowiedzi - -**_Sukces (200 OK)_** - -``` -Strona HTML z listą nagrań wideo. -``` - -## `/events` - -### Opis - -- pobiera wydarzenia z Google Calendar - -### Metoda - -`GET` - -#### Odpowiedzi - -**_Sukces (200 OK)_** - -``` -Lista wydarzeń -``` - -**_500 Internal Server Error_** - -- jeśli wystąpi błąd podczas pobierania wydarzeń - -## `/list_windows` - -### Opis - -- zwraca listę nazw wszystkich otwartych okien - -### Metoda - -`GET` - -#### Odpowiedzi - -**_Sukces (200 OK)_** - -``` -Lista tytułów okien -``` - -## `/generate_notes` - -### Opis - -- generuje transkrypcję tekstową z istniejącego pliku `.wav` - -### Metoda - -`POST` - -### Nagłówki - -- `Content-Type: application/x-www-form-urlencoded` - -### Body (JSON) - -| Pole | Typ | Wymagane | Opis | -|----------------|---------|----------|-----------------------------------------------------------| -| `title` | string | Tak | Nazwa pliku `.wav` (bez rozszerzenia) do przetworzenia | - -#### Odpowiedzi - -**_Sukces (200 OK)_** - -```json -{ - "message": "Transkrypcja wygenerowana pomyślnie." -} -``` - -**_Błąd (400 Bad Request)_** - -Jeśli brak nazwy pliku: - -```json -{ - "error": "Brak nazwy pliku! Podaj tytuł pliku WAV." -} -``` - -**_Błąd (404 Not Found)_** - -Jeśli plik `.wav` nie istnieje: - -```json -{ - "error": "Plik moje_nagranie.wav nie istnieje w katalogu recordings!" -} -``` - -## `/my_notes` - -### Opis - -- wyświetla listę wygenerowanych notatek `.docx` - -### Metoda - -`GET` - -#### Odpowiedzi - -**_Sukces (200 OK)_** - -``` -Lista notatek w formie HTML -``` - -## `/zoom/login` - -### Opis - -- przekierowuje użytkownika do logowania w Zoom, aby uzyskać autoryzację - -### Metoda - -`GET` - -## `/zoom-meetings` - -### Opis - -- pobiera listę spotkań użytkownika Zoom po uzyskaniu tokena autoryzacyjnego - -### Metoda - -`GET` - -#### Odpowiedzi - -**_Sukces (200 OK)_** - -``` -Lista spotkań w formacie JSON -``` - -## `/zoom-meeting-participants` - -### Opis - -- pobiera uczestników danego spotkania Zoom - -### Metoda - -`GET` - -### Body (JSON) - -| Pole | Typ | Wymagane | Opis | -|----------------|---------|----------|--------------------------------| -| `meetingId` | string | Tak | Identyfikator spotkania Zoom | - -#### Odpowiedzi - -**_Sukces (200 OK)_** - -``` -Lista uczestników -``` - -## `/teams/login` - -### Opis - -- przekierowuje użytkownika do logowania w Microsoft Teams - -### Metoda - -`GET` - -## `/teams-events` - -### Opis - -- pobiera listę wydarzeń z kalendarza Microsoft Teams po autoryzacji - -### Metoda - -`GET` - -#### Odpowiedzi - -**_Sukces (200 OK)_** - -``` -Lista wydarzeń w formacie JSON -``` - -## `/teams-event-details` - -### Opis - -- pobiera szczegółowe informacje o wybranym wydarzeniu w Teams - -### Metoda - -`GET` - -### Body (JSON) - -| Pole | Typ | Wymagane | Opis | -|----------------|---------|----------|--------------------------------| -| `eventId` | string | Tak | Identyfikator wydarzenia Teams | - -#### Odpowiedzi - -**_Sukces (200 OK)_** - -``` -Szczegółowe informacje o wydarzeniu w formacie JSON -``` - -## `/google-calendar/login` - -### Opis - -- generuje link do logowania w Google, aby uzyskać autoryzację do kalendarza - -### Metoda - -`GET` - -## `/google-calendar/events` - -### Opis - -- pobiera listę wydarzeń z Google Calendar przy użyciu autoryzowanych danych - -### Metoda - -`GET` - -#### Odpowiedzi - -**_Sukces (200 OK)_** - -``` -Lista wydarzeń w formacie JSON -``` - -## `/google-calendar/event-details` - -### Opis - -- pobiera szczegółowe informacje o wybranym wydarzeniu z Google Calendar - -### Metoda - -`GET` - -### Body (JSON) - -| Pole | Typ | Wymagane | Opis | -|----------------|---------|----------|--------------------------------| -| `eventId` | string | Tak | Identyfikator wydarzenia | - -#### Odpowiedzi - -**_Sukces (200 OK)_** - -``` -Szczegóły wydarzenia w formacie JSON -``` - -## `/ms-calendar/login` - -### Opis - -- generuje link do logowania w MS Calendar - -### Metoda - -`GET` - -## `/ms-calendar/events` - -### Opis - -- pobiera listę wydarzeń z MS Calendar przy użyciu danych autoryzacyjnych - -### Metoda - -`GET` - -#### Odpowiedzi - -**_Sukces (200 OK)_** - -``` -Lista wydarzeń w formacie JSON -``` - -## `/ms-calendar/event-details` - -### Opis - --pobiera szczegółowe informacje o wybranym wydarzeniu z MS Calendar, w tym listę uczestników - -### Metoda - -`GET` - -### Body (JSON) - -| Pole | Typ | Wymagane | Opis | -|----------------|---------|----------|--------------------------------| -| `eventId` | string | Tak | Identyfikator wydarzenia | - -#### Odpowiedzi - -**_Sukces (200 OK)_** - -``` -Szczegółowe informacje o wydarzeniu w formacie JSON -``` +# API endpoints + +## `/` + +### Opis +Renderuje stronę główną + +### Metoda + +`GET` + +## `/record` + +### Opis +Renderuje podstronę do nagrywania + +### Metoda + +`GET` + +## `/record/record_window` + +### Opis +Rozpoczyna nagrywanie wybranego okna aplikacji w osobnym wątku + +### Metoda +`POST` + +### Nagłówki +- `Content-Type: application/json` + +### Body (JSON) +| Pole | Typ | Wymagane | Opis | +|----------------|---------|----------|------------------------------------------| +| `window_title` | string | Tak | Tytuł okna, które ma być nagrywane. | + +#### Przykład zapytania +```json +{ + "window_title": "Mój Dokument - Microsoft Word" +} +``` + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +```json +{ + "message": "Rozpoczęto nagrywanie okna: Mój Dokument - Microsoft Word" +} +``` + +**_Błąd (400 Bad Request)_** + +Jeśli nie podano `window_title`: + +```json +{ + "message": "Nie podano tytułu okna." +} +``` + +## `/record/stop_recording` + +### Opis +Zatrzymuje aktualnie trwające nagrywanie + +### Metoda +`POST` + +### Nagłówki +- brak wymaganych nagłówków + +#### Przykład zapytania +- brak danych w treści zapytania + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +```json +{ + "message": "Nagrywanie zakończone pomyślnie." +} +``` + +**_Błąd (500 Internal Server Error)_** + +W przypadku nieoczekiwanego błędu: + +```json +{ + "message": "Błąd podczas zatrzymywania nagrywania: " +} +``` + +## `/record/save` + +### Opis +Zapisuje nagranie przesłane jako plik i konwertuje je na format `mp4` - dodatkowo wyzwala generowanie transkrypcji w formacie `.docx` + +### Metoda + +`POST` + +### Nagłówki + +- `Content-Type: multipart/form-data` + +### Body (JSON) + +| Pole | Typ | Wymagane | Opis | +|----------------|---------|----------|-----------------------------------------------------------------------------| +| `file` | file | Tak | Nagranie video do zapisania (format: `.webm`) | +| `title` | string | Nie | Opcjonalna nazwa pliku - jeśli nie podano, używana jest bieżąca data i czas | + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Strona HTML z komunikatem: "Nagranie zapisane!". +``` + +**_Błąd (400 Bad Request)_** + +Jeśli brak pliku: + +```json +{ + "error": "Nie przekazano żadnego pliku!" +} +``` + +**_Błąd (500 Internal Server Error)_** + +W przypadku błędu konwersji: + +```json +{ + "error": "Konwersja do MP4 nie powiodła się." +} +``` + +## `/my_recordings` + +### Opis + +Zwraca listę zapisanych nagrań `mp4` + +### Metoda + +`GET` + +### Nagłówki + +- brak wymaganych nagłówków + +#### Przykład zapytania + +- brak danych w treści zapytania + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Strona HTML z listą nagrań wideo. +``` + +## `/events` + +### Opis + +Pobiera wydarzenia z Google Calendar + +### Metoda + +`GET` + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Lista wydarzeń +``` + +**_500 Internal Server Error_** + +- jeśli wystąpi błąd podczas pobierania wydarzeń + +## `/list_windows` + +### Opis + +Zwraca listę nazw wszystkich otwartych okien + +### Metoda + +`GET` + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Lista tytułów okien +``` + +## `/generate_notes` + +### Opis + +Generuje transkrypcję tekstową z istniejącego pliku `.wav` + +### Metoda + +`POST` + +### Nagłówki + +- `Content-Type: application/x-www-form-urlencoded` + +### Body (JSON) + +| Pole | Typ | Wymagane | Opis | +|----------------|---------|----------|-----------------------------------------------------------| +| `title` | string | Tak | Nazwa pliku `.wav` (bez rozszerzenia) do przetworzenia | + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +```json +{ + "message": "Transkrypcja wygenerowana pomyślnie." +} +``` + +**_Błąd (400 Bad Request)_** + +Jeśli brak nazwy pliku: + +```json +{ + "error": "Brak nazwy pliku! Podaj tytuł pliku WAV." +} +``` + +**_Błąd (404 Not Found)_** + +Jeśli plik `.wav` nie istnieje: + +```json +{ + "error": "Plik moje_nagranie.wav nie istnieje w katalogu recordings!" +} +``` + +## `/my_notes` + +### Opis + +Wyświetla listę wygenerowanych notatek `.docx` + +### Metoda + +`GEt` + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Lista notatek w formie HTML +``` +======= +# API endpoints + +**Wersja:** 2.2 + +**Data utworzenia:** 2025-01-25 T09:12:31Z + +**Data ostatniej aktualizacji:** 2025-02-05 T19:49:28Z + +## `/` + +### Opis +- renderuje stronę główną + +### Metoda + +`GET` + +## `/record` + +### Opis +- renderuje podstronę do nagrywania + +### Metoda + +`GET` + +## `/record/record_window` + +### Opis +- rozpoczyna nagrywanie wybranego okna w osobnym wątku + +### Metoda +`POST` + +### Nagłówki +- `Content-Type: application/json` + +### Body (JSON) +| Pole | Typ | Wymagane | Opis | +|----------------|---------|----------|------------------------------------------| +| `window_title` | string | Tak | Tytuł okna, które ma być nagrywane. | + +#### Przykład zapytania +```json +{ + "window_title": "Mój Dokument - Microsoft Word" +} +``` + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +```json +{ + "message": "Rozpoczęto nagrywanie okna: Mój Dokument - Microsoft Word" +} +``` + +**_Błąd (400 Bad Request)_** + +Jeśli nie podano `window_title`: + +```json +{ + "message": "Nie podano tytułu okna." +} +``` + +## `/record/stop_recording` + +### Opis +- zatrzymuje aktualnie trwające nagrywanie + +### Metoda +`POST` + +### Nagłówki +- brak wymaganych nagłówków + +#### Przykład zapytania +- brak danych w treści zapytania + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +```json +{ + "message": "Nagrywanie zakończone pomyślnie." +} +``` + +**_Błąd (500 Internal Server Error)_** + +W przypadku nieoczekiwanego błędu: + +```json +{ + "message": "Błąd podczas zatrzymywania nagrywania: " +} +``` + +## `/record/save` + +### Opis +- zapisuje nagranie przesłane jako plik i konwertuje je na format `mp4` - dodatkowo wyzwala generowanie transkrypcji w formacie `.docx` + +### Metoda + +`POST` + +### Nagłówki + +- `Content-Type: multipart/form-data` + +### Body (JSON) + +| Pole | Typ | Wymagane | Opis | +|----------------|---------|----------|-----------------------------------------------------------------------------| +| `file` | file | Tak | Nagranie video do zapisania (format: `.webm`) | +| `title` | string | Nie | Opcjonalna nazwa pliku - jeśli nie podano, używana jest bieżąca data i czas | + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Strona HTML z komunikatem: "Nagranie zapisane!". +``` + +**_Błąd (400 Bad Request)_** + +Jeśli brak pliku: + +```json +{ + "error": "Nie przekazano żadnego pliku!" +} +``` + +**_Błąd (500 Internal Server Error)_** + +W przypadku błędu konwersji: + +```json +{ + "error": "Konwersja do MP4 nie powiodła się." +} +``` + +## `/my_recordings` + +### Opis + +- zwraca listę zapisanych nagrań `mp4` + +### Metoda + +`GET` + +### Nagłówki + +- brak wymaganych nagłówków + +#### Przykład zapytania + +- brak danych w treści zapytania + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Strona HTML z listą nagrań wideo. +``` + +## `/events` + +### Opis + +- pobiera wydarzenia z Google Calendar + +### Metoda + +`GET` + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Lista wydarzeń +``` + +**_500 Internal Server Error_** + +- jeśli wystąpi błąd podczas pobierania wydarzeń + +## `/list_windows` + +### Opis + +- zwraca listę nazw wszystkich otwartych okien + +### Metoda + +`GET` + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Lista tytułów okien +``` + +## `/generate_notes` + +### Opis + +- generuje transkrypcję tekstową z istniejącego pliku `.wav` + +### Metoda + +`POST` + +### Nagłówki + +- `Content-Type: application/x-www-form-urlencoded` + +### Body (JSON) + +| Pole | Typ | Wymagane | Opis | +|----------------|---------|----------|-----------------------------------------------------------| +| `title` | string | Tak | Nazwa pliku `.wav` (bez rozszerzenia) do przetworzenia | + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +```json +{ + "message": "Transkrypcja wygenerowana pomyślnie." +} +``` + +**_Błąd (400 Bad Request)_** + +Jeśli brak nazwy pliku: + +```json +{ + "error": "Brak nazwy pliku! Podaj tytuł pliku WAV." +} +``` + +**_Błąd (404 Not Found)_** + +Jeśli plik `.wav` nie istnieje: + +```json +{ + "error": "Plik moje_nagranie.wav nie istnieje w katalogu recordings!" +} +``` + +## `/my_notes` + +### Opis + +- wyświetla listę wygenerowanych notatek `.docx` + +### Metoda + +`GET` + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Lista notatek w formie HTML +``` + +## `/zoom/login` + +### Opis + +- przekierowuje użytkownika do logowania w Zoom, aby uzyskać autoryzację + +### Metoda + +`GET` + +## `/zoom-meetings` + +### Opis + +- pobiera listę spotkań użytkownika Zoom po uzyskaniu tokena autoryzacyjnego + +### Metoda + +`GET` + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Lista spotkań w formacie JSON +``` + +## `/zoom-meeting-participants` + +### Opis + +- pobiera uczestników danego spotkania Zoom + +### Metoda + +`GET` + +### Body (JSON) + +| Pole | Typ | Wymagane | Opis | +|----------------|---------|----------|--------------------------------| +| `meetingId` | string | Tak | Identyfikator spotkania Zoom | + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Lista uczestników +``` + +## `/teams/login` + +### Opis + +- przekierowuje użytkownika do logowania w Microsoft Teams + +### Metoda + +`GET` + +## `/teams-events` + +### Opis + +- pobiera listę wydarzeń z kalendarza Microsoft Teams po autoryzacji + +### Metoda + +`GET` + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Lista wydarzeń w formacie JSON +``` + +## `/teams-event-details` + +### Opis + +- pobiera szczegółowe informacje o wybranym wydarzeniu w Teams + +### Metoda + +`GET` + +### Body (JSON) + +| Pole | Typ | Wymagane | Opis | +|----------------|---------|----------|--------------------------------| +| `eventId` | string | Tak | Identyfikator wydarzenia Teams | + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Szczegółowe informacje o wydarzeniu w formacie JSON +``` + +## `/google-calendar/login` + +### Opis + +- generuje link do logowania w Google, aby uzyskać autoryzację do kalendarza + +### Metoda + +`GET` + +## `/google-calendar/events` + +### Opis + +- pobiera listę wydarzeń z Google Calendar przy użyciu autoryzowanych danych + +### Metoda + +`GET` + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Lista wydarzeń w formacie JSON +``` + +## `/google-calendar/event-details` + +### Opis + +- pobiera szczegółowe informacje o wybranym wydarzeniu z Google Calendar + +### Metoda + +`GET` + +### Body (JSON) + +| Pole | Typ | Wymagane | Opis | +|----------------|---------|----------|--------------------------------| +| `eventId` | string | Tak | Identyfikator wydarzenia | + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Szczegóły wydarzenia w formacie JSON +``` + +## `/ms-calendar/login` + +### Opis + +- generuje link do logowania w MS Calendar + +### Metoda + +`GET` + +## `/ms-calendar/events` + +### Opis + +- pobiera listę wydarzeń z MS Calendar przy użyciu danych autoryzacyjnych + +### Metoda + +`GET` + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Lista wydarzeń w formacie JSON +``` + +## `/ms-calendar/event-details` + +### Opis + +-pobiera szczegółowe informacje o wybranym wydarzeniu z MS Calendar, w tym listę uczestników + +### Metoda + +`GET` + +### Body (JSON) + +| Pole | Typ | Wymagane | Opis | +|----------------|---------|----------|--------------------------------| +| `eventId` | string | Tak | Identyfikator wydarzenia | + +#### Odpowiedzi + +**_Sukces (200 OK)_** + +``` +Szczegółowe informacje o wydarzeniu w formacie JSON +``` diff --git a/instance/database.db b/instance/database.db new file mode 100644 index 0000000..429fa1f Binary files /dev/null and b/instance/database.db differ diff --git a/model.py b/model.py index 6d4821e..f1e01a7 100644 --- a/model.py +++ b/model.py @@ -1,46 +1,46 @@ -import torch -from whisper import load_model -import torchaudio -import os -from pydub import AudioSegment - -# Inicjalizacja modelu -model = load_model("base") # Model Whisper - -def load_and_convert_audio(audio_path): - """Funkcja wczytująca plik audio, konwertując go na WAV (16000 Hz, mono), jeśli jest to plik MP3""" - file_extension = os.path.splitext(audio_path)[1].lower() - - # Jeśli plik jest MP3, konwertujemy go do WAV - if file_extension == '.mp3': - wav_path = audio_path.replace('.mp3', '.wav') - audio = AudioSegment.from_mp3(audio_path) - audio = audio.set_frame_rate(16000) # Ustawienie częstotliwości próbkowania na 16000 - audio = audio.set_channels(1) # Ustawienie liczby kanałów na 1 (mono) - audio.export(wav_path, format="wav") - audio_path = wav_path # Ustawiamy ścieżkę na nowo skonwertowany plik WAV - - signal, sample_rate = torchaudio.load(audio_path) - # Jeśli wymagana jest zmiana częstotliwości próbkowania - if sample_rate != 16000: - signal = torchaudio.transforms.Resample(orig_freq=sample_rate, new_freq=16000)(signal) - return signal, 16000 - -def transcribe_audio(audio_path): - """Funkcja do transkrypcji mowy na tekst""" - signal, sample_rate = load_and_convert_audio(audio_path) - transcription = model.transcribe(audio_path) - return transcription - -def process_audio(audio_path): - """Główna funkcja przetwarzająca plik audio""" - transcription = transcribe_audio(audio_path) - return transcription - -if __name__ == "__main__": - audio_file = r"audio_files\test2.mp3" # ścieżka do pliku audio - try: - transcription = process_audio(audio_file) - print("Transkrypcja:", transcription) - except Exception as e: - print(f"Błąd: {e}") +import torch +from whisper import load_model +import torchaudio +import os +from pydub import AudioSegment + +# Inicjalizacja modelu +model = load_model("base") # Model Whisper + +def load_and_convert_audio(audio_path): + """Funkcja wczytująca plik audio, konwertując go na WAV (16000 Hz, mono), jeśli jest to plik MP3""" + file_extension = os.path.splitext(audio_path)[1].lower() + + # Jeśli plik jest MP3, konwertujemy go do WAV + if file_extension == '.mp3': + wav_path = audio_path.replace('.mp3', '.wav') + audio = AudioSegment.from_mp3(audio_path) + audio = audio.set_frame_rate(16000) # Ustawienie częstotliwości próbkowania na 16000 + audio = audio.set_channels(1) # Ustawienie liczby kanałów na 1 (mono) + audio.export(wav_path, format="wav") + audio_path = wav_path # Ustawiamy ścieżkę na nowo skonwertowany plik WAV + + signal, sample_rate = torchaudio.load(audio_path) + # Jeśli wymagana jest zmiana częstotliwości próbkowania + if sample_rate != 16000: + signal = torchaudio.transforms.Resample(orig_freq=sample_rate, new_freq=16000)(signal) + return signal, 16000 + +def transcribe_audio(audio_path): + """Funkcja do transkrypcji mowy na tekst""" + signal, sample_rate = load_and_convert_audio(audio_path) + transcription = model.transcribe(audio_path) + return transcription + +def process_audio(audio_path): + """Główna funkcja przetwarzająca plik audio""" + transcription = transcribe_audio(audio_path) + return transcription + +if __name__ == "__main__": + audio_file = r"audio_files\test2.mp3" # ścieżka do pliku audio + try: + transcription = process_audio(audio_file) + print("Transkrypcja:", transcription) + except Exception as e: + print(f"Błąd: {e}") diff --git a/projekt_2025/.idea/workspace.xml b/projekt_2025/.idea/workspace.xml new file mode 100644 index 0000000..616713d --- /dev/null +++ b/projekt_2025/.idea/workspace.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + 1738708733184 + + + + \ No newline at end of file diff --git a/testy/test_routes.py b/testy/test_routes.py index 4e8c570..d880a99 100644 --- a/testy/test_routes.py +++ b/testy/test_routes.py @@ -1,18 +1,18 @@ -import unittest -from main import app - -class TestRoutes(unittest.TestCase): - def setUp(self): - self.client = app.test_client() - - def test_index(self): - response = self.client.get("/") - self.assertEqual(response.status_code, 200) - - def test_google_calendar(self): - response = self.client.get("/google-calendar") - self.assertIn("error", response.get_json()) - - def test_ms_calendar(self): - response = self.client.get("/ms-calendar") - self.assertIn("error", response.get_json()) +import unittest +from main import app + +class TestRoutes(unittest.TestCase): + def setUp(self): + self.client = app.test_client() + + def test_index(self): + response = self.client.get("/") + self.assertEqual(response.status_code, 200) + + def test_google_calendar(self): + response = self.client.get("/google-calendar") + self.assertIn("error", response.get_json()) + + def test_ms_calendar(self): + response = self.client.get("/ms-calendar") + self.assertIn("error", response.get_json()) diff --git a/testy/tests.js b/testy/tests.js index 70b4d86..e78b709 100644 --- a/testy/tests.js +++ b/testy/tests.js @@ -1,56 +1,56 @@ -import { Selector } from 'testcafe'; - -fixture('Testowanie strony głównej') - .page('http://127.0.0.1:8080/') - .skipJsErrors(true); - -test('Testowanie ładowania strony głównej', async t => { - await t - .expect(Selector('h1').withText('Witaj w aplikacji!').exists).ok('Strona główna nie załadowała się poprawnie'); -}); - -test('Testowanie przycisku "Nagraj spotkanie"', async t => { - const recordButton = Selector('#record-btn'); - - await t - .click(recordButton) - .expect(Selector('#startButton').exists).ok('Nie można rozpocząć nagrywania'); -}); - -test('Testowanie strony "Moje notatki"', async t => { - const notesButton = Selector('#notes-btn'); - - await t - .click(notesButton) - .expect(Selector('h1').withText('Moje Notatki').visible) - .ok('Nie przekierowano na stronę notatek', { timeout: 5000 }); - - const notesList = Selector('.notes-list li'); - const notesCount = await notesList.count; - - if (notesCount === 0) { - console.log('Brak notatek do wyświetlenia, test zakończony sukcesem.'); - } else { - // Jeśli są notatki, testujemy, czy jest ich więcej niż 0 - await t.expect(notesCount).gt(0, 'Brak notatek do wyświetlenia'); - } -}); - - -test('Testowanie wyświetlania sekcji "Moje nagrania"', async t => { - const myRecordingsButton = Selector('#recordings-btn'); - - await t - .click(myRecordingsButton) - .expect(Selector('h1').withText('Moje Nagrania').exists) - .ok('Nie przekierowano na stronę Moje Nagrania', { timeout: 5000 }); - - const recordingsList = Selector('.recording-list li'); - const recordingsCount = await recordingsList.count; - - if (recordingsCount === 0) { - console.log('Brak nagrań do wyświetlenia, test zakończony sukcesem.'); - } else { - await t.expect(recordingsCount).gt(0, 'Brak nagrań do wyświetlenia'); - } -}); +import { Selector } from 'testcafe'; + +fixture('Testowanie strony głównej') + .page('http://127.0.0.1:8080/') + .skipJsErrors(true); + +test('Testowanie ładowania strony głównej', async t => { + await t + .expect(Selector('h1').withText('Witaj w aplikacji!').exists).ok('Strona główna nie załadowała się poprawnie'); +}); + +test('Testowanie przycisku "Nagraj spotkanie"', async t => { + const recordButton = Selector('#record-btn'); + + await t + .click(recordButton) + .expect(Selector('#startButton').exists).ok('Nie można rozpocząć nagrywania'); +}); + +test('Testowanie strony "Moje notatki"', async t => { + const notesButton = Selector('#notes-btn'); + + await t + .click(notesButton) + .expect(Selector('h1').withText('Moje Notatki').visible) + .ok('Nie przekierowano na stronę notatek', { timeout: 5000 }); + + const notesList = Selector('.notes-list li'); + const notesCount = await notesList.count; + + if (notesCount === 0) { + console.log('Brak notatek do wyświetlenia, test zakończony sukcesem.'); + } else { + // Jeśli są notatki, testujemy, czy jest ich więcej niż 0 + await t.expect(notesCount).gt(0, 'Brak notatek do wyświetlenia'); + } +}); + + +test('Testowanie wyświetlania sekcji "Moje nagrania"', async t => { + const myRecordingsButton = Selector('#recordings-btn'); + + await t + .click(myRecordingsButton) + .expect(Selector('h1').withText('Moje Nagrania').exists) + .ok('Nie przekierowano na stronę Moje Nagrania', { timeout: 5000 }); + + const recordingsList = Selector('.recording-list li'); + const recordingsCount = await recordingsList.count; + + if (recordingsCount === 0) { + console.log('Brak nagrań do wyświetlenia, test zakończony sukcesem.'); + } else { + await t.expect(recordingsCount).gt(0, 'Brak nagrań do wyświetlenia'); + } +}); diff --git a/testy/testy_funkcjonalne.md b/testy/testy_funkcjonalne.md index 6b14e64..432a9d8 100644 --- a/testy/testy_funkcjonalne.md +++ b/testy/testy_funkcjonalne.md @@ -1,77 +1,77 @@ -# Testy wymagań funkcjonalnych - -- jednostkowe -- integracyjne -- akceptacyjne - -## Jednostkowe - -**Cel:** sprawdzenie poprawności działania trzech tras (routes) w naszej aplikacji webowej - - `/` strona główna - - `/google-calendar` wyświetlanie kalendarza Google - - `ms-calendar` kalendarz Microsoft - -Testy te weryfikują, czy aplikacja: - -- zwraca poprawny kod statusu HTTP 200 dla strony głównej -- zwraca odpowiedź zawierającą błąd (w formacie JSON) dla dwóch pozostałych tras, tj. `/google-calendar` i `/ms-calendar` - -**Narzędzie:** framework `unittest` w języku Python - -`\testy\test_routes.py` - -https://github.com/DevStranger/projekt_2025/blob/cebadf649cadffb94e4f25582b5b6d69349d9bbf/testy/test_routes.py - -Testy te zakończyły się powodzeniem ✔ - -## Integracyjne - -### 1. Test integracji przycisków z przekierowaniami ✔ - -**Cel:** sprawdzenie czy przyciski nawigacyjne poprawnie przekierowują użytkownika do odpowiednich stron i czy dane są poprawnie ładowane z backendu - -**Narzędzie:** - (manualnie) - -Kliknięcie przycisku „Moje notatki” przekierowuje użytkownika do strony z listą notatek. Podobnie dla „Moje nagrania”. - -### 2. Test integracji frontendu z backendem (ładowanie danych) - -**Cel:** sprawdzenie czy frontend poprawnie otrzymuje dane z backendu i je wyświetla - -**Narzędzie:** - (manualnie) - -Na stronie "Moje notatki" frontend wysyła zapytanie do backendu o listę dostępnych notatek, a backend zwraca odpowiednią listę. Na stronie "Moje nagrania" frontend wysyła zapytanie o nagrania, a backend zwraca odpowiednie dane. - -### 3. Test integracji z systemem plików - -**Cel:** sprawdzenie czy pliki notatek i nagrań są poprawnie przechowywane i dostępne na serwerze -**Narzędzie:** - (manualnie) - -Plik jest poprawnie zapisany na serwerze w odpowiednim katalogu i odpowiednio konwertowany. Po załadowaniu strony z notatkami użytkownik ma możliwość pobrania tych plików z serwera. - -***Wszystkie powyższ testy zostały także przeprowadzone automatycznie z użyciem narzędzia TestCafe (patrz: testy akceptacyjne)*** - -## Akceptacyjne - -**Cel:** weryfikacja poprawności działania funkcji aplikacji poprzez sprawdzenie jej zachowania w różnych scenariuszach użytkowych - -**Narzędzie:** testcafe - -### Przeprowadzane testy - -`/testy/tests.js` - -https://github.com/DevStranger/projekt_2025/blob/ac420128c920077b187d1cff3820a46d57407100/testy/tests.js - -### Przebieg testów - -![Zrzut ekranu 2025-01-25 163754](https://github.com/user-attachments/assets/6924ec1d-0aeb-4407-8c84-aa8752cc0aae) -![Zrzut ekranu 2025-01-25 162753](https://github.com/user-attachments/assets/bebd74ca-7bc3-45e1-b23f-8c965b412e72) - -### Wyniki testów - -![Zrzut ekranu 2025-01-25 164639](https://github.com/user-attachments/assets/33eb9518-f378-42c6-a2db-072a34dfeb23) - -Po stworzeniu nagrań z użyciem aplikacji: - -![Zrzut ekranu 2025-01-25 164930](https://github.com/user-attachments/assets/db0a8b38-5836-4524-a925-ac1f2055fad0) +# Testy wymagań funkcjonalnych + +- jednostkowe +- integracyjne +- akceptacyjne + +## Jednostkowe + +**Cel:** sprawdzenie poprawności działania trzech tras (routes) w naszej aplikacji webowej + - `/` strona główna + - `/google-calendar` wyświetlanie kalendarza Google + - `ms-calendar` kalendarz Microsoft + +Testy te weryfikują, czy aplikacja: + +- zwraca poprawny kod statusu HTTP 200 dla strony głównej +- zwraca odpowiedź zawierającą błąd (w formacie JSON) dla dwóch pozostałych tras, tj. `/google-calendar` i `/ms-calendar` + +**Narzędzie:** framework `unittest` w języku Python + +`\testy\test_routes.py` + +https://github.com/DevStranger/projekt_2025/blob/cebadf649cadffb94e4f25582b5b6d69349d9bbf/testy/test_routes.py + +Testy te zakończyły się powodzeniem ✔ + +## Integracyjne + +### 1. Test integracji przycisków z przekierowaniami ✔ + +**Cel:** sprawdzenie czy przyciski nawigacyjne poprawnie przekierowują użytkownika do odpowiednich stron i czy dane są poprawnie ładowane z backendu + +**Narzędzie:** - (manualnie) + +Kliknięcie przycisku „Moje notatki” przekierowuje użytkownika do strony z listą notatek. Podobnie dla „Moje nagrania”. + +### 2. Test integracji frontendu z backendem (ładowanie danych) + +**Cel:** sprawdzenie czy frontend poprawnie otrzymuje dane z backendu i je wyświetla + +**Narzędzie:** - (manualnie) + +Na stronie "Moje notatki" frontend wysyła zapytanie do backendu o listę dostępnych notatek, a backend zwraca odpowiednią listę. Na stronie "Moje nagrania" frontend wysyła zapytanie o nagrania, a backend zwraca odpowiednie dane. + +### 3. Test integracji z systemem plików + +**Cel:** sprawdzenie czy pliki notatek i nagrań są poprawnie przechowywane i dostępne na serwerze +**Narzędzie:** - (manualnie) + +Plik jest poprawnie zapisany na serwerze w odpowiednim katalogu i odpowiednio konwertowany. Po załadowaniu strony z notatkami użytkownik ma możliwość pobrania tych plików z serwera. + +***Wszystkie powyższ testy zostały także przeprowadzone automatycznie z użyciem narzędzia TestCafe (patrz: testy akceptacyjne)*** + +## Akceptacyjne + +**Cel:** weryfikacja poprawności działania funkcji aplikacji poprzez sprawdzenie jej zachowania w różnych scenariuszach użytkowych + +**Narzędzie:** testcafe + +### Przeprowadzane testy + +`/testy/tests.js` + +https://github.com/DevStranger/projekt_2025/blob/ac420128c920077b187d1cff3820a46d57407100/testy/tests.js + +### Przebieg testów + +![Zrzut ekranu 2025-01-25 163754](https://github.com/user-attachments/assets/6924ec1d-0aeb-4407-8c84-aa8752cc0aae) +![Zrzut ekranu 2025-01-25 162753](https://github.com/user-attachments/assets/bebd74ca-7bc3-45e1-b23f-8c965b412e72) + +### Wyniki testów + +![Zrzut ekranu 2025-01-25 164639](https://github.com/user-attachments/assets/33eb9518-f378-42c6-a2db-072a34dfeb23) + +Po stworzeniu nagrań z użyciem aplikacji: + +![Zrzut ekranu 2025-01-25 164930](https://github.com/user-attachments/assets/db0a8b38-5836-4524-a925-ac1f2055fad0)