It is currently April 20th, 2019, 3:13 pm

Speaking TextClock Version 3

Post your work-in-progress and completed skins to share and discuss.
User avatar
jsmorley
Developer
Posts: 19178
Joined: April 19th, 2009, 11:02 pm
Location: Fort Hunt, Virginia, USA

Speaking TextClock Version 3

jsmorley » February 4th, 2019, 5:25 pm

TextClock3_Feb 5, 2019.rmskin

1.jpg


Demonstrates using Lua to turn numbers into text, and uses the new Speech plugin, which is described at Speech Plugin.

TextClock3.ini:

Code: Select all

[Rainmeter]
Update=1000
DynamicWindowSize=1
AccurateText=1
BackgroundMode=2
SolidColor=0,0,0,1
LeftMouseUpAction=[!CommandMeasure SpeechHost "[MeasureScript]"]

[Metadata]
Name=TextClock3
Author=JSMorley
Version=3.0 - February 4, 2019
License=Creative Commons Attribution-Non-Commercial-Share Alike 3.0
Information=Displays the time with a "minutes before/after hour" text format||i.e "QUARTER AFTER TEN"||Displays the date in text format||Displays the weather in text format||Speaks the weather with the Speech plugin on click

[Variables]
; LocationCode can be obtained at http://wxdata.weather.com/wxdata/search/search?where=YourCityName
LocationCode=USVA0944
; UnitOfMeasure can be "i" (Fahrenheit / Imperial) or "m" (Celsius / Metric)
UnitOfMeasure=i

AfterText=after
UntilText=to
HalfText=past

[SpeechHost]
Measure=Plugin
Plugin=Speech
; Use Name if you know the name of the Text-to-Speech voice you want on your system, or use a
; combination of Gender and / or Index to find the voice you want. Use Debug=1 to list the 
; voice names available on your system in the Rainmeter log.
Name=Microsoft Zira Desktop
;Gender=Female
;Index=1
;Debug=1

[WeatherParent]
Measure=WebParser
URL=https://wxdata.weather.com/wxdata/weather/local/#LocationCode#?cc=*&unit=#UnitOfMeasure#&dayf=1
RegExp=(?siU)<ut>(.*)</ut>.*<cc>.*<tmp>(.*)</tmp>.*<t>(.*)</t>
FinishAction=[!EnableMeasure MeasureScript]

[UnitsChild]
Measure=WebParser
URL=[WeatherParent]
StringIndex=1

[TemperatureChild]
Measure=WebParser
URL=[WeatherParent]
StringIndex=2

[ConditionsChild]
Measure=WebParser
URL=[WeatherParent]
StringIndex=3

[MeasureScript]
Measure=Script
ScriptFile=TextClock3.lua
Disabled=1

[MeterTime]
Meter=String
InlineSetting=Face | Segoe UI
InlineSetting2=Size | 20
InlineSetting3=Weight | 100
InlineSetting4=Color | 255,255,255,255
InlineSetting5=Case | Upper
InlineSetting6=Size | 17
InlinePattern6=(?i)minutes?
InlineSetting7=Size | 17
InlinePattern7=(?i)#AfterText# || #UntilText# || #HalfText#
InlineSetting8=Size | 17
InlinePattern8=(?i)it's
InlineSetting9=Size | 17
InlinePattern9=(?i)in the
SolidColor=0,0,0,1
AntiAlias=1

[MeterDate]
Meter=String
Y=0R
InlineSetting=Face | Segoe UI
InlineSetting2=Size | 20
InlineSetting3=Weight | 100
InlineSetting4=Color | 255,255,255,255
InlineSetting5=Case | Upper
InlineSetting6=Size | 17
InlinePattern6=(?i)the
InlineSetting7=Size | 17
InlinePattern7=(?i)of
InlineSetting8=Size | 17
InlinePattern8=(?i)^on
SolidColor=0,0,0,1
AntiAlias=1

[MeterConditions]
Meter=String
Y=0R
InlineSetting=Face | Segoe UI
InlineSetting2=Size | 20
InlineSetting3=Weight | 100
InlineSetting4=Color | 255,255,255,255
InlineSetting5=Case | Upper
InlineSetting6=Size | 17
InlinePattern6=(?i)the weather is
InlineSetting7=Size | 17
InlinePattern7=(?i)and
SolidColor=0,0,0,1
AntiAlias=1

[MeterTemperature]
Meter=String
Y=0R
InlineSetting=Face | Segoe UI
InlineSetting2=Size | 20
InlineSetting3=Weight | 100
InlineSetting4=Color | 255,255,255,255
InlineSetting5=Case | Upper
InlineSetting6=Size | 17
InlinePattern6=(?i)it's
InlineSetting7=Size | 17
InlinePattern7=(?i)at
InlineSetting8=Size | 17
InlinePattern8=(?i)degrees?
InlineSetting9=Size | 17
InlinePattern9=(?i)and
SolidColor=0,0,0,1
AntiAlias=1
TextClock3.lua:

Code: Select all

function Initialize()

		measureUnits = SKIN:GetMeasure('UnitsChild')
		measureTemperature = SKIN:GetMeasure('TemperatureChild')
		measureConditions = SKIN:GetMeasure('ConditionsChild')
		
		afterText = SKIN:GetVariable('AfterText')
		untilText = SKIN:GetVariable('UntilText')
		halfText = SKIN:GetVariable('HalfText')
		
		dayTable = {}
		
		dayTable['0'] = ''
		dayTable['1'] = 'first'
		dayTable['2'] = 'second'
		dayTable['3'] = 'third'
		dayTable['4'] = 'forth'
		dayTable['5'] = 'fifth'
		dayTable['6'] = 'sixth'
		dayTable['7'] = 'seventh'
		dayTable['8'] = 'eighth'
		dayTable['9'] = 'ninth'
		dayTable['10'] = 'tenth'
		dayTable['11'] = 'eleventh'
		dayTable['12'] = 'twelfth'
		dayTable['13'] = 'thirteenth'
		dayTable['14'] = 'fourteenth'
		dayTable['15'] = 'fifteenth'
		dayTable['16'] = 'sixteenth'
		dayTable['17'] = 'seventeenth'
		dayTable['18'] = 'eighteenth'
		dayTable['19'] = 'nineteenth'
		dayTable['20'] = 'twentieth'
		dayTable['30'] = 'thirtieth'
		
		numTable = {}
		
		numTable['0'] = 'zero'
		numTable['1'] = 'one'
		numTable['2'] = 'two'
		numTable['3'] = 'three'
		numTable['4'] = 'four'
		numTable['5'] = 'five'
		numTable['6'] = 'six'
		numTable['7'] = 'seven'
		numTable['8'] = 'eight'
		numTable['9'] = 'nine'
		numTable['10'] = 'ten'
		numTable['11'] = 'eleven'
		numTable['12'] = 'twelve'
		numTable['13'] = 'thirteen'
		numTable['14'] = 'fourteen'
		numTable['15'] = 'fifteen'
		numTable['16'] = 'sixteen'
		numTable['17'] = 'seventeen'
		numTable['18'] = 'eighteen'
		numTable['19'] = 'nineteen'
		numTable['20'] = 'twenty'
		numTable['30'] = 'thirty'
		numTable['40'] = 'forty'
		numTable['50'] = 'fifty'
		numTable['60'] = 'sixty'
		numTable['70'] = 'seventy'
		numTable['80'] = 'eighty'
		numTable['90'] = 'ninety'
		numTable['100'] = 'one hundred'

end

function Update()

	speakTime = TimeString()
	speakDate = DateString()
	speakConditions = ConditionsString()
	speakTemperature = TemperatureString()
	
	return speakTime .. ', ' .. speakDate .. '. ' .. speakConditions .. ', an ' .. speakTemperature

end

function TimeString()

	hour24 = tonumber(os.date('%H'))
	hour12 = tonumber(os.date('%I'))
	minute = tonumber(os.date('%M'))
	
	if hour24 < 12 then
		partOfDay = 'morning'
	elseif hour24 >= 12 and hour24 < 17 then
		partOfDay = 'afternoon'
	else
		partOfDay = 'evening'
	end
	
	if minute < 30 then
		adjustedMinute = minute
		midText = afterText
	elseif minute == 30 then
		adjustedMinute = minute
		midText = halfText
	else
		adjustedMinute = 60 - minute
		hour12 = hour12 == 12 and 1 or hour12 + 1
		midText = untilText		
	end

	if adjustedMinute >= 20 and adjustedMinute < 30 then 
		if adjustedMinute == 20 then
			minuteText = 'twenty'
		else
			minuteText = 'twenty'..'-'..numTable[tostring(adjustedMinute - 20)]
		end
	else
		if adjustedMinute < 10 then
			if adjustedMinute == 1 then
				minuteText = numTable[tostring(adjustedMinute)]..' minute'
			else
				minuteText = numTable[tostring(adjustedMinute)]..' minutes'
			end
		elseif adjustedMinute == 15 then
			minuteText = 'quarter'
		elseif adjustedMinute == 30 then
			minuteText = 'half'
		else
			minuteText = numTable[tostring(adjustedMinute)]
		end
	end

	if adjustedMinute == 0 then
		retString = 'it\'s '..numTable[tostring(hour12)]..' o\'clock'..' in the '..partOfDay
	else
		retString = 'it\'s '..minuteText.. ' '..midText..' '..numTable[tostring(hour12)]..' in the '..partOfDay
	end
	
	if hour24 == 12 and minute == 0 then
		retString = 'it\'s '..numTable[tostring(hour12)]..' o\'clock noon'
	end
	if hour24 == 0 and minute == 0 then
		retString = 'it\'s '..numTable[tostring(hour12)]..' o\'clock midnight'
	end
	
	SKIN:Bang('!SetOption', 'MeterTime', 'Text', retString)
	return retString

end

function DateString()

	weekdayName = os.date('%A')
	monthName = os.date('%B')
	
	dayNumber = os.date('%d')

	if tonumber(dayNumber) < 20 or tonumber(dayNumber) % 10 == 0 then
		dayText = dayTable[tostring(tonumber(dayNumber))]
	else
		dayLeft = string.sub(dayNumber, 1, 1) * 10
		dayRight = string.sub(dayNumber, 2)
		dayText = numTable[tostring(dayLeft)]..'-'..dayTable[dayRight]
	end
	
	SKIN:Bang('!SetOption', 'MeterDate', 'Text', 'on '..weekdayName..', the '..dayText..' of '..monthName)
	return 'on '..weekdayName..', the '..dayText..' of '..monthName
	
end

function ConditionsString()

	conditionsText = measureConditions:GetStringValue()
	conditionsText = string.gsub(conditionsText, ' / ', ' and ')
	SKIN:Bang('!SetOption', 'MeterConditions', 'Text', 'the weather is '..conditionsText)
	return 'the weather is '..conditionsText

end

function TemperatureString()

	tempUnit = measureUnits:GetStringValue()
	tempValue = tonumber(measureTemperature:GetStringValue())

	if tempUnit == 'C' then
		if tempValue > 31.7 then tempDesc = 'really hot'
			elseif tempValue > 26.1 then tempDesc = 'sort of hot'
			elseif tempValue > 15 then tempDesc = 'fairly warm'
			elseif tempValue > 9.4 then tempDesc = 'fairly cool'
			elseif tempValue > 0 then tempDesc = 'sort of chilly'
			else tempDesc = 'really cold'
		end
	end
	
	if tempUnit == 'F' then
		if tempValue > 89 then tempDesc = 'really hot'
			elseif tempValue > 79 then tempDesc = 'sort of hot'
			elseif tempValue > 59 then tempDesc = 'fairly warm'
			elseif tempValue > 49 then tempDesc = 'fairly cool'
			elseif tempValue > 32 then tempDesc = 'sort of chilly'
			else tempDesc = 'really cold'
		end
	end	
	
	if tempValue < 0 then
		tempValue = math.abs(tempValue)
		belowZero = 1
	else
		belowZero = 0
	end
	
	if tempValue > 100 then
		hundredText = 'one hundred and '
		tempValue = tempValue - 100
	else
		hundredText = ''
	end
	
	if tempValue < 20 or tempValue % 10 == 0 then
		tempText = numTable[tostring(tempValue)]
	else
		tempLeft = string.sub(tempValue, 1, 1) * 10
		tempRight = string.sub(tempValue, 2)
		tempText = numTable[tostring(tempLeft)]..'-'..numTable[tempRight]
	end
	
	if tempValue == 1 and hundredText == '' then 
		degreesText = ' degree'
	else
		degreesText = ' degrees'
	end
	
	if belowZero == 0 then
		SKIN:Bang('!SetOption', 'MeterTemperature', 'Text', 'it\'s '..tempDesc..' at '..hundredText..tempText..degreesText)
		return 'it\'s '..tempDesc..' at '..hundredText..tempText..degreesText
	else
		SKIN:Bang('!SetOption', 'MeterTemperature', 'Text', 'it\'s '..tempDesc..' at '..hundredText..tempText..degreesText..' below zero')
		return 'it\'s '..tempDesc..' at '..hundredText..tempText..degreesText..' below zero'
	end
	
end
You do not have the required permissions to view the files attached to this post.
User avatar
ikarus1969
Posts: 322
Joined: February 28th, 2011, 3:20 pm
Location: Vienna, Austria

Re: Speaking TextClock Version 3

ikarus1969 » February 5th, 2019, 8:26 am

During adapting your text clock to german i stumbled across a minor error in the lua code in the function TimeString():

At the end there are 3 if-statements.
The first one (adjustedMinute == 0) has a return in both the then and the else part so the other two if-statements never will be executed.
I think it would help if you introduce a variable before the 3 if-statements which will be set to the desired string and use this variable at the end of the function to set the meter-text and the return-value.

Code: Select all

	ret = ''

	if adjustedMinute == 0 then
		ret = 'it\'s '..numTable[tostring(hour12)]..' o\'clock'..' in the '..partOfDay
	else
		ret = 'it\'s '..minuteText.. ' '..midText..' '..numTable[tostring(hour12)]..' in the '..partOfDay
	end
	if hour24 == 12 and minute == 0 then
		ret = 'it\'s '..numTable[tostring(hour12)]..' o\'clock noon'
	end
	if hour24 == 0 and minute == 0 then
		ret = 'it\'s '..numTable[tostring(hour12)]..' o\'clock midnight'
	end		
	
	SKIN:Bang('!SetOption', 'MeterTime', 'Text', ret)
	return ret
User avatar
jsmorley
Developer
Posts: 19178
Joined: April 19th, 2009, 11:02 pm
Location: Fort Hunt, Virginia, USA

Re: Speaking TextClock Version 3

jsmorley » February 5th, 2019, 1:57 pm

ikarus1969 wrote:
February 5th, 2019, 8:26 am
During adapting your text clock to german i stumbled across a minor error in the lua code in the function TimeString():

At the end there are 3 if-statements.
The first one (adjustedMinute == 0) has a return in both the then and the else part so the other two if-statements never will be executed.
I think it would help if you introduce a variable before the 3 if-statements which will be set to the desired string and use this variable at the end of the function to set the meter-text and the return-value.

Code: Select all

	ret = ''

	if adjustedMinute == 0 then
		ret = 'it\'s '..numTable[tostring(hour12)]..' o\'clock'..' in the '..partOfDay
	else
		ret = 'it\'s '..minuteText.. ' '..midText..' '..numTable[tostring(hour12)]..' in the '..partOfDay
	end
	if hour24 == 12 and minute == 0 then
		ret = 'it\'s '..numTable[tostring(hour12)]..' o\'clock noon'
	end
	if hour24 == 0 and minute == 0 then
		ret = 'it\'s '..numTable[tostring(hour12)]..' o\'clock midnight'
	end		
	
	SKIN:Bang('!SetOption', 'MeterTime', 'Text', ret)
	return ret
You are absolutely right. I added in the speech stuff without double checking all possibilities for the time, and what used to work fine for just the Text bangs didn't when I added the returns to accommodate speech. Fixed in the .rmskin in the first post. Thanks for finding that!

I didn't initialize the retString variable, as that step isn't really needed. It can never be appended to or return an old value. The first if/then/else ensures that at least one of the if statements must be true.

I guess this skin could be localized, but after a quick look at it I decided against it, as the way the date and time is "spoken" in different languages, and even different cultures within languages, is subtle and difficult to get right. Things like "quarter to seven" or "half past eight" may not have an equivalent in how the time is "spoken" in other languages. So I went with what "sounded natural" to my American English ear. Feel free to distribute a German version if you like.