"""Transcribe episodes.""" import datetime from io import BytesIO, StringIO import boto3 from docx import Document from webvtt import Caption, WebVTT from podcast import config from podcast.constants.feature_flag import FEATURE_PODCAST_IA_RESTRUCTURE from podcast.logic import email from podcast.logic import user from podcast.logic import user_v2 as user_v2_logic from podcast.models import ows_asset_transcoder as oat from podcast.models import transcription as transcription_model from podcast.utils import feature_flag_utils from podcast.utils.exc import OwsError transcription_client = None def _transcription_client(): global transcription_client if not transcription_client: transcription_client = boto3.client('transcribe') return transcription_client def _start_transcription(job_name, job_uri, media_format, num_speakers, language): transcription = _transcription_client() settings = {} if num_speakers > 1: settings = { 'MaxSpeakerLabels': num_speakers, 'ShowSpeakerLabels': True } transcription.start_transcription_job( TranscriptionJobName=job_name, Media={'MediaFileUri': job_uri}, MediaFormat=media_format, LanguageCode=language, OutputBucketName=config.OUTPUT_ASSETS_BUCKET_NAME, Settings=settings ) def _timestamp(): return int(datetime.datetime.utcnow().timestamp()) def _start(episode_id, key, num_speakers, language): """Start a transcription.""" time = _timestamp() job_name = 'owspodcasttranscribe_{}_{}'.format(key, time) job_uri = 's3://{}/{}'.format(config.INPUT_ASSETS_BUCKET_NAME, key) media_format = key.split('.')[-1] _start_transcription(job_name, job_uri, media_format, num_speakers, language) transcription_model.start(episode_id) def start_for_episode(episode_id, data): """Start a transcription.""" user.current_user_has_read_only_access_then_raise() if feature_flag_utils.get_feature_flag(FEATURE_PODCAST_IA_RESTRUCTURE): user_v2_logic.current_user_owns_episodes_or_raise([episode_id]) else: user.current_user_owns_episodes_or_raise([episode_id]) asset = oat.get_episode_input_audio(episode_id) if asset: _start(episode_id, asset, data['num_speakers'], data['language']) def update_transcription(episode_id, transcript): # called by the lambda and the frontend """Update episode function. Args: episode_id (int): The unique identifier of the episode transcript (str): The transcription """ transcription = transcription_model.get(episode_id) email.send_transcription_finished(transcription) return transcription_model.update( episode_id, {'is_transcript_processing': False, 'transcript': transcript}) def get_transcription(episode_id): """Get episode transcription. Args: episode_id (int): The unique identifier of the episode Returns: transcript (dict): The transcription """ if feature_flag_utils.get_feature_flag(FEATURE_PODCAST_IA_RESTRUCTURE): user_v2_logic.current_user_owns_episodes_or_raise([episode_id]) else: user.current_user_owns_episodes_or_raise([episode_id]) transcription = transcription_model.get(episode_id) if not transcription: raise OwsError.not_found() return transcription.to_dict() def do_transcriptions_for_episodes_exist(episode_ids): """Check whether episodes have transcriptions. Args: episode_ids (int): The unique identifiers of the episodes """ if feature_flag_utils.get_feature_flag(FEATURE_PODCAST_IA_RESTRUCTURE): user_v2_logic.current_user_owns_episodes_or_raise(episode_ids) else: user.current_user_owns_episodes_or_raise(episode_ids) transcripts = transcription_model.get_all_by_episode_ids(episode_ids) transcripts_episode_ids = [transcript.episode_id for transcript in transcripts] has_transcription = [] for episode_id in episode_ids: episode_has_transcript = int(episode_id) in transcripts_episode_ids has_transcription.append({ 'transcription': episode_has_transcript, 'episode_id': int(episode_id)}) return {'items': has_transcription} def _is_new_caption(content, caption_text, idx, items): end_of_sentence = content[-1] in ['.', '!', '?'] and len(content) > 2 caption_overflow = len(caption_text) > 80 end_of_block = idx + 1 >= len(items) return caption_overflow or end_of_sentence or end_of_block def _format_content(content, caption_text): if len(caption_text) < 1: return content if (len(caption_text) + len(content)) > 40 and '\n' not in caption_text: return '\n{}'.format(content) return ' {}'.format(content) def _get_timestamp(time, word_formatted=False): seconds = float(time) timestamp = str(datetime.timedelta(seconds=seconds)) if word_formatted: return '[{}]'.format(timestamp) # add microseconds otherwise it will break when trying to create the caption if '.' not in timestamp: timestamp += '.000' return timestamp def _create_captions(speaker_blocks): vtt = WebVTT() for block in speaker_blocks: items = block['items'] caption_text = '' start = None for idx, item in enumerate(items): start = _get_timestamp(item['start_time']) if not start else start end = _get_timestamp(item['end_time']) caption_text += _format_content(item['content'], caption_text) if _is_new_caption(item['content'], caption_text, idx, items): caption = Caption(start, end, caption_text) vtt.captions.append(caption) caption_text = '' start = None return vtt def get_vtt_or_srt(transcript, file_type): """Download episode transaction in vtt or srt format.""" vtt_file = _create_captions(transcript) read_file = StringIO() vtt_file.write(read_file, file_type) f = BytesIO() f.write(read_file.getvalue().encode('utf-8')) f.seek(0) return f def _insert_timestamps(prev_end, item, timestamp): start = float(item['start_time']) end = float(item['end_time']) if start <= float(timestamp) < end: return True if not prev_end: return False return float(prev_end) <= float(timestamp) < start def get_doc(transcript, interval): """Download episode transaction in word format.""" timestamp = interval document = Document() prev_end_time = None for block in transcript: paragraph = '{}:'.format(block['speaker_label']) items = block['items'] for item in items: if _insert_timestamps(prev_end_time, item, timestamp): paragraph += ' {}'.format(_get_timestamp(timestamp, True)) timestamp += interval paragraph += ' {}'.format(item['content']) prev_end_time = item['end_time'] document.add_paragraph(paragraph) f = BytesIO() document.save(f) f.seek(0) return f def get_file(episode_id, file_type, interval): """Download episode transcription.""" if feature_flag_utils.get_feature_flag(FEATURE_PODCAST_IA_RESTRUCTURE): user_v2_logic.current_user_owns_episodes_or_raise([episode_id]) else: user.current_user_owns_episodes_or_raise([episode_id]) transcript = transcription_model.get(episode_id) if file_type == 'doc': return get_doc(transcript.transcript['transcript'], interval) if file_type in ['vtt', 'srt']: return get_vtt_or_srt(transcript.transcript['transcript'], file_type) raise OwsError.bad_request('Invalid file type requested') def delete_by_episode_id(episode_id): """Soft delete transcription by episode id.""" if feature_flag_utils.get_feature_flag(FEATURE_PODCAST_IA_RESTRUCTURE): user_v2_logic.current_user_owns_episodes_or_raise([episode_id]) else: user.current_user_owns_episodes_or_raise([episode_id]) transcription = transcription_model.get(episode_id) if transcription: transcription_model.delete(episode_id) return transcription.to_dict()