3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 #from gstfile import GstFile
20 from GPlayer
import VideoWidget
21 from GPlayer
import GstPlayer
22 from Subtitles
import Subtitles
23 #from datetime import time
26 from streams
import Media
27 from streams
import Stream
28 from MediaInfo
import MediaInfo
29 from SouffleurXML
import ProjectXML
33 #tell pyGTK, if possible, that we want GTKv2
36 #Some distributions come with GTK2, but not pyGTK
43 print "You need to install pyGTK or GTKv2 ",
44 print "or set your PYTHONPATH correctly."
45 print "try: export PYTHONPATH=",
46 print "/usr/local/lib/python2.2/site-packages/"
48 #now we have both gtk and gtk.glade imported
49 #Also, we know we are running GTK v2
56 In this init we are going to display the main
59 gladefile
="souffleur.glade"
60 windowname
="MAIN_WINDOW"
63 self
.p_position
= gst
.CLOCK_TIME_NONE
64 self
.p_duration
= gst
.CLOCK_TIME_NONE
65 self
.UPDATE_INTERVAL
=100
71 self
.videoWidgetGst
= None
77 #self.videoWidget=VideoWidget();
78 #gtk.glade.set_custom_handler(self.videoWidget, VideoWidget())
80 #gtk.glade.set_custom_handler(self.custom_handler)
81 self
.wTree
=gtk
.glade
.XML (gladefile
,windowname
)
82 self
.gladefile
= gladefile
83 # we only have two callbacks to register, but
84 # you could register any number, or use a
85 # special class that automatically
86 # registers all callbacks. If you wanted to pass
87 # an argument, you would use a tuple like this:
88 # dic = { "on button1_clicked" : (self.button1_clicked, arg1,arg2) , ...
89 #dic = { "on_button1_clicked" : self.button1_clicked, \
90 # "gtk_main_quit" : (gtk.mainquit) }
91 dic
= { "gtk_main_quit" : (gtk
.main_quit
),\
92 "on_main_file_quit_activate": (gtk
.main_quit
), \
93 "on_main_file_open_activate": self
.mainFileOpen
, \
94 "on_TOOL_PLAY_clicked": self
.playerPlay
,\
95 "on_TOOL_STOP_clicked": self
.playerStop
,\
96 "on_MEDIA_ADJUSTMENT_button_press_event": self
.buttonPressAdjustment
,\
97 "on_MEDIA_ADJUSTMENT_button_release_event": self
.buttonReleaseAdjustment
,\
98 "on_MEDIA_ADJUSTMENT_change_value": self
.changeValueAdjustment
,\
99 "on_VIDEO_OUT_PUT_expose_event": self
.exposeEventVideoOut
,\
100 "on_TOOL_START_clicked": self
.cb_setSubStartTime
,\
101 "on_TOOL_END_clicked": self
.cb_setSubEndTime
,\
102 "on_TOOL_SAVE_clicked": self
.cb_subChangeSave
,\
103 "on_TOOL_DELETE_clicked": self
.cb_subDel
,\
104 "on_main_file_save_activate": self
.cb_onSaveMenu
,\
105 "on_main_file_save_as_activate": self
.cb_onSaveAsMenu
,\
106 "on_main_file_new_activate": self
.cb_onNewMenu
,\
107 "on_TOOL_FIRST_clicked": self
.cb_onToolFirst
,\
108 "on_TOOL_LAST_clicked": self
.cb_onToolLast
,\
109 "on_MAIN_VIEW_STREAMS_activate": self
.cb_onStreamsWindow
,\
110 "on_MAIN_VIEW_SUBTITLES_activate": self
.cb_onSubtitleWindow
,\
111 "on_LIST_SUBS_cursor_changed": self
.cb_onSubsListSelect
}
112 self
.wTree
.signal_autoconnect (dic
)
114 self
.windowProjectOpen
=None
115 self
.windowProjectSO
=None
117 self
.windowMediaOpen
=None
118 self
.windowStreams
=gtk
.glade
.XML (self
.gladefile
,"STREAM_WINDOW")
119 dic
= {"on_TOOL_DEL_STREAM_clicked": self
.cb_delStream
,\
120 "on_TOOL_MOD_STREAM_clicked": self
.cb_modStream
,\
121 "on_TOOL_SAVE_STREAM_clicked": self
.cb_saveStream
,\
122 "on_TOOL_ADD_STREAM_clicked": self
.cb_addNewStream
,\
123 "on_STREAM_WINDOW_delete_event": self
.cb_StreamWindowDelete
}
124 self
.windowStreams
.signal_autoconnect (dic
)
125 ### Setup LIST_STREAMS
126 LIST
= self
.windowStreams
.get_widget("LIST_STREAMS")
128 self
.streamsTreeStore
= gtk
.TreeStore(gobject
.TYPE_STRING
, gobject
.TYPE_UINT
)
129 LIST
.set_model(self
.streamsTreeStore
)
130 cell
= gtk
.CellRendererText()
131 tvcolumn
= gtk
.TreeViewColumn('Streams', cell
, text
= 0)
132 LIST
.append_column(tvcolumn
)
134 self
.windowSubsList
=gtk
.glade
.XML (self
.gladefile
,"SUBS_LIST")
135 dic
= {"on_LIST_SUBS_cursor_changed": self
.cb_onSubsListSelect
,\
136 "on_SUBS_LIST_delete_event": self
.cb_onSubsWindowDelete
}
137 self
.windowSubsList
.signal_autoconnect (dic
)
138 SUBLIST
= self
.windowSubsList
.get_widget("LIST_SUBS")
140 self
.subsListStore
= gtk
.ListStore(gobject
.TYPE_UINT
,
143 SUBLIST
.set_model(self
.subsListStore
)
144 cell
= gtk
.CellRendererText()
145 tvcolumn
= gtk
.TreeViewColumn('Start', cell
, text
= 0)
146 SUBLIST
.append_column(tvcolumn
)
147 cell
= gtk
.CellRendererText()
148 tvcolumn
= gtk
.TreeViewColumn('End', cell
, text
= 1)
149 SUBLIST
.append_column(tvcolumn
)
150 cell
= gtk
.CellRendererText()
151 tvcolumn
= gtk
.TreeViewColumn('Text', cell
, text
= 2)
152 SUBLIST
.append_column(tvcolumn
)
153 WND
=self
.windowStreams
.get_widget("STREAM_WINDOW")
155 WND
=self
.windowSubsList
.get_widget("SUBS_LIST")
157 ### Main window setup
158 self
.videoWidget
= self
.wTree
.get_widget("VIDEO_OUT_PUT")
159 self
.adjustment
= self
.wTree
.get_widget("MEDIA_ADJUSTMENT")
160 self
.SubEdit
= self
.wTree
.get_widget("VIEW_SUB")
161 self
.labelHour
= self
.wTree
.get_widget("LABEL_HOUR")
162 self
.labelMin
= self
.wTree
.get_widget("LABEL_MIN")
163 self
.labelSec
= self
.wTree
.get_widget("LABEL_SEC")
164 self
.labelMSec
= self
.wTree
.get_widget("LABEL_MSEC")
165 self
.subStartTime
= self
.wTree
.get_widget("SUB_START_TIME")
166 self
.subEndTime
= self
.wTree
.get_widget("SUB_END_TIME")
167 self
.playButton
= self
.wTree
.get_widget("TOOL_PLAY")
169 #==============================================================================
170 def cb_onSubsWindowDelete(self
, widget
, event
):
173 #==============================================================================
174 def cb_StreamWindowDelete(self
, widget
, event
):
177 #==============================================================================
178 def cb_onSubtitleWindow(self
, menu
):
179 if self
.windowSubsList
:
180 WND
=self
.windowSubsList
.get_widget("SUBS_LIST")
182 #==============================================================================
183 def cb_onStreamsWindow(self
, menu
):
184 if self
.windowStreams
:
185 WND
=self
.windowStreams
.get_widget("STREAM_WINDOW")
187 #==============================================================================
188 def cb_onToolLast(self
, widget
):
190 time
= self
.Subtitle
.subKeys
[-1]
191 self
.setEditSubtitle(self
.Subtitle
.getSub(time
))
192 self
.player
.seek(time
*1000000)
193 #==============================================================================
194 def cb_onToolFirst(self
, widget
):
196 time
= self
.Subtitle
.subKeys
[0]
197 self
.setEditSubtitle(self
.Subtitle
.getSub(time
))
198 self
.player
.seek(time
*1000000)
199 #==============================================================================
200 def getSubtitle(self
, source
):
201 for i
in self
.Subtitles
:
202 if i
.subSource
==source
:
205 #==============================================================================
206 def cb_saveStream(self
, widget
):
207 if not self
.windowStreams
:
209 if not self
.streamsTreeStore
:
211 TView
= self
.windowStreams
.get_widget("LIST_STREAMS")
212 TSelec
= TView
.get_selection()
213 TModel
, TIter
= TSelec
.get_selected()
216 N
=TModel
.get_value(TIter
, 1)
217 mInfo
= self
.media
[N
]
218 if "subtitle" in mInfo
.MIME
:
219 tSubtitle
= self
.getSubtitle(mInfo
.Streams
[0].ID
)
220 tSubtitle
.subSave(mInfo
.source
, 1)
221 #==============================================================================
222 def cb_modStream(self
, widget
):
223 if not self
.windowStreams
:
225 if not self
.streamsTreeStore
:
227 TView
= self
.windowStreams
.get_widget("LIST_STREAMS")
228 TSelec
= TView
.get_selection()
229 TModel
, TIter
= TSelec
.get_selected()
232 N
=TModel
.get_value(TIter
, 1)
233 mInfo
= self
.media
[N
]
234 if "subtitle" in mInfo
.MIME
:
235 self
.setSubtitle(mInfo
.Streams
[0].ID
)
236 #==============================================================================
237 def setSubtitle(self
, source
):
238 for i
in self
.Subtitles
:
239 if i
.subSource
==source
:
243 if (self
.windowStreams
):
244 WND
=self
.windowSubsList
.get_widget("SUBS_LIST")
246 self
.subsWindowUpdate()
247 #==============================================================================
248 def updateStreamWindow(self
):
249 if not self
.streamsTreeStore
:
251 self
.streamsTreeStore
.clear()
252 for mInfo
in self
.media
:
253 iter = self
.streamsTreeStore
.append(None)
254 self
.streamsTreeStore
.set(iter, 0, mInfo
.MIME
+ " ("+mInfo
.source
+")", 1, self
.media
.index(mInfo
))
255 for i
in mInfo
.Streams
:
256 child
= self
.streamsTreeStore
.append(iter)
257 self
.streamsTreeStore
.set(child
, 0, i
.MIME
+ " ("+i
.Name
+")", 1, self
.media
.index(mInfo
))
258 #==============================================================================
259 def cb_delStream(self
, widget
):
260 if not self
.windowStreams
:
262 if not self
.streamsTreeStore
:
264 TView
= self
.windowStreams
.get_widget("LIST_STREAMS")
265 TSelec
= TView
.get_selection()
266 TModel
, TIter
= TSelec
.get_selected()
269 N
=TModel
.get_value(TIter
, 1)
271 self
.updateStreamWindow()
272 #==============================================================================
273 def cb_openMediaCancel(self
, widget
):
274 if self
.windowMediaOpen
:
275 WND
=self
.windowMediaOpen
.get_widget("OPEN_MEDIA")
277 #==============================================================================
278 def cb_openMediaOpen(self
, widget
):
279 WND
=self
.windowMediaOpen
.get_widget("OPEN_MEDIA")
280 FN
=WND
.get_filename()
283 MI
= MediaInfo(URI
, FN
, self
.lastID
)
285 tMedia
= MI
.getMedia()
287 self
.addMedia(tMedia
)
288 #==============================================================================
289 def cb_addNewStream(self
, widget
):
290 if(self
.windowMediaOpen
==None):
291 self
.windowMediaOpen
=gtk
.glade
.XML (self
.gladefile
,"OPEN_MEDIA")
292 dic
={"on_OPEN_BUTTON_CANCEL_clicked": self
.cb_openMediaCancel
,\
293 "on_OPEN_BUTTON_OPEN_clicked": self
.cb_openMediaOpen
}
294 self
.windowMediaOpen
.signal_autoconnect(dic
)
296 WND
=self
.windowMediaOpen
.get_widget("OPEN_MEDIA")
298 self
.windowMediaOpen
=None
302 #==============================================================================
303 def cb_onNewMenu(self
, menu
):
304 if self
.windowStreams
:
305 WND
=self
.windowStreams
.get_widget("STREAM_WINDOW")
307 #==============================================================================
308 def setEditSubtitle(self
, Sub
):
309 if not self
.Subtitle
:
312 if (self
.curSub
!=-1):
315 self
.SubEdit
.set_buffer(BUF
)
317 self
.setSubStartTime(0)
318 self
.setSubEndTime(0)
320 if (Sub
.start_time
!=self
.curSub
):
322 BUF
.set_text(Sub
.text
)
323 self
.SubEdit
.set_buffer(BUF
)
324 self
.curSub
=int(Sub
.start_time
)
325 self
.setSubStartTime(Sub
.start_time
)
326 self
.setSubEndTime(Sub
.end_time
)
327 #==============================================================================
328 def cb_onSubsListSelect(self
, widget
):
330 Selection
= widget
.get_selection()
333 Model
, Rows
= Selection
.get_selected_rows()
335 Row
= Model
[Rows
[0][0]]
337 Sub
= self
.Subtitle
.subs
[Row
[0]]
338 self
.setEditSubtitle(Sub
)
341 if self
.player
.is_playing():
344 real
= long(Row
[0]) # in ns
345 self
.player
.seek(real
*1000000)
346 # allow for a preroll
347 self
.player
.get_state(timeout
=50*gst
.MSECOND
) # 50 ms
350 #==============================================================================
351 def subsWindowUpdate(self
):
352 if not self
.Subtitle
:
354 if self
.windowSubsList
:
355 self
.subsListStore
.clear()
356 for i
in self
.Subtitle
.subKeys
:
357 S
=self
.Subtitle
.subs
[i
]
358 iter = self
.subsListStore
.append(None)
359 self
.subsListStore
.set(iter, 0, int(S
.start_time
),
362 #==============================================================================
363 def saveProject(self
):
364 if not self
.PFileName
:
366 if self
.PFileName
[-4:]!=".spf":
367 self
.PFileName
=self
.PFileName
+".spf"
369 PXML
.addHeadInfo("title", "Soufleur development version")
370 PXML
.addHeadInfo("desc", "This is version current at development stage.")
371 PXML
.addHeadInfo("author", "DarakuTenshi")
372 PXML
.addHeadInfo("email", "otaky@ukr.net")
373 PXML
.addHeadInfo("info", "Sample of save function")
376 for i
in self
.Subtitles
:
378 PXML
.write(self
.PFileName
)
379 #==============================================================================
380 def cb_projectSaveOpen(self
, widget
):
381 WND
=self
.windowProjectSO
.get_widget("SAVE_OPEN_PFILE")
382 self
.PFileName
=WND
.get_filename()
385 #==============================================================================
386 def cb_projectSaveCancel(self
, widget
):
387 if(self
.windowProjectSO
==None): return
388 WND
=self
.windowProjectSO
.get_widget("SAVE_OPEN_PFILE")
390 #==============================================================================
391 def cb_onSaveAsMenu(self
, widget
):
393 self
.cb_onSaveMenu(widget
)
394 #==============================================================================
395 def cb_onSaveMenu(self
, widget
):
399 if(self
.windowProjectSO
==None):
400 self
.windowProjectSO
=gtk
.glade
.XML (self
.gladefile
,"SAVE_OPEN_PFILE")
401 dic
={"on_PROJECT_BUTTON_CANCEL_clicked": self
.cb_projectSaveCancel
,\
402 "on_PROJECT_BUTTON_OK_clicked": self
.cb_projectSaveOpen
}
403 self
.windowProjectSO
.signal_autoconnect(dic
)
404 WND
=self
.windowProjectSO
.get_widget("SAVE_OPEN_PFILE")
405 WND
.set_action(gtk
.FILE_CHOOSER_ACTION_SAVE
)
406 OKB
= self
.windowProjectSO
.get_widget("PROJECT_BUTTON_OK")
407 OKB
.set_label("gtk-save")
408 OKB
.set_use_stock(True)
409 Filter
=gtk
.FileFilter()
410 Filter
.set_name("Souffleur project file")
411 Filter
.add_pattern("*.spf")
412 WND
.add_filter(Filter
)
414 WND
=self
.windowProjectSO
.get_widget("SAVE_OPEN_PFILE")
416 self
.windowProjectSO
=None
417 self
.cb_onSaveMenu(widget
)
420 #==============================================================================
421 def cb_subDel(self
, widget
):
422 if (self
.Subtitle
!= None) and (self
.curSub
!= -1):
423 self
.Subtitle
.subDel(self
.curSub
)
424 #==============================================================================
425 def cb_subChangeSave(self
, widget
):
426 if (self
.Subtitle
!= None):
427 if (self
.curSub
!= -1):
428 BUF
= self
.SubEdit
.get_buffer()
429 TEXT
= BUF
.get_text(BUF
.get_start_iter(), BUF
.get_end_iter())
430 self
.Subtitle
.subs
[int(self
.curSub
)].text
= str(TEXT
)
431 self
.Subtitle
.subs
[int(self
.curSub
)].end_time
=self
.subEndTime
.get_value_as_int()
432 if self
.Subtitle
.subs
[int(self
.curSub
)].start_time
!=self
.subStartTime
.get_value_as_int():
433 newTime
=self
.subStartTime
.get_value_as_int()
434 self
.Subtitle
.subs
[int(self
.curSub
)].start_time
=newTime
435 self
.Subtitle
.subUpdate(int(self
.curSub
))
436 self
.curSub
= newTime
437 #for i in self.Subtitles:
438 # if i.subSource == self.Subtitle.subSource:
439 # self.Subtitles[self.Subtitles.index(i)]=self.Subtitle
440 self
.subsWindowUpdate()
443 #==============================================================================
445 ST
= self
.subStartTime
.get_value()
446 ET
= self
.subEndTime
.get_value()
447 BUF
= self
.SubEdit
.get_buffer()
448 Text
= BUF
.get_text(BUF
.get_start_iter(), BUF
.get_end_iter())
449 if (( ST
> 0 ) and ( ET
> ST
) and ( Text
!= "" )):
450 self
.Subtitle
.subAdd(ST
, ET
, Text
, None, 1)
452 #==============================================================================
453 def cb_setSubStartTime(self
, widget
):
454 self
.subStartTime
.set_value(self
.p_position
/1000000)
455 #==============================================================================
456 def cb_setSubEndTime(self
, widget
):
457 self
.subEndTime
.set_value(self
.p_position
/1000000)
458 #==============================================================================
459 def setSubStartTime(self
, time
):
460 self
.subStartTime
.set_value(time
)
461 #==============================================================================
462 def setSubEndTime(self
, time
):
463 self
.subEndTime
.set_value(time
)
464 #==============================================================================
465 def exposeEventVideoOut(self
, widget
, event
):
466 if self
.videoWidgetGst
:
467 self
.videoWidgetGst
.do_expose_event(event
)
468 #==============================================================================
469 def changeValueAdjustment(self
, widget
, t1
, t2
):
470 #if (not self.scroll):
471 real
= long(self
.adjustment
.get_value()) # in ns
472 self
.player
.seek(real
)
473 # allow for a preroll
474 self
.player
.get_state(timeout
=50*gst
.MSECOND
) # 50 ms
476 #==============================================================================
477 def buttonReleaseAdjustment(self
, widget
, event
):
479 #==============================================================================
480 def buttonPressAdjustment(self
, widget
, event
):
482 #==============================================================================
483 def playerStop(self
, widget
):
485 if self
.player
.is_playing():
488 #==============================================================================
489 def playerPlay(self
, widget
):
492 #==============================================================================
493 def mainFileOpen(self
, widget
):
494 if(self
.windowProjectOpen
==None):
495 self
.windowProjectOpen
=gtk
.glade
.XML (self
.gladefile
,"SAVE_OPEN_PFILE")
496 dic
={"on_PROJECT_BUTTON_CANCEL_clicked": self
.openFileCancel
,\
497 "on_PROJECT_BUTTON_OK_clicked": self
.openFileOpen
}
498 self
.windowProjectOpen
.signal_autoconnect(dic
)
499 WND
=self
.windowProjectOpen
.get_widget("SAVE_OPEN_PFILE")
500 WND
.set_action(gtk
.FILE_CHOOSER_ACTION_OPEN
)
501 OKB
= self
.windowProjectOpen
.get_widget("PROJECT_BUTTON_OK")
502 OKB
.set_label("gtk-open")
503 OKB
.set_use_stock(True)
504 Filter
=gtk
.FileFilter()
505 Filter
.set_name("Souffleur project file")
506 Filter
.add_pattern("*.spf")
507 WND
.add_filter(Filter
)
509 WND
=self
.windowProjectOpen
.get_widget("SAVE_OPEN_PFILE")
511 self
.windowProjectOpen
=None
512 self
.mainFileOpen(widget
)
516 #==============================================================================
517 def openFileCancel(self
, widget
):
518 if(self
.windowProjectOpen
==None): return
519 WND
=self
.windowProjectOpen
.get_widget("SAVE_OPEN_PFILE")
522 #==============================================================================
523 def openFileOpen(self
, widget
):
524 WND
=self
.windowProjectOpen
.get_widget("SAVE_OPEN_PFILE")
525 self
.PFileName
=WND
.get_filename()
528 PXML
.load(self
.PFileName
)
529 for i
in PXML
.getMedia():
532 for i
in PXML
.getSubtitle():
533 self
.Subtitles
.append(i
)
534 if len(self
.media
)>0:
535 WND
=self
.windowStreams
.get_widget("STREAM_WINDOW")
538 #==============================================================================
539 def addMedia(self
, mInfo
):
543 self
.media
.append(mInfo
)
544 self
.lastID
= mInfo
.lastID
545 self
.updateStreamWindow()
546 if "subtitle" in mInfo
.MIME
:
547 tSubtitle
= Subtitles()
548 tSubtitle
.subLoad(mInfo
.source
, mInfo
.Streams
[0].ID
)
549 self
.Subtitles
.append(tSubtitle
)
551 self
.videoWidgetGst
=VideoWidget(self
.videoWidget
)
552 self
.player
=GstPlayer(self
.videoWidgetGst
)
553 self
.player
.set_location("file://"+mInfo
.source
)
554 if self
.videoWidget
.flags() & gtk
.REALIZED
:
557 self
.videoWidget
.connect_after('realize',
558 lambda *x
: self
.play_toggled())
560 #==============================================================================
561 def play_toggled(self
):
562 if self
.player
.is_playing():
564 self
.playButton
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
565 #self.playButton.set_icon_name(gtk.STOCK_MEDIA_PLAY)
568 if self
.update_id
== -1:
569 self
.update_id
= gobject
.timeout_add(self
.UPDATE_INTERVAL
,
570 self
.update_scale_cb
)
571 self
.playButton
.set_stock_id(gtk
.STOCK_MEDIA_PAUSE
)
572 #==============================================================================
573 def update_scale_cb(self
):
574 had_duration
= self
.p_duration
!= gst
.CLOCK_TIME_NONE
575 self
.p_position
, self
.p_duration
= self
.player
.query_position()
576 if self
.p_duration
!= self
.t_duration
:
577 self
.t_duration
= self
.p_duration
578 self
.adjustment
.set_range(0, self
.t_duration
)
579 tmSec
= self
.p_position
/1000000
587 TText
= self
.Subtitle
.getSub(MSec
)
588 if self
.player
.is_playing():
590 self
.setEditSubtitle(TText
)
592 self
.setEditSubtitle(None)
593 if (self
.p_position
!= gst
.CLOCK_TIME_NONE
):# and (not self.scroll):
594 value
= self
.p_position
595 self
.adjustment
.set_value(value
)
596 self
.labelHour
.set_text("%02d"%Hour
)
597 self
.labelMin
.set_text("%02d"%Min
)
598 self
.labelSec
.set_text("%02d"%Sec
)
599 self
.labelMSec
.set_text("%09d"%MSec
)
601 #==============================================================================
603 #==============================================================================
604 souffleur
=Souffleur()