diff --git a/ai-services/background-music-api/config.py b/ai-services/background-music-api/config.py new file mode 100644 index 00000000..958b46a3 --- /dev/null +++ b/ai-services/background-music-api/config.py @@ -0,0 +1,7 @@ +## SECRETS ## +from dotenv import load_dotenv +import os + +storage_account_key = "" +connection_string = "" +container_name = "" diff --git a/ai-services/background-music-api/main.py b/ai-services/background-music-api/main.py new file mode 100644 index 00000000..8f9750b8 --- /dev/null +++ b/ai-services/background-music-api/main.py @@ -0,0 +1,175 @@ +import os +from yt_dlp import YoutubeDL +from yt_dlp.utils import DownloadError +from yt_dlp.extractor import get_info_extractor +from moviepy.editor import VideoFileClip, AudioFileClip, concatenate_audioclips +from spleeter.separator import Separator +import shutil +from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip +from azure.storage.blob import BlobServiceClient +from config import ( + storage_account_key, + connection_string, + container_name, +) +import urllib.parse +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +import math +from pydub import AudioSegment + +app = FastAPI() +export_type = "flac" + + +def utils_add_bg_music(file_path, video_link): + file_name = file_path.replace(".flac", "") + ydl = YoutubeDL({"format": "best"}) + with YoutubeDL( + {"format": "best", "outtmpl": "{}.%(ext)s".format(file_name)} + ) as ydl: + ydl.download([video_link]) + + video = VideoFileClip(file_name + ".mp4") + audio = video.audio + + audio_file = file_name + ".wav" + audio.write_audiofile(audio_file) + audio = AudioFileClip(audio_file) + count = 1 + duration_of_clip = 60 # in seconds, duration of final audio clip + src_duration = math.ceil( + audio.duration + ) # in seconds, the duration of the original audio + audio_file_paths_bg = [] + + for i in range(0, src_duration, duration_of_clip): + ffmpeg_extract_subclip( + audio_file, i, i + 60, targetname=f"{file_name}_{count}.wav" + ) + audio_file_paths_bg.append(f"{file_name}_{count}.wav") + count += 1 + + separator = Separator( + "spleeter:2stems" + ) # Load the 2stems (vocals/accompaniment) model + bg_music = [] + for a_file in audio_file_paths_bg: + # Use Spleeter to separate vocals and accompaniment + separation = separator.separate_to_file(a_file, "output") + temp_file_path = os.path.join( + "output", a_file.split("/")[-1].replace(".wav", "") + ) + bg_music.append(temp_file_path + "/accompaniment.wav") + + final_paths = [] + concatenated_bg_audios = audio_file.replace(".wav", "_bg_final.wav") + clips = [AudioFileClip(c) for c in bg_music] + final_clip_1 = concatenate_audioclips(clips) + final_clip_1.write_audiofile(concatenated_bg_audios) + + sound1 = AudioSegment.from_file(concatenated_bg_audios) + AudioSegment.from_file(file_path.split("/")[-1]).export( + file_path.replace(".flac", ".wav"), format="wav" + ) + sound2 = AudioSegment.from_file( + os.path.join( + file_path.split("/")[-1].replace(".flac", ".wav"), + ) + ) + audio1 = sound1 + audio2 = sound2 + 2 + combined = sound2.overlay(audio1) + + combined.export(file_path.replace(".wav", "_final.wav"), format="wav") + for fname in audio_file_paths_bg: + if os.path.isfile(fname): + os.remove(fname) + + try: + shutil.rmtree("output") + os.remove(concatenated_bg_audios) + os.remove( + os.path.join( + file_path.split("/")[-1].replace(".flac", ".wav"), + ) + ) + os.remove(file_path.replace(".flac", "") + ".mp4") + except OSError as e: + print("Error: %s - %s." % (e.filename, e.strerror)) + return file_path.replace(".wav", "_final.wav") + +def upload_audio_to_azure_blob(file_path, export_type, export): + blob_service_client = BlobServiceClient.from_connection_string(connection_string) + if export == False: + AudioSegment.from_wav(file_path + "final.wav").export( + file_path + "final.flac", format="flac" + ) + full_path_audio = file_path + "final.flac" + blob_client_audio = blob_service_client.get_blob_client( + container=container_name, blob=file_path.split("/")[-1] + ".flac" + ) + else: + full_path_audio = file_path.replace(".flac", "") + "." + export_type + blob_client_audio = blob_service_client.get_blob_client( + container=container_name, + blob=file_path.split("/")[-1].replace(".flac", "") + "." + export_type, + ) + with open(full_path_audio, "rb") as data: + try: + if not blob_client_audio.exists(): + blob_client_audio.upload_blob(data) + print("Audio uploaded successfully!") + print(blob_client_audio.url) + else: + blob_client_audio.delete_blob() + print("Old Audio deleted successfully!") + blob_client_audio.upload_blob(data) + print("New audio uploaded successfully!") + except Exception as e: + print("This audio can't be uploaded") + return blob_client_audio.url + + +if __name__ == "__main__": + os.mkdir("temporary_audio_storage") + +class BGMusicRequest(BaseModel): + azure_audio_url: str + youtube_url:str + +def download_from_azure_blob(file_path): + blob_service_client = BlobServiceClient.from_connection_string(connection_string) + encoded_file_path = file_path.split("/")[-1] + encoded_url_path = urllib.parse.unquote(encoded_file_path) + blob_client = blob_service_client.get_blob_client( + container=container_name, blob=encoded_url_path + ) + with open(file=file_path.split("/")[-1], mode="wb") as sample_blob: + download_stream = blob_client.download_blob() + sample_blob.write(download_stream.readall()) + + +@app.post("/add_background_music") +async def add_background_music(audio_request: BGMusicRequest): + url = audio_request.azure_audio_url + youtube_url = audio_request.youtube_url + print("Downloading") + download_from_azure_blob(str(url)) + print("Downloaded") + file_path = url.split("/")[-1] + file_path_music = utils_add_bg_music(file_path, youtube_url) + AudioSegment.from_file(file_path_music).export( + file_path.split("/")[-1].replace(".flac", "") + "." + export_type, + format=export_type, + ) + azure_url_audio = upload_audio_to_azure_blob( + file_path, export_type, export=True + ) + try: + os.remove(file_path) + os.remove(file_path.split("/")[-1].replace(".flac", "") + "." + export_type) + except: + print("Error in removing files") + return {"status": "Success", "output": azure_url_audio} diff --git a/ai-services/background-music-api/requirements.txt b/ai-services/background-music-api/requirements.txt new file mode 100644 index 00000000..4a1b7417 --- /dev/null +++ b/ai-services/background-music-api/requirements.txt @@ -0,0 +1,11 @@ +pydub +webvtt-py +yt-dlp +ffmpeg +gevent +webrtcvad +fastapi[all] +azure-storage-blob +pydub +moviepy +spleeter diff --git a/backend/.env.example b/backend/.env.example index 5292803b..d40e5a8f 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,11 +1,11 @@ -DEBUG=true -SECRET_KEY= -ALLOWED_HOSTS= -FRONTEND_URL= -DOMAIN= +DEBUG=false +SECRET_KEY='' +ALLOWED_HOSTS=backend.chitralekha.ai4bharat.org +FRONTEND_URL=https://chitralekha.ai4bharat.org +DOMAIN="chitralekha.ai4bharat.org/#" ENABLE_CORS=true -CORS_TRUSTED_ORIGINS= +CORS_TRUSTED_ORIGINS=https://*.ai4bharat.org POSTGRES_DB_NAME='postgres' # Insert your database name here POSTGRES_DB_USERNAME='postgres' # Insert your PostgreSQL username here @@ -13,28 +13,31 @@ POSTGRES_DB_PASSWORD='password' #Insert your PostgreSQL password here. POSTGRES_DB_HOST='db' POSTGRES_DB_PORT='5432' -DEFAULT_FROM_EMAIL= -EMAIL_HOST= -SMTP_USERNAME= -SMTP_PASSWORD= +DEFAULT_FROM_EMAIL="" +EMAIL_HOST="" +SMTP_USERNAME="" +SMTP_PASSWORD="" CELERY_BROKER_URL="redis://redis:6379" -# FLOWER_BASIC_AUTH=username:password +FLOWER_BASIC_AUTH=username:password FLOWER_URL="http://flower:5555" DHRUVA_KEY="" -# ASR_API_URL="" + ENGLISH_ASR_API_URL="" INDIC_ASR_API_URL="" SERVICE_ID_HINDI="" SERVICE_ID_INDO_ARYAN="" SERVICE_ID_DRAVIDIAN="" + NMT_API_URL="" MISC_TTS_API_URL="" INDO_ARYAN_TTS_API_URL="" DRAVIDIAN_TTS_API_URL="" TRANSLITERATION_URL="" -ALIGN_JSON_URL= +BG_MUSIC_API_URL="" + +ALIGN_JSON_URL="" AZURE_STORAGE_ACCOUNT_KEY="" AZURE_STORAGE_CONNECTION_STRING="" diff --git a/backend/backend/celery.py b/backend/backend/celery.py index 201f575a..4165090c 100644 --- a/backend/backend/celery.py +++ b/backend/backend/celery.py @@ -30,6 +30,18 @@ "task": "send_new_tasks_mail", "schedule": crontab(minute=0, hour=1), # execute everyday at 1 am }, + "Send_mail_to_org_owners": { + "task": "send_new_users_to_org_owner", + "schedule": crontab(minute=1, hour=1), # execute everyday at 1 am + }, + "Send_mail_to_users": { + "task": "send_eta_reminders", + "schedule": crontab(minute=2, hour=1), # execute everyday at 1 am + }, + "Send_newsletters": { + "task": "send_newsletter", + "schedule": crontab(minute=3, hour=1), # execute everyday at 1 am + }, } celery_app.autodiscover_tasks() diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 84102e44..14ab6ebc 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -64,6 +64,7 @@ "users", "voiceover", "youtube", + "newsletter", ] MIDDLEWARE = [ diff --git a/backend/backend/urls.py b/backend/backend/urls.py index fbb64a62..809b2849 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -20,6 +20,8 @@ from drf_yasg.views import get_schema_view from drf_yasg import openapi from drf_yasg.generators import OpenAPISchemaGenerator +from video.views import TransliterationAPIView + ## Utility Classes class BothHttpAndHttpsSchemaGenerator(OpenAPISchemaGenerator): @@ -68,6 +70,12 @@ def get_schema(self, request=None, public=False): path("transcript/", include("transcript.urls")), path("voiceover/", include("voiceover.urls")), path("youtube/", include("youtube.urls")), + path( + "api/generic/transliteration///", + TransliterationAPIView.as_view(), + name="transliteration-api", + ), + path("newsletter/", include("newsletter.urls")), re_path( r"^swagger(?P\.json|\.yaml)$", schema_view.without_ui(cache_timeout=0), diff --git a/backend/config.py b/backend/config.py index a2accb1f..3388c985 100644 --- a/backend/config.py +++ b/backend/config.py @@ -15,6 +15,7 @@ youtube_api_key = os.getenv("YOUTUBE_API_KEY") align_json_url = os.getenv("ALIGN_JSON_URL") transliteration_url = os.getenv("TRANSLITERATION_URL") +bg_music_url = os.getenv("BG_MUSIC_API_URL") storage_account_key = os.getenv("AZURE_STORAGE_ACCOUNT_KEY") connection_string = os.getenv("AZURE_STORAGE_CONNECTION_STRING") diff --git a/backend/newsletter/__init__.py b/backend/newsletter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/newsletter/admin.py b/backend/newsletter/admin.py new file mode 100644 index 00000000..093740ec --- /dev/null +++ b/backend/newsletter/admin.py @@ -0,0 +1,30 @@ +from django.contrib import admin + +from .models import Newsletter, SubscribedUsers + + +# Show particular fields in the admin panel +class NewsletterAdmin(admin.ModelAdmin): + """ + YoutubeAdmin class to render the youtube admin panel. + """ + + list_display = ( + "newsletter_uuid", + "submitter_id", + "content", + "category", + "created_at", + ) + + +class SubscribedUsersAdmin(admin.ModelAdmin): + """ + YoutubeAdmin class to render the youtube admin panel. + """ + + list_display = ("user", "subscribed_categories") + + +admin.site.register(Newsletter, NewsletterAdmin) +admin.site.register(SubscribedUsers, SubscribedUsersAdmin) diff --git a/backend/newsletter/apps.py b/backend/newsletter/apps.py new file mode 100644 index 00000000..3d514eaa --- /dev/null +++ b/backend/newsletter/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class NewsletterConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "newsletter" diff --git a/backend/newsletter/migrations/0001_initial.py b/backend/newsletter/migrations/0001_initial.py new file mode 100644 index 00000000..10c33599 --- /dev/null +++ b/backend/newsletter/migrations/0001_initial.py @@ -0,0 +1,91 @@ +# Generated by Django 4.1.5 on 2023-10-19 05:50 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="SubscribedUsers", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="subscribed_user", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="Newsletter", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "newsletter_uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + unique=True, + verbose_name="Newsletter UUID", + ), + ), + ("content", models.TextField(help_text="Newsletter Content")), + ( + "category", + models.CharField( + blank=True, + choices=[("NEW_FEATURE", "New Feature")], + default=None, + max_length=35, + null=True, + verbose_name="Category of newsletter", + ), + ), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Newsletter Created At" + ), + ), + ( + "submitter_id", + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="newsletter_submitter", + to=settings.AUTH_USER_MODEL, + verbose_name="newsletter_submitter", + ), + ), + ], + ), + ] diff --git a/backend/newsletter/migrations/0002_alter_subscribedusers_user.py b/backend/newsletter/migrations/0002_alter_subscribedusers_user.py new file mode 100644 index 00000000..cc766e54 --- /dev/null +++ b/backend/newsletter/migrations/0002_alter_subscribedusers_user.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.5 on 2023-11-03 01:38 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("newsletter", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="subscribedusers", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="Submitter Name", + ), + ), + ] diff --git a/backend/newsletter/migrations/0003_alter_newsletter_submitter_id_and_more.py b/backend/newsletter/migrations/0003_alter_newsletter_submitter_id_and_more.py new file mode 100644 index 00000000..87ae825a --- /dev/null +++ b/backend/newsletter/migrations/0003_alter_newsletter_submitter_id_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 4.1.5 on 2023-11-03 03:26 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("newsletter", "0002_alter_subscribedusers_user"), + ] + + operations = [ + migrations.AlterField( + model_name="newsletter", + name="submitter_id", + field=models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="Submitter User", + ), + preserve_default=False, + ), + migrations.AlterField( + model_name="subscribedusers", + name="user", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="subscribed_user", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/backend/newsletter/migrations/__init__.py b/backend/newsletter/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/newsletter/models.py b/backend/newsletter/models.py new file mode 100644 index 00000000..5ecbceae --- /dev/null +++ b/backend/newsletter/models.py @@ -0,0 +1,57 @@ +from django.db import models +import uuid +from django.conf import settings +from django.contrib.postgres.fields import ArrayField +from django import forms +from users.models import User + + +NEWSLETTER_CATEGORY = (("NEW_FEATURE", "New Feature"),) + + +class Newsletter(models.Model): + """ + Model for the Newsletter object. + """ + + newsletter_uuid = models.UUIDField( + default=uuid.uuid4, + unique=True, + editable=False, + verbose_name="Newsletter UUID", + primary_key=False, + ) + submitter_id = models.ForeignKey( + User, + verbose_name="Submitter User", + on_delete=models.CASCADE, + ) + content = models.TextField(help_text=("Newsletter Content")) + category = models.CharField( + choices=NEWSLETTER_CATEGORY, + max_length=35, + default=None, + verbose_name="Category of newsletter", + null=True, + blank=True, + ) + created_at = models.DateTimeField( + auto_now_add=True, verbose_name="Newsletter Created At" + ) + + def __str__(self): + return str(self.newsletter_uuid) + + +class SubscribedUsers(models.Model): + user = models.OneToOneField( + settings.AUTH_USER_MODEL, + related_name="subscribed_user", + on_delete=models.CASCADE, + null=False, + blank=False, + ) + subscribed_categories = forms.MultipleChoiceField() + + def __str__(self): + return str(self.user.email) diff --git a/backend/newsletter/serializers.py b/backend/newsletter/serializers.py new file mode 100644 index 00000000..e0b603e8 --- /dev/null +++ b/backend/newsletter/serializers.py @@ -0,0 +1,10 @@ +from rest_framework import serializers +from .models import Newsletter +from django.db import migrations, models + + +class NewsletterSerializer(serializers.ModelSerializer): + content = serializers.ListField(child=serializers.DictField()) + class Meta: + model = Newsletter + fields = ("newsletter_uuid", "submitter_id", "content", "category") diff --git a/backend/newsletter/tasks.py b/backend/newsletter/tasks.py new file mode 100644 index 00000000..f60db924 --- /dev/null +++ b/backend/newsletter/tasks.py @@ -0,0 +1,32 @@ +from celery import shared_task +from backend.celery import celery_app +from .models import SubscribedUsers, Newsletter +from django.utils import timezone +from django.conf import settings +from django.core.mail import send_mail +import logging + + +@shared_task(name="send_newsletter") +def send_newsletter(): + logging.info("Sending Newsletter") + now = timezone.now() + past_24_hours = now - timezone.timedelta(hours=24) + + # Get all objects created in the past 24 hours + newsletters = Newsletter.objects.filter(created_at__gte=past_24_hours).all() + if newsletters is not None: + subscribed_users = SubscribedUsers.objects.all() + for newsletter in newsletters: + for subscribed_user in subscribed_users: + logging.info("Sending Mail to %s", subscribed_user.user.email) + try: + send_mail( + "Chitralekha - Newsletter", + "", + settings.DEFAULT_FROM_EMAIL, + [subscribed_user.user.email], + html_message=newsletter.content, + ) + except: + logging.info("Mail can't be sent.") diff --git a/backend/newsletter/templates/cl_newsletter.html b/backend/newsletter/templates/cl_newsletter.html new file mode 100644 index 00000000..edee864d --- /dev/null +++ b/backend/newsletter/templates/cl_newsletter.html @@ -0,0 +1,242 @@ + + + + + Welcome to Chitralekha + + + + + + + + + + + + + + +
Preview - Welcome to Chitralekha
+
+
+ + + + + + +
+
+ + + + + + + +
+
 
+
+ + + + + + +
+ + +
+

Chitralekha

+
+
+
+
+
+
+ +
+ + + + +
+
+ + + + +
+ +
+
+
+
+
+
+ + + + + + +
+
+ + + + + + +
+
+ + + + + + + + + + + +
+
+
+
+
+

Hello

+

We are delighted to share that we have released new features in Chitralekha. Now, you can add Background Music to your audios as well.Feel free to use the new feature and give us your valuable feedback.

+
+
+
+
+
+
+

Hello World

+

We are delighted to share that we have released new features in Chitralekha. Now, you can add Background Music to your audios as well.
Feel free to use the new feature and give us your valuable feedback.

+
+
+
+
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + + +
+
+
+
Update your email preferences to choose the types of emails you receive, or you can unsubscribe from all future emails.
+
+
+
+
+
+ + + + + + +
+
+ + + + +
+
 
+
+
+
+
+
+ + + diff --git a/backend/newsletter/templates/cl_newsletter_1.html b/backend/newsletter/templates/cl_newsletter_1.html new file mode 100644 index 00000000..e3e5b042 --- /dev/null +++ b/backend/newsletter/templates/cl_newsletter_1.html @@ -0,0 +1,196 @@ + + + + + Welcome to Chitralekha + + + + + + + + + + + + + + +
Preview - Welcome to Chitralekha
+
+
+ + + + + + +
+
+ + + + + + +
+
 
+
+
+
+
+ +
+ + + + +
+
+ + + + +
+ +
+
+
+
+
+
+ + + + + + +
+
+ + + + + + +
+
+ + {% include 'variable.html' %} + +
+
+
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + + +
+
+
+
Update your email preferences to choose the types of emails you receive, or you can unsubscribe from all future emails.
+
+
+
+
+
+ + + + + + +
+
+ + + + +
+
 
+ +
+
+
+
+
+ + + diff --git a/backend/newsletter/templates/variable.html b/backend/newsletter/templates/variable.html new file mode 100644 index 00000000..4c3da8d0 --- /dev/null +++ b/backend/newsletter/templates/variable.html @@ -0,0 +1,44 @@ + + +
+ +
+ + +
+

+ Chitra +

+

+ P111 +

+
+ + + + +
+ +
+ + +
+

+ Chitra +

+

+ P214 +

+
+ + diff --git a/backend/newsletter/tests.py b/backend/newsletter/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/backend/newsletter/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/newsletter/urls.py b/backend/newsletter/urls.py new file mode 100644 index 00000000..9a2f1629 --- /dev/null +++ b/backend/newsletter/urls.py @@ -0,0 +1,10 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from rest_framework import routers +from . import views + +router = routers.DefaultRouter() + +router.register(r"", views.NewsletterViewSet, basename="newsletter") + +urlpatterns = router.urls diff --git a/backend/newsletter/views.py b/backend/newsletter/views.py new file mode 100644 index 00000000..00053410 --- /dev/null +++ b/backend/newsletter/views.py @@ -0,0 +1,339 @@ +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema +from rest_framework import status +from rest_framework.decorators import api_view +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet +from rest_framework.decorators import action +from users.models import User +from django.db.models import Count, Q +from rest_framework.permissions import IsAuthenticated +import webvtt +from io import StringIO +import json, sys +from .models import NEWSLETTER_CATEGORY, Newsletter, SubscribedUsers +from .serializers import NewsletterSerializer +from users.models import User +from rest_framework.response import Response +from rest_framework import status +import logging +from django.http import HttpResponse +import requests +from django.http import HttpRequest +from organization.decorators import is_admin +from django.shortcuts import render +from django.template import loader +from django.http import HttpResponse +from django import template +from pathlib import Path +import os +from bs4 import BeautifulSoup +from django.core.mail import send_mail +from django.conf import settings + + +class NewsletterViewSet(ModelViewSet): + """ + API ViewSet for the Video model. + Performs CRUD operations on the Video model. + Endpoint: /video/api/ + Methods: GET, POST, PUT, DELETE + """ + + queryset = Newsletter.objects.all() + serializer_class = NewsletterSerializer + permission_classes = (IsAuthenticated,) + + @is_admin + def create(self, request, pk=None, *args, **kwargs): + category = request.data.get("category") + content = request.data.get("content") + submitter_id = request.data.get("submitter_id") + template_id = request.data.get("template_id") + BASE_DIR = Path(__file__).resolve().parent.parent + + if content is None or content == "": + return Response( + {"message": "missing param : content can't be empty"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if template_id == 1: + if len(content) != 0: + final_html_content = "" + for c in content: + header_variable = c["header"] + paragraph_variable = c["paragraph"] + html_content = """

{header}

{paragraph}

""".format( + header=header_variable, paragraph=paragraph_variable + ) + final_html_content = final_html_content + html_content + requested_html = os.path.join( + BASE_DIR, "newsletter", "templates", "cl_newsletter_1.html" + ) + file_html = open( + os.path.join(BASE_DIR, "newsletter", "templates", "variable.html"), + "w", + ) + soup = BeautifulSoup(final_html_content, "html.parser") + file_html.write(soup.prettify()) + context = {"variable": ""} + file_html.close() + html_file = loader.get_template(requested_html) + html_content = html_file.render(context, request) + elif template_id == 2: + if len(content) != 0: + final_html_content = "" + for c in content: + header_variable = c["header"] + paragraph_variable = c["paragraph"] + video_poster = c["image"] + youtube_url = c["youtube_url"] + html_content = """
image instead of video

{header}

{paragraph}

""".format( + header=header_variable, + video_poster=video_poster, + youtube_url=youtube_url, + paragraph=paragraph_variable, + ) + final_html_content = final_html_content + html_content + requested_html = os.path.join( + BASE_DIR, "newsletter", "templates", "cl_newsletter_1.html" + ) + file_html = open( + os.path.join(BASE_DIR, "newsletter", "templates", "variable.html"), + "w", + ) + soup = BeautifulSoup(final_html_content, "html.parser") + file_html.write(soup.prettify()) + context = {"variable": ""} + file_html.close() + html_file = loader.get_template(requested_html) + html_content = html_file.render(context, request) + elif template_id == 3: + if type(content) == dict: + message = base64.b64decode(content["html"]).decode("utf-8") + f = open('content.html','w') + f.write(message) + f.close() + + # Parse the file using an HTML parser. + parser = html.parser.HTMLParser() + with open('content.html', 'rb') as f: + parser.feed(f.read().decode('utf-8')) + + # Check for common HTML errors. + if parser.error_list: + return Response( + {"message": "Error in HTML."}, + status=status.HTTP_400_BAD_REQUEST, + ) + html_content = message + else: + return Response( + {"message": "Template not supported."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + new_newsletter = Newsletter( + content=html_content, + submitter_id=User.objects.get(pk=submitter_id), + category="NEW_FEATURE", + ) + new_newsletter.save() + return Response( + {"message": "Newsletter is successfully submitted."}, + status=status.HTTP_200_OK, + ) + + + @is_admin + @swagger_auto_schema(request_body=NewsletterSerializer) + @action(detail=False, methods=["post"], url_path="preview") + def preview(self, request, pk=None, *args, **kwargs): + category = request.data.get("category") + content = request.data.get("content") + submitter_id = request.data.get("submitter_id") + template_id = request.data.get("template_id") + BASE_DIR = Path(__file__).resolve().parent.parent + + if content is None or content == "": + return Response( + {"message": "missing param : content can't be empty"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if template_id == 1: + if len(content) != 0: + final_html_content = "" + for c in content: + header_variable = c["header"] + paragraph_variable = c["paragraph"] + html_content = """

{header}

{paragraph}

""".format( + header=header_variable, paragraph=paragraph_variable + ) + final_html_content = final_html_content + html_content + requested_html = os.path.join( + BASE_DIR, "newsletter", "templates", "cl_newsletter_1.html" + ) + file_html = open( + os.path.join(BASE_DIR, "newsletter", "templates", "variable.html"), + "w", + ) + soup = BeautifulSoup(final_html_content, "html.parser") + file_html.write(soup.prettify()) + context = {"variable": ""} + file_html.close() + html_file = loader.get_template(requested_html) + html_content = html_file.render(context, request) + elif template_id == 2: + if len(content) != 0: + final_html_content = "" + for c in content: + header_variable = c["header"] + paragraph_variable = c["paragraph"] + video_poster = c["image"] + youtube_url = c["youtube_url"] + html_content = """
image instead of video

{header}

{paragraph}

""".format( + header=header_variable, + video_poster=video_poster, + youtube_url=youtube_url, + paragraph=paragraph_variable, + ) + final_html_content = final_html_content + html_content + requested_html = os.path.join( + BASE_DIR, "newsletter", "templates", "cl_newsletter_1.html" + ) + file_html = open( + os.path.join(BASE_DIR, "newsletter", "templates", "variable.html"), + "w", + ) + soup = BeautifulSoup(final_html_content, "html.parser") + file_html.write(soup.prettify()) + context = {"variable": ""} + file_html.close() + html_file = loader.get_template(requested_html) + html_content = html_file.render(context, request) + elif template_id == 3: + if len(content) != 0: + message = base64.b64decode(content).decode("utf-8") + f = open('content.html','w') + f.write(message) + f.close() + + # Parse the file using an HTML parser. + parser = html.parser.HTMLParser() + with open('content.html', 'rb') as f: + parser.feed(f.read().decode('utf-8')) + + # Check for common HTML errors. + if parser.error_list: + return Response( + {"message": "Error in HTML."}, + status=status.HTTP_400_BAD_REQUEST, + ) + html_content = message + else: + return Response( + {"message": "Template not supported."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + new_newsletter = Newsletter( + content=html_content, + submitter_id=User.objects.get(pk=submitter_id), + category="NEW_FEATURE", + ) + new_newsletter.save() + return Response( + {"html": html_content}, + status=status.HTTP_200_OK, + ) + + + @swagger_auto_schema( + method="post", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "email": openapi.Schema(type=openapi.TYPE_STRING), + "newsletter_id": openapi.Schema( + type=openapi.TYPE_STRING, + ), + }, + required=["email"], + ), + responses={200: "Subscribed Successfully."}, + ) + @action(detail=False, methods=["post"], url_path="send_mail_temp") + def send_mail_temp(self, request): + newsletter_id = request.data.get("newsletter_id") + email = request.data.get("email") + + if email is None: + return Response( + {"message": "missing param : Email can't be empty"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + try: + user = User.objects.get(email=email) + except: + return Response( + {"message": "User with this Email Id doesn't exist."}, + status=status.HTTP_400_BAD_REQUEST, + ) + newsletter = Newsletter.objects.filter(newsletter_uuid=newsletter_id).first() + print(newsletter.content) + send_mail( + "Chitralekha E-Newsletter", + "", + settings.DEFAULT_FROM_EMAIL, + [email], + html_message=newsletter.content, + ) + return Response( + {"message": "Newsletter is successfully sent."}, + status=status.HTTP_200_OK, + ) + + @swagger_auto_schema( + method="post", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "email": openapi.Schema(type=openapi.TYPE_STRING), + }, + required=["email"], + ), + responses={200: "Subscribed Successfully."}, + ) + @action(detail=False, methods=["post"], url_path="subscribe") + def subscribe(self, request): + categories = request.data.get("categories") + email = request.data.get("email") + + if email is None: + return Response( + {"message": "missing param : Email can't be empty"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + try: + user = User.objects.get(email=email) + except: + return Response( + {"message": "User with this Email Id doesn't exist."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + sub_user, created = SubscribedUsers.objects.get_or_create(user=user) + if not created: + return Response( + {"message": "User is already subscribed."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + return Response( + {"message": "Newsletter is successfully subscribed."}, + status=status.HTTP_200_OK, + ) diff --git a/backend/organization/views.py b/backend/organization/views.py index 48b72090..aaaf3801 100644 --- a/backend/organization/views.py +++ b/backend/organization/views.py @@ -1000,7 +1000,7 @@ def get_aggregated_report_languages(self, request, pk=None, *args, **kwargs): transcript_dict = { "language": { "value": dict(TRANSLATION_LANGUAGE_CHOICES)[elem["language"]], - "label": "Media Language", + "label": "Source Language", }, "total_duration": { "value": round(elem["total_duration"].total_seconds() / 3600, 3), @@ -1015,22 +1015,22 @@ def get_aggregated_report_languages(self, request, pk=None, *args, **kwargs): translation_dict = { "src_language": { "value": dict(TRANSLATION_LANGUAGE_CHOICES)[elem["src_language"]], - "label": "Src Language", + "label": "Source Language", }, "tgt_language": { "value": dict(TRANSLATION_LANGUAGE_CHOICES)[elem["tgt_language"]], - "label": "Tgt Language", + "label": "Target Language", }, "translation_duration": { "value": round( elem["translation_duration"].total_seconds() / 3600, 3 ), - "label": "Translated Duration (Hours)", + "label": "Duration (Hours)", "viewColumns": False, }, "transcripts_translated": { "value": elem["transcripts_translated"], - "label": "Translation Tasks Count", + "label": "Tasks Count", }, } translation_data.append(translation_dict) @@ -1136,11 +1136,11 @@ def get_report_projects(self, request, pk=None, *args, **kwargs): "num_videos": {"value": elem["num_videos"], "label": "Video count"}, "total_transcriptions": { "value": transcript_duration, - "label": "Transcripted Duration (Hours)", + "label": "Duration (Hours)", }, "total_translations": { "value": translation_duration, - "label": "Translated Duration (Hours)", + "label": "Duration (Hours)", }, "total_word_count": { "value": int(transcript_word_count + translation_word_count), diff --git a/backend/project/views.py b/backend/project/views.py index 1afdfdc5..2e58072d 100644 --- a/backend/project/views.py +++ b/backend/project/views.py @@ -1482,7 +1482,8 @@ def get_report_languages(self, request, pk=None, *args, **kwargs): .values("language") ) transcript_statistics = ( - prj_transcriptions.annotate(total_duration=Sum(F("video__duration"))) + prj_transcriptions.annotate(transcripts=Count("id")) + .annotate(total_duration=Sum(F("video__duration"))) .annotate(word_count=Sum(Cast(F("payload__word_count"), FloatField()))) .order_by("-total_duration") ) @@ -1525,6 +1526,10 @@ def get_report_languages(self, request, pk=None, *args, **kwargs): "label": "Duration (Hours)", "viewColumns": False, }, + "transcripts": { + "value": elem["transcripts"], + "label": "Tasks Count", + }, "word_count": { "value": elem["word_count"], "label": "Word Count", diff --git a/backend/task/views.py b/backend/task/views.py index 808c0263..12cf99fc 100644 --- a/backend/task/views.py +++ b/backend/task/views.py @@ -23,6 +23,7 @@ send_mail_to_user, get_bad_sentences, get_bad_sentences_in_progress, + get_bad_sentences_in_progress_for_transcription ) from transcript.models import ( Transcript, @@ -2460,6 +2461,10 @@ def update_multiple_tasks(self, request, *args, **kwargs): permission = self.has_translate_review_permission( user_obj, [task.video] ) + elif task.task_type == "VOICEOVER_EDIT": + permission = self.has_voice_over_edit_permission( + user_obj, [task.video] + ) else: logging.info("Not a Valid Type") @@ -2783,7 +2788,7 @@ def get_fail_info(self, request, pk, *args, **kwargs): return Response( { "data": bad_sentences, - "message": "Sentences with time issues are returned.", + "message": "Sentences with issues are returned.", }, status=status.HTTP_200_OK, ) @@ -2794,12 +2799,12 @@ def get_fail_info(self, request, pk, *args, **kwargs): ) elif "TRANSCRIPTION" in task.task_type: transcript = get_transcript_id(task) - bad_sentences = get_bad_sentences_in_progress(transcript, "") + bad_sentences = get_bad_sentences_in_progress_for_transcription(transcript, "") if len(bad_sentences) > 0: return Response( { "data": bad_sentences, - "message": "Sentences with time issues are returned.", + "message": "Sentences with issues are returned.", }, status=status.HTTP_200_OK, ) @@ -2815,28 +2820,59 @@ def get_fail_info(self, request, pk, *args, **kwargs): ) @is_project_owner + @swagger_auto_schema( + method="post", + manual_parameters=[ + openapi.Parameter( + "delete_voiceover_and_reopen", + openapi.IN_QUERY, + description=("Delete dependent voiceover and reopen('true'/'false')"), + type=openapi.TYPE_STRING, + required=False, + ), + ], + ) @action(detail=True, methods=["post"], url_path="reopen_translation_task") def reopen_translation_task(self, request, pk, *args, **kwargs): + delete_voiceover_and_reopen=request.query_params.get("delete_voiceover_and_reopen") + if delete_voiceover_and_reopen=='true': + delete_voiceover_and_reopen=True + else: + delete_voiceover_and_reopen=False + try: task = Task.objects.get(pk=pk) except Task.DoesNotExist: return Response( {"message": "Task not found"}, status=status.HTTP_404_NOT_FOUND ) - if task.status == "FAILED" and "TRANSLATION" in task.task_type: - translation_completed_obj = ( - Translation.objects.filter(status="TRANSLATION_EDIT_COMPLETE") - .filter(target_language=task.target_language) - .filter(video=task.video) - .first() - ) - translation_inprogress_obj = ( - Translation.objects.filter(status="TRANSLATION_EDIT_INPROGRESS") - .filter(target_language=task.target_language) - .filter(video=task.video) - .all() - ) + if "TRANSLATION_REVIEW" in task.task_type: + translation_completed_obj = ( + Translation.objects.filter(status="TRANSLATION_REVIEW_COMPLETE") + .filter(target_language=task.target_language) + .filter(video=task.video) + .first() + ) + translation_inprogress_obj = ( + Translation.objects.filter(status="TRANSLATION_REVIEW_INPROGRESS") + .filter(target_language=task.target_language) + .filter(video=task.video) + .all() + ) + else: + translation_completed_obj = ( + Translation.objects.filter(status="TRANSLATION_EDIT_COMPLETE") + .filter(target_language=task.target_language) + .filter(video=task.video) + .first() + ) + translation_inprogress_obj = ( + Translation.objects.filter(status="TRANSLATION_EDIT_INPROGRESS") + .filter(target_language=task.target_language) + .filter(video=task.video) + .all() + ) if ( translation_inprogress_obj is not None and translation_completed_obj is not None @@ -2844,7 +2880,7 @@ def reopen_translation_task(self, request, pk, *args, **kwargs): translation_completed_obj.parent = None translation_completed_obj.save() translation_inprogress_obj.delete() - translation_completed_obj.status = "TRANSLATION_EDIT_INPROGRESS" + translation_completed_obj.status = "TRANSLATION_REVIEW_INPROGRESS" if "TRANSLATION_REVIEW" in task.task_type else "TRANSLATION_EDIT_INPROGRESS" translation_completed_obj.save() task.status = "REOPEN" task.save() @@ -2854,6 +2890,22 @@ def reopen_translation_task(self, request, pk, *args, **kwargs): status=status.HTTP_400_BAD_REQUEST, ) if task.status == "COMPLETE" and "TRANSLATION" in task.task_type: + translation_review_task=( + Task.objects.filter(video=task.video) + .filter(target_language=task.target_language) + .filter(task_type="TRANSLATION_REVIEW") + .first() + ) + if ( + "TRANSLATION_EDIT" in task.task_type + and translation_review_task is not None + and translation_review_task.is_active==True + ): + return Response( + {"message": "Can not reopen this task. Corrosponding Translation Review task is active"}, + status=status.HTTP_400_BAD_REQUEST, + ) + voiceover_task = ( Task.objects.filter(video=task.video) .filter(target_language=task.target_language) @@ -2861,35 +2913,116 @@ def reopen_translation_task(self, request, pk, *args, **kwargs): .first() ) if voiceover_task is not None: - response = [ - { - "task_type": voiceover_task.get_task_type_label, - "target_language": voiceover_task.get_target_language_label, - "video_name": voiceover_task.video.name, - "id": voiceover_task.id, - "video_id": voiceover_task.video.id, - } - ] - return Response( - { - "response": response, - "message": "Can't reopen the Translation task as there is a dependent VoiceOver task.", - }, - status=status.HTTP_409_CONFLICT, - ) + if delete_voiceover_and_reopen==False: + response = [ + { + "task_type": voiceover_task.get_task_type_label, + "target_language": voiceover_task.get_target_language_label, + "video_name": voiceover_task.video.name, + "id": voiceover_task.id, + "video_id": voiceover_task.video.id, + } + ] + return Response( + { + "response": response, + "message": "There is a dependant VoiceOver task. Do you still want to reopen this task?", + }, + status=status.HTTP_409_CONFLICT, + ) + else: + if "TRANSLATION_REVIEW" in task.task_type: + translation_completed_obj = ( + Translation.objects.filter(status="TRANSLATION_REVIEW_COMPLETE") + .filter(target_language=task.target_language) + .filter(video=task.video) + .first() + ) + translation_inprogress_obj = ( + Translation.objects.filter(status="TRANSLATION_REVIEW_INPROGRESS") + .filter(target_language=task.target_language) + .filter(video=task.video) + .all() + ) + + else: + translation_completed_obj = ( + Translation.objects.filter(status="TRANSLATION_EDIT_COMPLETE") + .filter(target_language=task.target_language) + .filter(video=task.video) + .first() + ) + translation_inprogress_obj = ( + Translation.objects.filter(status="TRANSLATION_EDIT_INPROGRESS") + .filter(target_language=task.target_language) + .filter(video=task.video) + .all() + ) + voice_over_selected_source_obj = ( + VoiceOver.objects.filter(status="VOICEOVER_SELECT_SOURCE") + .filter(video=task.video) + .filter(target_language=task.target_language) + .first() + ) + voice_over_inprogress_obj = ( + VoiceOver.objects.filter(status="VOICEOVER_EDIT_INPROGRESS") + .filter(video=task.video) + .filter(target_language=task.target_language) + .first() + ) + + if ( + translation_inprogress_obj is not None + and translation_completed_obj is not None + and voice_over_selected_source_obj is not None + and voice_over_inprogress_obj is None + ): + translation_completed_obj.parent = None + translation_completed_obj.save() + translation_inprogress_obj.delete() + translation_completed_obj.status = "TRANSLATION_REVIEW_INPROGRESS" if "TRANSLATION_REVIEW" in task.task_type else "TRANSLATION_EDIT_INPROGRESS" + translation_completed_obj.save() + task.status = "REOPEN" + task.save() + + voice_over_selected_source_obj.delete() + voiceover_task.is_active = False + voiceover_task.save() + + + else: + return Response( + {"message": "Can not reopen this task."}, + status=status.HTTP_400_BAD_REQUEST, + ) else: - translation_completed_obj = ( - Translation.objects.filter(status="TRANSLATION_EDIT_COMPLETE") - .filter(target_language=task.target_language) - .filter(video=task.video) - .first() - ) - translation_inprogress_obj = ( - Translation.objects.filter(status="TRANSLATION_EDIT_INPROGRESS") - .filter(target_language=task.target_language) - .filter(video=task.video) - .all() - ) + if "TRANSLATION_REVIEW" in task.task_type: + translation_completed_obj = ( + Translation.objects.filter(status="TRANSLATION_REVIEW_COMPLETE") + .filter(target_language=task.target_language) + .filter(video=task.video) + .first() + ) + translation_inprogress_obj = ( + Translation.objects.filter(status="TRANSLATION_REVIEW_INPROGRESS") + .filter(target_language=task.target_language) + .filter(video=task.video) + .all() + ) + else: + translation_completed_obj = ( + Translation.objects.filter(status="TRANSLATION_EDIT_COMPLETE") + .filter(target_language=task.target_language) + .filter(video=task.video) + .first() + ) + translation_inprogress_obj = ( + Translation.objects.filter(status="TRANSLATION_EDIT_INPROGRESS") + .filter(target_language=task.target_language) + .filter(video=task.video) + .all() + ) + if ( translation_inprogress_obj is not None and translation_completed_obj is not None @@ -2897,7 +3030,7 @@ def reopen_translation_task(self, request, pk, *args, **kwargs): translation_completed_obj.parent = None translation_completed_obj.save() translation_inprogress_obj.delete() - translation_completed_obj.status = "TRANSLATION_EDIT_INPROGRESS" + translation_completed_obj.status = "TRANSLATION_REVIEW_INPROGRESS" if "TRANSLATION_REVIEW" in task.task_type else "TRANSLATION_EDIT_INPROGRESS" translation_completed_obj.save() task.status = "REOPEN" task.save() diff --git a/backend/transcript/views.py b/backend/transcript/views.py index d4b54c70..546e83cf 100644 --- a/backend/transcript/views.py +++ b/backend/transcript/views.py @@ -46,6 +46,7 @@ TRANSCRIPTION_REVIEW_COMPLETE, ) +from voiceover.utils import get_bad_sentences_in_progress_for_transcription from .decorators import is_transcript_editor from .serializers import TranscriptSerializer from .utils.asr import get_asr_supported_languages, make_asr_api_call @@ -805,6 +806,44 @@ def send_mail_to_user(task): logging.info("Email is not enabled %s", task.user.email) +def check_if_transcription_correct(transcription_obj, task): + bad_sentences = get_bad_sentences_in_progress_for_transcription(transcription_obj, task) + if len(bad_sentences)>0: + transcription = ( + Transcript.objects.filter(video=task.video) + .filter(status="TRANSCRIPTION_EDIT_INPROGRESS") + .first() + ) + if transcription is not None: + transcription_obj.status = "TRANSCRIPTION_EDIT_INPROGRESS" + transcription_obj.parent_transcript=transcription.parent_transcript + transcription_obj.save() + transcription.parent_transcript = None + transcription.save() + transcription.delete() + task.status = "INPROGRESS" + task.save() + else: + transcription = ( + Transcript.objects.filter(video=task.video) + .filter(status="TRANSCRIPTION_SELECT_SOURCE") + .first() + ) + transcription.parent_transcript = None + transcription.save() + transcription.delete() + task.status = "SELECTED_SOURCE" + transcription_obj.status = "TRANSCRIPTION_SELECT_SOURCE" + task.save() + transcription_obj.save() + + response = { + "data": bad_sentences, + "message": "Transcription task couldn't be completed. Please correct the following sentences.", + } + return response + return None + def change_active_status_of_next_tasks(task, transcript_obj): tasks = Task.objects.filter(video=task.video) activate_translations = True @@ -1473,6 +1512,15 @@ def save_transcription(request): transcript_obj.save() task.status = "COMPLETE" task.save() + response = check_if_transcription_correct(transcript_obj, task) + if type(response) == dict: + return Response( + { + "data": response["data"], + "message": response["message"], + }, + status=status.HTTP_400_BAD_REQUEST, + ) change_active_status_of_next_tasks(task, transcript_obj) else: transcript_obj = ( @@ -1563,6 +1611,15 @@ def save_transcription(request): transcript_obj.save() task.status = "COMPLETE" task.save() + response = check_if_transcription_correct(transcript_obj, task) + if type(response) == dict: + return Response( + { + "data": response["data"], + "message": response["message"], + }, + status=status.HTTP_400_BAD_REQUEST, + ) change_active_status_of_next_tasks(task, transcript_obj) else: tc_status = TRANSCRIPTION_REVIEW_INPROGRESS diff --git a/backend/translation/migrations/0017_alter_translation_translation_type.py b/backend/translation/migrations/0017_alter_translation_translation_type.py new file mode 100644 index 00000000..2f18e8ff --- /dev/null +++ b/backend/translation/migrations/0017_alter_translation_translation_type.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.5 on 2023-12-05 13:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("translation", "0016_alter_translation_translation_type"), + ] + + operations = [ + migrations.AlterField( + model_name="translation", + name="translation_type", + field=models.CharField( + choices=[ + ("MACHINE_GENERATED", "Machine Generated"), + ("MANUALLY_CREATED", "Manually Created"), + ], + max_length=35, + verbose_name="Translation Type", + ), + ), + ] diff --git a/backend/translation/models.py b/backend/translation/models.py index 8809b937..f791ea5d 100644 --- a/backend/translation/models.py +++ b/backend/translation/models.py @@ -23,7 +23,7 @@ TRANSLATION_TYPE_CHOICES = ( (MACHINE_GENERATED, "Machine Generated"), (MANUALLY_CREATED, "Manually Created"), - (ORIGINAL_SOURCE, "Original Source"), + # (ORIGINAL_SOURCE, "Original Source"), ) TRANSLATION_STATUS = ( diff --git a/backend/translation/views.py b/backend/translation/views.py index 31b33c55..2f21d192 100644 --- a/backend/translation/views.py +++ b/backend/translation/views.py @@ -370,6 +370,13 @@ def get_translation_id(task): .filter(status="TRANSLATION_REVIEWER_ASSIGNED") .first() ) + if task.status == "REOPEN": + translation_id = ( + translation.filter(video=task.video) + .filter(target_language=task.target_language) + .filter(status="TRANSLATION_REVIEW_INPROGRESS") + .first() + ) if task.status == "INPROGRESS": translation_id = ( translation.filter(video=task.video) diff --git a/backend/user_reports.py b/backend/user_reports.py index b9e060d0..bc85f285 100644 --- a/backend/user_reports.py +++ b/backend/user_reports.py @@ -24,6 +24,7 @@ from users.models import User from video.models import Video import logging +from organization.models import Organization def get_completed_tasks(): @@ -53,6 +54,7 @@ def get_completed_tasks(): tasks_managed.append( { "Project Name": task.video.project_id.title, + "Task ID": task.id, "Task Type": task.get_task_type_label, "Video Name": task.video.name, "Video Url": task.video.url, @@ -122,6 +124,7 @@ def get_new_tasks(): tasks_managed.append( { "Project Name": task.video.project_id.title, + "Task ID": task.id, "Task Type": task.get_task_type_label, "Video Name": task.video.name, "Video Url": task.video.url, @@ -162,3 +165,118 @@ def get_new_tasks(): ) else: html_table_df_tasks = "" + + +def get_eta_reminders(): + users = User.objects.filter(enable_mail=True).filter(has_accepted_invite=True).all() + now = timezone.now() + eta_today = now + timezone.timedelta(hours=1) + + # Get all objects created in the past 24 hours + for user in users: + tasks_assigned = ( + Task.objects.filter(user=user) + .filter(eta__date=eta_today) + .filter(is_active=True) + .all() + ) + task_assigned_info = [] + for task in tasks_assigned: + task_assigned_info.append( + { + "Task ID": task.id, + "Task Type": task.get_task_type_label, + "Video Name": task.video.name, + "Video Url": task.video.url, + } + ) + if len(task_assigned_info) > 0: + df = pd.DataFrame.from_records(task_assigned_info) + blankIndex = [""] * len(df) + df.index = blankIndex + html_table_df_tasks = build_table( + df, + "orange_light", + font_size="medium", + text_align="left", + width="auto", + index=False, + ) + message = ( + "Dear " + + str(user.first_name + " " + user.last_name) + + ",\n Follwing Tasks are due for today." + ) + + email_to_send = ( + "

" + + message + + "


Due Tasks For Today

" + + html_table_df_tasks + ) + logging.info("Sending Mail to %s", user.email) + send_mail( + "Chitralekha - Due Tasks", + message, + settings.DEFAULT_FROM_EMAIL, + [user.email], + html_message=email_to_send, + ) + else: + html_table_df_tasks = "" + + +def get_new_users(): + organization_owners = [ + organization.organization_owner for organization in Organization.objects.all() + ] + now = timezone.now() + past_24_hours = now - timezone.timedelta(hours=24) + users = User.objects.filter(date_joined__gte=past_24_hours) + + # Get all objects created in the past 24 hours + for org_owner in organization_owners: + users_in_org = users.filter(organization=org_owner.organization) + new_users = [] + for user in users_in_org: + new_users.append( + { + "Email": user.email, + "Role": user.get_role_label, + "Name": user.first_name + " " + user.last_name, + "Languages": ", ".join(user.languages), + } + ) + if len(new_users) > 0: + df = pd.DataFrame.from_records(new_users) + blankIndex = [""] * len(df) + df.index = blankIndex + html_table_df_tasks = build_table( + df, + "orange_light", + font_size="medium", + text_align="left", + width="auto", + index=False, + ) + message = ( + "Dear " + + str(org_owner.first_name + " " + org_owner.last_name) + + ",\n Following users have signed up." + ) + email_to_send = ( + "

" + + message + + "


New Users

" + + html_table_df_tasks + ) + logging.info("Sending Mail to %s", org_owner.email) + send_mail( + "Chitralekha - New Users", + message, + settings.DEFAULT_FROM_EMAIL, + [org_owner.email], + html_message=email_to_send, + ) + else: + html_table_df_tasks = "" diff --git a/backend/users/migrations/0009_alter_user_date_joined.py b/backend/users/migrations/0009_alter_user_date_joined.py new file mode 100644 index 00000000..1445d29b --- /dev/null +++ b/backend/users/migrations/0009_alter_user_date_joined.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1.5 on 2023-11-21 22:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0008_alter_user_phone"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="date_joined", + field=models.DateTimeField( + blank=True, null=True, verbose_name="date joined" + ), + ), + ] diff --git a/backend/users/models.py b/backend/users/models.py index 6fbffdbe..4e741c35 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -100,7 +100,9 @@ class User(AbstractBaseUser, PermissionsMixin): help_text=("Indicates whether mailing is enable or not."), ) - date_joined = models.DateTimeField(verbose_name="date joined", default=timezone.now) + date_joined = models.DateTimeField( + verbose_name="date joined", blank=True, null=True + ) activity_at = models.DateTimeField( verbose_name="last annotation activity by the user", auto_now=True diff --git a/backend/users/tasks.py b/backend/users/tasks.py index df54fcc5..de375750 100644 --- a/backend/users/tasks.py +++ b/backend/users/tasks.py @@ -4,6 +4,8 @@ from celery.schedules import crontab from backend.celery import celery_app from user_reports import * +from organization.models import Organization +from .models import User @shared_task(name="send_completed_tasks_mail") @@ -14,3 +16,11 @@ def send_completed_tasks_mail(): @shared_task(name="send_new_tasks_mail") def send_new_tasks_mail(): get_new_tasks() + +@shared_task(name="send_new_users_to_org_owner") +def send_new_users_to_org_owner(): + get_new_users() + +@shared_task(name="send_eta_reminders") +def send_eta_reminders(): + get_eta_reminders() diff --git a/backend/users/views.py b/backend/users/views.py index 97e9d521..3bb7eeaa 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -35,6 +35,7 @@ from project.models import Project from project.serializers import ProjectSerializer import json +import datetime regex = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b" @@ -146,6 +147,7 @@ def sign_up_user(self, request, pk=None): user.first_name = request.data.get("first_name", "") user.last_name = request.data.get("last_name", "") user.languages = request.data.get("languages") + user.date_joined = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") serialized.save() return Response({"message": "User signed up"}, status=status.HTTP_200_OK) else: diff --git a/backend/video/models.py b/backend/video/models.py index 5c4f735e..78ed887d 100644 --- a/backend/video/models.py +++ b/backend/video/models.py @@ -8,6 +8,7 @@ FEMALE = "FEMALE" GENDER = ((MALE, "Male"), (FEMALE, "Female")) +MULTISPEAKER_AGE_GROUP = ("1-10", "11-20", "21-60", "61-100") VIDEO_STATUS = ( ("NEW", "NEW"), diff --git a/backend/video/urls.py b/backend/video/urls.py index 173fb0c2..32789a86 100644 --- a/backend/video/urls.py +++ b/backend/video/urls.py @@ -1,10 +1,15 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter - +from .views import TransliterationAPIView from . import views urlpatterns = [ path("", views.get_video, name="get_video"), + path( + "api/generic/transliteration///", + TransliterationAPIView.as_view(), + name="transliteration-api", + ), path("delete_video", views.delete_video, name="delete_video"), path("list_recent", views.list_recent, name="list_recent"), path("list_tasks", views.list_tasks, name="list_tasks"), diff --git a/backend/video/utils.py b/backend/video/utils.py index 795a368c..8713bd15 100644 --- a/backend/video/utils.py +++ b/backend/video/utils.py @@ -734,3 +734,16 @@ def create_video( new_request.GET["task_type"] = task_type new_request.GET["target_language"] = target_language return get_video_func(new_request) + + +def find_duplicates(data, key): + temp = [] + duplicates = [] + + for dictionary in data: + if dictionary[key] not in temp: + temp.append(dictionary[key]) + else: + duplicates.append(dictionary[key]) + + return duplicates diff --git a/backend/video/views.py b/backend/video/views.py index 0a1aee1b..fe8f33f7 100644 --- a/backend/video/views.py +++ b/backend/video/views.py @@ -13,7 +13,7 @@ from transcript.models import ORIGINAL_SOURCE, Transcript from translation.models import Translation from project.decorators import is_project_owner -from .models import Video, GENDER +from .models import Video, GENDER, MULTISPEAKER_AGE_GROUP from .serializers import VideoSerializer from .utils import * from django.utils import timezone @@ -35,7 +35,8 @@ from organization.models import Organization from config import * from collections import Counter - +from rest_framework.views import APIView +import config accepted_languages = [ "as", @@ -92,6 +93,41 @@ ] +class TransliterationAPIView(APIView): + def get(self, request, target_language, data, *args, **kwargs): + json_data = { + "input": [{"source": data}], + "config": { + "language": { + "sourceLanguage": "en", + "targetLanguage": target_language, + }, + "isSentence": False, + }, + } + logging.info("Calling Transliteration API") + response_transliteration = requests.post( + config.transliteration_url, + headers={"authorization": config.dhruva_key}, + json=json_data, + ) + + transliteration_output = response_transliteration.json() + if response_transliteration.status_code == 200: + response = { + "error": "", + "input": data, + "result": transliteration_output["output"][0]["target"], + "success": True, + } + else: + response = {"error": "", "input": data, "result": [], "success": False} + return Response( + response, + status=status.HTTP_200_OK, + ) + + @swagger_auto_schema( method="post", request_body=openapi.Schema( @@ -258,7 +294,6 @@ def list_recent(request): # In the future, if that constraint is removed then we might need to alter the logic. try: - # Get the relevant videos, based on the audio only param video_list = Video.objects.filter(audio_only=is_audio_only) @@ -507,6 +542,14 @@ def download_all(request): type=openapi.TYPE_STRING, description="Gender of video's voice", ), + "multiple_speaker": openapi.Schema( + type=openapi.TYPE_BOOLEAN, + description="Multiple speaker true or false", + ), + "speaker_info": openapi.Schema( + type=openapi.TYPE_OBJECT, + description="Speaker info of video", + ), }, required=["video_id"], ), @@ -522,9 +565,13 @@ def update_video(request): video_id = request.data.get("video_id") description = request.data.get("description") gender = request.data.get("gender") + multiple_speaker = request.data.get("multiple_speaker", "false") + speaker_info = request.data.get("speaker_info") + multiple_speaker = multiple_speaker.lower() == "true" try: video = Video.objects.get(id=video_id) + errors = [] if description is not None: video.description = description @@ -534,14 +581,80 @@ def update_video(request): if gender.upper() in gender_list: video.gender = gender.upper() - video.save() + if multiple_speaker is not None: + video.multiple_speaker = multiple_speaker - return Response( - { - "message": "Video updated successfully.", - }, - status=status.HTTP_200_OK, - ) + if speaker_info is not None: + # Get the task transcript status for the video, if none or selected source + task = ( + Task.objects.filter(video_id=video_id) + .filter(task_type="TRANSCRIPTION_EDIT") + .filter(status__in=["SELECTED_SOURCE"]) + ) + if not task: + errors.append( + { + "message": f"Video's transcript status must be selected source or none", + } + ) + + speaker_info_for_update = [] + gender_list = [gender[0] for gender in GENDER] + + # Find dictionary matching value in list + dubplicte_ids = find_duplicates(speaker_info, "id") + if dubplicte_ids: + errors.append( + { + "message": f"Ids must be unique Age in : {i}", + } + ) + + for i in speaker_info: + speaker_info_obj = {} + + if i["name"] is not None: + speaker_info_obj["name"] = i["name"] + + if i["gender"].upper() in gender_list: + speaker_info_obj["gender"] = i["gender"].upper() + else: + errors.append( + { + "message": f"Invalid Gender in : {i}", + } + ) + + if i["age"] in MULTISPEAKER_AGE_GROUP: + speaker_info_obj["age"] = i["age"] + else: + errors.append( + { + "message": f"Invalid Age in : {i}", + } + ) + + if i["id"] is not None: + speaker_info_obj["id"] = i["id"] + + speaker_info_for_update.append(speaker_info_obj) + + video.speaker_info = speaker_info_for_update + + if len(errors) > 0: + return Response( + {"message": "Invalid Data", "response": errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + else: + video.save() + + return Response( + { + "message": "Video updated successfully.", + }, + status=status.HTTP_200_OK, + ) except Video.DoesNotExist: return Response( {"message": "Video not found"}, status=status.HTTP_404_NOT_FOUND diff --git a/backend/voiceover/tasks.py b/backend/voiceover/tasks.py index 0c1fa089..aefc8924 100644 --- a/backend/voiceover/tasks.py +++ b/backend/voiceover/tasks.py @@ -21,9 +21,15 @@ storage_account_key, connection_string, container_name, + bg_music_url, ) from pydub import AudioSegment from backend.celery import celery_app +import math +from moviepy.editor import VideoFileClip, AudioFileClip, concatenate_audioclips +import re +import json +import requests @shared_task() @@ -48,7 +54,7 @@ def celery_integration(file_name, voice_over_obj_id, video, task_id): @shared_task() -def export_voiceover_async(task_id, export_type, user_id): +def export_voiceover_async(task_id, export_type, user_id, bg_music): user = User.objects.get(pk=user_id) task = Task.objects.get(pk=task_id) voice_over = ( @@ -61,16 +67,31 @@ def export_voiceover_async(task_id, export_type, user_id): download_from_azure_blob(str(voice_over.azure_url_audio)) logging.info("Downloaded audio from Azure Blob %s", voice_over.azure_url_audio) file_path = voice_over.azure_url_audio.split("/")[-1] - AudioSegment.from_file(file_path).export( - file_path.split("/")[-1].replace(".flac", "") + "." + export_type, - format=export_type, - ) - logging.info("Uploading audio to Azure Blob %s", voice_over.azure_url_audio) - azure_url_audio = upload_audio_to_azure_blob( - file_path, export_type, export=True - ) - os.remove(file_path) - os.remove(file_path.split("/")[-1].replace(".flac", "") + "." + export_type) + video_link = task.video.url + if bg_music == "true": + json_data = json.dumps( + {"azure_audio_url": voice_over.azure_url_audio,"youtube_url": video_link} + ) + response = requests.post( + bg_music_url, + data=json_data, + ) + logging.info("Response Received") + azure_url_audio = response.json()["output"] + else: + AudioSegment.from_file(file_path).export( + file_path.split("/")[-1].replace(".flac", "") + "." + export_type, + format=export_type, + ) + logging.info("Uploading audio to Azure Blob %s", voice_over.azure_url_audio) + azure_url_audio = upload_audio_to_azure_blob( + file_path, export_type, export=True + ) + try: + os.remove(file_path) + os.remove(file_path.split("/")[-1].replace(".flac", "") + "." + export_type) + except: + logging.info("Error in removing files") send_audio_mail_to_user(task, azure_url_audio, user) else: logging.info("Error in exporting %s", str(task_id)) diff --git a/backend/voiceover/utils.py b/backend/voiceover/utils.py index e5287935..81461226 100644 --- a/backend/voiceover/utils.py +++ b/backend/voiceover/utils.py @@ -27,6 +27,7 @@ from yt_dlp.utils import DownloadError from yt_dlp.extractor import get_info_extractor from django.http import HttpRequest +from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip from moviepy.editor import VideoFileClip, AudioFileClip, concatenate_audioclips from mutagen.wave import WAVE import numpy @@ -43,6 +44,7 @@ from django.core.mail import send_mail import operator import urllib.parse +import shutil def get_tts_url(language): @@ -465,6 +467,7 @@ def get_bad_sentences_in_progress(translation_obj, target_language): "end_time": text["end_time"], "text": text["text"], "target_text": text["target_text"], + "issue_type": "Time issue in the sentence." } ) if ind != 0 and ind < len(translation["payload"]): @@ -495,9 +498,12 @@ def get_bad_sentences_in_progress(translation_obj, target_language): "end_time": text["end_time"], "text": text["text"], "target_text": text["target_text"], + "issue_type": "Time issue in the sentence.", } ) - elif ("text" in text.keys() and text["end_time"] > (str(0) + str(translation_obj.video.duration) + str(".000"))): + elif "text" in text.keys() and text["end_time"] > ( + str(0) + str(translation_obj.video.duration) + str(".000") + ): problem_sentences.append( { "index": (ind % 50) + 1, @@ -506,6 +512,7 @@ def get_bad_sentences_in_progress(translation_obj, target_language): "end_time": text["end_time"], "text": text["text"], "target_text": text["target_text"], + "issue_type": "Time issue in the sentence." } ) elif "text" in text.keys() and text["start_time"] == text["end_time"]: @@ -517,11 +524,117 @@ def get_bad_sentences_in_progress(translation_obj, target_language): "end_time": text["end_time"], "text": text["text"], "target_text": text["target_text"], + "issue_type": "Time issue in the sentence." + } + ) + else: + pass + if ( + ("text" in text.keys() and len(text['text'])<1) + or ("target_text" in text.keys() and len(text['target_text'])<1) + ): + problem_sentences.append( + { + "page_number": (ind // 50) + 1, + "index": (ind % 50) + 1, + "start_time": text["start_time"], + "end_time": text["end_time"], + "text": text["text"], + "target_text": text["target_text"], + "issue_type": "Empty card is not allowed." + } + ) + return problem_sentences + +def get_bad_sentences_in_progress_for_transcription(transcription_obj, target_language): + problem_sentences = [] + translation = transcription_obj.payload + compare_with_index = -1 + last_valid_index = -1 + for ind, text in enumerate(translation["payload"]): + if ( + "text" in text.keys() + and not compare_time(text["end_time"], text["start_time"])[0] + ): + problem_sentences.append( + { + "index": (ind % 50) + 1, + "page_number": (ind // 50) + 1, + "start_time": text["start_time"], + "end_time": text["end_time"], + "text": text["text"], + "issue_type": "Time issue in the sentence." + } + ) + if ind != 0 and ind < len(translation["payload"]): + compare = False + if "text" in translation["payload"][ind - 1] and "text" in text.keys(): + compare_with_index = ind - 1 + last_valid_index = ind + compare = True + elif ( + "text" in text.keys() and "text" not in translation["payload"][ind - 1] + ): + compare_with_index = last_valid_index + compare = True + else: + pass + if ( + compare + and compare_time( + translation["payload"][compare_with_index]["end_time"], + text["start_time"], + )[0] + ): + problem_sentences.append( + { + "index": (ind % 50) + 1, + "page_number": (ind // 50) + 1, + "start_time": text["start_time"], + "end_time": text["end_time"], + "text": text["text"], + "issue_type": "Time issue in the sentence.", + } + ) + elif "text" in text.keys() and text["end_time"] > ( + str(0) + str(transcription_obj.video.duration) + str(".000") + ): + problem_sentences.append( + { + "index": (ind % 50) + 1, + "page_number": (ind // 50) + 1, + "start_time": text["start_time"], + "end_time": text["end_time"], + "text": text["text"], + "issue_type": "Time issue in the sentence." + } + ) + elif "text" in text.keys() and text["start_time"] == text["end_time"]: + problem_sentences.append( + { + "index": (ind % 50) + 1, + "page_number": (ind // 50) + 1, + "start_time": text["start_time"], + "end_time": text["end_time"], + "text": text["text"], + "issue_type": "Time issue in the sentence." } ) else: pass + if ("text" in text.keys() and len(text['text'])<1): + problem_sentences.append( + { + "page_number": (ind // 50) + 1, + "index": (ind % 50) + 1, + "start_time": text["start_time"], + "end_time": text["end_time"], + "text": text["text"], + "issue_type": "Empty card is not allowed." + } + ) return problem_sentences + def process_translation_payload(translation_obj, target_language): diff --git a/backend/voiceover/views.py b/backend/voiceover/views.py index 65a59403..91c7e91f 100644 --- a/backend/voiceover/views.py +++ b/backend/voiceover/views.py @@ -1186,6 +1186,13 @@ def get_voice_over_types(request): type=openapi.TYPE_STRING, required=True, ), + openapi.Parameter( + "bg_music", + openapi.IN_QUERY, + description=("export type parameter true/false"), + type=openapi.TYPE_BOOLEAN, + required=True, + ), ], responses={200: "VO is exported"}, ) @@ -1193,6 +1200,7 @@ def get_voice_over_types(request): def export_voiceover(request): task_id = request.query_params.get("task_id") export_type = request.query_params.get("export_type") + bg_music = request.query_params.get("bg_music") if task_id is None: return Response( {"message": "missing param : task_id"}, @@ -1238,34 +1246,35 @@ def export_voiceover(request): {"message": "Audio was not created for this Voice Over Task."}, status=status.HTTP_400_BAD_REQUEST, ) - elif export_type == "flac": + elif bg_music == "true": + export_voiceover_async.delay( + voice_over.task.id, export_type, request.user.id, bg_music + ) return Response( - { - "azure_url": voice_over.azure_url_audio, - }, + {"message": "Please wait. The audio link will be emailed to you."}, status=status.HTTP_200_OK, ) + elif export_type == "flac": + return Response( + {"azure_url": voice_over.azure_url_audio}, status=status.HTTP_200_OK + ) elif export_type == "mp3": logging.info( "Downloading audio from Azure Blob %s", voice_over.azure_url_audio ) export_voiceover_async.delay( - voice_over.task.id, export_type, request.user.id + voice_over.task.id, export_type, request.user.id, bg_music ) return Response( - { - "message": "Please wait. The audio link will be emailed to you.", - }, + {"message": "Please wait. The audio link will be emailed to you."}, status=status.HTTP_200_OK, ) elif export_type == "wav": export_voiceover_async.delay( - voice_over.task.id, export_type, request.user.id + voice_over.task.id, export_type, request.user.id, bg_music ) return Response( - { - "message": "Please wait. The audio link will be emailed to you.", - }, + {"message": "Please wait. The audio link will be emailed to you."}, status=status.HTTP_200_OK, ) else: