File Coverage

blib/lib/SpeL/Wizard.pm
Criterion Covered Total %
statement 180 276 65.2
branch 48 114 42.1
condition 1 3 33.3
subroutine 19 20 95.0
pod 3 3 100.0
total 251 416 60.3


line stmt bran cond sub pod time code
1             # -*- cperl -*-
2             # PODNAME: Spel Wizard class
3             # ABSTRACT: engine to build audio files from the spel files generated by SpeL and maintain their up-to-dateness
4              
5              
6              
7 38     38   271 use strict;
  38         78  
  38         1552  
8 38     38   259 use warnings;
  38         82  
  38         2341  
9             package SpeL::Wizard;
10              
11 38     38   17985 use SpeL::Parser::Auxiliary;
  38         198  
  38         3380  
12 38     38   23526 use SpeL::Parser::Chunk;
  38         265  
  38         3571  
13              
14 38     38   516 use SpeL::I18n;
  38         103  
  38         1073  
15 38     38   226 use SpeL::Object::Command;
  38         138  
  38         1666  
16 38     38   223 use SpeL::Object::Environment;
  38         76  
  38         1413  
17              
18 38     38   208 use IO::File;
  38         74  
  38         5859  
19 38     38   251 use File::Path;
  38         81  
  38         3027  
20 38     38   22097 use FindBin;
  38         50353  
  38         2396  
21              
22 38     38   300 use Regexp::Grammars;
  38         89  
  38         411  
23 38     38   29205 use Regexp::Common qw /number/;
  38         113403  
  38         173  
24 38     38   151626 use Digest::MD5::File qw(md5_hex);
  38         2329979  
  38         311  
25              
26 38     38   50839 use IPC::Run;
  38         1459717  
  38         2555  
27              
28 38     38   467 use Data::Dumper;
  38         96  
  38         17372  
29             $Data::Dumper::Sortkeys = 1;
30             $Data::Dumper::Indent = 1;
31              
32              
33             sub new {
34 38     38 1 126 my $class = shift;
35              
36 38         114 my $self = {};
37 38 50       181 $class = (ref $class ? ref $class : $class );
38 38         133 bless $self, $class;
39              
40             $self->{auxDB} = {
41 38         462 newlabel => [],
42             bibcite => [],
43             };
44              
45              
46 38         170 $self->{argument} = $_[0];
47 38         132 $self->{config} = $_[1];
48            
49 38         525 $self->{auxParser} = SpeL::Parser::Auxiliary->new();
50 38         455 $self->{chunkParser} = SpeL::Parser::Chunk->new();
51              
52 38         157 return $self;
53             }
54              
55              
56             sub parseAuxFile {
57 38     38 1 88 my $this = shift;
58 38         127 my ( $verbosity, $test ) = @_;
59              
60 38         1074 my ( $volume, $path, $file ) = File::Spec->splitpath( $this->{argument} );
61            
62 38         738 my $auxFileName = File::Spec->catpath( $volume, $path, $file . '.aux' );
63            
64 38 100       3576 unless( -r $auxFileName ) {
65 37 50       1367 warn( "- no $auxFileName available" ) if( $verbosity >= 2 );
66 37         313 return 1;
67             }
68            
69 1         22 $this->{auxParser}->parseAuxFile( $auxFileName );
70 1         10 $this->{auxDB} = $this->{auxParser}->database();
71 1         5 $SpeL::Object::Command::labelhash = $this->{auxDB}->{newlabel};
72 1         4 $SpeL::Object::Command::citationhash = $this->{auxDB}->{bibcite};
73              
74 1 50       4 if ( $test ) {
75 1         19 say STDOUT Data::Dumper->Dump( [ $this->{auxDB} ] , [ qw(auxDB) ] );
76 1         0 return 0;
77             }
78 0         0 return 1;
79             }
80              
81              
82              
83              
84             sub parseChunks {
85 37     37 1 98 my $this = shift;
86 37         133 my ( $verbosity, $test, $debug ) = @_;
87 37 50       152 $debug =~ s/\.tex$// if( defined( $debug ) );
88            
89 37         598 my ( $volume, $path, $file ) = File::Spec->splitpath( $this->{argument} );
90              
91 37         371 my $spelIdxFileName = File::Spec->catpath( $volume, $path, $file . '.spelidx' );
92 37 50       2465 unless( -r $spelIdxFileName ) {
93 0 0       0 warn( "- no $spelIdxFileName available" ) if( $verbosity >= 2 );
94 0         0 return;
95             }
96              
97             # count number of lines in spel file
98 37         130 my $nrLines = 0;
99 37         79 my $chunks;
100 37         286 my $spelIdxFile = _openIOFile( "$spelIdxFileName", '<', "input file" );
101 37         3327 while( my $line = <$spelIdxFile> ) {
102 229 100       1272 unless( $line =~ /(?:^format)|(?:^audiodir)|(?:^server)|(?:^chunkdir)|(?:^mac)|(?:^env)|(?:^language)/ ) {
103             # count number of chunks to read
104 37         100 ++$nrLines;
105             # register in database
106 37         124 chomp( $line );
107 37         278 my ($label, $rest ) = split( /\|/, $line );
108 37         467 $chunks->{$rest} = 1;
109             }
110             }
111 37         502 $spelIdxFile->close();
112              
113 37 50       897 die( "Error: found no chunks to read" ) unless $nrLines;
114            
115             # open the playlist
116 37         493 my $m3uFileName = File::Spec->catpath( $volume, $path, $file . '.m3u' );
117 37         189 my $m3uFile = _openIOFile( $m3uFileName, '>', 'playlist' );
118 37         194 print $m3uFile
119             "#EXTM3U\n" .
120             "#EXTINF: Playlist for audiobook generated with SpeLbox\n";
121              
122             # parse spel file
123 37         101 my $linenr = 0;
124 37         192 $spelIdxFile = _openIOFile( "$spelIdxFileName", '<', 'input file' );
125              
126 37         200 my $tts = $this->{config}->{engine}->{tts};
127 37         456 my $audiodir;
128             my $chunkdir;
129 37         0 my $server;
130 37         0 my $exec;
131 37         0 my $format;
132 37         0 my $language;
133 37         0 my $languagetag;
134 37         0 my $voice;
135              
136 37         97 my $m3u_db = {};
137 37         77 my $m3u_db_active = [];
138              
139 37         77 my $fullFilePath;
140              
141 37         85 my $ran_at_least_once = 0;
142 37         621 while( my $line = <$spelIdxFile> ) {
143              
144             # parse the line
145 229         483 chomp $line;
146 229         830 my ($label, $rest ) = split( /\|/, $line );
147            
148 229 100       623 if ( $label eq 'format' ) {
149 37         128 $format = $rest;
150 37 50       1055 if( -r "$FindBin::Bin/$tts" ) {
151 37         120 $exec = "$FindBin::Bin/$tts";
152             }
153             else {
154 0         0 die( "Error: cannot find text-to-speech engine '$tts'" );
155             }
156 37         183 next;
157             }
158            
159 192 100       509 if ( $label eq 'language' ) {
160 37         121 $language = $rest;
161 37         166 $languagetag = $this->{config}->{languagetags}->{$language};
162            
163 37 50       713 $SpeL::I18n::lh = SpeL::I18n->get_handle( $languagetag )
164             or die( "Error: I'm not capable of reading the language '$language'\n" );
165            
166             die( "Error: engine '$tts' is not configured with a voice for language '$language'\n" )
167 37 50       7992 unless exists $this->{config}->{voices}->{$language};
168 37         140 $voice = $this->{config}->{voices}->{$language};
169 37         299 next;
170             }
171            
172 155 100       360 if ( $label eq 'audiodir' ) {
173 37         94 $audiodir = $rest;
174 37         2379 mkpath( $audiodir );
175 37         211 next;
176             }
177              
178 118 50       301 if ( $label eq 'server' ) {
179 0         0 $server = $rest;
180 0         0 next;
181             }
182              
183 118 100       289 if ( $label eq 'chunkdir' ) {
184 37         95 $chunkdir = $rest;
185 37         140 next;
186             }
187              
188 81 50       252 if ( $label eq 'envpp' ) {
189 0         0 my ( undef, $env, $argcount, $optarg, $replacement ) = split( /\|/, $line );
190            
191 0         0 push @{$SpeL::Parser::Chunk::prepenvlist},
  0         0  
192             {
193             env => $env,
194             argc => $argcount,
195             optarg => $optarg,
196             replacement => $replacement,
197             };
198            
199 0         0 next;
200             }
201            
202 81 100       224 if ( $label eq 'macpp' ) {
203 15         59 my ( undef, $macro, $argcount, $optarg, $replacement ) = split( /\|/, $line );
204             # a star must be a literal star, not a regexp star
205 15         40 $macro =~ s/\*/\\*/;
206 15         32 push @{$SpeL::Parser::Chunk::prepmacrolist},
  15         90  
207             {
208             macro => $macro,
209             argc => $argcount,
210             optarg => $optarg,
211             replacement => $replacement,
212             };
213            
214 15         54 next;
215             }
216            
217 66 100       221 if ( $label eq 'macad' ) {
218 29         120 my ( undef, $macro, $argcount, $optarg, $reader ) = split( /\|/, $line );
219 29         196 $SpeL::Object::Command::macrohash->{$macro} =
220             {
221             argc => $argcount,
222             optarg => $optarg,
223             reader => $reader,
224             };
225 29         143 next;
226             }
227            
228 37 50       169 if ( $label eq 'envad' ) {
229 0         0 my ( undef, $env, $argcount, $optarg, $pre, $post )
230             = split( /\|/, $line );
231 0         0 $SpeL::Object::Environment::environmenthash->{$env} =
232             {
233             argc => $argcount,
234             optarg => $optarg,
235             pre => $pre,
236             post => $post,
237             };
238 0         0 next;
239             }
240              
241 37         107 my $filetoread = $rest;
242              
243 37 50 33     163 next if( defined( $debug ) and ( $debug ne $filetoread ) );
244 37         131 $ran_at_least_once = 1;
245            
246 37 50       306 die( "Error: $spelIdxFileName damaged - format not specified\n" )
247             unless defined $format;
248 37 50       421 die( "Error: $spelIdxFileName damaged - audio directory not specified\n" )
249             unless defined $audiodir;
250 37 50       127 die( "Error: $spelIdxFileName damaged - reader directory not specified\n" )
251             unless defined $chunkdir;
252 37 50       459 die( "Error: $spelIdxFileName damaged - language not specified\n" )
253             unless defined $language;
254              
255             # make the path OS ready
256 37         2118 $fullFilePath = File::Spec->catpath( $volume, $path,
257             File::Spec->catfile( $audiodir, split( /\//, $filetoread ) ));
258              
259             # read the text from the chunk file
260 37         1439 my $chunkFileName = File::Spec->catpath( $volume, $path,
261             File::Spec->catfile( $chunkdir, $filetoread . ".tex") );
262              
263 37 50       706 say STDERR '- Treating ' . pack( "A56", $chunkFileName ) if ( $verbosity >= 1 );
264              
265 37 50       4591 print STDERR
266             " Parsing " . pack( "A50", $fullFilePath . ".tex" ) .
267             sprintf( "[%3d%%]\r", 100 * $linenr / $nrLines )
268             if( $verbosity >= 1 );
269            
270 37         1731 $this->{chunkParser}->parseDocument( $chunkFileName, $debug );
271              
272 37 50       2921 print STDERR
273             " Parsed " . pack( "A50", $fullFilePath . ".tex" ) .
274             sprintf( "[%3d%%]\r", 100 * $linenr / $nrLines )
275             if( $verbosity >= 1 );
276              
277 37         153 my $text;
278 37         140 foreach( $label ) {
279             /^title$/ and do
280 37 100       439 {
281 1         20 $text = $SpeL::I18n::lh->maketext( 'title' ) . ": ";
282 1         159 next;
283             };
284             /^author$/ and do
285 36 50       246 {
286 0         0 $text = $SpeL::I18n::lh->maketext( 'author' ) . ": ";
287 0         0 next;
288             };
289             /^part\s+(.*)/ and do
290 36 50       221 {
291 0         0 $text = $SpeL::I18n::lh->maketext( 'part' ) . " $1: ";
292 0         0 next;
293             };
294             /^chapter\s+(.*)/ and do
295 36 50       289 {
296 0         0 $text = $SpeL::I18n::lh->maketext( 'chapter' ) . " $1: ";
297              
298             # make the activestack empty again
299 0         0 @$m3u_db_active = ();
300            
301 0         0 next;
302             };
303             /^((?:sub)*section)\s+(.*)/ and do
304 36 100       169 {
305 1         5 my ( $level, $label ) = ($1, $2);
306             # count the number of matches of sub in the section
307 1         4 my $count = () = $level =~ /sub/g;
308              
309             # pop as many actives of the activestack until stack has appropriate length
310 1         4 pop @$m3u_db_active while( $count < $#$m3u_db_active );
311              
312             # register yourself on the activestack
313 1         3 $m3u_db_active->[$count] = $rest;
314            
315             # make sure every chunk and section registers on the m3u_db for all activestack levels
316             # see elsewhere in the code
317              
318             # generate the level and labe text
319 1         13 $text = $SpeL::I18n::lh->maketext( $level ) . " $label: ";
320              
321             # say STDERR Data::Dumper->Dump( [ $m3u_db_active ], [ qw(dba) ] );
322            
323 1         120 next;
324             };
325             /^((?:sub)?paragraph)$/ and do
326 35 50       228 {
327 0         0 my $level = $1;
328            
329             # count the number of matches of sub in the paragraph and
330             # add three to it, as 0, 1 an 2 are taken by the sections,
331             # therefore a paragraph is 3 and a subparagraph 4
332 0         0 my $count = () = $level =~ /sub/g;
333 0         0 $count += scalar @{$m3u_db_active};
  0         0  
334            
335             # pop as many actives of the activestack until stack has appropriate length
336 0         0 pop @$m3u_db_active while( $count < $#$m3u_db_active );
337              
338             # register yourself on the activestack
339 0         0 $m3u_db_active->[$count] = $rest;
340            
341             # make sure every chunk and section registers on the m3u_db for all activestack levels
342             # see elsewhere in the code
343              
344             # a paragraph is not numbered and therefore does not need a lead-in.
345 0         0 $text = '';
346              
347             # say STDERR Data::Dumper->Dump( [ $m3u_db_active ], [ qw(dba) ] );
348            
349 0         0 next;
350             };
351             /^footnote\s+(.*)/ and do
352 35 50       217 {
353 0         0 $text = $SpeL::I18n::lh->maketext( 'footnote' ) . " $1: ";
354 0         0 next;
355             };
356             }
357              
358 37 50       508 say STDERR Data::Dumper->Dump( [ $this->{chunkParser}->object()->{tree}->{ElementList} ],
359             [ qw( Parsetree ) ] )
360             if( $test );
361            
362 37         33538 $text .= $this->{chunkParser}->object()->{tree}->{ElementList}->read(0);
363             ## clean up:
364             # double spaces
365 37         1162 $text =~ s/\s+/ /g;
366             # trailing spaces
367 37         531 $text =~ s/\s+$//;
368             # trailing comma
369 37         152 $text =~ s/,$//;
370              
371 37 50       200 if ( $test ) {
372 37         334 say STDOUT $text;
373 37         4228 return 0;
374             }
375             else {
376             # preprocess the file if there is a translation file
377 0         0 my $canonicalvoice = $voice;
378 0         0 $canonicalvoice =~ s/:/-/;
379 0         0 my $trfilename =
380             File::Spec->catfile( File::ShareDir::dist_dir( 'SpeL-Wizard' ),
381             "$tts-$canonicalvoice.tr" );
382            
383 0 0       0 if ( -r $trfilename ) {
384 0         0 my $trf = _openIOFile( $trfilename, "<", "translation file" );
385 0         0 while ( my $line = <$trf> ) {
386 0         0 chomp( $line );
387 0         0 $line =~ s/^\s+|\s+$//g;
388 0         0 my ( $key, $replace ) = split( /\s*:=\s*/, $line );
389 0 0       0 next unless( defined $replace );
390 0         0 $text =~ s/$key/$replace/gi;
391             }
392             }
393              
394              
395 0         0 my $text_md5_hex = "";
396 0 0       0 if ( defined( $text ) )
397             {
398 0         0 $text_md5_hex = md5_hex( $text ) . "-" . $languagetag;
399             }
400             else
401             {
402 0         0 $text = "";
403 0         0 $text_md5_hex = "";
404 0         0 warn( pack( "A74", "Warning: parser error on `$file'" ) . "\n" );
405             }
406              
407             # read existing MD5 sum
408 0         0 my $MD5SumFile = IO::File->new();
409 0         0 my $md5sum = "";
410 0 0       0 if ( $MD5SumFile->open( "<$fullFilePath.md5" ) )
411             {
412 0         0 $md5sum = <$MD5SumFile>;
413 0         0 $MD5SumFile->close();
414 0 0       0 $md5sum = "" unless defined( $md5sum );
415             }
416             else
417             {
418 0         0 _writeToFile( "$fullFilePath.md5", $text_md5_hex );
419             }
420              
421 0 0       0 if ( $md5sum ne $text_md5_hex )
422             {
423 0 0       0 print STDERR
424             " Creating " . pack( "A50", $fullFilePath . ".spel" ) .
425             sprintf( "[%3d%%]\r", 100 * $linenr++ / $nrLines )
426             if( $verbosity >= 1 );
427              
428             # write spel file to disk
429 0         0 _writeToFile( "$fullFilePath.spel", $text );
430 0         0 my $command = [
431             "perl",
432             "$exec",
433             "$fullFilePath.spel",
434             "$fullFilePath.$format",
435             "$voice" ];
436 0         0 my $out;
437 0 0       0 IPC::Run::run( $command, '>', \$out )
438             or die( "Error: could not start '$exec' with voice '$voice' " .
439             "(exit value $?)\n" );
440             # only write md5 file if audio generation was successful
441 0 0       0 if( -e "$fullFilePath.$format" ) {
442 0         0 _writeToFile( "$fullFilePath.md5", $text_md5_hex );
443             }
444             else {
445 0         0 die( "Error: audio generation was not successful.\n" .
446             " Check your text-to-speech setup\n" );
447             }
448             }
449             else
450             {
451 0 0       0 print STDERR
452             " Reusing " . pack( "A50", $fullFilePath . ".spel" ) .
453             sprintf( "[%3d%%]\r", 100 * $linenr++ / $nrLines )
454             if( $verbosity >= 1 );
455             }
456              
457             # update the section m3u database
458 0         0 for( my $i = 0; $i < @$m3u_db_active; ++$i ) {
459 0         0 push @{$m3u_db->{$m3u_db_active->[$i]}}, "$filetoread.$format";
  0         0  
460             }
461            
462             # update the global m3u file
463             # deze moet aangepast worden voor server-use
464 0 0       0 print $m3uFile ( ( $server eq 'local' ) ? "" : "$server/" ) . "$fullFilePath.$format\n";
465             }
466             }
467 0 0       0 die( "Error: nothing to read (wrong debug request?)\n" ) unless( $ran_at_least_once );
468            
469 0         0 $spelIdxFile->close();
470 0         0 $m3uFile->close();
471              
472 0 0       0 print STDERR
473             " Parsing " . pack( "A50", $fullFilePath . ".tex" ) .
474             sprintf( "[%3d%%]\r", 100 )
475             if( $verbosity >= 1 );
476              
477 0         0 say STDERR "- Generating m3u playlists";
478            
479             # write all m3u files corresponding to the (sub)sections
480 0         0 foreach my $key (sort keys %$m3u_db ) {
481 0         0 my $list = $m3u_db->{$key};
482 0         0 my $fn = File::Spec->catpath( $volume, $path,
483             File::Spec->catfile( $audiodir, $key . ".m3u" ) );
484 0         0 say STDERR " - Generating $fn";
485 0 0       0 if ( $server eq 'local' ) {
486 0         0 _writeToFile( $fn,
487             "#EXTM3U\n" .
488             "#EXTINF: section playlist generated with SpeLbox\n" .
489             join( "\n", @$list ) );
490             }
491             else {
492             _writeToFile( $fn,
493             "#EXTM3U\n" .
494             "#EXTINF: section playlist generated with SpeLbox\n" .
495 0         0 join( "\n", map { "$server/$audiodir/" . $_; } @$list ) );
  0         0  
496             }
497             }
498             # say STDERR Data::Dumper->Dump( [ $m3u_db ], [ qw( m3u_db ) ] );
499              
500            
501             # clean the speech directory from old files that are obsolete
502 0         0 say STDERR "- Cleaning directory " . pack( "A50", "'$audiodir'" );
503 0         0 my $audiodirpath =
504             File::Spec->catpath( $volume, $path,
505             File::Spec->catfile( $audiodir, '' ) );
506 0         0 my $audiodirectoryglob =
507             File::Spec->catpath( $volume, $path,
508             File::Spec->catfile( $audiodir, "*" ) );
509 0         0 foreach my $file ( glob( $audiodirectoryglob ) ) {
510 0         0 my $basename = $file;
511 0         0 $basename =~ s/^$audiodirpath//;
512 0         0 $basename =~ s/\\?(.*)\.(?:tex|spel|md5|m3u|ogg|mp3|wav)$/$1/;
513 0 0       0 unless( exists $chunks->{$basename} ) {
514 0         0 say STDERR " - Deleting $file because obsolete";
515 0 0       0 unlink $file if ( -e $file );
516             }
517             }
518             }
519              
520             sub _openIOFile {
521 111     111   406 my ( $fileName, $direction, $fileDesc ) = @_;
522 111         815 my $file = IO::File->new();
523 111 0       5096 $file->open( "$direction$fileName" )
    50          
524             or die( "Error: cannot open $fileDesc `$fileName' for "
525             . ( ( $direction eq '<' ) ? "reading" : "writing" ) );
526 111         12299 return $file;
527             }
528              
529             sub _writeToFile {
530 0     0     my ( $fileName, $text ) = @_;
531 0           my $file = IO::File->new();
532              
533 0 0         $file->open( ">$fileName" )
534             or die( "Error: cannot open file `$fileName' for writing\n" );
535 0           print $file $text;
536 0           $file->close();
537             }
538              
539              
540              
541             1;
542              
543             __END__