MIL-006: Spoken Malay word

Review record RC-028 (Go, autonomous mode, not independent).

Task: MIL-006#1
Task: MIL-006#2
Task: MIL-006#3
Task: MIL-006#4
Task: MIL-006#5
This commit is contained in:
2026-10-14 22:39:36 +08:00
parent 5900fda90a
commit b3c5116110
22 changed files with 566 additions and 20 deletions
@@ -0,0 +1,5 @@
"""Ports of the application layer.
@file __init__.py
@brief Holds the interfaces the use cases need from outside.
"""
@@ -0,0 +1,22 @@
"""Port for spoken words.
@file speech_port.py
@brief The SpeechPort interface the study session uses to say a word aloud.
"""
from typing import Protocol
class SpeechPort(Protocol):
"""Speaks a text aloud in a language."""
def speak(self, text: str, language_code: str) -> None:
"""Speak a text without blocking the caller for long.
An implementation does not raise: a failure is logged and ignored.
@param text The text to speak.
@param language_code Language of the text, for example "ms".
@return Nothing.
"""
...
@@ -6,6 +6,7 @@
from collections.abc import Callable, Sequence
from flash_card.application.ports.speech_port import SpeechPort
from flash_card.application.use_cases.study_session import StudySession
from flash_card.domain.entities.card import Card
from flash_card.domain.entities.deck import Deck
@@ -16,12 +17,14 @@ from flash_card.domain.repositories.vocabulary_repository import VocabularyRepos
def open_study_session(
vocabulary_repository: VocabularyRepository,
progress_repository: ProgressRepository,
speech: SpeechPort,
choose: Callable[[Sequence[Card]], Card],
) -> StudySession:
"""Build a session over the saved words to learn, else the whole word list.
@param vocabulary_repository Source of the delivered word list.
@param progress_repository Store of the words still to learn.
@param speech Speaks the front text of each drawn card.
@param choose Picks one card from a non-empty sequence.
@return The new session.
@throws VocabularyError When the saved progress, or the word list when no
@@ -30,4 +33,4 @@ def open_study_session(
deck = progress_repository.load()
if deck is None:
deck = Deck.from_vocabulary(vocabulary_repository.load())
return StudySession(deck, choose, progress_repository)
return StudySession(deck, choose, progress_repository, speech)
@@ -7,6 +7,7 @@
from collections.abc import Callable, Sequence
from flash_card.application.dto.card_dto import CardDto
from flash_card.application.ports.speech_port import SpeechPort
from flash_card.domain.entities.card import Card
from flash_card.domain.entities.deck import Deck
from flash_card.domain.repositories.progress_repository import ProgressRepository
@@ -20,13 +21,17 @@ class StudySession:
deck: Deck,
choose: Callable[[Sequence[Card]], Card],
progress: ProgressRepository,
speech: SpeechPort,
) -> None:
"""Create a session.
@param deck The cards still to learn.
@param choose Picks one card from a non-empty sequence.
@param progress Store of the words still to learn.
@param speech Speaks the front text of each drawn card.
"""
## @brief Speaks the front text of each drawn card.
self._speech = speech
## @brief Store of the words still to learn.
self._progress = progress
## @brief The cards still to learn.
@@ -60,8 +65,10 @@ class StudySession:
if self._deck.is_empty:
self._current = None
return None
self._current = self._deck.draw(self._choose, exclude=self._current)
return self._to_dto(self._current)
card = self._deck.draw(self._choose, exclude=self._current)
self._current = card
self._speech.speak(card.front, self._deck.language_pair.front_code)
return self._to_dto(card)
def mark_known(self) -> None:
"""Remove the current card from the deck and save the remaining cards.