diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a273bdf..c758d0c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -exclude: "^docs/|/migrations/" +exclude: "^docs/|/migrations/|^ml/" default_stages: [commit] repos: diff --git a/ml/openai_handle.py b/ml/openai_handle.py index b104ea0..4280026 100644 --- a/ml/openai_handle.py +++ b/ml/openai_handle.py @@ -9,27 +9,29 @@ regex = r"({(\n.+)+\n})" -description = """🍀 Что такое Pitch-Deck? Pitch-Deck представляет собой презентацию-тизер проекта/компании для -инвесторов, партнеров, журналистов и других заинтересованных лиц. Цель презентации - привлечение дополнительного -финансирования (инвестиций). Почему это проблема? +description = """ +🍀 Кейсодержатель: +ООО «Акселератор Возможностей» (https://ac-vo.ru/) при ИНТЦ МГУ «Воробьевы горы». +Организация технологических и инвестиционных мероприятий, курирование инновационной деятельности внутри ИНТЦ МГУ «Воробьевы горы» -🍀 Проблема #1. Недостаток средств: Для многих стартапов ограниченные финансы создают преграду при разработке -качественного Pitch Deck. Отсутствие достаточных средств для найма профессиональных консультантов, дизайнеров и -копирайтеров, а также для проведения исследований рынка, может привести к созданию менее привлекательной и -малоинформативной презентации, что затрудняет привлечение инвестиций. +Раскроем небольшую тайну венчура — для привлечения денежных средств и защиты своего проекта, стартапу нужен Pitch-Deck. -🍀 Проблема #2. Недостаток экспертизы: Проблемой для стартапов является недостаток экспертизы для проведения -необходимых исследований и корректного отражения их результатов в Pitch Deck. Не всегда у стартапов есть нужные -знания в области маркетинга, финансов и анализа рынка, что затрудняет создание убедительной и информативной -презентации для привлечения инвестиций. +🍀 Что такое Pitch-Deck? +Pitch-Deck представляет собой презентацию-тизер проекта/компании для инвесторов, партнеров, журналистов и других заинтересованных лиц. Цель презентации - привлечение дополнительного финансирования (инвестиций). +Почему это проблема? -🍀 Проблема #3. Недостаток времени Молодым компаниям для привлечения инвестиций требуется подготовить целый пакет -документов, одним из которых является Pitch Deck. Особенностью стартапов является сравнительного молодая и небольшая -команда, у которой чисто физически не хватает времени на разработку инвестиционных материалов, ведь они полностью -погружены в процесс разработки и улучшения продукта или сервиса. +🍀 Проблема #1. Недостаток средств: +Для многих стартапов ограниченные финансы создают преграду при разработке качественного Pitch Deck. Отсутствие достаточных средств для найма профессиональных консультантов, дизайнеров и копирайтеров, а также для проведения исследований рынка, может привести к созданию менее привлекательной и малоинформативной презентации, что затрудняет привлечение инвестиций. -🍀 ИДЕЯ: Основная идея кейса заключается в создании вспомогательного инструмента на основе ИИ, заточенного под -создание Pitch-Deck.""" +🍀 Проблема #2. Недостаток экспертизы: +Проблемой для стартапов является недостаток экспертизы для проведения необходимых исследований и корректного отражения их результатов в Pitch Deck. Не всегда у стартапов есть нужные знания в области маркетинга, финансов и анализа рынка, что затрудняет создание убедительной и информативной презентации для привлечения инвестиций. + +🍀 Проблема #3. Недостаток времени +Молодым компаниям для привлечения инвестиций требуется подготовить целый пакет документов, одним из которых является Pitch Deck. Особенностью стартапов является сравнительного молодая и небольшая команда, у которой чисто физически не хватает времени на разработку инвестиционных материалов, ведь они полностью погружены в процесс разработки и улучшения продукта или сервиса. + +🍀 ИДЕЯ: +Основная идея кейса заключается в создании вспомогательного инструмента на основе ИИ, заточенного под создание Pitch-Deck. +""" names_prompt = """ По тексту ответь или предположи ответ на вопросы в следующем формате: @@ -42,31 +44,32 @@ """ По тексту ответь или предположи ответ на вопросы в следующем формате: { - 'users': 'Кто будет пользоваться продуктом', - 'problems': 'Какие проблемы решает продукт', - 'actuality': 'Продолжите предложение: Актуальность проблемы подтверждается тем фактом, что...', - 'solve': 'Как решаем эти проблемы', - 'works': 'Как работает решение', + 'users': 'Кто будет пользоваться продуктом?', + 'problems': 'Какие проблемы решает продукт?', + 'actuality': 'Каким фактом обуславливается актуальность проблемы?', + 'solve': 'Как решаем эти проблемы?', + 'works': 'Как работает решение?', } """, """ По тексту ответь или предположи ответ на вопросы в следующем формате: { -'awards': 'Ценность продукта для пользователей', -'money': 'На чем проект зарабатывает? сколько и за что ему платят клиенты', -'aims': Напиши 3 цели: на месяц, на полгода и год, формат: {'1': цель на месяц, '2': цель на полгода, '3': цель на год}, -'investments_sold': 'На что потратить инвестиции под проект', -'financial_indicators': 'Напиши финансовые показатели проекта' + 'awards': 'Ценность продукта для пользователей', + 'money': 'На чем проект зарабатывает? сколько и за что ему платят клиенты', + 'aims': Напиши 3 цели: на месяц, на полгода и год, формат: {'1': цель на месяц, '2': цель на полгода, '3': цель на год}, + 'investments_sold': 'На что потратить инвестиции под проект', + 'financial_indicators': 'Напиши финансовые показатели проекта' } """, - """По тексту ответь или предположи ответ на вопросы в следующем формате: { 'achieve': 'Чего добьется команда после - освоения инвестиций', 'competitors_strength': 'Сильные стороны конкурентов', 'competitors_low': 'Слабые стороны - конкурентов', 'advantages': 'Какие могут быть преимущества над конкурентами', 'category': "На каком рынке - находится этот проект? Выбери из вариантов: 'Business Software', 'IndustrialTech', 'E-commerce', 'Advertising & - Marketing', 'Hardware', 'RetailTech', 'ConstructionTech', 'Web3', 'EdTech', 'Business Intelligence', - 'Cybersecurity', 'HrTech', 'Telecom & Communication', 'Media & Entertainment', 'FinTech', 'MedTech', 'Transport & - Logistics', 'Gaming', 'FoodTech', 'AI', 'WorkTech', 'Consumer Goods & Services', 'Aero & SpaceTech', - 'Legal & RegTech', 'Travel', 'PropTech', 'Energy', 'GreenTech'" } , + """ + По тексту ответь или предположи ответ на вопросы в следующем формате: + { + 'achieve': 'Чего добьется команда после освоения инвестиций', + 'competitors_strength': 'Сильные стороны конкурентов', + 'competitors_low': 'Слабые стороны конкурентов', + 'advantages': 'Какие могут быть преимущества над конкурентами', + 'category': "На каком рынке находится этот проект? Выбери из вариантов: 'Business Software', 'IndustrialTech', 'E-commerce', 'Advertising & Marketing', 'Hardware', 'RetailTech', 'ConstructionTech', 'Web3', 'EdTech', 'Business Intelligence', 'Cybersecurity', 'HrTech', 'Telecom & Communication', 'Media & Entertainment', 'FinTech', 'MedTech', 'Transport & Logistics', 'Gaming', 'FoodTech', 'AI', 'WorkTech', 'Consumer Goods & Services', 'Aero & SpaceTech', 'Legal & RegTech', 'Travel', 'PropTech', 'Energy', 'GreenTech'" + } """, ] @@ -103,9 +106,13 @@ def create_hints(description: str, stage: int): messages=[{"role": "user", "content": description + "\n" + prompts[stage]}], ) str_content = chat_completion.choices[0].message.content - print(str_content) - filtered_content = list(re.finditer(regex, str_content, re.MULTILINE))[-1].group() - if not len(filtered_content): + try: + filtered_content = list(re.finditer(regex, str_content, re.MULTILINE))[ + -1 + ].group() + if not len(filtered_content): + raise ValueError(f"answer doesnt pass validation, {filtered_content}") + except: raise ValueError(f"answer doesnt pass validation, {filtered_content}") content = literal_eval(filtered_content) for assertion_statement in assertions[stage]: @@ -147,7 +154,7 @@ def create_name_hint(description: str): answer = literal_eval(chat_completion.choices[0].message.content)["names"].split( ", " ) - assert len(answer) == 5 + print(answer) return {"type": "names", "value": answer} diff --git a/pitch_deck_generator/decks/api/serializers.py b/pitch_deck_generator/decks/api/serializers.py index d82af5e..df5fc36 100644 --- a/pitch_deck_generator/decks/api/serializers.py +++ b/pitch_deck_generator/decks/api/serializers.py @@ -5,6 +5,7 @@ from rest_framework.generics import get_object_or_404 from pitch_deck_generator.decks.models import ( + PdfToPPTXStorage, PitchDeck, Question, QuestionAnswer, @@ -40,11 +41,17 @@ class HintSerializer(serializers.Serializer): class PresentationAnswerSerializer(serializers.Serializer): slug = serializers.CharField() answer = serializers.JSONField() + photos = serializers.ListSerializer(child=serializers.ImageField()) + + +class PitchDeckSlidePresentationSerializer(serializers.Serializer): + slide = serializers.IntegerField() + data = PresentationAnswerSerializer(many=True) class PitchDeckPresentationSerializer(serializers.Serializer): - slide = serializers.IntegerField() - data = PresentationAnswerSerializer(many=True) + deck = BasePitchDeckSerializer() + slide = PitchDeckSlidePresentationSerializer(many=True) class QuestionSerializer(serializers.ModelSerializer): @@ -100,7 +107,7 @@ class Meta: } def validate(self, data): - answer = data["answer"] + answer = data["answer"] if "answer" in data else None question = get_object_or_404( Question, id=self.context["view"].kwargs["question_id"] ) @@ -168,7 +175,7 @@ def validate(self, data): if answer: raise serializers.ValidationError("Answer should be blank") for key, value in data.items(): - if isinstance(value, InMemoryUploadedFile): + if isinstance(value, (TemporaryUploadedFile, InMemoryUploadedFile)): if "_" not in key: raise serializers.ValidationError( "You should use file_num for file keys" @@ -198,7 +205,7 @@ def validate(self, data): len_f = 0 for key, value in data.items(): - if isinstance(value, TemporaryUploadedFile): + if isinstance(value, (TemporaryUploadedFile, InMemoryUploadedFile)): if "_" not in key: raise serializers.ValidationError( "You should use file_num for file keys" @@ -247,19 +254,27 @@ def create(self, validated_data): q = QuestionAnswer.objects.get_or_create( deck_id=validated_data["deck_id"], question_id=validated_data["question_id"] )[0] - q.answer = validated_data["answer"] + q.answer = validated_data["answer"] if "answer" in validated_data else {} q.save() s = [ key for key, val in validated_data.items() - if isinstance(val, TemporaryUploadedFile) and key != "file" + if isinstance(val, (TemporaryUploadedFile, InMemoryUploadedFile)) + and key != "file" ] if "file" in validated_data: QuestionAnswerPhoto.objects.create(answer=q, file=validated_data["file"]) - elif s: + if s: s.sort(key=lambda x: int(x.split("_")[1])) for key in s: QuestionAnswerPhoto.objects.create(answer=q, file=validated_data[key]) return q + + +class PdfToPPTXSerializer(serializers.ModelSerializer): + class Meta: + model = PdfToPPTXStorage + fields = ["pdf", "pptx"] + extra_kwargs = {"pptx": {"read_only": True}} diff --git a/pitch_deck_generator/decks/api/urls.py b/pitch_deck_generator/decks/api/urls.py index c3997bd..7bd06aa 100644 --- a/pitch_deck_generator/decks/api/urls.py +++ b/pitch_deck_generator/decks/api/urls.py @@ -1,6 +1,7 @@ from django.urls import path from pitch_deck_generator.decks.api.views import ( + ConvertPdfToPPTXApiView, CreateQuestionAnswerApiView, GetDeckPresentationDataApiView, GetDeckQuestionApiView, @@ -15,6 +16,7 @@ urlpatterns = [ path("", ListDecksApiView.as_view()), path("", RetrievePitchApiView.as_view()), + path("pdf-to-pptx", ConvertPdfToPPTXApiView.as_view()), path( "question//presentation", GetDeckPresentationDataApiView.as_view() ), diff --git a/pitch_deck_generator/decks/api/views.py b/pitch_deck_generator/decks/api/views.py index ae521a8..4749bb5 100644 --- a/pitch_deck_generator/decks/api/views.py +++ b/pitch_deck_generator/decks/api/views.py @@ -7,6 +7,7 @@ AnswerSerializer, BasePitchDeckSerializer, HintSerializer, + PdfToPPTXSerializer, PitchDeckPresentationSerializer, PitchDeckSerializer, QuestionSerializer, @@ -74,6 +75,7 @@ def get(self, request, *args, **kwargs): class GetDeckPresentationDataApiView(generics.GenericAPIView): + queryset = PitchDeck.objects.none() serializer_class = PitchDeckPresentationSerializer structure = { @@ -84,7 +86,7 @@ class GetDeckPresentationDataApiView(generics.GenericAPIView): 5: ["market_values", "users"], 6: ["competitors", "competitors_strength", "competitors_low", "advantages"], 7: ["money", "finance_model"], - 8: ["how_much_investments", "financial_indicators"], + 8: ["how_much_investments", "financial_indicators", "users_metrics"], 9: ["your_role", "your_teammates", "past_investors"], 10: ["how_much_investments", "time_to_spend", "investments_sold"], 11: ["company_value", "future_value", "time_to_spend"], @@ -97,14 +99,27 @@ def get(self, request, *args, **kwargs): PitchDeck, id=self.kwargs["deck_id"], ) + re_data = { + "deck": BasePitchDeckSerializer().to_representation(deck), + } resp = [] data = deck.questions for slide, tags in self.structure.items(): slide_data = {"slide": slide, "data": []} for tag in tags: - slide_data["data"].append( - {"slug": tag, "answer": data[tag] if tag in data else {}} - ) - resp.append(slide_data) + b_data = {} + if tag in data: + if "answer" in data[tag]: + b_data["answer"] = data[tag]["answer"] + if "photos" in data[tag]: + b_data["photos"] = data[tag]["photos"] - return Response(resp) + slide_data["data"].append({"slug": tag, **b_data}) + resp.append(slide_data) + re_data["slides"] = resp + return Response(re_data) + + +class ConvertPdfToPPTXApiView(generics.CreateAPIView): + serializer_class = PdfToPPTXSerializer + parser_classes = [FormParser, MultiPartParser] diff --git a/pitch_deck_generator/decks/migrations/0008_pdftopptxstorage_alter_question_type.py b/pitch_deck_generator/decks/migrations/0008_pdftopptxstorage_alter_question_type.py new file mode 100644 index 0000000..8bfb659 --- /dev/null +++ b/pitch_deck_generator/decks/migrations/0008_pdftopptxstorage_alter_question_type.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.4 on 2023-08-26 21:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("decks", "0007_alter_question_type_alter_questionanswer_deck"), + ] + + operations = [ + migrations.CreateModel( + name="PdfToPPTXStorage", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("pdf", models.FileField(upload_to="pdf/")), + ("pptx", models.FileField(blank=True, null=True, upload_to="pptx/")), + ], + ), + migrations.AlterField( + model_name="question", + name="type", + field=models.CharField( + choices=[ + ("text", "Text"), + ("number", "Number"), + ("text_array", "text array"), + ("range", "Range"), + ("multiple_range", "multiple range"), + ("select", "Select"), + ("link", "Link"), + ("date", "Date"), + ("multiple_date_description", "multiple date description"), + ("photo", "Photo"), + ("multiple_photo", "multiple photo"), + ("photo_description", "photo description"), + ("multiple_link_description", "multiple link description"), + ("multiple_photo_description", "multiple photo description"), + ("multiple_links", "multiple links"), + ], + max_length=26, + ), + ), + ] diff --git a/pitch_deck_generator/decks/models.py b/pitch_deck_generator/decks/models.py index 1714794..808b8d8 100644 --- a/pitch_deck_generator/decks/models.py +++ b/pitch_deck_generator/decks/models.py @@ -85,3 +85,8 @@ class QuestionAnswerPhoto(models.Model): "QuestionAnswer", related_name="photos", on_delete=models.CASCADE ) file = models.ImageField(upload_to="uploads/") + + +class PdfToPPTXStorage(models.Model): + pdf = models.FileField(upload_to="pdf/") + pptx = models.FileField(upload_to="pptx/", blank=True, null=True) diff --git a/pitch_deck_generator/decks/signals.py b/pitch_deck_generator/decks/signals.py index c87524b..49b7d3b 100644 --- a/pitch_deck_generator/decks/signals.py +++ b/pitch_deck_generator/decks/signals.py @@ -1,8 +1,18 @@ +from io import BytesIO + +import requests +from django.core.files import File from django.db.models.signals import post_save, pre_save from django.dispatch import receiver -from pitch_deck_generator.decks.models import PitchDeck, QuestionAnswer +from pitch_deck_generator.decks.models import ( + PdfToPPTXStorage, + PitchDeck, + QuestionAnswer, +) from pitch_deck_generator.decks.tasks import ( + ML_HOST, + create_images_mokups, generate_numeric_values, qenerate_answer_qr, run_pitch_deck_calculation, @@ -23,9 +33,14 @@ def question_answer_create(sender, instance: QuestionAnswer, created, **kwargs): generate_numeric_values.apply_async( kwargs={"pk": instance.deck.pk}, countdown=1 ) + elif instance.question.inner_tag == "names": + instance.deck.name = instance.answer + instance.deck.save() elif instance.question.inner_tag in ["finance_model"]: - qenerate_answer_qr.apply_async(kwargs={"pk": instance.pk}, countdown=1) - save_answer_to_deck.apply_async(kwargs={"pk": instance.pk}, countdown=5) + qenerate_answer_qr.apply_async(kwargs={"pk": instance.pk}, countdown=5) + elif instance.question.inner_tag == "images": + create_images_mokups.apply_async(kwargs={"pk": instance.pk}, countdown=5) + save_answer_to_deck.apply_async(kwargs={"pk": instance.pk}, countdown=10) @receiver(pre_save, sender=QuestionAnswer) @@ -36,5 +51,23 @@ def question_answer_update(sender, instance: QuestionAnswer, **kwargs): kwargs={"pk": instance.deck.pk}, countdown=1 ) elif instance.question.inner_tag in ["finance_model"]: - qenerate_answer_qr.apply_async(kwargs={"pk": instance.pk}, countdown=1) - save_answer_to_deck.apply_async(kwargs={"pk": instance.pk}, countdown=5) + qenerate_answer_qr.apply_async(kwargs={"pk": instance.pk}, countdown=5) + elif instance.question.inner_tag == "images": + create_images_mokups.apply_async(kwargs={"pk": instance.pk}, countdown=5) + save_answer_to_deck.apply_async(kwargs={"pk": instance.pk}, countdown=10) + + +@receiver(post_save, sender=PdfToPPTXStorage) +def pdt_to_pptx_convert(sender, instance: PdfToPPTXStorage, created, **kwargs): + if created: + with open(instance.pdf.path, "rb") as f: + r = requests.post(ML_HOST + "convert-to-pptx", files={"in_file": f}).json() + data = requests.get(ML_HOST + r["file"][1:]).content + instance.pptx.save( + instance.pdf.path.split("/")[-1].replace("pdf", "pptx"), + File( + BytesIO(data), + instance.pdf.path.split("/")[-1].replace("pdf", "pptx"), + ), + ) + instance.save() diff --git a/pitch_deck_generator/decks/tasks.py b/pitch_deck_generator/decks/tasks.py index 91f74d4..5ec43e4 100644 --- a/pitch_deck_generator/decks/tasks.py +++ b/pitch_deck_generator/decks/tasks.py @@ -13,8 +13,9 @@ QuestionAnswerPhoto, QuestionDeckHint, ) +from pitch_deck_generator.decks.services import get_image_mokeup -ML_HOST = "https://purple-kids-drive.loca.lt/" +ML_HOST = "https://forty-eggs-slide.loca.lt/" data_types = { "names": ("text", 1), @@ -166,3 +167,13 @@ def qenerate_answer_qr(pk: int): answer=qa, file=File(tmp, name="qr.png"), ) + + +@shared_task +def create_images_mokups(pk: int): + qa = QuestionAnswer.objects.get(pk=pk) + for image in qa.photos.all(): + mokup_path = get_image_mokeup(image.file.path) + with open(mokup_path, "rb") as f: + image.file = File(f, name="mokup.png") + image.save()