Created
May 28, 2012 13:52
-
-
Save claws/2819315 to your computer and use it in GitHub Desktop.
Create Your Own Weather Report Audio Track Using Current BOM Data
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
This script retrieves weather forecast and observation | |
data from the Australian Bureau of Meteorology (BOM) | |
and converts them into a spoken word weather report | |
audio file. | |
This script was written to generate the first track for | |
my morning alarm clock playlist. | |
For more information and to download the intro and outro | |
mp3 jingles see: | |
http://metaclaws.com/2012/05/28/create_your_own_weather_report_audio_track_using_current_bom_data | |
""" | |
from twisted.internet import reactor, defer, utils | |
from txbom.forecasts import get_forecast, forecastToDict | |
from txbom.observations import get_observations | |
import StringIO | |
import datetime | |
import logging | |
import os | |
import txbom | |
# Configuration Settings | |
# The forecast identifier for your location can be found from the BOM web site. | |
forecast_id = "IDS10034" | |
# The observations identifier is a url that points to the JSON | |
# encoded observations for your location. The url link is | |
# typically found at the bottom of the BOM web page that shows | |
# observations for your location. | |
observation_id = "http://www.bom.gov.au/fwo/IDS60901/IDS60901.94675.json" | |
# Define the words to be spoken by the weather report making use | |
# of forecast and observation dict keywords for substitutions | |
# in a string.template. | |
# | |
# Valid Observation substitution keywords are: | |
# obsv_identifier : The observation identifier | |
# obsv_town : The observation town | |
# obsv_state : The observation state | |
# obsv_time_zone : The time zone | |
# obsv_refreshed : Time observations were last refreshed | |
# obsv_full_date_time : A worded date time string (e.g. Sunday 27 May 2012) | |
# obsv_date : The observation date | |
# obsv_time : The observation time | |
# obsv_air_temp : The air temperature | |
# obsv_apparent_temp : The apparent air temperature | |
# obsv_wind_dir : A worded wind direction, | |
# obsv_wind_speed_kmh : The wind speed, | |
# obsv_wind_gust_speed_kmh : The wind gust speed | |
# | |
# Valid forecast substitution keywords are: | |
# fcast_id : The forecast location identifier, | |
# fcast_town : The forecast location name, | |
# fcast_state : The forecast location state, | |
# fcast_date : The forecast date | |
# fcast_time : The forecast time | |
# fcast_warnings : A string of any weather warnings | |
# fcast_uv_alert : The time period for any uv alert | |
# fcast_uv_index : The uv index value | |
# fcast_uv_index_name : The uv index name (e.g. Moderate) | |
# fcast_today : A description of today (e.g. Sunday 27 May 2012) | |
# fcast_today_content : Forecast for today | |
# fcast_today_precis : A summary forecast for today | |
# fcast_today_temperature : Forecast max temp for today | |
# fcast_tomorrow : Forecast for tomorrow | |
# fcast_tomorrow_precis : A summary forecast for tomorrow | |
# fcast_tomorrow_minimum : The min temp tomorrow | |
# fcast_tomorrow_maximum : The max temp tomorrow | |
# fcast_five_days : A list of (day_name, temp_min, temp_max, precis) for the next five days | |
# fcast_raw : The full raw forecast string | |
# | |
report_template = """ | |
Current weather conditions for %(obsv_town)s, %(obsv_state)s on %(obsv_date)s at %(obsv_time)s. | |
The apparent temperature is %(obsv_apparent_temp)s degrees. | |
The wind is blowing from a %(obsv_wind_dir)s direction at %(obsv_wind_speed_kmh)s kilometers per hour, | |
gusting to %(obsv_wind_gust_speed_kmh)s kilometers per hour. | |
Latest forecast for %(fcast_town)s %(fcast_state)s issued on %(fcast_date)s at %(fcast_time)s. | |
%(fcast_today)s | |
%(fcast_today_content)s | |
Today's top temperature is forecast to be %(fcast_today_temperature)s. | |
This concludes the automated weather report. | |
""" | |
# Define a location to save temporary data files such as the | |
# text version of the weather report and the TTS .aac version | |
# of the file. | |
temp_dir = "" | |
# Define the location of the finished audio file. | |
# | |
weather_report_mp3_file = "weather_report.mp3" | |
# Path to TTS 'say' executable | |
# | |
text_to_speech_executable = "/usr/bin/say" | |
# Path to the ffmpeg executable used to convert weather report | |
# spoken word to mp3 and to join the intro, weather report and | |
# outro tracks into a single mp3 file. | |
# | |
ffmpeg_exe = "/usr/local/bin/ffmpeg" | |
# intro/outro mp3 files are added (before and after) to the weather report. | |
# This is useful if you want a jingle to surround your weather report - such | |
# as a news bulletin sound effect [i.e. from GarageBand, etc]. This is a | |
# aid to cueing a sleepy brain by building a memory association that makes | |
# you pay attention to the audio as you recognise that the weather forecast | |
# is about to come on. | |
intro_audio_file = "intro.mp3" | |
outro_audio_file = "outro.mp3" | |
# Available voices supported on OS X by the 'say' program | |
# female: | |
# Agnes | |
# Kathy | |
# Princess | |
# Vicki | |
# Victoria | |
# | |
# male: | |
# Alex | |
# Bruce | |
# Fred | |
# Junior | |
# Ralph | |
# Albert | |
# | |
# novelty: | |
# Bad News | |
# Bahh | |
# Bells | |
# Boing | |
# Bubbles | |
# Cellos | |
# Deranged | |
# Good News | |
# Hysterical | |
# Pipe Organ | |
# Trinoids | |
# Whisper | |
# Zarvox | |
text_to_speech_voice = "Alex" | |
@defer.inlineCallbacks | |
def generate_weather_report(): | |
""" | |
Retrieve weather report internet resources, | |
convert to a compact weather report text file suitable for text-to-speech, | |
convert text to a spoken weather report via text-to-speech tool, | |
convert intermediate text-to-speech output to an mp3. | |
package intro, weather report and outro into a single audio file. | |
""" | |
forecast_txt_file = os.path.join(temp_dir, 'forecast.txt') | |
weather_report_txt_file = os.path.join(temp_dir, 'weather_report.txt') | |
weather_report_tts_file = os.path.join(temp_dir, 'weather_report.aac') | |
logging.info("Retrieving forecast and observation data") | |
observations = yield get_observations(observation_id) | |
forecast = yield get_forecast(forecast_id) | |
logging.info("Forecast and observation data retrieval successful") | |
# save a copy of the retrieved forecast (at the temp location) | |
# for debugging purposes | |
fd = open(forecast_txt_file, 'w') | |
fd.write(forecast) | |
fd.close() | |
templateDict = processObservations(observations) | |
templateDict.update(processForecast(forecast)) | |
weatherReport = report_template % templateDict | |
logging.debug("Weather Report is:\n%s" % weatherReport) | |
fd = open(weather_report_txt_file, 'w') | |
fd.write(weatherReport) | |
fd.close() | |
logging.info("Generating weather report") | |
###################################################################### | |
# | |
# Pass weather report text file through a TTS engine | |
# to generate the spoken weather report. | |
# | |
command = text_to_speech_executable | |
command_args = ["-v", "%s" % text_to_speech_voice, | |
"-o", "%s" % weather_report_tts_file, | |
"-f", "%s" % weather_report_txt_file] | |
result = yield utils.getProcessValue(executable=command, args=command_args) | |
if result: | |
full_command = "%s %s" % (command, " ".join(command_args)) | |
err_str = "%s exited with status %s. Unable to generate file: %s" % (full_command, result, weather_report_tts_file) | |
logging.error(err_str) | |
raise Exception(err_str) | |
logging.debug("Converted weather report text output to TTS file: %s\n" % weather_report_tts_file) | |
###################################################################### | |
# | |
# Convert weather report TTS audio to mp3 and return mp3 filename | |
# | |
command = ffmpeg_exe | |
command_args = ["-y", # forces overwrite | |
"-i", "%s" % weather_report_tts_file, # input filename | |
"-ac", "1", # 1 (mono) channel | |
"-acodec", "libmp3lame", # use lame | |
"-ar", "44100", # bitrate of 44.1kHz | |
"-metadata", "title=\"Weather Report\"", # title tag | |
"-metadata", "artist=\"Weather Service\"", # artist tag | |
"-metadata", "album=\"Weather Report\"", # album tag | |
"%s" % weather_report_mp3_file] # output file path | |
result = yield utils.getProcessValue(executable=command, args=command_args) | |
if result: | |
full_command = "%s %s" % (command, " ".join(command_args)) | |
err_str = "%s exited with status %s. Unable to generate file: %s" % (full_command, result, weather_report_mp3_file) | |
logging.error(err_str) | |
raise Exception(err_str) | |
logging.debug("Converted weather report TTS output to mp3 file: %s\n" % weather_report_mp3_file) | |
###################################################################### | |
# | |
# Concatenate the intro, weather report and outro tracks into a | |
# single audio track. | |
# | |
# For this to work properly the intro/outro tracks need to have the | |
# same bitrate as the weather report. | |
# | |
# The weather report mp3 file is configured to contain title, artist | |
# and album mp3 meta data but this gets lost when the files get | |
# concatenated. The end result is that the mp3 tags will be extracted | |
# from the first track, the intro. So the mp3 tags on the intro file | |
# should be setup appropriately for your needs. | |
# | |
outFile = StringIO.StringIO() | |
if os.path.exists(intro_audio_file): | |
fd = open(intro_audio_file, 'r') | |
outFile.write(fd.read()) | |
fd.close() | |
fd = open(weather_report_mp3_file, 'r') | |
outFile.write(fd.read()) | |
fd.close() | |
if os.path.exists(outro_audio_file): | |
fd = open(outro_audio_file, 'r') | |
outFile.write(fd.read()) | |
fd.close() | |
fd = open(weather_report_mp3_file, 'w') | |
fd.write(outFile.getvalue()) | |
fd.close() | |
# remove intermediate file resources | |
del outFile | |
os.remove(weather_report_tts_file) | |
os.remove(weather_report_txt_file) | |
logging.info("Finished generating weather report: %s\n" % weather_report_mp3_file) | |
defer.returnValue(True) | |
def processObservations(observations): | |
""" | |
Process weather observations data into an appropriate | |
format for use with a spoken word weather report. | |
""" | |
currentObservationData = observations.current | |
observationData = {} | |
for field in currentObservationData.fields: | |
observationData[field] = getattr(currentObservationData, field) | |
# Also add fields from the observation header that can be | |
# used to help identify the observation location. | |
for field in observations.header.fields: | |
observationData[field] = getattr(observations.header, field) | |
# extract the observation datetime and convert it to a date string | |
# looking like this: Sunday 27 May 2012 | |
full_datetime = datetime.datetime.strptime(observationData["local_date_time_full"], | |
"%Y%m%d%H%M%S") | |
# create a date string looking like: | |
date_description = "%s %s %s" % (full_datetime.strftime("%A"), | |
str(int(full_datetime.strftime("%d"))), | |
full_datetime.strftime("%B %Y")) | |
# substitute textual wind direction words for acronyms. For example, | |
# use north west in place of NW. | |
direction = observationData["wind_dir"] | |
if direction in txbom.WindDirections: | |
direction_description = txbom.WindDirections[direction] | |
else: | |
direction_description = direction | |
observationsDict = {"obsv_identifier" : observationData["ID"], | |
"obsv_town" : observationData["name"], | |
"obsv_state" : observationData["state"], | |
"obsv_time_zone" : observationData["time_zone"], | |
"obsv_refreshed" : observationData["refresh_message"], | |
"obsv_full_date_time" : observationData["local_date_time_full"], | |
"obsv_date" : date_description, | |
"obsv_time" : observationData["local_date_time"].split("/")[1], | |
"obsv_air_temp" : observationData["air_temp"], | |
"obsv_apparent_temp" : observationData["apparent_t"], | |
"obsv_wind_dir" : direction_description, | |
"obsv_wind_speed_kmh" : observationData["wind_spd_kmh"], | |
"obsv_wind_gust_speed_kmh" : observationData["gust_kmh"]} | |
return observationsDict | |
def processForecast(forecast): | |
""" | |
Process a forecast from the BoM into an appropriate | |
format for use with a spoken word weather report. | |
""" | |
forecastDict = forecastToDict(forecast) | |
return forecastDict | |
def weatherReportFailure(failure): | |
logging.error("Error detected:\n%s" % str(failure)) | |
def run(): | |
""" Generate the weather report """ | |
d = generate_weather_report() | |
d.addCallback(lambda _: reactor.callLater(1.0, reactor.stop)) | |
d.addErrback(weatherReportFailure) | |
if __name__ == "__main__": | |
logging.basicConfig(level=logging.INFO, | |
format="%(asctime)s - %(levelname)s - %(message)s") | |
reactor.callWhenRunning(run) | |
reactor.run() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment