thai-talking-clock 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. mp3-thai-clock/0.mp3 +0 -0
  2. mp3-thai-clock/1.mp3 +0 -0
  3. mp3-thai-clock/10.mp3 +0 -0
  4. mp3-thai-clock/11.mp3 +0 -0
  5. mp3-thai-clock/12.mp3 +0 -0
  6. mp3-thai-clock/13.mp3 +0 -0
  7. mp3-thai-clock/14.mp3 +0 -0
  8. mp3-thai-clock/15.mp3 +0 -0
  9. mp3-thai-clock/16.mp3 +0 -0
  10. mp3-thai-clock/17.mp3 +0 -0
  11. mp3-thai-clock/18.mp3 +0 -0
  12. mp3-thai-clock/19.mp3 +0 -0
  13. mp3-thai-clock/2.mp3 +0 -0
  14. mp3-thai-clock/20.mp3 +0 -0
  15. mp3-thai-clock/21.mp3 +0 -0
  16. mp3-thai-clock/22.mp3 +0 -0
  17. mp3-thai-clock/23.mp3 +0 -0
  18. mp3-thai-clock/24.mp3 +0 -0
  19. mp3-thai-clock/25.mp3 +0 -0
  20. mp3-thai-clock/26.mp3 +0 -0
  21. mp3-thai-clock/27.mp3 +0 -0
  22. mp3-thai-clock/28.mp3 +0 -0
  23. mp3-thai-clock/29.mp3 +0 -0
  24. mp3-thai-clock/3.mp3 +0 -0
  25. mp3-thai-clock/30.mp3 +0 -0
  26. mp3-thai-clock/31.mp3 +0 -0
  27. mp3-thai-clock/32.mp3 +0 -0
  28. mp3-thai-clock/33.mp3 +0 -0
  29. mp3-thai-clock/34.mp3 +0 -0
  30. mp3-thai-clock/35.mp3 +0 -0
  31. mp3-thai-clock/36.mp3 +0 -0
  32. mp3-thai-clock/37.mp3 +0 -0
  33. mp3-thai-clock/38.mp3 +0 -0
  34. mp3-thai-clock/39.mp3 +0 -0
  35. mp3-thai-clock/4.mp3 +0 -0
  36. mp3-thai-clock/40.mp3 +0 -0
  37. mp3-thai-clock/41.mp3 +0 -0
  38. mp3-thai-clock/42.mp3 +0 -0
  39. mp3-thai-clock/43.mp3 +0 -0
  40. mp3-thai-clock/44.mp3 +0 -0
  41. mp3-thai-clock/45.mp3 +0 -0
  42. mp3-thai-clock/46.mp3 +0 -0
  43. mp3-thai-clock/47.mp3 +0 -0
  44. mp3-thai-clock/48.mp3 +0 -0
  45. mp3-thai-clock/49.mp3 +0 -0
  46. mp3-thai-clock/5.mp3 +0 -0
  47. mp3-thai-clock/50.mp3 +0 -0
  48. mp3-thai-clock/51.mp3 +0 -0
  49. mp3-thai-clock/52.mp3 +0 -0
  50. mp3-thai-clock/53.mp3 +0 -0
  51. mp3-thai-clock/54.mp3 +0 -0
  52. mp3-thai-clock/55.mp3 +0 -0
  53. mp3-thai-clock/56.mp3 +0 -0
  54. mp3-thai-clock/57.mp3 +0 -0
  55. mp3-thai-clock/58.mp3 +0 -0
  56. mp3-thai-clock/59.mp3 +0 -0
  57. mp3-thai-clock/6.mp3 +0 -0
  58. mp3-thai-clock/7.mp3 +0 -0
  59. mp3-thai-clock/8.mp3 +0 -0
  60. mp3-thai-clock/9.mp3 +0 -0
  61. mp3-thai-clock/begintime.mp3 +0 -0
  62. mp3-thai-clock/endtime.mp3 +0 -0
  63. mp3-thai-clock/hour.mp3 +0 -0
  64. mp3-thai-clock/minute.mp3 +0 -0
  65. mp3-thai-clock/second.mp3 +0 -0
  66. talking_clock/__init__.py +35 -0
  67. talking_clock/clock.py +198 -0
  68. talking_clock/exceptions.py +17 -0
  69. talking_clock/player.py +320 -0
  70. thai_talking_clock-1.0.0.dist-info/METADATA +122 -0
  71. thai_talking_clock-1.0.0.dist-info/RECORD +74 -0
  72. thai_talking_clock-1.0.0.dist-info/WHEEL +5 -0
  73. thai_talking_clock-1.0.0.dist-info/entry_points.txt +3 -0
  74. thai_talking_clock-1.0.0.dist-info/top_level.txt +2 -0
mp3-thai-clock/0.mp3 ADDED
Binary file
mp3-thai-clock/1.mp3 ADDED
Binary file
mp3-thai-clock/10.mp3 ADDED
Binary file
mp3-thai-clock/11.mp3 ADDED
Binary file
mp3-thai-clock/12.mp3 ADDED
Binary file
mp3-thai-clock/13.mp3 ADDED
Binary file
mp3-thai-clock/14.mp3 ADDED
Binary file
mp3-thai-clock/15.mp3 ADDED
Binary file
mp3-thai-clock/16.mp3 ADDED
Binary file
mp3-thai-clock/17.mp3 ADDED
Binary file
mp3-thai-clock/18.mp3 ADDED
Binary file
mp3-thai-clock/19.mp3 ADDED
Binary file
mp3-thai-clock/2.mp3 ADDED
Binary file
mp3-thai-clock/20.mp3 ADDED
Binary file
mp3-thai-clock/21.mp3 ADDED
Binary file
mp3-thai-clock/22.mp3 ADDED
Binary file
mp3-thai-clock/23.mp3 ADDED
Binary file
mp3-thai-clock/24.mp3 ADDED
Binary file
mp3-thai-clock/25.mp3 ADDED
Binary file
mp3-thai-clock/26.mp3 ADDED
Binary file
mp3-thai-clock/27.mp3 ADDED
Binary file
mp3-thai-clock/28.mp3 ADDED
Binary file
mp3-thai-clock/29.mp3 ADDED
Binary file
mp3-thai-clock/3.mp3 ADDED
Binary file
mp3-thai-clock/30.mp3 ADDED
Binary file
mp3-thai-clock/31.mp3 ADDED
Binary file
mp3-thai-clock/32.mp3 ADDED
Binary file
mp3-thai-clock/33.mp3 ADDED
Binary file
mp3-thai-clock/34.mp3 ADDED
Binary file
mp3-thai-clock/35.mp3 ADDED
Binary file
mp3-thai-clock/36.mp3 ADDED
Binary file
mp3-thai-clock/37.mp3 ADDED
Binary file
mp3-thai-clock/38.mp3 ADDED
Binary file
mp3-thai-clock/39.mp3 ADDED
Binary file
mp3-thai-clock/4.mp3 ADDED
Binary file
mp3-thai-clock/40.mp3 ADDED
Binary file
mp3-thai-clock/41.mp3 ADDED
Binary file
mp3-thai-clock/42.mp3 ADDED
Binary file
mp3-thai-clock/43.mp3 ADDED
Binary file
mp3-thai-clock/44.mp3 ADDED
Binary file
mp3-thai-clock/45.mp3 ADDED
Binary file
mp3-thai-clock/46.mp3 ADDED
Binary file
mp3-thai-clock/47.mp3 ADDED
Binary file
mp3-thai-clock/48.mp3 ADDED
Binary file
mp3-thai-clock/49.mp3 ADDED
Binary file
mp3-thai-clock/5.mp3 ADDED
Binary file
mp3-thai-clock/50.mp3 ADDED
Binary file
mp3-thai-clock/51.mp3 ADDED
Binary file
mp3-thai-clock/52.mp3 ADDED
Binary file
mp3-thai-clock/53.mp3 ADDED
Binary file
mp3-thai-clock/54.mp3 ADDED
Binary file
mp3-thai-clock/55.mp3 ADDED
Binary file
mp3-thai-clock/56.mp3 ADDED
Binary file
mp3-thai-clock/57.mp3 ADDED
Binary file
mp3-thai-clock/58.mp3 ADDED
Binary file
mp3-thai-clock/59.mp3 ADDED
Binary file
mp3-thai-clock/6.mp3 ADDED
Binary file
mp3-thai-clock/7.mp3 ADDED
Binary file
mp3-thai-clock/8.mp3 ADDED
Binary file
mp3-thai-clock/9.mp3 ADDED
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,35 @@
1
+ """Talking Clock package.
2
+
3
+ A cross-platform Python library for audio time announcement.
4
+ """
5
+
6
+ from .clock import TalkingClock
7
+ from .player import SystemAudioPlayer
8
+ from .exceptions import (
9
+ TalkingClockError,
10
+ AudioPlayerNotFoundError,
11
+ AudioPlaybackError,
12
+ MissingAudioFileError,
13
+ )
14
+
15
+ __version__ = "1.0.0"
16
+
17
+ __all__ = [
18
+ "TalkingClock",
19
+ "SystemAudioPlayer",
20
+ "TalkingClockError",
21
+ "AudioPlayerNotFoundError",
22
+ "AudioPlaybackError",
23
+ "MissingAudioFileError",
24
+ ]
25
+
26
+
27
+ def announce_current_time(
28
+ mp3_dir: str = None,
29
+ preferred_player: str = None,
30
+ blocking: bool = True,
31
+ ):
32
+ """Convenience function to announce the current system time immediately."""
33
+ player = SystemAudioPlayer(preferred_player=preferred_player)
34
+ clock = TalkingClock(mp3_dir=mp3_dir, player=player)
35
+ return clock.announce_time(blocking=blocking)
talking_clock/clock.py ADDED
@@ -0,0 +1,198 @@
1
+ """Talking Clock core module for constructing MP3 audio sequences and announcing time."""
2
+
3
+ import os
4
+ from datetime import datetime
5
+ from typing import List, Optional, Union
6
+
7
+ from .exceptions import MissingAudioFileError
8
+ from .player import SystemAudioPlayer
9
+
10
+
11
+ class TalkingClock:
12
+ """Talking Clock manager class."""
13
+
14
+ DEFAULT_MP3_DIR = "mp3-thai-clock"
15
+
16
+ REQUIRED_KEYWORD_FILES = [
17
+ "begintime.mp3",
18
+ "endtime.mp3",
19
+ "hour.mp3",
20
+ "minute.mp3",
21
+ "second.mp3",
22
+ ]
23
+
24
+ def __init__(
25
+ self,
26
+ mp3_dir: Optional[str] = None,
27
+ player: Optional[SystemAudioPlayer] = None,
28
+ use_12_hour: bool = False,
29
+ allow_dummy: bool = True,
30
+ ):
31
+ """Initialize Talking Clock.
32
+
33
+ Args:
34
+ mp3_dir: Path to directory containing MP3 audio files. If None, resolves default directory.
35
+ player: SystemAudioPlayer instance. If None, auto-creates and detects system player.
36
+ use_12_hour: If True, converts 24-hour hour (0-23) to 12-hour format (1-12). Default False (0-23).
37
+ allow_dummy: If True, allows falling back to dummy silent player when no system player is found.
38
+ """
39
+ self.mp3_dir = self._resolve_mp3_dir(mp3_dir)
40
+ self.player = player or SystemAudioPlayer(allow_dummy=allow_dummy)
41
+ self.use_12_hour = use_12_hour
42
+
43
+ def _resolve_mp3_dir(self, custom_dir: Optional[str]) -> str:
44
+ """Resolve MP3 directory path."""
45
+ if custom_dir:
46
+ if os.path.isabs(custom_dir):
47
+ return custom_dir
48
+ # Relative to current working directory
49
+ if os.path.exists(custom_dir):
50
+ return os.path.abspath(custom_dir)
51
+
52
+ # Try to locate mp3-thai-clock relative to package or workspace
53
+ pkg_dir = os.path.dirname(os.path.abspath(__file__))
54
+ parent_dir = os.path.dirname(pkg_dir)
55
+
56
+ candidates = [
57
+ os.path.join(parent_dir, self.DEFAULT_MP3_DIR),
58
+ os.path.join(os.getcwd(), self.DEFAULT_MP3_DIR),
59
+ self.DEFAULT_MP3_DIR,
60
+ ]
61
+
62
+ for path in candidates:
63
+ if os.path.exists(path):
64
+ return os.path.abspath(path)
65
+
66
+ # Fallback to provided directory or default string
67
+ return os.path.abspath(custom_dir or self.DEFAULT_MP3_DIR)
68
+
69
+ def verify_files(self) -> List[str]:
70
+ """Verify that all required MP3 files exist in the mp3_dir.
71
+
72
+ Returns:
73
+ List of missing file names (empty if all exist).
74
+ """
75
+ missing = []
76
+ # Check keyword files
77
+ for fname in self.REQUIRED_KEYWORD_FILES:
78
+ full_path = os.path.join(self.mp3_dir, fname)
79
+ if not os.path.isfile(full_path):
80
+ missing.append(fname)
81
+
82
+ # Check number files 0.mp3 to 59.mp3
83
+ for num in range(60):
84
+ fname = f"{num}.mp3"
85
+ full_path = os.path.join(self.mp3_dir, fname)
86
+ if not os.path.isfile(full_path):
87
+ missing.append(fname)
88
+
89
+ return missing
90
+
91
+ def get_sequence_for_time(
92
+ self,
93
+ hour: int,
94
+ minute: int,
95
+ second: int,
96
+ include_keywords: bool = True,
97
+ ) -> List[str]:
98
+ """Generate ordered list of MP3 file paths for the given hour, minute, and second.
99
+
100
+ Pattern:
101
+ begintime.mp3 -> {hour}.mp3 -> hour.mp3 -> {minute}.mp3 -> minute.mp3 -> {second}.mp3 -> second.mp3 -> endtime.mp3
102
+
103
+ Args:
104
+ hour: Integer hour (0-23 or 1-12 if 12-hour clock).
105
+ minute: Integer minute (0-59).
106
+ second: Integer second (0-59).
107
+ include_keywords: If True, includes begintime/endtime/hour/minute/second keyword mp3s.
108
+
109
+ Returns:
110
+ List of absolute paths to MP3 files.
111
+ """
112
+ if not (0 <= minute <= 59):
113
+ raise ValueError(f"Minute must be between 0 and 59, got {minute}")
114
+ if not (0 <= second <= 59):
115
+ raise ValueError(f"Second must be between 0 and 59, got {second}")
116
+
117
+ h_val = hour
118
+ if self.use_12_hour:
119
+ h_val = hour % 12
120
+ if h_val == 0:
121
+ h_val = 12
122
+ else:
123
+ if not (0 <= hour <= 23):
124
+ raise ValueError(f"Hour must be between 0 and 23 for 24-hour mode, got {hour}")
125
+
126
+ filenames = []
127
+ if include_keywords:
128
+ filenames.append("begintime.mp3")
129
+
130
+ filenames.append(f"{h_val}.mp3")
131
+ if include_keywords:
132
+ filenames.append("hour.mp3")
133
+
134
+ filenames.append(f"{minute}.mp3")
135
+ if include_keywords:
136
+ filenames.append("minute.mp3")
137
+
138
+ filenames.append(f"{second}.mp3")
139
+ if include_keywords:
140
+ filenames.append("second.mp3")
141
+
142
+ if include_keywords:
143
+ filenames.append("endtime.mp3")
144
+
145
+ file_paths = []
146
+ missing = []
147
+ for name in filenames:
148
+ path = os.path.join(self.mp3_dir, name)
149
+ if not os.path.isfile(path):
150
+ missing.append(name)
151
+ file_paths.append(path)
152
+
153
+ if missing:
154
+ raise MissingAudioFileError(
155
+ f"Missing required audio files in '{self.mp3_dir}': {', '.join(missing)}"
156
+ )
157
+
158
+ return file_paths
159
+
160
+ def get_current_time_sequence(self, include_keywords: bool = True) -> List[str]:
161
+ """Get MP3 audio sequence for the current system time."""
162
+ now = datetime.now()
163
+ return self.get_sequence_for_time(
164
+ hour=now.hour,
165
+ minute=now.minute,
166
+ second=now.second,
167
+ include_keywords=include_keywords,
168
+ )
169
+
170
+ def announce_time(
171
+ self,
172
+ hour: Optional[int] = None,
173
+ minute: Optional[int] = None,
174
+ second: Optional[int] = None,
175
+ dt: Optional[datetime] = None,
176
+ blocking: bool = True,
177
+ ) -> List[str]:
178
+ """Announce specific time or current time via audio playback.
179
+
180
+ Args:
181
+ hour: Optional hour integer.
182
+ minute: Optional minute integer.
183
+ second: Optional second integer.
184
+ dt: Optional datetime object (takes precedence over hour/minute/second if specified).
185
+ blocking: If True, waits for full audio playback to complete before returning.
186
+
187
+ Returns:
188
+ List of MP3 file paths that were played.
189
+ """
190
+ if dt is not None:
191
+ seq = self.get_sequence_for_time(dt.hour, dt.minute, dt.second)
192
+ elif hour is not None and minute is not None and second is not None:
193
+ seq = self.get_sequence_for_time(hour, minute, second)
194
+ else:
195
+ seq = self.get_current_time_sequence()
196
+
197
+ self.player.play_sequence(seq, blocking=blocking)
198
+ return seq
@@ -0,0 +1,17 @@
1
+ """Custom exceptions for Talking Clock library."""
2
+
3
+ class TalkingClockError(Exception):
4
+ """Base exception for Talking Clock library errors."""
5
+ pass
6
+
7
+ class AudioPlayerNotFoundError(TalkingClockError):
8
+ """Raised when no suitable audio player command is found on the host system."""
9
+ pass
10
+
11
+ class AudioPlaybackError(TalkingClockError):
12
+ """Raised when audio playback fails during execution."""
13
+ pass
14
+
15
+ class MissingAudioFileError(TalkingClockError):
16
+ """Raised when one or more required MP3 files are missing."""
17
+ pass
@@ -0,0 +1,320 @@
1
+ """Audio player detector and executor supporting macOS, Linux, and Windows."""
2
+
3
+ import os
4
+ import platform
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ import time
9
+ from typing import List, Optional, Tuple, Dict, Any
10
+
11
+ from .exceptions import AudioPlayerNotFoundError, AudioPlaybackError
12
+
13
+
14
+ class SystemAudioPlayer:
15
+ """Detects system command to play MP3 audio files across macOS, Linux, and Windows."""
16
+
17
+ PLAYER_CONFIGS: Dict[str, List[Dict[str, Any]]] = {
18
+ "darwin": [
19
+ {
20
+ "name": "afplay",
21
+ "binary": "afplay",
22
+ "args": ["{file}"],
23
+ "description": "macOS Native Audio Player",
24
+ },
25
+ {
26
+ "name": "ffplay",
27
+ "binary": "ffplay",
28
+ "args": ["-nodisp", "-autoexit", "-loglevel", "quiet", "{file}"],
29
+ "description": "FFmpeg Media Player",
30
+ },
31
+ {
32
+ "name": "mpv",
33
+ "binary": "mpv",
34
+ "args": ["--no-terminal", "{file}"],
35
+ "description": "MPV Media Player",
36
+ },
37
+ {
38
+ "name": "vlc",
39
+ "binary": "vlc",
40
+ "args": ["-I", "dummy", "--play-and-exit", "{file}"],
41
+ "description": "VLC Media Player",
42
+ },
43
+ {
44
+ "name": "cvlc",
45
+ "binary": "cvlc",
46
+ "args": ["--play-and-exit", "{file}"],
47
+ "description": "Command-line VLC",
48
+ },
49
+ ],
50
+ "linux": [
51
+ {
52
+ "name": "play",
53
+ "binary": "play",
54
+ "args": ["-q", "{file}"],
55
+ "description": "SoX Audio Player",
56
+ },
57
+ {
58
+ "name": "paplay",
59
+ "binary": "paplay",
60
+ "args": ["{file}"],
61
+ "description": "PulseAudio Sound Player",
62
+ },
63
+ {
64
+ "name": "mpg123",
65
+ "binary": "mpg123",
66
+ "args": ["-q", "{file}"],
67
+ "description": "MPG123 Console Player",
68
+ },
69
+ {
70
+ "name": "mpg321",
71
+ "binary": "mpg321",
72
+ "args": ["-q", "{file}"],
73
+ "description": "MPG321 Console Player",
74
+ },
75
+ {
76
+ "name": "ffplay",
77
+ "binary": "ffplay",
78
+ "args": ["-nodisp", "-autoexit", "-loglevel", "quiet", "{file}"],
79
+ "description": "FFmpeg Media Player",
80
+ },
81
+ {
82
+ "name": "mpv",
83
+ "binary": "mpv",
84
+ "args": ["--no-terminal", "{file}"],
85
+ "description": "MPV Media Player",
86
+ },
87
+ {
88
+ "name": "cvlc",
89
+ "binary": "cvlc",
90
+ "args": ["--play-and-exit", "{file}"],
91
+ "description": "Command-line VLC",
92
+ },
93
+ {
94
+ "name": "vlc",
95
+ "binary": "vlc",
96
+ "args": ["-I", "dummy", "--play-and-exit", "{file}"],
97
+ "description": "VLC Media Player",
98
+ },
99
+ {
100
+ "name": "aplay",
101
+ "binary": "aplay",
102
+ "args": ["-q", "{file}"],
103
+ "description": "ALSA Player",
104
+ },
105
+ ],
106
+ "windows": [
107
+ {
108
+ "name": "powershell",
109
+ "binary": "powershell",
110
+ "args": [
111
+ "-NoProfile",
112
+ "-NonInteractive",
113
+ "-Command",
114
+ (
115
+ "$wm = New-Object -ComObject WMPlayer.OCX; "
116
+ "$media = $wm.newMedia('{file}'); "
117
+ "$wm.currentMedia = $media; "
118
+ "while($wm.playState -ne 1 -and $wm.playState -ne 0 -and $wm.playState -ne 8) "
119
+ "{ Start-Sleep -m 100 }"
120
+ ),
121
+ ],
122
+ "description": "Windows PowerShell WMPlayer COM",
123
+ },
124
+ {
125
+ "name": "cmd_start",
126
+ "binary": "cmd",
127
+ "args": ["/c", "start", "/wait", "", "{file}"],
128
+ "description": "Windows Command Prompt Shell Launcher",
129
+ },
130
+ {
131
+ "name": "wmplayer",
132
+ "binary": "wmplayer",
133
+ "args": ["{file}", "/close"],
134
+ "description": "Windows Media Player",
135
+ },
136
+ {
137
+ "name": "ffplay",
138
+ "binary": "ffplay",
139
+ "args": ["-nodisp", "-autoexit", "-loglevel", "quiet", "{file}"],
140
+ "description": "FFmpeg Media Player",
141
+ },
142
+ {
143
+ "name": "mpv",
144
+ "binary": "mpv",
145
+ "args": ["--no-terminal", "{file}"],
146
+ "description": "MPV Media Player",
147
+ },
148
+ {
149
+ "name": "vlc",
150
+ "binary": "vlc",
151
+ "args": ["-I", "dummy", "--play-and-exit", "{file}"],
152
+ "description": "VLC Media Player",
153
+ },
154
+ ],
155
+ }
156
+
157
+ DUMMY_PLAYER: Dict[str, Any] = {
158
+ "name": "dummy",
159
+ "binary": "true",
160
+ "args": ["{file}"],
161
+ "description": "Dummy silent player for testing / headless environments",
162
+ }
163
+
164
+ def __init__(self, preferred_player: Optional[str] = None, allow_dummy: bool = False):
165
+ """Initialize SystemAudioPlayer.
166
+
167
+ Args:
168
+ preferred_player: Optional player name or command executable to force use.
169
+ allow_dummy: If True, falls back to a dummy silent player when no system player is installed.
170
+ """
171
+ self.os_type = self._detect_os()
172
+ self.preferred_player = preferred_player
173
+ self.allow_dummy = allow_dummy
174
+ self.detected_player = self.detect_player()
175
+
176
+ @staticmethod
177
+ def _detect_os() -> str:
178
+ """Detect normalized operating system name."""
179
+ sys_name = platform.system().lower()
180
+ if sys_name == "darwin":
181
+ return "darwin"
182
+ elif sys_name == "windows" or os.name == "nt":
183
+ return "windows"
184
+ else:
185
+ return "linux"
186
+
187
+ def get_supported_players(self) -> List[Dict[str, Any]]:
188
+ """Get the list of supported player configurations for current OS."""
189
+ return self.PLAYER_CONFIGS.get(self.os_type, self.PLAYER_CONFIGS["linux"])
190
+
191
+ def detect_player(self) -> Dict[str, Any]:
192
+ """Detect available player command on the current system.
193
+
194
+ Returns:
195
+ Dict containing name, binary path, args template, and description.
196
+
197
+ Raises:
198
+ AudioPlayerNotFoundError if no player executable is found.
199
+ """
200
+ if self.preferred_player:
201
+ # Check if preferred player exists as exact binary or name in config
202
+ binary_path = shutil.which(self.preferred_player)
203
+ if binary_path:
204
+ # Find matching config template if available
205
+ candidates = self.get_supported_players()
206
+ for c in candidates:
207
+ if c["name"] == self.preferred_player or c["binary"] == self.preferred_player:
208
+ return {
209
+ "name": c["name"],
210
+ "binary": binary_path,
211
+ "args": c["args"],
212
+ "description": c["description"],
213
+ }
214
+ return {
215
+ "name": self.preferred_player,
216
+ "binary": binary_path,
217
+ "args": ["{file}"],
218
+ "description": f"Custom player ({self.preferred_player})",
219
+ }
220
+
221
+ candidates = self.get_supported_players()
222
+ for candidate in candidates:
223
+ binary_path = shutil.which(candidate["binary"])
224
+ if binary_path:
225
+ return {
226
+ "name": candidate["name"],
227
+ "binary": binary_path,
228
+ "args": candidate["args"],
229
+ "description": candidate["description"],
230
+ }
231
+
232
+ if self.allow_dummy:
233
+ return self.DUMMY_PLAYER
234
+
235
+ raise AudioPlayerNotFoundError(
236
+ f"No compatible MP3 audio player found on system ({self.os_type}). "
237
+ f"Tested candidates: {[c['binary'] for c in candidates]}"
238
+ )
239
+
240
+ def build_command(self, file_path: str) -> List[str]:
241
+ """Build execution command for a given audio file path."""
242
+ abs_path = os.path.abspath(file_path)
243
+ player = self.detected_player
244
+ cmd = [player["binary"]]
245
+ for arg in player["args"]:
246
+ cmd.append(arg.format(file=abs_path))
247
+ return cmd
248
+
249
+ def play_file(self, file_path: str, blocking: bool = True) -> Optional[subprocess.Popen]:
250
+ """Play a single audio file.
251
+
252
+ Args:
253
+ file_path: Path to MP3 file.
254
+ blocking: If True, waits for playback to finish before returning.
255
+
256
+ Returns:
257
+ subprocess.Popen process object if non-blocking, None if blocking.
258
+
259
+ Raises:
260
+ FileNotFoundError if the target file does not exist.
261
+ AudioPlaybackError if execution fails.
262
+ """
263
+ if not os.path.isfile(file_path):
264
+ raise FileNotFoundError(f"Audio file not found: {file_path}")
265
+
266
+ cmd = self.build_command(file_path)
267
+ try:
268
+ process = subprocess.Popen(
269
+ cmd,
270
+ stdout=subprocess.DEVNULL,
271
+ stderr=subprocess.DEVNULL,
272
+ )
273
+ if blocking:
274
+ returncode = process.wait()
275
+ if returncode != 0:
276
+ raise AudioPlaybackError(
277
+ f"Player '{self.detected_player['name']}' exited with code {returncode} while playing '{file_path}'"
278
+ )
279
+ return None
280
+ return process
281
+ except Exception as e:
282
+ if isinstance(e, (FileNotFoundError, AudioPlaybackError)):
283
+ raise
284
+ raise AudioPlaybackError(f"Failed to execute audio player command: {e}") from e
285
+
286
+ def play_sequence(self, file_paths: List[str], blocking: bool = True) -> None:
287
+ """Play a list of audio files sequentially.
288
+
289
+ Args:
290
+ file_paths: List of MP3 file paths.
291
+ blocking: If True, plays synchronously in current thread. If False, runs in background thread.
292
+
293
+ Raises:
294
+ MissingAudioFileError / FileNotFoundError if files are missing.
295
+ AudioPlaybackError if playback fails.
296
+ """
297
+ for f in file_paths:
298
+ if not os.path.isfile(f):
299
+ raise FileNotFoundError(f"Audio file missing in sequence: {f}")
300
+
301
+ if blocking:
302
+ for f in file_paths:
303
+ self.play_file(f, blocking=True)
304
+ else:
305
+ import threading
306
+ thread = threading.Thread(
307
+ target=self._play_sequence_worker,
308
+ args=(file_paths,),
309
+ daemon=True,
310
+ )
311
+ thread.start()
312
+
313
+ def _play_sequence_worker(self, file_paths: List[str]) -> None:
314
+ """Worker thread method for non-blocking sequential playback."""
315
+ for f in file_paths:
316
+ try:
317
+ self.play_file(f, blocking=True)
318
+ except Exception as e:
319
+ sys.stderr.write(f"Background playback error playing {f}: {e}\n")
320
+ break
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: thai-talking-clock
3
+ Version: 1.0.0
4
+ Summary: A cross-platform Python library for audio time announcement using MP3 files.
5
+ Author: Billy
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Operating System :: MacOS
8
+ Classifier: Operating System :: POSIX :: Linux
9
+ Classifier: Operating System :: Microsoft :: Windows
10
+ Requires-Python: >=3.7
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Thai Talking Clock Python Library (`thai-talking-clock`)
14
+
15
+ A cross-platform Python library and test suite for a **Talking Clock** that evaluates time as integer hours, minutes, and seconds, builds the corresponding MP3 playback sequence, and auto-detects system commands to play audio on **macOS**, **Linux**, and **Windows**.
16
+
17
+ ```bash
18
+ pip install thai-talking-clock
19
+ ```
20
+
21
+ ---
22
+
23
+ ## 🎵 How It Works
24
+
25
+ 1. Evaluates hour, minute, and second as integers (e.g. `09:05:07` -> `hour = 9`, `minute = 5`, `second = 7`).
26
+ 2. Generates the exact MP3 playback sequence using the pattern:
27
+ $$\text{begintime.mp3} \rightarrow \text{\{hour\}.mp3} \rightarrow \text{hour.mp3} \rightarrow \text{\{minute\}.mp3} \rightarrow \text{minute.mp3} \rightarrow \text{\{second\}.mp3} \rightarrow \text{second.mp3} \rightarrow \text{endtime.mp3}$$
28
+
29
+ **Example for `09:05:07`:**
30
+ `begintime.mp3` $\rightarrow$ `9.mp3` $\rightarrow$ `hour.mp3` $\rightarrow$ `5.mp3` $\rightarrow$ `minute.mp3` $\rightarrow$ `7.mp3` $\rightarrow$ `second.mp3` $\rightarrow$ `endtime.mp3`
31
+
32
+ 3. Auto-detects available system audio players across OS platforms using `shutil.which`.
33
+
34
+ ---
35
+
36
+ ## 💻 Supported OS Audio Players
37
+
38
+ | Operating System | Supported System Commands (in auto-detection priority) |
39
+ | :--- | :--- |
40
+ | **macOS (`darwin`)** | `afplay` (native), `ffplay`, `mpv`, `vlc`, `cvlc` |
41
+ | **Linux** | `play` (SoX), `paplay` (PulseAudio), `mpg123`, `mpg321`, `ffplay`, `mpv`, `cvlc`, `vlc`, `aplay` |
42
+ | **Windows (`win32` / `nt`)** | `powershell` (WMPlayer COM / SoundPlayer), `cmd` (`cmd /c start /wait`), `wmplayer`, `ffplay`, `mpv`, `vlc` |
43
+
44
+ ---
45
+
46
+ ## 📁 Repository Structure
47
+
48
+ ```
49
+ talking-clock/
50
+ ├── mp3-thai-clock/ # 65 MP3 files (0.mp3 - 59.mp3, begintime.mp3, endtime.mp3, hour.mp3, minute.mp3, second.mp3)
51
+ ├── talking_clock/ # Core Python Library Package
52
+ │ ├── __init__.py # Package export and convenience function
53
+ │ ├── clock.py # TalkingClock class (time sequence builder & audio trigger)
54
+ │ ├── player.py # SystemAudioPlayer class (cross-platform OS player detector)
55
+ │ └── exceptions.py # Custom exception classes
56
+ ├── tests/ # Automated unit tests
57
+ │ ├── test_clock.py # Unit tests for clock sequence & pattern verification
58
+ │ └── test_player.py # Unit tests for OS player detection & mock execution
59
+ ├── test_talking_clock.py# Main CLI test runner & interactive test tool
60
+ ├── example.py # Simple code example
61
+ └── pyproject.toml # Python packaging definition
62
+ ```
63
+
64
+ ---
65
+
66
+ ## 🚀 Quick Usage
67
+
68
+ ### Python Code Usage
69
+
70
+ ```python
71
+ from talking_clock import TalkingClock, announce_current_time
72
+
73
+ # 1. Announce current system time immediately
74
+ announce_current_time()
75
+
76
+ # 2. Or instantiate TalkingClock for custom time announcements
77
+ clock = TalkingClock(mp3_dir="mp3-thai-clock")
78
+
79
+ # Get audio file sequence for 09:05:07
80
+ seq = clock.get_sequence_for_time(hour=9, minute=5, second=7)
81
+ print(seq)
82
+
83
+ # Play audio for 09:05:07
84
+ clock.announce_time(hour=9, minute=5, second=7)
85
+ ```
86
+
87
+ ---
88
+
89
+ ## 🧪 Running Tests & Command-Line Options
90
+
91
+ Run the test suite and CLI runner using `test_talking_clock.py`:
92
+
93
+ ```bash
94
+ # 1. Run all unit tests and test example 09:05:07 time announcement:
95
+ python3 test_talking_clock.py
96
+
97
+ # 2. Run unit tests only:
98
+ python3 test_talking_clock.py --unit
99
+
100
+ # 3. Dry-run test for specific time (09:05:07):
101
+ python3 test_talking_clock.py --time 09:05:07 --dry-run
102
+
103
+ # 4. List detected system audio player & OS candidates:
104
+ python3 test_talking_clock.py --list-players
105
+
106
+ # 5. Verify all 65 required MP3 files exist in mp3-thai-clock:
107
+ python3 test_talking_clock.py --verify
108
+
109
+ # 6. Announce current system time:
110
+ python3 test_talking_clock.py --now
111
+ ```
112
+
113
+ ---
114
+
115
+ ## 📜 Audio Licensing & Commercial Use
116
+
117
+ > [!NOTE]
118
+ > The MP3 voice audio files located in the `mp3-thai-clock/` directory were generated using **Microsoft Azure Text-to-Speech (Azure Cognitive Services Speech)** using a free subscription token from an Azure subscription.
119
+ >
120
+ > - **Licensing & Commercial Use**: Audio assets synthesized via Azure Text-to-Speech are subject to the [Microsoft Azure Legal Terms](https://azure.microsoft.com/en-us/support/legal/) and [Azure Cognitive Services Terms](https://azure.microsoft.com/en-us/support/legal/cognitive-services-terms/).
121
+ > - If you plan to use this library or the provided audio files for commercial purposes, please ensure your usage aligns with Microsoft Azure's licensing guidelines or replace the MP3 files with your own custom or commercially licensed voice samples.
122
+
@@ -0,0 +1,74 @@
1
+ mp3-thai-clock/0.mp3,sha256=GDeCjVQjtbcEnuc208HKnPnI4ujyQwJE0ANGLVf80T8,19584
2
+ mp3-thai-clock/1.mp3,sha256=jRmk4FeXoFjGulsI8l3lUDjl46VUMEXQS4bMLnX1vgk,17856
3
+ mp3-thai-clock/10.mp3,sha256=XM3q_oT9wz_jB-uadkjWnqC8KlKZFz6g74e2ADiZZPY,32256
4
+ mp3-thai-clock/11.mp3,sha256=1ZhcNnKuonvCG-G5s0CWlXaUl3u5gnwsQo0MTvI69v8,32832
5
+ mp3-thai-clock/12.mp3,sha256=kyRLGfMPQUCZi5DKS-C6r4DUDCCjcLM9awlBIUbeUpw,24768
6
+ mp3-thai-clock/13.mp3,sha256=lSj5bSEi2xg6LGBsIP07iUd7M8jJd2CUDwnaqsqkU64,24192
7
+ mp3-thai-clock/14.mp3,sha256=NKqSwKrQxTKKrk9c6nALfzeS8bOdv6gHJH_izWwdtiQ,22464
8
+ mp3-thai-clock/15.mp3,sha256=RlCN0PJ-GNSGnJrJzOOScnzD3f3WgBgGFC32s8W7PDQ,26496
9
+ mp3-thai-clock/16.mp3,sha256=Yp1sw-BuoFAwauoPbPzAasgjTcSXDWn-t18qn2-n1lE,15552
10
+ mp3-thai-clock/17.mp3,sha256=NNG2n4QXMfdAVE-jAB_zcMk3cuYej9z-OJZwf4Dzp8U,19584
11
+ mp3-thai-clock/18.mp3,sha256=cwD0g2ARdbJnTYu0Fl6ydwcauN0D4fW-5pW_b7rrGow,20736
12
+ mp3-thai-clock/19.mp3,sha256=er6d8XjMZz8jNeGu8NHK4TI6eeGp_6jaYxOX_yZWUmE,20736
13
+ mp3-thai-clock/2.mp3,sha256=V_B3tPX6Fl8MnIXaPrxcp0Zmt95y1vIdLzOXGYSkXHE,19008
14
+ mp3-thai-clock/20.mp3,sha256=7s8uun-xF6JzBYOHUGB_Eud7HNUdSFSLh9SNjm6_M7Q,29376
15
+ mp3-thai-clock/21.mp3,sha256=B5UQA4yEEtnGskSlRfgeMA7nXKmAxUEBubvMhJlI85Y,20160
16
+ mp3-thai-clock/22.mp3,sha256=1-riuXl_1gcSR1Fz1WhByYtCTsGWvp7MzNCyJiPQo04,24192
17
+ mp3-thai-clock/23.mp3,sha256=H-V2yw1yVP3qo_lK4iHQxPc9YV3N70J89Na6LPVybA8,25344
18
+ mp3-thai-clock/24.mp3,sha256=KJeywTNjlCFFgviHS491OjKlyLktW6dIBxv1yEfcBYI,22464
19
+ mp3-thai-clock/25.mp3,sha256=dWW3fRngKIm81fg4b4sA8X13Br-LaQb6Kr0EPH5_N6I,29952
20
+ mp3-thai-clock/26.mp3,sha256=OioFOU13W7qF4I9lpPsqCT6ONW6lSgIPNugF5mGutno,22464
21
+ mp3-thai-clock/27.mp3,sha256=YxhVjoARqmaG-Wmj0ZjiOhT77Oy7jAgJyERJ2m5Qfv8,19584
22
+ mp3-thai-clock/28.mp3,sha256=IG8i6FYYkOXxkJlDXl_BmfqRD9bxYnvxkU6MPuDb4Xk,20736
23
+ mp3-thai-clock/29.mp3,sha256=oRzmxZAGxQlAOocUEcgibWKkhwCpSeP4SZXziURtihY,26496
24
+ mp3-thai-clock/3.mp3,sha256=Cz5tFIk162I0vBR-FeiP1P9O6p3PFbI0pvllqdqJa6w,11520
25
+ mp3-thai-clock/30.mp3,sha256=uiw6CtAPSp53tRQWy7-oaPeTgKs8mlX2a2QYuj7kXyI,18432
26
+ mp3-thai-clock/31.mp3,sha256=Q0tIfhWK4geG91n0CabWRdwQDkbdvvqh6KvLRZd_DOU,17280
27
+ mp3-thai-clock/32.mp3,sha256=0MZDch997JaPhFHeiSXx5dPS5EvJM5PLbI2Mp9AXI9A,26496
28
+ mp3-thai-clock/33.mp3,sha256=blAaFmzq0whG_beh1yddZ5Yt3oWXSkkPx1Y8he-m8fk,25344
29
+ mp3-thai-clock/34.mp3,sha256=4URHRMYNS_5DV5A5a_gKh90VdUECScRsFif9DzlVHXY,28224
30
+ mp3-thai-clock/35.mp3,sha256=Xee7b-E5Cm8ghaLbCFpCckZqm6peItPP9Z77lkTb5sM,24768
31
+ mp3-thai-clock/36.mp3,sha256=c9SAPj8nqBgt0wW9A3cq-owZsqyNMEydEwD9ungq38M,17856
32
+ mp3-thai-clock/37.mp3,sha256=nHOvLDvtT3JRhUs_Jj-knuxWlK8PBy259ZWd9O4s8IQ,18432
33
+ mp3-thai-clock/38.mp3,sha256=2DHBXWfJBJuzljVTnxIQxjunK_yeEo8-2j9YKNquGGw,24192
34
+ mp3-thai-clock/39.mp3,sha256=DY87aRGHycKpAy0gW-LP8XsZE51ceq-ahaGwj2nyUo8,42048
35
+ mp3-thai-clock/4.mp3,sha256=LbyLZ2j0C3uGUoCnkUo3trICfFz-wK39bnFsr2wcRuM,17856
36
+ mp3-thai-clock/40.mp3,sha256=9mm8QC_tq1RHvMT45rh9QCFOlh0AWQFANbHTt0Uo0NQ,20160
37
+ mp3-thai-clock/41.mp3,sha256=sX3FL9J7XOigssnZVbj6K1ABN0YP-Q0L6nIbUVPI8uo,18432
38
+ mp3-thai-clock/42.mp3,sha256=T9Ns104ptNV70cSpYQwO2fO4gEna8gZbG3Pea1BoW9s,20736
39
+ mp3-thai-clock/43.mp3,sha256=SAOzOgaspjqSh-F71g4YxHS2P7b1fq8yH0onkiFscJQ,32256
40
+ mp3-thai-clock/44.mp3,sha256=fBDsPj6d5YFLLi2wqXgQO4umoPwUXhwtZQat6PPKy1w,23040
41
+ mp3-thai-clock/45.mp3,sha256=rzL9-jsFYvhZxRSRzVIjvmtscgvcxSvOsAL3Kqrf0J8,22464
42
+ mp3-thai-clock/46.mp3,sha256=i-0LI4JCf__5Lr7tpemPDdviNxFZk1D98SqIaUthJiA,21312
43
+ mp3-thai-clock/47.mp3,sha256=p5Qjuf_u9ETYxcr6YI28kKvIIKCEihpKP5pb2QaHdJ0,18432
44
+ mp3-thai-clock/48.mp3,sha256=Of1yXaUZIRSzCzf5WkYQ_2suD_bnVkLw_MMC6lq8WTA,22464
45
+ mp3-thai-clock/49.mp3,sha256=7kQnXWq-0sn0-0R0MSoK3Z5DoElQmtkfZL3rVZ5lysY,21888
46
+ mp3-thai-clock/5.mp3,sha256=rbNbWPIb7JcjLxDaX9l9PF8q7j-7oQmnsPkuePpBS8E,20736
47
+ mp3-thai-clock/50.mp3,sha256=kBi8G_FVG828vo_Q8I-7OlpEH9APpI-iS7A4MtWfzco,17280
48
+ mp3-thai-clock/51.mp3,sha256=W_Mt9fVRG6WsqqFqG1zeENeVfPSO4g-dcCnmgG_oMCU,21312
49
+ mp3-thai-clock/52.mp3,sha256=o4xeb8Q1DQUK-c865nYntEDmHhx6wdnoaOXfcZIxDys,25920
50
+ mp3-thai-clock/53.mp3,sha256=sN8iwPMbU8tlc1Q4flGdNtxWBxyMSYkEGjYWquAufI8,27072
51
+ mp3-thai-clock/54.mp3,sha256=by_tKZihkHormeA-kpbwAaOdN6mLdPi7aZN6Kwrg6Bs,18432
52
+ mp3-thai-clock/55.mp3,sha256=IGnSrsv6QgjUWruNLJ8gaLX6iBbCj_E3-sIbPEBhN-0,26496
53
+ mp3-thai-clock/56.mp3,sha256=0eHTXImY3n1EQ3EcKkkjHdFXqC1rfMW5SzfHDDUAC_o,27072
54
+ mp3-thai-clock/57.mp3,sha256=WvU0PqJp-dXgK8WDhk7vhfhP-2DBVmcV4_rRSFfIWSs,19008
55
+ mp3-thai-clock/58.mp3,sha256=jRp7JuI89pkg4u1mk9nu-xhu6XVwLWWrduCDWeLiXPk,20736
56
+ mp3-thai-clock/59.mp3,sha256=kMC7y8sgjEhPrg3VACYlKh_4IgFMm0b4TTH_Hv6xav8,25344
57
+ mp3-thai-clock/6.mp3,sha256=dUGA4X92cWVbQcic7MdtSqULTQ25IcRIj3X5qA9wIIk,20736
58
+ mp3-thai-clock/7.mp3,sha256=w4rvOaYDnQGnCjxZB4ghNMDETSlyTwX8VnV6EJHlQNo,13248
59
+ mp3-thai-clock/8.mp3,sha256=a7APLXfBT9aP2sTqZiWbuUvu9tMHdcE9BXTyYJsltCQ,10944
60
+ mp3-thai-clock/9.mp3,sha256=4z2RnmpSdvwfkbC_aXKojQaJlImklPo4Cdt_5m6IGIc,47808
61
+ mp3-thai-clock/begintime.mp3,sha256=NLRuIlY5_imRmPnnMFYezExN5vjJgwX0fA6G6bUzxA4,52416
62
+ mp3-thai-clock/endtime.mp3,sha256=CLHBmzPeUSRCwr7J-VePijF0lt4-IRY6yTCDYx1b2oA,63936
63
+ mp3-thai-clock/hour.mp3,sha256=WZAEGYRzMUOjwDKDsLDcqrdzM_ogptz7IwcmCVNf4nU,24768
64
+ mp3-thai-clock/minute.mp3,sha256=41CfbPTwIl37KxUs6NqoFcBwKRpquoRkajl1rOKQ8Y4,20160
65
+ mp3-thai-clock/second.mp3,sha256=jrsTl7Xe76wdVxG8ZLd9sC4WT1LcFkMi4Hg39kKEiR8,22464
66
+ talking_clock/__init__.py,sha256=ZpskRGArCVDPUhQgIT1ldyFadvhDMsgFUw8BgVcHPpU,861
67
+ talking_clock/clock.py,sha256=2eYj9hxQ6mcGadBP4AzACy7bFg9tlBsf1LAhlM_HGfY,6738
68
+ talking_clock/exceptions.py,sha256=etIN1wya1SH6FgbpVUgo6li9wUH6QNtZMhbH1ivDIR8,541
69
+ talking_clock/player.py,sha256=N-jjDmZ47dXjQHN4qzVfp8BkGYK5WR39avK_3oIqvXg,11555
70
+ thai_talking_clock-1.0.0.dist-info/METADATA,sha256=TRxgoyCS_f_efMhhiALQr4VDP-QiG049mV3WKHT9Dec,5010
71
+ thai_talking_clock-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
72
+ thai_talking_clock-1.0.0.dist-info/entry_points.txt,sha256=qcmPgyWTE2FljY4xssREP0BulGaFIB2ytOCAHf60w90,103
73
+ thai_talking_clock-1.0.0.dist-info/top_level.txt,sha256=JKWeqB0T12jpaROERGoZmI5USE_b4eG8h9nBY5I40zE,29
74
+ thai_talking_clock-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ talking-clock = test_talking_clock:main
3
+ thai-talking-clock = test_talking_clock:main
@@ -0,0 +1,2 @@
1
+ mp3-thai-clock
2
+ talking_clock