Skip to content

Commit b11c386

Browse files
Nik-09seanlip
andauthored
Fix oppia#23421: Adds Exploration voiceover support for Android scripts (oppia#23455)
* Upgrades elastic search version * Adds voiceover compatibility for Android * Fixes exploration data return in old schema * Adds backend tests * Fixes mypy issues * Fixes backend tests --------- Co-authored-by: Sean Lip <[email protected]>
1 parent adc8152 commit b11c386

File tree

7 files changed

+495
-16
lines changed

7 files changed

+495
-16
lines changed

assets/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export default {
8787
"Statistics", "Trigonometry", "Welcome"],
8888
"ACTIVITY_TYPE_EXPLORATION": "exploration",
8989
"ACTIVITY_TYPE_EXPLORATION_TRANSLATIONS": "exp_translations",
90+
"ACTIVITY_TYPE_EXPLORATION_VOICEOVERS": "exp_voiceovers",
9091
"ACTIVITY_TYPE_COLLECTION": "collection",
9192
"ACTIVITY_TYPE_STORY": "story",
9293
"ACTIVITY_TYPE_SKILL": "skill",

core/controllers/android.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
classroom_domain,
2727
exp_domain,
2828
exp_fetchers,
29+
exp_services,
2930
skill_domain,
3031
skill_fetchers,
3132
story_domain,
@@ -37,6 +38,8 @@
3738
topic_domain,
3839
topic_fetchers,
3940
translation_fetchers,
41+
voiceover_domain,
42+
voiceover_services,
4043
)
4144

4245
from typing import Dict, List, Optional, TypedDict, Union
@@ -99,7 +102,7 @@ class _ActivityDataResponseDictRequiredFields(TypedDict):
99102

100103
id: str
101104
payload: Union[
102-
exp_domain.ExplorationDict,
105+
exp_domain.ExplorationDictForAndroid,
103106
story_domain.StoryDict,
104107
skill_domain.SkillDict,
105108
subtopic_page_domain.SubtopicPageDict,
@@ -108,6 +111,7 @@ class _ActivityDataResponseDictRequiredFields(TypedDict):
108111
classroom_config_domain.ClassroomDict,
109112
topic_domain.TopicDict,
110113
Dict[str, feconf.TranslatedContentDict],
114+
Dict[str, voiceover_domain.EntityVoiceoversDict],
111115
classroom_domain.ClassroomDict,
112116
None
113117
]
@@ -146,6 +150,7 @@ class AndroidActivityHandler(base.BaseHandler[
146150
'choices': [
147151
constants.ACTIVITY_TYPE_EXPLORATION,
148152
constants.ACTIVITY_TYPE_EXPLORATION_TRANSLATIONS,
153+
constants.ACTIVITY_TYPE_EXPLORATION_VOICEOVERS,
149154
constants.ACTIVITY_TYPE_STORY,
150155
constants.ACTIVITY_TYPE_SKILL,
151156
constants.ACTIVITY_TYPE_SUBTOPIC,
@@ -191,12 +196,17 @@ def get(self) -> None:
191196
activity_data['id'],
192197
strict=False,
193198
version=activity_data.get('version'))
199+
200+
exploration_dict_for_android: Optional[
201+
exp_domain.ExplorationDictForAndroid] = None
202+
if exploration is not None:
203+
exploration_dict_for_android = (
204+
exp_services.to_exploration_dict_for_android(exploration)
205+
)
194206
activities.append({
195207
'id': activity_data['id'],
196208
'version': activity_data.get('version'),
197-
'payload': (
198-
exploration.to_dict() if exploration is not None
199-
else None)
209+
'payload': exploration_dict_for_android
200210
})
201211
elif activity_type == constants.ACTIVITY_TYPE_STORY:
202212
for activity_data in activities_data:
@@ -315,6 +325,37 @@ def get(self) -> None:
315325
if translation is not None
316326
else None)
317327
})
328+
elif activity_type == constants.ACTIVITY_TYPE_EXPLORATION_VOICEOVERS:
329+
for activity_data in activities_data:
330+
version = activity_data.get('version')
331+
language_code = activity_data.get('language_code')
332+
if version is None or language_code is None:
333+
raise self.InvalidInputException(
334+
'Version and language code must be specified '
335+
'for voiceovers'
336+
)
337+
entity_voiceovers = (
338+
voiceover_services.
339+
fetch_entity_voiceovers_by_language_code(
340+
activity_data['id'],
341+
feconf.ENTITY_TYPE_EXPLORATION,
342+
version,
343+
language_code
344+
)
345+
)
346+
347+
language_accent_code_to_entity_voiceover = {}
348+
for entity_voiceover in entity_voiceovers:
349+
language_accent_code_to_entity_voiceover[
350+
entity_voiceover.language_accent_code
351+
] = entity_voiceover.to_dict()
352+
353+
activities.append({
354+
'id': activity_data['id'],
355+
'version': version,
356+
'language_code': language_code,
357+
'payload': language_accent_code_to_entity_voiceover
358+
})
318359
else:
319360
for activity_data in activities_data:
320361
topic = topic_fetchers.get_topic_by_id(

core/controllers/android_test.py

Lines changed: 169 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,33 @@
1616

1717
from __future__ import annotations
1818

19+
from core import feconf
1920
from core.constants import constants
2021
from core.domain import (
2122
classroom_config_services,
2223
exp_domain,
2324
exp_fetchers,
2425
exp_services,
26+
state_domain,
2527
topic_fetchers,
2628
)
2729
from core.platform import models
2830
from core.tests import test_utils
2931

3032
MYPY = False
3133
if MYPY: # pragma: no cover
32-
from mypy_imports import secrets_services, translation_models
34+
from mypy_imports import (
35+
secrets_services,
36+
translation_models,
37+
voiceover_models,
38+
)
3339

3440
secrets_services = models.Registry.import_secrets_services()
3541

36-
(translation_models,) = models.Registry.import_models([
37-
models.Names.TRANSLATION])
42+
(translation_models, voiceover_models,) = models.Registry.import_models([
43+
models.Names.TRANSLATION,
44+
models.Names.VOICEOVER
45+
])
3846

3947

4048
class InitializeAndroidTestDataHandlerTest(test_utils.GenericTestBase):
@@ -133,7 +141,8 @@ def test_get_exploration_returns_correct_json(self) -> None:
133141
[{
134142
'id': 'exp_id',
135143
'version': 1,
136-
'payload': exploration.to_dict()
144+
'payload': exp_services.to_exploration_dict_for_android(
145+
exploration)
137146
}]
138147
)
139148

@@ -166,7 +175,8 @@ def test_get_different_versions_of_exploration_returns_correct_json(
166175
[{
167176
'id': 'exp_id',
168177
'version': 1,
169-
'payload': exploration.to_dict()
178+
'payload': exp_services.to_exploration_dict_for_android(
179+
exploration)
170180
}]
171181
)
172182
self.assertEqual(
@@ -179,7 +189,8 @@ def test_get_different_versions_of_exploration_returns_correct_json(
179189
[{
180190
'id': 'exp_id',
181191
'version': 2,
182-
'payload': new_exploration.to_dict()
192+
'payload': exp_services.to_exploration_dict_for_android(
193+
new_exploration)
183194
}]
184195
)
185196

@@ -212,11 +223,13 @@ def test_get_multiple_versions_at_a_time_returns_correct_json(self) -> None:
212223
[{
213224
'id': 'exp_id',
214225
'version': 2,
215-
'payload': new_exploration.to_dict()
226+
'payload': exp_services.to_exploration_dict_for_android(
227+
new_exploration)
216228
}, {
217229
'id': 'exp_id',
218230
'version': 1,
219-
'payload': exploration.to_dict()
231+
'payload': exp_services.to_exploration_dict_for_android(
232+
exploration)
220233
}]
221234
)
222235

@@ -231,11 +244,13 @@ def test_get_multiple_versions_at_a_time_returns_correct_json(self) -> None:
231244
[{
232245
'id': 'exp_id',
233246
'version': 1,
234-
'payload': exploration.to_dict()
247+
'payload': exp_services.to_exploration_dict_for_android(
248+
exploration)
235249
}, {
236250
'id': 'exp_id',
237251
'version': 2,
238-
'payload': new_exploration.to_dict()
252+
'payload': exp_services.to_exploration_dict_for_android(
253+
new_exploration)
239254
}]
240255
)
241256

@@ -259,7 +274,8 @@ def test_get_with_invalid_versions_returns_correct_json(self) -> None:
259274
}, {
260275
'id': 'exp_id',
261276
'version': 1,
262-
'payload': exploration.to_dict()
277+
'payload': exp_services.to_exploration_dict_for_android(
278+
exploration)
263279
}]
264280
)
265281

@@ -275,7 +291,8 @@ def test_get_with_invalid_versions_returns_correct_json(self) -> None:
275291
[{
276292
'id': 'exp_id',
277293
'version': 1,
278-
'payload': exploration.to_dict()
294+
'payload': exp_services.to_exploration_dict_for_android(
295+
exploration)
279296
}, {
280297
'id': 'exp_id',
281298
'version': 3,
@@ -502,6 +519,146 @@ def test_get_exploration_translation_with_zero_items_returns_correct_json(
502519
[]
503520
)
504521

522+
def test_get_exploration_voiceover_without_version_fails(self) -> None:
523+
with self.secrets_swap:
524+
self.assertEqual(
525+
self.get_json(
526+
'/android_data?activity_type=exp_voiceovers&'
527+
'activities_data=['
528+
' {"id": "voiceover_id", "language_code": "es"}'
529+
']',
530+
headers={'X-ApiKey': 'secret'},
531+
expected_status_int=400
532+
)['error'],
533+
'Version and language code must be specified '
534+
'for voiceovers'
535+
)
536+
537+
def test_get_exploration_voiceover_returns_correct_json(self) -> None:
538+
dummy_manual_voiceover_dict_1: state_domain.VoiceoverDict = {
539+
'filename': 'filename1.mp3',
540+
'file_size_bytes': 3000,
541+
'needs_update': False,
542+
'duration_secs': 6.1
543+
}
544+
dummy_autogenerated_voiceover_dict: state_domain.VoiceoverDict = {
545+
'filename': 'filename2.mp3',
546+
'file_size_bytes': 3500,
547+
'needs_update': False,
548+
'duration_secs': 5.9
549+
}
550+
dummy_manual_voiceover_dict_2: state_domain.VoiceoverDict = {
551+
'filename': 'filename3.mp3',
552+
'file_size_bytes': 3500,
553+
'needs_update': False,
554+
'duration_secs': 5.9
555+
}
556+
voiceover_models.EntityVoiceoversModel.create_new(
557+
feconf.ENTITY_TYPE_EXPLORATION, 'exp_id_1', 1, 'en-US', {
558+
'content_0': {
559+
'manual': dummy_manual_voiceover_dict_1,
560+
'auto': dummy_autogenerated_voiceover_dict
561+
}
562+
}, {}).put()
563+
564+
voiceover_models.EntityVoiceoversModel.create_new(
565+
feconf.ENTITY_TYPE_EXPLORATION, 'exp_id_1', 1, 'en-NG', {
566+
'content_0': {
567+
'manual': dummy_manual_voiceover_dict_2,
568+
'auto': None
569+
}
570+
}, {}).put()
571+
572+
voiceover_autogeneration_policy_model = (
573+
voiceover_models.VoiceoverAutogenerationPolicyModel(
574+
id=voiceover_models.VOICEOVER_AUTOGENERATION_POLICY_ID)
575+
)
576+
voiceover_autogeneration_policy_model.language_codes_mapping = {
577+
'en': {
578+
'en-US': True,
579+
'en-NG': False
580+
}
581+
}
582+
(
583+
voiceover_autogeneration_policy_model.
584+
autogenerated_voiceovers_are_enabled
585+
) = True
586+
voiceover_autogeneration_policy_model.update_timestamps()
587+
voiceover_autogeneration_policy_model.put()
588+
589+
expected_payload = {
590+
'en-NG': {
591+
'automated_voiceovers_audio_offsets_msecs': {},
592+
'entity_id': 'exp_id_1',
593+
'entity_type': 'exploration',
594+
'entity_version': 1,
595+
'language_accent_code': 'en-NG',
596+
'voiceovers_mapping': {
597+
'content_0': {
598+
'auto': None,
599+
'manual': {
600+
'duration_secs': 5.9,
601+
'file_size_bytes': 3500,
602+
'filename': 'filename3.mp3',
603+
'needs_update': False,
604+
},
605+
}
606+
},
607+
},
608+
'en-US': {
609+
'automated_voiceovers_audio_offsets_msecs': {},
610+
'entity_id': 'exp_id_1',
611+
'entity_type': 'exploration',
612+
'entity_version': 1,
613+
'language_accent_code': 'en-US',
614+
'voiceovers_mapping': {
615+
'content_0': {
616+
'auto': {
617+
'duration_secs': 5.9,
618+
'file_size_bytes': 3500,
619+
'filename': 'filename2.mp3',
620+
'needs_update': False,
621+
},
622+
'manual': {
623+
'duration_secs': 6.1,
624+
'file_size_bytes': 3000,
625+
'filename': 'filename1.mp3',
626+
'needs_update': False,
627+
},
628+
}
629+
},
630+
},
631+
}
632+
with self.secrets_swap:
633+
response = self.get_json(
634+
'/android_data?activity_type=exp_voiceovers&'
635+
'activities_data=[{'
636+
' "id": "exp_id_1", '
637+
' "language_code": "en", '
638+
' "version": 1'
639+
'}]',
640+
headers={'X-ApiKey': 'secret'},
641+
expected_status_int=200
642+
)
643+
self.assertEqual(response[0]['payload'], expected_payload)
644+
self.assertEqual(response[0]['id'], 'exp_id_1')
645+
self.assertEqual(response[0]['language_code'], 'en')
646+
self.assertEqual(response[0]['version'], 1)
647+
648+
def test_get_exploration_voiceovers_with_zero_items_returns_correct_json(
649+
self
650+
) -> None:
651+
with self.secrets_swap:
652+
self.assertEqual(
653+
self.get_json(
654+
'/android_data?activity_type=exp_voiceovers&'
655+
'activities_data=[]',
656+
headers={'X-ApiKey': 'secret'},
657+
expected_status_int=200
658+
),
659+
[]
660+
)
661+
505662
def test_get_topic_returns_correct_json(self) -> None:
506663
topic = self.save_new_topic('topic_id', 'user_id')
507664
with self.secrets_swap:

0 commit comments

Comments
 (0)