├── .gitignore ├── LICENSE ├── README.md ├── RoboFile.php ├── account.sample.php └── robo.phar /.gitignore: -------------------------------------------------------------------------------- 1 | account.php 2 | *.mp3 3 | *.json 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Easy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LazyAudioBook 2 | 将 txt 文件生成语音书。 3 | 4 | ## 使用指南 5 | 6 | - 需要本地php7环境(Mac电脑自带) 7 | - 到 http://yuyin.baidu.com/tts 创建应用,并取得 appkey 和 appsecret 8 | - 下载本项目,解压后在目录根下运行:php robo.phar convert,按提示输入信息即可 9 | 10 | ## 注意 11 | 12 | - 为避免每次输入 appkey 和 appsecret , 可将其写入 account.sample.php 并改名为 account.php 13 | -------------------------------------------------------------------------------- /RoboFile.php: -------------------------------------------------------------------------------- 1 | ask("请输入百度语音合成服务的appkey"); 21 | $skey = $this->ask("请输入百度语音合成服务的appsecret"); 22 | } 23 | else 24 | { 25 | $akey = $GLOBALS['baidu_akey']; 26 | $skey = $GLOBALS['baidu_skey']; 27 | } 28 | 29 | $i = 0; 30 | // 每次发送 400 个字符 31 | $max = 400; 32 | $len = 0; 33 | $ship = []; 34 | $audio_count = 1; 35 | 36 | 37 | 38 | $last_file = 'last.json'; 39 | if( file_exists( $last_file ) ) 40 | { 41 | $info = json_decode( file_get_contents( $last_file ) , 1 ); 42 | $this->save = $info['save']; 43 | $content_lines = $info['content_lines']; 44 | if( isset( $info['voice_type'] ) ) 45 | $this->voice_type = $info['voice_type']; 46 | } 47 | else 48 | { 49 | 50 | 51 | if( $from === null ) 52 | { 53 | // 获取txt文件路径 54 | $path = $this->askDefault("请输入要转换的txt文件(仅支持UTF8格式)","/Users/Easy/Desktop/money.txt"); 55 | 56 | }else 57 | { 58 | $path = $from; 59 | } 60 | 61 | if( !file_exists( $path ) ) 62 | { 63 | $this->say("该文件不存在"); 64 | return false; 65 | } 66 | 67 | 68 | if( $to === null ) 69 | $this->save = $this->askDefault("请输入生成mp3文件的地址",'out.mp3'); 70 | else 71 | $this->save = $to; 72 | 73 | 74 | if( $type === null ) 75 | $this->voice_type = $this->askDefault("请输入生成语音的风格,3-情感男声;4-情感女生",'3'); 76 | else 77 | $this->voice_type = $type; 78 | 79 | $content_lines = file( $path ); 80 | 81 | $EC = mb_detect_encoding( join( "\r\n" , $content_lines ) , "UTF8, GB2312, GBK , CP936"); 82 | 83 | if( $EC != "UTF8" ) 84 | { 85 | foreach( $content_lines as $key => $value ) 86 | { 87 | $content_lines[$key] = mb_convert_encoding( $value , "UTF8" , $EC ); 88 | } 89 | } 90 | 91 | $new_lines = []; 92 | foreach( $content_lines as $key => $value ) 93 | { 94 | // 如果单行文字超过了最大长度 95 | if( mb_strlen( $value , 'UTF8' ) > $max ) 96 | { 97 | // 分拆成几句 98 | $subs = mb_str_split( $value , $max , 'UTF8' ); 99 | foreach( $subs as $item ) 100 | { 101 | array_push( $new_lines , $item ); 102 | } 103 | } 104 | else 105 | { 106 | $new_lines[] = $value; 107 | } 108 | //$content_lines[$key] = mb_convert_encoding( $value , "UTF8" , $EC ); 109 | } 110 | 111 | $content_lines = $new_lines; 112 | } 113 | 114 | // 读取全部文件内容 115 | // 直接按行分割 116 | 117 | 118 | $this->say("读取文件成功,共".count($content_lines)."行"); 119 | 120 | 121 | // show( $content_lines , 100 ); 122 | 123 | while( $len <= $max && count( $content_lines ) > 0 ) 124 | { 125 | $snap_lines = $content_lines; 126 | // 从最上边取出 127 | $now_line = array_shift( $content_lines ); 128 | 129 | $do_convert = false; 130 | 131 | if( $len + mb_strlen( $now_line , 'UTF8' ) > $max ) 132 | { 133 | array_unshift( $content_lines , $now_line ); 134 | 135 | // 调用音频转换函数 136 | $do_convert = true; 137 | 138 | 139 | } 140 | else 141 | { 142 | array_push( $ship , $now_line ); 143 | $len += mb_strlen( $now_line , 'UTF8' ); 144 | 145 | if( count( $content_lines ) == 0 ) $do_convert = true; 146 | 147 | } 148 | 149 | if( $do_convert ) 150 | { 151 | if( $this->txt_to_audio( $audio_count++, $akey , $skey , $ship ) ) 152 | { 153 | if( count( $content_lines ) === 0 ) 154 | { 155 | $this->say("转化完成 🥇"); 156 | @unlink( 'last.json' ); 157 | exit; 158 | } 159 | 160 | $this->say("待转化段数". count( $content_lines ) ); 161 | 162 | // 保存当前工作数据和目标文件 163 | $last = []; 164 | $last['save'] = $this->save; 165 | $last['voice_type'] = $this->voice_type; 166 | $last['content_lines'] = $content_lines; 167 | 168 | 169 | file_put_contents( 'last.json' , json_encode( $last , JSON_UNESCAPED_UNICODE ) ); 170 | 171 | 172 | 173 | // 清空 174 | $ship = []; 175 | $len = 0; 176 | } 177 | else 178 | { 179 | // 回滚 180 | 181 | // // 保存当前工作数据和目标文件 182 | // $last = []; 183 | // $last['save'] = $this->save; 184 | // $last['content_lines'] = $snap_lines; 185 | 186 | // file_put_contents( 'last.json' , json_encode( $last , JSON_UNESCAPED_UNICODE ) ); 187 | 188 | $this->say("音频转换失败,程序中止"); 189 | break; 190 | } 191 | 192 | 193 | } 194 | 195 | $i++; 196 | // if( $i > 500 ) break; 197 | 198 | } 199 | 200 | if( $len >= $max ) 201 | { 202 | $this->say("len = $len , max = $max ,转化结束"); 203 | } 204 | 205 | if( count( $content_lines ) <= 0 ) 206 | { 207 | $this->say( count( $content_lines ) . "<--待处理行数为零 ,转化结束"); 208 | } 209 | 210 | 211 | } 212 | 213 | private function txt_to_audio( $count , $akey , $skey , $data_array ) 214 | { 215 | // 如果没有token 216 | if( !isset( $GLOBALS['token'] ) ) 217 | { 218 | $this->say( "Token 不存在,换取 token" ); 219 | $ret = file_get_contents( "https://openapi.baidu.com/oauth/2.0/token?grant_type=client_credentials&client_id=" . urlencode( $akey ) . "&client_secret=" . urlencode( $skey ) ); 220 | 221 | // print_r( json_decode( $ret , 1 ) ); 222 | 223 | $bad_token = true; 224 | 225 | if( $ret ) 226 | { 227 | if( $info = json_decode( $ret , 1 ) ) 228 | { 229 | if( isset( $info['access_token'] ) ) 230 | { 231 | $this->say("换取Token成功"); 232 | $GLOBALS['token'] = $info['access_token']; 233 | $bad_token = false; 234 | } 235 | } 236 | } 237 | 238 | if( $bad_token ) 239 | { 240 | $this->say("换取Token失败"); 241 | return false; 242 | } 243 | } 244 | $text = join( "\r\n" , $data_array ); 245 | 246 | $this->say("转换..." . mb_substr( trim( $text ) , 0 , 30 , 'UTF8' )); 247 | if( mb_strlen( trim( $text ) , 'UTF8' ) < 1 ) 248 | { 249 | $this->say("文字空白,跳过"); 250 | return true; 251 | } 252 | // 获取音频下载地址: 253 | $re_try = 0; 254 | 255 | get_audio: 256 | 257 | $audio = file_get_contents( 'http://tsn.baidu.com/text2audio?lan=zh&ctp=1&cuid=LOCALMAC1022&tok=' . urlencode( $GLOBALS['token'] ) . '&tex=' . urlencode( urlencode( $text ) ) . '&vol=9&per=' . intval( $this->voice_type ) . '&spd=5&pit=5'); 258 | 259 | $headers = parseHeaders( $http_response_header ); 260 | if( $headers['Content-Type'] == 'audio/mp3' ) 261 | { 262 | file_put_contents( $this->save , $audio , FILE_APPEND ); 263 | $this->say( "此部分已追加写入音频文件 🤠 \r\n" ); 264 | return true; 265 | } 266 | else 267 | { 268 | // $this->say( "音频转码失败,转换中止" ); 269 | $re_try++; 270 | print_r( $audio ); 271 | 272 | if( $re_try < 2 ) goto get_audio; 273 | else return false; 274 | } 275 | 276 | 277 | 278 | } 279 | 280 | 281 | } 282 | 283 | function parseHeaders( $headers ) 284 | { 285 | $head = array(); 286 | foreach( $headers as $k=>$v ) 287 | { 288 | $t = explode( ':', $v, 2 ); 289 | if( isset( $t[1] ) ) 290 | $head[ trim($t[0]) ] = trim( $t[1] ); 291 | else 292 | { 293 | $head[] = $v; 294 | if( preg_match( "#HTTP/[0-9\.]+\s+([0-9]+)#",$v, $out ) ) 295 | $head['reponse_code'] = intval($out[1]); 296 | } 297 | } 298 | return $head; 299 | } 300 | 301 | function mb_str_split($string,$string_length=1,$charset='utf-8') 302 | { 303 | if(mb_strlen($string,$charset)>$string_length || !$string_length) 304 | { 305 | do { 306 | $c = mb_strlen($string,$charset); 307 | $parts[] = mb_substr($string,0,$string_length,$charset); 308 | $string = mb_substr($string,$string_length,$c-$string_length,$charset); 309 | }while(!empty($string)); 310 | } else { 311 | $parts = array($string); 312 | } 313 | return $parts; 314 | } 315 | 316 | function show( $array , $len ) 317 | { 318 | for( $i = 0 ; $i<$len ; $i++ ) 319 | { 320 | echo "[$i]>>>>".$array[$i]."<<<<<\r\n"; 321 | } 322 | } -------------------------------------------------------------------------------- /account.sample.php: -------------------------------------------------------------------------------- 1 |