Thought I had staged it

This commit is contained in:
Carlos-Mesquita
2024-11-09 09:30:54 +00:00
parent 09998478d1
commit e955a16abf
15 changed files with 343 additions and 466 deletions

View File

@@ -1,8 +1,10 @@
import random
from typing import Optional from typing import Optional
from dependency_injector.wiring import Provide, inject from dependency_injector.wiring import Provide, inject
from fastapi import APIRouter, Depends, Path, Query, UploadFile from fastapi import APIRouter, Depends, Path, Query, UploadFile
from app.configs.constants import EducationalContent
from app.dtos.reading import ReadingDTO from app.dtos.reading import ReadingDTO
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
from app.controllers.abc import IReadingController from app.controllers.abc import IReadingController
@@ -21,8 +23,6 @@ async def upload(
solutions: UploadFile = None, solutions: UploadFile = None,
reading_controller: IReadingController = Depends(Provide[controller]) reading_controller: IReadingController = Depends(Provide[controller])
): ):
print(exercises.filename)
#print(solutions.filename)
return await reading_controller.import_exam(exercises, solutions) return await reading_controller.import_exam(exercises, solutions)
@reading_router.get( @reading_router.get(
@@ -36,6 +36,7 @@ async def generate_passage(
passage: int = Path(..., ge=1, le=3), passage: int = Path(..., ge=1, le=3),
reading_controller: IReadingController = Depends(Provide[controller]) reading_controller: IReadingController = Depends(Provide[controller])
): ):
topic = random.choice(EducationalContent.TOPICS) if not topic else topic
return await reading_controller.generate_reading_passage(passage, topic, word_count) return await reading_controller.generate_reading_passage(passage, topic, word_count)
@reading_router.post( @reading_router.post(

View File

@@ -2,31 +2,57 @@ import random
from typing import Optional from typing import Optional
from dependency_injector.wiring import inject, Provide from dependency_injector.wiring import inject, Provide
from fastapi import APIRouter, Path, Query, Depends, BackgroundTasks from fastapi import APIRouter, Path, Query, Depends
from pydantic import BaseModel
from app.dtos.video import Task, TaskStatus
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
from app.configs.constants import EducationalContent from app.configs.constants import EducationalContent
from app.controllers.abc import ISpeakingController from app.controllers.abc import ISpeakingController
from app.dtos.speaking import (
SaveSpeakingDTO, GenerateVideo1DTO, GenerateVideo2DTO, GenerateVideo3DTO
)
controller = "speaking_controller" controller = "speaking_controller"
speaking_router = APIRouter() speaking_router = APIRouter()
@speaking_router.get( class Video(BaseModel):
'/1', text: str
avatar: Optional[str] = None
@speaking_router.post(
'/media',
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))] dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
) )
@inject @inject
async def get_speaking_task( async def generate_video(
first_topic: str = Query(default=random.choice(EducationalContent.MTI_TOPICS)), video: Video,
second_topic: str = Query(default=random.choice(EducationalContent.MTI_TOPICS)),
difficulty: str = Query(default=random.choice(EducationalContent.DIFFICULTIES)),
speaking_controller: ISpeakingController = Depends(Provide[controller]) speaking_controller: ISpeakingController = Depends(Provide[controller])
): ):
return await speaking_controller.get_speaking_part(1, first_topic, difficulty, second_topic) return await speaking_controller.generate_video(video.text, video.avatar)
@speaking_router.get(
'/media/{vid_id}',
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
)
@inject
async def pool_video(
vid_id: str = Path(...),
speaking_controller: ISpeakingController = Depends(Provide[controller])
):
return await speaking_controller.pool_video(vid_id)
@speaking_router.get(
'/avatars',
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
)
@inject
async def get_avatars(
speaking_controller: ISpeakingController = Depends(Provide[controller])
):
return await speaking_controller.get_avatars()
@speaking_router.get( @speaking_router.get(
@@ -35,64 +61,27 @@ async def get_speaking_task(
) )
@inject @inject
async def get_speaking_task( async def get_speaking_task(
task: int = Path(..., ge=2, le=3), task: int = Path(..., ge=1, le=3),
topic: str = Query(default=random.choice(EducationalContent.MTI_TOPICS)), topic: Optional[str] = Query(None),
first_topic: Optional[str] = Query(None),
second_topic: Optional[str] = Query(None),
difficulty: str = Query(default=random.choice(EducationalContent.DIFFICULTIES)), difficulty: str = Query(default=random.choice(EducationalContent.DIFFICULTIES)),
speaking_controller: ISpeakingController = Depends(Provide[controller]) speaking_controller: ISpeakingController = Depends(Provide[controller])
): ):
return await speaking_controller.get_speaking_part(task, topic, difficulty) if not second_topic:
topic_or_first_topic = topic if topic else random.choice(EducationalContent.MTI_TOPICS)
else:
topic_or_first_topic = first_topic if first_topic else random.choice(EducationalContent.MTI_TOPICS)
second_topic = second_topic if second_topic else random.choice(EducationalContent.MTI_TOPICS)
return await speaking_controller.get_speaking_part(task, topic_or_first_topic, second_topic, difficulty)
@speaking_router.post(
'/',
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
)
@inject
async def save_speaking(
data: SaveSpeakingDTO,
background_tasks: BackgroundTasks,
speaking_controller: ISpeakingController = Depends(Provide[controller])
):
return await speaking_controller.save_speaking(data, background_tasks)
"""
async def generate_video(self, text: str, avatar: str):
return await self._vid_gen.create_video(text, avatar)
@speaking_router.post( async def pool_video(self, vid_id: str):
'/generate_video/1', return await self._vid_gen.pool_status(vid_id)
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))] """
)
@inject
async def generate_video_1(
data: GenerateVideo1DTO,
speaking_controller: ISpeakingController = Depends(Provide[controller])
):
return await speaking_controller.generate_video(
1, data.avatar, data.first_topic, data.questions, second_topic=data.second_topic
)
@speaking_router.post(
'/generate_video/2',
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
)
@inject
async def generate_video_2(
data: GenerateVideo2DTO,
speaking_controller: ISpeakingController = Depends(Provide[controller])
):
return await speaking_controller.generate_video(
2, data.avatar, data.topic, [data.question], prompts=data.prompts, suffix=data.suffix
)
@speaking_router.post(
'/generate_video/3',
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
)
@inject
async def generate_video_3(
data: GenerateVideo3DTO,
speaking_controller: ISpeakingController = Depends(Provide[controller])
):
return await speaking_controller.generate_video(
3, data.avatar, data.topic, data.questions
)

View File

@@ -99,87 +99,6 @@ class QuestionType(Enum):
READING_PASSAGE_3 = "Reading Passage 3" READING_PASSAGE_3 = "Reading Passage 3"
class HeygenAvatars(Enum):
MATTHEW_NOAH = "5912afa7c77c47d3883af3d874047aaf"
VERA_CERISE = "9e58d96a383e4568a7f1e49df549e0e4"
EDWARD_TONY = "d2cdd9c0379a4d06ae2afb6e5039bd0c"
TANYA_MOLLY = "045cb5dcd00042b3a1e4f3bc1c12176b"
KAYLA_ABBI = "1ae1e5396cc444bfad332155fdb7a934"
JEROME_RYAN = "0ee6aa7cc1084063a630ae514fccaa31"
TYLER_CHRISTOPHER = "5772cff935844516ad7eeff21f839e43"
from enum import Enum
class ELAIAvatars(Enum):
# Works
GIA_BUSINESS = {
"avatar_code": "gia.business",
"avatar_gender": "female",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/gia/business/gia_business.png",
"avatar_canvas": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/gia/business/gia_business.png",
"voice_id": "EXAVITQu4vr4xnSDxMaL",
"voice_provider": "elevenlabs"
}
# Works
VADIM_BUSINESS = {
"avatar_code": "vadim.business",
"avatar_gender": "male",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/vadim/business/vadim_business.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/vadim/business/vadim_business.png",
"voice_id": "flq6f7yk4E4fJM5XTYuZ",
"voice_provider": "elevenlabs"
}
ORHAN_BUSINESS = {
"avatar_code": "orhan.business",
"avatar_gender": "male",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/orhan/business/orhan.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/orhan/business/orhan.png",
"voice_id": "en-US-AndrewMultilingualNeural",
"voice_provider": "azure"
}
FLORA_BUSINESS = {
"avatar_code": "flora.business",
"avatar_gender": "female",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/flora/business/flora_business.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/flora/business/flora_business.png",
"voice_id": "en-US-JaneNeural",
"voice_provider": "azure"
}
SCARLETT_BUSINESS = {
"avatar_code": "scarlett.business",
"avatar_gender": "female",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/scarlett/business/scarlett_business.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/scarlett/business/scarlett_business.png",
"voice_id": "en-US-NancyNeural",
"voice_provider": "azure"
}
PARKER_CASUAL = {
"avatar_code": "parker.casual",
"avatar_gender": "male",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/parker/casual/parker_casual.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/parker/casual/parker_casual.png",
"voice_id": "en-US-TonyNeural",
"voice_provider": "azure"
}
ETHAN_BUSINESS = {
"avatar_code": "ethan.business",
"avatar_gender": "male",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/ethan/business/ethan_business_low.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/ethan/business/ethan_business_low.png",
"voice_id": "en-US-JasonNeural",
"voice_provider": "azure"
}
class FilePaths: class FilePaths:
AUDIO_FILES_PATH = 'download-audio/' AUDIO_FILES_PATH = 'download-audio/'
FIREBASE_LISTENING_AUDIO_FILES_PATH = 'listening_recordings/' FIREBASE_LISTENING_AUDIO_FILES_PATH = 'listening_recordings/'

View File

@@ -44,11 +44,17 @@ class DependencyInjector:
self._container.llm = providers.Factory(OpenAI, client=self._container.openai_client) self._container.llm = providers.Factory(OpenAI, client=self._container.openai_client)
self._container.stt = providers.Factory(OpenAIWhisper, model=self._container.whisper_model) self._container.stt = providers.Factory(OpenAIWhisper, model=self._container.whisper_model)
self._container.tts = providers.Factory(AWSPolly, client=self._container.polly_client) self._container.tts = providers.Factory(AWSPolly, client=self._container.polly_client)
with open('app/services/impl/third_parties/elai/elai_conf.json', 'r') as file: with open('app/services/impl/third_parties/elai/conf.json', 'r') as file:
elai_conf = json.load(file) elai_conf = json.load(file)
with open('app/services/impl/third_parties/elai/avatars.json', 'r') as file:
elai_avatars = json.load(file)
with open('app/services/impl/third_parties/heygen/avatars.json', 'r') as file:
heygen_avatars = json.load(file)
self._container.vid_gen = providers.Factory( self._container.vid_gen = providers.Factory(
ELAI, client=self._container.http_client, token=os.getenv("ELAI_TOKEN"), conf=elai_conf Heygen, client=self._container.http_client, token=os.getenv("HEY_GEN_TOKEN"), avatars=heygen_avatars
) )
self._container.ai_detector = providers.Factory( self._container.ai_detector = providers.Factory(
GPTZero, client=self._container.http_client, gpt_zero_key=os.getenv("GPT_ZERO_API_KEY") GPTZero, client=self._container.http_client, gpt_zero_key=os.getenv("GPT_ZERO_API_KEY")
@@ -79,8 +85,8 @@ class DependencyInjector:
self._container.reading_service = providers.Factory(ReadingService, llm=self._container.llm) self._container.reading_service = providers.Factory(ReadingService, llm=self._container.llm)
self._container.speaking_service = providers.Factory( self._container.speaking_service = providers.Factory(
SpeakingService, llm=self._container.llm, vid_gen=self._container.vid_gen, SpeakingService, llm=self._container.llm,
file_storage=self._container.firebase_instance, document_store=self._container.document_store, file_storage=self._container.firebase_instance,
stt=self._container.stt stt=self._container.stt
) )
@@ -144,7 +150,7 @@ class DependencyInjector:
) )
self._container.speaking_controller = providers.Factory( self._container.speaking_controller = providers.Factory(
SpeakingController, speaking_service=self._container.speaking_service SpeakingController, speaking_service=self._container.speaking_service, vid_gen=self._container.vid_gen
) )
self._container.writing_controller = providers.Factory( self._container.writing_controller = providers.Factory(

View File

@@ -7,19 +7,17 @@ from fastapi import BackgroundTasks
class ISpeakingController(ABC): class ISpeakingController(ABC):
@abstractmethod @abstractmethod
async def get_speaking_part(self, task: int, topic: str, difficulty: str, second_topic: Optional[str] = None): async def get_speaking_part(self, task: int, topic: str, second_topic: str, difficulty: str):
pass pass
@abstractmethod @abstractmethod
async def save_speaking(self, data, background_tasks: BackgroundTasks): async def get_avatars(self):
pass pass
@abstractmethod @abstractmethod
async def generate_video( async def generate_video(self, text: str, avatar: Optional[str]):
self, part: int, avatar: str, topic: str, questions: list[str], pass
*,
second_topic: Optional[str] = None, @abstractmethod
prompts: Optional[list[str]] = None, async def pool_video(self, vid_id: str):
suffix: Optional[str] = None,
):
pass pass

View File

@@ -1,47 +1,26 @@
import logging import logging
import uuid import random
from typing import Optional from typing import Optional
from fastapi import BackgroundTasks
from app.controllers.abc import ISpeakingController from app.controllers.abc import ISpeakingController
from app.dtos.speaking import SaveSpeakingDTO from app.services.abc import ISpeakingService, IVideoGeneratorService
from app.services.abc import ISpeakingService
from app.configs.constants import ExamVariant, MinTimers
from app.configs.question_templates import getSpeakingTemplate
class SpeakingController(ISpeakingController): class SpeakingController(ISpeakingController):
def __init__(self, speaking_service: ISpeakingService): def __init__(self, speaking_service: ISpeakingService, vid_gen: IVideoGeneratorService):
self._service = speaking_service self._service = speaking_service
self._vid_gen = vid_gen
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
async def get_speaking_part(self, task: int, topic: str, difficulty: str, second_topic: Optional[str] = None): async def get_speaking_part(self, task: int, topic: str, second_topic: str, difficulty: str):
return await self._service.get_speaking_part(task, topic, difficulty, second_topic) return await self._service.get_speaking_part(task, topic, second_topic, difficulty)
async def save_speaking(self, data: SaveSpeakingDTO, background_tasks: BackgroundTasks): async def get_avatars(self):
exercises = data.exercises return await self._vid_gen.get_avatars()
min_timer = data.minTimer
template = getSpeakingTemplate() async def generate_video(self, text: str, avatar: Optional[str]):
template["minTimer"] = min_timer return await self._vid_gen.create_video(text, avatar)
if min_timer < MinTimers.SPEAKING_MIN_TIMER_DEFAULT: async def pool_video(self, vid_id: str):
template["variant"] = ExamVariant.PARTIAL.value return await self._vid_gen.pool_status(vid_id)
else:
template["variant"] = ExamVariant.FULL.value
req_id = str(uuid.uuid4())
self._logger.info(f'Received request to save speaking with id: {req_id}')
background_tasks.add_task(self._service.create_videos_and_save_to_db, exercises, template, req_id)
self._logger.info('Started background task to save speaking.')
# Return response without waiting for create_videos_and_save_to_db to finish
return {**template, "id": req_id}
async def generate_video(self, *args, **kwargs):
return await self._service.generate_video(*args, **kwargs)

View File

@@ -1,9 +1,9 @@
import random import random
from typing import List, Dict from typing import List, Dict, Optional
from pydantic import BaseModel from pydantic import BaseModel
from app.configs.constants import MinTimers, ELAIAvatars from app.configs.constants import MinTimers
class SaveSpeakingDTO(BaseModel): class SaveSpeakingDTO(BaseModel):
@@ -21,22 +21,7 @@ class GradeSpeakingAnswersDTO(BaseModel):
class GenerateVideo1DTO(BaseModel): class GenerateVideo1DTO(BaseModel):
avatar: str = (random.choice(list(ELAIAvatars))).name avatar: str = Optional[str]
questions: List[str] questions: List[str]
first_topic: str first_topic: str
second_topic: str second_topic: str
class GenerateVideo2DTO(BaseModel):
avatar: str = (random.choice(list(ELAIAvatars))).name
prompts: List[str] = []
suffix: str = ""
question: str
topic: str
class GenerateVideo3DTO(BaseModel):
avatar: str = (random.choice(list(ELAIAvatars))).name
questions: List[str]
topic: str

15
app/dtos/video.py Normal file
View File

@@ -0,0 +1,15 @@
import random
from enum import Enum
from typing import Optional
from pydantic import BaseModel
class TaskStatus(Enum):
STARTED = "STARTED"
IN_PROGRESS = "IN_PROGRESS"
COMPLETED = "COMPLETED"
ERROR = "ERROR"
class Task(BaseModel):
status: TaskStatus
result: Optional[str] = None

View File

@@ -6,7 +6,7 @@ class ISpeakingService(ABC):
@abstractmethod @abstractmethod
async def get_speaking_part( async def get_speaking_part(
self, part: int, topic: str, difficulty: str, second_topic: Optional[str] = None self, part: int, topic: str, second_topic: str, difficulty: str
) -> Dict: ) -> Dict:
pass pass
@@ -14,16 +14,3 @@ class ISpeakingService(ABC):
async def grade_speaking_task(self, task: int, answers: List[Dict]) -> Dict: async def grade_speaking_task(self, task: int, answers: List[Dict]) -> Dict:
pass pass
@abstractmethod
async def create_videos_and_save_to_db(self, exercises: List[Dict], template: Dict, req_id: str):
pass
@abstractmethod
async def generate_video(
self, part: int, avatar: str, topic: str, questions: list[str],
*,
second_topic: Optional[str] = None,
prompts: Optional[list[str]] = None,
suffix: Optional[str] = None,
):
pass

View File

@@ -1,8 +1,22 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, List
class IVideoGeneratorService(ABC): class IVideoGeneratorService(ABC):
def __init__(self, avatars: Dict):
self._avatars = avatars
async def get_avatars(self) -> List[Dict]:
return [
{"name": name, "gender": data["avatar_gender"]}
for name, data in self._avatars.items()
]
@abstractmethod @abstractmethod
async def create_video(self, text: str, avatar: str): async def create_video(self, text: str, avatar: str):
pass pass
@abstractmethod
async def pool_status(self, video_id: str):
pass

View File

@@ -9,7 +9,7 @@ from app.repositories.abc import IFileStorage, IDocumentStore
from app.services.abc import ISpeakingService, ILLMService, IVideoGeneratorService, ISpeechToTextService from app.services.abc import ISpeakingService, ILLMService, IVideoGeneratorService, ISpeechToTextService
from app.configs.constants import ( from app.configs.constants import (
FieldsAndExercises, GPTModels, TemperatureSettings, FieldsAndExercises, GPTModels, TemperatureSettings,
ELAIAvatars, FilePaths FilePaths
) )
from app.helpers import TextHelper from app.helpers import TextHelper
@@ -17,14 +17,12 @@ from app.helpers import TextHelper
class SpeakingService(ISpeakingService): class SpeakingService(ISpeakingService):
def __init__( def __init__(
self, llm: ILLMService, vid_gen: IVideoGeneratorService, self, llm: ILLMService,
file_storage: IFileStorage, document_store: IDocumentStore, file_storage: IFileStorage,
stt: ISpeechToTextService stt: ISpeechToTextService
): ):
self._llm = llm self._llm = llm
self._vid_gen = vid_gen
self._file_storage = file_storage self._file_storage = file_storage
self._document_store = document_store
self._stt = stt self._stt = stt
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
@@ -102,7 +100,7 @@ class SpeakingService(ISpeakingService):
} }
async def get_speaking_part( async def get_speaking_part(
self, part: int, topic: str, difficulty: str, second_topic: Optional[str] = None self, part: int, topic: str, second_topic: str, difficulty: str
) -> Dict: ) -> Dict:
task_values = self._tasks[f'task_{part}']['get'] task_values = self._tasks[f'task_{part}']['get']
@@ -416,190 +414,6 @@ class SpeakingService(ISpeakingService):
) )
return response["fixed_text"] return response["fixed_text"]
async def create_videos_and_save_to_db(self, exercises, template, req_id):
template = await self._create_video_per_part(exercises, template, 1)
template = await self._create_video_per_part(exercises, template, 2)
template = await self._create_video_per_part(exercises, template, 3)
await self._document_store.save_to_db_with_id("speaking", template, req_id)
self._logger.info(f'Saved speaking to DB with id {req_id} : {str(template)}')
async def _create_video_per_part(self, exercises: List[Dict], template: Dict, part: int):
avatar = (random.choice(list(ELAIAvatars))).name
template_index = part - 1
# Using list comprehension to find the element with the desired value in the 'type' field
found_exercises = [element for element in exercises if element.get('type') == part]
# Check if any elements were found
if found_exercises:
exercise = found_exercises[0]
self._logger.info(f'Creating video for speaking part {part}')
if part in {1, 3}:
questions = []
for question in exercise["questions"]:
result = await self._create_video(
question,
avatar,
f'Failed to create video for part {part} question: {str(exercise["question"])}'
)
if result is not None:
video = {
"text": question,
"video_path": result["video_path"],
"video_url": result["video_url"]
}
questions.append(video)
template["exercises"][template_index]["prompts"] = questions
if part == 1:
template["exercises"][template_index]["first_title"] = exercise["first_topic"]
template["exercises"][template_index]["second_title"] = exercise["second_topic"]
else:
template["exercises"][template_index]["title"] = exercise["topic"]
else:
result = await self._create_video(
exercise["question"],
avatar,
f'Failed to create video for part {part} question: {str(exercise["question"])}'
)
if result is not None:
template["exercises"][template_index]["prompts"] = exercise["prompts"]
template["exercises"][template_index]["text"] = exercise["question"]
template["exercises"][template_index]["title"] = exercise["topic"]
template["exercises"][template_index]["video_url"] = result["video_url"]
template["exercises"][template_index]["video_path"] = result["video_path"]
if not found_exercises:
template["exercises"].pop(template_index)
return template
async def generate_video(
self, part: int, avatar: str, topic: str, questions: list[str],
*,
second_topic: Optional[str] = None,
prompts: Optional[list[str]] = None,
suffix: Optional[str] = None,
):
params = locals()
params.pop('self')
request_id = str(uuid.uuid4())
self._logger.info(
f'POST - generate_video_{part} - Received request to generate video {part}. '
f'Use this id to track the logs: {request_id} - Request data: " + {params}'
)
part_questions = self._get_part_questions(part, questions, avatar)
videos = []
self._logger.info(f'POST - generate_video_{part} - {request_id} - Creating videos for speaking part {part}.')
for question in part_questions:
self._logger.info(f'POST - generate_video_{part} - {request_id} - Creating video for question: {question}')
result = await self._create_video(
question,
avatar,
'POST - generate_video_{p} - {r} - Failed to create video for part {p} question: {q}'.format(
p=part, r=request_id, q=question
)
)
if result is not None:
self._logger.info(f'POST - generate_video_{part} - {request_id} - Video created')
self._logger.info(
f'POST - generate_video_{part} - {request_id} - Uploaded video to firebase: {result["video_url"]}'
)
video = {
"text": question,
"video_path": result["video_path"],
"video_url": result["video_url"]
}
videos.append(video)
if part == 2 and len(videos) == 0:
raise Exception(f'Failed to create video for part 2 question: {questions[0]}')
return self._get_part_response(part, topic, videos, second_topic, prompts, suffix)
@staticmethod
def _get_part_questions(part: int, questions: list[str], avatar: str):
part_questions: list[str] = []
if part == 1:
id_to_name = {
"5912afa7c77c47d3883af3d874047aaf": "MATTHEW",
"9e58d96a383e4568a7f1e49df549e0e4": "VERA",
"d2cdd9c0379a4d06ae2afb6e5039bd0c": "EDWARD",
"045cb5dcd00042b3a1e4f3bc1c12176b": "TANYA",
"1ae1e5396cc444bfad332155fdb7a934": "KAYLA",
"0ee6aa7cc1084063a630ae514fccaa31": "JEROME",
"5772cff935844516ad7eeff21f839e43": "TYLER",
}
part_questions.extend(
[
"Hello my name is " + id_to_name.get(avatar) + ", what is yours?",
"Do you work or do you study?",
*questions
]
)
elif part == 2:
# Removed as the examiner should not say what is on the card.
# question = question + " In your answer you should consider: " + " ".join(prompts) + suffix
part_questions.append(f'{questions[0]}\nYou have 1 minute to take notes.')
elif part == 3:
part_questions = questions
return part_questions
@staticmethod
def _get_part_response(
part: int,
topic: str,
videos: list[dict],
second_topic: Optional[str],
prompts: Optional[list[str]],
suffix: Optional[str]
):
response = {}
if part == 1:
response = {
"prompts": videos,
"first_title": topic,
"second_title": second_topic,
"type": "interactiveSpeaking"
}
if part == 2:
response = {
"prompts": prompts,
"title": topic,
"suffix": suffix,
"type": "speaking",
# includes text, video_url and video_path
**videos[0]
}
if part == 3:
response = {
"prompts": videos,
"title": topic,
"type": "interactiveSpeaking",
}
response["id"] = str(uuid.uuid4())
return response
async def _create_video(self, question: str, avatar: str, error_message: str):
result = await self._vid_gen.create_video(question, avatar)
if result is not None:
sound_file_path = FilePaths.VIDEO_FILES_PATH + result
firebase_file_path = FilePaths.FIREBASE_SPEAKING_VIDEO_FILES_PATH + result
url = await self._file_storage.upload_file_firebase_get_url(firebase_file_path, sound_file_path)
return {
"video_path": firebase_file_path,
"video_url": url
}
self._logger.error(error_message)
return None
@staticmethod @staticmethod
def _grade_template(): def _grade_template():

View File

@@ -1,15 +1,8 @@
import asyncio
import os
import logging
from asyncio import sleep
from copy import deepcopy from copy import deepcopy
from logging import getLogger
import aiofiles
from charset_normalizer.md import getLogger
from httpx import AsyncClient from httpx import AsyncClient
from app.configs.constants import ELAIAvatars from app.dtos.video import Task, TaskStatus
from app.services.abc import IVideoGeneratorService from app.services.abc import IVideoGeneratorService
@@ -17,7 +10,9 @@ class ELAI(IVideoGeneratorService):
_ELAI_ENDPOINT = 'https://apis.elai.io/api/v1/videos' _ELAI_ENDPOINT = 'https://apis.elai.io/api/v1/videos'
def __init__(self, client: AsyncClient, token: str, conf: dict): def __init__(self, client: AsyncClient, token: str, avatars: dict, *, conf: dict):
super().__init__(deepcopy(avatars))
self._http_client = client self._http_client = client
self._conf = deepcopy(conf) self._conf = deepcopy(conf)
self._logger = getLogger(__name__) self._logger = getLogger(__name__)
@@ -31,14 +26,13 @@ class ELAI(IVideoGeneratorService):
"Authorization": f"Bearer {token}" "Authorization": f"Bearer {token}"
} }
async def create_video(self, text: str, avatar: str): async def create_video(self, text: str, avatar: str):
avatar_url = ELAIAvatars[avatar].value.get("avatar_url") avatar_url = self._avatars[avatar].get("avatar_url")
avatar_code = ELAIAvatars[avatar].value.get("avatar_code") avatar_code = self._avatars[avatar].get("avatar_code")
avatar_gender = ELAIAvatars[avatar].value.get("avatar_gender") avatar_gender = self._avatars[avatar].get("avatar_gender")
avatar_canvas = ELAIAvatars[avatar].value.get("avatar_canvas") avatar_canvas = self._avatars[avatar].get("avatar_canvas")
voice_id = ELAIAvatars[avatar].value.get("voice_id") voice_id = self._avatars[avatar].get("voice_id")
voice_provider = ELAIAvatars[avatar].value.get("voice_provider") voice_provider = self._avatars[avatar].get("voice_provider")
self._conf["slides"][0]["canvas"]["objects"][0]["src"] = avatar_url self._conf["slides"][0]["canvas"]["objects"][0]["src"] = avatar_url
self._conf["slides"]["avatar"] = { self._conf["slides"]["avatar"] = {
@@ -59,37 +53,32 @@ class ELAI(IVideoGeneratorService):
if video_id: if video_id:
await self._http_client.post(f'{self._ELAI_ENDPOINT}/render/{video_id}', headers=self._GET_HEADER) await self._http_client.post(f'{self._ELAI_ENDPOINT}/render/{video_id}', headers=self._GET_HEADER)
return Task(
result=video_id,
status=TaskStatus.STARTED,
)
else:
return Task(status=TaskStatus.ERROR)
while True: async def pool_status(self, video_id: str) -> Task:
response = await self._http_client.get(f'{self._ELAI_ENDPOINT}/{video_id}', headers=self._GET_HEADER) response = await self._http_client.get(f'{self._ELAI_ENDPOINT}/{video_id}', headers=self._GET_HEADER)
response_data = response.json() response_data = response.json()
if response_data['status'] == 'ready': if response_data['status'] == 'ready':
self._logger.info(response_data) self._logger.info(response_data)
return Task(
download_url = response_data.get('url') status=TaskStatus.COMPLETED,
output_directory = 'download-video/' result=response_data.get('url')
output_filename = video_id + '.mp4' )
elif response_data['status'] == 'failed':
response = await self._http_client.get(download_url) self._logger.error('Video creation failed.')
return Task(
if response.status_code == 200: status=TaskStatus.ERROR,
os.makedirs(output_directory, exist_ok=True) result=response_data.get('url')
output_path = os.path.join(output_directory, output_filename) )
else:
with open(output_path, 'wb') as f: self._logger.info('Video is still processing.')
f.write(response.content) return Task(
status=TaskStatus.IN_PROGRESS,
self._logger.info(f"File '{output_filename}' downloaded successfully.") result=video_id
return output_filename )
else:
self._logger.error(f"Failed to download file. Status code: {response.status_code}")
return None
elif response_data['status'] == 'failed':
self._logger.error('Video creation failed.')
break
else:
self._logger.info('Video is still processing. Checking again in 10 seconds...')
await sleep(10)

View File

@@ -0,0 +1,58 @@
{
"Gia": {
"avatar_code": "gia.business",
"avatar_gender": "female",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/gia/business/gia_business.png",
"avatar_canvas": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/gia/business/gia_business.png",
"voice_id": "EXAVITQu4vr4xnSDxMaL",
"voice_provider": "elevenlabs"
},
"Vadim": {
"avatar_code": "vadim.business",
"avatar_gender": "male",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/vadim/business/vadim_business.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/vadim/business/vadim_business.png",
"voice_id": "flq6f7yk4E4fJM5XTYuZ",
"voice_provider": "elevenlabs"
},
"Orhan": {
"avatar_code": "orhan.business",
"avatar_gender": "male",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/orhan/business/orhan.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/orhan/business/orhan.png",
"voice_id": "en-US-AndrewMultilingualNeural",
"voice_provider": "azure"
},
"Flora": {
"avatar_code": "flora.business",
"avatar_gender": "female",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/flora/business/flora_business.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/flora/business/flora_business.png",
"voice_id": "en-US-JaneNeural",
"voice_provider": "azure"
},
"Scarlett": {
"avatar_code": "scarlett.business",
"avatar_gender": "female",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/scarlett/business/scarlett_business.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/scarlett/business/scarlett_business.png",
"voice_id": "en-US-NancyNeural",
"voice_provider": "azure"
},
"Parker": {
"avatar_code": "parker.casual",
"avatar_gender": "male",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/parker/casual/parker_casual.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/parker/casual/parker_casual.png",
"voice_id": "en-US-TonyNeural",
"voice_provider": "azure"
},
"Ethan": {
"avatar_code": "ethan.business",
"avatar_gender": "male",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/ethan/business/ethan_business_low.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/ethan/business/ethan_business_low.png",
"voice_id": "en-US-JasonNeural",
"voice_provider": "azure"
}
}

View File

@@ -0,0 +1,93 @@
import asyncio
import os
import logging
import random
from copy import deepcopy
import aiofiles
from httpx import AsyncClient
from app.dtos.video import Task, TaskStatus
from app.services.abc import IVideoGeneratorService
class Heygen(IVideoGeneratorService):
_GET_VIDEO_URL = 'https://api.heygen.com/v1/video_status.get'
def __init__(self, client: AsyncClient, token: str, avatars: dict):
super().__init__(deepcopy(avatars))
self._get_header = {
'X-Api-Key': token
}
self._post_header = {
'X-Api-Key': token,
'Content-Type': 'application/json'
}
self._http_client = client
self._logger = logging.getLogger(__name__)
async def create_video(self, text: str, avatar: str):
if not avatar:
random_avatar_name = random.choice(list(self._avatars.keys()))
avatar = self._avatars[random_avatar_name]["id"]
#["id"]
else:
avatar = self._avatars[avatar]["id"]
create_video_url = f'https://api.heygen.com/v2/template/{avatar}/generate'
data = {
"test": False,
"caption": False,
"title": "video_title",
"variables": {
"script_here": {
"name": "script_here",
"type": "text",
"properties": {
"content": text
}
}
}
}
response = await self._http_client.post(create_video_url, headers=self._post_header, json=data)
self._logger.info(response.status_code)
self._logger.info(response.json())
video_id = response.json()["data"]["video_id"]
return Task(
result=video_id,
status=TaskStatus.STARTED,
)
async def pool_status(self, video_id: str) -> Task:
response = await self._http_client.get(self._GET_VIDEO_URL, headers=self._get_header, params={
'video_id': video_id
})
response_data = response.json()
status = response_data["data"]["status"]
error = response_data["data"]["error"]
if status != "completed" and error is None:
self._logger.info(f"Status: {status}")
return Task(
status=TaskStatus.IN_PROGRESS,
result=video_id
)
if error:
self._logger.error('Video creation failed.')
return Task(
status=TaskStatus.ERROR,
result=response_data.get('url')
)
url = response.json()['data']['video_url']
self._logger.info(f'Successfully generated video: {url}')
return Task(
status=TaskStatus.COMPLETED,
result=url
)

View File

@@ -0,0 +1,30 @@
{
"Matthew Noah": {
"id": "5912afa7c77c47d3883af3d874047aaf",
"avatar_gender": "male"
},
"Vera Cerise": {
"id": "9e58d96a383e4568a7f1e49df549e0e4",
"avatar_gender": "female"
},
"Edward Tony": {
"id": "d2cdd9c0379a4d06ae2afb6e5039bd0c",
"avatar_gender": "male"
},
"Tanya Molly": {
"id": "045cb5dcd00042b3a1e4f3bc1c12176b",
"avatar_gender": "female"
},
"Kayla Abbi": {
"id": "1ae1e5396cc444bfad332155fdb7a934",
"avatar_gender": "female"
},
"Jerome Ryan": {
"id": "0ee6aa7cc1084063a630ae514fccaa31",
"avatar_gender": "male"
},
"Tyler Christopher": {
"id": "5772cff935844516ad7eeff21f839e43",
"avatar_gender": "male"
}
}