1 # SPDX-FileCopyrightText: 2022-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 """Define the POV render engine from generic Blender RenderEngine class."""
8 import builtins
as __builtin__
11 from sys
import platform
15 from bpy
.utils
import register_class
, unregister_class
18 def console_get(context
):
19 #context = bpy.context
20 for win
in context
.window_manager
.windows
:
21 if win
.screen
is not None:
23 for area
in scr
.areas
:
24 if area
.type == 'CONSOLE':
25 for space
in area
.spaces
:
26 if space
.type == 'CONSOLE':
27 return area
, space
, win
, scr
28 return None, None, None, None
30 def console_write(txt
):
31 area
, space
, window
, screen
= console_get()
34 context
= bpy
.context
.copy()
38 region
=area
.regions
[-1],
42 with bpy
.context
.temp_override(**context
):
43 for line
in txt
.split("\n"):
44 bpy
.ops
.console
.scrollback_append(text
=line
, type='INFO')
46 class RENDER_OT_test(bpy.types.Operator):
47 bl_idname = 'pov.oha_test'
49 bl_options = {'REGISTER', 'UNDO'}
51 txt: bpy.props.StringProperty(
55 def execute(self, context):
57 console_write(self.txt)
60 self.report({'INFO'}, 'Printing report to Info window.')
63 def console_print(*args, **kwargs):
65 #screens = (win.screen for win in context.window_manager.windows if win.screen is not None)
66 for win in context.window_manager.windows:
67 if win.screen is not None:
70 if a.type == 'CONSOLE':
74 c['space_data'] = a.spaces.active
75 c['region'] = a.regions[-1]
78 s = " ".join([str(arg) for arg in args])
79 for line in s.split("\n"):
80 bpy.ops.console.scrollback_append(c, text=line, type='INFO')
82 except BaseException as e:
84 print('An exception occurred: {}'.format(e))
88 def print(*args, **kwargs):
89 console_print(*args, **kwargs) # to Python Console
90 __builtin__.print(*args, **kwargs) # to System Console
93 user_dir
= bpy
.utils
.resource_path('USER')
94 preview_dir
= os
.path
.join(user_dir
, "preview")
95 # Make sure Preview directory exists and is empty
96 smoke_path
= os
.path
.join(preview_dir
, "smoke.df3")
98 class PovRender(bpy
.types
.RenderEngine
):
99 """Define the external renderer"""
101 bl_idname
= 'POVRAY_RENDER'
102 bl_label
= "Persitence Of Vision"
103 bl_use_eevee_viewport
= True
104 bl_use_shading_nodes_custom
= False
108 def _locate_binary():
109 """Identify POV engine"""
110 addon_prefs
= bpy
.context
.preferences
.addons
[__package__
].preferences
112 # Use the system preference if its set.
113 if pov_binary
:= addon_prefs
.filepath_povray
:
114 if os
.path
.exists(pov_binary
):
116 # Implicit else, as here return was still not triggered:
117 print("User Preferences path to povray %r NOT FOUND, checking $PATH" % pov_binary
)
120 # assume if there is a 64bit binary that the user has a 64bit capable OS
121 if platform
.startswith('win'):
124 win_reg_key
= winreg
.OpenKey(
125 winreg
.HKEY_CURRENT_USER
, "Software\\POV-Ray\\v3.7\\Windows"
127 win_home
= winreg
.QueryValueEx(win_reg_key
, "Home")[0]
129 # First try 64bits UberPOV
130 pov_binary
= os
.path
.join(win_home
, "bin", "uberpov64.exe")
131 if os
.path
.exists(pov_binary
):
134 # Then try 64bits POV
135 pov_binary
= os
.path
.join(win_home
, "bin", "pvengine64.exe")
136 if os
.path
.exists(pov_binary
):
139 # search the path all os's
140 pov_binary_default
= "povray"
142 os_path_ls
= os
.getenv("PATH").split(':') + [""]
144 for dir_name
in os_path_ls
:
145 pov_binary
= os
.path
.join(dir_name
, pov_binary_default
)
146 if os
.path
.exists(pov_binary
):
150 def _export(self
, depsgraph
, pov_path
, image_render_path
):
151 """gather all necessary output files paths user defined and auto generated and export there"""
153 scene
= bpy
.context
.scene
154 if scene
.pov
.tempfiles_enable
:
155 self
._temp
_file
_in
= tempfile
.NamedTemporaryFile(suffix
=".pov", delete
=False).name
156 # PNG with POV 3.7, can show the background color with alpha. In the long run using the
157 # POV-Ray interactive preview like bishop 3D could solve the preview for all formats.
158 self
._temp
_file
_out
= tempfile
.NamedTemporaryFile(suffix
=".png", delete
=False).name
159 # self._temp_file_out = tempfile.NamedTemporaryFile(suffix=".tga", delete=False).name
160 self
._temp
_file
_ini
= tempfile
.NamedTemporaryFile(suffix
=".ini", delete
=False).name
161 log_path
= os
.path
.join(tempfile
.gettempdir(), "alltext.out")
163 self
._temp
_file
_in
= pov_path
+ ".pov"
164 # PNG with POV 3.7, can show the background color with alpha. In the long run using the
165 # POV-Ray interactive preview like bishop 3D could solve the preview for all formats.
166 self
._temp
_file
_out
= image_render_path
+ ".png"
167 # self._temp_file_out = image_render_path + ".tga"
168 self
._temp
_file
_ini
= pov_path
+ ".ini"
169 scene_path
= scene
.pov
.scene_path
170 abs_log_path
= bpy
.path
.abspath(scene_path
)
171 log_path
= os
.path
.join(abs_log_path
, "alltext.out")
173 self._temp_file_in = "/test.pov"
174 # PNG with POV 3.7, can show the background color with alpha. In the long run using the
175 # POV-Ray interactive preview like bishop 3D could solve the preview for all formats.
176 self._temp_file_out = "/test.png"
177 #self._temp_file_out = "/test.tga"
178 self._temp_file_ini = "/test.ini"
181 self
._temp
_file
_log
= log_path
182 # self._temp_file_log = log_path.replace('\\', '/') # unnecessary relying on os.path
184 if scene
.pov
.text_block
== "":
186 def info_callback(txt
):
187 self
.update_stats("", "POV-Ray 3.7: " + txt
)
189 # os.makedirs(user_dir, exist_ok=True) # handled with previews
190 os
.makedirs(preview_dir
, exist_ok
=True)
192 render
.write_pov(self
._temp
_file
_in
, scene
, info_callback
)
196 def _render(self
, depsgraph
):
197 """Export necessary files and render image."""
198 scene
= bpy
.context
.scene
200 os
.remove(self
._temp
_file
_out
) # so as not to load the old file
204 pov_binary
= PovRender
._locate
_binary
()
206 print("POV-Ray 3.7: could not execute povray, possibly POV-Ray isn't installed")
209 render
.write_pov_ini(
210 self
._temp
_file
_ini
, self
._temp
_file
_log
, self
._temp
_file
_in
, self
._temp
_file
_out
213 print("***-STARTING-***")
216 # Always add user preferences include path field when specified
217 if (pov_documents
:= bpy
.context
.preferences
.addons
[__package__
].preferences
.docpath_povray
)!="":
218 extra_args
.append("+L"+ pov_documents
)
219 if scene
.pov
.command_line_switches
!= "":
220 extra_args
.extend(iter(scene
.pov
.command_line_switches
.split(" ")))
221 self
._is
_windows
= False
222 if platform
.startswith('win'):
223 self
._is
_windows
= True
224 if "/EXIT" not in extra_args
and not scene
.pov
.pov_editor
:
225 extra_args
.append("/EXIT")
227 # added -d option to prevent render window popup which leads to segfault on linux
228 extra_args
.append("-d")
232 self
._process
= subprocess
.Popen(
233 [pov_binary
, self
._temp
_file
_ini
] + extra_args
,
234 stdout
=subprocess
.PIPE
,
235 stderr
=subprocess
.STDOUT
,
239 print("POV-Ray 3.7: could not execute '%s'" % pov_binary
)
242 traceback
.print_exc()
243 print("***-DONE-***")
247 print("Engine ready!...")
248 print("Command line arguments passed: " + str(extra_args
))
252 """Delete temp files and unpacked ones"""
253 for f
in (self
._temp
_file
_in
, self
._temp
_file
_ini
, self
._temp
_file
_out
):
259 # Wait a bit before retrying file might be still in use by Blender,
260 # and Windows does not know how to delete a file in use!
261 time
.sleep(self
.DELAY
)
262 for i
in render
.unpacked_images
:
268 # Wait a bit before retrying file might be still in use by Blender,
269 # and Windows does not know how to delete a file in use!
270 time
.sleep(self
.DELAY
)
271 # avoid some crashes if memory leaks from one render to the next?
272 #self.free_blender_memory()
274 def render(self
, depsgraph
):
275 """Export necessary files from text editor and render image."""
277 scene
= bpy
.context
.scene
279 x
= int(r
.resolution_x
* r
.resolution_percentage
* 0.01)
280 y
= int(r
.resolution_y
* r
.resolution_percentage
* 0.01)
281 print("\n***INITIALIZING***")
283 # This makes some tests on the render, returning True if all goes good, and False if
284 # it was finished one way or the other.
285 # It also pauses the script (time.sleep())
287 time
.sleep(self
.DELAY
)
289 # User interrupts the rendering
290 if self
.test_break():
292 self
._process
.terminate()
293 print("***POV INTERRUPTED***")
298 poll_result
= self
._process
.poll()
299 except AttributeError:
300 print("***CHECK POV PATH IN PREFERENCES***")
302 # POV process is finisehd, one way or the other
303 if poll_result
is not None:
305 print("***POV PROCESS FAILED : %s ***" % poll_result
)
306 self
.update_stats("", "POV-Ray 3.7: Failed")
311 if bpy
.context
.scene
.pov
.text_block
!= "":
312 if scene
.pov
.tempfiles_enable
:
313 self
._temp
_file
_in
= tempfile
.NamedTemporaryFile(suffix
=".pov", delete
=False).name
314 self
._temp
_file
_out
= tempfile
.NamedTemporaryFile(suffix
=".png", delete
=False).name
315 # self._temp_file_out = tempfile.NamedTemporaryFile(suffix=".tga", delete=False).name
316 self
._temp
_file
_ini
= tempfile
.NamedTemporaryFile(suffix
=".ini", delete
=False).name
317 self
._temp
_file
_log
= os
.path
.join(tempfile
.gettempdir(), "alltext.out")
319 pov_path
= scene
.pov
.text_block
320 image_render_path
= os
.path
.splitext(pov_path
)[0]
321 self
._temp
_file
_out
= os
.path
.join(preview_dir
, image_render_path
)
322 self
._temp
_file
_in
= os
.path
.join(preview_dir
, pov_path
)
323 self
._temp
_file
_ini
= os
.path
.join(
324 preview_dir
, (os
.path
.splitext(self
._temp
_file
_in
)[0] + ".INI")
326 self
._temp
_file
_log
= os
.path
.join(preview_dir
, "alltext.out")
330 os.remove(self._temp_file_in) # so as not to load the old file
334 print(scene
.pov
.text_block
)
335 text
= bpy
.data
.texts
[scene
.pov
.text_block
]
336 with
open(self
._temp
_file
_in
, "w") as file:
337 # Why are the newlines needed?
339 file.write(text
.as_string())
343 # has to be called to update the frame on exporting animations
344 scene
.frame_set(scene
.frame_current
)
346 pov_binary
= PovRender
._locate
_binary
()
349 print("Could not execute POV-Ray, which installation possibly isn't standard ?")
352 # start ini UI options export
353 self
.update_stats("", "POV-Ray 3.7: Exporting ini options from Blender")
355 render
.write_pov_ini(
362 print("***-STARTING-***")
366 if scene
.pov
.command_line_switches
!= "":
367 for new_arg
in scene
.pov
.command_line_switches
.split(" "):
368 extra_args
.append(new_arg
)
370 if platform
.startswith('win'):
371 if "/EXIT" not in extra_args
and not scene
.pov
.pov_editor
:
372 extra_args
.append("/EXIT")
374 # added -d option to prevent render window popup which leads to segfault on linux
375 extra_args
.append("-d")
379 if scene
.pov
.sdl_window_enable
and not platform
.startswith(
381 ): # segfault on linux == False !!!
382 env
= {'POV_DISPLAY_SCALED': 'off'}
383 env
.update(os
.environ
)
384 self
._process
= subprocess
.Popen(
385 [pov_binary
, self
._temp
_file
_ini
],
386 stdout
=subprocess
.PIPE
,
387 stderr
=subprocess
.STDOUT
,
391 self
._process
= subprocess
.Popen(
392 [pov_binary
, self
._temp
_file
_ini
] + extra_args
,
393 stdout
=subprocess
.PIPE
,
394 stderr
=subprocess
.STDOUT
,
398 print("POV-Ray 3.7: could not execute '%s'" % pov_binary
)
401 traceback
.print_exc()
402 print("***-DONE-***")
406 print("Engine ready!...")
407 print("Command line arguments passed: " + str(extra_args
))
409 self
.update_stats("", "POV-Ray 3.7: Parsing File")
411 # Indented in main function now so repeated here but still not working
412 # to bring back render result to its buffer
414 if os
.path
.exists(self
._temp
_file
_out
):
415 xmin
= int(r
.border_min_x
* x
)
416 ymin
= int(r
.border_min_y
* y
)
417 xmax
= int(r
.border_max_x
* x
)
418 ymax
= int(r
.border_max_y
* y
)
419 result
= self
.begin_result(0, 0, x
, y
)
420 lay
= result
.layers
[0]
422 time
.sleep(self
.DELAY
)
424 lay
.load_from_file(self
._temp
_file
_out
)
426 print("***POV ERROR WHILE READING OUTPUT FILE***")
427 self
.end_result(result
)
428 # print(self._temp_file_log) #bring the pov log to blender console with proper path?
431 ) as f
: # The with keyword automatically closes the file when you are done
432 print(f
.read()) # console_write(f.read())
434 self
.update_stats("", "")
436 if scene
.pov
.tempfiles_enable
or scene
.pov
.deletefiles_enable
:
441 # if r.image_settings.file_format == 'OPENEXR':
443 # render.image_settings.color_mode = 'RGBA'
446 # r.image_settings.file_format = 'TARGA'
447 # r.image_settings.color_mode = 'RGBA'
449 blend_scene_name
= bpy
.data
.filepath
.split(os
.path
.sep
)[-1].split(".")[0]
452 image_render_path
= ""
454 # has to be called to update the frame on exporting animations
455 scene
.frame_set(scene
.frame_current
)
457 if not scene
.pov
.tempfiles_enable
:
460 pov_path
= bpy
.path
.abspath(scene
.pov
.scene_path
).replace('\\', '/')
462 if bpy
.data
.is_saved
:
463 pov_path
= bpy
.path
.abspath("//")
465 pov_path
= tempfile
.gettempdir()
466 elif pov_path
.endswith("/"):
468 pov_path
= bpy
.path
.abspath("//")
470 pov_path
= bpy
.path
.abspath(scene
.pov
.scene_path
)
472 if not os
.path
.exists(pov_path
):
474 os
.makedirs(pov_path
)
475 except BaseException
as e
:
477 print('An exception occurred: {}'.format(e
))
480 traceback
.print_exc()
482 print("POV-Ray 3.7: Cannot create scenes directory: %r" % pov_path
)
484 "", "POV-Ray 3.7: Cannot create scenes directory %r" % pov_path
491 image_render_path = bpy.path.abspath(scene.pov.renderimage_path).replace('\\','/')
492 if image_render_path == "":
493 if bpy.data.is_saved:
494 image_render_path = bpy.path.abspath("//")
496 image_render_path = tempfile.gettempdir()
497 #print("Path: " + image_render_path)
498 elif path.endswith("/"):
499 if image_render_path == "/":
500 image_render_path = bpy.path.abspath("//")
502 image_render_path = bpy.path.abspath(scene.pov.)
503 if not os.path.exists(path):
504 print("POV-Ray 3.7: Cannot find render image directory")
505 self.update_stats("", "POV-Ray 3.7: Cannot find render image directory")
511 if scene
.pov
.scene_name
== "":
512 if blend_scene_name
!= "":
513 pov_scene_name
= blend_scene_name
515 pov_scene_name
= "untitled"
517 pov_scene_name
= scene
.pov
.scene_name
518 if os
.path
.isfile(pov_scene_name
):
519 pov_scene_name
= os
.path
.basename(pov_scene_name
)
520 pov_scene_name
= pov_scene_name
.split('/')[-1].split('\\')[-1]
521 if not pov_scene_name
:
522 print("POV-Ray 3.7: Invalid scene name")
523 self
.update_stats("", "POV-Ray 3.7: Invalid scene name")
526 pov_scene_name
= os
.path
.splitext(pov_scene_name
)[0]
528 print("Scene name: " + pov_scene_name
)
529 print("Export path: " + pov_path
)
530 pov_path
= os
.path
.join(pov_path
, pov_scene_name
)
531 pov_path
= os
.path
.realpath(pov_path
)
533 image_render_path
= pov_path
534 # print("Render Image path: " + image_render_path)
537 self
.update_stats("", "POV-Ray 3.7: Exporting data from Blender")
538 self
._export
(depsgraph
, pov_path
, image_render_path
)
539 self
.update_stats("", "POV-Ray 3.7: Parsing File")
541 if not self
._render
(depsgraph
):
542 self
.update_stats("", "POV-Ray 3.7: Not found")
547 # x = int(r.resolution_x * r.resolution_percentage * 0.01)
548 # y = int(r.resolution_y * r.resolution_percentage * 0.01)
550 # Wait for the file to be created
551 # XXX This is no more valid, as 3.7 always creates output file once render is finished!
552 parsing
= re
.compile(br
"= \[Parsing\.\.\.\] =")
553 rendering
= re
.compile(br
"= \[Rendering\.\.\.\] =")
554 percent
= re
.compile(r
"\(([0-9]{1,3})%\)")
555 # print("***POV WAITING FOR FILE***")
560 # POV in Windows did not output its stdout/stderr, it displayed them in its GUI
561 # But now writes file
563 self
.update_stats("", "POV-Ray 3.7: Rendering File")
565 t_data
= self
._process
.stdout
.read(10000)
570 # XXX This is working for UNIX, not sure whether it might need adjustments for
572 # First replace is for windows
573 t_data
= str(t_data
).replace('\\r\\n', '\\n').replace('\\r', '\r')
574 lines
= t_data
.split('\\n')
575 last_line
+= lines
[0]
577 print('\n'.join(lines
), end
="")
578 last_line
= lines
[-1]
580 if rendering
.search(data
):
581 _pov_rendering
= True
582 match
= percent
.findall(str(data
))
584 self
.update_stats("", "POV-Ray 3.7: Rendering File (%s%%)" % match
[-1])
586 self
.update_stats("", "POV-Ray 3.7: Rendering File")
588 elif parsing
.search(data
):
589 self
.update_stats("", "POV-Ray 3.7: Parsing File")
591 if os
.path
.exists(self
._temp
_file
_out
):
592 # print("***POV FILE OK***")
593 # self.update_stats("", "POV-Ray 3.7: Rendering")
597 xmin
= int(r
.border_min_x
* x
)
598 ymin
= int(r
.border_min_y
* y
)
599 xmax
= int(r
.border_max_x
* x
)
600 ymax
= int(r
.border_max_y
* y
)
602 # print("***POV UPDATING IMAGE***")
603 result
= self
.begin_result(0, 0, x
, y
)
604 # XXX, tests for border render.
605 # result = self.begin_result(xmin, ymin, xmax - xmin, ymax - ymin)
606 # result = self.begin_result(0, 0, xmax - xmin, ymax - ymin)
607 lay
= result
.layers
[0]
609 # This assumes the file has been fully written We wait a bit, just in case!
610 time
.sleep(self
.DELAY
)
612 lay
.load_from_file(self
._temp
_file
_out
)
613 # XXX, tests for border render.
614 # lay.load_from_file(self._temp_file_out, xmin, ymin)
616 print("***POV ERROR WHILE READING OUTPUT FILE***")
618 # Not needed right now, might only be useful if we find a way to use temp raw output of
619 # pov 3.7 (in which case it might go under _test_wait()).
622 # possible the image wont load early on.
624 lay.load_from_file(self._temp_file_out)
625 # XXX, tests for border render.
626 #lay.load_from_file(self._temp_file_out, xmin, ymin)
627 #lay.load_from_file(self._temp_file_out, xmin, ymin)
631 # Update while POV-Ray renders
633 # print("***POV RENDER LOOP***")
635 # test if POV-Ray exists
636 if self._process.poll() is not None:
637 print("***POV PROCESS FINISHED***")
642 if self.test_break():
644 self._process.terminate()
645 print("***POV PROCESS INTERRUPTED***")
651 # Would be nice to redirect the output
652 # stdout_value, stderr_value = self._process.communicate() # locks
654 # check if the file updated
655 new_size = os.path.getsize(self._temp_file_out)
657 if new_size != prev_size:
661 time.sleep(self.DELAY)
664 self
.end_result(result
)
666 print("***NO POV OUTPUT IMAGE***")
668 print("***POV INPUT FILE WRITTEN***")
670 # print(filename_log) #bring the pov log to blender console with proper path?
673 self
._temp
_file
_log
, encoding
='utf-8'
674 ) as f
: # The with keyword automatically closes the file when you are done
676 if isinstance(msg
, str):
679 elif type(msg
) == bytes
:
680 #stdmsg = msg.split('\n')
681 stdmsg
= msg
.encode('utf-8', "replace")
682 # stdmsg = msg.encode("utf-8", "replace")
684 # stdmsg = msg.decode(encoding)
686 # msg.encode('utf-8').decode('utf-8')
687 stdmsg
.replace("\t", " ")
688 print(stdmsg
) # console_write(stdmsg) # todo fix segfault and use
689 except FileNotFoundError
:
690 print("No render log to read")
691 self
.update_stats("", "")
693 if scene
.pov
.tempfiles_enable
or scene
.pov
.deletefiles_enable
:
696 sound_on
= bpy
.context
.preferences
.addons
[__package__
].preferences
.use_sounds
697 finished_render_message
= "\'Et Voilà!\'"
699 if platform
.startswith('win') and sound_on
:
700 # Could not find tts Windows command so playing beeps instead :-)
701 # "Korobeiniki"(Коробе́йники)
702 # aka "A-Type" Tetris theme
705 winsound
.Beep(494, 250) # B
706 winsound
.Beep(370, 125) # F
707 winsound
.Beep(392, 125) # G
708 winsound
.Beep(440, 250) # A
709 winsound
.Beep(392, 125) # G
710 winsound
.Beep(370, 125) # F#
711 winsound
.Beep(330, 275) # E
712 winsound
.Beep(330, 125) # E
713 winsound
.Beep(392, 125) # G
714 winsound
.Beep(494, 275) # B
715 winsound
.Beep(440, 125) # A
716 winsound
.Beep(392, 125) # G
717 winsound
.Beep(370, 275) # F
718 winsound
.Beep(370, 125) # F
719 winsound
.Beep(392, 125) # G
720 winsound
.Beep(440, 250) # A
721 winsound
.Beep(494, 250) # B
722 winsound
.Beep(392, 250) # G
723 winsound
.Beep(330, 350) # E
725 winsound
.Beep(440, 250) # A
726 winsound
.Beep(440, 150) # A
727 winsound
.Beep(523, 125) # D8
728 winsound
.Beep(659, 250) # E8
729 winsound
.Beep(587, 125) # D8
730 winsound
.Beep(523, 125) # C8
731 winsound
.Beep(494, 250) # B
732 winsound
.Beep(494, 125) # B
733 winsound
.Beep(392, 125) # G
734 winsound
.Beep(494, 250) # B
735 winsound
.Beep(440, 150) # A
736 winsound
.Beep(392, 125) # G
737 winsound
.Beep(370, 250) # F#
738 winsound
.Beep(370, 125) # F#
739 winsound
.Beep(392, 125) # G
740 winsound
.Beep(440, 250) # A
741 winsound
.Beep(494, 250) # B
742 winsound
.Beep(392, 250) # G
743 winsound
.Beep(330, 300) # E
745 # Mac supports natively say command
746 elif platform
== "darwin":
747 # We don't want the say command to block Python,
748 # so we add an ampersand after the message
749 # but if the os TTS package isn't up to date it
750 # still does thus, the try except clause
752 os
.system("say -v Amelie %s &" % finished_render_message
)
753 except BaseException
as e
:
755 print("your Mac may need an update, try to restart computer")
757 # While Linux frequently has espeak installed or at least can suggest
758 # Maybe windows could as well ?
759 elif platform
== "linux":
760 # We don't want the espeak command to block Python,
761 # so we add an ampersand after the message
762 # but if the espeak TTS package isn't installed it
763 # still does thus, the try except clause
765 os
.system("echo %s | espeak &" % finished_render_message
)
766 except BaseException
as e
:
782 for cls
in reversed(classes
):
783 unregister_class(cls
)