yaz/commit

faceted video navigation

authorMichiel Hildebrand
Wed Jul 6 17:03:55 2011 +0200
committerMichiel Hildebrand
Wed Jul 6 17:03:55 2011 +0200
commitad3a9f3c2b70fbb7c21b608dc2273bfeed6f8f9f
tree8bade1d7626f350dbf3e46411ded142b6b2d8012
parent8bf3e31de8c61c74922fbdd63d452ba9c06453d5
Diff style: patch stat
diff --git a/api/reconcile.pl b/api/reconcile.pl
index e2049ae..00fd73d 100644
--- a/api/reconcile.pl
+++ b/api/reconcile.pl
@@ -43,7 +43,7 @@ flush_reconcile_cache :-
 %	Returns a json object.
 
 http_reconcile(Request) :-
- 	http_parameters(Request,
+	http_parameters(Request,
 			[ query(Query,
 				[atom,
 				 optional(true),
@@ -53,7 +53,7 @@ http_reconcile(Request) :-
 				[json,
 				 optional(true),
 				 description('a json object of the form {q1:{query:STRING}, ...}')]),
-  			  limit(Limit,
+			  limit(Limit,
 			      [number, default(3),
 			       description('Number of results to return per query')]),
 			  type(Type,
@@ -72,7 +72,7 @@ http_reconcile(Request) :-
 	(   nonvar(Queries), Queries = json(QueryList)
 	->  reconcile_list(QueryList, Limit, Type, Properties, Results),
 	    reply(Callback, json(Results))
- 	;   nonvar(Query)
+	;   nonvar(Query)
 	->  reconcile(Query, Limit, Type, Properties, Hits),
 	    hits_to_json_results(Hits, Results),
 	    reply(Callback, json([result=Results]))
@@ -98,7 +98,7 @@ reconcile_list([], _, _, _, []).
 reconcile_list([Key=json([query=Query])|Ts], Max, Type, Properties, [Key=json([result=Results])|Rs]) :-
 	reconcile(Query, Max, Type, Properties, Hits),
 	hits_to_json_results(Hits, Results),
- 	reconcile_list(Ts, Max,  Type, Properties, Rs).
+	reconcile_list(Ts, Max,  Type, Properties, Rs).
 
 %%	reconcile(+Query, +MaxResults, +Type, +Properties,
 %%      -Concept:hit(score,uri,property,label))
@@ -113,7 +113,7 @@ reconcile(Query, Max, Type, Properties, Hits) :-
 	!,
 	reconcile_filter(Hits0, Max, Type, Properties, Hits).
 reconcile(Query, Max, Type, Properties, Hits) :-
- 	label_list(LabelList),
+	label_list(LabelList),
 	find_resource_by_name(Query, Hits0, [attributes(LabelList),match(case),distance(true)]),
 	assert(reconcile_cache(Query, Hits0)),
 	reconcile_filter(Hits0, Max, Type, Properties, Hits).
@@ -214,7 +214,7 @@ http_save_reconcile(Request) :-
 	http_parameters(Request,
 			[ entry(TagEntry,
 				[description('URI of tagentry event')]),
-  			  uri(URI,
+			  uri(URI,
 			      [description('URI of resource tag is reconciled with')])
 			]),
 	valid_reconcile(TagEntry, URI, User, Tag, Error),
@@ -233,10 +233,10 @@ valid_reconcile(TagEntry, _URI, _User, Tag, Error) :-
 	).
 
 assert_recon(ReconcileEvent, TagEntry, URI, User) :-
-  	rdf_assert(ReconcileEvent, rdf:type, pprime:'ReconcileEvent', recon),
+	rdf_assert(ReconcileEvent, rdf:type, pprime:'ReconcileEvent', recon),
 	rdf_assert(ReconcileEvent, pprime:reconciles, TagEntry, recon),
 	rdf_assert(ReconcileEvent, pprime:reconcilesWith, URI, recon),
- 	rdf_assert(ReconcileEvent, sem:hasActor, User, recon).
+	rdf_assert(ReconcileEvent, sem:hasActor, User, recon).
 
 reconcile_event_uri(TagEntry, ReconcileEventURI) :-
 	% base URI on number of existing reconciliation for TagEntry
@@ -264,7 +264,7 @@ json_reply_error(Error) :-
 
 freebase_reconcile(Tags, ReconciledTags) :-
 	freebase_url(URL),
-       	freebase_query(Tags, Query),
+	freebase_query(Tags, Query),
 	freebase_option_string(Options),
 	www_form_encode(Query, EncQuery),
 	concat_atom([URL, '?queries=', EncQuery, Options], Request),
diff --git a/applications/yaz_fplayer.pl b/applications/yaz_fplayer.pl
new file mode 100644
index 0000000..8bed8f7
--- /dev/null
+++ b/applications/yaz_fplayer.pl
@@ -0,0 +1,241 @@
+:- module(yaz_fplayer,
+	  []).
+
+:- use_module(library(http/http_dispatch)).
+:- use_module(library(http/http_parameters)).
+:- use_module(library(http/http_path)).
+:- use_module(library(http/html_write)).
+:- use_module(library(http/html_head)).
+:- use_module(library(http/http_json)).
+:- use_module(library(http/js_write)).
+:- use_module(library(http/json)).
+:- use_module(library(http/http_session)).
+:- use_module(user(user_db)).
+:- use_module(library(semweb/rdf_db)).
+:- use_module(library(semweb/rdfs)).
+:- use_module(library(semweb/rdf_label)).
+
+:- use_module(library(yaz_util)).
+:- use_module(library(yui3)).
+:- use_module(library(video_annotation)).
+
+:- use_module(components(label)).
+:- use_module(components(yaz_page)).
+:- use_module(components(yaz_video_item)).
+
+:- use_module(api(reconcile)).
+
+
+:- http_handler(yaz(fplayer), http_yaz_fplayer, []).
+
+
+subject(R) :-
+	rdf(R, skos:inScheme, gtaa:'Onderwerpen').
+subject(R) :-
+	rdfs_individual_of(R, cornetto:'Synset').
+location(R) :-
+	rdf(R, skos:inScheme, gtaa:'GeografischeNamen').
+person(R) :-
+	rdf(R, skos:inScheme, gtaa:'Persoonsnamen').
+person(R) :-
+	rdf(R, skos:inScheme, gtaa:'Namen').
+
+
+%%	http_yaz_fplayer(+Request)
+%
+%	Emit an HTML page to link tags to concepts.
+
+http_yaz_fplayer(Request) :-
+	http_parameters(Request,
+			[ video(Video,
+				[description('Current video')]),
+			  process(Process,
+			       [optional(true),
+				desription('When set only annotations within this process are shown')]),
+			  user(User,
+				[optional(true),
+				 description('When set only annotations from this user are shown')]),
+			  interval(Interval,
+				   [default(10), number,
+				    description('When set one entry per tag is returned in interval (in milliseconds)')]),
+			  confirmed(Confirmed,
+				    [boolean, default(false),
+				     description('When true only tags that are entered by >1 user are shown')])
+			]),
+	Options0 = [video(Video),
+		    process(Process),
+		    user(User),
+		    confirmed(Confirmed),
+		    interval(Interval)
+		  ],
+	delete_nonground(Options0, Options),
+	video_annotations(Video, Annotations, Options),
+	maplist(annotation_pair, Annotations, TagEntries0),
+	merge_entries(TagEntries0, TagEntries),
+	concept_entries(TagEntries, subject, Subjects, Rest0),
+	concept_entries(Rest0, location, Locations, Rest),
+	concept_entries(Rest, person, Persons, _),
+	html_page(Video, Subjects, Locations, Persons, Options).
+
+annotation_pair(annotation(Value,_,_,[E0|_],_), Value-E1) :-
+	E0 = i(URI, Time),
+	E1 = entry(URI,Value,Time).
+
+concept_entries(TagEntries, Goal, ConceptEntries, Rest) :-
+	concept_entries_(TagEntries, Goal, ConceptEntries0, Rest),
+	merge_entries(ConceptEntries0, ConceptEntries1),
+	maplist(concept_pair, ConceptEntries1, ConceptEntries).
+
+concept_entries_([], _, [], []).
+concept_entries_([Tag-Entries|Ts], Goal, [Concept-Entries|Cs], Rest) :-
+	tag_concept(Tag, Goal, Concept),
+	!,
+	concept_entries_(Ts, Goal, Cs, Rest).
+concept_entries_([Tag-Entries|Ts], Goal, Cs, [Tag-Entries|Rest]) :-
+	concept_entries_(Ts, Goal, Cs, Rest).
+
+tag_concept(Tag, Goal, Concept) :-
+	tag_value(Tag, Value),
+	reconcile(Value, 3, Hits),
+	member(hit(_,Concept,_,_), Hits),
+	call(Goal, Concept).
+
+concept_pair(Concept-Es, annotation(uri(Concept,Label), 0, 0, Es)) :-
+	rdf_display_label(Concept, Label).
+
+%%	html_page(+Video, +Concepts, +Options)
+%
+%	Emit an HTML page for concept gardening
+
+html_page(Video, Subjects, Locations, Persons, Options) :-
+	reply_html_page(yaz,
+			[ title(['YAZ - ', Video])
+			],
+			[ \html_requires(css('fplayer.css')),
+			  \html_requires(css('tag.css')),
+			  \yaz_video_header(Video),
+			  div(id(tags),
+			      [ \tag_facet(person, 'Person/Organization'),
+				\tag_facet(location, 'Location'),
+				\tag_facet(subject, 'Subject')
+			      ]),
+			  div(id(video),
+			      [ div(id(videoplayer), []),
+				div([id(videoframes)], [])
+			      ]),
+			  script(type('text/javascript'),
+				\html_video_page_yui(Video, Subjects, Locations, Persons, Options))
+			]).
+
+tag_facet(Id, Label) -->
+	html(div(class(tagfacet),
+		 [ div(class(hd), Label),
+		   div(class(bd),
+		       div(id(Id), []))
+		 ])).
+
+html_video_page_yui(Video, Subjects, Locations, Persons, Options) -->
+	{ video_source(Video, Src, Duration),
+	  http_location_by_id(serve_video_frame, FrameServer),
+	  http_absolute_location(js('videoplayer/'), FilePath, []),
+	  http_absolute_location(js('videoplayer/videoplayer.js'), Videoplayer, []),
+	  http_absolute_location(js('videoframes/videoframes.js'), VideoFrames, []),
+	  http_absolute_location(js('tagplayer/tagplayer.js'), Tagplayer, []),
+	  http_absolute_location(js('timeline/timeline.js'), Timeline, []),
+	  annotation_to_json(Persons, JSONPersons),
+	  annotation_to_json(Locations, JSONLocations),
+	  annotation_to_json(Subjects, JSONSubjects)
+	},
+	html_requires(js('videoplayer/swfobject.js')),
+	js_yui3([{modules:{'video-player':{fullpath:Videoplayer},
+			   'video-frames':{fullpath:VideoFrames},
+			   'tag-player':{fullpath:Tagplayer},
+			   'timeline':{fullpath:Timeline}
+			  }}
+		],
+		[node,event,widget,anim,
+		 'json','querystring-stringify-simple',io,
+		 'video-player','video-frames','tag-player',
+		 timeline
+		],
+		[ \js_new(person,
+			 'Y.mazzle.TagPlayer'({tags:JSONPersons,
+					       height:150,
+					       width:200,
+					       topIndent:symbol(false)
+					      })),
+		  \js_new(location,
+			  'Y.mazzle.TagPlayer'({tags:JSONLocations,
+						height:150,
+						width:200,
+						topIndent:symbol(false)
+					       })),
+		  \js_new(subject,
+			 'Y.mazzle.TagPlayer'({tags:JSONSubjects,
+					       height:150,
+					       width:200,
+					       topIndent:symbol(false)
+					      })),
+		  \js_new(videoPlayer,
+			  'Y.mazzle.VideoPlayer'({filepath:FilePath,
+						  src:Src,
+						  width:560,
+						  height:400,
+						  autoplay:symbol(false),
+						  controls:symbol(true),
+						  duration:Duration
+						 })),
+		  \js_new(videoFrames,
+			 'Y.mazzle.VideoFrames'({frameServer:FrameServer,
+						 video:Src,
+						 duration:Duration,
+						 playerPath:FilePath,
+						 width:560,
+						 confirm:symbol(true),
+						 showRelated:symbol(false),
+						 showTime:symbol(true)
+						})),
+		  \js_yui3_decl(params, json(Options)),
+		  \js_call('videoPlayer.render'('#videoplayer')),
+		  \js_call('videoFrames.render'('#videoframes')),
+		  \js_call('person.render'('#person')),
+		  \js_call('location.render'('#location')),
+		  \js_call('subject.render'('#subject')),
+		  \js_yui3_on(person, itemSelect, \js_tag_select),
+		  \js_yui3_on(location, itemSelect, \js_tag_select),
+		  \js_yui3_on(subject, itemSelect, \js_tag_select),
+		  \js_yui3_on(videoFrames, frameSelect, \js_frame_select)
+		]).
+
+
+js_tag_select -->
+	js_function([e],
+		    \[
+'    var tag = e.tag;
+     var entry = tag.annotations[0];console.log(entry);\n',
+'    var time = (entry.startTime/1000)-3;
+     videoPlayer.setTime(time, true);\n',
+'    videoFrames.set("frames", tag.annotations);\n'
+		     ]).
+
+js_frame_select -->
+	js_function([e],
+		    \[
+'    var frame = e.frame;
+     var time = (frame.startTime/1000)-3;
+     videoPlayer.setTime(time, true);\n'
+		     ]).
+
+
+
+merge_entries(Pairs, Merged) :-
+	keysort(Pairs, Sorted),
+	group_pairs_by_key(Sorted, Grouped),
+	maplist(value_flatten, Grouped, Merged).
+
+value_flatten(Key-Lists, Key-List) :-
+	flatten(Lists, List).
+
+
+tag_value(literal(Tag), Tag).
+tag_value(uri(_URI,Tag), Tag).
diff --git a/applications/yaz_tag.pl b/applications/yaz_tag.pl
index 824ca59..0cad6b5 100644
--- a/applications/yaz_tag.pl
+++ b/applications/yaz_tag.pl
@@ -46,13 +46,13 @@ http_yaz_tag_edit(Request) :-
 				 [default(page),
 				  oneof([page,form]),
 				  description('Return a complete html page or only the form')])
-  			]),
+			]),
 	update_tag(Action, Entry, Value, Concept, Role),
 	(   rdf(Entry, rdf:type, pprime:'TagEntry')
 	->  annotation_value(Entry, _TagId, Label)
 	;   Label = Entry
 	),
- 	%annotation_provenance(Entry, Provenance),
+	%annotation_provenance(Entry, Provenance),
 	tag_concepts(Label, Concepts),
 	(   Format = page
 	->  html_page(Entry, Label, Provenance, Concepts, Concept, Role)
@@ -62,6 +62,8 @@ http_yaz_tag_edit(Request) :-
 	    print_html(HTML)
 	).
 
+
+
 annotation_value(Entry, TagId, Label) :-
 	rdf(Entry, rdf:value, TagId),
 	rdf_label(TagId, Lit),
@@ -157,7 +159,7 @@ html_roles(Selected) -->
 html_radiobox(Group, Value, Selected, Label) -->
 	{ radio_checked(Value, Selected, Checked)
 	},
- 	html([input([type(radio), name(Group), value(Value), Checked]),
+	html([input([type(radio), name(Group), value(Value), Checked]),
 	      span(Label)
 	     ]).
 
@@ -170,21 +172,21 @@ html_provenance([action(_,Time,User,_,Action)|T]) -->
 	html_provenance(T).
 
 html_provenance_action(added(_, _, Value, _PlayHead), Time, User) -->
- 	html(div(class(paction),
+	html(div(class(paction),
 		 [ 'added ',
 		   \html_tag_value(Value),
 		   \html_time_user(Time, User)])).
 html_provenance_action(removed(_, _), Time, User) -->
- 	html(div(class(paction),
+	html(div(class(paction),
 		 [ 'removed ',
 		   \html_time_user(Time, User)])).
 html_provenance_action(valueChange(_, Value), Time, User) -->
- 	html(div(class(paction),
+	html(div(class(paction),
 		 [ 'changed to ',
 		   \html_tag_value(Value),
 		   \html_time_user(Time, User)])).
 html_provenance_action(timeChange(_, PlayHead), Time, User) -->
- 	html(div(class(paction),
+	html(div(class(paction),
 		 [ 'changed time to ', PlayHead,
 		   \html_time_user(Time, User)])).
 
diff --git a/lib/yaz_util.pl b/lib/yaz_util.pl
index 57ab08f..3d1d482 100644
--- a/lib/yaz_util.pl
+++ b/lib/yaz_util.pl
@@ -16,7 +16,7 @@
 	    tag_entry_time/2,
 	    iri_to_url/2,
 	    delete_nonground/2,
- 	    video_source/2,
+	    video_source/2,
 	    video_source/3,
 	    annotation_to_json/2,
 	    video_desc/2,
@@ -104,14 +104,14 @@ list_limit_([H|T], N, [H|T1], Rest) :-
 %	elements in the value list.
 
 pairs_sort_by_value_count(Grouped, Sorted) :-
- 	pairs_value_count(Grouped, Counted),
+	pairs_value_count(Grouped, Counted),
 	keysort(Counted, Sorted0),
 	reverse(Sorted0, Sorted).
 
 pairs_value_count([], []).
 pairs_value_count([Key-Values|T], [Count-Key|Rest]) :-
 	length(Values, Count),
- 	pairs_value_count(T, Rest).
+	pairs_value_count(T, Rest).
 
 
 %%	sort_by_arg(+ListOfTerms, +Arg, -SortedList)
@@ -296,7 +296,7 @@ delete_nonground([H|T], [H|Rest]) :-
 	!,
 	delete_nonground(T, Rest).
 delete_nonground([_H|T], Rest) :-
- 	delete_nonground(T, Rest).
+	delete_nonground(T, Rest).
 
 
 %%	video_source(+URL, -Video)
@@ -337,6 +337,7 @@ http:convert_parameter(jsonresource, Atom, Term) :-
     literal(type:atom, value:_) + [type=literal],
     literal(value:_) + [type=literal],
     i(uri:atom, time:number),
+    entry(entry:atom, tag:_, startTime:number),
     annotation(tag:_, count:number),
     annotation(tag:_, tags:list, count:number),
     annotation(tag:_, startTime:number, endTime:number, annotations:list),
diff --git a/web/css/fplayer.css b/web/css/fplayer.css
new file mode 100644
index 0000000..53ee5f7
--- /dev/null
+++ b/web/css/fplayer.css
@@ -0,0 +1,118 @@
+#body {
+	margin: 0 auto;
+}
+
+.video-results h2 {
+	margin-bottom: 0;
+}
+.video-results .desc {
+	margin-bottom: 15px;
+	max-height: 2em;
+	color: #888;
+	font-size: 95%;
+	clear: both;
+}
+
+/* element style */
+
+#video {
+	float: left;
+	margin-left: 10px;
+}
+
+/* tag player */
+#tags {
+	float: left;
+}
+#tags input {
+	width: 100%;
+	border-width: 0 0 1px 0;
+	border-style: solid;
+	border-color: #CCC;
+	padding: 4px 0;
+	background: url("../icons/search_bg.png") no-repeat scroll 98% 60% #FFFFFF;
+}
+#tags .hd {
+	padding: 5px;
+	font-weight: bold;
+	background
+}
+
+.yui3-tag-player {
+	background: transparent;
+	overflow: auto;
+	border: 1px solid #CCCCCC;
+	margin-bottom: 5px;
+}	
+/* tag carousel */
+.yui3-tag-player ul {
+	margin: 0;
+	padding: 0;
+}
+.yui3-tag-player li {
+	overflow: hidden;
+	list-style: none;
+	margin: 1px 0;
+	padding: 4px 8px;
+	/*border-top: 1px solid #f2f2f2;*/
+}
+.yui3-tag-player li:nth-child(even) {
+	background-color: #EEE;
+}
+.yui3-tag-player li.focus .label {
+    font-size: 150%;
+}
+.yui3-tag-player li .hidden {
+	display: none;
+}
+.yui3-tag-player li .label {
+	cursor: pointer;
+	float: left;
+}
+.yui3-tag-player li .count {
+	float: right;
+	color: #AAA;
+}
+
+/* video frames */
+.yui3-video-frames {
+	margin-top: 10px;
+} 
+.yui3-video-frames-content {
+	overflow: hidden;
+	width: 100%;
+}
+.yui3-video-frames .header {
+	padding-bottom: 2px;
+	margin: 12px 0 4px;
+	font-weight: bold;
+	border-bottom: 1px solid #CCC;
+	clear: both;
+}
+.yui3-video-frames ul.frames-list {
+	margin: 0;
+	padding: 0;
+}
+.yui3-video-frames li {
+	width: 175px;
+	float: left;
+	overflow: hidden;
+	list-style: none;
+	margin: 0 10px 10px 0;
+}
+.yui3-video-frames img {
+	width: 100%;
+}
+.yui3-video-frames .image {
+	position: relative;
+	z-index: 2;
+	height: 98px;
+	overflow: hidden;
+}
+.yui3-video-frames .tag,
+.yui3-video-frames .frame-confirm {
+	text-align: center;
+	padding: 3px 0;
+	background-color: #DDD;
+	cursor: pointer;
+}
diff --git a/web/css/player.css b/web/css/player.css
index 9f9667e..32d6d29 100644
--- a/web/css/player.css
+++ b/web/css/player.css
@@ -34,7 +34,6 @@
 /* tag player */
 #tags {
 	float: left;
-	border: 1px solid #CCCCCC
 }
 #tags input {
 	width: 100%;
@@ -54,13 +53,20 @@
 	margin: 2px 0;
 	padding: 1px 4px;
 }
+#tags .hd {
+	padding: 5px;
+	font-weight: bold;
+	background
+}
 
 #tagplayer.hidden {
 	display: none;
 }
-#tagplayer .yui3-tag-player {
+.yui3-tag-player {
 	background: transparent;
 	overflow: auto;
+	border: 1px solid #CCCCCC;
+	margin-bottom: 5px;
 }	
 /* tag carousel */
 .yui3-tag-player ul {
diff --git a/web/js/videoframes/videoframes.js b/web/js/videoframes/videoframes.js
index 88e8db8..833202e 100644
--- a/web/js/videoframes/videoframes.js
+++ b/web/js/videoframes/videoframes.js
@@ -51,6 +51,12 @@ YUI.add('video-frames', function(Y) {
 		},
 		interval : {
 			value: 10
+		},
+		showRelated : {
+			value: true
+		},
+		showTime : {
+			value: false
 		}
 	};
 
@@ -94,17 +100,19 @@ YUI.add('video-frames', function(Y) {
 			for(var i=0; i < maxFrames; i++) {
 				list.append('<li class="frame hidden"></li>');
 			}
-			
-			// create list for related frames
-			node.appendChild('<div class="header">similar annotations:</div>');
-			related = node.appendChild(Node.create(VideoFrames.LIST_TEMPLATE));
-			related.addClass("related");
-			for(var i=0; i < maxFrames; i++) {
-				related.append('<li class="frame hidden"></li>');
-			}
-			
 			this.listNode = list;
-			this.relatedNode = related;
+			
+			if(this.get("showRelated")) {
+				// create list for related frames
+				node.appendChild('<div class="header">similar annotations:</div>');
+				related = node.appendChild(Node.create(VideoFrames.LIST_TEMPLATE));
+				related.addClass("related");
+				for(var i=0; i < maxFrames; i++) {
+					related.append('<li class="frame hidden"></li>');
+				}
+				this.relatedNode = related;
+			}	
+
 		},
 		
 		_renderFrames : function() {
@@ -113,7 +121,9 @@ YUI.add('video-frames', function(Y) {
 		},
 		_renderRelated : function() {
 			var frames = this.get("related");
-			this._updateFrames(this.relatedNode, frames);
+			if(this.relatedNode) {
+				this._updateFrames(this.relatedNode, frames);
+			}
 		},
 		
 		_updateFrames : function(node, frames) {
@@ -137,10 +147,10 @@ YUI.add('video-frames', function(Y) {
 		formatFrame : function(frame) {
 			var frameServer = this.get("frameServer"),
 				video = this.get("video"),
-				time = (frame.startTime/1000),
+				time = frame.startTime/1000,
 				label = frame.tag.label,
 				role = frame.role ? frame.role : '';
-
+ 
 			var html = '<div class="image">'
 				+'<img src="'+frameServer+'?url='+video+'&time='+time+'">'
 				+'</div>';
@@ -148,13 +158,22 @@ YUI.add('video-frames', function(Y) {
 				//html += '<div class="frame-confirm">click to confirm</div>';
 				html += '<div class="frame-confirm '+role+'">'
 					+this._roleLabel(role)
-					+label+'</div>';
+					+label+' '+this.formatTime(time)+
+					'</div>';
 			} else {
-				html += '<div class="tag">'+label+'</div>';
+				html += '<div class="tag">'+label+' '+this.formatTime(time)+'</div>';
 			}
 
 			return html;
 		},
+		formatTime : function(totalSeconds) {
+			if(this.get("showTime")) {
+				var minutes = Math.floor(totalSeconds / 60);
+				var seconds = Math.floor(totalSeconds % 60);
+				var spacer = (seconds<10) ? 0 : '';
+			 	return '<span class="time">'+minutes+':'+spacer+seconds+'</span>';
+			}	
+		},
 		
 		_roleLabel : function(role) {
 			if(role=="depicted") {