Ένα χαρακτηριστικό φωναχτής ανάγνωσης (read-aloud) έχει μία ορατή δουλειά πέρα από τη φωνή: καθώς εκφωνείται κάθε λέξη, πρέπει να φωτίζει αυτήν τη λέξη στη σελίδα και να την κρατά σε οπτική επαφή. Για να το κάνετε αυτό, χρειάζεστε το πλαίσιο οριοθέτησης (bounding box) κάθε λέξης, με ευρετήριο στην ίδια ροή χαρακτήρων από την οποία διαβάζει ο κινητήρας ομιλίας. Αν έχετε τα κουτιά αλλά χάσετε την ευρετηρίαση, η επισήμανση παρασύρεται μια ή δύο λέξεις πίσω από τον ήχο· αν έχετε την ευρετηρίαση αλλά κακομεταχειριστείτε την κατάσταση της σελίδας, η επισήμανση προσγειώνεται σε εντελώς λάθος σελίδα. Το τμήμα της ομιλίας από αυτό, ο ίδιος ο συνθέτης (synthesizer), είναι το τμήμα που σπάνια σπάει. Το SAPI αναφέρει τα όρια των λέξεων στον χαρακτήρα. Αυτό που σπάει είναι το λεπτό στρώμα αντιστοίχισης μεταξύ μιας μετατόπισης (offset) χαρακτήρα στο buffer ομιλίας και ενός ορθογωνίου στη σελίδα που έχει αποδοθεί
Το PDFium Component παρέχει αυτή την αντιστοίχιση για Delphi, C++Builder και Lazarus, με τα κουτιά λέξεων (word boxes) να είναι διαθέσιμα από την έκδοση v1.53 και τον κέρσορα παρακολούθησης (tracking cursor) από την v1.56. Η επιφάνεια είναι σκόπιμα στενή: μια κλήση που επιστρέφει τα κουτιά λέξεων για μια σελίδα, ένας ιχνηλάτης (tracker) που μετατρέπει μια μετατόπιση (offset) χαρακτήρα σε βαμμένη επισήμανση (highlight), και μερικές ιδιότητες για χρώμα και αυτόματη κύλιση. Όσο στενή κι αν είναι, η σειρά με την οποία καλείτε τα πράγματα αποφασίζει εάν η δυνατότητα λειτουργεί, και οι περισσότερες από τις αποτυχίες παρακάτω προέρχονται από την κλήση των σωστών συναρτήσεων με λάθος σειρά
Οι χαρακτήρες δεν είναι λέξεις και οι κινητήρες TTS μιλούν σε χαρακτήρες
Ένας κινητήρας ομιλίας καταναλώνει μια επίπεδη συμβολοσειρά (flat string) και αναφέρει την πρόοδο ως θέσεις χαρακτήρων μέσα σε αυτήν τη συμβολοσειρά. Μια σελίδα PDF έχει γλυφικά τοποθετημένα στον χώρο της σελίδας, όπου μια "λέξη" είναι ένα ευρετικό σύμπλεγμα (heuristic cluster) εκτελέσεων γλυφικών. Τα δύο συστήματα συντεταγμένων δεν μοιράζονται τίποτα, εκτός εάν το κείμενο που παραδίδετε στον συνθέτη είναι byte προς byte το κείμενο από το οποίο υπολογίστηκαν τα κουτιά λέξεων. Αυτός είναι ο πρώτος κανόνας και είναι ασυγχώρητος. Αν κανονικοποιήσετε τα κενά διαστήματα (whitespace), αφαιρέσετε τα μαλακά ενωτικά (soft hyphens) ή "καθαρίσετε" με άλλο τρόπο το εξαγόμενο κείμενο πριν το εκφωνήσετε, κάθε μετατόπιση (offset) κατάντη θα είναι σιωπηλά λάθος. Εκφωνήστε ακριβώς αυτό που εξάγατε ή διατηρήστε έναν ρητό πίνακα επαναντιστοίχισης (remapping) μετατόπισης. Δεν υπάρχει τρίτη επιλογή που να επιβιώνει σε πραγματικά έγγραφα
Ο πίνακας επαναντιστοίχισης (remapping) δεν είναι μια υποθετική οριακή περίπτωση (edge case). Τη στιγμή που το UI σας εισάγει μια προφορική ανακοίνωση σελίδας ("σελίδα πέντε") ή επεκτείνει μια συντομογραφία για τον συνθέτη, η προφορική συμβολοσειρά αποκλίνει από την εξαγόμενη. Καταγράψτε τη θέση και το μήκος κάθε εισαγωγής, και στη συνέχεια αφαιρέστε τη συσσωρευμένη προσαρμογή πριν από κάθε κλήση παρακολούθησης (tracking). Είναι ίσως είκοσι γραμμές τήρησης βιβλίων (bookkeeping) και είναι η διαφορά μεταξύ μιας επισήμανσης (highlight) που επιβιώνει από το επόμενο αίτημα λειτουργίας και μίας που σπάει την πρώτη φορά που κάποιος ζητά προφορικές επικεφαλίδες
Τι σας δίνει ένα κουτί λέξης (word box)
Κάθε εγγραφή TPdfWordBox φέρει το κείμενο της λέξης, τον αρχικό δείκτη (StartIndex) και το πλήθος (Count) χαρακτήρων εντός του κειμένου της σελίδας, ένα Rect στο χώρο της σελίδας, και τον αριθμό σελίδας (Page) με βάση το 1. Το πεδίο StartIndex είναι η γέφυρα μεταξύ των δύο συστημάτων συντεταγμένων: είναι η ίδια μετατόπιση (offset) που θα παραδώσει το SAPI καθώς διαβάζει. Το PageWordBoxes επιστρέφει τον πλήρη πίνακα για την ενεργή σελίδα:
procedure TReaderForm.PreparePage(PageNo: Integer);
begin
PdfView.PageNumber := PageNo; // the view's word boxes track its displayed page
FWords := PdfView.PageWordBoxes;
FPageText := BuildSpeechText(FWords); // concatenate Word.Text in order
if Length(FWords) = 0 then
HandleImageOnlyPage(PageNo); // a scan with no text layer
end;
Το σχόλιο σχετικά με τη σειρά έχει μεγάλη σημασία (load-bearing). Το PageWordBoxes του προγράμματος προβολής δημιουργεί μάρκες (tokenizes) στο επίπεδο κειμένου της σελίδας που εμφανίζει τη δεδομένη στιγμή η προβολή, επομένως πλοηγηθείτε πρώτα στην προβολή και εξάγετε δεύτερον· δεν απαιτείται απόδοση (rendering), μόνο ένα ανοιχτό έγγραφο. (Το στοιχείο (component) εγγράφου, TPdf, εκθέτει τα δικά του PageWordBoxes συνδεδεμένα (keyed) με το Pdf.PageNumber για ακέφαλη (headless) χρήση. Οι δύο αριθμοί σελίδων είναι ανεξάρτητοι, κάτι που αποτελεί δική του παγίδα). Ένα κενό αποτέλεσμα σε μια σελίδα που φέρει ορατά περιεχόμενο σημαίνει μια σάρωση μόνο με εικόνα. Προωθήστε το στο OCR, ή τουλάχιστον ανακοινώστε το ("η σελίδα 4 δεν περιέχει αναγνώσιμο κείμενο"), αντί να αφήσετε τη φωνή να σιγήσει χωρίς καμία εξήγηση
Σύνδεση των ορίων λέξεων SAPI με τον ιχνηλάτη (tracker)
Το TrackReadingWordAt, στο πρόγραμμα προβολής, είναι ο μεντεσές όλου του χαρακτηριστικού. Δώστε του έναν αριθμό σελίδας και έναν δείκτη χαρακτήρα· βρίσκει το κουτί της λέξης (word box) που περιέχει αυτόν τον χαρακτήρα, ζωγραφίζει τον κέρσορα ανάγνωσης σε αυτό, και επιστρέφει τον δείκτη της λέξης, ή -1 όταν ο δείκτης πέφτει ανάμεσα σε λέξεις. Η ειδοποίηση ορίου λέξης (word-boundary notification) του SAPI παρέχει ακριβώς τη θέση χαρακτήρα που θέλει:
procedure TReaderForm.OnSpeechWordBoundary(StreamPos: Integer);
var
WordIdx: Integer;
begin
// Maps the offset to a word box and moves the highlight in one call
WordIdx := PdfView.TrackReadingWordAt(FPageNo, StreamPos);
if WordIdx < 0 then
Exit; // boundary fell outside any word: keep last highlight
end;
Δύο αμυντικές λεπτομέρειες κερδίζουν τη θέση τους εδώ. Πρώτον, το TrackReadingWordAt διατηρεί τη δική του κρυφή μνήμη (cache) κουτιών λέξεων για την παρακολουθούμενη σελίδα, η οποία ανακατασκευάζεται αυτόματα όταν αλλάζει η σελίδα, ώστε το κόστος ανά όριο να παραμένει σταθερό, ανεξάρτητα από το πόσο γρήγορα φτάνουν τα όρια. Δεύτερον, δεν ελέγχει τα όρια (bounds-check) γενναιόδωρα. Ένας δείκτης ίσος ή μεγαλύτερος από το πλήθος χαρακτήρων της σελίδας επιστρέφει -1 αντί να περιοριστεί (clamping) στην τελευταία λέξη. Αντιμετωπίστε το -1 ως "διατηρήστε την προηγούμενη επισήμανση", ποτέ ως σφάλμα, επειδή οι εκτελέσεις (runs) σημείων στίξης και τα κενά διαστήματα μεταξύ των λέξεων παράγουν νομίμως όρια που δεν ανήκουν σε καμία λέξη. Αν καταγράφετε κάθε -1, θα ταφείτε. Αντίθετα, μετρήστε τα ανά σελίδα και εξετάστε προσεκτικά κάθε σελίδα όπου η αναλογία αυξάνεται κατακόρυφα (spikes), καθώς αυτό συνήθως σημαίνει μια αναντιστοιχία κανονικοποίησης κειμένου που παραπέμπει πίσω στον πρώτο κανόνα
Ο ίδιος ο κέρσορας: χρώμα, παρακολούθηση (follow) και εκκαθάριση
Η SetReadingWord χρωματίζει απευθείας την επισήμανση (highlight) όταν κρατάτε εσείς οι ίδιοι το κουτί της λέξης (word box), η ReadingWordColor του δίνει στυλ, και η ReadingWordFollow := True κυλά την προβολή ακριβώς τόσο ώστε να διατηρείται ορατή η εκφωνούμενη λέξη. Αυτή η τελευταία ιδιότητα κερδίζει τη θέση της. Μια χειροκίνητη κύλιση "κεντραρίσματος της τρέχουσας λέξης" κάνει τη σελίδα να κλυδωνίζεται σε κάθε αλλαγή γραμμής, και οι αναγνώστες με ευαισθησία στην κίνηση (motion-sensitive) θα απενεργοποιήσουν ολόκληρο το χαρακτηριστικό μέσα σε ένα λεπτό. Η επισήμανση αποδίδεται μόνο στη σελίδα που εμφανίζεται αυτήν τη στιγμή στο ενεργό TPdfView, επομένως η ανάγνωση πολλών σελίδων πρέπει να προωθεί το PageNumber συγχρονισμένα με την ομιλία, και στη συνέχεια να εκτελεί ξανά το βήμα προετοιμασίας για τη νέα σελίδα πριν φτάσει το πρώτο συμβάν ορίου. Αν το παραλείψετε αυτό, οι πρώτες επισημάνσεις σε κάθε σελίδα θα δείχνουν σε ξεπερασμένες (stale) συντεταγμένες
procedure TReaderForm.StopReading;
begin
FVoice.Stop; // halt SAPI playback first
PdfView.ClearReadingWord; // then remove the highlight; a stale cursor reads as a bug
end;
Η συμμετρία στον τερματισμό είναι αυτό που διατηρεί την επισήμανση ειλικρινή. Κάθε μονοπάτι παύσης, διακοπής και αλλαγής σελίδας πρέπει να τελειώνει σε ClearReadingWord. Αν το παραλείψετε, ένα κεχριμπαρένιο ορθογώνιο θα κάθεται σε μια σταματημένη σελίδα και θα μοιάζει ακριβώς με ελάττωμα, το οποίο είναι το είδος του προβλήματος που κάθε ελεγκτής (tester) θα καταχωρήσει παρόλο που τίποτα δεν είναι πραγματικά σπασμένο
Ο ρυθμός ομιλίας ασκεί μεγαλύτερη πίεση (stresses) σε αυτήν τη διοχέτευση (pipeline) από ό,τι το μέγεθος του εγγράφου. Στις 300 λέξεις ανά λεπτό, τα συμβάντα ορίων φτάνουν κάθε 200 ms, και στις ταχύτερες ταχύτητες του SAPI έρχονται πιο γρήγορα από ό,τι μπορεί να παρακολουθήσει άνετα το μάτι. Η σωστή απόκριση είναι να συνενώνετε (coalesce), όχι να βάζετε σε ουρά (queue). Αν φτάσει ένα νέο όριο ενώ εκκρεμεί ακόμα μια ενημέρωση επισήμανσης (highlight), απορρίψτε την παλιά (stale) και ζωγραφίστε την πιο πρόσφατη. Ένας κέρσορας που επισκέπτεται κάθε λέξη με τη σειρά αλλά καθυστερεί μισό δευτερόλεπτο δίνει την αίσθηση ότι είναι χαλασμένος· ένας που περιστασιακά παραλείπει μια λέξη παραμένοντας συγχρονισμένος με τη φωνή, όχι
Οριακές περιπτώσεις (Edge cases) που χωρίζουν τα demo από τα προϊόντα
Μερικές κατηγορίες εγγράφων αποκαλύπτουν τις ραφές. Οι συνδυαστικοί χαρακτήρες (combining characters) είναι οι πιο ανεπαίσθητοι: ακολουθίες Unicode, όπως ένα βασικό γράμμα συν ένα συνδυαστικό διακριτικό, μπορούν να καταλάβουν περισσότερους δείκτες χαρακτήρων (character indices) από ό,τι υποδηλώνει η οπτική λέξη, επομένως οποιαδήποτε αριθμητική μετατόπισης (offset arithmetic) που προϋποθέτει έναν δείκτη ανά γλυφικό μετατοπίζεται (drifts) σιγά-σιγά. Αυτό είναι το ισχυρότερο επιχείρημα για να αφήσετε την TrackReadingWordAt να αναλάβει την αντιστοίχιση, αντί να υπολογίζετε τους αριθμούς λέξεων με το χέρι. Ο συλλαβισμός (hyphenation) είναι πιο κοινός αλλά και πιο συνηθισμένος: μια λέξη που σπάει σε μια αλλαγή γραμμής γίνεται δύο κουτιά (boxes), και αν την εκφωνήσετε ως ενιαίο σύμβολο (token), το συμβάν ορίου για το δεύτερο μισό της καταλήγει στο πρώτο κουτί. Αυτό είναι συνήθως εντάξει, αλλά είναι μια απόφαση, οπότε πάρτε την επίτηδες αντί να την ανακαλύψετε. Η προσθήκη ετικετών (tagging) αλλάζει την ίδια τη σειρά ανάγνωσης. Όταν ένα έγγραφο φέρει σωστές ετικέτες δομής (η επικράτεια του ISO 14289, PDF/UA), η αλληλουχία λέξεων ακολουθεί τη λογική δομή· χωρίς αυτές επιστρέφει (falls back) στην ευρετική (heuristics) της διάταξης, και μια μη επισημασμένη (untagged) σελίδα δύο στηλών μπορεί να διαβαστεί ευθεία και στις δύο στήλες. Οι περιστρεφόμενες σελίδες είναι η τελευταία συνηθισμένη περίπτωση: το Rect κάθε λέξης την οριοθετεί ακόμα σωστά στον χώρο της σελίδας, αλλά μια πολιτική παρακολούθησης προβολής (viewport-follow) ρυθμισμένη για οριζόντια ροή κυλά με κραδασμούς (scrolls jarringly) όταν το κείμενο τρέχει κάθετα, επομένως διατηρήστε τουλάχιστον ένα περιστρεφόμενο έγγραφο στο σύνολο δοκιμών παλινδρόμησης (regression set). Για τον χειρισμό της σειράς ανάγνωσης, τις μονάδες σε επίπεδο πρότασης μέσω του ReadingUnits και την ευρύτερη υποστηρικτική στοίβα (assistive stack), δείτε τη δημιουργία ενός προσβάσιμου προγράμματος ανάγνωσης PDF στο Delphi
Ένας περιορισμός της πλατφόρμας διαμορφώνει την ανάπτυξη (deployment). Το SAPI αφορά μόνο τα Windows. Το API κουτιού λέξης (word-box) και παρακολούθησης (tracking) είναι πανομοιότυπο byte-προς-byte στο Lazarus και το FPC, αλλά οι εκδόσεις (builds) Linux και macOS χρειάζονται έναν διαφορετικό συνθέτη συνδεδεμένο πίσω από τα ίδια συμβάντα ορίων· αυτή η ρύθμιση καλύπτεται στην ενότητα εκτέλεση του προγράμματος προβολής στο Lazarus και το FPC. Το κόστος επισήμανσης αλληλεπιδρά επίσης με την κρυφή μνήμη (cache) της σελίδας σας μόλις ανεβούν οι ρυθμοί ομιλίας, και η αριθμητική προϋπολογισμού (budget arithmetic) στην προσωρινή αποθήκευση απόδοσης (render caching) και την απόδοση ζουμ (zoom performance) μεταφέρεται εδώ χωρίς αλλαγή
Όταν η επισήμανση (highlighting) μίας λέξης είναι η λάθος λεπτομέρεια (granularity)
Το καραόκε σε επίπεδο λέξης δεν είναι πάντα αυτό που θέλει ένας αναγνώστης. Σε υψηλούς ρυθμούς ομιλίας, το τρεμόπαιγμα (flickering) του κέρσορα λέξη προς λέξη γίνεται από μόνο του ένας οπτικός θόρυβος, και ορισμένοι ακροατές παρακολουθούν μια πρόταση πιο άνετα από ό,τι ένα στροβοσκοπικό (strobe) φως μεμονωμένων λέξεων. Για αυτήν την περίπτωση το στοιχείο (component) εκθέτει μια πιο χονδροειδή μονάδα (coarser unit). Η ιδιότητα ReadingUnits επιστρέφει μονάδες σε επίπεδο πρότασης και μπλοκ, καθεμία με τα δικά της ορθογώνια επισήμανσης (highlight rectangles), και τις ζωγραφίζετε με την SetReadingHighlight αντί για την SetReadingWord. Η συνδεσμολογία (wiring) έχει το ίδιο σχήμα: μια μετατόπιση ορίου εξακολουθεί να οδηγεί το ποια μονάδα φωτίζεται, αλλά η μονάδα που επισημαίνετε εκτείνεται σε μια ρήτρα (clause) ή μια γραμμή αντί για μια ενιαία μάρκα (token). Οι πιο αργοί αναγνώστες και η αναπαραγωγή με υψηλό ρυθμό (high-rate) τείνουν και οι δύο να το προτιμούν, και τίποτα δεν σας εμποδίζει να προσφέρετε και τις δύο λειτουργίες πίσω από μια ρύθμιση
Αξίζει να σημειώσετε τα κατώτατα όρια εκδόσεων (version floors) προτού χτίσετε πάνω σε αυτό: τα κουτιά λέξεων (word boxes) χρειάζονται PDFium Component v1.53 ή νεότερη έκδοση, και ο κέρσορας παρακολούθησης χρειάζεται την v1.56. Το πλήρες API ανάγνωσης, οι μονάδες σε επίπεδο πρότασης και ένα λειτουργικό demo φωναχτής ανάγνωσης (read-aloud demo) βρίσκονται στη σελίδα προϊόντος για το PDFium Component