Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Video editor supports transcripts [FC-0076] #36058

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion openedx/core/djangoapps/xblock/rest_api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def xblock_handler(
"""
# To support sandboxed XBlocks, custom frontends, and other use cases, we
# authenticate requests using a secure token in the URL. see
# openedx.core.djangoapps.xblock.utils.get_secure_hash_for_xblock_handler
# openedx.core.djangoapps.xblock.utils.get_secure_token_for_xblock_handler
# for details and rationale.
if not validate_secure_token_for_xblock_handler(user_id, str(usage_key), secure_token):
raise PermissionDenied("Invalid/expired auth token.")
Expand Down
42 changes: 42 additions & 0 deletions xmodule/tests/test_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,48 @@ def test_export_to_xml(self, mock_val_api):
course_id=self.block.scope_ids.usage_id.context_key,
)

def test_export_to_xml_without_video_id(self):
"""
Test that we write the correct XML without video_id on export.
"""
self.block.youtube_id_0_75 = 'izygArpw-Qo'
self.block.youtube_id_1_0 = 'p2Q6BrNhdh8'
self.block.youtube_id_1_25 = '1EeWXzPdhSA'
self.block.youtube_id_1_5 = 'rABDYkeK0x8'
self.block.show_captions = False
self.block.start_time = datetime.timedelta(seconds=1.0)
self.block.end_time = datetime.timedelta(seconds=60)
self.block.track = 'http://www.example.com/track'
self.block.handout = 'http://www.example.com/handout'
self.block.download_track = True
self.block.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source1.ogg']
self.block.download_video = True
self.block.transcripts = {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'}

xml = self.block.definition_to_xml(self.file_system)
parser = etree.XMLParser(remove_blank_text=True)
xml_string = '''\
<video
url_name="SampleProblem"
start_time="0:00:01"
show_captions="false"
end_time="0:01:00"
download_video="true"
download_track="true"
youtube="0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"
transcripts='{"ge": "german_translation.srt", "ua": "ukrainian_translation.srt"}'
>
<source src="http://www.example.com/source.mp4"/>
<source src="http://www.example.com/source1.ogg"/>
<track src="http://www.example.com/track"/>
<handout src="http://www.example.com/handout"/>
<transcript language="ge" src="german_translation.srt" />
<transcript language="ua" src="ukrainian_translation.srt" />
</video>
'''
expected = etree.XML(xml_string, parser=parser)
self.assertXmlEqual(expected, xml)

@patch('xmodule.video_block.video_block.edxval_api')
def test_export_to_xml_val_error(self, mock_val_api):
# Export should succeed without VAL data if video does not exist
Expand Down
22 changes: 22 additions & 0 deletions xmodule/video_block/transcripts_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,8 @@ def manage_video_subtitles_save(item, user, old_metadata=None, generate_translat
)
except TranscriptException:
pass
except AttributeError:
pass
if reraised_message:
item.save_with_metadata(user)
raise TranscriptException(reraised_message)
Expand Down Expand Up @@ -1019,6 +1021,26 @@ def get_transcript_from_contentstore(video, language, output_format, transcripts
except (KeyError, NotFoundError):
continue

if transcript_content is None and language == 'en':
# `get_transcript_for_video`` can get the transcript using just the filename,
# but in the above loop the filename from 'en' is overwritten.
#
# If it doesn't yet have the transcription and the language is 'en',
# check again but this time using the original filename.
#
# The use case for which this has been implemented is when copying a video from
# a library and pasting it into a course.
# The asset is copied, but we only have the filename to obtain the content.
try:
input_format, base_name, transcript_content = get_transcript_for_video(
video.location,
subs_id=None,
file_name=other_languages['en'],
language=language
)
except (KeyError, NotFoundError):
pass

if transcript_content is None:
raise NotFoundError('No transcript for `{lang}` language'.format(
lang=language
Expand Down
10 changes: 7 additions & 3 deletions xmodule/video_block/video_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -855,11 +855,15 @@ def definition_to_xml(self, resource_fs): # lint-amnesty, pylint: disable=too-m
if new_transcripts.get('en'):
xml.set('sub', '')

# Update `transcripts` attribute in the xml
xml.set('transcripts', json.dumps(transcripts, sort_keys=True))

except edxval_api.ValVideoNotFoundError:
pass
else:
if transcripts.get('en'):
xml.set('sub', '')

if transcripts:
# Update `transcripts` attribute in the xml
xml.set('transcripts', json.dumps(transcripts, sort_keys=True))

# Sorting transcripts for easy testing of resulting xml
for transcript_language in sorted(transcripts.keys()):
Expand Down
18 changes: 16 additions & 2 deletions xmodule/video_block/video_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
from django.core.files.base import ContentFile
from django.utils.timezone import now
from edxval.api import create_external_video, create_or_update_video_transcript, delete_video_transcript
from opaque_keys.edx.locator import CourseLocator
from opaque_keys.edx.locator import CourseLocator, LibraryLocatorV2
from opaque_keys.edx.keys import UsageKeyV2
from webob import Response
from xblock.core import XBlock
from xblock.exceptions import JsonHandlerError

from xmodule.exceptions import NotFoundError
from xmodule.fields import RelativeTime
from openedx.core.djangoapps.content_libraries import api as lib_api

from .transcripts_utils import (
Transcript,
Expand Down Expand Up @@ -517,8 +519,9 @@ def studio_transcript(self, request, dispatch):
try:
# Convert SRT transcript into an SJSON format
# and upload it to S3.
content = transcript_file.read()
sjson_subs = Transcript.convert(
content=transcript_file.read().decode('utf-8'),
content=content.decode('utf-8'),
input_format=Transcript.SRT,
output_format=Transcript.SJSON
).encode()
Expand All @@ -541,6 +544,17 @@ def studio_transcript(self, request, dispatch):
self.transcripts.pop(language_code, None)
self.transcripts[new_language_code] = f'{edx_video_id}-{new_language_code}.srt'
response = Response(json.dumps(payload), status=201)

if isinstance(self.scope_ids.usage_id, UsageKeyV2):
usage_key = self.scope_ids.usage_id
if isinstance(usage_key.context_key, LibraryLocatorV2):
# Save transcript as static asset in Learning Core if is a library component
filename = f"static/{self.transcripts[new_language_code]}"
lib_api.add_library_block_static_asset_file(
usage_key,
filename,
content,
)
except (TranscriptsGenerationException, UnicodeDecodeError):
response = Response(
json={
Expand Down
Loading