refactor: Move upload/delete transcript int video_config service
Merge pull request #37793 from openedx/farhan/upload-delete-transcript
This commit is contained in:
@@ -130,6 +130,7 @@ def _get_course_block(course_descriptor, user):
|
||||
""" Gets course block that takes into account user state and permissions """
|
||||
# Adding courseware imports here to insulate other apps (e.g. schedules) to
|
||||
# avoid import errors.
|
||||
# TODO remove the LMS dependency https://github.com/openedx/edx-platform/issues/37659
|
||||
from lms.djangoapps.courseware.model_data import FieldDataCache
|
||||
from lms.djangoapps.courseware.block_render import get_block_for_descriptor
|
||||
|
||||
|
||||
@@ -9,9 +9,13 @@ for the extracted video block in xblocks-contrib repository.
|
||||
import logging
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from opaque_keys.edx.locator import LibraryLocatorV2
|
||||
from xblocks_contrib.video.exceptions import TranscriptNotFoundError
|
||||
|
||||
from openedx.core.djangoapps.video_config import sharing
|
||||
from django.core.files import File
|
||||
from django.core.files.base import ContentFile
|
||||
from edxval.api import create_external_video, create_or_update_video_transcript, delete_video_transcript
|
||||
from organizations.api import get_course_organization
|
||||
from openedx.core.djangoapps.video_config.models import (
|
||||
CourseYoutubeBlockedFlag,
|
||||
@@ -19,6 +23,16 @@ from openedx.core.djangoapps.video_config.models import (
|
||||
)
|
||||
from openedx.core.djangoapps.video_config.toggles import TRANSCRIPT_FEEDBACK
|
||||
from openedx.core.djangoapps.video_pipeline.config.waffle import DEPRECATE_YOUTUBE
|
||||
from openedx.core.djangoapps.content_libraries.api import (
|
||||
add_library_block_static_asset_file,
|
||||
delete_library_block_static_asset_file,
|
||||
)
|
||||
from openedx.core.djangoapps.video_config.transcripts_utils import (
|
||||
Transcript,
|
||||
clean_video_id,
|
||||
get_html5_ids,
|
||||
remove_subs_from_store,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,6 +44,9 @@ class VideoConfigService:
|
||||
This service abstracts away edx-platform specific functionality
|
||||
that the Video XBlock needs, allowing the Video XBlock to be
|
||||
extracted to a separate repository.
|
||||
|
||||
TODO: This service could be improved in a few ways:
|
||||
https://github.com/openedx/edx-platform/issues/37656
|
||||
"""
|
||||
|
||||
def get_public_video_url(self, usage_id: UsageKey) -> str:
|
||||
@@ -120,3 +137,117 @@ class VideoConfigService:
|
||||
raise TranscriptNotFoundError(
|
||||
f"Failed to get transcript: {exc}"
|
||||
) from exc
|
||||
|
||||
def upload_transcript(
|
||||
self,
|
||||
*,
|
||||
video_block,
|
||||
language_code: str,
|
||||
new_language_code: str | None,
|
||||
transcript_file: File,
|
||||
edx_video_id: str | None,
|
||||
) -> None:
|
||||
"""
|
||||
Store a transcript, however the runtime prefers to.
|
||||
|
||||
Mutates:
|
||||
* video_block.transcripts
|
||||
* video_block.edx_video_id, iff a new video is created in edx-val.
|
||||
|
||||
Can raise:
|
||||
* UnicodeDecodeError
|
||||
* TranscriptsGenerationException
|
||||
"""
|
||||
is_library = isinstance(video_block.usage_key.context_key, LibraryLocatorV2)
|
||||
content: bytes = transcript_file.read()
|
||||
if is_library:
|
||||
# Save transcript as static asset in Learning Core if is a library component
|
||||
filename = f'static/transcript-{new_language_code}.srt'
|
||||
add_library_block_static_asset_file(video_block.usage_key, filename, content)
|
||||
else:
|
||||
edx_video_id = clean_video_id(edx_video_id)
|
||||
if not edx_video_id:
|
||||
# Back-populate the video ID for an external video.
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
edx_video_id = create_external_video(display_name='external video')
|
||||
video_block.edx_video_id = edx_video_id
|
||||
filename = f'{edx_video_id}-{new_language_code}.srt'
|
||||
# Convert SRT transcript into an SJSON format and upload it to S3 if a course component
|
||||
sjson_subs = Transcript.convert(
|
||||
content=content.decode('utf-8'),
|
||||
input_format=Transcript.SRT,
|
||||
output_format=Transcript.SJSON
|
||||
).encode()
|
||||
create_or_update_video_transcript(
|
||||
video_id=edx_video_id,
|
||||
language_code=language_code,
|
||||
metadata={
|
||||
'file_format': Transcript.SJSON,
|
||||
'language_code': new_language_code,
|
||||
},
|
||||
file_data=ContentFile(sjson_subs),
|
||||
)
|
||||
|
||||
# If a new transcript is added, then both new_language_code and
|
||||
# language_code fields will have the same value.
|
||||
if language_code != new_language_code:
|
||||
video_block.transcripts.pop(language_code, None)
|
||||
video_block.transcripts[new_language_code] = filename
|
||||
|
||||
if is_library:
|
||||
_save_transcript_field(video_block)
|
||||
|
||||
def delete_transcript(
|
||||
self,
|
||||
*,
|
||||
video_block,
|
||||
edx_video_id: str | None,
|
||||
language_code: str,
|
||||
) -> None:
|
||||
"""
|
||||
Delete a transcript from the runtime's storage.
|
||||
"""
|
||||
edx_video_id = clean_video_id(edx_video_id)
|
||||
if edx_video_id:
|
||||
delete_video_transcript(video_id=edx_video_id, language_code=language_code)
|
||||
if isinstance(video_block.context_key, LibraryLocatorV2):
|
||||
transcript_name = video_block.transcripts.pop(language_code, None)
|
||||
if transcript_name:
|
||||
delete_library_block_static_asset_file(video_block.usage_key, f"static/{transcript_name}")
|
||||
_save_transcript_field(video_block)
|
||||
else:
|
||||
if language_code == 'en':
|
||||
# remove any transcript file from content store for the video ids
|
||||
possible_sub_ids = [
|
||||
video_block.sub, # pylint: disable=access-member-before-definition
|
||||
video_block.youtube_id_1_0
|
||||
] + get_html5_ids(video_block.html5_sources)
|
||||
for sub_id in possible_sub_ids:
|
||||
remove_subs_from_store(sub_id, video_block, language_code)
|
||||
# update metadata as `en` can also be present in `transcripts` field
|
||||
remove_subs_from_store(
|
||||
video_block.transcripts.pop(language_code, None), video_block, language_code
|
||||
)
|
||||
# also empty `sub` field
|
||||
video_block.sub = '' # pylint: disable=attribute-defined-outside-init
|
||||
else:
|
||||
remove_subs_from_store(
|
||||
video_block.transcripts.pop(language_code, None), video_block, language_code
|
||||
)
|
||||
|
||||
|
||||
def _save_transcript_field(video_block):
|
||||
"""
|
||||
Hacky workaround to ensure that transcript field is saved for Learning Core video blocks.
|
||||
|
||||
It's not clear why this is necessary.
|
||||
"""
|
||||
field = video_block.fields['transcripts']
|
||||
if video_block.transcripts:
|
||||
transcripts_copy = video_block.transcripts.copy()
|
||||
# Need to delete to overwrite, it's weird behavior,
|
||||
# but it only works like this.
|
||||
field.delete_from(video_block)
|
||||
field.write_to(video_block, transcripts_copy)
|
||||
else:
|
||||
field.delete_from(video_block)
|
||||
|
||||
@@ -28,7 +28,6 @@ from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.exceptions import NotFoundError
|
||||
|
||||
from xmodule.video_block.bumper_utils import get_bumper_settings
|
||||
from xblocks_contrib.video.exceptions import TranscriptsGenerationException
|
||||
|
||||
|
||||
@@ -834,6 +833,11 @@ class VideoTranscriptsMixin:
|
||||
is_bumper(bool): If True, the request is for the bumper transcripts
|
||||
include_val_transcripts(bool): If True, include edx-val transcripts as well
|
||||
"""
|
||||
# TODO: This causes a circular import when imported at the top-level.
|
||||
# This import will be removed as part of the VideoBlock extraction.
|
||||
# https://github.com/openedx/edx-platform/issues/36282
|
||||
from xmodule.video_block.bumper_utils import get_bumper_settings
|
||||
|
||||
if is_bumper:
|
||||
transcripts = copy.deepcopy(get_bumper_settings(self).get('transcripts', {}))
|
||||
sub = transcripts.pop("en", "")
|
||||
|
||||
@@ -22,7 +22,6 @@ from xblock.field_data import DictFieldData, FieldData, SplitFieldData
|
||||
from xblock.fields import Scope, ScopeIds
|
||||
from xblock.runtime import IdReader, KvsFieldData, MemoryIdManager, Runtime
|
||||
|
||||
from openedx.core.djangoapps.video_config.services import VideoConfigService
|
||||
from xmodule.errortracker import make_error_tracker
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore.django import XBlockI18nService
|
||||
@@ -229,6 +228,8 @@ class XBlockRuntime(RuntimeShim, Runtime):
|
||||
Submit a grade for the block.
|
||||
"""
|
||||
if self.user and not self.user.is_anonymous:
|
||||
# TODO: we shouldn't be using an LMS API here.
|
||||
# https://github.com/openedx/edx-platform/issues/37660
|
||||
grades_signals.SCORE_PUBLISHED.send(
|
||||
sender=None,
|
||||
block=block,
|
||||
@@ -343,6 +344,8 @@ class XBlockRuntime(RuntimeShim, Runtime):
|
||||
elif service_name == 'error_tracker':
|
||||
return make_error_tracker()
|
||||
elif service_name == 'video_config':
|
||||
# Import here to avoid circular dependency
|
||||
from openedx.core.djangoapps.video_config.services import VideoConfigService
|
||||
return VideoConfigService()
|
||||
|
||||
# Otherwise, fall back to the base implementation which loads services
|
||||
|
||||
Reference in New Issue
Block a user