Web Images Videos Maps News Shopping Gmail more »
Recently Visited Groups | Help | Sign in
Google Groups Home
Running Coach (#82)
There are currently too many topics in this group that display first. To make this topic appear first, remove this option from another topic.
There was an error processing your request. Please try again.
flag
  9 messages - Collapse all  -  Translate all to Translated (View all originals)
The group you are posting to is a Usenet group. Messages posted to this group will make your email address visible to anyone on the Internet.
Your reply message has not been sent.
Your post was successful
 
From:
To:
Cc:
Followup To:
Add Cc | Add Followup-to | Edit Subject
Subject:
Validation:
For verification purposes please type the characters you see in the picture below or the numbers you hear by clicking the accessibility icon. Listen and type the numbers you hear
 
Ruby Quiz  
View profile  
 More options Jun 9 2006, 9:28 am
From: Ruby Quiz <ja...@grayproductions.net>
Date: Fri, 9 Jun 2006 22:28:20 +0900
Local: Fri, Jun 9 2006 9:28 am
Subject: [QUIZ] Running Coach (#82)
The three rules of Ruby Quiz:

1.  Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2.  Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3.  Enjoy!

Suggestion:  A [QUIZ] in the subject of emails about the problem helps everyone
on Ruby Talk follow the discussion.  Please reply to the original quiz message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- =-=-=

by Benjohn Barnes

I've started to jog with my girlfriend. It's hell. We're following a "programme"
at this web site:

        http://www.coolrunning.com/engine/2/2_3/181.shtml

The aim is to get you from being able to alternate hobbling and brisk walking,
to being able to jog for 20 minutes solidly. Over eight weeks you exercise for
twenty minutes, three times a week. Over the eight weeks, the ratio of jog to
walk steadily increases, and the jogs get longer, while the walks become
shorter.

I was explaining to a friend that it's incredibly difficult for me to look at a
stop watch and work out in my head if we're supposed to be jogging or walking,
how many more jogs we've got to do, and when I can stop and rest. He suggested:
'why not tape yourself giving prompts about when to start and stop'. A brilliant
plan. 'Even better, record it on to your phone'. Genius! Except I'm the kind of
person who's lazy enough to spend eight times as long writing a program to try
to do this for me.

So, the quiz is:

Write a program to create the tracks for each of the eight weeks. Make it give
helpful and enthusiastic advice like "you've got to run for another minute / 30
seconds / 15 seconds ...", "walk now for two minutes, you've got three jogs
left", "you're on jog 2 of 6", or "well done, that's your last jog. Don't forget
to cool down and stretch!"

I just used my Mac's speech synth, and parked my phone near to the speaker on
record, in a quiet room (except for the planes every minute heading down to
Heathrow). There'd be "bonus points" for actually creating the MP3 directly. Of
course, you don't really need to get the computer to speak. It could just print
out the messages at the appropriate time.


    Reply to author    Forward  
You must Sign in before you can post messages.
To post a message you must first join this group.
Please update your nickname on the subscription settings page before posting.
You do not have the permission required to post.
Benjohn Barnes  
View profile  
 More options Jun 13 2006, 2:25 am
From: Benjohn Barnes <benj...@fysh.org>
Date: Tue, 13 Jun 2006 15:25:48 +0900
Local: Tues, Jun 13 2006 2:25 am
Subject: [QUIZ part solution] Running Coach (#82)
I thought that this problem was actually a lot more subtle than it  
seemed on the outside. Getting the coach to put together sensible and  
varied sentences seemed to be hard. I really wanted to find a much  
more elegant solution, perhaps some sort of template based approach;  
so far that has eluded me though. It seems to be something that  
doesn't easily factor down in to simple and clean functions. I had  
trouble, at least.

Here's the chunk of code that I have - it's currently only for week  
3! You could, of course, write a similar function for the other  
weeks, but there's _got_ to be a better way than that?

def count_down(s, activity)
   # Encouragement for the last few seconds (which could get annoying  
on longer runs?).
   counts =  { 10 =>  "10 more seconds.",
               20 =>  "20 seconds to go.",
               30 =>  "Half a minute to go.",
               60 =>  "You have 1 more minute of #{activity} left.",
               90 =>  "You have 1 and a half minutes of #{activity}  
to go."}

   # Add in encouragement / prompts for minutes.
   [2, 3, 4, 6, 8, 10, 12, 15, 20, 25, 30].each {|m| counts[m*60] =  
"You have #{m} minutes of #{activity} to go."}

   # Build an ordered array of the possible lengths of time, and find  
the index of this
   # activity's length.
   times = counts.keys.sort
   start_index = times.index(s); raise "#{s} is not a known time."  
unless start_index

   # Count down through the time prompts. I bet inject could do this  
too :)
   start_index.downto(0) do |i|
     this_time = times[i]
     next_time = i>0 ? times[i-1] : 0
     delay_to_next = this_time - next_time
     message = counts[this_time]
     say message
     wait delay_to_next
   end
end

def say(to_say)
   system("say \"#{to_say}\"")
end

def wait(s)
   @wait_until ||= Time.now
   @wait_until += s
   while((w = @wait_until - Time.new) > 0)
     sleep w
   end
end

# For testing it's really helpful to redefine the above to...
def say(m); puts m; end
def wait(s); puts "Waiting for #{s} seconds."; end

# Code to deal with just week 3!
def week_3
   wait(0)
   say "Start your first short run."
   count_down(90, 'running')
   say "Stop running now. You have 1 long run and two short ones left."
   count_down(90, 'walking')

   say "Start the first long run now."
   count_down(3*60, 'running')
   say "Stop running now. You have a short run and a long run left."
   count_down(3*60, 'walking')

   say "Start your second short run."
   count_down(90, 'running')
   say "Stop running. You have 1 more long run left."
   count_down(90, 'walking')

   say "Start your last run now."
   count_down(3*60, 'running')
   say "Stop running. After this walk, you will have finished."
   count_down(3*60, 'walking')

   say "Great! You've finished for today."
end

# Call week 3's code.
week_3


    Reply to author    Forward  
You must Sign in before you can post messages.
To post a message you must first join this group.
Please update your nickname on the subscription settings page before posting.
You do not have the permission required to post.
Adam Shelly  
View profile  
 More options Jun 13 2006, 9:41 pm
From: "Adam Shelly" <adam.she...@gmail.com>
Date: Wed, 14 Jun 2006 10:41:54 +0900
Local: Tues, Jun 13 2006 9:41 pm
Subject: Re: [QUIZ part solution] Running Coach (#82)
Here's my attempt:
At the moment it only prints text to the screen.  I have an
interesting idea about generating a wav file directly, but I'm going
to have trouble getting that done before the summary deadline.  The
script gets the exercise plan from a text file with a simple format.
It doesn't do any error checking of arguments or file format.

Usage:  ruby -d coach.rb weekly_plan.txt
Use the -d switch unless you want to wait 20 minutes for all the output.

-----week3.txt-----
run 90
walk 90
run 180
walk 180
run 90
walk 90
run 180
walk 180
----------

-----coach.rb-----
$CheerThreshold = 6   #decrease to get more random encouragement
$LongThreshold = 120  #minimum time to be considered a "long" run

class Phase
  attr_reader :action, :seconds
  def initialize action, time
    @action = action.downcase
    @seconds = time.to_i
  end
end

class Coach
  def initialize filename
    File.open(filename) {|f|
      @rawdata = f.read.split("\n")
    }
    @duration = 0
    @runs = @longs = @walks = 0
    @encouragometer = 0
    @step = [30,15,10,5,5]
  end

  def coach
    build_timeline
    say summarize(2)
    say start_prompt
    @time = Time.now
    @target_time = @time
    while (phase = @phases.shift)
      update_summary phase
      narrate_phase phase
      if @phases.size > 0
        say transition(@phases[0].action)
        say summarize(rand(2))
      end
    end
    say finish_line
  end

  def narrate_phase phase
    say what_to_do_for(phase)
    @target_time += phase.seconds
    delta = (@target_time - Time.now).to_i
    stepidx = 0
    while (delta > 0)
      stepidx+=1 if delta < @step[stepidx]+1
      wait_time = delta%@step[stepidx]
      wait_time += @step[stepidx] if wait_time <= 0
      wait(wait_time)
      delta = (@target_time - Time.now).to_i
      encourage_maybe
      say whats_left(phase.action,delta) if delta > 0
    end
  end

  def update_summary phase
    @duration -= phase.seconds
    @runs -= 1 if phase.action == 'run'
    @longs -= 1 if phase.action == 'run' and phase.seconds >= $LongThreshold
    @walks -= 1 if phase.action == 'walk'
  end

  def build_timeline
    @phases = @rawdata.map {|command|
      p = Phase.new(*command.split)
      @duration += p.seconds
      @runs += 1 if p.action == 'run'
      @longs += 1 if p.action == 'run' and p.seconds >= $LongThreshold
      @walks += 1 if p.action == 'walk'
      p
    }
  end

  def say s
    puts s
    #todo: replace with speech
  end
  def wait n
    if $DEBUG
      puts "...waiting #{n} seconds..."
      @target_time -= n
    else
      $stdout.flush
      sleep(n)
    end
  end

  def encourage_maybe
    @encouragometer += rand(3)
    if (@encouragometer > $CheerThreshold)
      say cheer
      @encouragometer = 0
    end
  end

  def timesay secs
    secs = secs.to_i
    s = ""
    if secs > 60
      min = secs/60
      secs -= min*60
      s += "#{min} minute"
      s += 's' if min > 1
      s += ' and ' if secs > 0
    end
    if secs > 0
      s += "#{secs} second"
      s += 's' if secs > 1
    end
    s
  end

  # All the phrases should be below this line, not mixed up in the logic
  def what_to_do_for phase
    s = "#{phase.action} for #{timesay(phase.seconds)} \n"
    s += "You are almost done" if @phases.size == 1
    s
  end
  def whats_left act, time
    timestr = timesay(time)
    s = [
      "You have #{timestr} more to #{act}",
      "#{act} for #{timestr} more",
      "only #{timestr} left of #{act}ing",
      "You have #{timestr} more to #{act}",
      "#{timestr} left in this phase",
      "There are #{timestr} until the next activity"
    ]
    s[rand(s.size)]
  end
  def start_prompt
    "are you ready, go!"
  end
  def transition next_act
    s = ["OK, you can #{next_act} now",
         "get ready to #{next_act}"]
    s[rand(s.size)]
  end
  def finish_line
    "you are done, rest now."
  end
  def cheer
    c = ["Keep it up!", "Way to go!", "Good Job!"]
    c[rand(c.size)]
  end
  def summarize degree
    shorts = @runs - @longs
    s = "you have #{timesay(@duration)}"
    if degree > 0
      if degree > 1
        s+= " for "
      else
        s+= " to go and there are " if @runs > 0
      end
      s+="#{@longs} long run" if @longs > 0
      s+="s" if @longs > 1
      s+=" and" if @longs > 0 and shorts > 0 and degree <=1
      s+=" #{shorts} short run" if shorts > 0
      s+="s" if shorts > 1
      if degree >1
        s+=" and" if @longs+shorts > 0
        s+=" #{@walks} walk" if @walks > 0
        s+="s" if @walks > 1
      else
        s+=" left"
      end
    else
      s+= " left to exercise"
    end
    s
  end
end

Coach.new(ARGV[0]||"week3.txt").coach
----------

-Adam


    Reply to author    Forward  
You must Sign in before you can post messages.
To post a message you must first join this group.
Please update your nickname on the subscription settings page before posting.
You do not have the permission required to post.
benj...@fysh.org  
View profile  
 More options Jun 14 2006, 4:54 am
From: benj...@fysh.org
Date: Wed, 14 Jun 2006 17:54:56 +0900
Local: Wed, Jun 14 2006 4:54 am
Subject: Re: [QUIZ part solution] Running Coach (#82)

> Here's my attempt:
> At the moment it only prints text to the screen.  I have an
> interesting idea about generating a wav file directly, but I'm going
> to have trouble getting that done before the summary deadline.  The
> script gets the exercise plan from a text file with a simple format.
> It doesn't do any error checking of arguments or file format.

Dude - that's extremely cool :) It's a hell of a lot more comprehensive
than my attempt. Good work :)

I really like the "encouragometer" member state, your use of
randomisation, and the way you break up the phase in to steps - it seems
a much less rigid solution than my own.

You have got it giving lots of propper phrases too, by just getting on
with the job and using lots of branching :) I got far too hung up with
trying to unify it all, and didn't really get anywhere at all! Perhaps
from the point you've got to now, it would be possible to factor out
some common ideas? Perhaps some kind of automatic pluralisation would be
useful in "summarize"? Perhaps it's not worth it though?!

I was thinking that catagorisation of a phase in to "long" or "short"
could also go in to the definition file? That would cope with weeks
where there are no longer or shorter runs, and would easily allow
tailoring to each week's requirements. It would be a phase description
to go along with the phase's activity.

Maybe I'll have to revisit mine again now :)

Cheers,
  Benjohn


    Reply to author    Forward  
You must Sign in before you can post messages.
To post a message you must first join this group.
Please update your nickname on the subscription settings page before posting.
You do not have the permission required to post.
Leslie Viljoen  
View profile  
 More options Jun 14 2006, 10:06 am
From: "Leslie Viljoen" <leslievilj...@gmail.com>
Date: Wed, 14 Jun 2006 23:06:11 +0900
Local: Wed, Jun 14 2006 10:06 am
Subject: Re: [QUIZ part solution] Running Coach (#82)
On 6/14/06, Adam Shelly <adam.she...@gmail.com> wrote:

> Here's my attempt:

Here's a quick version that is closer to having speech synth. It's not
a real synthesiser, but if you can provide the corresponding ogg files
it can look for certain phrases and play them. The result should sound
a bit better than a real synthesiser since the sections will be spoken
fairly naturally. Only 53 files to record!

Well maybe the strings you use could be made a bit more similar to
shorten the list a bit ;-)

Les

------------------------

$CheerThreshold = 6   #decrease to get more random encouragement
$LongThreshold = 120  #minimum time to be considered a "long" run

class SpeechSynth
        def initialize
                @known = ["minutes left in this phase",
                                                                                "you are done, rest now.",
                                                                                "until the next activity",
                                                                                "OK, you can walk now",
                                                                                "seconds more to walk",
                                                                                "seconds more to run",
                                                                                "to go and there are",
                                                                                "OK, you can run now",
                                                                                "You are almost done",
                                                                                "are you ready, go!",
                                                                                "get ready to walk",
                                                                                "get ready to run",
                                                                                "short run left",
                                                                                "in this phase",
                                                                                "to exercise",
                                                                                "Keep it up!",
                                                                                "to exersize",
                                                                                "Way to go!",
                                                                                "of walking",
                                                                                "short runs",
                                                                                "minute and",
                                                                                "There are",
                                                                                "Good job!",
                                                                                "long runs",
                                                                                "of runing",
                                                                                "You have",
                                                                                "walk for",
                                                                                "long run",
                                                                                "minutes",
                                                                                "seconds",
                                                                                "to walk",
                                                                                "run for",
                                                                                "to run",
                                                                                "walks",
                                                                                "left",
                                                                                "only",
                                                                                "more",
                                                                                "for",
                                                                                "and",
                                                                                "60",
                                                                                "30",
                                                                                "15",
                                                                                "18",
                                                                                "55",
                                                                                "7",
                                                                                "6",
                                                                                "5",
                                                                                "9",
                                                                                "3",
                                                                                "2",
                                                                                "1",
                                                                                "8",
                                                                                "4"]
        end

        def playFile(fileName)
                puts "PLAY: '" +fileName + "'"
                #on linux this can be:
                #`play #{filename}`
        end

        def known(searchPhrase)
                @known.each do |phrase|
                        return $~[1] if searchPhrase =~ /^(#{phrase}).*/i
                end
                puts "UNKNOWN PHRASE: #{searchPhrase}"
                return nil
        end

        def say(sentence)
                while sentence.length > 0    
                        knownPhrase = known(sentence)
                        if knownPhrase
                                playFile(knownPhrase + ".ogg")
                                sentence = sentence[knownPhrase.length+1..-1]
                                sentence = "" if !sentence
                        else
                                sentence = ""
                        end
                        sentence.strip!
                end
        end
end

class Phase
 attr_reader :action, :seconds
 def initialize action, time
   @action = action.downcase
   @seconds = time.to_i
 end
end

class Coach
 def initialize filename
         @synth = SpeechSynth.new
   File.open(filename) {|f|
     @rawdata = f.read.split("\n")
   }
   @duration = 0
   @runs = @longs = @walks = 0
   @encouragometer = 0
   @step = [30,15,10,5,5]
 end

 def coach
   build_timeline
   say summarize(2)
   say start_prompt
   @time = Time.now
   @target_time = @time
   while (phase = @phases.shift)
     update_summary phase
     narrate_phase phase
     if @phases.size > 0
       say transition(@phases[0].action)
       say summarize(rand(2))
     end
   end
   say finish_line
 end

 def narrate_phase phase
   say what_to_do_for(phase)
   @target_time += phase.seconds
   delta = (@target_time - Time.now).to_i
   stepidx = 0
   while (delta > 0)
     stepidx+=1 if delta < @step[stepidx]+1
     wait_time = delta%@step[stepidx]
     wait_time += @step[stepidx] if wait_time <= 0
     wait(wait_time)
     delta = (@target_time - Time.now).to_i
     encourage_maybe
     say whats_left(phase.action,delta) if delta > 0
   end
 end

 def update_summary phase
   @duration -= phase.seconds
   @runs -= 1 if phase.action == 'run'
   @longs -= 1 if phase.action == 'run' and phase.seconds >= $LongThreshold
   @walks -= 1 if phase.action == 'walk'
 end

 def build_timeline
   @phases = @rawdata.map {|command|
     p = Phase.new(*command.split)
     @duration += p.seconds
     @runs += 1 if p.action == 'run'
     @longs += 1 if p.action == 'run' and p.seconds >= $LongThreshold
     @walks += 1 if p.action == 'walk'
     p
   }
 end

 def say s
   @synth.say(s)
 end

 def wait n
   if $DEBUG
     puts "...waiting #{n} seconds..."
     @target_time -= n
   else
     $stdout.flush
     sleep(n)
   end
 end

 def encourage_maybe
   @encouragometer += rand(3)
   if (@encouragometer > $CheerThreshold)
     say cheer
     @encouragometer = 0
   end
 end

 def timesay secs
   secs = secs.to_i
   s = ""
   if secs > 60
     min = secs/60
     secs -= min*60
     s += "#{min} minute"
     s += 's' if min > 1
     s += ' and ' if secs > 0
   end
   if secs > 0
     s += "#{secs} second"
     s += 's' if secs > 1
   end
   s
 end

 # All the phrases should be below this line, not mixed up in the logic
 def what_to_do_for phase
   s = "#{phase.action} for #{timesay(phase.seconds)} \n"
   #s += "You are almost done" if @phases.size == 1
   s
 end
 def whats_left act, time
   timestr = timesay(time)
   s = [
     "You have #{timestr} more to #{act}",
     "#{act} for #{timestr} more",
     "only #{timestr} left of #{act}ing",
     "You have #{timestr} more to #{act}",
     "#{timestr} left in this phase",
     "There are #{timestr} until the next activity"
   ]
   s[rand(s.size)]
 end
 def start_prompt
   "are you ready, go!"
 end
 def transition next_act
   s = ["OK, you can #{next_act} now",
        "get ready to #{next_act}"]
   s[rand(s.size)]
 end
 def finish_line
   "you are done, rest now."
 end
 def cheer
   c = ["Keep it up!", "Way to go!", "Good Job!"]
   c[rand(c.size)]
 end
 def summarize degree
   shorts = @runs - @longs
   s = "you have #{timesay(@duration)}"
   if degree > 0
     if degree > 1
       s+= " for "
     else
       s+= " to go and there are " if @runs > 0
     end
     s+="#{@longs} long run" if @longs > 0
     s+="s" if @longs > 1
     s+=" and" if @longs > 0 and shorts > 0 and degree <=1
     s+=" #{shorts} short run" if shorts > 0
     s+="s" if shorts > 1
     if degree >1
       s+=" and" if @longs+shorts > 0
       s+=" #{@walks} walk" if @walks > 0
       s+="s" if @walks > 1
     else
       s+=" left"
     end
   else
     s+= " left to exercise"
   end
   s
 end
end

Coach.new(ARGV[0]||"week3.txt").coach


    Reply to author    Forward  
You must Sign in before you can post messages.
To post a message you must first join this group.
Please update your nickname on the subscription settings page before posting.
You do not have the permission required to post.
Ruby Quiz  
View profile  
 More options Jun 15 2006, 9:47 am
From: Ruby Quiz <ja...@grayproductions.net>
Date: Thu, 15 Jun 2006 22:47:37 +0900
Local: Thurs, Jun 15 2006 9:47 am
Subject: [SUMMARY] Running Coach (#82)
This quiz turns out to be a little bit of work, if you want to get some decent
feedback to the user.  Adam Shelly hammered out a reasonably complete solution
though, so let's have a look at it:

        $CheerThreshold = 6   #decrease to get more random encouragement
        $LongThreshold = 120  #minimum time to be considered a "long" run

        class Phase
         attr_reader :action, :seconds
         def initialize action, time
           @action = action.downcase
           @seconds = time.to_i
         end
        end

        # ...

We can see some setup work here for variables that allow users to tweak the
output.  We also have the trivial Phase class definition, which is just a data
class for linking actions and times.

Here's the main event loop:

        class Coach
         def initialize filename
           File.open(filename) {|f|
             @rawdata = f.read.split("\n")
           }
           @duration = 0
           @runs = @longs = @walks = 0
           @encouragometer = 0
           @step = [30,15,10,5,5]
         end

         def coach
           build_timeline
           say summarize(2)
           say start_prompt
           @time = Time.now
           @target_time = @time
           while (phase = @phases.shift)
             update_summary phase
             narrate_phase phase
             if @phases.size > 0
               say transition(@phases[0].action)
               say summarize(rand(2))
             end
           end
           say finish_line
         end

         # ...

There's nothing too interesting about initialize() which is just assigning
defaults to the instance variables.  Have a look at the coach() method though.
This is the process the application runs through, and I really like how well it
reads.  It builds up the timeline of events, hits user with a summary and
starting prompt, then launches into Phase processing.  Each Phase is narrated to
the user, and then the code transitions naturally to the next Phase.  Finally
the code sends the finish line message to indicate a successful workout.

Let's see what narrating a phase involves:

         # ...

         def narrate_phase phase
           say what_to_do_for(phase)
           @target_time += phase.seconds
           delta = (@target_time - Time.now).to_i
           stepidx = 0
           while (delta > 0)
             stepidx+=1 if delta < @step[stepidx]+1
             wait_time = delta % @step[stepidx]
             wait_time += @step[stepidx] if wait_time <= 0
             wait(wait_time)
             delta = (@target_time - Time.now).to_i
             encourage_maybe
             say whats_left(phase.action,delta) if delta > 0
           end
         end

         # ...

Obviously, this method is mostly about time management.  It breaks a Phase down
into smaller chunks, so that it can provide encouragement frequently and inform
the user of what is left to be done.

Note the clever output messages here again that read so naturally:
what_to_do_for(), encourage_maybe(), and whats_left().

         # ...

         def update_summary phase
           @duration -= phase.seconds
           @runs -= 1 if phase.action == 'run'
           @longs -= 1 if phase.action == 'run' and phase.seconds >= $LongThreshold
           @walks -= 1 if phase.action == 'walk'
         end

         def build_timeline
           @phases = @rawdata.map {|command|
             p = Phase.new(*command.split)
             @duration += p.seconds
             @runs += 1 if p.action == 'run'
             @longs += 1 if p.action == 'run' and p.seconds >= $LongThreshold
             @walks += 1 if p.action == 'walk'
             p
           }
         end

         # ...

These two methods are quite similar save that one adds and the other subtracts.
First, build_timeline() constructs the Phase objects from the import file.  As
it goes through, it counts things like the total number of walks and runs a
person needs to complete.  Then, update_summary() runs inside each Phase of the
event loop ticking off the walks and runs the user has completed.

Here's the say() method that would eventually need to be replaced with speech
programming:

         # ...

         def say s
           puts s
           #todo: replace with speech
         end

         # ...

Now, take a look at this:

         # ...

         def wait n
                 if $DEBUG
                   puts "...waiting #{n} seconds..."
                   @target_time -= n
                 else
                   $stdout.flush
                   sleep(n)
                 end
         end

         # ...

This is obviously the delay method and it mainly just calls sleep().  However, I
like how it can be set to just explain what the pause would have been, in $DEBUG
mode.  That makes testing the application much more pleasant.

Two more helper methods:

         # ...

         def encourage_maybe
           @encouragometer += rand(3)
           if (@encouragometer > $CheerThreshold)
             say cheer
             @encouragometer = 0
           end
         end

         def timesay secs
           secs = secs.to_i
           s = ""
           if secs > 60
             min = secs/60
             secs -= min*60
             s += "#{min} minute"
             s += 's' if min > 1
             s += ' and ' if secs > 0
           end
           if secs > 0
             s += "#{secs} second"
             s += 's' if secs > 1
           end
           s
         end

         # ...

There's the definition for the encourage_maybe() call I pointed out earlier.  It
just randomly decides if a cheer should be emitted.

The other method, timesay(), is a helper like we are use to in Rails.  It just
humanizes the output of some number of seconds by breaking it into minutes and
seconds.

Next the code has several output methods, of which I'll just show a couple:

         # ...

         # All the phrases should be below this line, not mixed up in the logic
         def what_to_do_for phase
           s = "#{phase.action} for #{timesay(phase.seconds)} \n"
           s += "You are almost done" if @phases.size == 1
           s
         end
         def whats_left act, time
           timestr = timesay(time)
           s = [
             "You have #{timestr} more to #{act}",
             "#{act} for #{timestr} more",
             "only #{timestr} left of #{act}ing",
             "You have #{timestr} more to #{act}",
             "#{timestr} left in this phase",
             "There are #{timestr} until the next activity"
           ]
           s[rand(s.size)]
         end

         # ... several more output routines not shown ...
        end

        # ...

You can see that these methods just use simple conditional logic or random picks
to vary the program's output.  With several of these methods, the end result is
a fairly good mix of prompts for the user.

Here's the last line that turns it into a solution:

        # ...

        Coach.new(ARGV[0]||"week3.txt").coach

My thanks to those who stole the time from their busy running schedules to code
up a solution.  These scripts should have us all in shape by RubyConf!

Tomorrow, we will try an extremely common computerism, but see if we can handle
it a little better than the usual treatment...


    Reply to author    Forward  
You must Sign in before you can post messages.
To post a message you must first join this group.
Please update your nickname on the subscription settings page before posting.
You do not have the permission required to post.
Adam Shelly  
View profile  
 More options Jun 15 2006, 7:46 pm
From: "Adam Shelly" <adam.she...@gmail.com>
Date: Fri, 16 Jun 2006 08:46:45 +0900
Local: Thurs, Jun 15 2006 7:46 pm
Subject: Re: [QUIZ part solution] Running Coach (#82)
On 6/14/06, Leslie Viljoen <leslievilj...@gmail.com> wrote:

> Here's a quick version that is closer to having speech synth. It's not
> a real synthesiser, but if you can provide the corresponding ogg files
> it can look for certain phrases and play them. The result should sound
> a bit better than a real synthesiser since the sections will be spoken
> fairly naturally. Only 53 files to record!

I know this part is after the summary  (Thanks for the nice writeup) ,
but I wanted to share.

I had an idea similar to Leslie's, but I wanted to actually write out
an audio file, instead of sending the narration to the speakers.  The
solution has 2 parts.

class WaveRead extracts all the information from a wave file.  I put
it together in under 2 hours last night.  It was so much easier to
write than the one in I did C a few years ago, and I'm really pleased
the result.  It's clean and extensible.  I already have an idea for
making it trivial to add the other chunk definitions.

class WaveSpeaker writes a new wave file with everything it was told
to say.  It does this by using a wave file feature called cues, which
are a way of marking a point in the file and giving it a name.   I
created a wave file with several words, and a cue marking each one.
(see more about this below.)  WaveSpeaker parses this file, and starts
writing a new output file with the same format.  Then, when #say is
called, it looks for each word in the list of cues, and if found,
pastes the appropriate part of the source wave into the output file.
It inserts silence for each #wait, compensating for the length of the
previous sentences.  At the end it just fixes up the filesize data,
and closes the file.  All you need to do is convert the file to MP3
and transfer to your iPod.

------wavespeaker.rb------
require 'Ostruct'

class RiffRead
  def initialize io
    @io = io
    raise "Not a RIFF file" if io.read(4) != "RIFF"
    @size = get_long
    @type = get_word
  end
  def parse
    chunks = []
    chk = get_chunk
    while chk
      chunks << chk
      chk = get_chunk
    end
    chunks
  end
  def self.get_long io
    io.read(4).unpack('V')[0]
  end
  def self.get_short io
    io.read(2).unpack('v')[0]
  end
  def self.get_word io
    io.read(4)
  end

private
  def get_chunk
    tag = get_word
    return nil if !tag
    if tag == 'LIST'
      handle_list
    else
      size = get_long
      size+=1 if size%2 != 0
      data = handle_tag(tag,size)
      data ||= @io.read(size)
      [tag, size, data]
    end
  end
  def handle_tag tag,size
    funcname = "parse_"+tag.strip
    if methods.include? funcname
      return self.send(funcname, size)
    end
  end
  def handle_list
    listsize = get_long
    @listtype = get_word
    ['LIST',listsize,@listtype]
  end
  def get_long
    self.class::get_long @io
  end
  def get_short
    self.class::get_short @io
  end
  def get_word
    self.class::get_word @io
  end
end

def make_cue io
  cue = OpenStruct.new
  cue.name = RiffRead::get_long io
  cue.position = RiffRead::get_long io
  cue.chkname = RiffRead::get_word io
  cue.chkstart = RiffRead::get_long io
  cue.blockkstart = RiffRead::get_long io
  cue.samplestart = RiffRead::get_long io
  cue
end

class WaveRead < RiffRead
  attr_reader :cues,:labels,:format, :data
  def initialize io
    super
    raise "Not a Wave File" if @type != 'WAVE'
  end
  def parse_fmt size
    @format = OpenStruct.new
    @format.data = @io.read(size)
    @format.size = size
    @format.tag = format.data[0,2].unpack('v')[0]
    @format.channels = format.data[2,2].unpack('v')[0]
    @format.samples_per_sec = format.data[4,4].unpack('V')[0]
    @format.bytes_per_sec = format.data[8,4].unpack('V')[0]
    @format.blockAlign = format.data[12,2].unpack('v')[0]
    @format
  end
  def parse_data size
    @data = @io.read(size)
  end
  def parse_cue size
    @cues = []
    numcues = get_long
    numcues.times  do
      @cues << make_cue(@io)
    end
    @cues
  end
  def parse_labl size
    id = get_long
    string = @io.read(size-4)
    @labels||=[]
    @labels << [id,string.strip]
    @labels.last
  end
  def parse_note size
    id = get_long
    string = @io.read(size-4)
    @notes||=[]
    @notes << [id,string.strip]
    @notes.last
  end
end

class WaveSpeaker
  def initialize filename
    File.open(filename, "rb") do |f|
      @data = WaveRead.new(f)
      @data.parse
    end
    @elapsed = 0
  end
  def begin outfile
    @out = File.open(outfile, "wb")
    @out.write('RIFF')
    @filesize_marker = @out.pos
    @out.write [0].pack('V')
    @written = @out.write('WAVEfmt ')
    @written+= @out.write [@data.format.size].pack('V')
    @written+= @out.write @data.format.data
    @written+= @out.write('data')
    @datasize_marker = @out.pos
    @written+= @out.write [0].pack('V')
  end
  def say string
    fixup(string).split.each do |str|
      str = fixup(str)
      if str == 'COMMA'
        wait 0.2
      else
        cue_id = nil
        @data.labels.each_with_index{|label,i|
          if label[1].downcase == str.downcase
            cue_id = i
            break
          end
        }
        if cue_id
          #p "saying #{str}"
          start = @data.cues[cue_id].samplestart*2
          endpt = @data.cues[cue_id+1].samplestart*2
          endpt+=1 if (endpt-start)%2 != 0
          @written+= @out.write(@data.data[start...endpt])
          @elapsed += (endpt-start).to_f / @data.format.bytes_per_sec
        else
          p "CAN'T FIND <#{str}>"
        end
      end
    end
  end
  def wait seconds
    a = "\0"
    delay = (seconds - @elapsed)
    p delay
    if delay > 0
      bytes = (delay * @data.format.bytes_per_sec).to_i
      p "wait #{bytes}"
      bytes+=1 if (bytes%2 != 0)
      silence = a*bytes
      @written+= @out.write silence
      @elapsed = 0
    else
      @elapsed -= seconds
    end
  end
  def fixup str
    #remove punctuation, mark pauses
    str.gsub!(/,/," COMMA ")
    str.gsub!(/[^\w\s]/,"")
    str
  end
  def quit
    @out.seek @filesize_marker
    @out.write [@written].pack('V')
    @out.seek @datasize_marker
    @out.write [@written-@datasize_marker+4].pack('V')
    @out.close
    p @written
  end
end

if __FILE__ == $0
  wr = WaveSpeaker.new("coach.wav")
  wr.begin("todays_run.wav")
  wr.say 'run 60 seconds'
  wr.wait 1
  wr.say 'walk 15 minutes'
  wr.quit
end
-----end-----

To get to work with my solution, just add the following lines:
in Coach#initialize, add
   @speaker = WaveSpeaker.new "coach.wav"
   @speaker.begin "current_workout.wav"

at the end of Coach#coach add
   @speaker.quit

and replace these two functions:
 def say s
   @speaker.say s
 end
 def wait n
   @speaker.wait n
   @target_time -= n
 end

To get the source file, I generated a wave file with 53 words from my
coaching script using a synth (couldn't find a microphone), and used
my wave editor's auto cue feature to insert numbered cues in all the
gaps between words.  After running simple script to replace the
numbers with the words, I have a complete solution that produces a 20
minute long wav file of a robot coach.  It would probably be better if
you used a real voice.  If anyone is actually interested in this, I
can give you more details on the wave file creation.

-Adam


    Reply to author    Forward  
You must Sign in before you can post messages.
To post a message you must first join this group.
Please update your nickname on the subscription settings page before posting.
You do not have the permission required to post.
Leslie Viljoen  
View profile  
 More options Jun 16 2006, 10:21 am
From: "Leslie Viljoen" <leslievilj...@gmail.com>
Date: Fri, 16 Jun 2006 23:21:20 +0900
Local: Fri, Jun 16 2006 10:21 am
Subject: Re: [QUIZ part solution] Running Coach (#82)
On 6/16/06, Adam Shelly <adam.she...@gmail.com> wrote:

> On 6/14/06, Leslie Viljoen <leslievilj...@gmail.com> wrote:

> > Here's a quick version that is closer to having speech synth. It's not
> > a real synthesiser, but if you can provide the corresponding ogg files
> > it can look for certain phrases and play them. The result should sound
> > a bit better than a real synthesiser since the sections will be spoken
> > fairly naturally. Only 53 files to record!

> I know this part is after the summary  (Thanks for the nice writeup) ,
> but I wanted to share.

> I had an idea similar to Leslie's, but I wanted to actually write out
> an audio file, instead of sending the narration to the speakers.  The
> solution has 2 parts.

What we need now is your master wav file! This software is cool, let's use it!
(look mummy, there goes a fit geek!)

Les


    Reply to author    Forward  
You must Sign in before you can post messages.
To post a message you must first join this group.
Please update your nickname on the subscription settings page before posting.
You do not have the permission required to post.
Adam Shelly  
View profile  
 More options Jun 17 2006, 1:54 am
From: "Adam Shelly" <adam.she...@gmail.com>
Date: Sat, 17 Jun 2006 14:54:16 +0900
Local: Sat, Jun 17 2006 1:54 am
Subject: Re: [QUIZ part solution] Running Coach (#82)
On 6/16/06, Leslie Viljoen <leslievilj...@gmail.com> wrote:

> What we need now is your master wav file! This software is cool, let's use it!
> (look mummy, there goes a fit geek!)

File is at:
http://rubyurl.com/i6x

There are at least 3 problems with it
- It's missing a bunch of numbers, so you often get useless messages
like: "you have . left to run" .  Look for the "Can't Find ..."
messages in the output to see what other numbers you need.
- the pauses between words are too long.
- The voice is really annoying.

If you are really interested, I'd record your own voice, or someone
who motivates you.
The process to mark it up is actually simple.
Make sure there is a bit of silence between each word in your recording.
Then use the AutoCue feature of your wave editor to insert cues at the
end of each silence. - I used GoldWave, which made it really easy.
Delete any false cues in the middle of words, and add any missing
ones. - I only found one extra cue.
Then run this script, making sure that the list of words matches what
you recorded.

-----cuefixer.rb-----
require 'wavespeaker.rb'
Words = %w( run walk for second seconds minute minutes you are almost done
have more to only left this phase there until the next activity
ready go ok can now get rest keep it up way good job and long short
runs walks exercise 1 2 3 4 5 6 10 15 30 60 in)

class CueFixer < RiffRead
  def initialize io
    super
    raise "Not a Wave File" if @type != 'WAVE'
    @out = File.open("recued_coach.wav", "wb")
    @out.write('RIFF')
    @filesize_marker = @out.pos
    @out.write [0].pack('V')
    @written = @out.write('WAVE')
  end
  def close
    @out.seek @filesize_marker
    @out.write [@written].pack('V')
    @out.seek @listsize_marker
    @out.write [@written-@liststart].pack('V')
    @out.close
    p @written
  end
  def handle_tag tag,size
    @written += @out.write tag
    chunksize_marker = @out.pos
    @written += @out.write [size].pack('V')
    funcname = "parse_"+tag.strip
    if methods.include? funcname
      size = self.send(funcname, size)
      here = @out.pos
      @out.seek chunksize_marker
      @out.write [size].pack('V')
      @out.seek here
    else
      data = @io.read(size)
      @written += @out.write data
      data
    end
  end
  def handle_list
    @written += @out.write 'LIST'
    @listsize_marker = @io.pos
    @liststart = @written+4
    @written += @out.write @io.read(8)
  end
  def parse_labl size
    id = get_long
    osize = @out.write [id].pack('V')
    @written+=osize
    string = @io.read(size-4)
    newcue = Words[id]
    p newcue
    @written += @out.write(newcue)
    @written += @out.write "\0"
    osize += newcue.size + 1
    if osize%2 != 0
      @written += @out.write "\0"
    end
    osize
  end
end

w = CueFixer.new(File.new("fullcoach.wav","rb"))
w.parse
w.close
-----end-----

Good Luck,
-Adam


    Reply to author    Forward  
You must Sign in before you can post messages.
To post a message you must first join this group.
Please update your nickname on the subscription settings page before posting.
You do not have the permission required to post.
End of messages
« Back to Discussions « Newer topic     Older topic »

Create a group - Google Groups - Google Home - Terms of Service - Privacy Policy
©2009 Google