1 #! /ufs/guido/bin/sgi/python
3 # Video bag of tricks: record video(+audio) in various formats and modes
7 # - improve user interface
9 # - command line options to set initial settings
10 # - save settings in a file
18 sts
= os
.system('makemap') # Must be before "import fl" to work
32 sys
.path
.append('/ufs/jack/src/av/vcr')
42 watchcursor
.defwatch(WATCH
)
45 ## fl.set_graphics_mode(0, 1)
46 vb
= VideoBagOfTricks()
51 StopCapture
= 'StopCapture'
53 VideoFormatLabels
= ['Video off', 'rgb8', 'grey8', 'grey4', 'grey2', \
54 'grey2 dith', 'mono dith', 'mono thresh', 'rgb24', 'rgb24-jpeg', \
56 VideoFormats
= ['', 'rgb8', 'grey', 'grey4', 'grey2', \
57 'grey2', 'mono', 'mono', 'rgb', 'jpeg', 'compress']
59 VideoModeLabels
= ['Continuous', 'Burst', 'Single frame', 'VCR sync']
60 [VM_CONT
, VM_BURST
, VM_SINGLE
, VM_VCR
] = range(1, 5)
62 AudioFormatLabels
= ['Audio off', \
63 '16 bit mono', '16 bit stereo', '8 bit mono', '8 bit stereo']
64 [A_OFF
, A_16_MONO
, A_16_STEREO
, A_8_MONO
, A_8_STEREO
] = range(1, 6)
66 VcrSpeedLabels
= ['normal', '1/3', '1/5', '1/10', '1/30', 'single-step']
67 VcrSpeeds
= [None, 5, 4, 3, 2, 1, 0]
69 RgbSizeLabels
= ['full', 'quarter', 'sixteenth']
72 if os
.environ
.has_key('HOME'):
73 HOME
=os
.environ
['HOME']
76 VB_INIT_FILE
=HOME
+ '/.Vb_init'
78 VB_INIT_KEYS
=['vfile', 'vmode', 'mono_thresh', 'vformat', 'comp_scheme', \
79 'rgb24_size', 'afile', 'aformat']
81 class VideoBagOfTricks
:
87 formdef
= flp
.parse_form('VbForm', 'form')
88 flp
.create_full_form(self
, formdef
)
90 if self
.vmode
<> VM_CONT
:
91 self
.g_cont
.hide_object()
92 if self
.vmode
<> VM_BURST
:
93 self
.g_burst
.hide_object()
94 if self
.vmode
<> VM_SINGLE
:
95 self
.g_single
.hide_object()
96 if self
.vmode
<> VM_VCR
:
97 self
.g_vcr
.hide_object()
98 if self
.vformat
<> 'compress':
99 self
.g_compress
.hide_object()
105 self
.optfullsizewindow()
107 fl
.set_event_call_back(self
.do_event
)
116 # Get position of video window
117 gl
.winset(self
.window
)
118 x
, y
= gl
.getorigin()
119 width
, height
= gl
.getsize()
120 # Calculate position of form window
122 x2
= x1
+ int(self
.form
.w
) - 1
124 y1
= y2
- int(self
.form
.h
) + 1
125 # Position and show form window
126 gl
.prefposition(x1
, x2
, y1
, y2
)
127 self
.form
.show_form(FL
.PLACE_FREE
, FL
.TRUE
, 'Vb Control')
129 def getdefaultdefaults(self
):
131 self
.vfile
= 'film.video'
133 self
.mono_thresh
= 128
134 self
.vformat
= 'rgb8'
135 self
.comp_scheme
= 'Uncompressed'
137 # Missing: drop, rate, maxmem, nframes, rate, vcrspeed
139 self
.afile
= 'film.aiff'
142 def getdefaults(self
):
143 self
.getdefaultdefaults()
144 # XXXX Read defaults file and override.
146 fp
= open(VB_INIT_FILE
, 'r')
148 print 'Vb: no init file'
151 data
= fp
.read(1000000)
153 self
.initcont
= eval(data
)
155 print 'Vb: Ill-formatted init file'
157 for k
in self
.initcont
.keys():
159 setattr(self
, k
, self
.initcont
[k
])
161 def savedefaults(self
):
163 for k
in VB_INIT_KEYS
:
164 newdb
[k
] = getattr(self
, k
)
165 if newdb
<> self
.initcont
:
167 fp
= open(VB_INIT_FILE
, 'w')
169 print 'Vb: Cannot create', VB_INIT_FILE
174 def setdefaults(self
):
179 self
.c_vformat
.clear_choice()
180 for label
in VideoFormatLabels
:
181 self
.c_vformat
.addto_choice(label
)
182 self
.c_vformat
.set_choice(1 + VideoFormats
.index(self
.vformat
))
183 self
.c_vmode
.clear_choice()
184 for label
in VideoModeLabels
:
185 self
.c_vmode
.addto_choice(label
)
186 self
.c_vmode
.set_choice(self
.vmode
)
188 self
.b_drop
.set_button(1)
189 self
.in_rate
.set_input('2')
190 self
.in_maxmem
.set_input('1.0')
191 self
.in_nframes
.set_input('0')
192 self
.in_nframes_vcr
.set_input('1')
193 self
.in_rate_vcr
.set_input('1')
194 self
.c_vcrspeed
.clear_choice()
195 for label
in VcrSpeedLabels
:
196 self
.c_vcrspeed
.addto_choice(label
)
197 self
.c_vcrspeed
.set_choice(4)
198 self
.c_rgb24_size
.clear_choice()
199 for label
in RgbSizeLabels
:
200 self
.c_rgb24_size
.addto_choice(label
)
201 self
.c_rgb24_size
.set_choice(self
.rgb24_size
)
203 algs
= cl
.QueryAlgorithms(CL
.VIDEO
)
204 self
.all_comp_schemes
= []
205 for i
in range(0, len(algs
), 2):
206 if algs
[i
+1] in (CL
.COMPRESSOR
, CL
.CODEC
):
207 self
.all_comp_schemes
.append(algs
[i
])
208 self
.c_cformat
.clear_choice()
209 for label
in self
.all_comp_schemes
:
210 self
.c_cformat
.addto_choice(label
)
211 i
= self
.all_comp_schemes
.index(self
.comp_scheme
)
212 self
.c_cformat
.set_choice(i
+1)
216 self
.c_aformat
.clear_choice()
217 for label
in AudioFormatLabels
:
218 self
.c_aformat
.addto_choice(label
)
219 self
.c_aformat
.set_choice(self
.aformat
)
224 self
.video
= sv
.OpenVideo()
225 except sv
.error
, msg
:
226 print 'Error opening video:', msg
228 param
= [SV
.BROADCAST
, SV
.PAL
]
229 if self
.video
: self
.video
.GetParam(param
)
230 if param
[1] == SV
.PAL
:
233 elif param
[1] == SV
.NTSC
:
237 print 'Unknown video standard:', param
[1]
239 self
.maxx
, self
.maxy
= x
, y
243 def makewindow(self
):
244 x
, y
= self
.maxx
, self
.maxy
252 # Place the window at (150, 150) from top left
253 # (the video board likes this location...)
259 gl
.prefposition(x1
, x2
, y1
, y2
)
260 self
.window
= gl
.winopen('Vb video')
267 gl
.qdevice(DEVICE
.LEFTMOUSE
)
268 gl
.qdevice(DEVICE
.WINQUIT
)
269 gl
.qdevice(DEVICE
.WINSHUT
)
271 def optfullsizewindow(self
):
274 gl
.winset(self
.window
)
276 x
, y
= self
.maxx
, self
.maxy
278 x
, y
= self
.curx
, self
.cury
279 left
, bottom
= gl
.getorigin()
280 width
, height
= gl
.getsize()
281 bottom
= bottom
+height
-y
282 gl
.prefposition(left
, left
+x
-1, bottom
, bottom
+y
-1)
287 gl
.maxsize(self
.maxx
, self
.maxy
)
292 if not self
.video
: return
295 self
.curx
, self
.cury
= x
, y
296 self
.video
.SetSize(x
, y
)
297 drop
= self
.b_drop
.get_button()
299 param
= [SV
.FIELDDROP
, 1, SV
.GENLOCK
, SV
.GENLOCK_OFF
]
301 param
= [SV
.FIELDDROP
, 0, SV
.GENLOCK
, SV
.GENLOCK_ON
]
303 param
= param
+[SV
.COLOR
, SV
.DEFAULT_COLOR
, \
307 param
= param
+[SV
.COLOR
, SV
.MONO
, SV
.DITHER
, 0, \
309 self
.video
.BindGLWindow(self
.window
, SV
.IN_REPLACE
)
310 self
.video
.SetParam(param
)
312 def rebindvideo(self
):
313 gl
.winset(self
.window
)
321 ok
= self
.vcr
.still()
325 self
.b_capture
.set_button(0)
327 # Event handler (catches resize of video window)
329 def do_event(self
, dev
, val
):
330 #print 'Event:', dev, val
331 if dev
in (DEVICE
.WINSHUT
, DEVICE
.WINQUIT
):
333 if dev
== DEVICE
.REDRAW
and val
== self
.window
:
337 # Video controls: format, mode, file
339 def cb_vformat(self
, *args
):
342 if self
.mono_use_thresh
:
343 s
= `self
.mono_thresh`
344 s
= fl
.show_input('Please enter mono threshold', s
)
347 self
.mono_thresh
= string
.atoi(s
)
348 except string
.atoi_error
:
349 fl
.show_message('Bad input, using', \
350 `self
.mono_thresh`
, '')
353 def cb_cformat(self
, *args
):
354 i
= self
.c_cformat
.get_choice()
355 self
.comp_scheme
= self
.all_comp_schemes
[i
-1]
358 def cb_vmode(self
, *args
):
361 self
.vmode
= self
.c_vmode
.get_choice()
362 self
.form
.freeze_form()
363 self
.g_cont
.hide_object()
364 self
.g_burst
.hide_object()
365 self
.g_single
.hide_object()
366 self
.g_vcr
.hide_object()
367 if self
.vmode
== VM_CONT
:
368 self
.g_cont
.show_object()
369 elif self
.vmode
== VM_BURST
:
370 self
.g_burst
.show_object()
371 elif self
.vmode
== VM_SINGLE
:
372 self
.g_single
.show_object()
373 elif self
.vmode
== VM_VCR
:
374 self
.g_vcr
.show_object()
375 self
.form
.unfreeze_form()
377 def cb_vfile(self
, *args
):
378 filename
= self
.vfile
379 hd
, tl
= os
.path
.split(filename
)
380 filename
= fl
.file_selector('Video save file:', hd
, '', tl
)
383 hd
, tl
= os
.path
.split(filename
)
384 if hd
== os
.getcwd():
386 self
.vfile
= filename
388 # Video mode specific video controls
390 def cb_rate(self
, *args
):
393 def cb_drop(self
, *args
):
396 def cb_maxmem(self
, *args
):
399 def cb_nframes(self
, *args
):
402 def cb_fps(self
, *args
):
405 def cb_nframes_vcr(self
, *args
):
408 def cb_rate_vcr(self
, *args
):
411 def cb_vcrspeed(self
, *args
):
414 def cb_rgb24_size(self
, *args
):
415 i
= self
.c_rgb24_size
.get_choice()
419 # Audio controls: format, file
421 def cb_aformat(self
, *args
):
424 def cb_afile(self
, *args
):
425 filename
= self
.afile
426 hd
, tl
= os
.path
.split(filename
)
427 filename
= fl
.file_selector('Audio save file:', hd
, '', tl
)
430 hd
, tl
= os
.path
.split(filename
)
431 if hd
== os
.getcwd():
433 self
.afile
= filename
435 # General controls: capture, reset, play, quit
437 def cb_capture(self
, *args
):
440 if not self
.b_capture
.get_button():
442 if not self
.video
or not self
.vformat
:
445 if self
.vmode
== VM_CONT
:
447 elif self
.vmode
== VM_BURST
:
449 elif self
.vmode
== VM_SINGLE
:
450 self
.single_capture(None, None)
451 elif self
.vmode
== VM_VCR
:
454 def cb_reset(self
, *args
):
457 def cb_play(self
, *args
):
459 sts
= os
.system('Vplay -q ' + self
.vfile
+ ' &')
461 def cb_quit(self
, *args
):
466 def burst_capture(self
):
468 gl
.winset(self
.window
)
471 fl
.show_message('Sorry, no 24 bit continuous capture yet', '', '')
473 vformat
= SV
.RGB8_FRAMES
474 nframes
= self
.getint(self
.in_nframes
, 0)
476 maxmem
= self
.getint(self
.in_maxmem
, 1.0)
477 memsize
= int(maxmem
* 1024 * 1024)
478 nframes
= self
.calcnframes(memsize
)
479 info
= (vformat
, x
, y
, nframes
, 1)
481 info2
, data
, bitvec
= self
.video
.CaptureBurst(info
)
482 except sv
.error
, msg
:
483 self
.b_capture
.set_button(0)
485 fl
.show_message('Capture error:', str(msg
), '')
487 if info
<> info2
: print info
, '<>', info2
488 self
.save_burst(info2
, data
, bitvec
)
491 def calcnframes(self
, memsize
):
492 gl
.winset(self
.window
)
495 pixels
= pixels
/2 # XXX always assume fields
496 if self
.mono
or self
.grey
:
499 n
= memsize
/(4*pixels
)
502 def save_burst(self
, info
, data
, bitvec
):
503 (vformat
, x
, y
, nframes
, rate
) = info
504 self
.open_if_closed()
508 tpf
= 1000 / 50.0 # XXX
509 for frameno
in range(0, nframes
*2):
510 if frameno
<> 0 and \
511 bitvec
[frameno
] == bitvec
[frameno
-1]:
512 nskipped
= nskipped
+ 1
516 # XXX Works only for fields and top-to-bottom
518 start
= frameno
*fieldsize
519 field
= data
[start
:start
+fieldsize
]
520 realframeno
= realframeno
+ 1
521 fn
= int(realframeno
*tpf
)
522 if not self
.write_frame(fn
, field
):
525 def cont_capture(self
):
526 saved_label
= self
.b_capture
.label
527 self
.b_capture
.label
= 'Stop\n' + saved_label
528 self
.open_if_closed()
530 fps
= 59.64 # Fields per second
531 # XXX (fps of Indigo monitor, not of PAL or NTSC!)
532 tpf
= 1000.0 / fps
# Time per field in msec
537 void
= fl
.check_forms()
541 cd
, id = self
.video
.GetCaptureData()
545 id = id + 2*self
.rate
546 data
= cd
.InterleaveFields(1)
547 cd
.UnlockCaptureData()
549 if not self
.write_frame(t
, data
):
555 # If recording audio, can't capture multiple sequences
557 self
.b_capture
.label
= saved_label
559 def single_capture(self
, stepfunc
, timecode
):
560 self
.open_if_closed()
564 cd
, id = self
.video
.GetCaptureData()
569 if stepfunc
: # This might step the video
570 d
=stepfunc() # to the next frame
572 data
= cd
.InterleaveFields(1)
574 x
, y
= self
.vout
.getsize()
575 if self
.use_compress
:
576 if self
.rgb24_size
== 1:
577 data
= cd
.YUVtoYUV422DC(0)
578 elif self
.rgb24_size
== 2:
579 data
= cd
.YUVtoYUV422DC_quarter(1)
582 elif self
.rgb24_size
== 3:
583 data
= cd
.YUVtoYUV422DC_sixteenth(1)
587 data
= cd
.YUVtoRGB(1)
588 if self
.maxx
*self
.maxy
*4 <> len(data
):
589 print 'maxx,maxy,exp,got=', self
.maxx
,
590 print self
.maxy
,self
.maxx
*self
.maxy
*4,
592 fl
.showmessage('Wrong sized data')
594 if self
.rgb24_size
<> 1:
595 data
= imageop
.scale(data
, 4, \
596 self
.maxx
, self
.maxy
, x
, y
)
599 data
= jpeg
.compress(data
, x
, y
, 4)
600 if self
.use_compress
:
601 data
= self
.compressor
.Compress(1, data
)
602 cd
.UnlockCaptureData()
605 timecode
= (self
.nframes
+1) * (1000/25)
606 return self
.write_frame(timecode
, data
)
608 def vcr_capture(self
):
611 print 'Connecting to VCR ...'
613 print 'Waiting for VCR to come online ...'
615 print 'Preparing VCR ...'
616 if not (self
.vcr
.fmmode('dnr') and \
617 self
.vcr
.dmcontrol('digital slow')):
618 self
.vcr_error('digital slow failed')
621 except VCR
.error
, msg
:
625 if not self
.vcr
.still():
626 self
.vcr_error('still failed')
628 self
.open_if_closed()
629 rate
= self
.getint(self
.in_rate_vcr
, 1)
631 vcrspeed
= self
.c_vcrspeed
.get_choice()
632 vcrspeed
= VcrSpeeds
[vcrspeed
]
634 stepfunc
= self
.vcr
.step
637 self
.speed_factor
= rate
638 addr
= start_addr
= self
.vcr
.sense()
639 if not self
.single_capture(None, 0):
641 print 'captured %02d:%02d:%02d:%02d' % self
.vcr
.addr2tc(addr
)
642 count
= self
.getint(self
.in_nframes_vcr
, 1) - 1
645 if not self
.vcr
.step():
646 self
.vcr_error('step failed')
647 here
= self
.vcr
.sense()
649 rate
= rate
- (here
- addr
)
652 if not self
.vcr
.fwdshuttle(vcrspeed
):
653 self
.vcr_error('fwd shuttle failed')
658 here
= self
.vcr
.sense()
659 except VCR
.error
, msg
:
664 print 'Missed', here
-addr
-1,
665 print 'frame' + 's'*(here
-addr
-1 <> 1)
666 cycle
= (cycle
+1) % rate
668 tc
= (here
-start_addr
)*40
669 if not self
.single_capture(stepfunc
, \
672 print 'captured %02d:%02d:%02d:%02d' \
673 % self
.vcr
.addr2tc(here
)
676 if self
.vcr
and not self
.vcr
.still():
677 self
.vcr_error('still failed')
679 def vcr_error(self
, msg
):
681 fl
.show_message('VCR error:', str(msg
), '')
683 # Init/end continuous capture mode
687 if self
.vmode
== VM_CONT
:
688 self
.rate
= self
.getint(self
.in_rate
, 2)
691 x
, y
= self
.vout
.getsize()
693 info
= (SV
.YUV411_FRAMES
, x
, y
, qsize
, self
.rate
)
695 info
= (SV
.RGB8_FRAMES
, x
, y
, qsize
, self
.rate
)
696 info2
= self
.video
.InitContinuousCapture(info
)
698 # XXX This is really only debug info
699 print 'Info mismatch: requested', info
, 'got', info2
702 self
.video
.EndContinuousCapture()
707 gl
.winset(self
.window
)
709 title
= 'Vb ' + self
.vfile
+ ' (%dx%d)' % (x
, y
)
712 def get_vformat(self
):
713 i
= self
.c_vformat
.get_choice()
714 label
= VideoFormatLabels
[i
-1]
715 format
= VideoFormats
[i
-1]
716 if format
== 'compress' and cl
== None:
717 fl
.show_message('Sorry, no compression library support')
720 self
.vformat
= format
721 if self
.vformat
== '':
722 self
.form
.freeze_form()
723 self
.g_video
.hide_object()
724 self
.g_cont
.hide_object()
725 self
.g_burst
.hide_object()
726 self
.g_single
.hide_object()
727 self
.form
.unfreeze_form()
729 self
.g_video
.show_object()
730 if self
.vmode
== VM_CONT
:
731 self
.g_cont
.show_object()
732 elif self
.vmode
== VM_BURST
:
733 self
.g_burst
.show_object()
734 elif self
.vmode
== VM_SINGLE
:
735 self
.g_single
.show_object()
737 self
.rgb
= (format
[:3] == 'rgb' or format
== 'compress')
738 self
.mono
= (format
== 'mono')
739 self
.grey
= (format
[:4] == 'grey')
740 self
.use_24
= (format
in ('rgb', 'jpeg', 'compress'))
742 self
.g_rgb24
.show_object()
744 self
.g_rgb24
.hide_object()
745 self
.use_jpeg
= (format
== 'jpeg')
746 self
.mono_use_thresh
= (label
== 'mono thresh')
747 self
.use_compress
= (format
== 'compress')
748 if self
.use_compress
:
749 self
.g_compress
.show_object()
751 self
.g_compress
.hide_object()
754 self
.greybits
= string
.atoi(s
)
757 if label
== 'grey2 dith':
762 if self
.greybits
== 2:
763 convertor
= imageop
.grey2grey2
764 elif self
.greybits
== 4:
765 convertor
= imageop
.grey2grey4
766 elif self
.greybits
== -2:
767 convertor
= imageop
.dither2grey2
768 self
.convertor
= convertor
769 self
.optfullsizewindow()
771 def get_aformat(self
):
773 self
.aformat
= self
.c_aformat
.get_choice()
774 if self
.aformat
== A_OFF
:
775 self
.g_audio
.hide_object()
777 self
.g_audio
.show_object()
779 def init_compressor(self
, w
, h
):
780 self
.compressor
= None
781 scheme
= cl
.QuerySchemeFromName(CL
.VIDEO
, self
.comp_scheme
)
782 self
.compressor
= cl
.OpenCompressor(scheme
)
783 parambuf
= [CL
.IMAGE_WIDTH
, w
, \
784 CL
.IMAGE_HEIGHT
, h
, \
785 CL
.ORIGINAL_FORMAT
, CL
.YUV422DC
]
786 self
.compressor
.SetParams(parambuf
)
787 return self
.compressor
.Compress(0, '')
789 def open_if_closed(self
):
797 def open_video(self
):
799 gl
.winset(self
.window
)
802 if self
.rgb24_size
== 2:
804 elif self
.rgb24_size
== 3:
806 vout
= VFile
.VoutFile(self
.vfile
)
807 vout
.setformat(self
.vformat
)
808 if self
.vformat
== 'compress':
809 cheader
= self
.init_compressor(x
, y
)
810 vout
.setcompressheader(cheader
)
812 if self
.vmode
== VM_BURST
:
817 self
.speed_factor
= 1
818 self
.t_nframes
.label
= `self
.nframes`
820 def write_frame(self
, t
, data
):
821 t
= t
* self
.speed_factor
826 data
= self
.convertor(data
, len(data
), 1)
828 if self
.mono_use_thresh
:
829 data
= imageop
.grey2mono(data
, \
833 data
= imageop
.dither2mono(data
, \
836 self
.vout
.writeframe(int(t
), data
, None)
839 if msg
== (0, 'Error 0'):
841 fl
.show_message('IOError', str(msg
), '')
843 self
.nframes
= self
.nframes
+ 1
844 self
.t_nframes
.label
= `self
.nframes`
847 def close_video(self
):
851 self
.t_nframes
.label
= ''
855 if msg
== (0, 'Error 0'):
857 fl
.show_message('IOError', str(msg
), '')
859 self
.compressor
= None
861 # Watch cursor handling
864 gl
.winset(self
.form
.window
)
865 gl
.setcursor(WATCH
, 0, 0)
866 gl
.winset(self
.window
)
867 gl
.setcursor(WATCH
, 0, 0)
870 gl
.winset(self
.form
.window
)
871 gl
.setcursor(ARROW
, 0, 0)
872 gl
.winset(self
.window
)
873 gl
.setcursor(ARROW
, 0, 0)
875 # Numeric field handling
877 def getint(self
, field
, default
):
879 value
= string
.atoi(field
.get_input())
880 except string
.atoi_error
:
882 field
.set_input(`value`
)
885 def getfloat(self
, field
, default
):
887 value
= float(eval(field
.get_input()))
889 value
= float(default
)
890 field
.set_input(`value`
)
895 def open_audio(self
):
896 if self
.aformat
== A_OFF
:
903 params
= [AL
.INPUT_RATE
, 0]
904 al
.getparams(AL
.DEFAULT_DEVICE
, params
)
906 self
.aout
= aifc
.open(self
.afile
, 'w')
907 if self
.aformat
in (A_16_STEREO
, A_8_STEREO
):
911 if self
.aformat
in (A_16_STEREO
, A_16_MONO
):
915 self
.aout
.setnchannels(nch
)
916 self
.aout
.setsampwidth(width
)
917 self
.aout
.setframerate(rate
)
922 self
.aport
= al
.openport('Vb audio record', 'r', c
)
926 thread
.start_new_thread(self
.record_audio
, ())
928 def start_audio(self
):
929 if self
.aformat
== A_OFF
:
933 def record_audio(self
, *args
):
934 # This function runs in a separate thread
935 # Currently no semaphores are used
936 while not self
.audio_stop
:
937 data
= self
.aport
.readsamps(4000)
939 self
.aout
.writeframes(data
)
943 def stop_audio(self
):
946 def close_audio(self
):
950 while self
.audio_busy
:
955 self
.aport
.closeport()
961 except KeyboardInterrupt: