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.
- mp3-thai-clock/0.mp3 +0 -0
- mp3-thai-clock/1.mp3 +0 -0
- mp3-thai-clock/10.mp3 +0 -0
- mp3-thai-clock/11.mp3 +0 -0
- mp3-thai-clock/12.mp3 +0 -0
- mp3-thai-clock/13.mp3 +0 -0
- mp3-thai-clock/14.mp3 +0 -0
- mp3-thai-clock/15.mp3 +0 -0
- mp3-thai-clock/16.mp3 +0 -0
- mp3-thai-clock/17.mp3 +0 -0
- mp3-thai-clock/18.mp3 +0 -0
- mp3-thai-clock/19.mp3 +0 -0
- mp3-thai-clock/2.mp3 +0 -0
- mp3-thai-clock/20.mp3 +0 -0
- mp3-thai-clock/21.mp3 +0 -0
- mp3-thai-clock/22.mp3 +0 -0
- mp3-thai-clock/23.mp3 +0 -0
- mp3-thai-clock/24.mp3 +0 -0
- mp3-thai-clock/25.mp3 +0 -0
- mp3-thai-clock/26.mp3 +0 -0
- mp3-thai-clock/27.mp3 +0 -0
- mp3-thai-clock/28.mp3 +0 -0
- mp3-thai-clock/29.mp3 +0 -0
- mp3-thai-clock/3.mp3 +0 -0
- mp3-thai-clock/30.mp3 +0 -0
- mp3-thai-clock/31.mp3 +0 -0
- mp3-thai-clock/32.mp3 +0 -0
- mp3-thai-clock/33.mp3 +0 -0
- mp3-thai-clock/34.mp3 +0 -0
- mp3-thai-clock/35.mp3 +0 -0
- mp3-thai-clock/36.mp3 +0 -0
- mp3-thai-clock/37.mp3 +0 -0
- mp3-thai-clock/38.mp3 +0 -0
- mp3-thai-clock/39.mp3 +0 -0
- mp3-thai-clock/4.mp3 +0 -0
- mp3-thai-clock/40.mp3 +0 -0
- mp3-thai-clock/41.mp3 +0 -0
- mp3-thai-clock/42.mp3 +0 -0
- mp3-thai-clock/43.mp3 +0 -0
- mp3-thai-clock/44.mp3 +0 -0
- mp3-thai-clock/45.mp3 +0 -0
- mp3-thai-clock/46.mp3 +0 -0
- mp3-thai-clock/47.mp3 +0 -0
- mp3-thai-clock/48.mp3 +0 -0
- mp3-thai-clock/49.mp3 +0 -0
- mp3-thai-clock/5.mp3 +0 -0
- mp3-thai-clock/50.mp3 +0 -0
- mp3-thai-clock/51.mp3 +0 -0
- mp3-thai-clock/52.mp3 +0 -0
- mp3-thai-clock/53.mp3 +0 -0
- mp3-thai-clock/54.mp3 +0 -0
- mp3-thai-clock/55.mp3 +0 -0
- mp3-thai-clock/56.mp3 +0 -0
- mp3-thai-clock/57.mp3 +0 -0
- mp3-thai-clock/58.mp3 +0 -0
- mp3-thai-clock/59.mp3 +0 -0
- mp3-thai-clock/6.mp3 +0 -0
- mp3-thai-clock/7.mp3 +0 -0
- mp3-thai-clock/8.mp3 +0 -0
- mp3-thai-clock/9.mp3 +0 -0
- mp3-thai-clock/begintime.mp3 +0 -0
- mp3-thai-clock/endtime.mp3 +0 -0
- mp3-thai-clock/hour.mp3 +0 -0
- mp3-thai-clock/minute.mp3 +0 -0
- mp3-thai-clock/second.mp3 +0 -0
- talking_clock/__init__.py +35 -0
- talking_clock/clock.py +198 -0
- talking_clock/exceptions.py +17 -0
- talking_clock/player.py +320 -0
- thai_talking_clock-1.0.0.dist-info/METADATA +122 -0
- thai_talking_clock-1.0.0.dist-info/RECORD +74 -0
- thai_talking_clock-1.0.0.dist-info/WHEEL +5 -0
- thai_talking_clock-1.0.0.dist-info/entry_points.txt +3 -0
- 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
|
mp3-thai-clock/hour.mp3
ADDED
|
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
|
talking_clock/player.py
ADDED
|
@@ -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,,
|