branch: externals/greader
commit 5a7e9cdfa3b8cb7f6a5dd5ef5e87daca148ea27c
Author: Michelangelo Rodriguez <michelangelo.rodrig...@gmail.com>
Commit: Michelangelo Rodriguez <michelangelo.rodrig...@gmail.com>

    greader version 0.10.0
    
    Module greader-audiobook.el added, please see the commentary for more 
information.
---
 greader-audiobook.el | 310 ++++++++++++++++++++++++++++++++++++++++++++++++---
 greader.el           |   2 +-
 2 files changed, 297 insertions(+), 15 deletions(-)

diff --git a/greader-audiobook.el b/greader-audiobook.el
index 98cc0130c1..9458390ef5 100644
--- a/greader-audiobook.el
+++ b/greader-audiobook.el
@@ -1,27 +1,25 @@
-;; greader-audiobook.el: Converts buffers into audio. -*- lexical-binding: t; 
-*-
+;;; greader-audiobook.el --- Converts buffers into audio. -*- lexical-binding: 
t; -*-
 ;; 
 ;; Filename: greader-audiobook.el
-;; Description: 
-;; Author: Michelangelo Rodriguez
+;; Description: converts the current buffer into an audiobook using espeak.
+;; Author: Michelangelo Rodriguez <michelangelo.rodrig...@gmail.com>
 ;; Maintainer: Michelangelo Rodriguez
 ;; <michelangelo.rodrig...@gmail.com>
 
 ;; Created: Dom Mar 31 00:32:55 2024 (+0100)
-;; Version: 
 ;; Package-Requires: ()
-;; Last-Updated: 
-;;           By: 
-;;     Update #: 0
-;; URL: 
-;; Doc URL: 
-;; Keywords: 
-;; Compatibility: 
+;; URL: https://gitlab.com/michelangelo-rodriguez/greader
 ;; 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;; 
-;;; Commentary: 
-;; greader-audiobook.el offers an interface that greader's backends
-;; can use to achieve the conversion between text and sound.
+;;; Commentary:
+;; This module define just one command:
+;; `greader-audiobook-buffer'.  All the rest of the functionality is
+;; controlled by customizing the module through customizing the group
+;; `greader-audiobook' so:
+;; 'M-x customize-group <RET> greader-audiobook <RET>.
+;; Please see the documentation of each single customization item, and
+;; the documentation of `greader-audiobook-buffer'.
 
 ;; 
 ;; 
@@ -48,9 +46,293 @@
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;; 
 ;;; Code:
+(require 'subr-x)
+;; variable definitions
+(require 'greader-dict)
+(declare-function greader-dehyphenate nil)
+(declare-function greader-get-rate nil)
+(declare-function greader-get-language nil)
 
+(defgroup greader-audiobook
+  nil
+  "Greader audiobook configuration."
+  :group 'greader)
 
+(defcustom greader-audiobook-compress t
+  "When enabled, compress the directory created using zip."
+  :type '(boolean))
 
 
+(defcustom greader-audiobook-cancel-intermediate-wave-files nil
+  "Whether cancel or not intermediate wave files.
+This variable is used when variable `greader-audiobook-compress' is enabled."
+  :type 'boolean)
+
+(defcustom greader-audiobook-base-directory (concat
+                                            user-emacs-directory
+                                            "audiobooks/")
+  "Base directory in which store converted audiobooks."
+  :type 'string)
+
+(defcustom greader-audiobook-block-size "15"
+  "Specify the size of each block produced when converting the document.
+If you specify a string, it should contain a number to specify the
+size in minutes based on `greader-get-rate', so the calculus is
+approximate.
+If you specify a non-negative number, it will be treated as a size in
+characters.
+In any case the size or the time are approximate, because the
+block will end at an end of sentence.
+If the value is 0 or \"0\", an unique file will be generated.
+If current `major-mode' is in the variable `greader-audiobook-modes',
+this variable will be ignored to honor the mode specified in
+`greader-audiobook-modes'."
+  :type '(choice (natnum :tag "size in characters") (string :tag "size
+  in minutes")))
+
+(defcustom greader-audiobook-modes '((ereader-mode . ""))
+  "Different treatment of block based on the current major mode.
+Instead of numerical block size, use a string to determine the end of
+each block."
+  :type '(alist :key-type (symbol :tag "mode") :value-type (string)))
+
+(defcustom greader-audiobook-transcode-wave-files nil
+  "If enabled,  transcode original wave files using `ffmpeg'."
+  :type '(boolean))
+
+(defcustom greader-audiobook-transcode-format "mp3"
+  "Specify the format in which transcode original wave files.
+You should specify the format without the initial dot, so for example
+if you want to transcode original files in flac format, you should
+set this variable to \"flac\" \(not \".flac\"\)."
+  :type '(string :tag "format (without extension)"))
+
+(defcustom greader-audiobook-ffmpeg-extra-global-args nil
+  "List of strings containing extra output arguments to pass to ffmpeg."
+
+  :type '(repeat (string :tag "argument")))
+
+(defcustom greader-audiobook-ffmpeg-extra-output-args nil
+  "Extra output arguments to pass to ffmpeg."
+  :type '(repeat (string :tag "argument")))
+(defcustom greader-audiobook-zip-args nil
+  "Arguments to pass to the zip utility."
+  :type '(repeat (string :tag "argument")))
+
+(defcustom greader-audiobook-compress-remove-original nil
+  "When enabled, remove the original directory of the book converted.
+In this way, you will have only the zipped file containing the book."
+  :type 'boolean)
+;; functions
+(defun greader-audiobook--get-block ()
+  "Get a block of text in current buffer.
+This function uses `greader-audiobook-block-size' to determine the
+position of the end of the block.
+If the current major mode is in `greader-audiobook-modes', the
+associated string has priority over `greader-audiobook-block-size.
+Return a cons with start and end of the block or nil if at end of the buffer."
+
+  (save-excursion
+    (let ((start (point))
+         (end (point-max))
+         (words (count-words (point) (point-max))))
+      (if (assq major-mode greader-audiobook-modes)
+         (progn
+           (search-forward
+            (cdr (assq major-mode greader-audiobook-modes))
+            nil t 1)
+           (setq end (point)))
+       (pcase greader-audiobook-block-size
+         ((pred numberp)
+          (when
+              (< (+ (point) greader-audiobook-block-size) (point-max))
+            (cond
+             ((> greader-audiobook-block-size 0)
+              (goto-char (+ (point) greader-audiobook-block-size))
+              (when (thing-at-point 'sentence)
+                (forward-sentence))
+              (setq end (point))))))
+         ((pred stringp)
+          (cond
+           ((> (string-to-number greader-audiobook-block-size) 0)
+            (when (< (*
+                      (string-to-number greader-audiobook-block-size)
+                      (greader-get-rate))
+                     words)
+              (forward-word (* (string-to-number
+                                greader-audiobook-block-size)
+                               (greader-get-rate)))
+              (when (thing-at-point 'sentence)
+                (forward-sentence)))
+            (setq end (point)))))
+         (_
+          (error "Cannot determine the block size"))))
+      (if (> end start)
+         (cons start end)
+       nil))))
+
+(defun greader-audiobook-convert-block (filename)
+  "Convert a block of text in the current buffer, saving it in FILENAME.
+If variable `greader-dict-mode' or
+variable `greader-dict-toggle-filters' are enabled,
+substitutions will be performed on the block.
+After conversion, point will be moved to the end of the block.
+Return the generated file name, or nil if at end of the buffer."
+
+  (let*
+      ((command "espeak-ng")
+       (rate (concat "-s" (number-to-string (greader-get-rate))))
+       (language (concat "-v" (greader-get-language)))
+       (wave-file (concat "-w" filename))
+       (output nil)
+       (block (greader-audiobook--get-block))
+       (text (when block (buffer-substring (car block) (cdr block)))))
+    (if block
+       (progn
+         (setq text (greader-dehyphenate text))
+         (when (or greader-dict-mode greader-dict-toggle-filters)
+           (setq text (greader-dict-check-and-replace text)))
+         (setq output (call-process command nil nil nil rate language
+                                    wave-file text))
+         (when (= output 0)
+           (goto-char (cdr block)))
+         filename)
+      nil)))
+
+(defun greader-audiobook--count-blocks ()
+  "Return the number of total blocks that constitutes a buffer."
+  (save-excursion
+    (let ((blocks 0)
+         (block (greader-audiobook--get-block)))
+      (while block
+       (setq blocks (+ blocks 1))
+       (goto-char (cdr block))
+       (setq block (greader-audiobook--get-block)))
+      blocks)))
+
+
+(defun greader-audiobook-transcode-file (filename)
+  "Transcode FILENAME using ffmpeg.
+You have certain control of how this happens by configuring
+`greader-audiobook-ffmpeg-extra-global-args', and
+`greader-audiobook-ffmpeg-extra-output-args'."
+
+  (let
+      ((ffmpeg-args (append greader-audiobook-ffmpeg-extra-global-args
+                           (list "-i" filename)
+                           greader-audiobook-ffmpeg-extra-output-args
+                           (list (concat
+                                  (file-name-sans-extension filename)
+                                  "."
+                                  greader-audiobook-transcode-format))))
+       (result nil))
+    (setq result (apply 'call-process "ffmpeg" nil "*ffmpeg-output*"
+                       nil ffmpeg-args))
+    (unless (eq result 0)
+      (error "Error while transcoding, see buffer `*ffmpeg-output*'"))))
+
+(defun greader-audiobook--calculate-file-name (counter total-blocks)
+  "Calculate a file name based on the length of TOTAL-BLOCKS.
+COUNTER represents the current file name."
+
+  (let* ((counter-string (number-to-string counter))
+        (total-blocks-string (number-to-string total-blocks))
+        (filename nil)
+        (counter-chars 0))
+    (while (< counter-chars (- (length
+                                total-blocks-string)(length counter-string)))
+      (setq filename (concat filename "0"))
+      (setq counter-chars (+ counter-chars 1)))
+    (setq filename (concat filename counter-string ".wav"))))
+
+(defun greader-audiobook-compress (book-directory)
+  "Compress given BOOK-DIRECTORY."
+  (let ((zip-args (append (list "-rj")greader-audiobook-zip-args (list (concat
+                                                           
(string-remove-suffix
+                                                            "/"
+                                                            book-directory)
+                                                           ".zip"))
+                         (list book-directory)))
+       (result nil))
+    (setq result (apply 'call-process "zip" nil "*audiobook-zip*" nil
+                       zip-args))
+    (unless (eq result 0)
+      (error "Error while compressing, see buffer *audiobook-zip* for
+more information"))))
+
+;;;###autoload
+(defun greader-audiobook-buffer (&optional start-position)
+  "Convert current buffer to an audiobook starting at START-POSITION.
+With prefix, the conversion will start from the beginning of the
+buffer, otherwise it will start from point to the end.
+If region is active, only the region will be converted.
+This function will create a directory under
+`greader-audiobook-base-directory with the same name as the
+buffer without the extension, if any."
+
+  (interactive "P")
+  (message "Preparing for conversion (this could take some time...)")
+  (let ((end-position (point-max)))
+    (cond
+     ((not start-position)
+      (setq start-position (point)))
+     ((listp start-position)
+      (setq start-position (point-min)))
+     ((region-active-p)
+      (setq start-position (region-beginning))
+      (setq end-position (region-end))))
+    (save-excursion
+      (save-restriction
+       (narrow-to-region start-position end-position)
+       (goto-char start-position)
+       (unless (file-exists-p greader-audiobook-base-directory)
+         (make-directory greader-audiobook-base-directory))
+       (let* ((book-directory (concat (file-name-sans-extension
+                                       (buffer-name))
+                                      "/"))
+              (default-directory (concat
+                                  greader-audiobook-base-directory
+                                  book-directory))
+              (output-file-name nil)
+              (output-file-counter 1)
+              (total-blocks (greader-audiobook--count-blocks)))
+         (unless (file-exists-p default-directory)
+           (make-directory default-directory))
+         (message "Starting conversion of %s ."
+                  book-directory)
+         (while (greader-audiobook--get-block)
+           (setq output-file-name
+                 (greader-audiobook--calculate-file-name
+                  output-file-counter total-blocks))
+           (message "converting block %d of %d"
+                    output-file-counter total-blocks)
+           (setq output-file-name
+                 (greader-audiobook-convert-block output-file-name))
+           (if output-file-name
+               (progn
+                 (when greader-audiobook-transcode-wave-files
+                   (message "Transcoding block to %s..."
+                            greader-audiobook-transcode-format)
+                   (greader-audiobook-transcode-file
+                    output-file-name)
+                   (when
+                       greader-audiobook-cancel-intermediate-wave-files
+                     (delete-file output-file-name)))
+                 (setq output-file-counter (+ output-file-counter 1)))
+             (error "An error has occurred while converting")))
+         (when greader-audiobook-compress
+           (setq default-directory greader-audiobook-base-directory)
+           (message "compressing %s..." book-directory)
+           (greader-audiobook-compress book-directory)
+           (when greader-audiobook-compress-remove-original
+             (delete-directory book-directory t t)
+             (setq book-directory (concat (string-remove-suffix "/"
+                                                                book-directory)
+                                          ".zip"))))
+         (message "conversion terminated and saved in %s"
+                  (concat greader-audiobook-base-directory
+                          book-directory)))))))
+
+(provide 'greader-audiobook)
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;; greader-audiobook.el ends here
diff --git a/greader.el b/greader.el
index 0be16e911a..77cb51dfac 100644
--- a/greader.el
+++ b/greader.el
@@ -6,7 +6,7 @@
 ;; Author: Michelangelo Rodriguez <michelangelo.rodrig...@gmail.com>
 ;; Keywords: tools, accessibility
 ;; URL: https://www.gitlab.com/michelangelo-rodriguez/greader
-;; Version: 0.9.24
+;; Version: 0.10.0
 
 ;; This program is free software; you can redistribute it and/or modify
 ;; it under the terms of the GNU General Public License as published by

Reply via email to