Archive for Uncategorized

nlp.pl

% Natural Language Parser for Scribbler II Robot
% by Justin Mangue, 2013
%
% Uses DCG grammar parsing to parse natural language input into Python code, to be executed on a Scribbler II robot.
%
% References used:
% * "The Art of Prolog", Sterling & Shapiro, 1986.

% ParseToCode is the main routine.  Takes a sentence and converts it to a list of executable python code.
% Call with parseToCode([sentence,as,a,list],Status,Code).
parseToCode(Sentence, Status, CodeListOut) :-
	compound_sentence(ParseList, Sentence, []),
	Status = valid,
	generate_code_list(ParseList, [], CodeListOut), 
	!.	
	
parseToCode(Sentence, Status, CodeOut) :-
	+ compound_sentence(_, Sentence, []),
	Status = invalid_sentence,
	CodeOut = 'null',
	!.
	
%%% GRAMMAR DEFINITIONS %%%
	
% Compound Sentence is the top level.  Consists of an optional address statement followed by one or more simple sentences joined by a connective.
compound_sentence(Compound) -->
	optionally_address(robot),
	simple_sentence(Command),
	connective(and),
	compound_sentence(Sentence),
	{ append([Command], [Sentence], Compound_NotFlat) },
	{ flatten2(Compound_NotFlat, Compound) }.

compound_sentence(Command) -->
	optionally_address(robot),
	simple_sentence(Command).
	
% A simple sentence is a command phrase, possibly followed by an optional repetition clause.
simple_sentence([loop(C,T)]) --> 
	command_phrase(C), 
	(to_number(T), [times] ; to_number_eng(T)).
	
simple_sentence([Simple]) --> 
	command_phrase(Simple).
  
% Command phrases are the basic command-level structure.  Most consist of an action and some sort of argument.
command_phrase(move(D,S)) --> 
  action(go), 
  direction(D),
  optional(for),
  unit_to_seconds(S).
  
command_phrase(move(D,S)) --> 
  action(go), 
  unit_to_seconds(S),
  direction(D).
  
command_phrase(move(forward,S)) --> 
  action(go), 
  unit_to_seconds(S).
  
command_phrase(move(D,3)) --> 
  action(go), 
  direction(D).
  
command_phrase(turn(D,S)) --> 
  action(turn), 
  optional(to),
  (optional(the) ; optional(your)),
  direction(D), 
  unit_to_seconds(S).

command_phrase(turn(D,S)) --> 
  action(turn), 
  optional(to),
  (optional(the) ; optional(your)),
  direction(D),
  { S is 90*(3.25 / 360) }.  % 90 degrees by default
  
command_phrase(turn(right,D)) -->
  action(turn),
  direction(around),
  { D is 180 * (3.25 / 360) }.  % 180 degree turn
  
command_phrase(turn(Dir,D)) -->
  action(spin),
  direction(around),
  optional(to),
  (optional(the) ; optional(your)),
  direction(Dir),
  { D is 3.25 }.  % 360 degree turn
  
command_phrase(turn(right,D)) -->
  action(spin),
  direction(around),
  { D is 3.25 }.  % 360 degree turn
	
command_phrase(pic(Mode)) -->
	action(take),
	adposition(a),
	photomode(Mode),
	object(picture).
	
command_phrase(pic(0)) -->  % b&w pic by default, for speed reasons
	action(take),
	adposition(a),
	object(picture).
	
command_phrase(wait(S)) -->
	action(wait),
	optional(for),
	unit_to_seconds(S).
	
command_phrase(wait(3.0)) -->
	action(wait).	
	
command_phrase(beep) -->
	action(beep).
	
command_phrase(moonwalk(S)) -->
	action(moonwalk),
	optional(for),
	unit_to_seconds(S).
	
command_phrase(moonwalk(4.0)) -->
	action(moonwalk).
	
command_phrase(move_until_wall(D)) -->
	command_phrase(move(D,_)),
	condition(until),
	[you],
	condition(encounter),
	object(wall).
	
% Vocabulary/synonym definitions
	
action(go) --> [go] ; [move] ; [drive] ; [roll] ; [scoot].
action(turn) --> [turn] ; [rotate] ; [swivel].
action(spin) --> [spin].
action(take) --> [take] ; [obtain] ; [get] ; [snap].
action(wait) --> [wait] ; [pause] ; [stop].
action(beep) --> [beep].
action(moonwalk) --> [moonwalk].

direction(forward) --> [forward] ; [forwards] ; [ahead] ; [up].
direction(backward) --> [backward] ; [backwards] ; [back].
direction(left) --> [left] ; [counter-clockwise] ; [counter],[clockwise].
direction(right) --> [right] ; [clockwise].
direction(around) --> [around] ; [in],adposition(a),[circle].

adposition(a) --> [a] ; [one].

condition(until) --> [until].
condition(encounter) --> [encounter] ; [reach] ; [sense] ; [hit].

photomode(0) --> [grayscale] ; [greyscale] ; [gray] ; [grey] ; [black],[and],[white] ; [black],[&],[white].
photomode(1) --> [color].

object(picture) --> [photo] ; [picture] ; [pic] ; [snapshot].
object(wall) --> [a],[wall] ; [the],[wall] ; [an],[obstacle] ; [something].

pronoun(robot) --> [robot] ; [scribbler].

connective(and) --> [and] ; [then] ; [comma] ; [and], [then] ; [comma], [then] ; [comma], [and], [then].

% Optional form of address
optionally_address(robot) -->
	(optional(robot) ; optional(scribbler)),
	optional(comma),
	optional(please).
	
% Allow a DCG word to be optional
optional(X) --> [X] ; [].

% Number parsing predicates
% English number recognition from 0.0 - 9999.9

to_number(N) --> num(N1), [point], digit(N2), { N is N1 + (0.1 * N2) }.
to_number(N) --> num(N).

to_number_eng(1) --> [once].
to_number_eng(2) --> [twice].
to_number_eng(3) --> [thrice].

num(0) --> [zero].
num(N) --> xxxx(N).
num(N) --> xxx(N).
num(N) --> xx(N).
num(N) --> digit(N).

num(N) --> [N], { number(N) }.

xxxx(N) --> digit(D), [thousand], xxx(N1), { N is D*1000+N1 }.
xxx(N) --> digit(D), [hundred], rest_xxx(N1), { N is D*100+N1 }.

rest_xxx(0) --> [].
rest_xxx(N) --> [and], xx(N).
rest_xxx(N) --> xx(N).

xx(N) --> digit(N).
xx(N) --> teen(N).
xx(N) --> tens(T), rest_xx(N1), { N is T+N1 }.

rest_xx(0) --> [].
rest_xx(N) --> digit(N).

digit(1) --> [one].
digit(2) --> [two].
digit(3) --> [three].
digit(4) --> [four].
digit(5) --> [five].
digit(6) --> [six].
digit(7) --> [seven].
digit(8) --> [eight].
digit(9) --> [nine].

teen(10) --> [ten].
teen(11) --> [eleven].
teen(12) --> [twelve].
teen(13) --> [thirteen].
teen(14) --> [fourteen].
teen(15) --> [fifteen].
teen(16) --> [sixteen].
teen(17) --> [seventeen].
teen(18) --> [eighteen].
teen(19) --> [nineteen].

tens(20) --> [twenty].
tens(30) --> [thirty].
tens(40) --> [forty].
tens(50) --> [fifty].
tens(60) --> [sixty].
tens(70) --> [seventy].
tens(80) --> [eighty].
tens(90) --> [ninety].

% Scribbler Unit conversions
% All of the Myro commands for Scribbler are expressed in seconds, so conversions of other unit types to seconds are required.

% Seconds -> Seconds
unit_to_seconds(S) -->
	to_number(S), { + S = 1 }, [seconds] ;
	to_number(S), { S = 1 }, [second].
	
% Milliseconds -> Seconds
unit_to_seconds(MS) -->
	to_number(S), { + S = 1 }, { MS is (S / 1000) }, [milliseconds] ;
	to_number(S), { S = 1 }, { MS is (S / 1000) },[millisecond].

% Feet -> Seconds
unit_to_seconds(S) -->
	to_number(Feet), { + Feet = 1 }, { S is Feet * 2.05 }, [feet] ; 
	to_number(Feet), { Feet = 1 }, { S is 2.05 }, [foot].

% Inches -> Seconds
unit_to_seconds(S) -->
	to_number(Inches), { + Inches = 1 }, { S is Inches * (2.05 / 12) }, [inches] ; 
	to_number(Inches), { Inches = 1 }, { S is (2.05 / 12) }, [inch].
	
% Degrees -> Seconds
unit_to_seconds(S) -->
	to_number(Degrees), { + Degrees = 1 }, { S is Degrees * (3.25 / 360) }, ([degrees] ; [º]) ;
	to_number(Degrees), { Degrees = 1 }, { S is (3.25 / 360) }, ([degree] ; [º]).
	
%%% CODE GENERATION %%%
% The top level structure is a code list, which is a list of one or more code fragments to be executed.
% This part is pretty messy, but is needed to handle passing instructions back to Python from Prolog.

generate_code_list([], CodeList, CodeList).
generate_code_list([H|T], CodeList, CodeListOut) :-
	generate_code(H, CodeLineOut),
	append(CodeList, CodeLineOut, NewCodeList),
	generate_code_list(T, NewCodeList, CodeListOut).

generate_code(move(forward,S), CodeOut) :-
	string_to_atom(Keyword, forward),
	CodeOut = [[Keyword,1.0,S]], !.

generate_code(move(backward,S), CodeOut) :-
	string_to_atom(Keyword, backward),
	CodeOut = [[Keyword,1.0,S]], !.

generate_code(move(left,S), CodeOut) :-
	string_to_atom(Keyword1, turnLeft),
	string_to_atom(Keyword2, forward),
	Ninety is 90*(3.25 / 360),
	CodeOut = [[Keyword1,1.0,Ninety],[Keyword2,1.0,S]], !.
	
generate_code(move(right,S), CodeOut) :-
	string_to_atom(Keyword1, turnRight),
	string_to_atom(Keyword2, forward),
	Ninety is 90*(3.25 / 360),
	CodeOut = [[Keyword1,1.0,Ninety],[Keyword2,1.0,S]], !.
	
generate_code(turn(left,S), CodeOut) :-
	string_to_atom(Keyword, turnLeft),
	CodeOut = [[Keyword,1.0,S]], !.
	
generate_code(turn(right,S), CodeOut) :-
	string_to_atom(Keyword, turnRight),
	CodeOut = [[Keyword,1.0,S]], !.
	
generate_code(wait(S), CodeOut) :-
	string_to_atom(Keyword, wait),
	CodeOut = [[Keyword,S]], !.
	
generate_code(pic(Mode), CodeOut) :-
	string_to_atom(Keyword, takePhoto),
	CodeOut = [[Keyword,Mode]], !.

generate_code(beep, CodeOut) :-
	string_to_atom(Keyword, beep),
	CodeOut = [[Keyword,0.4, 640]], !.
	
generate_code(loop(C,T), CodeOut) :-
	generate_code(C,Code),
	append_n_times(Code,[],T,CodeOut), !.
	
generate_code(move_until_wall(forward), CodeOut) :-
	string_to_atom(Keyword, move_until_wall),
	CodeOut = [[Keyword]], !.
	
generate_code(move_until_wall(left), CodeOut) :-
	string_to_atom(Keyword1, turnLeft),
	string_to_atom(Keyword2, move_until_wall),
	Ninety is 90*(3.25 / 360),
	CodeOut = [[Keyword1,1.0,Ninety],[Keyword2]], !.
	
generate_code(move_until_wall(right), CodeOut) :-
	string_to_atom(Keyword1, turnRight),
	string_to_atom(Keyword2, move_until_wall),
	Ninety is 90*(3.25 / 360),
	CodeOut = [[Keyword1,1.0,Ninety],[Keyword2]], !.

generate_code(move_until_wall(backward), CodeOut) :-
	string_to_atom(Keyword1, turnRight),
	string_to_atom(Keyword2, move_until_wall),
	string_to_atom(Keyword3, turnLeft),
	OneEighty is 180*(3.25 / 360),
	CodeOut = [[Keyword1,1.0,OneEighty],[Keyword2],[Keyword3,1.0,OneEighty]], !.

generate_code(moonwalk(S), CodeOut) :-
	string_to_atom(Keyword, moonwalk),
	CodeOut = [[Keyword,S]], !.
	
%%%  MISC SUPPORT PREDICATES %%%

% Check if S is a sublist of L
sublist(S, L) :-
  append(_, L2, L),
  append(S, _, L2).
  
% Builds a list of N instances of item C, then returns the result in L
append_n_times(_,L,0,L):- !.
append_n_times(C,L,N,Result):-
	append(L,C,NewL),
	NewN is N-1,
	append_n_times(C,NewL,NewN,Result).
 
% Flatten a list of lists
flatten2([], []) :- !.
flatten2([L|Ls], FlatL) :-
    !,
    flatten2(L, NewL),
    flatten2(Ls, NewLs),
    append(NewL, NewLs, FlatL).
flatten2(L, [L]).

scribpro.py

# coding=latin-1
#
# File: scribpro.py
# by Justin Mangue
#
# Description:
# This is a script that enables use of a Prolog natural language parsing component to control a Scribbler II robot
# over bluetooth.  Use in sync with nlp.pl.
#
# Dependencies:
# Python 2.7.4         http://www.python.org/getit/      
# PySwip 0.2.3         https://code.google.com/p/pyswip/
# PySpeech             https://code.google.com/p/pyspeech/
# Myro 2.9.5           http://myro.roboteducation.org/download/
# Swi-Prolog 6.2.6     http://www.swi-prolog.org/Download.html

# IMPORTS
from sys import exit
from myro import *
from pyswip import Prolog
import speech as pyspeech

# GLOBAL SETTINGS
verNum = '0.6'              # ScribPro Version Number
scribblerPort = 'COM5'      # Scribbler Bluetooth Port
prologFile = 'nlp.pl'       # Prolog file to consult
debug = 'on'                # Debug mode, if enabled, shows detailed prolog parse information
inputMode = "text"          # Input in text mode by default
outputMode = "text"         # Output to text by default

# CLASSES
class PrologResult(object):
  def __init__(self, prologcall, status, pythoncode):
     self.prologcall = prologcall
     self.status = status
     self.pythoncode = pythoncode

# FUNCTIONS

def strClean(string):
    """ strClean(str) -- converts "A string like This" into a list of lowercase atoms
        i.e. [a,string,like,this] for use in Prolog. also converts commas to "and"s """
    finalstring = "["
    loweredstring = string.lower()
    exclude = '!"#$%&'()*+-./:;<=>?@[\]^_`{|}~'
    for c in exclude:
        loweredstring = loweredstring.replace(c,"")    
    for ch in loweredstring:
        if ch == ' ':
            finalstring = finalstring + ","
        elif ch == ',':
            finalstring = finalstring + ",comma"
        #elif ch == u'xb0' or ch == '°':
        #    finalstring = finalstring + ",degrees"
        else:
            finalstring = finalstring + ch
    return finalstring + "]"
       
def parse(inStr):
    """parse(str) -- cleans the user input and then throws it at Prolog for analysis"""
    codeOut = "null"
    sentence = strClean(inStr)
    prologCall = 'parseToCode(' + sentence + ', Status, CodeOut).'
    for soln in prolog.query(prologCall, maxresult=1):
        statusOut = ''.join(soln["Status"])
        if statusOut == 'valid':
            codeOut = buildMultiCode(soln["CodeOut"])
    return PrologResult(prologCall,statusOut,codeOut)

def buildCode(prologterms):
    """converts a [instruction, param1, param2, ...] list into Python-exec-friendly "instruction(param1,param2,...)" functor format"""
    parameters = [str(item) for item in prologterms]
    if len(parameters) > 2:
        codeOut = parameters[0] + '('
        for x in range(len(parameters)-2):
            codeOut = codeOut + parameters[x+1] + ', '
        codeOut = codeOut + parameters[len(parameters)-1] + ')'
    elif len(parameters) == 2:
        codeOut = parameters[0] + '(' + parameters[1] + ')'
    else:
        codeOut = parameters[0] + '()'
    return codeOut

def buildMultiCode(instructions):
    """converts a list of [instruction, param1, param2] codes into a list of Python-exec-friendly "instruction(param1,param2,...)" strings
       i.e. buildMultiCode([('forward',['1.0','3.0']),('stop',[])]) -> ['forward(1.0, 3.0)', 'stop()']"""
    multiCodeOut = []
    for x in instructions:
        line = buildCode(x)
        multiCodeOut.append(line)
    return multiCodeOut


def execute(code):       # potential security concerns here!
    """executes multiple lines of python script in sequence"""
    for line in code:
        if type(line) is list:
            execute(line)
        else:
            exec line

def toggle_inputMode(mode):
    """toggles between text and voice mode"""
    if mode == "text":
        print "Now activating voice input."
        return "voice"
    else:
        print "Switching back to text-input mode."
        return "text"

def toggle_outputMode(mode):
    """toggles between text and voice mode"""
    if mode == "text":
        print "Now activating voice output."
        return "voice"
    else:
        print "Switching back to text-only output mode."
        return "text"

def toggle_debugMode():
    """toggles between debug mode being on/off"""
    if debug == "on":
        print "Disabling debug mode."
        return "off"
    else:
        print "Enabling debug mode."
        return "on"

# addon robot commands
def takePhoto(mode):
    """Take and display a photo in the specified color mode. 2=color(fast), 1=color, 0=gray"""
    if mode == 2:
        pic = takePicture("jpeg-fast")
    elif mode == 1:
        pic = takePicture("color")
    else:
        pic = takePicture("gray")
    show(pic,"Scribby Cam")
    return

def move_until_wall():
    """Repeatedly moves forward in small increments until wall is sensed"""
    while not wall():
        forward(1.0, 0.7)
    return

def moonwalk(time):
    """Moonwalk backwards for time"""
    while timeRemaining(time):
        forward(.25,.1)                         
        backward(1,.3)                          

# MAIN 
def main():
    # global variable references
    global debug
    global prolog
    global inputMode
    global outputMode
    
    # initialize the robot
    print "***************** ScribPro v" + verNum + " *****************"
    init(scribblerPort)                           
    print "Scribbler II found on bluetooth (" + scribblerPort + ")."
    
    # instantiation of Prolog interface
    prolog = Prolog()
    prolog.consult(prologFile)
    print "Prolog has been initialized."
    print "Loaded " + prologFile + " into SWI-Prolog!"

    print "Starting in " + inputMode + " input mode. ['voice' to toggle]"
    print "Starting in " + outputMode + " output mode. ['sound' to toggle]"
    print "*************************************************"
    
    # main routine
    while(1):
        # grab a line of input, either text or voice
        if inputMode == "voice":
            try:
                inStr = pyspeech.input("nAwaiting voice command. (Ctrl-C to enter text)")
                print "Voice command: " + inStr + "n"
            except KeyboardInterrupt:
                inStr = raw_input("Type in a command: ")
        else:
            inStr = raw_input("nEnter a command: ")

        # parse the input and behave appropriately
        if (inStr == 'quit' or inStr == 'exit'):
            print "Exiting..."
            exit(0)
        elif inStr == 'reload':
            newprolog = Prolog()
            newprolog.consult(prologFile)
            prolog = newprolog
            print "Reloaded " + prologFile + " into Prolog!"
        elif inStr == 'voice':
            inputMode = toggle_inputMode(inputMode)
        elif inStr == 'sound':
            outputMode = toggle_outputMode(outputMode)
        elif inStr == 'debug':
            debug = toggle_debugMode()
        else:
            Result = parse(inStr)
            if Result.status == 'valid':
                if outputMode == "voice":
                    pyspeech.say("Okay")
                print "Okay."
                execute(Result.pythoncode)
            else:
                if outputMode == "voice":
                    errorMsg = "I don't understand " + inStr
                    pyspeech.say(errorMsg)
                print "I don't understand "" + inStr + ""."
                    
            # Print detailed debug information
            if debug == "on":   
                print "nProlog query executed:", Result.prologcall
                print "Status:", Result.status
                print "CodeOut:", Result.pythoncode

if __name__ == "__main__":
    main()